diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..2d6d258f4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.{kt,kts}] +ktlint_code_style = intellij_idea +ktlint_standard_no-wildcard-imports = disabled \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 788a30544..da09df8e0 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -6,11 +6,12 @@ body: - type: markdown attributes: value: | - # ReVanced Extended Patches bug report + # ReVanced Extended bug report Before creating a new bug report, please keep the following in mind: - - **Do not submit a duplicate bug report**: You can review existing bug reports [here](https://github.com/YT-Advanced/ReX-patches/labels/Bug%20report). + - If your bug is not related to [unique features](https://github.com/anddea/revanced-patches/wiki/Unique-features), open your bug report in [ReVanced](https://github.com/ReVanced/revanced-patches/issues) and [RVX](https://github.com/inotia00/ReVanced_Extended/issues) first. + - **Do not submit a duplicate bug report**: You can review existing bug reports [here](https://github.com/anddea/revanced-patches/labels/Bug%20report). - **Check if this issue reproduces in unpatched apps as well**: Most bugs can also be reproduced in unpatched apps. - type: dropdown attributes: @@ -36,7 +37,7 @@ body: attributes: label: Application description: Write down the application and version where the issue occurs. - placeholder: e.g. YouTube v19.02.39 + placeholder: e.g. YouTube v19.16.39 validations: required: true - type: textarea @@ -48,7 +49,7 @@ body: - Add images and videos if possible - List used patches if applicable validations: - required: true + required: true - type: textarea attributes: label: Error logs @@ -73,10 +74,14 @@ body: label: Acknowledgements description: Your bug report will be closed if you don't follow the checklist below. options: - - label: This issue does not reproduce on unpatched YouTube or YT Music. + - label: This issue does not reproduce on unpatched apps. required: true - label: This issue is not a duplicate of an existing bug report. required: true + - label: I did not use any settings marked as `Experimental Flags`. + required: true + - label: I have patched the APK according to the [documentation](https://github.com/inotia00/revanced-documentation#readme). + required: true - label: I have chosen an appropriate title. required: true - label: All requested information has been provided properly. diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 548db4c8e..ac8a99163 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -6,11 +6,11 @@ body: - type: markdown attributes: value: | - # ReVanced Extended Patches feature request + # ReVanced Extended feature request Before creating a new feature request, please keep the following in mind: - - **Do not submit a duplicate feature request**: You can review existing feature requests [here](https://github.com/YT-Advanced/ReX-patches/labels/Feature%20request). + - **Do not submit a duplicate feature request**: You can review existing feature requests [here](https://github.com/anddea/revanced-patches/labels/Feature%20request). - type: dropdown attributes: label: Application @@ -30,7 +30,7 @@ body: - type: textarea attributes: label: Motivation - description: | + description: | A strong motivation is necessary for a feature request to be considered. - Why should this feature be implemented? diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml deleted file mode 100644 index 1a7c4fa42..000000000 --- a/.github/ISSUE_TEMPLATE/question.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: ❓ Question -description: Leave your questions about the patches or other things. -title: 'question: ' -labels: ['Question'] -body: - - type: markdown - attributes: - value: | - # ReX question - - Before creating a new question, please keep the following in mind: - - - **Do not submit a duplicate question**: You can review existing questions [here](https://github.com/YT-Advanced/ReX-patches/labels/Question). - - type: dropdown - attributes: - label: Application - options: - - YouTube - - YouTube Music - - Other - validations: - required: true - - type: textarea - attributes: - label: Question description - description: | - - Describe your question in detail - - Add images and videos if possible - - type: checkboxes - id: acknowledgements - attributes: - label: Acknowledgements - description: Your issue will be closed if you haven't done these steps. - options: - - label: This issue is not a duplicate of an existing question. - required: true - - label: I have chosen an appropriate title. - required: true - - label: All requested information has been provided properly. - required: true - - label: I have written the title and contents in English. - required: true diff --git a/.github/ISSUE_TEMPLATE/suggestion.yml b/.github/ISSUE_TEMPLATE/suggestion.yml deleted file mode 100644 index c15d8aef1..000000000 --- a/.github/ISSUE_TEMPLATE/suggestion.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: 📜 Suggestion -description: Leave any other suggestions about the patches or other things. -title: 'suggestion: ' -labels: ['Suggestion'] -body: - - type: markdown - attributes: - value: | - # ReX suggestion - - Before creating a new suggestion, please keep the following in mind: - - - **Do not submit a duplicate suggestion**: You can review existing suggestions [here](https://github.com/YT-Advanced/ReX-patches/labels/Suggestion). - - **Do not write feature requests**: This is a suggestion, not a feature request. - - type: dropdown - attributes: - label: Application - options: - - YouTube - - YouTube Music - - Other - validations: - required: true - - type: textarea - attributes: - label: Suggestion description - description: | - - Describe your suggestion in detail - - Add images and videos if possible - - type: checkboxes - id: acknowledgements - attributes: - label: Acknowledgements - description: Your issue will be closed if you haven't done these steps. - options: - - label: This issue is not a duplicate of an existing suggestion. - required: true - - label: This is not a feature request. - required: true - - label: I have chosen an appropriate title. - required: true - - label: All requested information has been provided properly. - required: true - - label: I have written the title and contents in English. - required: true diff --git a/.github/config.yml b/.github/config.yml index 09ed019c1..075f56b53 100644 --- a/.github/config.yml +++ b/.github/config.yml @@ -1,2 +1,2 @@ firstPRMergeComment: > - Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) if you want to receive a contributor role. + Thank you for contributing to ReVanced. Join us on [Discord](https://revanced.app/discord) to receive a role for your contribution. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..93e7caf35 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,22 @@ +version: 2 +updates: + - package-ecosystem: github-actions + labels: [] + directory: / + target-branch: dev + schedule: + interval: monthly + + - package-ecosystem: npm + labels: [] + directory: / + target-branch: dev + schedule: + interval: monthly + + - package-ecosystem: gradle + labels: [] + directory: / + target-branch: dev + schedule: + interval: monthly diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 250871bcc..193a26af0 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -16,6 +16,12 @@ jobs: with: fetch-depth: 0 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + - name: Cache Gradle uses: burrunan/gradle-cache-action@v1 diff --git a/.github/workflows/open_pull_request.yml b/.github/workflows/open_pull_request.yml index 46021f4a8..721ab088d 100644 --- a/.github/workflows/open_pull_request.yml +++ b/.github/workflows/open_pull_request.yml @@ -20,8 +20,9 @@ jobs: - name: Open pull request uses: repo-sync/pull-request@v2 with: - destination_branch: 'main' + destination_branch: main pr_title: 'chore: ${{ env.MESSAGE }}' pr_body: | This pull request will ${{ env.MESSAGE }}. + pr_draft: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4d42ee549..9aa3a001b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,6 +10,11 @@ on: jobs: release: name: Release + permissions: + contents: write + issues: write + pull-requests: write + packages: write runs-on: ubuntu-latest steps: - name: Checkout @@ -20,13 +25,20 @@ jobs: persist-credentials: false fetch-depth: 0 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + - name: Cache Gradle uses: burrunan/gradle-cache-action@v1 - name: Build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gradlew generateMeta generatePatchesFiles clean + # To update `README.md` and `patches.json`, the command `./gradlew generatePatchesFiles clean` should be used instead of the command `./gradlew build clean` + run: ./gradlew generatePatchesFiles clean - name: Setup Node.js uses: actions/setup-node@v4 @@ -46,5 +58,5 @@ jobs: - name: Release env: - GITHUB_TOKEN: ${{ secrets.REPOSITORY_PUSH_ACCESS }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npm exec semantic-release diff --git a/.gitignore b/.gitignore index 9f2e41679..a30497ffc 100644 --- a/.gitignore +++ b/.gitignore @@ -122,9 +122,12 @@ gradle-app.setting # Dependency directories node_modules/ -# gradle properties, due to Github token +# Gradle properties, due to Github token ./gradle.properties +# One package is called the same as the Gradle build folder +!**/src/**/build/ + .DS_Store local.properties __pycache__ diff --git a/.idea/misc.xml b/.idea/misc.xml index e7f3afad1..bbdaad2de 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,8 +1,7 @@ - - + \ No newline at end of file diff --git a/.releaserc b/.releaserc index 6193511b8..0abaf5291 100644 --- a/.releaserc +++ b/.releaserc @@ -24,8 +24,9 @@ "README.md", "CHANGELOG.md", "gradle.properties", - "patches.json" - ] + "patches.json", + ], + "message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" } ], [ @@ -33,11 +34,11 @@ { "assets": [ { - "path": "build/libs/revanced-patches*" + "path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)" }, { "path": "patches.json" - } + }, ], successComment: false } diff --git a/CHANGELOG.md b/CHANGELOG.md index b78e26954..b50ea264e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,159 @@ +# [3.0.0-dev.8](https://github.com/anddea/revanced-patches/compare/v3.0.0-dev.7...v3.0.0-dev.8) (2024-12-23) + + +### Features + +* **YouTube - Navigation bar components:** Add `Enable translucent status bar` setting (for YouTube 19.25+) ([2cf269b](https://github.com/anddea/revanced-patches/commit/2cf269bcda307b6546d7ab82d5fcd33bc8c0b329)) + +# [3.0.0-dev.7](https://github.com/anddea/revanced-patches/compare/v3.0.0-dev.6...v3.0.0-dev.7) (2024-12-23) + + +### Bug Fixes + +* **Reddit - Hide ads:** `Hide ads` patch fails on `2024.17.0` ([8426f74](https://github.com/anddea/revanced-patches/commit/8426f7471e1a54963cb56131f3e1362068375d1b)) +* **YouTube - Shorts components:** Patch failing during building with certain patch selection ([a442ff3](https://github.com/anddea/revanced-patches/commit/a442ff38ca1a4ee9372fb4bf1bbdc8f8c67a1c20)) +* **YouTube - Toolbar components:** `Hide voice search button` setting does not work ([cb6868a](https://github.com/anddea/revanced-patches/commit/cb6868a8eb46b75f4741ea58adb684aa9c37b4d3)) +* **YouTube Music:** App crashes when including `Hide action bar components` patch ([e7646e2](https://github.com/anddea/revanced-patches/commit/e7646e2410e6e622207ddf8b89011d3507f1ea30)) + + +### Features + +* **YouTube - Navigation bar components:** Bring back`Enable translucent navigation bar` setting ([158f8da](https://github.com/anddea/revanced-patches/commit/158f8da6c7432c9df07a3b91d7c25f87ec14aaef)) +* **YouTube - Seekbar components:** Bring back `Enable Cairo seekbar` setting (for YouTube 19.23.40-19.32.36) ([fed8915](https://github.com/anddea/revanced-patches/commit/fed89158dd9dd63552362c580d4eebfd57715d68)) +* **YouTube Music - Spoof client:** Limit support version to 7.16.53 and change default client preset ([20beb64](https://github.com/anddea/revanced-patches/commit/20beb648ae675e313ff099da4dbdf6d6b91e9285)) + +# [3.0.0-dev.6](https://github.com/anddea/revanced-patches/compare/v3.0.0-dev.5...v3.0.0-dev.6) (2024-12-21) + + +### Bug Fixes + +* **YouTube - Custom branding icon:** Patch option `restoreOldSplashAnimation` not working in YouTube 19.32.39+ ([b81d32e](https://github.com/anddea/revanced-patches/commit/b81d32eea4ca5ba26c6294d9467f6823d2088ca0)) +* **YouTube - Hide feed components:** `Hide carousel shelf` hiding in library in certain situations ([10e4667](https://github.com/anddea/revanced-patches/commit/10e466737ac09f4ab328d520c211a9b1a8e413ab)) +* **YouTube - Miniplayer:** Use estimated maximum on screen size for devices with low density screens ([efeb5fb](https://github.com/anddea/revanced-patches/commit/efeb5fba02d660bfd3f19b59fc1ef21ba11ea062)) +* **YouTube - Theme:** Splash background color not applied in latest YouTube client ([a8c4462](https://github.com/anddea/revanced-patches/commit/a8c446222b9a75e0c74af78dfe029334d8f3dfa0)) +* **YouTube - Video playback:** Applying default video quality to Shorts causes the beginning of the shorts to get stuck in a loop ([9c4c56e](https://github.com/anddea/revanced-patches/commit/9c4c56eefa65b1c3f61e23cf78375272b33e679f)) +* **YouTube Music - Hide action bar components:** `Hide Download button` setting not working in YouTube Music 7.25.52 ([85dfb09](https://github.com/anddea/revanced-patches/commit/85dfb09521cff167e9029b25d48c9d84a33ed57b)) +* **YouTube Music - SponsorBlock:** `Change segment behavior` and `About` sections are hidden in the settings ([176adb8](https://github.com/anddea/revanced-patches/commit/176adb89102f96c22e8a437d019c688df933e2dd)) +* **YouTube:** Splash screen background color does not change in dark mode if `Theme` patch is excluded ([28df1b4](https://github.com/anddea/revanced-patches/commit/28df1b4172262719fcc86b038e4a617bce0dc6e9)) +* **YouTube:** When clicking on timestamps in comments, playback speed sometimes changes to 1.0x (unpatched YouTube bug) ([93c4bf8](https://github.com/anddea/revanced-patches/commit/93c4bf8fb63fe7f06f3908bc6e755c568f86667f)) + + +### Features + +* **YouTube - Navigation bar components:** Add `Disable translucent status bar` setting ([fe09dbc](https://github.com/anddea/revanced-patches/commit/fe09dbcece9324f224950cd784caabf2db64bf6c)) +* **YouTube - Navigation bar components:** Separate `Enable translucent navigation bar` setting into `Disable light translucent bar` and `Disable dark translucent bar` settings ([602de6e](https://github.com/anddea/revanced-patches/commit/602de6e9692c1af4ab892726305ce2e6d4a04f08)) +* **YouTube - Shorts components:** Add `Restore old player layout` setting (YouTube 18.29.38 ~ 19.16.39) ([bf8afdd](https://github.com/anddea/revanced-patches/commit/bf8afddae275b9ef7568fb68398749d2b3f47941)) +* **YouTube - Shorts components:** Add styles to custom actions dialog ([e95b064](https://github.com/anddea/revanced-patches/commit/e95b0643131bfe20ded2747734c34c603838812c)) +* **YouTube - Swipe controls:** Change the setting name `Enable watch panel gestures` to `Disable watch panel gestures`, and change the setting name `Enable swipe to change video` to `Disable swipe to change video` ([375cf3a](https://github.com/anddea/revanced-patches/commit/375cf3ad331c3823ee17cbea450f8e87510e58b0)) +* **YouTube Music - Hide action bar components:** Limit the available versions of the `Override Download action button` setting to 7.16.53 ([16ead35](https://github.com/anddea/revanced-patches/commit/16ead35768678e7ac4a3327635a2f6f3ecf6bce2)) +* **YouTube Music - Spoof client:** Add `Use old client` and `Default client` settings ([bb3bd2a](https://github.com/anddea/revanced-patches/commit/bb3bd2a9d799c0177ad39c8d59b0884c510b5864)) +* **YouTube:** Support version `19.44.39` ([22419fd](https://github.com/anddea/revanced-patches/commit/22419fd9d53bd2234283423a8e9fec30b1fff3fc)) + +# [3.0.0-dev.5](https://github.com/anddea/revanced-patches/compare/v3.0.0-dev.4...v3.0.0-dev.5) (2024-12-17) + + +### Bug Fixes + +* **YouTube - Enable gradient loading screen:** `Enable gradient loading screen` not working on YouTube 19.34.42+ ([d6b6a42](https://github.com/anddea/revanced-patches/commit/d6b6a427e31e3324163679735b7dceac0234460a)) +* **YouTube - Hide ads:** Hide new type of featured promotions ([c1dadc0](https://github.com/anddea/revanced-patches/commit/c1dadc0e2b45149974d8a50c2d7de0e05e1537a2)) +* **YouTube - Hide feed components:** `Hide carousel shelf` hiding in library in certain situations ([6323421](https://github.com/anddea/revanced-patches/commit/6323421d7c3a99c278258f19ca36a489b6345875)) +* **YouTube - Hide feed components:** `Hide carousel shelf` not hiding in home feed in certain situations ([0c690b1](https://github.com/anddea/revanced-patches/commit/0c690b1ee732764588ef600f37097b662b11e902)) +* **YouTube - Hide feed components:** New kind of community posts are not hidden ([a58ed6b](https://github.com/anddea/revanced-patches/commit/a58ed6b19727940f28b8e1d1247dcd742942495c)) +* **YouTube - Hide player flyout menu:** `Sleep timer menu` always hidden in YouTube 19.34.42 ([fa42f5f](https://github.com/anddea/revanced-patches/commit/fa42f5f820772fddba720f9895a21b7f21c2182f)) +* **YouTube - MaterialYou:** Theme not applied to notification dots in YouTube 19.34.42+ ([fd31d87](https://github.com/anddea/revanced-patches/commit/fd31d87ba5925305882d761f3455c05007988ddf)) +* **YouTube - Player components:** `Hide seek message` not working on YouTube 19.34.42 ([8cb3b4b](https://github.com/anddea/revanced-patches/commit/8cb3b4b91ae9531824a124ae897b1d9729f8f340)) +* **YouTube - Seekbar components:** `Custom seekbar color` not applied to gradient seekbar in YouTube 19.34.42 ([b3ac64c](https://github.com/anddea/revanced-patches/commit/b3ac64c05a44d8b7d035de94f2d9cec26d7514f3)) +* **YouTube - Shorts components:** `Hide Shorts shelves` not hiding in home feed in certain situations ([3481e01](https://github.com/anddea/revanced-patches/commit/3481e01a7c224d262ec7b23e1f132787dc3f838e)) +* **YouTube - Spoof streaming data:** On `iOS` clients, livestreams always start from the beginning ([4e60bf5](https://github.com/anddea/revanced-patches/commit/4e60bf514bd5480e35bb102d2f4758f766b311a8)) +* **YouTube - Spoof streaming data:** Videos end 1 second early on iOS client ([b2cc033](https://github.com/anddea/revanced-patches/commit/b2cc03320e934532425d852ac5832dffdfefb98c)) +* **YouTube - VideoInformation:** Channel name not fetched in YouTube 19.34.42 ([2e19453](https://github.com/anddea/revanced-patches/commit/2e194533eb9c7169fdbd6ec321422dca19d54a34)) +* **YouTube & YouTube Music - Custom branding icon:** Patching fails in some environments when the path entered in the patch options contains uppercase letters ([786bc36](https://github.com/anddea/revanced-patches/commit/786bc36e2a99ae9b2c15c7659c3ac1f4c9ee26f6)) +* **YouTube Music - Spoof client:** Action bar not loading as of YouTube Music 7.17.51 ([943c288](https://github.com/anddea/revanced-patches/commit/943c28866ab7af53fb986b648c87f8baf0d67ff9)) + + +### Features + +* **YouTube - Custom branding icon:** Add `YouTube Black` icon ([e706c5f](https://github.com/anddea/revanced-patches/commit/e706c5fc67c2318505ea1e5588437bba0040c85a)) +* **YouTube - Custom branding icon:** Restrict the version that can use the patch option `Restore old splash animation` to 19.16.39 (deprecated) ([8589c5a](https://github.com/anddea/revanced-patches/commit/8589c5afc4b84ef680f56dee7b93ad3e93df9985)) +* **YouTube - Navigation bar components:** Add missing resource for Cairo notification icon (YouTube 19.34.42+) ([2982725](https://github.com/anddea/revanced-patches/commit/2982725d8a9fdd81280d9ba79425d0bd05b3431d)) +* **YouTube - Player components:** Add `Hide Chat summary in live chat` setting ([963dbe8](https://github.com/anddea/revanced-patches/commit/963dbe89970b64ff99d277bfada6c3dfb403f4bb)) +* **YouTube - Remove background playback restrictions:** Add PiP mode support in Shorts ([4fc44b2](https://github.com/anddea/revanced-patches/commit/4fc44b2ba56c78aaa87b28396a0ef72e0e9fe3f9)) +* **YouTube - Seekbar components:** Change default seekbar color to match new branding ([26d8ba6](https://github.com/anddea/revanced-patches/commit/26d8ba6c2bc2a81725bbf61a7bf3a4090175e156)) +* **YouTube - Seekbar components:** Remove `Enable Cairo seekbar` setting, which is no longer needed (Enabled by default in YouTube 19.34.42) ([c12f4ae](https://github.com/anddea/revanced-patches/commit/c12f4aeddff61a1c2ac00c26c59777bd76e91f69)) +* **YouTube - Shorts components:** Add `Change Shorts background repeat state` setting (YouTube 19.34.42+) ([84d6ccc](https://github.com/anddea/revanced-patches/commit/84d6ccc7cf738650bbfbf15b42fc5b1be02f3d98)) +* **YouTube - Shorts components:** Add `Custom actions in toolbar` setting (YouTube 18.38.44+) ([6732b2b](https://github.com/anddea/revanced-patches/commit/6732b2b373b6e4208c2966f4ea262ebe49886f92)) +* **YouTube - Shorts components:** Add `Custom actions` setting (YouTube 19.05.36+) ([ff5b527](https://github.com/anddea/revanced-patches/commit/ff5b5279d4c21eabeedaf2d6a1e8ae25871ae042)) +* **YouTube - Spoof app version:** Add target version `19.26.42 - Disable Cairo icon in navigation and toolbar` and `19.33.37 - Restore old playback speed flyout panel` ([671e809](https://github.com/anddea/revanced-patches/commit/671e8098c516b05af82eb1609e6740e49d79838c)) +* **YouTube - Spoof streaming data:** Remove `Skip iOS livestream playback` setting (no longer needed) ([b5e507c](https://github.com/anddea/revanced-patches/commit/b5e507c7a3718843469f7e366f96a53d279c28a0)) +* **YouTube & YouTube Music - Settings:** Add `RVX settings summaries` to patch options ([6211b44](https://github.com/anddea/revanced-patches/commit/6211b448c9979218fe566c8c00e69c09ebe37790)) +* **YouTube Music - Custom branding icon:** Delete old `Revancify Yellow` icon ([#893](https://github.com/anddea/revanced-patches/issues/893)) ([0c09f4d](https://github.com/anddea/revanced-patches/commit/0c09f4df563c57f9645506f04fb8ca8f1a399334)) +* **YouTube Music - Hide player flyout menu:** Add `Hide Speed dial menu` setting ([42b6bd5](https://github.com/anddea/revanced-patches/commit/42b6bd5e994c3fc22457bf79bfbc55cd15de5734)) +* **YouTube Music:** Add `Disable DRC audio` patch ([a3b458d](https://github.com/anddea/revanced-patches/commit/a3b458d50f769cd35d7e7b5ff9144aec0f8dc199)) +* **YouTube Music:** Add `Spoof streaming data` patch ([ef14e5a](https://github.com/anddea/revanced-patches/commit/ef14e5acc93e9a9ad5d9eef861023f1d5623e4ff)) +* **YouTube Music:** Support version `7.25.52` ([d8fac8b](https://github.com/anddea/revanced-patches/commit/d8fac8b82c5983efd8096f533e175befe4ba396a)) +* **YouTube:** Support version `19.38.41` ([756e02f](https://github.com/anddea/revanced-patches/commit/756e02f91a642936f5ab502dcfc33d05eff6eabd)) + +# [3.0.0-dev.4](https://github.com/anddea/revanced-patches/compare/v3.0.0-dev.3...v3.0.0-dev.4) (2024-12-12) + + +### Bug Fixes + +* **YouTube Music - Visual preferences icons:** Custom branding icons did not work ([45fa7fd](https://github.com/anddea/revanced-patches/commit/45fa7fd9169218bd0cee46b8413aee7611212b0b)) + +# [3.0.0-dev.3](https://github.com/anddea/revanced-patches/compare/v3.0.0-dev.2...v3.0.0-dev.3) (2024-12-12) + + +### Bug Fixes + +* **YouTube - Overlay buttons:** Play all button did not work for all videos when using all content by time ascending ([e4e51f5](https://github.com/anddea/revanced-patches/commit/e4e51f583ebbf2986a1077860503e3e94c3a3f05)) +* **YouTube - Seekbr components:** Reverse start and end colors for Cairo seekbar ([e9bd106](https://github.com/anddea/revanced-patches/commit/e9bd106114c1669426d10830b544d7936a0728a1)) + +# [3.0.0-dev.2](https://github.com/anddea/revanced-patches/compare/v3.0.0-dev.1...v3.0.0-dev.2) (2024-12-12) + + +### Bug Fixes + +* **YouTube - Visual preferences icons:** Add missing icons in the Manager ([a9b443a](https://github.com/anddea/revanced-patches/commit/a9b443a738ca6f94a98ee32d9cd7ad0837ce66a8)) + +# [3.0.0-dev.1](https://github.com/anddea/revanced-patches/compare/v2.232.0-dev.1...v3.0.0-dev.1) (2024-12-11) + + +### Bug Fixes + +* **YouTube - Return YouTube Dislike:** Show Shorts dislikes with new A/B button icons ([ad0d15e](https://github.com/anddea/revanced-patches/commit/ad0d15e832c0a51997c35780d880f1bcb2a8b495)) +* **YouTube - Shorts components:** Do not hide Shorts action buttons on app first launch ([f5cd017](https://github.com/anddea/revanced-patches/commit/f5cd0173a845352da7d4d5d13f5d545eaf8217b1)) +* **YouTube - SponsorBlock:** Fix create new segment crash on tablet custom roms ([58b5fbf](https://github.com/anddea/revanced-patches/commit/58b5fbfcc77d46831f61a3099fbf37c01ae2b2ba)) +* **YouTube - Spoof streaming data:** Fix memory leak in `ByteArrayOutputStream` ([42d7bbe](https://github.com/anddea/revanced-patches/commit/42d7bbe8da244c73802e37cab646ccad590d7bdb)) +* **YouTube - Video playback:** Correctly set default quality when changing from a low quality video ([8cbe976](https://github.com/anddea/revanced-patches/commit/8cbe9766a4f26aeee909a799b834c4c85efd7e9d)) + + +### Code Refactoring + +* Bump ReVanced Patcher & merge integrations ([7dde697](https://github.com/anddea/revanced-patches/commit/7dde697995b3fa02749eff52cf50d1f903fc54ef)) + + +### Features + +* **YouTube - Overlay buttons:** Replace `Time-ordered playlist` button with `Play all` button ([5a15809](https://github.com/anddea/revanced-patches/commit/5a15809c96c4d6e988b196be8d0a85a828fe1d1b)) +* **YouTube - Spoof streaming data:** Rename the `iOS Compatibility mode` setting to `Skip iOS livestream playback` ([efbc77d](https://github.com/anddea/revanced-patches/commit/efbc77d6a0090cc15bec935aa993155cbf8bdd0c)) +* **YouTube - Theme:** Add `Pale Blue`, `Pale Green`, `Pale Orange` light colors ([1bed931](https://github.com/anddea/revanced-patches/commit/1bed9310b7343f1d860c4738b512fce91ffc3895)) +* **YouTube Music - Hide ads:** Changed the default value of `Hide fullscreen ads` setting to off and added a warning to the setting ([d337d21](https://github.com/anddea/revanced-patches/commit/d337d2115ef78fd1b2c8d2fd2e528d913b7978e9)) +* **YouTube Music:** Add `Spoof client` patch ([09c7967](https://github.com/anddea/revanced-patches/commit/09c796784cbd70fde471773d2ecb5f2123855b73)) +* **YouTube:** Support version `19.34.42` ([2018306](https://github.com/anddea/revanced-patches/commit/2018306d5f578ac9915f0a6001391999896fdacc)) + + +### BREAKING CHANGES + +* Patches and Integrations are now merged + +# [2.232.0-dev.1](https://github.com/anddea/revanced-patches/compare/v2.231.0...v2.232.0-dev.1) (2024-11-10) + + +### Features + +* **YouTube - Spoof app version:** Remove obsolete `19.13.37` spoof target ([743999e](https://github.com/anddea/revanced-patches/commit/743999e864892f33ee4153950339edc8ffae2578)) +* **YouTube - Spoof streaming data:** Add `iOS Compatibility mode` setting ([48b26eb](https://github.com/anddea/revanced-patches/commit/48b26eb9d2b5076248af96c2342fdcd7f29b8a51)) + # [2.231.0](https://github.com/anddea/revanced-patches/compare/v2.230.0...v2.231.0) (2024-11-07) diff --git a/README-template.md b/README-template.md index 34f7342a3..08f6dd760 100644 --- a/README-template.md +++ b/README-template.md @@ -21,40 +21,28 @@ Example: { "name": "Alternative thumbnails", "description": "Adds options to replace video thumbnails using the DeArrow API or image captures from the video.", - "compatiblePackages":[ - { - "name": "com.google.android.youtube", - "versions": "COMPATIBLE_PACKAGE_YOUTUBE" - } - ], "use":true, - "requiresIntegrations":false, + "compatiblePackages": { + "com.google.android.youtube": "COMPATIBLE_PACKAGE_YOUTUBE" + }, "options": [] }, { "name": "Bitrate default value", "description": "Sets the audio quality to 'Always High' when you first install the app.", - "compatiblePackages": [ - { - "name": "com.google.android.apps.youtube.music", - "versions": "COMPATIBLE_PACKAGE_MUSIC" - } - ], "use":true, - "requiresIntegrations":false, + "compatiblePackages": { + "com.google.android.apps.youtube.music": "COMPATIBLE_PACKAGE_MUSIC" + }, "options": [] }, { "name": "Hide ads", "description": "Adds options to hide ads.", - "compatiblePackages": [ - { - "name": "com.reddit.frontpage", - "versions": "COMPATIBLE_PACKAGE_REDDIT" - } - ], "use":true, - "requiresIntegrations":true, + "compatiblePackages": { + "com.reddit.frontpage": "COMPATIBLE_PACKAGE_REDDIT" + }, "options": [] } ] diff --git a/README.md b/README.md index aeb70118a..b331530fd 100644 --- a/README.md +++ b/README.md @@ -13,68 +13,69 @@ Check the [wiki](https://github.com/anddea/revanced-patches/wiki) for resources | 💊 Patch | 📜 Description | 🏹 Target Version | |:--------:|:--------------:|:-----------------:| -| `Alternative thumbnails` | Adds options to replace video thumbnails using the DeArrow API or image captures from the video. | 18.29.38 ~ 19.16.39 | -| `Ambient mode control` | Adds options to disable Ambient mode and to bypass Ambient mode restrictions. | 18.29.38 ~ 19.16.39 | -| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 18.29.38 ~ 19.16.39 | -| `Change player flyout menu toggles` | Adds an option to use text toggles instead of switch toggles within the additional settings menu. | 18.29.38 ~ 19.16.39 | -| `Change share sheet` | Add option to change from in-app share sheet to system share sheet. | 18.29.38 ~ 19.16.39 | -| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 18.29.38 ~ 19.16.39 | -| `Custom Shorts action buttons` | Changes, at compile time, the icon of the action buttons of the Shorts player. | 18.29.38 ~ 19.16.39 | -| `Custom branding icon for YouTube` | Changes the YouTube app icon to the icon specified in options.json. | 18.29.38 ~ 19.16.39 | -| `Custom branding name for YouTube` | Renames the YouTube app to the name specified in options.json. | 18.29.38 ~ 19.16.39 | -| `Custom double tap length` | Adds Double-tap to seek values that are specified in options.json. | 18.29.38 ~ 19.16.39 | -| `Description components` | Adds options to hide and disable description components. | 18.29.38 ~ 19.16.39 | -| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 18.29.38 ~ 19.16.39 | -| `Disable auto audio tracks` | Adds an option to disable audio tracks from being automatically enabled. | 18.29.38 ~ 19.16.39 | -| `Disable auto captions` | Adds an option to disable captions from being automatically enabled. | 18.29.38 ~ 19.16.39 | -| `Disable haptic feedback` | Adds options to disable haptic feedback when swiping in the video player. | 18.29.38 ~ 19.16.39 | -| `Disable resuming Shorts on startup` | Adds an option to disable the Shorts player from resuming on app startup when Shorts were last being watched. | 18.29.38 ~ 19.16.39 | -| `Disable splash animation` | Adds an option to disable the splash animation on app startup. | 18.29.38 ~ 19.16.39 | -| `Enable OPUS codec` | Adds an options to enable the OPUS audio codec if the player response includes. | 18.29.38 ~ 19.16.39 | -| `Enable debug logging` | Adds an option to enable debug logging. | 18.29.38 ~ 19.16.39 | -| `Enable external browser` | Adds an option to always open links in your browser instead of in the in-app-browser. | 18.29.38 ~ 19.16.39 | -| `Enable gradient loading screen` | Adds an option to enable the gradient loading screen. | 18.29.38 ~ 19.16.39 | -| `Enable open links directly` | Adds an option to skip over redirection URLs in external links. | 18.29.38 ~ 19.16.39 | -| `Force player buttons background` | Changes the dark background surrounding the video player controls at compile time. | 18.29.38 ~ 19.16.39 | -| `Force snackbar theme` | Force snackbar background color to match selected theme. | 18.29.38 ~ 19.16.39 | -| `Fullscreen components` | Adds options to hide or change components related to fullscreen. | 18.29.38 ~ 19.16.39 | -| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 18.29.38 ~ 19.16.39 | -| `Hide Shorts dimming` | Removes, at compile time, the dimming effect at the top and bottom of Shorts videos. | 18.29.38 ~ 19.16.39 | -| `Hide action buttons` | Adds options to hide action buttons under videos. | 18.29.38 ~ 19.16.39 | -| `Hide ads` | Adds options to hide ads. | 18.29.38 ~ 19.16.39 | -| `Hide comments components` | Adds options to hide components related to comments. | 18.29.38 ~ 19.16.39 | -| `Hide feed components` | Adds options to hide components related to feeds. | 18.29.38 ~ 19.16.39 | -| `Hide feed flyout menu` | Adds the ability to hide feed flyout menu components using a custom filter. | 18.29.38 ~ 19.16.39 | -| `Hide layout components` | Adds options to hide general layout components. | 18.29.38 ~ 19.16.39 | -| `Hide player buttons` | Adds options to hide buttons in the video player. | 18.29.38 ~ 19.16.39 | -| `Hide player flyout menu` | Adds options to hide player flyout menu components. | 18.29.38 ~ 19.16.39 | -| `Hide shortcuts` | Remove, at compile time, the app shortcuts that appears when app icon is long pressed. | 18.29.38 ~ 19.16.39 | -| `Hook YouTube Music actions` | Adds support for opening music in RVX Music using the in-app YouTube Music button. | 18.29.38 ~ 19.16.39 | -| `Hook download actions` | Adds support to download videos with an external downloader app using the in-app download button. | 18.29.38 ~ 19.16.39 | -| `Layout switch` | Adds an option to spoof the dpi in order to use a tablet or phone layout. | 18.29.38 ~ 19.16.39 | -| `MaterialYou` | Applies the MaterialYou theme for Android 12+ devices. | 18.29.38 ~ 19.16.39 | -| `Miniplayer` | Adds options to change the in app minimized player, and if patching target 19.16+ adds options to use modern miniplayers. | 18.29.38 ~ 19.16.39 | -| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 18.29.38 ~ 19.16.39 | -| `Overlay buttons` | Adds options to display overlay buttons in the video player. | 18.29.38 ~ 19.16.39 | -| `Player components` | Adds options to hide or change components related to the video player. | 18.29.38 ~ 19.16.39 | -| `Remove background playback restrictions` | Removes restrictions on background playback, including for music and kids videos. | 18.29.38 ~ 19.16.39 | -| `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 18.29.38 ~ 19.16.39 | -| `Return YouTube Dislike` | Adds an option to show the dislike count of videos using the Return YouTube Dislike API. | 18.29.38 ~ 19.16.39 | -| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 18.29.38 ~ 19.16.39 | -| `Sanitize sharing links` | Adds an option to remove tracking query parameters from URLs when sharing links. | 18.29.38 ~ 19.16.39 | -| `Seekbar components` | Adds options to hide or change components related to the seekbar. | 18.29.38 ~ 19.16.39 | -| `Settings for YouTube` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 18.29.38 ~ 19.16.39 | -| `Shorts components` | Adds options to hide or change components related to YouTube Shorts. | 18.29.38 ~ 19.16.39 | -| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as sponsored content. | 18.29.38 ~ 19.16.39 | -| `Spoof app version` | Adds options to spoof the YouTube client version. This can be used to restore old UI elements and features. | 18.29.38 ~ 19.16.39 | -| `Spoof streaming data` | Adds options to spoof the streaming data to allow video playback. | 18.29.38 ~ 19.16.39 | -| `Spoof watch history` | Adds an option to change the domain of the watch history or check its status. | 18.29.38 ~ 19.16.39 | -| `Swipe controls` | Adds options for controlling volume and brightness with swiping, and whether to enter fullscreen when swiping down below the player. | 18.29.38 ~ 19.16.39 | -| `Theme` | Changes the app's theme to the values specified in options.json. | 18.29.38 ~ 19.16.39 | -| `Toolbar components` | Adds options to hide or change components located on the toolbar, such as toolbar buttons, search bar, and header. | 18.29.38 ~ 19.16.39 | -| `Translations for YouTube` | Add translations or remove string resources. | 18.29.38 ~ 19.16.39 | -| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 18.29.38 ~ 19.16.39 | -| `Visual preferences icons for YouTube` | Adds icons to specific preferences in the settings. | 18.29.38 ~ 19.16.39 | +| `Alternative thumbnails` | Adds options to replace video thumbnails using the DeArrow API or image captures from the video. | 18.29.38 ~ 19.44.39 | +| `Ambient mode control` | Adds options to disable Ambient mode and to bypass Ambient mode restrictions. | 18.29.38 ~ 19.44.39 | +| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 18.29.38 ~ 19.44.39 | +| `Change player flyout menu toggles` | Adds an option to use text toggles instead of switch toggles within the additional settings menu. | 18.29.38 ~ 19.44.39 | +| `Change share sheet` | Add option to change from in-app share sheet to system share sheet. | 18.29.38 ~ 19.44.39 | +| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 18.29.38 ~ 19.44.39 | +| `Custom Shorts action buttons` | Changes, at compile time, the icon of the action buttons of the Shorts player. | 18.29.38 ~ 19.44.39 | +| `Custom branding icon for YouTube` | Changes the YouTube app icon to the icon specified in patch options. | 18.29.38 ~ 19.44.39 | +| `Custom branding name for YouTube` | Renames the YouTube app to the name specified in patch options. | 18.29.38 ~ 19.44.39 | +| `Custom double tap length` | Adds Double-tap to seek values that are specified in patch options. | 18.29.38 ~ 19.44.39 | +| `Custom header for YouTube` | Applies a custom header in the top left corner within the app. | 18.29.38 ~ 19.44.39 | +| `Description components` | Adds options to hide and disable description components. | 18.29.38 ~ 19.44.39 | +| `Disable QUIC protocol` | Adds an option to disable CronetEngine's QUIC protocol. | 18.29.38 ~ 19.44.39 | +| `Disable auto audio tracks` | Adds an option to disable audio tracks from being automatically enabled. | 18.29.38 ~ 19.44.39 | +| `Disable auto captions` | Adds an option to disable captions from being automatically enabled. | 18.29.38 ~ 19.44.39 | +| `Disable haptic feedback` | Adds options to disable haptic feedback when swiping in the video player. | 18.29.38 ~ 19.44.39 | +| `Disable resuming Shorts on startup` | Adds an option to disable the Shorts player from resuming on app startup when Shorts were last being watched. | 18.29.38 ~ 19.44.39 | +| `Disable splash animation` | Adds an option to disable the splash animation on app startup. | 18.29.38 ~ 19.44.39 | +| `Enable OPUS codec` | Adds an options to enable the OPUS audio codec if the player response includes. | 18.29.38 ~ 19.44.39 | +| `Enable debug logging` | Adds an option to enable debug logging. | 18.29.38 ~ 19.44.39 | +| `Enable external browser` | Adds an option to always open links in your browser instead of in the in-app-browser. | 18.29.38 ~ 19.44.39 | +| `Enable gradient loading screen` | Adds an option to enable the gradient loading screen. | 18.29.38 ~ 19.44.39 | +| `Enable open links directly` | Adds an option to skip over redirection URLs in external links. | 18.29.38 ~ 19.44.39 | +| `Force player buttons background` | Changes the dark background surrounding the video player controls at compile time. | 18.29.38 ~ 19.44.39 | +| `Force snackbar theme` | Changes snackbar background color to match selected theme at compile time. | 18.29.38 ~ 19.44.39 | +| `Fullscreen components` | Adds options to hide or change components related to fullscreen. | 18.29.38 ~ 19.44.39 | +| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 18.29.38 ~ 19.44.39 | +| `Hide Shorts dimming` | Removes, at compile time, the dimming effect at the top and bottom of Shorts videos. | 18.29.38 ~ 19.44.39 | +| `Hide action buttons` | Adds options to hide action buttons under videos. | 18.29.38 ~ 19.44.39 | +| `Hide ads` | Adds options to hide ads. | 18.29.38 ~ 19.44.39 | +| `Hide comments components` | Adds options to hide components related to comments. | 18.29.38 ~ 19.44.39 | +| `Hide feed components` | Adds options to hide components related to feeds. | 18.29.38 ~ 19.44.39 | +| `Hide feed flyout menu` | Adds the ability to hide feed flyout menu components using a custom filter. | 18.29.38 ~ 19.44.39 | +| `Hide layout components` | Adds options to hide general layout components. | 18.29.38 ~ 19.44.39 | +| `Hide player buttons` | Adds options to hide buttons in the video player. | 18.29.38 ~ 19.44.39 | +| `Hide player flyout menu` | Adds options to hide player flyout menu components. | 18.29.38 ~ 19.44.39 | +| `Hide shortcuts` | Remove, at compile time, the app shortcuts that appears when app icon is long pressed. | 18.29.38 ~ 19.44.39 | +| `Hook YouTube Music actions` | Adds support for opening music in RVX Music using the in-app YouTube Music button. | 18.29.38 ~ 19.44.39 | +| `Hook download actions` | Adds support to download videos with an external downloader app using the in-app download button. | 18.29.38 ~ 19.44.39 | +| `Layout switch` | Adds an option to spoof the dpi in order to use a tablet or phone layout. | 18.29.38 ~ 19.44.39 | +| `MaterialYou` | Applies the MaterialYou theme for Android 12+ devices. | 18.29.38 ~ 19.44.39 | +| `Miniplayer` | Adds options to change the in app minimized player, and if patching target 19.16+ adds options to use modern miniplayers. | 18.29.38 ~ 19.44.39 | +| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 18.29.38 ~ 19.44.39 | +| `Overlay buttons` | Adds options to display overlay buttons in the video player. | 18.29.38 ~ 19.44.39 | +| `Player components` | Adds options to hide or change components related to the video player. | 18.29.38 ~ 19.44.39 | +| `Remove background playback restrictions` | Removes restrictions on background playback, including for music and kids videos. | 18.29.38 ~ 19.44.39 | +| `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 18.29.38 ~ 19.44.39 | +| `Return YouTube Dislike` | Adds an option to show the dislike count of videos using the Return YouTube Dislike API. | 18.29.38 ~ 19.44.39 | +| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 18.29.38 ~ 19.44.39 | +| `Sanitize sharing links` | Adds an option to remove tracking query parameters from URLs when sharing links. | 18.29.38 ~ 19.44.39 | +| `Seekbar components` | Adds options to hide or change components related to the seekbar. | 18.29.38 ~ 19.44.39 | +| `Settings for YouTube` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 18.29.38 ~ 19.44.39 | +| `Shorts components` | Adds options to hide or change components related to YouTube Shorts. | 18.29.38 ~ 19.44.39 | +| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as sponsored content. | 18.29.38 ~ 19.44.39 | +| `Spoof app version` | Adds options to spoof the YouTube client version. This can be used to restore old UI elements and features. | 18.29.38 ~ 19.44.39 | +| `Spoof streaming data` | Adds options to spoof the streaming data to allow playback. | 18.29.38 ~ 19.44.39 | +| `Spoof watch history` | Adds an option to change the domain of the watch history or check its status. | 18.29.38 ~ 19.44.39 | +| `Swipe controls` | Adds options for controlling volume and brightness with swiping, and whether to enter fullscreen when swiping down below the player. | 18.29.38 ~ 19.44.39 | +| `Theme` | Changes the app's theme to the values specified in patch options. | 18.29.38 ~ 19.44.39 | +| `Toolbar components` | Adds options to hide or change components located on the toolbar, such as toolbar buttons, search bar, and header. | 18.29.38 ~ 19.44.39 | +| `Translations for YouTube` | Add translations or remove string resources. | 18.29.38 ~ 19.44.39 | +| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 18.29.38 ~ 19.44.39 | +| `Visual preferences icons for YouTube` | Adds icons to specific preferences in the settings. | 18.29.38 ~ 19.44.39 | ### [📦 `com.google.android.apps.youtube.music`](https://play.google.com/store/apps/details?id=com.google.android.apps.youtube.music) @@ -82,43 +83,46 @@ Check the [wiki](https://github.com/anddea/revanced-patches/wiki) for resources | 💊 Patch | 📜 Description | 🏹 Target Version | |:--------:|:--------------:|:-----------------:| -| `Amoled` | Applies a pure black theme to some components. | 6.20.51 ~ 7.16.53 | -| `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 7.16.53 | -| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 6.20.51 ~ 7.16.53 | -| `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 7.16.53 | -| `Change share sheet` | Add option to change from in-app share sheet to system share sheet. | 6.20.51 ~ 7.16.53 | -| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 7.16.53 | -| `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in options.json. | 6.20.51 ~ 7.16.53 | -| `Custom branding name for YouTube Music` | Renames the YouTube Music app to the name specified in options.json. | 6.20.51 ~ 7.16.53 | -| `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 7.16.53 | -| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 7.16.53 | -| `Disable auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 7.16.53 | -| `Disable dislike redirection` | Adds an option to disable redirection to the next track when clicking the Dislike button. | 6.20.51 ~ 7.16.53 | -| `Enable OPUS codec` | Adds an option to use the OPUS audio codec instead of the MP4A audio codec. | 6.20.51 ~ 7.16.53 | -| `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 7.16.53 | -| `Enable landscape mode` | Adds an option to enable landscape mode when rotating the screen on phones. | 6.20.51 ~ 7.16.53 | -| `Flyout menu components` | Adds options to hide or change flyout menu components. | 6.20.51 ~ 7.16.53 | -| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 6.20.51 ~ 7.16.53 | -| `Hide account components` | Adds options to hide components related to the account menu. | 6.20.51 ~ 7.16.53 | -| `Hide action bar components` | Adds options to hide action bar components and replace the offline download button with an external download button. | 6.20.51 ~ 7.16.53 | -| `Hide ads` | Adds options to hide ads. | 6.20.51 ~ 7.16.53 | -| `Hide layout components` | Adds options to hide general layout components. | 6.20.51 ~ 7.16.53 | -| `Hide overlay filter` | Removes, at compile time, the dark overlay that appears when player flyout menus are open. | 6.20.51 ~ 7.16.53 | -| `Hide player overlay filter` | Removes, at compile time, the dark overlay that appears when single-tapping in the player. | 6.20.51 ~ 7.16.53 | -| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 6.20.51 ~ 7.16.53 | -| `Player components` | Adds options to hide or change components related to the player. | 6.20.51 ~ 7.16.53 | -| `Remove background playback restrictions` | Removes restrictions on background playback, including for kids videos. | 6.20.51 ~ 7.16.53 | -| `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 6.20.51 ~ 7.16.53 | -| `Restore old style library shelf` | Adds an option to return the Library tab to the old style. | 6.20.51 ~ 7.16.53 | -| `Return YouTube Dislike` | Adds an option to show the dislike count of songs using the Return YouTube Dislike API. | 6.20.51 ~ 7.16.53 | -| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 6.20.51 ~ 7.16.53 | -| `Sanitize sharing links` | Adds an option to remove tracking query parameters from URLs when sharing links. | 6.20.51 ~ 7.16.53 | -| `Settings for YouTube Music` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 6.20.51 ~ 7.16.53 | -| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections. | 6.20.51 ~ 7.16.53 | +| `Amoled` | Applies a pure black theme to some components. | 6.20.51 ~ 7.25.53 | +| `Bitrate default value` | Sets the audio quality to 'Always High' when you first install the app. | 6.20.51 ~ 7.25.53 | +| `Bypass image region restrictions` | Adds an option to use a different host for static images, so that images blocked in some countries can be received. | 6.20.51 ~ 7.25.53 | +| `Certificate spoof` | Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate. | 6.20.51 ~ 7.25.53 | +| `Change share sheet` | Add option to change from in-app share sheet to system share sheet. | 6.20.51 ~ 7.25.53 | +| `Change start page` | Adds an option to set which page the app opens in instead of the homepage. | 6.20.51 ~ 7.25.53 | +| `Custom branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 7.25.53 | +| `Custom branding name for YouTube Music` | Renames the YouTube Music app to the name specified in patch options. | 6.20.51 ~ 7.25.53 | +| `Custom header for YouTube Music` | Applies a custom header in the top left corner within the app. | 6.20.51 ~ 7.25.53 | +| `Disable Cairo splash animation` | Adds an option to disable Cairo splash animation. | 7.06.54 ~ 7.25.53 | +| `Disable DRC audio` | Adds an option to disable DRC (Dynamic Range Compression) audio. | 6.20.51 ~ 7.25.53 | +| `Disable auto captions` | Adds an option to disable captions from being automatically enabled. | 6.20.51 ~ 7.25.53 | +| `Disable dislike redirection` | Adds an option to disable redirection to the next track when clicking the Dislike button. | 6.20.51 ~ 7.25.53 | +| `Enable OPUS codec` | Adds an options to enable the OPUS audio codec if the player response includes. | 6.20.51 ~ 7.25.53 | +| `Enable debug logging` | Adds an option to enable debug logging. | 6.20.51 ~ 7.25.53 | +| `Enable landscape mode` | Adds an option to enable landscape mode when rotating the screen on phones. | 6.20.51 ~ 7.25.53 | +| `Flyout menu components` | Adds options to hide or change flyout menu components. | 6.20.51 ~ 7.25.53 | +| `GmsCore support` | Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services. | 6.20.51 ~ 7.25.53 | +| `Hide account components` | Adds options to hide components related to the account menu. | 6.20.51 ~ 7.25.53 | +| `Hide action bar components` | Adds options to hide action bar components and replace the offline download button with an external download button. | 6.20.51 ~ 7.25.53 | +| `Hide ads` | Adds options to hide ads. | 6.20.51 ~ 7.25.53 | +| `Hide layout components` | Adds options to hide general layout components. | 6.20.51 ~ 7.25.53 | +| `Hide overlay filter` | Removes, at compile time, the dark overlay that appears when player flyout menus are open. | 6.20.51 ~ 7.25.53 | +| `Hide player overlay filter` | Removes, at compile time, the dark overlay that appears when single-tapping in the player. | 6.20.51 ~ 7.25.53 | +| `Navigation bar components` | Adds options to hide or change components related to the navigation bar. | 6.20.51 ~ 7.25.53 | +| `Player components` | Adds options to hide or change components related to the player. | 6.20.51 ~ 7.25.53 | +| `Remove background playback restrictions` | Removes restrictions on background playback, including for kids videos. | 6.20.51 ~ 7.25.53 | +| `Remove viewer discretion dialog` | Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction. | 6.20.51 ~ 7.25.53 | +| `Restore old style library shelf` | Adds an option to return the Library tab to the old style. | 6.20.51 ~ 7.25.53 | +| `Return YouTube Dislike` | Adds an option to show the dislike count of songs using the Return YouTube Dislike API. | 6.20.51 ~ 7.25.53 | +| `Return YouTube Username` | Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3. | 6.20.51 ~ 7.25.53 | +| `Sanitize sharing links` | Adds an option to remove tracking query parameters from URLs when sharing links. | 6.20.51 ~ 7.25.53 | +| `Settings for YouTube Music` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 6.20.51 ~ 7.25.53 | +| `SponsorBlock` | Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections. | 6.20.51 ~ 7.25.53 | | `Spoof app version` | Adds options to spoof the YouTube Music client version. This can remove the radio mode restriction in Canadian regions or disable real-time lyrics. | 6.20.51 ~ 7.16.53 | -| `Translations for YouTube Music` | Add translations or remove string resources. | 6.20.51 ~ 7.16.53 | -| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 6.20.51 ~ 7.16.53 | -| `Visual preferences icons for YouTube Music` | Adds icons to specific preferences in the settings. | 6.20.51 ~ 7.16.53 | +| `Spoof client` | Adds options to spoof the client to allow playback. | 6.20.51 ~ 7.16.53 | +| `Spoof streaming data` | Adds options to spoof the streaming data to allow playback. | 6.20.51 ~ 7.25.53 | +| `Translations for YouTube Music` | Add translations or remove string resources. | 6.20.51 ~ 7.25.53 | +| `Video playback` | Adds options to customize settings related to video playback, such as default video quality and playback speed. | 6.20.51 ~ 7.25.53 | +| `Visual preferences icons for YouTube Music` | Adds icons to specific preferences in the settings. | 6.20.51 ~ 7.25.53 | ### [📦 `com.reddit.frontpage`](https://play.google.com/store/apps/details?id=com.reddit.frontpage) @@ -126,19 +130,19 @@ Check the [wiki](https://github.com/anddea/revanced-patches/wiki) for resources | 💊 Patch | 📜 Description | 🏹 Target Version | |:--------:|:--------------:|:-----------------:| -| `Change package name` | Changes the package name for Reddit to the name specified in options.json. | 2023.12.0 ~ 2024.17.0 | -| `Custom branding name for Reddit` | Renames the Reddit app to the name specified in options.json. | 2023.12.0 ~ 2024.17.0 | -| `Disable screenshot popup` | Adds an option to disable the popup that appears when taking a screenshot. | 2023.12.0 ~ 2024.17.0 | -| `Hide Recently Visited shelf` | Adds an option to hide the Recently Visited shelf in the sidebar. | 2023.12.0 ~ 2024.17.0 | -| `Hide ads` | Adds options to hide ads. | 2023.12.0 ~ 2024.17.0 | -| `Hide navigation buttons` | Adds options to hide buttons in the navigation bar. | 2023.12.0 ~ 2024.17.0 | -| `Hide recommended communities shelf` | Adds an option to hide the recommended communities shelves in subreddits. | 2023.12.0 ~ 2024.17.0 | -| `Open links directly` | Adds an option to skip over redirection URLs in external links. | 2023.12.0 ~ 2024.17.0 | -| `Open links externally` | Adds an option to always open links in your browser instead of in the in-app-browser. | 2023.12.0 ~ 2024.17.0 | -| `Premium icon` | Unlocks premium app icons. | 2023.12.0 ~ 2024.17.0 | -| `Remove subreddit dialog` | Adds options to remove the NSFW community warning and notifications suggestion dialogs by dismissing them automatically. | 2023.12.0 ~ 2024.17.0 | -| `Sanitize sharing links` | Adds an option to remove tracking query parameters from URLs when sharing links. | 2023.12.0 ~ 2024.17.0 | -| `Settings for Reddit` | Applies mandatory patches to implement ReVanced Extended settings into the application. | 2023.12.0 ~ 2024.17.0 | +| `Change package name` | Changes the package name for Reddit to the name specified in patch options. | ALL | +| `Custom branding name for Reddit` | Renames the Reddit app to the name specified in patch options. | ALL | +| `Disable screenshot popup` | Adds an option to disable the popup that appears when taking a screenshot. | ALL | +| `Hide Recently Visited shelf` | Adds an option to hide the Recently Visited shelf in the sidebar. | ALL | +| `Hide ads` | Adds options to hide ads. | ALL | +| `Hide navigation buttons` | Adds options to hide buttons in the navigation bar. | ALL | +| `Hide recommended communities shelf` | Adds an option to hide the recommended communities shelves in subreddits. | ALL | +| `Open links directly` | Adds an option to skip over redirection URLs in external links. | ALL | +| `Open links externally` | Adds an option to always open links in your browser instead of in the in-app-browser. | ALL | +| `Premium icon` | Unlocks premium app icons. | ALL | +| `Remove subreddit dialog` | Adds options to remove the NSFW community warning and notifications suggestion dialogs by dismissing them automatically. | ALL | +| `Sanitize sharing links` | Adds an option to remove tracking query parameters from URLs when sharing links. | ALL | +| `Settings for Reddit` | Applies mandatory patches to implement ReVanced Extended settings into the application. | ALL | @@ -154,56 +158,43 @@ Example: { "name": "Alternative thumbnails", "description": "Adds options to replace video thumbnails using the DeArrow API or image captures from the video.", - "compatiblePackages":[ - { - "name": "com.google.android.youtube", - "versions": [ - "18.29.38", - "18.33.40", - "18.38.44", - "18.48.39", - "19.05.36", - "19.16.39" - ] - } - ], "use":true, - "requiresIntegrations":false, + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, "options": [] }, { "name": "Bitrate default value", "description": "Sets the audio quality to 'Always High' when you first install the app.", - "compatiblePackages": [ - { - "name": "com.google.android.apps.youtube.music", - "versions": [ - "6.20.51", - "6.29.59", - "6.42.55", - "6.51.53", - "7.16.53" - ] - } - ], "use":true, - "requiresIntegrations":false, + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, "options": [] }, { "name": "Hide ads", "description": "Adds options to hide ads.", - "compatiblePackages": [ - { - "name": "com.reddit.frontpage", - "versions": [ - "2023.12.0", - "2024.17.0" - ] - } - ], "use":true, - "requiresIntegrations":true, + "compatiblePackages": { + "com.reddit.frontpage": "ALL" + }, "options": [] } ] diff --git a/api/revanced-patches.api b/api/revanced-patches.api deleted file mode 100644 index 6d688233b..000000000 --- a/api/revanced-patches.api +++ /dev/null @@ -1,2025 +0,0 @@ -public final class app/revanced/generator/MainKt { - public static synthetic fun main ([Ljava/lang/String;)V -} - -public final class app/revanced/patches/all/misc/versioncode/ChangeVersionCodePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/all/misc/versioncode/ChangeVersionCodePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/account/components/AccountComponentsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/account/components/AccountComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/actionbar/components/ActionBarComponentsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/actionbar/components/ActionBarComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/ads/general/AdsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/ads/general/AdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/ads/general/MusicAdsPatch : app/revanced/patches/shared/ads/BaseAdsPatch { - public static final field INSTANCE Lapp/revanced/patches/music/ads/general/MusicAdsPatch; -} - -public final class app/revanced/patches/music/flyoutmenu/components/FlyoutMenuComponentsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/flyoutmenu/components/FlyoutMenuComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/flyoutmenu/components/FlyoutMenuComponentsResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/flyoutmenu/components/FlyoutMenuComponentsResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/flyoutmenu/components/fingerprints/TrimSilenceConfigFingerprint : app/revanced/util/fingerprint/LiteralValueFingerprint { - public static final field INSTANCE Lapp/revanced/patches/music/flyoutmenu/components/fingerprints/TrimSilenceConfigFingerprint; -} - -public final class app/revanced/patches/music/flyoutmenu/components/fingerprints/TrimSilenceSwitchFingerprint : app/revanced/util/fingerprint/LiteralValueFingerprint { - public static final field INSTANCE Lapp/revanced/patches/music/flyoutmenu/components/fingerprints/TrimSilenceSwitchFingerprint; -} - -public final class app/revanced/patches/music/general/amoled/AmoledPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/general/amoled/AmoledPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/general/autocaptions/AutoCaptionsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/general/autocaptions/AutoCaptionsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/general/components/LayoutComponentsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/general/components/LayoutComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/general/components/fingerprints/SearchBarFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/music/general/components/fingerprints/SearchBarFingerprint; - public final fun indexOfVisibilityInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I -} - -public final class app/revanced/patches/music/general/components/fingerprints/SearchBarParentFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/music/general/components/fingerprints/SearchBarParentFingerprint; -} - -public final class app/revanced/patches/music/general/dialog/ViewerDiscretionDialogBytecodePatch : app/revanced/patches/shared/dialog/BaseViewerDiscretionDialogPatch { - public static final field INSTANCE Lapp/revanced/patches/music/general/dialog/ViewerDiscretionDialogBytecodePatch; -} - -public final class app/revanced/patches/music/general/dialog/ViewerDiscretionDialogPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/general/dialog/ViewerDiscretionDialogPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/general/landscapemode/LandScapeModePatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/general/landscapemode/LandScapeModePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/general/oldstylelibraryshelf/OldStyleLibraryShelfPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/general/oldstylelibraryshelf/OldStyleLibraryShelfPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/general/redirection/DislikeRedirectionPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/general/redirection/DislikeRedirectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/general/spoofappversion/SpoofAppVersionBytecodePatch : app/revanced/patches/shared/spoofappversion/BaseSpoofAppVersionPatch { - public static final field INSTANCE Lapp/revanced/patches/music/general/spoofappversion/SpoofAppVersionBytecodePatch; -} - -public final class app/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/general/startpage/ChangeStartPagePatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/general/startpage/ChangeStartPagePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/layout/branding/icon/CustomBrandingIconPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/branding/icon/CustomBrandingIconPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public final fun getAppIcon ()Lapp/revanced/patcher/patch/options/PatchOption; -} - -public final class app/revanced/patches/music/layout/branding/name/CustomBrandingNamePatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/branding/name/CustomBrandingNamePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/layout/header/ChangeHeaderPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/header/ChangeHeaderPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/layout/overlayfilter/OverlayFilterBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/overlayfilter/OverlayFilterBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/layout/overlayfilter/OverlayFilterPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/overlayfilter/OverlayFilterPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/layout/playeroverlay/PlayerOverlayFilterPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/playeroverlay/PlayerOverlayFilterPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/layout/translations/TranslationsPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/translations/TranslationsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/misc/bitrate/BitrateDefaultValuePatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/bitrate/BitrateDefaultValuePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/misc/codecs/OpusCodecBytecodePatch : app/revanced/patches/shared/opus/BaseOpusCodecsPatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/codecs/OpusCodecBytecodePatch; -} - -public final class app/revanced/patches/music/misc/codecs/OpusCodecPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/codecs/OpusCodecPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/misc/debugging/DebuggingPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/debugging/DebuggingPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/misc/share/ShareSheetPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/share/ShareSheetPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/splash/CairoSplashAnimationPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/misc/thumbnails/BypassImageRegionRestrictionsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/thumbnails/BypassImageRegionRestrictionsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/misc/tracking/SanitizeUrlQueryPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/tracking/SanitizeUrlQueryPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/navigation/components/NavigationBarComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/player/components/PlayerComponentsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/player/components/PlayerComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/player/components/PlayerComponentsResourcePatch : app/revanced/patcher/patch/ResourcePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/music/player/components/PlayerComponentsResourcePatch; - public fun close ()V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/utils/compatibility/Constants { - public static final field INSTANCE Lapp/revanced/patches/music/utils/compatibility/Constants; - public final fun getCOMPATIBLE_PACKAGE ()Ljava/util/Set; -} - -public final class app/revanced/patches/music/utils/fix/androidauto/AndroidAutoCertificatePatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/fix/androidauto/AndroidAutoCertificatePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/utils/fix/client/SpoofUserAgentPatch : app/revanced/patches/shared/spoofuseragent/BaseSpoofUserAgentPatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/fix/client/SpoofUserAgentPatch; -} - -public final class app/revanced/patches/music/utils/fix/fileprovider/FileProviderPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/fix/fileprovider/FileProviderPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/utils/fix/header/RestoreOldHeaderPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/fix/header/RestoreOldHeaderPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/utils/flyoutmenu/FlyoutMenuHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/flyoutmenu/FlyoutMenuHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/utils/gms/GmsCoreSupportPatch : app/revanced/patches/shared/gms/BaseGmsCoreSupportPatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/gms/GmsCoreSupportPatch; -} - -public final class app/revanced/patches/music/utils/gms/GmsCoreSupportResourcePatch : app/revanced/patches/shared/gms/BaseGmsCoreSupportResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/gms/GmsCoreSupportResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/utils/imageurlhook/CronetImageUrlHookPatch : app/revanced/patches/shared/imageurlhook/BaseCronetImageUrlHookPatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/imageurlhook/CronetImageUrlHookPatch; -} - -public final class app/revanced/patches/music/utils/integrations/Constants { - public static final field ACCOUNT_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field ACCOUNT_PATH Ljava/lang/String; - public static final field ACTIONBAR_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field ACTIONBAR_PATH Ljava/lang/String; - public static final field ADS_PATH Ljava/lang/String; - public static final field COMPONENTS_PATH Ljava/lang/String; - public static final field FLYOUT_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field FLYOUT_PATH Ljava/lang/String; - public static final field GENERAL_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field GENERAL_PATH Ljava/lang/String; - public static final field INSTANCE Lapp/revanced/patches/music/utils/integrations/Constants; - public static final field INTEGRATIONS_PATH Ljava/lang/String; - public static final field MISC_PATH Ljava/lang/String; - public static final field NAVIGATION_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field NAVIGATION_PATH Ljava/lang/String; - public static final field PATCHES_PATH Ljava/lang/String; - public static final field PATCH_STATUS_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field PLAYER_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field PLAYER_PATH Ljava/lang/String; - public static final field SHARED_PATH Ljava/lang/String; - public static final field UTILS_PATH Ljava/lang/String; - public static final field VIDEO_PATH Ljava/lang/String; -} - -public final class app/revanced/patches/music/utils/integrations/IntegrationsPatch : app/revanced/patches/shared/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/music/utils/mainactivity/MainActivityResolvePatch : app/revanced/patches/shared/mainactivity/BaseMainActivityResolvePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/mainactivity/MainActivityResolvePatch; -} - -public final class app/revanced/patches/music/utils/playertype/PlayerTypeHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/playertype/PlayerTypeHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/resourceid/SharedResourceIdPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public final fun getAccountSwitcherAccessibility ()J - public final fun getBottomSheetRecyclerView ()J - public final fun getButtonContainer ()J - public final fun getButtonIconPaddingMedium ()J - public final fun getChipCloud ()J - public final fun getColorGrey ()J - public final fun getDarkBackground ()J - public final fun getDesignBottomSheetDialog ()J - public final fun getEndButtonsContainer ()J - public final fun getFloatingLayout ()J - public final fun getHistoryMenuItem ()J - public final fun getInlineTimeBarAdBreakMarkerColor ()J - public final fun getInterstitialsContainer ()J - public final fun getIsTablet ()J - public final fun getLikeDislikeContainer ()J - public final fun getMainActivityLaunchAnimation ()J - public final fun getMenuEntry ()J - public final fun getMiniPlayerDefaultText ()J - public final fun getMiniPlayerMdxPlaying ()J - public final fun getMiniPlayerPlayPauseReplayButton ()J - public final fun getMiniPlayerViewPager ()J - public final fun getMusicNotifierShelf ()J - public final fun getMusicTasteBuilderShelf ()J - public final fun getNamesInactiveAccountThumbnailSize ()J - public final fun getOfflineSettingsMenuItem ()J - public final fun getPlayerOverlayChip ()J - public final fun getPlayerViewPager ()J - public final fun getPrivacyTosFooter ()J - public final fun getQualityAuto ()J - public final fun getRemixGenericButtonSize ()J - public final fun getSlidingDialogAnimation ()J - public final fun getTapBloomView ()J - public final fun getText1 ()J - public final fun getToolTipContentView ()J - public final fun getTopBarMenuItemImageView ()J - public final fun getTopEnd ()J - public final fun getTopStart ()J - public final fun getTosFooter ()J - public final fun getTouchOutside ()J - public final fun getTrimSilenceSwitch ()J - public final fun getVarispeedUnavailableTitle ()J - public final fun setAccountSwitcherAccessibility (J)V - public final fun setBottomSheetRecyclerView (J)V - public final fun setButtonContainer (J)V - public final fun setButtonIconPaddingMedium (J)V - public final fun setChipCloud (J)V - public final fun setColorGrey (J)V - public final fun setDarkBackground (J)V - public final fun setDesignBottomSheetDialog (J)V - public final fun setEndButtonsContainer (J)V - public final fun setFloatingLayout (J)V - public final fun setHistoryMenuItem (J)V - public final fun setInlineTimeBarAdBreakMarkerColor (J)V - public final fun setInterstitialsContainer (J)V - public final fun setIsTablet (J)V - public final fun setLikeDislikeContainer (J)V - public final fun setMainActivityLaunchAnimation (J)V - public final fun setMenuEntry (J)V - public final fun setMiniPlayerDefaultText (J)V - public final fun setMiniPlayerMdxPlaying (J)V - public final fun setMiniPlayerPlayPauseReplayButton (J)V - public final fun setMiniPlayerViewPager (J)V - public final fun setMusicNotifierShelf (J)V - public final fun setMusicTasteBuilderShelf (J)V - public final fun setNamesInactiveAccountThumbnailSize (J)V - public final fun setOfflineSettingsMenuItem (J)V - public final fun setPlayerOverlayChip (J)V - public final fun setPlayerViewPager (J)V - public final fun setPrivacyTosFooter (J)V - public final fun setQualityAuto (J)V - public final fun setRemixGenericButtonSize (J)V - public final fun setSlidingDialogAnimation (J)V - public final fun setTapBloomView (J)V - public final fun setText1 (J)V - public final fun setToolTipContentView (J)V - public final fun setTopBarMenuItemImageView (J)V - public final fun setTopEnd (J)V - public final fun setTopStart (J)V - public final fun setTosFooter (J)V - public final fun setTouchOutside (J)V - public final fun setTrimSilenceSwitch (J)V - public final fun setVarispeedUnavailableTitle (J)V -} - -public final class app/revanced/patches/music/utils/returnyoutubedislike/ReturnYouTubeDislikeBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/returnyoutubedislike/ReturnYouTubeDislikeBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/utils/returnyoutubedislike/ReturnYouTubeDislikePatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/returnyoutubedislike/ReturnYouTubeDislikePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/utils/returnyoutubeusername/ReturnYouTubeUsernamePatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/returnyoutubeusername/ReturnYouTubeUsernamePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/utils/settings/CategoryType : java/lang/Enum { - public static final field ACCOUNT Lapp/revanced/patches/music/utils/settings/CategoryType; - public static final field ACTION_BAR Lapp/revanced/patches/music/utils/settings/CategoryType; - public static final field ADS Lapp/revanced/patches/music/utils/settings/CategoryType; - public static final field FLYOUT Lapp/revanced/patches/music/utils/settings/CategoryType; - public static final field GENERAL Lapp/revanced/patches/music/utils/settings/CategoryType; - public static final field MISC Lapp/revanced/patches/music/utils/settings/CategoryType; - public static final field NAVIGATION Lapp/revanced/patches/music/utils/settings/CategoryType; - public static final field PLAYER Lapp/revanced/patches/music/utils/settings/CategoryType; - public static final field RETURN_YOUTUBE_DISLIKE Lapp/revanced/patches/music/utils/settings/CategoryType; - public static final field RETURN_YOUTUBE_USERNAME Lapp/revanced/patches/music/utils/settings/CategoryType; - public static final field SETTINGS Lapp/revanced/patches/music/utils/settings/CategoryType; - public static final field SPONSOR_BLOCK Lapp/revanced/patches/music/utils/settings/CategoryType; - public static final field VIDEO Lapp/revanced/patches/music/utils/settings/CategoryType; - public final fun getAdded ()Z - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public final fun getValue ()Ljava/lang/String; - public final fun setAdded (Z)V - public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/music/utils/settings/CategoryType; - public static fun values ()[Lapp/revanced/patches/music/utils/settings/CategoryType; -} - -public final class app/revanced/patches/music/utils/settings/ResourceUtils { - public static final field ACTIVITY_HOOK_TARGET_CLASS Ljava/lang/String; - public static final field INSTANCE Lapp/revanced/patches/music/utils/settings/ResourceUtils; - public static final field PREFERENCE_CATEGORY_TAG_NAME Ljava/lang/String; - public static final field PREFERENCE_SCREEN_TAG_NAME Ljava/lang/String; - public static final field SETTINGS_HEADER_PATH Ljava/lang/String; - public static final field SWITCH_PREFERENCE_TAG_NAME Ljava/lang/String; - public final fun addMicroGPreference (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public final fun addPreferenceCategory (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;)V - public final fun addPreferenceCategoryUnderPreferenceScreen (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/lang/String;)V - public final fun addPreferenceWithIntent (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public final fun addRVXSettingsPreference (Lapp/revanced/patcher/data/ResourceContext;)V - public final fun addSwitchPreference (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V - public final fun getIconType ()Ljava/lang/String; - public final fun getMusicPackageName ()Ljava/lang/String; - public final fun setIconType (Ljava/lang/String;)V - public final fun setMusicPackageName (Ljava/lang/String;)V - public final fun setPreferenceScreenIcon (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;)V - public final fun sortPreferenceCategory (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;)V - public final fun updatePackageName (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;)V -} - -public final class app/revanced/patches/music/utils/settings/SettingsBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/settings/SettingsBytecodePatch; - public static field contexts Lapp/revanced/patcher/data/BytecodeContext; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun getContexts ()Lapp/revanced/patcher/data/BytecodeContext; - public final fun setContexts (Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/music/utils/settings/SettingsPatch : app/revanced/util/patch/BaseResourcePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/music/utils/settings/SettingsPatch; - public static field contexts Lapp/revanced/patcher/data/ResourceContext; - public fun close ()V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public final fun getContexts ()Lapp/revanced/patcher/data/ResourceContext; - public final fun setContexts (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/utils/settings/VisualPreferencesIconsPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/settings/VisualPreferencesIconsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/utils/sponsorblock/SponsorBlockBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/sponsorblock/SponsorBlockBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/utils/sponsorblock/SponsorBlockPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/sponsorblock/SponsorBlockPatch; - public static field context Lapp/revanced/patcher/data/ResourceContext; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public final fun getContext ()Lapp/revanced/patcher/data/ResourceContext; - public final fun setContext (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/music/utils/videotype/VideoTypeHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/utils/videotype/VideoTypeHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/video/information/VideoInformationPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/video/information/VideoInformationPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/video/playback/CustomPlaybackSpeedPatch : app/revanced/patches/shared/customspeed/BaseCustomPlaybackSpeedPatch { - public static final field INSTANCE Lapp/revanced/patches/music/video/playback/CustomPlaybackSpeedPatch; -} - -public final class app/revanced/patches/music/video/playback/VideoPlaybackPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/video/playback/VideoPlaybackPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/ad/banner/BannerAdsPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/ad/banner/BannerAdsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/reddit/ad/comments/CommentAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/ad/comments/CommentAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/ad/general/AdsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/ad/general/AdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/layout/branding/name/CustomBrandingNamePatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/layout/branding/name/CustomBrandingNamePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/reddit/layout/branding/packagename/ChangePackageNamePatch : app/revanced/util/patch/BaseResourcePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/reddit/layout/branding/packagename/ChangePackageNamePatch; - public fun close ()V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/reddit/layout/communities/RecommendedCommunitiesPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/layout/communities/RecommendedCommunitiesPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/layout/navigation/NavigationButtonsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/layout/navigation/NavigationButtonsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/layout/premiumicon/PremiumIconPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/layout/premiumicon/PremiumIconPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/layout/recentlyvisited/RecentlyVisitedShelfPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/layout/recentlyvisited/RecentlyVisitedShelfPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/layout/subredditdialog/SubRedditDialogPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/layout/subredditdialog/SubRedditDialogPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/layout/toolbar/ToolBarButtonPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/layout/toolbar/ToolBarButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/misc/openlink/OpenLinksDirectlyPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/misc/openlink/OpenLinksDirectlyPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/misc/openlink/OpenLinksExternallyPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/misc/openlink/OpenLinksExternallyPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/misc/tracking/url/SanitizeUrlQueryPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/misc/tracking/url/SanitizeUrlQueryPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/utils/compatibility/Constants { - public static final field INSTANCE Lapp/revanced/patches/reddit/utils/compatibility/Constants; - public final fun getCOMPATIBLE_PACKAGE ()Ljava/util/Set; -} - -public final class app/revanced/patches/reddit/utils/integrations/Constants { - public static final field INSTANCE Lapp/revanced/patches/reddit/utils/integrations/Constants; - public static final field INTEGRATIONS_PATH Ljava/lang/String; - public static final field PATCHES_PATH Ljava/lang/String; -} - -public final class app/revanced/patches/reddit/utils/integrations/IntegrationsPatch : app/revanced/patches/shared/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/utils/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public final fun getCancelButton ()J - public final fun getLabelAcknowledgements ()J - public final fun getScreenShotShareBanner ()J - public final fun getTextAppearanceRedditBaseOldButtonColored ()J - public final fun getToolBarNavSearchCtaContainer ()J - public final fun setCancelButton (J)V - public final fun setLabelAcknowledgements (J)V - public final fun setScreenShotShareBanner (J)V - public final fun setTextAppearanceRedditBaseOldButtonColored (J)V - public final fun setToolBarNavSearchCtaContainer (J)V -} - -public final class app/revanced/patches/reddit/utils/settings/SettingsBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/utils/settings/SettingsBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/utils/settings/SettingsPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/utils/settings/SettingsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public abstract class app/revanced/patches/shared/ads/BaseAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INTEGRATIONS_CLASS_DESCRIPTOR Ljava/lang/String; - public fun (Ljava/lang/String;Ljava/lang/String;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/shared/captions/BaseAutoCaptionsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/shared/captions/BaseAutoCaptionsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/customspeed/BaseCustomPlaybackSpeedPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/lang/String;F)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/dialog/BaseViewerDiscretionDialogPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/shared/drawable/DrawableColorPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/shared/drawable/DrawableColorPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun injectCall (Ljava/lang/String;)V -} - -public final class app/revanced/patches/shared/elements/StringsElementsUtils { - public static final field INSTANCE Lapp/revanced/patches/shared/elements/StringsElementsUtils; -} - -public abstract class app/revanced/patches/shared/gms/BaseGmsCoreSupportPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INTEGRATIONS_CLASS_DESCRIPTOR Ljava/lang/String; - public fun (Ljava/lang/String;Lapp/revanced/patcher/fingerprint/MethodFingerprint;Lkotlin/reflect/KClass;Lapp/revanced/patches/shared/gms/BaseGmsCoreSupportResourcePatch;Ljava/util/Set;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Lapp/revanced/patcher/fingerprint/MethodFingerprint;Lkotlin/reflect/KClass;Lapp/revanced/patches/shared/gms/BaseGmsCoreSupportResourcePatch;Ljava/util/Set;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/gms/BaseGmsCoreSupportResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field Companion Lapp/revanced/patches/shared/gms/BaseGmsCoreSupportResourcePatch$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/shared/gms/BaseGmsCoreSupportResourcePatch$Companion { -} - -public abstract class app/revanced/patches/shared/imageurlhook/BaseCronetImageUrlHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field Companion Lapp/revanced/patches/shared/imageurlhook/BaseCronetImageUrlHookPatch$Companion; - public fun (Z)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/shared/imageurlhook/BaseCronetImageUrlHookPatch$Companion { -} - -public abstract class app/revanced/patches/shared/integrations/BaseIntegrationsPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/util/Set;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/integrations/BaseIntegrationsPatch$IntegrationsFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun invoke (Ljava/lang/String;)V -} - -public abstract interface class app/revanced/patches/shared/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IHookInsertIndexResolver : kotlin/jvm/functions/Function1 { - public abstract fun invoke (Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/Integer; -} - -public final class app/revanced/patches/shared/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IHookInsertIndexResolver$DefaultImpls { - public static fun invoke (Lapp/revanced/patches/shared/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IHookInsertIndexResolver;Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/Integer; -} - -public abstract interface class app/revanced/patches/shared/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IRegisterResolver : kotlin/jvm/functions/Function1 { - public abstract fun invoke (Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/String; -} - -public final class app/revanced/patches/shared/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IRegisterResolver$DefaultImpls { - public static fun invoke (Lapp/revanced/patches/shared/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IRegisterResolver;Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/String; -} - -public final class app/revanced/patches/shared/integrations/Constants { - public static final field COMPONENTS_PATH Ljava/lang/String; - public static final field INSTANCE Lapp/revanced/patches/shared/integrations/Constants; - public static final field INTEGRATIONS_PATH Ljava/lang/String; - public static final field INTEGRATIONS_SETTING_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field INTEGRATIONS_UTILS_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field INTEGRATIONS_UTILS_PATH Ljava/lang/String; - public static final field PATCHES_PATH Ljava/lang/String; - public static final field SPANS_PATH Ljava/lang/String; -} - -public final class app/revanced/patches/shared/litho/LithoFilterPatch : app/revanced/patcher/patch/BytecodePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/shared/litho/LithoFilterPatch; - public fun close ()V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/mainactivity/BaseMainActivityResolvePatch : app/revanced/patcher/patch/BytecodePatch { - public field mainActivityMutableClass Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass; - public field onConfigurationChangedMethod Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; - public field onCreateMethod Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; - public fun (Lapp/revanced/patcher/fingerprint/MethodFingerprint;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun getMainActivityMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass; - public final fun getOnConfigurationChangedMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; - public final fun getOnCreateMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; - public final fun injectConstructorMethodCall (Ljava/lang/String;Ljava/lang/String;)V - public final fun injectOnBackPressedMethodCall (Ljava/lang/String;Ljava/lang/String;)V - public final fun injectOnCreateMethodCall (Ljava/lang/String;Ljava/lang/String;)V - public final fun setMainActivityMutableClass (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;)V - public final fun setOnConfigurationChangedMethod (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)V - public final fun setOnCreateMethod (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)V -} - -public final class app/revanced/patches/shared/mapping/ResourceMappingPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/shared/mapping/ResourceMappingPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public final fun getId (Lapp/revanced/patches/shared/mapping/ResourceType;Ljava/lang/String;)J -} - -public final class app/revanced/patches/shared/mapping/ResourceMappingPatch$ResourceElement { - public fun (Ljava/lang/String;Ljava/lang/String;J)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()J - public final fun copy (Ljava/lang/String;Ljava/lang/String;J)Lapp/revanced/patches/shared/mapping/ResourceMappingPatch$ResourceElement; - public static synthetic fun copy$default (Lapp/revanced/patches/shared/mapping/ResourceMappingPatch$ResourceElement;Ljava/lang/String;Ljava/lang/String;JILjava/lang/Object;)Lapp/revanced/patches/shared/mapping/ResourceMappingPatch$ResourceElement; - public fun equals (Ljava/lang/Object;)Z - public final fun getId ()J - public final fun getName ()Ljava/lang/String; - public final fun getType ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class app/revanced/patches/shared/mapping/ResourceType : java/lang/Enum { - public static final field ATTR Lapp/revanced/patches/shared/mapping/ResourceType; - public static final field BOOL Lapp/revanced/patches/shared/mapping/ResourceType; - public static final field COLOR Lapp/revanced/patches/shared/mapping/ResourceType; - public static final field DIMEN Lapp/revanced/patches/shared/mapping/ResourceType; - public static final field DRAWABLE Lapp/revanced/patches/shared/mapping/ResourceType; - public static final field ID Lapp/revanced/patches/shared/mapping/ResourceType; - public static final field INTEGER Lapp/revanced/patches/shared/mapping/ResourceType; - public static final field LAYOUT Lapp/revanced/patches/shared/mapping/ResourceType; - public static final field STRING Lapp/revanced/patches/shared/mapping/ResourceType; - public static final field STYLE Lapp/revanced/patches/shared/mapping/ResourceType; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public final fun getValue ()Ljava/lang/String; - public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/shared/mapping/ResourceType; - public static fun values ()[Lapp/revanced/patches/shared/mapping/ResourceType; -} - -public abstract class app/revanced/patches/shared/opus/BaseOpusCodecsPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/lang/String;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/shared/overlaybackground/OverlayBackgroundUtils { - public static final field INSTANCE Lapp/revanced/patches/shared/overlaybackground/OverlayBackgroundUtils; -} - -public final class app/revanced/patches/shared/returnyoutubeusername/BaseReturnYouTubeUsernamePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/shared/returnyoutubeusername/BaseReturnYouTubeUsernamePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/shared/settingmenu/SettingsMenuPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/shared/settingmenu/SettingsMenuPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/shared/spans/InclusiveSpanPatch : app/revanced/patcher/patch/BytecodePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/shared/spans/InclusiveSpanPatch; - public fun close ()V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/spoofappversion/BaseSpoofAppVersionPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/lang/String;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/spoofuseragent/BaseSpoofUserAgentPatch : app/revanced/patches/shared/transformation/BaseTransformInstructionsPatch { - public fun (Ljava/lang/String;)V - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Triple; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Triple;)V -} - -public final class app/revanced/patches/shared/textcomponent/TextComponentPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/shared/textcomponent/TextComponentPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun hookSpannableString (Ljava/lang/String;Ljava/lang/String;)V - public final fun hookTextComponent (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; - public static synthetic fun hookTextComponent$default (Lapp/revanced/patches/shared/textcomponent/TextComponentPatch;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; -} - -public final class app/revanced/patches/shared/tracking/BaseSanitizeUrlQueryPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/shared/tracking/BaseSanitizeUrlQueryPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/transformation/BaseTransformInstructionsPatch : app/revanced/patcher/patch/BytecodePatch { - public fun ()V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public abstract fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public final fun findPatchIndices (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;)Lkotlin/sequences/Sequence; - public abstract fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V -} - -public abstract interface class app/revanced/patches/shared/transformation/IMethodCall { - public abstract fun getDefinedClassName ()Ljava/lang/String; - public abstract fun getMethodName ()Ljava/lang/String; - public abstract fun getMethodParams ()[Ljava/lang/String; - public abstract fun getReturnType ()Ljava/lang/String; - public abstract fun replaceInvokeVirtualWithIntegrations (Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lcom/android/tools/smali/dexlib2/iface/instruction/formats/Instruction35c;I)V -} - -public final class app/revanced/patches/shared/transformation/IMethodCall$DefaultImpls { - public static fun replaceInvokeVirtualWithIntegrations (Lapp/revanced/patches/shared/transformation/IMethodCall;Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lcom/android/tools/smali/dexlib2/iface/instruction/formats/Instruction35c;I)V -} - -public final class app/revanced/patches/shared/translations/TranslationsUtils { - public static final field INSTANCE Lapp/revanced/patches/shared/translations/TranslationsUtils; -} - -public final class app/revanced/patches/shared/translations/TranslationsUtilsKt { - public static final fun getAPP_LANGUAGES ()[Ljava/lang/String; -} - -public final class app/revanced/patches/shared/viewgroup/ViewGroupMarginLayoutParamsHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/shared/viewgroup/ViewGroupMarginLayoutParamsHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/ads/general/AdsBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/ads/general/AdsBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/ads/general/AdsPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/ads/general/AdsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/ads/general/VideoAdsPatch : app/revanced/patches/shared/ads/BaseAdsPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/ads/general/VideoAdsPatch; -} - -public final class app/revanced/patches/youtube/alternative/thumbnails/AlternativeThumbnailsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/alternative/thumbnails/AlternativeThumbnailsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/alternative/thumbnails/BypassImageRegionRestrictionsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/alternative/thumbnails/BypassImageRegionRestrictionsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/feed/components/FeedComponentsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/feed/components/FeedComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/feed/flyoutmenu/FeedFlyoutMenuPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/feed/flyoutmenu/FeedFlyoutMenuPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/general/audiotracks/AudioTracksPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/audiotracks/AudioTracksPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/general/autocaptions/AutoCaptionsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/autocaptions/AutoCaptionsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/general/components/LayoutComponentsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/components/LayoutComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/general/dialog/ViewerDiscretionDialogBytecodePatch : app/revanced/patches/shared/dialog/BaseViewerDiscretionDialogPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/dialog/ViewerDiscretionDialogBytecodePatch; -} - -public final class app/revanced/patches/youtube/general/dialog/ViewerDiscretionDialogPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/dialog/ViewerDiscretionDialogPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/general/downloads/DownloadActionsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/downloads/DownloadActionsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/general/layoutswitch/LayoutSwitchPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/layoutswitch/LayoutSwitchPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/general/loadingscreen/GradientLoadingScreenPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/loadingscreen/GradientLoadingScreenPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/general/miniplayer/MiniplayerPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/miniplayer/MiniplayerPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/general/music/YouTubeMusicActionsPatch : app/revanced/util/patch/BaseBytecodePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/music/YouTubeMusicActionsPatch; - public fun close ()V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/general/snackbar/ForceSnackbarTheme : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/snackbar/ForceSnackbarTheme; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/general/splashanimation/SplashAnimationPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/splashanimation/SplashAnimationPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/general/spoofappversion/SpoofAppVersionBytecodePatch : app/revanced/patches/shared/spoofappversion/BaseSpoofAppVersionPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/spoofappversion/SpoofAppVersionBytecodePatch; -} - -public final class app/revanced/patches/youtube/general/spoofappversion/SpoofAppVersionPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/spoofappversion/SpoofAppVersionPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/general/startpage/ChangeStartPagePatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/startpage/ChangeStartPagePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/general/toolbar/fingerprints/SearchBarFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/toolbar/fingerprints/SearchBarFingerprint; -} - -public final class app/revanced/patches/youtube/general/toolbar/fingerprints/SearchBarParentFingerprint : app/revanced/util/fingerprint/LiteralValueFingerprint { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/toolbar/fingerprints/SearchBarParentFingerprint; -} - -public final class app/revanced/patches/youtube/general/toolbar/fingerprints/SearchResultFingerprint : app/revanced/util/fingerprint/LiteralValueFingerprint { - public static final field INSTANCE Lapp/revanced/patches/youtube/general/toolbar/fingerprints/SearchResultFingerprint; -} - -public final class app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public final fun getAppIcon ()Lapp/revanced/patcher/patch/options/PatchOption; -} - -public final class app/revanced/patches/youtube/layout/branding/name/CustomBrandingNamePatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/branding/name/CustomBrandingNamePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/dimming/ShortsDimmingPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/dimming/ShortsDimmingPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/doubletaplength/DoubleTapLengthPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/doubletaplength/DoubleTapLengthPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/playerbuttonbg/PlayerButtonBackgroundPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/playerbuttonbg/PlayerButtonBackgroundPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/shortcut/ShortcutPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/shortcut/ShortcutPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/theme/BaseThemePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/theme/BaseThemePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/theme/MaterialYouPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/theme/MaterialYouPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/theme/ThemePatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/theme/ThemePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/translations/TranslationsPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/translations/TranslationsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/visual/VisualPreferencesIconsPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/visual/VisualPreferencesIconsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/codecs/OpusCodecBytecodePatch : app/revanced/patches/shared/opus/BaseOpusCodecsPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/codecs/OpusCodecBytecodePatch; -} - -public final class app/revanced/patches/youtube/misc/codecs/OpusCodecPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/codecs/OpusCodecPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/debugging/DebuggingPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/debugging/DebuggingPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/externalbrowser/OpenLinksExternallyBytecodePatch : app/revanced/patches/shared/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/externalbrowser/OpenLinksExternallyBytecodePatch; - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Pair; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Pair;)V -} - -public final class app/revanced/patches/youtube/misc/externalbrowser/OpenLinksExternallyPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/externalbrowser/OpenLinksExternallyPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/openlinksdirectly/OpenLinksDirectlyPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/openlinksdirectly/OpenLinksDirectlyPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/openlinksdirectly/fingerprints/OpenLinksDirectlyFingerprintPrimary : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/openlinksdirectly/fingerprints/OpenLinksDirectlyFingerprintPrimary; -} - -public final class app/revanced/patches/youtube/misc/openlinksdirectly/fingerprints/OpenLinksDirectlyFingerprintSecondary : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/openlinksdirectly/fingerprints/OpenLinksDirectlyFingerprintSecondary; -} - -public final class app/revanced/patches/youtube/misc/quic/QUICProtocolPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/quic/QUICProtocolPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/share/ShareSheetPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/share/ShareSheetPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/tracking/SanitizeUrlQueryPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/tracking/SanitizeUrlQueryPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/watchhistory/WatchHistoryPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/watchhistory/WatchHistoryPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/player/action/ActionButtonsPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/player/action/ActionButtonsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/player/ambientmode/AmbientModeSwitchPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/player/ambientmode/AmbientModeSwitchPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/player/buttons/PlayerButtonsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/player/buttons/PlayerButtonsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/player/comments/CommentsComponentPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/player/comments/CommentsComponentPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/player/components/PlayerComponentsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/player/components/PlayerComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/player/descriptions/DescriptionComponentsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/player/descriptions/DescriptionComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/player/flyoutmenu/hide/PlayerFlyoutMenuPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/player/flyoutmenu/hide/PlayerFlyoutMenuPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/player/flyoutmenu/toggle/ChangeTogglePatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/player/flyoutmenu/toggle/ChangeTogglePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/player/hapticfeedback/HapticFeedBackPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/player/hapticfeedback/HapticFeedBackPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/player/speedoverlay/SpeedOverlayPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/player/speedoverlay/SpeedOverlayPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/shorts/components/ShortsAnimationPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/shorts/components/ShortsAnimationPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/shorts/components/ShortsComponentPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/shorts/components/ShortsComponentPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/shorts/components/ShortsNavigationBarPatch : app/revanced/util/patch/MultiMethodBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/shorts/components/ShortsNavigationBarPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/shorts/components/ShortsRepeatPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/shorts/components/ShortsRepeatPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/shorts/components/ShortsTimeStampPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/shorts/components/ShortsTimeStampPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/shorts/components/ShortsToolBarPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/shorts/components/ShortsToolBarPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/shorts/startupshortsreset/ResumingShortsOnStartupPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/shorts/startupshortsreset/ResumingShortsOnStartupPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/swipe/controls/SwipeControlsPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/swipe/controls/SwipeControlsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/bottomsheet/BottomSheetHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/bottomsheet/BottomSheetHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/castbutton/CastButtonPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/castbutton/CastButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/compatibility/Constants { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/compatibility/Constants; - public final fun getCOMPATIBLE_PACKAGE ()Ljava/util/Set; -} - -public final class app/revanced/patches/youtube/utils/controlsoverlay/ControlsOverlayConfigPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/controlsoverlay/ControlsOverlayConfigPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/fix/bottomui/CfBottomUIPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/fix/bottomui/CfBottomUIPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/fix/cairo/CairoSettingsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/fix/cairo/CairoSettingsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/fix/doublebacktoclose/DoubleBackToClosePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/fix/doublebacktoclose/DoubleBackToClosePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/fix/shortsplayback/ShortsPlaybackPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/fix/shortsplayback/ShortsPlaybackPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/fix/streamingdata/SpoofUserAgentPatch : app/revanced/patches/shared/spoofuseragent/BaseSpoofUserAgentPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/fix/streamingdata/SpoofUserAgentPatch; -} - -public final class app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/SuggestedVideoEndScreenPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/SuggestedVideoEndScreenPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/fix/swiperefresh/SwipeRefreshPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/fix/swiperefresh/SwipeRefreshPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/flyoutmenu/FlyoutMenuHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/flyoutmenu/FlyoutMenuHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/gms/GmsCoreSupportPatch : app/revanced/patches/shared/gms/BaseGmsCoreSupportPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/gms/GmsCoreSupportPatch; -} - -public final class app/revanced/patches/youtube/utils/gms/GmsCoreSupportResourcePatch : app/revanced/patches/shared/gms/BaseGmsCoreSupportResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/gms/GmsCoreSupportResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/utils/imageurlhook/CronetImageUrlHookPatch : app/revanced/patches/shared/imageurlhook/BaseCronetImageUrlHookPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/imageurlhook/CronetImageUrlHookPatch; -} - -public final class app/revanced/patches/youtube/utils/integrations/Constants { - public static final field ADS_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field ADS_PATH Ljava/lang/String; - public static final field ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field ALTERNATIVE_THUMBNAILS_PATH Ljava/lang/String; - public static final field COMPONENTS_PATH Ljava/lang/String; - public static final field FEED_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field FEED_PATH Ljava/lang/String; - public static final field GENERAL_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field GENERAL_PATH Ljava/lang/String; - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/integrations/Constants; - public static final field INTEGRATIONS_PATH Ljava/lang/String; - public static final field MISC_PATH Ljava/lang/String; - public static final field OVERLAY_BUTTONS_PATH Ljava/lang/String; - public static final field PATCHES_PATH Ljava/lang/String; - public static final field PATCH_STATUS_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field PLAYER_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field PLAYER_PATH Ljava/lang/String; - public static final field SHARED_PATH Ljava/lang/String; - public static final field SHORTS_CLASS_DESCRIPTOR Ljava/lang/String; - public static final field SHORTS_PATH Ljava/lang/String; - public static final field SPANS_PATH Ljava/lang/String; - public static final field SWIPE_PATH Ljava/lang/String; - public static final field UTILS_PATH Ljava/lang/String; - public static final field VIDEO_PATH Ljava/lang/String; -} - -public final class app/revanced/patches/youtube/utils/integrations/IntegrationsPatch : app/revanced/patches/shared/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/youtube/utils/lockmodestate/LockModeStateHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/lockmodestate/LockModeStateHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/lottie/LottieAnimationViewHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/lottie/LottieAnimationViewHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/mainactivity/MainActivityResolvePatch : app/revanced/patches/shared/mainactivity/BaseMainActivityResolvePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/mainactivity/MainActivityResolvePatch; -} - -public final class app/revanced/patches/youtube/utils/navigation/NavigationBarHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/navigation/NavigationBarHookPatch; - public final fun addBottomBarContainerHook (Ljava/lang/String;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun getHookNavigationButtonCreated ()Lkotlin/jvm/functions/Function1; -} - -public final class app/revanced/patches/youtube/utils/pip/PiPStateHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/pip/PiPStateHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/playercontrols/PlayerControlsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/playercontrols/PlayerControlsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/playercontrols/PlayerControlsVisibilityHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/playercontrols/PlayerControlsVisibilityHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/recyclerview/BottomSheetRecyclerViewPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/recyclerview/BottomSheetRecyclerViewPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public final fun getAccountSwitcherAccessibility ()J - public final fun getActionBarRingo ()J - public final fun getActionBarRingoBackground ()J - public final fun getActionBarSearchResultsViewMic ()J - public final fun getAdAttribution ()J - public final fun getAppRelatedEndScreenResults ()J - public final fun getAppearance ()J - public final fun getAutoNavPreviewStub ()J - public final fun getAutoNavToggle ()J - public final fun getBackgroundCategory ()J - public final fun getBadgeLabel ()J - public final fun getBar ()J - public final fun getBarContainerHeight ()J - public final fun getBottomBarContainer ()J - public final fun getBottomSheetFooterText ()J - public final fun getBottomSheetRecyclerView ()J - public final fun getBottomUiContainerStub ()J - public final fun getCaptionToggleContainer ()J - public final fun getCastMediaRouteButton ()J - public final fun getCfFullscreenButton ()J - public final fun getChannelListSubMenu ()J - public final fun getCompactLink ()J - public final fun getCompactListItem ()J - public final fun getComponentLongClickListener ()J - public final fun getContentPill ()J - public final fun getControlsLayoutStub ()J - public final fun getDarkBackground ()J - public final fun getDarkSplashAnimation ()J - public final fun getDesignBottomSheet ()J - public final fun getDonationCompanion ()J - public final fun getDrawerContentView ()J - public final fun getDrawerResults ()J - public final fun getEasySeekEduContainer ()J - public final fun getEditSettingsAction ()J - public final fun getEmojiPickerIcon ()J - public final fun getEndScreenElementLayoutCircle ()J - public final fun getEndScreenElementLayoutIcon ()J - public final fun getEndScreenElementLayoutVideo ()J - public final fun getExpandButtonDown ()J - public final fun getFab ()J - public final fun getFadeDurationFast ()J - public final fun getFilterBarHeight ()J - public final fun getFloatyBarTopMargin ()J - public final fun getFullScreenButton ()J - public final fun getFullScreenEngagementOverlay ()J - public final fun getFullScreenEngagementPanel ()J - public final fun getHorizontalCardList ()J - public final fun getImageOnlyTab ()J - public final fun getInlineTimeBarColorizedBarPlayedColorDark ()J - public final fun getInlineTimeBarPlayedNotHighlightedColor ()J - public final fun getInsetOverlayViewLayout ()J - public final fun getInterstitialsContainer ()J - public final fun getMenuItemView ()J - public final fun getMetaPanel ()J - public final fun getModernMiniPlayerClose ()J - public final fun getModernMiniPlayerExpand ()J - public final fun getModernMiniPlayerForwardButton ()J - public final fun getModernMiniPlayerRewindButton ()J - public final fun getMusicAppDeeplinkButtonView ()J - public final fun getNotice ()J - public final fun getNotificationBigPictureIconWidth ()J - public final fun getOfflineActionsVideoDeletedUndoSnackbarText ()J - public final fun getPlayerCollapseButton ()J - public final fun getPlayerVideoTitleView ()J - public final fun getPosterArtWidthDefault ()J - public final fun getQualityAuto ()J - public final fun getQuickActionsElementContainer ()J - public final fun getReelDynRemix ()J - public final fun getReelDynShare ()J - public final fun getReelFeedbackLike ()J - public final fun getReelFeedbackPause ()J - public final fun getReelFeedbackPlay ()J - public final fun getReelForcedMuteButton ()J - public final fun getReelPlayerFooter ()J - public final fun getReelPlayerRightPivotV2Size ()J - public final fun getReelRightDislikeIcon ()J - public final fun getReelRightLikeIcon ()J - public final fun getReelTimeBarPlayedColor ()J - public final fun getReelVodTimeStampsContainer ()J - public final fun getReelWatchPlayer ()J - public final fun getRelatedChipCloudMargin ()J - public final fun getRightComment ()J - public final fun getScrimOverlay ()J - public final fun getScrubbing ()J - public final fun getSeekEasyHorizontalTouchOffsetToStartScrubbing ()J - public final fun getSeekUndoEduOverlayStub ()J - public final fun getSlidingDialogAnimation ()J - public final fun getSubtitleMenuSettingsFooterInfo ()J - public final fun getSuggestedAction ()J - public final fun getTapBloomView ()J - public final fun getTitleAnchor ()J - public final fun getToolTipContentView ()J - public final fun getTotalTime ()J - public final fun getTouchArea ()J - public final fun getVarispeedUnavailableTitle ()J - public final fun getVideoQualityBottomSheet ()J - public final fun getVideoQualityUnavailableAnnouncement ()J - public final fun getVideoZoomSnapIndicator ()J - public final fun getVoiceSearch ()J - public final fun getYouTubeControlsOverlaySubtitleButton ()J - public final fun getYouTubeLogo ()J - public final fun getYtOutlinePictureInPictureWhite ()J - public final fun getYtOutlineVideoCamera ()J - public final fun getYtOutlineXWhite ()J - public final fun getYtPremiumWordMarkHeader ()J - public final fun getYtWordMarkHeader ()J - public final fun setAccountSwitcherAccessibility (J)V - public final fun setActionBarRingo (J)V - public final fun setActionBarRingoBackground (J)V - public final fun setActionBarSearchResultsViewMic (J)V - public final fun setAdAttribution (J)V - public final fun setAppRelatedEndScreenResults (J)V - public final fun setAppearance (J)V - public final fun setAutoNavPreviewStub (J)V - public final fun setAutoNavToggle (J)V - public final fun setBackgroundCategory (J)V - public final fun setBadgeLabel (J)V - public final fun setBar (J)V - public final fun setBarContainerHeight (J)V - public final fun setBottomBarContainer (J)V - public final fun setBottomSheetFooterText (J)V - public final fun setBottomSheetRecyclerView (J)V - public final fun setBottomUiContainerStub (J)V - public final fun setCaptionToggleContainer (J)V - public final fun setCastMediaRouteButton (J)V - public final fun setCfFullscreenButton (J)V - public final fun setChannelListSubMenu (J)V - public final fun setCompactLink (J)V - public final fun setCompactListItem (J)V - public final fun setComponentLongClickListener (J)V - public final fun setContentPill (J)V - public final fun setControlsLayoutStub (J)V - public final fun setDarkBackground (J)V - public final fun setDarkSplashAnimation (J)V - public final fun setDesignBottomSheet (J)V - public final fun setDonationCompanion (J)V - public final fun setDrawerContentView (J)V - public final fun setDrawerResults (J)V - public final fun setEasySeekEduContainer (J)V - public final fun setEditSettingsAction (J)V - public final fun setEmojiPickerIcon (J)V - public final fun setEndScreenElementLayoutCircle (J)V - public final fun setEndScreenElementLayoutIcon (J)V - public final fun setEndScreenElementLayoutVideo (J)V - public final fun setExpandButtonDown (J)V - public final fun setFab (J)V - public final fun setFadeDurationFast (J)V - public final fun setFilterBarHeight (J)V - public final fun setFloatyBarTopMargin (J)V - public final fun setFullScreenButton (J)V - public final fun setFullScreenEngagementOverlay (J)V - public final fun setFullScreenEngagementPanel (J)V - public final fun setHorizontalCardList (J)V - public final fun setImageOnlyTab (J)V - public final fun setInlineTimeBarColorizedBarPlayedColorDark (J)V - public final fun setInlineTimeBarPlayedNotHighlightedColor (J)V - public final fun setInsetOverlayViewLayout (J)V - public final fun setInterstitialsContainer (J)V - public final fun setMenuItemView (J)V - public final fun setMetaPanel (J)V - public final fun setModernMiniPlayerClose (J)V - public final fun setModernMiniPlayerExpand (J)V - public final fun setModernMiniPlayerForwardButton (J)V - public final fun setModernMiniPlayerRewindButton (J)V - public final fun setMusicAppDeeplinkButtonView (J)V - public final fun setNotice (J)V - public final fun setNotificationBigPictureIconWidth (J)V - public final fun setOfflineActionsVideoDeletedUndoSnackbarText (J)V - public final fun setPlayerCollapseButton (J)V - public final fun setPlayerVideoTitleView (J)V - public final fun setPosterArtWidthDefault (J)V - public final fun setQualityAuto (J)V - public final fun setQuickActionsElementContainer (J)V - public final fun setReelDynRemix (J)V - public final fun setReelDynShare (J)V - public final fun setReelFeedbackLike (J)V - public final fun setReelFeedbackPause (J)V - public final fun setReelFeedbackPlay (J)V - public final fun setReelForcedMuteButton (J)V - public final fun setReelPlayerFooter (J)V - public final fun setReelPlayerRightPivotV2Size (J)V - public final fun setReelRightDislikeIcon (J)V - public final fun setReelRightLikeIcon (J)V - public final fun setReelTimeBarPlayedColor (J)V - public final fun setReelVodTimeStampsContainer (J)V - public final fun setReelWatchPlayer (J)V - public final fun setRelatedChipCloudMargin (J)V - public final fun setRightComment (J)V - public final fun setScrimOverlay (J)V - public final fun setScrubbing (J)V - public final fun setSeekEasyHorizontalTouchOffsetToStartScrubbing (J)V - public final fun setSeekUndoEduOverlayStub (J)V - public final fun setSlidingDialogAnimation (J)V - public final fun setSubtitleMenuSettingsFooterInfo (J)V - public final fun setSuggestedAction (J)V - public final fun setTapBloomView (J)V - public final fun setTitleAnchor (J)V - public final fun setToolTipContentView (J)V - public final fun setTotalTime (J)V - public final fun setTouchArea (J)V - public final fun setVarispeedUnavailableTitle (J)V - public final fun setVideoQualityBottomSheet (J)V - public final fun setVideoQualityUnavailableAnnouncement (J)V - public final fun setVideoZoomSnapIndicator (J)V - public final fun setVoiceSearch (J)V - public final fun setYouTubeControlsOverlaySubtitleButton (J)V - public final fun setYouTubeLogo (J)V - public final fun setYtOutlinePictureInPictureWhite (J)V - public final fun setYtOutlineVideoCamera (J)V - public final fun setYtOutlineXWhite (J)V - public final fun setYtPremiumWordMarkHeader (J)V - public final fun setYtWordMarkHeader (J)V -} - -public final class app/revanced/patches/youtube/utils/returnyoutubedislike/general/ReturnYouTubeDislikePatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/returnyoutubedislike/general/ReturnYouTubeDislikePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/returnyoutubedislike/rollingnumber/ReturnYouTubeDislikeRollingNumberPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/returnyoutubedislike/rollingnumber/ReturnYouTubeDislikeRollingNumberPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/returnyoutubedislike/shorts/ReturnYouTubeDislikeShortsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/returnyoutubedislike/shorts/ReturnYouTubeDislikeShortsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/returnyoutubeusername/ReturnYouTubeUsernamePatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/returnyoutubeusername/ReturnYouTubeUsernamePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/settings/ResourceUtils { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/settings/ResourceUtils; - public static final field TARGET_PREFERENCE_PATH Ljava/lang/String; - public static final field YOUTUBE_SETTINGS_PATH Ljava/lang/String; - public final fun addPreference (Lapp/revanced/patcher/data/ResourceContext;[Ljava/lang/String;)V - public final fun addPreferenceFragment (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/lang/String;)V - public final fun getIconType ()Ljava/lang/String; - public final fun getYoutubePackageName ()Ljava/lang/String; - public final fun setYoutubePackageName (Ljava/lang/String;)V - public final fun updateGmsCorePackageName (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/lang/String;)V - public final fun updatePackageName (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/lang/String;)V - public final fun updatePatchStatus (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;)V - public final fun updatePatchStatusIcon (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;)V - public final fun updatePatchStatusLabel (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;)V - public final fun updatePatchStatusSettings (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/lang/String;)V - public final fun updatePatchStatusTheme (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;)V -} - -public final class app/revanced/patches/youtube/utils/settings/SettingsBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/settings/SettingsBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/settings/SettingsPatch : app/revanced/util/patch/BaseResourcePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/settings/SettingsPatch; - public fun close ()V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/utils/sponsorblock/SponsorBlockBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/sponsorblock/SponsorBlockBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/sponsorblock/SponsorBlockPatch : app/revanced/util/patch/BaseResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/sponsorblock/SponsorBlockPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/utils/toolbar/ToolBarHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/toolbar/ToolBarHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/utils/trackingurlhook/TrackingUrlHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/utils/trackingurlhook/TrackingUrlHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/information/VideoInformationPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/information/VideoInformationPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/information/fingerprints/PlayerControllerSetTimeReferenceFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/information/fingerprints/PlayerControllerSetTimeReferenceFingerprint; -} - -public final class app/revanced/patches/youtube/video/playback/CustomPlaybackSpeedPatch : app/revanced/patches/shared/customspeed/BaseCustomPlaybackSpeedPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/playback/CustomPlaybackSpeedPatch; -} - -public final class app/revanced/patches/youtube/video/playback/VideoPlaybackPatch : app/revanced/util/patch/BaseBytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/playback/VideoPlaybackPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch : app/revanced/patcher/patch/BytecodePatch, java/io/Closeable, java/util/Set, kotlin/jvm/internal/markers/KMutableSet { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch; - public fun add (Lapp/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch$Hook;)Z - public synthetic fun add (Ljava/lang/Object;)Z - public fun addAll (Ljava/util/Collection;)Z - public fun clear ()V - public fun close ()V - public fun contains (Lapp/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch$Hook;)Z - public final fun contains (Ljava/lang/Object;)Z - public fun containsAll (Ljava/util/Collection;)Z - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun getSize ()I - public fun isEmpty ()Z - public fun iterator ()Ljava/util/Iterator; - public fun remove (Lapp/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch$Hook;)Z - public final fun remove (Ljava/lang/Object;)Z - public fun removeAll (Ljava/util/Collection;)Z - public fun retainAll (Ljava/util/Collection;)Z - public final fun size ()I - public fun toArray ()[Ljava/lang/Object; - public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; -} - -public final class app/revanced/patches/youtube/video/videoid/VideoIdPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/videoid/VideoIdPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun hookPlayerResponseVideoId (Ljava/lang/String;)V - public final fun hookVideoId (Ljava/lang/String;)V -} - -public final class app/revanced/util/BytecodeUtilsKt { - public static final field REGISTER_TEMPLATE_REPLACEMENT Ljava/lang/String; - public static final fun addStaticFieldToIntegration (Lapp/revanced/patcher/data/BytecodeContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V - public static synthetic fun addStaticFieldToIntegration$default (Lapp/revanced/patcher/data/BytecodeContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)V - public static final fun alsoResolve (Lapp/revanced/patcher/fingerprint/MethodFingerprint;Lapp/revanced/patcher/data/BytecodeContext;Lapp/revanced/patcher/fingerprint/MethodFingerprint;)Lapp/revanced/patcher/fingerprint/MethodFingerprintResult; - public static final fun cloneMutable (Lcom/android/tools/smali/dexlib2/iface/Method;IZLjava/lang/String;ILjava/util/List;Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; - public static synthetic fun cloneMutable$default (Lcom/android/tools/smali/dexlib2/iface/Method;IZLjava/lang/String;ILjava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; - public static final fun containsWideLiteralInstructionValue (Lcom/android/tools/smali/dexlib2/iface/Method;J)Z - public static final fun findMethodOrThrow (Lapp/revanced/patcher/data/BytecodeContext;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; - public static synthetic fun findMethodOrThrow$default (Lapp/revanced/patcher/data/BytecodeContext;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; - public static final fun findMethodsOrThrow (Lapp/revanced/patcher/data/BytecodeContext;Ljava/lang/String;)Ljava/util/Set; - public static final fun findMutableMethodOf (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; - public static final fun findOpcodeIndicesReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)Ljava/util/List; - public static final fun findOpcodeIndicesReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Lkotlin/jvm/functions/Function1;)Ljava/util/List; - public static final fun getException (Lapp/revanced/patcher/fingerprint/MethodFingerprint;)Lapp/revanced/patcher/patch/PatchException; - public static final fun getException (Lapp/revanced/util/fingerprint/MultiMethodFingerprint;)Lapp/revanced/patcher/patch/PatchException; - public static final fun getFiveRegisters (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)Ljava/lang/String; - public static final fun getMethodCall (Lapp/revanced/patcher/fingerprint/MethodFingerprint;)Ljava/lang/String; - public static final fun getMethodCall (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)Ljava/lang/String; - public static final fun getWalkerMethod (Lapp/revanced/patcher/fingerprint/MethodFingerprintResult;Lapp/revanced/patcher/data/BytecodeContext;I)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; - public static final fun getWalkerMethod (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lapp/revanced/patcher/data/BytecodeContext;I)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; - public static final fun indexOfFirstInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;ILcom/android/tools/smali/dexlib2/Opcode;)I - public static final fun indexOfFirstInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;ILkotlin/jvm/functions/Function1;)I - public static synthetic fun indexOfFirstInstruction$default (Lcom/android/tools/smali/dexlib2/iface/Method;ILcom/android/tools/smali/dexlib2/Opcode;ILjava/lang/Object;)I - public static synthetic fun indexOfFirstInstruction$default (Lcom/android/tools/smali/dexlib2/iface/Method;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)I - public static final fun indexOfFirstInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;ILcom/android/tools/smali/dexlib2/Opcode;)I - public static final fun indexOfFirstInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;ILkotlin/jvm/functions/Function1;)I - public static final fun indexOfFirstInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)I - public static synthetic fun indexOfFirstInstructionOrThrow$default (Lcom/android/tools/smali/dexlib2/iface/Method;ILcom/android/tools/smali/dexlib2/Opcode;ILjava/lang/Object;)I - public static synthetic fun indexOfFirstInstructionOrThrow$default (Lcom/android/tools/smali/dexlib2/iface/Method;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)I - public static final fun indexOfFirstInstructionReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lcom/android/tools/smali/dexlib2/Opcode;)I - public static final fun indexOfFirstInstructionReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)I - public static synthetic fun indexOfFirstInstructionReversed$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lcom/android/tools/smali/dexlib2/Opcode;ILjava/lang/Object;)I - public static synthetic fun indexOfFirstInstructionReversed$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)I - public static final fun indexOfFirstInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)I - public static final fun indexOfFirstInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lcom/android/tools/smali/dexlib2/Opcode;)I - public static final fun indexOfFirstInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)I - public static synthetic fun indexOfFirstInstructionReversedOrThrow$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lcom/android/tools/smali/dexlib2/Opcode;ILjava/lang/Object;)I - public static synthetic fun indexOfFirstInstructionReversedOrThrow$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)I - public static final fun indexOfFirstStringInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;)I - public static final fun indexOfFirstStringInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;)I - public static final fun indexOfFirstWideLiteralInstructionValue (Lcom/android/tools/smali/dexlib2/iface/Method;J)I - public static final fun indexOfFirstWideLiteralInstructionValueOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;J)I - public static final fun injectHideViewCall (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;IILjava/lang/String;Ljava/lang/String;)V - public static final fun injectLiteralInstructionBooleanCall (Lapp/revanced/patcher/fingerprint/MethodFingerprint;ILjava/lang/String;)V - public static final fun injectLiteralInstructionBooleanCall (Lapp/revanced/patcher/fingerprint/MethodFingerprint;JLjava/lang/String;)V - public static final fun injectLiteralInstructionViewCall (Lapp/revanced/patcher/data/BytecodeContext;JLjava/lang/String;)V - public static final fun injectLiteralInstructionViewCall (Lapp/revanced/patcher/fingerprint/MethodFingerprint;JLjava/lang/String;)V - public static final fun injectLiteralInstructionViewCall (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;JLjava/lang/String;)V - public static final fun isDeprecated (Lapp/revanced/patcher/fingerprint/MethodFingerprint;)Z - public static final fun parametersEqual (Ljava/lang/Iterable;Ljava/lang/Iterable;)Z - public static final fun replaceLiteralInstructionCall (Lapp/revanced/patcher/data/BytecodeContext;JLjava/lang/String;)V - public static final fun resultOrThrow (Lapp/revanced/patcher/fingerprint/MethodFingerprint;)Lapp/revanced/patcher/fingerprint/MethodFingerprintResult; - public static final fun resultOrThrow (Lapp/revanced/util/fingerprint/MultiMethodFingerprint;)Ljava/util/List; - public static final fun returnEarly (Ljava/util/List;Z)V - public static synthetic fun returnEarly$default (Ljava/util/List;ZILjava/lang/Object;)V - public static final fun transformFields (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/data/BytecodeContext;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lkotlin/jvm/functions/Function1;)V - public static final fun updatePatchStatus (Lapp/revanced/patcher/data/BytecodeContext;Ljava/lang/String;Ljava/lang/String;)V -} - -public final class app/revanced/util/ResourceGroup { - public fun (Ljava/lang/String;[Ljava/lang/String;)V - public final fun getResourceDirectoryName ()Ljava/lang/String; - public final fun getResources ()[Ljava/lang/String; -} - -public final class app/revanced/util/ResourceUtilsKt { - public static final fun addEntryValues (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V - public static synthetic fun addEntryValues$default (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)V - public static final fun adoptChild (Lorg/w3c/dom/Node;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V - public static final fun appendAppVersion (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;)V - public static final fun cloneNodes (Lorg/w3c/dom/Node;Lorg/w3c/dom/Node;)V - public static final fun copyFile (Lapp/revanced/patcher/data/ResourceContext;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)Z - public static final fun copyResources (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;[Lapp/revanced/util/ResourceGroup;Z)V - public static synthetic fun copyResources$default (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;[Lapp/revanced/util/ResourceGroup;ZILjava/lang/Object;)V - public static final fun copyResourcesWithRename (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/util/Map;)V - public static final fun copyXmlNode (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lkotlin/Unit; - public static final fun copyXmlNode (Ljava/lang/String;Lapp/revanced/patcher/util/DomFileEditor;Lapp/revanced/patcher/util/DomFileEditor;)Ljava/lang/AutoCloseable; - public static final fun doRecursively (Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V - public static final fun getClassLoader ()Ljava/lang/ClassLoader; - public static final fun getResourceGroup (Ljava/util/List;[Ljava/lang/String;)Ljava/util/List; - public static final fun insertNode (Lorg/w3c/dom/Node;Ljava/lang/String;Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V - public static final fun lowerCaseOrThrow (Lapp/revanced/patcher/patch/options/PatchOption;)Ljava/lang/String; - public static final fun startsWithAny (Ljava/lang/String;[Ljava/lang/String;)Z - public static final fun underBarOrThrow (Lapp/revanced/patcher/patch/options/PatchOption;)Ljava/lang/String; - public static final fun updatePathData (Lorg/w3c/dom/Document;Ljava/lang/String;)V - public static final fun valueOrThrow (Lapp/revanced/patcher/patch/options/PatchOption;)I - public static final fun valueOrThrow (Lapp/revanced/patcher/patch/options/PatchOption;)Ljava/lang/String; -} - -public abstract class app/revanced/util/fingerprint/LiteralValueFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function0;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public abstract class app/revanced/util/fingerprint/MultiMethodFingerprint { - public static final field Companion Lapp/revanced/util/fingerprint/MultiMethodFingerprint$Companion; - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getAccessFlags ()Ljava/lang/Integer; - public final fun getCustomFingerprint ()Lkotlin/jvm/functions/Function2; - public final fun getOpcodes ()Ljava/lang/Iterable; - public final fun getParameters ()Ljava/lang/Iterable; - public final fun getResult ()Ljava/util/List; - public final fun getReturnType ()Ljava/lang/String; - public final fun getStrings ()Ljava/lang/Iterable; - public final fun setResult (Ljava/util/List;)V -} - -public final class app/revanced/util/fingerprint/MultiMethodFingerprint$Companion { - public final fun resolve (Lapp/revanced/util/fingerprint/MultiMethodFingerprint;Lapp/revanced/patcher/data/BytecodeContext;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z - public final fun resolve (Lapp/revanced/util/fingerprint/MultiMethodFingerprint;Lapp/revanced/patcher/data/BytecodeContext;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/ClassDef;)Z - public final fun resolve (Ljava/lang/Iterable;Lapp/revanced/patcher/data/BytecodeContext;Ljava/lang/Iterable;)V -} - -public abstract class app/revanced/util/patch/BaseBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;ZZ)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public abstract class app/revanced/util/patch/BaseResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZ)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public abstract class app/revanced/util/patch/MultiMethodBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public fun ()V - public fun (Ljava/util/Set;Ljava/util/Set;)V - public synthetic fun (Ljava/util/Set;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun getFingerprints ()Ljava/util/Set; - public final fun getMultiFingerprints ()Ljava/util/Set; -} - diff --git a/build.gradle.kts b/build.gradle.kts deleted file mode 100644 index a98b4d837..000000000 --- a/build.gradle.kts +++ /dev/null @@ -1,146 +0,0 @@ -import org.gradle.kotlin.dsl.support.listFilesOrdered - -plugins { - alias(libs.plugins.kotlin) - alias(libs.plugins.binary.compatibility.validator) - `maven-publish` - signing -} - -group = "app.revanced" - -repositories { - mavenCentral() - mavenLocal() - google() - maven { - // A repository must be speficied for some reason. "registry" is a dummy. - url = uri("https://maven.pkg.github.com/revanced/registry") - credentials { - username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") - password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") - } - } -} - -dependencies { - implementation(libs.revanced.patcher) - implementation(libs.smali) - // TODO: Required because build fails without it. Find a way to remove this dependency. - implementation(libs.guava) - // Used in JsonGenerator. - implementation(libs.gson) -} - -kotlin { - jvmToolchain(11) -} - -tasks { - withType(Jar::class) { - exclude("app/revanced/meta") - - manifest { - attributes["Name"] = "ReVanced Patches" - attributes["Description"] = "Patches for ReVanced." - attributes["Version"] = version - attributes["Timestamp"] = System.currentTimeMillis().toString() - attributes["Source"] = "git@github.com:revanced/revanced-patches.git" - attributes["Author"] = "ReVanced" - attributes["Contact"] = "contact@revanced.app" - attributes["Origin"] = "https://revanced.app" - attributes["License"] = "GNU General Public License v3.0" - } - } - - register("buildDexJar") { - description = "Build and add a DEX to the JAR file" - group = "build" - - dependsOn(build) - - doLast { - val d8 = File(System.getenv("ANDROID_HOME")).resolve("build-tools") - .listFilesOrdered().last().resolve("d8").absolutePath - - val patchesJar = configurations.archives.get().allArtifacts.files.files.first().absolutePath - val workingDirectory = layout.buildDirectory.dir("libs").get().asFile - - exec { - workingDir = workingDirectory - commandLine = listOf(d8, "--release", patchesJar) - } - - exec { - workingDir = workingDirectory - commandLine = listOf("zip", "-u", patchesJar, "classes.dex") - } - } - } - - register("generatePatchesFiles") { - description = "Generate patches files" - - dependsOn(build) - - classpath = sourceSets["main"].runtimeClasspath - mainClass.set("app.revanced.generator.MainKt") - } - - // Needed by gradle-semantic-release-plugin. - // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 - publish { - dependsOn("buildDexJar") - dependsOn("generatePatchesFiles") - } -} - -publishing { - repositories { - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/anddea/revanced-patches") - credentials { - username = System.getenv("GITHUB_ACTOR") - password = System.getenv("GITHUB_TOKEN") - } - } - } - - publications { - create("revanced-patches-publication") { - from(components["java"]) - - pom { - name = "ReVanced Patches" - description = "Patches for ReVanced." - url = "https://revanced.app" - - licenses { - license { - name = "GNU General Public License v3.0" - url = "https://www.gnu.org/licenses/gpl-3.0.en.html" - } - } - developers { - developer { - id = "ReVanced" - name = "ReVanced" - email = "contact@revanced.app" - } - } - scm { - connection = "scm:git:git://github.com/revanced/revanced-patches.git" - developerConnection = "scm:git:git@github.com:revanced/revanced-patches.git" - url = "https://github.com/revanced/revanced-patches" - } - } - } - } -} - -signing { - useGpgCmd() - - sign(publishing.publications["revanced-patches-publication"]) -} diff --git a/extensions/shared/build.gradle.kts b/extensions/shared/build.gradle.kts new file mode 100644 index 000000000..90bd2ac9e --- /dev/null +++ b/extensions/shared/build.gradle.kts @@ -0,0 +1,30 @@ +extension { + name = "extensions/shared.rve" +} + +android { + namespace = "app.revanced.extension" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + } + + buildTypes { + release { + isMinifyEnabled = true + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} + +dependencies { + compileOnly(libs.annotation) + compileOnly(libs.preference) + implementation(libs.lang3) + + compileOnly(project(":extensions:shared:stub")) +} diff --git a/extensions/shared/proguard-rules.pro b/extensions/shared/proguard-rules.pro new file mode 100644 index 000000000..8f804140d --- /dev/null +++ b/extensions/shared/proguard-rules.pro @@ -0,0 +1,9 @@ +-dontobfuscate +-dontoptimize +-keepattributes * +-keep class app.revanced.** { + *; +} +-keep class com.google.** { + *; +} diff --git a/extensions/shared/src/main/AndroidManifest.xml b/extensions/shared/src/main/AndroidManifest.xml new file mode 100644 index 000000000..e960b0003 --- /dev/null +++ b/extensions/shared/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/account/AccountPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/account/AccountPatch.java new file mode 100644 index 000000000..5e5fb6a06 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/account/AccountPatch.java @@ -0,0 +1,66 @@ +package app.revanced.extension.music.patches.account; + +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.isSDKAbove; + +import android.view.View; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class AccountPatch { + + private static String[] accountMenuBlockList; + + static { + accountMenuBlockList = Settings.HIDE_ACCOUNT_MENU_FILTER_STRINGS.get().split("\\n"); + // Some settings should not be hidden. + if (isSDKAbove(24)) { + accountMenuBlockList = Arrays.stream(accountMenuBlockList) + .filter(item -> !Objects.equals(item, str("settings"))) + .toArray(String[]::new); + } else { + List tmp = new ArrayList<>(Arrays.asList(accountMenuBlockList)); + tmp.remove(str("settings")); // "Settings" should appear only once in the account menu + accountMenuBlockList = tmp.toArray(new String[0]); + } + } + + public static void hideAccountMenu(CharSequence charSequence, View view) { + if (!Settings.HIDE_ACCOUNT_MENU.get()) + return; + + if (charSequence == null) { + if (Settings.HIDE_ACCOUNT_MENU_EMPTY_COMPONENT.get()) + view.setVisibility(View.GONE); + + return; + } + + for (String filter : accountMenuBlockList) { + if (!filter.isEmpty() && charSequence.toString().equals(filter)) + view.setVisibility(View.GONE); + } + } + + public static boolean hideHandle(boolean original) { + return Settings.HIDE_HANDLE.get() || original; + } + + public static void hideHandle(TextView textView, int visibility) { + final int finalVisibility = Settings.HIDE_HANDLE.get() + ? View.GONE + : visibility; + textView.setVisibility(finalVisibility); + } + + public static int hideTermsContainer() { + return Settings.HIDE_TERMS_CONTAINER.get() ? View.GONE : View.VISIBLE; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/actionbar/ActionBarPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/actionbar/ActionBarPatch.java new file mode 100644 index 000000000..d973918ee --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/actionbar/ActionBarPatch.java @@ -0,0 +1,89 @@ +package app.revanced.extension.music.patches.actionbar; + +import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition; +import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition; + +import android.view.View; + +import androidx.annotation.NonNull; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.utils.VideoUtils; + +@SuppressWarnings("unused") +public class ActionBarPatch { + + @NonNull + private static String buttonType = ""; + + public static boolean hideActionBarLabel() { + return Settings.HIDE_ACTION_BUTTON_LABEL.get(); + } + + public static boolean hideActionButton() { + for (ActionButton actionButton : ActionButton.values()) + if (actionButton.enabled && actionButton.name.equals(buttonType)) + return true; + + return false; + } + + public static void hideLikeDislikeButton(View view) { + final boolean enabled = Settings.HIDE_ACTION_BUTTON_LIKE_DISLIKE.get(); + hideViewUnderCondition( + enabled, + view + ); + hideViewBy0dpUnderCondition( + enabled, + view + ); + } + + public static void inAppDownloadButtonOnClick(View view) { + if (!Settings.EXTERNAL_DOWNLOADER_ACTION_BUTTON.get()) { + return; + } + + if (buttonType.equals(ActionButton.DOWNLOAD.name)) + view.setOnClickListener(imageView -> VideoUtils.launchExternalDownloader()); + } + + public static void setButtonType(@NonNull Object obj) { + final String buttonType = obj.toString(); + + for (ActionButton actionButton : ActionButton.values()) + if (buttonType.contains(actionButton.identifier)) + setButtonType(actionButton.name); + } + + public static void setButtonType(@NonNull String newButtonType) { + buttonType = newButtonType; + } + + public static void setButtonTypeDownload(int type) { + if (type != 0) + return; + + setButtonType(ActionButton.DOWNLOAD.name); + } + + private enum ActionButton { + ADD_TO_PLAYLIST("ACTION_BUTTON_ADD_TO_PLAYLIST", "69487224", Settings.HIDE_ACTION_BUTTON_ADD_TO_PLAYLIST.get()), + COMMENT_DISABLED("ACTION_BUTTON_COMMENT", "76623563", Settings.HIDE_ACTION_BUTTON_COMMENT.get()), + COMMENT_ENABLED("ACTION_BUTTON_COMMENT", "138681778", Settings.HIDE_ACTION_BUTTON_COMMENT.get()), + DOWNLOAD("ACTION_BUTTON_DOWNLOAD", "73080600", Settings.HIDE_ACTION_BUTTON_DOWNLOAD.get()), + RADIO("ACTION_BUTTON_RADIO", "48687757", Settings.HIDE_ACTION_BUTTON_RADIO.get()), + SHARE("ACTION_BUTTON_SHARE", "90650344", Settings.HIDE_ACTION_BUTTON_SHARE.get()); + + private final String name; + private final String identifier; + private final boolean enabled; + + ActionButton(String name, String identifier, boolean enabled) { + this.name = name; + this.identifier = identifier; + this.enabled = enabled; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/ads/MusicAdsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/ads/MusicAdsPatch.java new file mode 100644 index 000000000..3cf53b27e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/ads/MusicAdsPatch.java @@ -0,0 +1,15 @@ +package app.revanced.extension.music.patches.ads; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class MusicAdsPatch { + + public static boolean hideMusicAds() { + return !Settings.HIDE_MUSIC_ADS.get(); + } + + public static boolean hideMusicAds(boolean original) { + return !Settings.HIDE_MUSIC_ADS.get() && original; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/ads/PremiumPromotionPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/ads/PremiumPromotionPatch.java new file mode 100644 index 000000000..7a863606f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/ads/PremiumPromotionPatch.java @@ -0,0 +1,40 @@ +package app.revanced.extension.music.patches.ads; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.utils.Logger; + +@SuppressWarnings("unused") +public class PremiumPromotionPatch { + + public static void hidePremiumPromotion(View view) { + if (!Settings.HIDE_PREMIUM_PROMOTION.get()) + return; + + view.getViewTreeObserver().addOnGlobalLayoutListener(() -> { + try { + if (!(view instanceof ViewGroup viewGroup)) { + return; + } + if (!(viewGroup.getChildAt(0) instanceof ViewGroup mealBarLayoutRoot)) { + return; + } + if (!(mealBarLayoutRoot.getChildAt(0) instanceof LinearLayout linearLayout)) { + return; + } + if (!(linearLayout.getChildAt(0) instanceof ImageView imageView)) { + return; + } + if (imageView.getVisibility() == View.VISIBLE) { + view.setVisibility(View.GONE); + } + } catch (Exception ex) { + Logger.printException(() -> "hideGetPremium failure", ex); + } + }); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/ads/PremiumRenewalPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/ads/PremiumRenewalPatch.java new file mode 100644 index 000000000..f5efd9c56 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/ads/PremiumRenewalPatch.java @@ -0,0 +1,39 @@ +package app.revanced.extension.music.patches.ads; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings("unused") +public class PremiumRenewalPatch { + + public static void hidePremiumRenewal(LinearLayout buttonContainerView) { + if (!Settings.HIDE_PREMIUM_RENEWAL.get()) + return; + + buttonContainerView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { + try { + Utils.runOnMainThreadDelayed(() -> { + if (!(buttonContainerView.getChildAt(0) instanceof ViewGroup closeButtonParentView)) + return; + if (!(closeButtonParentView.getChildAt(0) instanceof TextView closeButtonView)) + return; + if (closeButtonView.getText().toString().equals(str("dialog_got_it_text"))) + Utils.clickView(closeButtonView); + else + Utils.hideViewByLayoutParams((View) buttonContainerView.getParent()); + }, 0 + ); + } catch (Exception ex) { + Logger.printException(() -> "hidePremiumRenewal failure", ex); + } + }); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/ActionButtonsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/ActionButtonsFilter.java new file mode 100644 index 000000000..f0d636279 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/ActionButtonsFilter.java @@ -0,0 +1,89 @@ +package app.revanced.extension.music.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; + +@SuppressWarnings("unused") +public final class ActionButtonsFilter extends Filter { + private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.eml"; + + private final StringFilterGroup actionBarRule; + private final StringFilterGroup bufferFilterPathRule; + private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList(); + + public ActionButtonsFilter() { + actionBarRule = new StringFilterGroup( + null, + VIDEO_ACTION_BAR_PATH_PREFIX + ); + addIdentifierCallbacks(actionBarRule); + + bufferFilterPathRule = new StringFilterGroup( + null, + "|ContainerType|button.eml|" + ); + final StringFilterGroup downloadButton = new StringFilterGroup( + Settings.HIDE_ACTION_BUTTON_DOWNLOAD, + "music_download_button.eml" + ); + final StringFilterGroup likeDislikeContainer = new StringFilterGroup( + Settings.HIDE_ACTION_BUTTON_LIKE_DISLIKE, + "segmented_like_dislike_button.eml" + ); + addPathCallbacks( + bufferFilterPathRule, + downloadButton, + likeDislikeContainer + ); + + bufferButtonsGroupList.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_ACTION_BUTTON_COMMENT, + "yt_outline_message_bubble" + ), + new ByteArrayFilterGroup( + Settings.HIDE_ACTION_BUTTON_ADD_TO_PLAYLIST, + "yt_outline_list_add" + ), + new ByteArrayFilterGroup( + Settings.HIDE_ACTION_BUTTON_SHARE, + "yt_outline_share" + ), + new ByteArrayFilterGroup( + Settings.HIDE_ACTION_BUTTON_RADIO, + "yt_outline_youtube_mix" + ) + ); + } + + private boolean isEveryFilterGroupEnabled() { + for (StringFilterGroup group : pathCallbacks) + if (!group.isEnabled()) return false; + + for (ByteArrayFilterGroup group : bufferButtonsGroupList) + if (!group.isEnabled()) return false; + + return true; + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (!path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX)) { + return false; + } + if (matchedGroup == actionBarRule && !isEveryFilterGroupEnabled()) { + return false; + } + if (matchedGroup == bufferFilterPathRule && !bufferButtonsGroupList.check(protobufBufferArray).isFiltered()) { + return false; + } + + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/AdsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/AdsFilter.java new file mode 100644 index 000000000..de4c65985 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/AdsFilter.java @@ -0,0 +1,31 @@ +package app.revanced.extension.music.patches.components; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; + +@SuppressWarnings("unused") +public final class AdsFilter extends Filter { + + public AdsFilter() { + final StringFilterGroup alertBannerPromo = new StringFilterGroup( + Settings.HIDE_PROMOTION_ALERT_BANNER, + "alert_banner_promo.eml" + ); + + final StringFilterGroup paidPromotionLabel = new StringFilterGroup( + Settings.HIDE_PAID_PROMOTION_LABEL, + "music_paid_content_overlay.eml" + ); + + addIdentifierCallbacks(alertBannerPromo, paidPromotionLabel); + + final StringFilterGroup statementBanner = new StringFilterGroup( + Settings.HIDE_GENERAL_ADS, + "statement_banner" + ); + + addPathCallbacks(statementBanner); + + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/CustomFilter.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/CustomFilter.java new file mode 100644 index 000000000..b3c766133 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/CustomFilter.java @@ -0,0 +1,164 @@ +package app.revanced.extension.music.patches.components; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.utils.ByteTrieSearch; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +/** + * Allows custom filtering using a path and optionally a proto buffer string. + */ +@SuppressWarnings("unused") +public final class CustomFilter extends Filter { + + private static void showInvalidSyntaxToast(@NonNull String expression) { + Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression)); + } + + private static class CustomFilterGroup extends StringFilterGroup { + /** + * Optional character for the path that indicates the custom filter path must match the start. + * Must be the first character of the expression. + */ + public static final String SYNTAX_STARTS_WITH = "^"; + + /** + * Optional character that separates the path from a proto buffer string pattern. + */ + public static final String SYNTAX_BUFFER_SYMBOL = "$"; + + /** + * @return the parsed objects + */ + @NonNull + @SuppressWarnings("ConstantConditions") + static Collection parseCustomFilterGroups() { + String rawCustomFilterText = Settings.CUSTOM_FILTER_STRINGS.get(); + if (rawCustomFilterText.isBlank()) { + return Collections.emptyList(); + } + + // Map key is the path including optional special characters (^ and/or $) + Map result = new HashMap<>(); + Pattern pattern = Pattern.compile( + "(" // map key group + + "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)" // optional starts with + + "([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*)" // path + + "(\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E?)" // optional buffer symbol + + ")" // end map key group + + "(.*)"); // optional buffer string + + for (String expression : rawCustomFilterText.split("\n")) { + if (expression.isBlank()) continue; + + Matcher matcher = pattern.matcher(expression); + if (!matcher.find()) { + showInvalidSyntaxToast(expression); + continue; + } + + final String mapKey = matcher.group(1); + final boolean pathStartsWith = !matcher.group(2).isEmpty(); + final String path = matcher.group(3); + final boolean hasBufferSymbol = !matcher.group(4).isEmpty(); + final String bufferString = matcher.group(5); + + if (path.isBlank() || (hasBufferSymbol && bufferString.isBlank())) { + showInvalidSyntaxToast(expression); + continue; + } + + // Use one group object for all expressions with the same path. + // This ensures the buffer is searched exactly once + // when multiple paths are used with different buffer strings. + CustomFilterGroup group = result.get(mapKey); + if (group == null) { + group = new CustomFilterGroup(pathStartsWith, path); + result.put(mapKey, group); + } + if (hasBufferSymbol) { + group.addBufferString(bufferString); + } + } + + return result.values(); + } + + final boolean startsWith; + ByteTrieSearch bufferSearch; + + CustomFilterGroup(boolean startsWith, @NonNull String path) { + super(Settings.CUSTOM_FILTER, path); + this.startsWith = startsWith; + } + + void addBufferString(@NonNull String bufferString) { + if (bufferSearch == null) { + bufferSearch = new ByteTrieSearch(); + } + bufferSearch.addPattern(bufferString.getBytes()); + } + + @NonNull + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("CustomFilterGroup{"); + builder.append("path="); + if (startsWith) builder.append(SYNTAX_STARTS_WITH); + builder.append(filters[0]); + + if (bufferSearch != null) { + String delimitingCharacter = "❙"; + builder.append(", bufferStrings="); + builder.append(delimitingCharacter); + for (byte[] bufferString : bufferSearch.getPatterns()) { + builder.append(new String(bufferString)); + builder.append(delimitingCharacter); + } + } + builder.append("}"); + return builder.toString(); + } + } + + public CustomFilter() { + Collection groups = CustomFilterGroup.parseCustomFilterGroups(); + + if (!groups.isEmpty()) { + CustomFilterGroup[] groupsArray = groups.toArray(new CustomFilterGroup[0]); + Logger.printDebug(() -> "Using Custom filters: " + Arrays.toString(groupsArray)); + addPathCallbacks(groupsArray); + } + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + // All callbacks are custom filter groups. + CustomFilterGroup custom = (CustomFilterGroup) matchedGroup; + if (custom.startsWith && contentIndex != 0) { + return false; + } + if (custom.bufferSearch != null && !custom.bufferSearch.matches(protobufBufferArray)) { + return false; + } + + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/LayoutComponentsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/LayoutComponentsFilter.java new file mode 100644 index 000000000..672431969 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/LayoutComponentsFilter.java @@ -0,0 +1,39 @@ +package app.revanced.extension.music.patches.components; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; + +@SuppressWarnings("unused") +public final class LayoutComponentsFilter extends Filter { + + public LayoutComponentsFilter() { + + final StringFilterGroup buttonShelf = new StringFilterGroup( + Settings.HIDE_BUTTON_SHELF, + "entry_point_button_shelf.eml" + ); + + final StringFilterGroup carouselShelf = new StringFilterGroup( + Settings.HIDE_CAROUSEL_SHELF, + "music_grid_item_carousel.eml" + ); + + final StringFilterGroup playlistCardShelf = new StringFilterGroup( + Settings.HIDE_PLAYLIST_CARD_SHELF, + "music_container_card_shelf.eml" + ); + + final StringFilterGroup sampleShelf = new StringFilterGroup( + Settings.HIDE_SAMPLE_SHELF, + "immersive_card_shelf.eml" + ); + + addIdentifierCallbacks( + buttonShelf, + carouselShelf, + playlistCardShelf, + sampleShelf + ); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/PlayerComponentsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/PlayerComponentsFilter.java new file mode 100644 index 000000000..52056aecc --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/PlayerComponentsFilter.java @@ -0,0 +1,25 @@ +package app.revanced.extension.music.patches.components; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; + +@SuppressWarnings("unused") +public final class PlayerComponentsFilter extends Filter { + + public PlayerComponentsFilter() { + addIdentifierCallbacks( + new StringFilterGroup( + Settings.HIDE_COMMENT_CHANNEL_GUIDELINES, + "channel_guidelines_entry_banner.eml", + "community_guidelines.eml" + ) + ); + addPathCallbacks( + new StringFilterGroup( + Settings.HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS, + "|CellType|ContainerType|ContainerType|ContainerType|ContainerType|ContainerType|" + ) + ); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/PlayerFlyoutMenuFilter.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/PlayerFlyoutMenuFilter.java new file mode 100644 index 000000000..5f6701466 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/PlayerFlyoutMenuFilter.java @@ -0,0 +1,19 @@ +package app.revanced.extension.music.patches.components; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; + +@SuppressWarnings("unused") +public final class PlayerFlyoutMenuFilter extends Filter { + + public PlayerFlyoutMenuFilter() { + addIdentifierCallbacks( + new StringFilterGroup( + Settings.HIDE_FLYOUT_MENU_3_COLUMN_COMPONENT, + "music_highlight_menu_item_carousel.eml", + "tile_button_carousel.eml" + ) + ); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/ShareSheetMenuFilter.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/ShareSheetMenuFilter.java new file mode 100644 index 000000000..7c6d16817 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/components/ShareSheetMenuFilter.java @@ -0,0 +1,33 @@ +package app.revanced.extension.music.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.music.patches.misc.ShareSheetPatch; +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; + +/** + * Abuse LithoFilter for {@link ShareSheetPatch}. + */ +public final class ShareSheetMenuFilter extends Filter { + // Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread. + public static volatile boolean isShareSheetMenuVisible; + + public ShareSheetMenuFilter() { + addIdentifierCallbacks( + new StringFilterGroup( + Settings.CHANGE_SHARE_SHEET, + "share_sheet_container.eml" + ) + ); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + isShareSheetMenuVisible = true; + + return false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/flyout/FlyoutPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/flyout/FlyoutPatch.java new file mode 100644 index 000000000..d3b86723d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/flyout/FlyoutPatch.java @@ -0,0 +1,177 @@ +package app.revanced.extension.music.patches.flyout; + +import static app.revanced.extension.shared.utils.ResourceUtils.getIdentifier; +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.clickView; +import static app.revanced.extension.shared.utils.Utils.runOnMainThreadDelayed; + +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.shared.VideoType; +import app.revanced.extension.music.utils.VideoUtils; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.ResourceUtils.ResourceType; + +@SuppressWarnings("unused") +public class FlyoutPatch { + + public static int enableCompactDialog(int original) { + if (!Settings.ENABLE_COMPACT_DIALOG.get()) + return original; + + return Math.max(original, 600); + } + + public static boolean enableTrimSilence(boolean original) { + if (!Settings.ENABLE_TRIM_SILENCE.get()) + return original; + + return VideoType.getCurrent().isPodCast() || original; + } + + public static boolean enableTrimSilenceSwitch(boolean original) { + if (!Settings.ENABLE_TRIM_SILENCE.get()) + return original; + + return VideoType.getCurrent().isPodCast() && original; + } + + public static boolean hideComponents(@Nullable Enum flyoutMenuEnum) { + if (flyoutMenuEnum == null) + return false; + + final String flyoutMenuName = flyoutMenuEnum.name(); + + Logger.printDebug(() -> "flyoutMenu: " + flyoutMenuName); + + for (FlyoutPanelComponent component : FlyoutPanelComponent.values()) + if (component.name.equals(flyoutMenuName) && component.enabled) + return true; + + return false; + } + + public static void hideLikeDislikeContainer(View view) { + if (!Settings.HIDE_FLYOUT_MENU_LIKE_DISLIKE.get()) + return; + + if (view.getParent() instanceof ViewGroup viewGroup) { + viewGroup.removeView(view); + } + } + + private static volatile boolean lastMenuWasDismissQueue = false; + + private static WeakReference touchOutSideViewRef = new WeakReference<>(null); + + public static void setTouchOutSideView(View touchOutSideView) { + touchOutSideViewRef = new WeakReference<>(touchOutSideView); + } + + public static void replaceComponents(@Nullable Enum flyoutPanelEnum, @NonNull TextView textView, @NonNull ImageView imageView) { + if (flyoutPanelEnum == null) + return; + + final String enumString = flyoutPanelEnum.name(); + final boolean isDismissQue = enumString.equals("DISMISS_QUEUE"); + final boolean isReport = enumString.equals("FLAG"); + + if (isDismissQue) { + replaceDismissQueue(textView, imageView); + } else if (isReport) { + replaceReport(textView, imageView, lastMenuWasDismissQueue); + } + lastMenuWasDismissQueue = isDismissQue; + } + + private static void replaceDismissQueue(@NonNull TextView textView, @NonNull ImageView imageView) { + if (!Settings.REPLACE_FLYOUT_MENU_DISMISS_QUEUE.get()) + return; + + if (!(textView.getParent() instanceof ViewGroup clickAbleArea)) + return; + + runOnMainThreadDelayed(() -> { + textView.setText(str("revanced_replace_flyout_menu_dismiss_queue_watch_on_youtube_label")); + imageView.setImageResource(getIdentifier("yt_outline_youtube_logo_icon_vd_theme_24", ResourceType.DRAWABLE, clickAbleArea.getContext())); + clickAbleArea.setOnClickListener(viewGroup -> VideoUtils.openInYouTube()); + }, 0L + ); + } + + private static final ColorFilter cf = new PorterDuffColorFilter(Color.parseColor("#ffffffff"), PorterDuff.Mode.SRC_ATOP); + + private static void replaceReport(@NonNull TextView textView, @NonNull ImageView imageView, boolean wasDismissQueue) { + if (!Settings.REPLACE_FLYOUT_MENU_REPORT.get()) + return; + + if (Settings.REPLACE_FLYOUT_MENU_REPORT_ONLY_PLAYER.get() && !wasDismissQueue) + return; + + if (!(textView.getParent() instanceof ViewGroup clickAbleArea)) + return; + + runOnMainThreadDelayed(() -> { + textView.setText(str("playback_rate_title")); + imageView.setImageResource(getIdentifier("yt_outline_play_arrow_half_circle_black_24", ResourceType.DRAWABLE, clickAbleArea.getContext())); + imageView.setColorFilter(cf); + clickAbleArea.setOnClickListener(view -> { + clickView(touchOutSideViewRef.get()); + VideoUtils.showPlaybackSpeedFlyoutMenu(); + }); + }, 0L + ); + } + + private enum FlyoutPanelComponent { + SAVE_EPISODE_FOR_LATER("BOOKMARK_BORDER", Settings.HIDE_FLYOUT_MENU_SAVE_EPISODE_FOR_LATER.get()), + SHUFFLE_PLAY("SHUFFLE", Settings.HIDE_FLYOUT_MENU_SHUFFLE_PLAY.get()), + RADIO("MIX", Settings.HIDE_FLYOUT_MENU_START_RADIO.get()), + SUBSCRIBE("SUBSCRIBE", Settings.HIDE_FLYOUT_MENU_SUBSCRIBE.get()), + EDIT_PLAYLIST("EDIT", Settings.HIDE_FLYOUT_MENU_EDIT_PLAYLIST.get()), + DELETE_PLAYLIST("DELETE", Settings.HIDE_FLYOUT_MENU_DELETE_PLAYLIST.get()), + PLAY_NEXT("QUEUE_PLAY_NEXT", Settings.HIDE_FLYOUT_MENU_PLAY_NEXT.get()), + ADD_TO_QUEUE("QUEUE_MUSIC", Settings.HIDE_FLYOUT_MENU_ADD_TO_QUEUE.get()), + SAVE_TO_LIBRARY("LIBRARY_ADD", Settings.HIDE_FLYOUT_MENU_SAVE_TO_LIBRARY.get()), + REMOVE_FROM_LIBRARY("LIBRARY_REMOVE", Settings.HIDE_FLYOUT_MENU_REMOVE_FROM_LIBRARY.get()), + SAVE_TO_PLAYLIST("ADD_TO_PLAYLIST", Settings.HIDE_FLYOUT_MENU_SAVE_TO_PLAYLIST.get()), + REMOVE_FROM_PLAYLIST("REMOVE_FROM_PLAYLIST", Settings.HIDE_FLYOUT_MENU_REMOVE_FROM_PLAYLIST.get()), + DOWNLOAD("OFFLINE_DOWNLOAD", Settings.HIDE_FLYOUT_MENU_DOWNLOAD.get()), + GO_TO_EPISODE("INFO", Settings.HIDE_FLYOUT_MENU_GO_TO_EPISODE.get()), + GO_TO_PODCAST("BROADCAST", Settings.HIDE_FLYOUT_MENU_GO_TO_PODCAST.get()), + GO_TO_ALBUM("ALBUM", Settings.HIDE_FLYOUT_MENU_GO_TO_ALBUM.get()), + GO_TO_ARTIST("ARTIST", Settings.HIDE_FLYOUT_MENU_GO_TO_ARTIST.get()), + VIEW_SONG_CREDIT("PEOPLE_GROUP", Settings.HIDE_FLYOUT_MENU_VIEW_SONG_CREDIT.get()), + PIN_TO_SPEED_DIAL("KEEP", Settings.HIDE_FLYOUT_MENU_PIN_TO_SPEED_DIAL.get()), + UNPIN_FROM_SPEED_DIAL("KEEP_OFF", Settings.HIDE_FLYOUT_MENU_UNPIN_FROM_SPEED_DIAL.get()), + SHARE("SHARE", Settings.HIDE_FLYOUT_MENU_SHARE.get()), + DISMISS_QUEUE("DISMISS_QUEUE", Settings.HIDE_FLYOUT_MENU_DISMISS_QUEUE.get()), + HELP("HELP_OUTLINE", Settings.HIDE_FLYOUT_MENU_HELP.get()), + REPORT("FLAG", Settings.HIDE_FLYOUT_MENU_REPORT.get()), + QUALITY("SETTINGS_MATERIAL", Settings.HIDE_FLYOUT_MENU_QUALITY.get()), + CAPTIONS("CAPTIONS", Settings.HIDE_FLYOUT_MENU_CAPTIONS.get()), + STATS_FOR_NERDS("PLANNER_REVIEW", Settings.HIDE_FLYOUT_MENU_STATS_FOR_NERDS.get()), + SLEEP_TIMER("MOON_Z", Settings.HIDE_FLYOUT_MENU_SLEEP_TIMER.get()); + + private final boolean enabled; + private final String name; + + FlyoutPanelComponent(String name, boolean enabled) { + this.enabled = enabled; + this.name = name; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/general/GeneralPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/general/GeneralPatch.java new file mode 100644 index 000000000..72d3ba3f3 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/general/GeneralPatch.java @@ -0,0 +1,181 @@ +package app.revanced.extension.music.patches.general; + +import static app.revanced.extension.music.utils.ExtendedUtils.isSpoofingToLessThan; +import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition; + +import android.app.AlertDialog; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.ImageView; + +import app.revanced.extension.music.settings.Settings; + +/** + * @noinspection ALL + */ +@SuppressWarnings("unused") +public class GeneralPatch { + + // region [Change start page] patch + + public static String changeStartPage(final String browseId) { + if (!browseId.equals("FEmusic_home")) + return browseId; + + return Settings.CHANGE_START_PAGE.get(); + } + + // endregion + + // region [Disable dislike redirection] patch + + public static boolean disableDislikeRedirection() { + return Settings.DISABLE_DISLIKE_REDIRECTION.get(); + } + + // endregion + + // region [Enable landscape mode] patch + + public static boolean enableLandScapeMode(boolean original) { + return Settings.ENABLE_LANDSCAPE_MODE.get() || original; + } + + // endregion + + // region [Hide layout components] patch + + public static int hideCastButton(int original) { + return Settings.HIDE_CAST_BUTTON.get() ? View.GONE : original; + } + + public static void hideCastButton(View view) { + hideViewBy0dpUnderCondition(Settings.HIDE_CAST_BUTTON.get(), view); + } + + public static void hideCategoryBar(View view) { + hideViewBy0dpUnderCondition(Settings.HIDE_CATEGORY_BAR.get(), view); + } + + public static boolean hideFloatingButton() { + return Settings.HIDE_FLOATING_BUTTON.get(); + } + + public static boolean hideTapToUpdateButton() { + return Settings.HIDE_TAP_TO_UPDATE_BUTTON.get(); + } + + public static boolean hideHistoryButton(boolean original) { + return !Settings.HIDE_HISTORY_BUTTON.get() && original; + } + + public static void hideNotificationButton(View view) { + if (view.getParent() instanceof ViewGroup viewGroup) { + hideViewBy0dpUnderCondition(Settings.HIDE_NOTIFICATION_BUTTON, viewGroup); + } + } + + public static boolean hideSoundSearchButton(boolean original) { + if (!Settings.SETTINGS_INITIALIZED.get()) { + return original; + } + return !Settings.HIDE_SOUND_SEARCH_BUTTON.get(); + } + + public static void hideVoiceSearchButton(ImageView view, int visibility) { + final int finalVisibility = Settings.HIDE_VOICE_SEARCH_BUTTON.get() + ? View.GONE + : visibility; + + view.setVisibility(finalVisibility); + } + + public static void hideTasteBuilder(View view) { + view.setVisibility(View.GONE); + } + + + // endregion + + // region [Hide overlay filter] patch + + public static void disableDimBehind(Window window) { + if (window != null) { + // Disable AlertDialog's background dim. + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + } + } + + // endregion + + // region [Remove viewer discretion dialog] patch + + /** + * Injection point. + *

+ * The {@link AlertDialog#getButton(int)} method must be used after {@link AlertDialog#show()} is called. + * Otherwise {@link AlertDialog#getButton(int)} method will always return null. + * https://stackoverflow.com/a/4604145 + *

+ * That's why {@link AlertDialog#show()} is absolutely necessary. + * Instead, use two tricks to hide Alertdialog. + *

+ * 1. Change the size of AlertDialog to 0. + * 2. Disable AlertDialog's background dim. + *

+ * This way, AlertDialog will be completely hidden, + * and {@link AlertDialog#getButton(int)} method can be used without issue. + */ + public static void confirmDialog(final AlertDialog dialog) { + if (!Settings.REMOVE_VIEWER_DISCRETION_DIALOG.get()) { + return; + } + + // This method is called after AlertDialog#show(), + // So we need to hide the AlertDialog before pressing the possitive button. + final Window window = dialog.getWindow(); + final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + if (window != null && button != null) { + WindowManager.LayoutParams params = window.getAttributes(); + params.height = 0; + params.width = 0; + + // Change the size of AlertDialog to 0. + window.setAttributes(params); + + // Disable AlertDialog's background dim. + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + + button.callOnClick(); + } + } + + // endregion + + // region [Restore old style library shelf] patch + + public static String restoreOldStyleLibraryShelf(final String browseId) { + final boolean oldStyleLibraryShelfEnabled = + Settings.RESTORE_OLD_STYLE_LIBRARY_SHELF.get() || isSpoofingToLessThan("5.38.00"); + return oldStyleLibraryShelfEnabled && browseId.equals("FEmusic_library_landing") + ? "FEmusic_liked" + : browseId; + } + + // endregion + + // region [Spoof app version] patch + + public static String getVersionOverride(String version) { + if (!Settings.SPOOF_APP_VERSION.get()) + return version; + + return Settings.SPOOF_APP_VERSION_TARGET.get(); + } + + // endregion + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/general/SettingsMenuPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/general/SettingsMenuPatch.java new file mode 100644 index 000000000..27359dcc9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/general/SettingsMenuPatch.java @@ -0,0 +1,41 @@ +package app.revanced.extension.music.patches.general; + +import androidx.preference.PreferenceScreen; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.patches.BaseSettingsMenuPatch; + +@SuppressWarnings("unused") +public final class SettingsMenuPatch extends BaseSettingsMenuPatch { + + public static void hideSettingsMenu(PreferenceScreen mPreferenceScreen) { + if (mPreferenceScreen == null) return; + for (SettingsMenuComponent component : SettingsMenuComponent.values()) + if (component.enabled) + removePreference(mPreferenceScreen, component.key); + } + + public static boolean hideParentToolsMenu(boolean original) { + return !Settings.HIDE_SETTINGS_MENU_PARENT_TOOLS.get() && original; + } + + private enum SettingsMenuComponent { + GENERAL("settings_header_general", Settings.HIDE_SETTINGS_MENU_GENERAL.get()), + PLAYBACK("settings_header_playback", Settings.HIDE_SETTINGS_MENU_PLAYBACK.get()), + DATA_SAVING("settings_header_data_saving", Settings.HIDE_SETTINGS_MENU_DATA_SAVING.get()), + DOWNLOADS_AND_STORAGE("settings_header_downloads_and_storage", Settings.HIDE_SETTINGS_MENU_DOWNLOADS_AND_STORAGE.get()), + NOTIFICATIONS("settings_header_notifications", Settings.HIDE_SETTINGS_MENU_NOTIFICATIONS.get()), + PRIVACY_AND_LOCATION("settings_header_privacy_and_location", Settings.HIDE_SETTINGS_MENU_PRIVACY_AND_LOCATION.get()), + RECOMMENDATIONS("settings_header_recommendations", Settings.HIDE_SETTINGS_MENU_RECOMMENDATIONS.get()), + PAID_MEMBERSHIPS("settings_header_paid_memberships", Settings.HIDE_SETTINGS_MENU_PAID_MEMBERSHIPS.get()), + ABOUT("settings_header_about_youtube_music", Settings.HIDE_SETTINGS_MENU_ABOUT.get()); + + private final String key; + private final boolean enabled; + + SettingsMenuComponent(String key, boolean enabled) { + this.key = key; + this.enabled = enabled; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/CairoSplashAnimationPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/CairoSplashAnimationPatch.java new file mode 100644 index 000000000..c1c9c5094 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/CairoSplashAnimationPatch.java @@ -0,0 +1,11 @@ +package app.revanced.extension.music.patches.misc; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class CairoSplashAnimationPatch { + + public static boolean disableCairoSplashAnimation(boolean original) { + return !Settings.DISABLE_CAIRO_SPLASH_ANIMATION.get() && original; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/DrcAudioPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/DrcAudioPatch.java new file mode 100644 index 000000000..94e1e5335 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/DrcAudioPatch.java @@ -0,0 +1,14 @@ +package app.revanced.extension.music.patches.misc; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class DrcAudioPatch { + + public static float disableDrcAudio(float original) { + if (!Settings.DISABLE_DRC_AUDIO.get()) { + return original; + } + return 0f; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/OpusCodecPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/OpusCodecPatch.java new file mode 100644 index 000000000..5dec961fa --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/OpusCodecPatch.java @@ -0,0 +1,11 @@ +package app.revanced.extension.music.patches.misc; + +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class OpusCodecPatch { + + public static boolean enableOpusCodec() { + return Settings.ENABLE_OPUS_CODEC.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/ShareSheetPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/ShareSheetPatch.java new file mode 100644 index 000000000..afcaaa77e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/ShareSheetPatch.java @@ -0,0 +1,41 @@ +package app.revanced.extension.music.patches.misc; + +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; + +import app.revanced.extension.music.patches.components.ShareSheetMenuFilter; +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings("unused") +public class ShareSheetPatch { + /** + * Injection point. + */ + public static void onShareSheetMenuCreate(final RecyclerView recyclerView) { + if (!Settings.CHANGE_SHARE_SHEET.get()) + return; + + recyclerView.getViewTreeObserver().addOnDrawListener(() -> { + try { + if (!ShareSheetMenuFilter.isShareSheetMenuVisible) + return; + if (!(recyclerView.getChildAt(0) instanceof ViewGroup shareContainer)) { + return; + } + if (!(shareContainer.getChildAt(shareContainer.getChildCount() - 1) instanceof ViewGroup shareWithOtherAppsView)) { + return; + } + ShareSheetMenuFilter.isShareSheetMenuVisible = false; + + recyclerView.setVisibility(View.GONE); + Utils.clickView(shareWithOtherAppsView); + } catch (Exception ex) { + Logger.printException(() -> "onShareSheetMenuCreate failure", ex); + } + }); + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofClientPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofClientPatch.java new file mode 100644 index 000000000..e3e651a36 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofClientPatch.java @@ -0,0 +1,87 @@ +package app.revanced.extension.music.patches.misc; + +import app.revanced.extension.music.patches.misc.client.AppClient.ClientType; +import app.revanced.extension.music.settings.Settings; + +@SuppressWarnings("unused") +public class SpoofClientPatch { + private static final ClientType CLIENT_TYPE = Settings.SPOOF_CLIENT_TYPE.get(); + public static final boolean SPOOF_CLIENT = Settings.SPOOF_CLIENT.get(); + + /** + * Injection point. + */ + public static int getClientTypeId(int originalClientTypeId) { + if (SPOOF_CLIENT) { + return CLIENT_TYPE.id; + } + + return originalClientTypeId; + } + + /** + * Injection point. + */ + public static String getClientVersion(String originalClientVersion) { + if (SPOOF_CLIENT) { + return CLIENT_TYPE.clientVersion; + } + + return originalClientVersion; + } + + /** + * Injection point. + */ + public static String getClientModel(String originalClientModel) { + if (SPOOF_CLIENT) { + return CLIENT_TYPE.deviceModel; + } + + return originalClientModel; + } + + /** + * Injection point. + */ + public static String getOsVersion(String originalOsVersion) { + if (SPOOF_CLIENT) { + return CLIENT_TYPE.osVersion; + } + + return originalOsVersion; + } + + /** + * Injection point. + */ + public static String getUserAgent(String originalUserAgent) { + if (SPOOF_CLIENT) { + return CLIENT_TYPE.userAgent; + } + + return originalUserAgent; + } + + /** + * Injection point. + */ + public static boolean isClientSpoofingEnabled() { + return SPOOF_CLIENT; + } + + /** + * Injection point. + *

+ * When spoofing the client to iOS, the playback speed menu is missing from the player response. + * This fix is required because playback speed is not available in YouTube Music Podcasts. + *

+ * Return true to force create the playback speed menu. + */ + public static boolean forceCreatePlaybackSpeedMenu(boolean original) { + if (SPOOF_CLIENT) { + return true; + } + return original; + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/client/AppClient.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/client/AppClient.java new file mode 100644 index 000000000..487ef08c4 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/client/AppClient.java @@ -0,0 +1,118 @@ +package app.revanced.extension.music.patches.misc.client; + +import android.os.Build; + +public class AppClient { + + // Audio codec is MP4A. + private static final String CLIENT_VERSION_ANDROID_MUSIC_4_27 = "4.27.53"; + + // Audio codec is OPUS. + private static final String CLIENT_VERSION_ANDROID_MUSIC_5_29 = "5.29.53"; + + private static final String PACKAGE_NAME_ANDROID_MUSIC = "com.google.android.apps.youtube.music"; + private static final String DEVICE_MODEL_ANDROID_MUSIC = Build.MODEL; + private static final String OS_VERSION_ANDROID_MUSIC = Build.VERSION.RELEASE; + + // Audio codec is MP4A. + private static final String CLIENT_VERSION_IOS_MUSIC_6_21 = "6.21"; + + // Audio codec is OPUS. + private static final String CLIENT_VERSION_IOS_MUSIC_7_04 = "7.04"; + + private static final String PACKAGE_NAME_IOS_MUSIC = "com.google.ios.youtubemusic"; + private static final String DEVICE_MODEL_IOS_MUSIC = "iPhone14,3"; + private static final String OS_VERSION_IOS_MUSIC = "15.7.1.19H117"; + private static final String USER_AGENT_VERSION_IOS_MUSIC = "15_7_1"; + + private AppClient() { + } + + private static String androidUserAgent(String clientVersion) { + return PACKAGE_NAME_ANDROID_MUSIC + + "/" + + clientVersion + + " (Linux; U; Android " + + OS_VERSION_ANDROID_MUSIC + + "; GB) gzip"; + } + + private static String iOSUserAgent(String clientVersion) { + return PACKAGE_NAME_IOS_MUSIC + + "/" + + clientVersion + + "(" + + DEVICE_MODEL_IOS_MUSIC + + "; U; CPU iOS " + + USER_AGENT_VERSION_IOS_MUSIC + + " like Mac OS X)"; + } + + public enum ClientType { + ANDROID_MUSIC_4_27(21, + DEVICE_MODEL_ANDROID_MUSIC, + OS_VERSION_ANDROID_MUSIC, + androidUserAgent(CLIENT_VERSION_ANDROID_MUSIC_4_27), + CLIENT_VERSION_ANDROID_MUSIC_4_27 + ), + ANDROID_MUSIC_5_29(21, + DEVICE_MODEL_ANDROID_MUSIC, + OS_VERSION_ANDROID_MUSIC, + androidUserAgent(CLIENT_VERSION_ANDROID_MUSIC_5_29), + CLIENT_VERSION_ANDROID_MUSIC_5_29 + ), + IOS_MUSIC_6_21( + 26, + DEVICE_MODEL_IOS_MUSIC, + OS_VERSION_IOS_MUSIC, + iOSUserAgent(CLIENT_VERSION_IOS_MUSIC_6_21), + CLIENT_VERSION_IOS_MUSIC_6_21 + ), + IOS_MUSIC_7_04( + 26, + DEVICE_MODEL_IOS_MUSIC, + OS_VERSION_IOS_MUSIC, + iOSUserAgent(CLIENT_VERSION_IOS_MUSIC_7_04), + CLIENT_VERSION_IOS_MUSIC_7_04 + ); + + /** + * YouTube + * client type + */ + public final int id; + + /** + * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) + */ + public final String deviceModel; + + /** + * Device OS version. + */ + public final String osVersion; + + /** + * Player user-agent. + */ + public final String userAgent; + + /** + * App version. + */ + public final String clientVersion; + + ClientType(int id, + String deviceModel, + String osVersion, + String userAgent, + String clientVersion + ) { + this.id = id; + this.deviceModel = deviceModel; + this.clientVersion = clientVersion; + this.osVersion = osVersion; + this.userAgent = userAgent; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/navigation/NavigationPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/navigation/NavigationPatch.java new file mode 100644 index 000000000..bf99b8fe6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/navigation/NavigationPatch.java @@ -0,0 +1,56 @@ +package app.revanced.extension.music.patches.navigation; + +import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition; + +import android.graphics.Color; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.utils.ResourceUtils; + +@SuppressWarnings("unused") +public class NavigationPatch { + private static final int colorGrey12 = + ResourceUtils.getColor("revanced_color_grey_12"); + public static Enum lastPivotTab; + + public static int enableBlackNavigationBar() { + return Settings.ENABLE_BLACK_NAVIGATION_BAR.get() + ? Color.BLACK + : colorGrey12; + } + + public static void hideNavigationLabel(TextView textview) { + hideViewUnderCondition(Settings.HIDE_NAVIGATION_LABEL.get(), textview); + } + + public static void hideNavigationButton(@NonNull View view) { + if (Settings.HIDE_NAVIGATION_BAR.get() && view.getParent() != null) { + hideViewUnderCondition(true, (View) view.getParent()); + return; + } + + for (NavigationButton button : NavigationButton.values()) + if (lastPivotTab.name().equals(button.name)) + hideViewUnderCondition(button.enabled, view); + } + + private enum NavigationButton { + HOME("TAB_HOME", Settings.HIDE_NAVIGATION_HOME_BUTTON.get()), + SAMPLES("TAB_SAMPLES", Settings.HIDE_NAVIGATION_SAMPLES_BUTTON.get()), + EXPLORE("TAB_EXPLORE", Settings.HIDE_NAVIGATION_EXPLORE_BUTTON.get()), + LIBRARY("LIBRARY_MUSIC", Settings.HIDE_NAVIGATION_LIBRARY_BUTTON.get()), + UPGRADE("TAB_MUSIC_PREMIUM", Settings.HIDE_NAVIGATION_UPGRADE_BUTTON.get()); + + private final boolean enabled; + private final String name; + + NavigationButton(String name, boolean enabled) { + this.enabled = enabled; + this.name = name; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/player/PlayerPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/player/PlayerPatch.java new file mode 100644 index 000000000..71eed73e2 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/player/PlayerPatch.java @@ -0,0 +1,212 @@ +package app.revanced.extension.music.patches.player; + +import static app.revanced.extension.shared.utils.Utils.hideViewByRemovingFromParentUnderCondition; +import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition; +import static app.revanced.extension.shared.utils.Utils.isSDKAbove; + +import android.annotation.SuppressLint; +import android.graphics.Color; +import android.view.View; + +import java.util.Arrays; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.shared.VideoType; +import app.revanced.extension.music.utils.VideoUtils; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings({"unused"}) +public class PlayerPatch { + private static final int MUSIC_VIDEO_GREY_BACKGROUND_COLOR = -12566464; + private static final int MUSIC_VIDEO_ORIGINAL_BACKGROUND_COLOR = -16579837; + + @SuppressLint("StaticFieldLeak") + public static View previousButton; + @SuppressLint("StaticFieldLeak") + public static View nextButton; + + public static boolean disableMiniPlayerGesture() { + return Settings.DISABLE_MINI_PLAYER_GESTURE.get(); + } + + public static boolean disablePlayerGesture() { + return Settings.DISABLE_PLAYER_GESTURE.get(); + } + + public static boolean enableColorMatchPlayer() { + return Settings.ENABLE_COLOR_MATCH_PLAYER.get(); + } + + public static int enableBlackPlayerBackground(int originalColor) { + return Settings.ENABLE_BLACK_PLAYER_BACKGROUND.get() + && originalColor != MUSIC_VIDEO_GREY_BACKGROUND_COLOR + ? Color.BLACK + : originalColor; + } + + public static boolean enableForceMinimizedPlayer(boolean original) { + return Settings.ENABLE_FORCE_MINIMIZED_PLAYER.get() || original; + } + + public static boolean enableMiniPlayerNextButton(boolean original) { + return !Settings.ENABLE_MINI_PLAYER_NEXT_BUTTON.get() && original; + } + + public static View[] getViewArray(View[] oldViewArray) { + if (previousButton != null) { + if (nextButton != null) { + return getViewArray(getViewArray(oldViewArray, previousButton), nextButton); + } else { + return getViewArray(oldViewArray, previousButton); + } + } else { + return oldViewArray; + } + } + + private static View[] getViewArray(View[] oldViewArray, View newView) { + final int oldViewArrayLength = oldViewArray.length; + + View[] newViewArray = Arrays.copyOf(oldViewArray, oldViewArrayLength + 1); + newViewArray[oldViewArrayLength] = newView; + return newViewArray; + } + + public static void setNextButton(View nextButtonView) { + if (nextButtonView == null) + return; + + hideViewUnderCondition( + !Settings.ENABLE_MINI_PLAYER_NEXT_BUTTON.get(), + nextButtonView + ); + + nextButtonView.setOnClickListener(PlayerPatch::setNextButtonOnClickListener); + } + + // rest of the implementation added by patch. + private static void setNextButtonOnClickListener(View view) { + if (Settings.ENABLE_MINI_PLAYER_NEXT_BUTTON.get()) + view.getClass(); + } + + public static void setPreviousButton(View previousButtonView) { + if (previousButtonView == null) + return; + + hideViewUnderCondition( + !Settings.ENABLE_MINI_PLAYER_PREVIOUS_BUTTON.get(), + previousButtonView + ); + + previousButtonView.setOnClickListener(PlayerPatch::setPreviousButtonOnClickListener); + } + + // rest of the implementation added by patch. + private static void setPreviousButtonOnClickListener(View view) { + if (Settings.ENABLE_MINI_PLAYER_PREVIOUS_BUTTON.get()) + view.getClass(); + } + + public static boolean enableSwipeToDismissMiniPlayer() { + return Settings.ENABLE_SWIPE_TO_DISMISS_MINI_PLAYER.get(); + } + + public static boolean enableSwipeToDismissMiniPlayer(boolean original) { + return !Settings.ENABLE_SWIPE_TO_DISMISS_MINI_PLAYER.get() && original; + } + + public static Object enableSwipeToDismissMiniPlayer(Object object) { + return Settings.ENABLE_SWIPE_TO_DISMISS_MINI_PLAYER.get() ? null : object; + } + + public static int enableZenMode(int originalColor) { + if (Settings.ENABLE_ZEN_MODE.get() && originalColor == MUSIC_VIDEO_ORIGINAL_BACKGROUND_COLOR) { + if (Settings.ENABLE_ZEN_MODE_PODCAST.get() || !VideoType.getCurrent().isPodCast()) { + return MUSIC_VIDEO_GREY_BACKGROUND_COLOR; + } + } + return originalColor; + } + + public static void hideAudioVideoSwitchToggle(View view, int originalVisibility) { + if (Settings.HIDE_AUDIO_VIDEO_SWITCH_TOGGLE.get()) { + originalVisibility = View.GONE; + } + view.setVisibility(originalVisibility); + } + + public static void hideDoubleTapOverlayFilter(View view) { + hideViewByRemovingFromParentUnderCondition(Settings.HIDE_DOUBLE_TAP_OVERLAY_FILTER, view); + } + + public static int hideFullscreenShareButton(int original) { + return Settings.HIDE_FULLSCREEN_SHARE_BUTTON.get() ? 0 : original; + } + + public static void setShuffleState(Enum shuffleState) { + if (!Settings.REMEMBER_SHUFFLE_SATE.get()) + return; + Settings.ALWAYS_SHUFFLE.save(shuffleState.ordinal() == 1); + } + + public static void shuffleTracks() { + shuffleTracks(false); + } + + public static void shuffleTracksWithDelay() { + shuffleTracks(true); + } + + private static void shuffleTracks(boolean needDelay) { + if (!Settings.ALWAYS_SHUFFLE.get()) + return; + + if (needDelay) { + Utils.runOnMainThreadDelayed(VideoUtils::shuffleTracks, 1000); + } else { + VideoUtils.shuffleTracks(); + } + } + + public static boolean rememberRepeatState(boolean original) { + return Settings.REMEMBER_REPEAT_SATE.get() || original; + } + + public static boolean rememberShuffleState() { + return Settings.REMEMBER_SHUFFLE_SATE.get(); + } + + public static boolean restoreOldCommentsPopUpPanels() { + return restoreOldCommentsPopUpPanels(true); + } + + public static boolean restoreOldCommentsPopUpPanels(boolean original) { + if (!Settings.SETTINGS_INITIALIZED.get()) { + return original; + } + return !Settings.RESTORE_OLD_COMMENTS_POPUP_PANELS.get() && original; + } + + public static boolean restoreOldPlayerBackground(boolean original) { + if (!Settings.SETTINGS_INITIALIZED.get()) { + return original; + } + if (!isSDKAbove(23)) { + // Disable this patch on Android 5.0 / 5.1 to fix a black play button. + // Android 5.x have a different design for play button, + // and if the new background is applied forcibly, the play button turns black. + // 6.20.51 uses the old background from the beginning, so there is no impact. + return original; + } + return !Settings.RESTORE_OLD_PLAYER_BACKGROUND.get(); + } + + public static boolean restoreOldPlayerLayout(boolean original) { + if (!Settings.SETTINGS_INITIALIZED.get()) { + return original; + } + return !Settings.RESTORE_OLD_PLAYER_LAYOUT.get(); + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/DrawableColorPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/DrawableColorPatch.java new file mode 100644 index 000000000..ddb7b0101 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/DrawableColorPatch.java @@ -0,0 +1,22 @@ +package app.revanced.extension.music.patches.utils; + +@SuppressWarnings("unused") +public class DrawableColorPatch { + private static final int[] DARK_VALUES = { + -14606047 // comments box background + }; + + public static int getLithoColor(int originalValue) { + if (anyEquals(originalValue, DARK_VALUES)) + return -16777215; + + return originalValue; + } + + private static boolean anyEquals(int value, int... of) { + for (int v : of) if (value == v) return true; + return false; + } +} + + diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/InitializationPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/InitializationPatch.java new file mode 100644 index 000000000..d968f6886 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/InitializationPatch.java @@ -0,0 +1,28 @@ +package app.revanced.extension.music.patches.utils; + +import static app.revanced.extension.music.utils.RestartUtils.showRestartDialog; + +import android.app.Activity; + +import androidx.annotation.NonNull; + +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings("unused") +public class InitializationPatch { + + /** + * The new layout is not loaded normally when the app is first installed. + * (Also reproduced on unPatched YouTube Music) + *

+ * To fix this, show the reboot dialog when the app is installed for the first time. + */ + public static void onCreate(@NonNull Activity mActivity) { + if (BaseSettings.SETTINGS_INITIALIZED.get()) + return; + + showRestartDialog(mActivity, "revanced_extended_restart_first_run", 3000); + Utils.runOnMainThreadDelayed(() -> BaseSettings.SETTINGS_INITIALIZED.save(true), 3000); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/PatchStatus.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/PatchStatus.java new file mode 100644 index 000000000..08abcf94a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/PatchStatus.java @@ -0,0 +1,12 @@ +package app.revanced.extension.music.patches.utils; + +@SuppressWarnings("unused") +public class PatchStatus { + public static boolean SpoofAppVersionDefaultBoolean() { + return false; + } + + public static String SpoofAppVersionDefaultString() { + return "6.11.52"; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/PlayerTypeHookPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/PlayerTypeHookPatch.java new file mode 100644 index 000000000..1efe2d1a8 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/PlayerTypeHookPatch.java @@ -0,0 +1,19 @@ +package app.revanced.extension.music.patches.utils; + +import androidx.annotation.Nullable; + +import app.revanced.extension.music.shared.PlayerType; + +@SuppressWarnings("unused") +public class PlayerTypeHookPatch { + /** + * Injection point. + */ + public static void setPlayerType(@Nullable Enum musicPlayerType) { + if (musicPlayerType == null) + return; + + PlayerType.setFromString(musicPlayerType.name()); + } +} + diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/ReturnYouTubeDislikePatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/ReturnYouTubeDislikePatch.java new file mode 100644 index 000000000..5e74f70a5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/ReturnYouTubeDislikePatch.java @@ -0,0 +1,153 @@ +package app.revanced.extension.music.patches.utils; + +import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote; + +import android.text.SpannableString; +import android.text.Spanned; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import app.revanced.extension.music.returnyoutubedislike.ReturnYouTubeDislike; +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; +import app.revanced.extension.shared.utils.Logger; + +/** + * Handles all interaction of UI patch components. + *

+ * Does not handle creating dislike spans or anything to do with {@link ReturnYouTubeDislikeApi}. + */ +@SuppressWarnings("unused") +public class ReturnYouTubeDislikePatch { + + /** + * Injection point. + *

+ * Called when a litho text component is initially created, + * and also when a Span is later reused again (such as scrolling off/on screen). + *

+ * This method is sometimes called on the main thread, but it usually is called _off_ the main thread. + * This method can be called multiple times for the same UI element (including after dislikes was added). + * + * @param original Original char sequence was created or reused by Litho. + * @return The original char sequence (if nothing should change), or a replacement char sequence that contains dislikes. + */ + public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, + @NonNull CharSequence original) { + try { + if (!Settings.RYD_ENABLED.get()) { + return original; + } + + String conversionContextString = conversionContext.toString(); + + if (!conversionContextString.contains("segmented_like_dislike_button.eml")) { + return original; + } + ReturnYouTubeDislike videoData = currentVideoData; + if (videoData == null) { + return original; // User enabled RYD while a video was on screen. + } + if (!(original instanceof Spanned)) { + original = new SpannableString(original); + } + return videoData.getDislikesSpan((Spanned) original, true); + } catch (Exception ex) { + Logger.printException(() -> "onLithoTextLoaded failure", ex); + } + return original; + } + + /** + * RYD data for the current video on screen. + */ + @Nullable + private static volatile ReturnYouTubeDislike currentVideoData; + + public static void onRYDStatusChange(boolean rydEnabled) { + ReturnYouTubeDislikeApi.resetRateLimits(); + // Must remove all values to protect against using stale data + // if the user enables RYD while a video is on screen. + clearData(); + } + + private static void clearData() { + currentVideoData = null; + } + + /** + * Injection point + *

+ * Called when a Shorts dislike Spannable is created + */ + public static Spanned onSpannedCreated(Spanned original) { + try { + if (original == null) { + return null; + } + ReturnYouTubeDislike videoData = currentVideoData; + if (videoData == null) { + return original; // User enabled RYD while a video was on screen. + } + return videoData.getDislikesSpan(original, false); + } catch (Exception ex) { + Logger.printException(() -> "onSpannedCreated failure", ex); + } + return original; + } + + /** + * Injection point. + */ + public static void newVideoLoaded(@Nullable String videoId) { + try { + if (!Settings.RYD_ENABLED.get()) { + return; + } + if (videoId == null || videoId.isEmpty()) { + return; + } + if (videoIdIsSame(currentVideoData, videoId)) { + return; + } + currentVideoData = ReturnYouTubeDislike.getFetchForVideoId(videoId); + } catch (Exception ex) { + Logger.printException(() -> "newVideoLoaded failure", ex); + } + } + + private static boolean videoIdIsSame(@Nullable ReturnYouTubeDislike fetch, @Nullable String videoId) { + return (fetch == null && videoId == null) + || (fetch != null && fetch.getVideoId().equals(videoId)); + } + + /** + * Injection point. + *

+ * Called when the user likes or dislikes. + */ + public static void sendVote(int vote) { + try { + if (!Settings.RYD_ENABLED.get()) { + return; + } + ReturnYouTubeDislike videoData = currentVideoData; + if (videoData == null) { + Logger.printDebug(() -> "Cannot send vote, as current video data is null"); + return; // User enabled RYD while a regular video was minimized. + } + + for (Vote v : Vote.values()) { + if (v.value == vote) { + videoData.sendVote(v); + + return; + } + } + Logger.printException(() -> "Unknown vote type: " + vote); + } catch (Exception ex) { + Logger.printException(() -> "sendVote failure", ex); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/VideoTypeHookPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/VideoTypeHookPatch.java new file mode 100644 index 000000000..c6c3e90c9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/VideoTypeHookPatch.java @@ -0,0 +1,19 @@ +package app.revanced.extension.music.patches.utils; + +import androidx.annotation.Nullable; + +import app.revanced.extension.music.shared.VideoType; + +@SuppressWarnings("unused") +public class VideoTypeHookPatch { + /** + * Injection point. + */ + public static void setVideoType(@Nullable Enum musicVideoType) { + if (musicVideoType == null) + return; + + VideoType.setFromString(musicVideoType.name()); + } +} + diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/video/CustomPlaybackSpeedPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/video/CustomPlaybackSpeedPatch.java new file mode 100644 index 000000000..4e5fc0d33 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/video/CustomPlaybackSpeedPatch.java @@ -0,0 +1,94 @@ +package app.revanced.extension.music.patches.video; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import androidx.annotation.NonNull; + +import java.util.Arrays; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings("unused") +public class CustomPlaybackSpeedPatch { + /** + * Maximum playback speed, exclusive value. Custom speeds must be less than this value. + */ + private static final float MAXIMUM_PLAYBACK_SPEED = 5; + + /** + * Custom playback speeds. + */ + private static float[] customPlaybackSpeeds; + + static { + loadCustomSpeeds(); + } + + /** + * Injection point. + */ + public static float[] getArray(float[] original) { + return userChangedCustomPlaybackSpeed() ? customPlaybackSpeeds : original; + } + + /** + * Injection point. + */ + public static int getLength(int original) { + return userChangedCustomPlaybackSpeed() ? customPlaybackSpeeds.length : original; + } + + /** + * Injection point. + */ + public static int getSize(int original) { + return userChangedCustomPlaybackSpeed() ? 0 : original; + } + + private static void resetCustomSpeeds(@NonNull String toastMessage) { + Utils.showToastLong(toastMessage); + Utils.showToastShort(str("revanced_extended_reset_to_default_toast")); + Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault(); + } + + public static void loadCustomSpeeds() { + try { + String[] speedStrings = Settings.CUSTOM_PLAYBACK_SPEEDS.get().split("\\s+"); + Arrays.sort(speedStrings); + if (speedStrings.length == 0) { + throw new IllegalArgumentException(); + } + customPlaybackSpeeds = new float[speedStrings.length]; + for (int i = 0, length = speedStrings.length; i < length; i++) { + final float speed = Float.parseFloat(speedStrings[i]); + if (speed <= 0 || arrayContains(customPlaybackSpeeds, speed)) { + throw new IllegalArgumentException(); + } + if (speed > MAXIMUM_PLAYBACK_SPEED) { + resetCustomSpeeds(str("revanced_custom_playback_speeds_invalid", MAXIMUM_PLAYBACK_SPEED + "")); + loadCustomSpeeds(); + return; + } + customPlaybackSpeeds[i] = speed; + } + } catch (Exception ex) { + Logger.printInfo(() -> "parse error", ex); + resetCustomSpeeds(str("revanced_custom_playback_speeds_parse_exception")); + loadCustomSpeeds(); + } + } + + private static boolean userChangedCustomPlaybackSpeed() { + return !Settings.CUSTOM_PLAYBACK_SPEEDS.isSetToDefault() && customPlaybackSpeeds != null; + } + + private static boolean arrayContains(float[] array, float value) { + for (float arrayValue : array) { + if (arrayValue == value) return true; + } + return false; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/video/PlaybackSpeedPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/video/PlaybackSpeedPatch.java new file mode 100644 index 000000000..843f0c84e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/video/PlaybackSpeedPatch.java @@ -0,0 +1,32 @@ +package app.revanced.extension.music.patches.video; + +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.showToastShort; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.utils.Logger; + +@SuppressWarnings("unused") +public class PlaybackSpeedPatch { + + public static float getPlaybackSpeed(final float playbackSpeed) { + try { + return Settings.DEFAULT_PLAYBACK_SPEED.get(); + } catch (Exception ex) { + Logger.printException(() -> "Failed to getPlaybackSpeed", ex); + } + return playbackSpeed; + } + + public static void userSelectedPlaybackSpeed(final float playbackSpeed) { + if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) + return; + + Settings.DEFAULT_PLAYBACK_SPEED.save(playbackSpeed); + + if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get()) + return; + + showToastShort(str("revanced_remember_playback_speed_toast", playbackSpeed + "x")); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/video/VideoQualityPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/video/VideoQualityPatch.java new file mode 100644 index 000000000..d9e7f8819 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/video/VideoQualityPatch.java @@ -0,0 +1,68 @@ +package app.revanced.extension.music.patches.video; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.shared.VideoInformation; +import app.revanced.extension.shared.settings.IntegerSetting; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings("unused") +public class VideoQualityPatch { + private static final int DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY = -2; + private static final IntegerSetting mobileQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_MOBILE; + private static final IntegerSetting wifiQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_WIFI; + + /** + * Injection point. + */ + public static void newVideoStarted(final String ignoredVideoId) { + final int preferredQuality = + Utils.getNetworkType() == Utils.NetworkType.MOBILE + ? mobileQualitySetting.get() + : wifiQualitySetting.get(); + + if (preferredQuality == DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY) + return; + + Utils.runOnMainThreadDelayed(() -> + VideoInformation.overrideVideoQuality( + VideoInformation.getAvailableVideoQuality(preferredQuality) + ), + 500 + ); + } + + /** + * Injection point. + */ + public static void userSelectedVideoQuality() { + Utils.runOnMainThreadDelayed(() -> + userSelectedVideoQuality(VideoInformation.getVideoQuality()), + 300 + ); + } + + private static void userSelectedVideoQuality(final int defaultQuality) { + if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get()) + return; + if (defaultQuality == DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY) + return; + + final Utils.NetworkType networkType = Utils.getNetworkType(); + + switch (networkType) { + case NONE -> { + Utils.showToastShort(str("revanced_remember_video_quality_none")); + return; + } + case MOBILE -> mobileQualitySetting.save(defaultQuality); + default -> wifiQualitySetting.save(defaultQuality); + } + + if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get()) + return; + + Utils.showToastShort(str("revanced_remember_video_quality_" + networkType.getName(), defaultQuality + "p")); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/shared/src/main/java/app/revanced/extension/music/returnyoutubedislike/ReturnYouTubeDislike.java new file mode 100644 index 000000000..69389d1a7 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/returnyoutubedislike/ReturnYouTubeDislike.java @@ -0,0 +1,584 @@ +package app.revanced.extension.music.returnyoutubedislike; + +import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote; +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.isSDKAbove; + +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.OvalShape; +import android.graphics.drawable.shapes.RectShape; +import android.icu.text.CompactDecimalFormat; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.text.style.ImageSpan; +import android.util.DisplayMetrics; +import android.util.TypedValue; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.text.NumberFormat; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.returnyoutubedislike.requests.RYDVoteData; +import app.revanced.extension.shared.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +/** + * Because Litho creates spans using multiple threads, this entire class supports multithreading as well. + */ +public class ReturnYouTubeDislike { + + /** + * Maximum amount of time to block the UI from updates while waiting for network call to complete. + *

+ * Must be less than 5 seconds, as per: + * + */ + private static final long MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH = 4000; + + /** + * How long to retain successful RYD fetches. + */ + private static final long CACHE_TIMEOUT_SUCCESS_MILLISECONDS = 7 * 60 * 1000; // 7 Minutes + + /** + * How long to retain unsuccessful RYD fetches, + * and also the minimum time before retrying again. + */ + private static final long CACHE_TIMEOUT_FAILURE_MILLISECONDS = 3 * 60 * 1000; // 3 Minutes + + /** + * Unique placeholder character, used to detect if a segmented span already has dislikes added to it. + * Can be any almost any non-visible character. + */ + private static final char MIDDLE_SEPARATOR_CHARACTER = '◎'; // 'bullseye' + + /** + * Cached lookup of all video ids. + */ + @GuardedBy("itself") + private static final Map fetchCache = new HashMap<>(); + + /** + * Used to send votes, one by one, in the same order the user created them. + */ + private static final ExecutorService voteSerialExecutor = Executors.newSingleThreadExecutor(); + + /** + * For formatting dislikes as number. + */ + @GuardedBy("ReturnYouTubeDislike.class") // not thread safe + private static CompactDecimalFormat dislikeCountFormatter; + + /** + * For formatting dislikes as percentage. + */ + @GuardedBy("ReturnYouTubeDislike.class") + private static NumberFormat dislikePercentageFormatter; + + public static Rect leftSeparatorBounds; + private static Rect middleSeparatorBounds; + + + static { + ReturnYouTubeDislikeApi.toastOnConnectionError = Settings.RYD_TOAST_ON_CONNECTION_ERROR.get(); + } + + private final String videoId; + + /** + * Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes. + * Absolutely cannot be holding any lock during calls to {@link Future#get()}. + */ + private final Future future; + + /** + * Time this instance and the fetch future was created. + */ + private final long timeFetched; + + /** + * Optional current vote status of the UI. Used to apply a user vote that was done on a previous video viewing. + */ + @Nullable + @GuardedBy("this") + private Vote userVote; + + /** + * Original dislike span, before modifications. + */ + @Nullable + @GuardedBy("this") + private Spanned originalDislikeSpan; + + /** + * Replacement like/dislike span that includes formatted dislikes. + * Used to prevent recreating the same span multiple times. + */ + @Nullable + @GuardedBy("this") + private SpannableString replacementLikeDislikeSpan; + + + /** + * Color of the left and middle separator, based on the color of the right separator. + * It's unknown where YT gets the color from, and the values here are approximated by hand. + * Ideally, this would be the actual color YT uses at runtime. + *

+ * Older versions before the 'Me' library tab use a slightly different color. + * If spoofing was previously used and is now turned off, + * or an old version was recently upgraded then the old colors are sometimes still used. + */ + private static int getSeparatorColor(boolean isLithoText) { + return isLithoText + ? 0x29AAAAAA + : 0x33FFFFFF; + } + + @NonNull + private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, + @NonNull RYDVoteData voteData, + boolean isLithoText) { + CharSequence oldLikes = oldSpannable; + + // YouTube creators can hide the like count on a video, + // and the like count appears as a device language specific string that says 'Like'. + // Check if the string contains any numbers. + if (!Utils.containsNumber(oldLikes)) { + if (Settings.RYD_ESTIMATED_LIKE.get()) { + // Likes are hidden by video creator + // + // RYD does not directly provide like data, but can use an estimated likes + // using the same scale factor RYD applied to the raw dislikes. + // + // example video: https://www.youtube.com/watch?v=UnrU5vxCHxw + // RYD data: https://returnyoutubedislikeapi.com/votes?videoId=UnrU5vxCHxw + Logger.printDebug(() -> "Using estimated likes"); + oldLikes = formatDislikeCount(voteData.getLikeCount()); + } else { + // Change the "Likes" string to show that likes and dislikes are hidden. + String hiddenMessageString = str("revanced_ryd_video_likes_hidden_by_video_owner"); + return newSpanUsingStylingOfAnotherSpan(oldSpannable, hiddenMessageString); + } + } + + SpannableStringBuilder builder = new SpannableStringBuilder("\u2009"); + if (!isLithoText) { + builder.append("\u2009"); + } + final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get(); + + if (middleSeparatorBounds == null) { + final DisplayMetrics dp = Utils.getResources().getDisplayMetrics(); + leftSeparatorBounds = new Rect(0, 0, + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.2f, dp), + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, isLithoText ? 23 : 25, dp)); + final int middleSeparatorSize = + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp); + middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize); + } + + if (!compactLayout) { + String leftSeparatorString = "\u200E "; // u200E = left to right character + Spannable leftSeparatorSpan = new SpannableString(leftSeparatorString); + ShapeDrawable shapeDrawable = new ShapeDrawable(new RectShape()); + shapeDrawable.getPaint().setColor(getSeparatorColor(isLithoText)); + shapeDrawable.setBounds(leftSeparatorBounds); + leftSeparatorSpan.setSpan(new VerticallyCenteredImageSpan(shapeDrawable), 1, 2, + Spannable.SPAN_INCLUSIVE_EXCLUSIVE); // drawable cannot overwrite RTL or LTR character + builder.append(leftSeparatorSpan); + } + + // likes + builder.append(newSpanUsingStylingOfAnotherSpan(oldSpannable, oldLikes)); + + // middle separator + String middleSeparatorString = compactLayout + ? "\u200E " + MIDDLE_SEPARATOR_CHARACTER + " " + : "\u200E \u2009" + MIDDLE_SEPARATOR_CHARACTER + "\u2009 "; // u2009 = 'narrow space' character + final int shapeInsertionIndex = middleSeparatorString.length() / 2; + Spannable middleSeparatorSpan = new SpannableString(middleSeparatorString); + ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape()); + shapeDrawable.getPaint().setColor(getSeparatorColor(isLithoText)); + shapeDrawable.setBounds(middleSeparatorBounds); + // Use original text width if using Rolling Number, + // to ensure the replacement styled span has the same width as the measured String, + // otherwise layout can be broken (especially on devices with small system font sizes). + middleSeparatorSpan.setSpan( + new VerticallyCenteredImageSpan(shapeDrawable), + shapeInsertionIndex, shapeInsertionIndex + 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + builder.append(middleSeparatorSpan); + + // dislikes + builder.append(newSpannableWithDislikes(oldSpannable, voteData)); + + return new SpannableString(builder); + } + + /** + * @return If the text is likely for a previously created likes/dislikes segmented span. + */ + public static boolean isPreviouslyCreatedSegmentedSpan(@NonNull String text) { + return text.indexOf(MIDDLE_SEPARATOR_CHARACTER) >= 0; + } + + private static boolean spansHaveEqualTextAndColor(@NonNull Spanned one, @NonNull Spanned two) { + // Cannot use equals on the span, because many of the inner styling spans do not implement equals. + // Instead, compare the underlying text and the text color to handle when dark mode is changed. + // Cannot compare the status of device dark mode, as Litho components are updated just before dark mode status changes. + if (!one.toString().equals(two.toString())) { + return false; + } + ForegroundColorSpan[] oneColors = one.getSpans(0, one.length(), ForegroundColorSpan.class); + ForegroundColorSpan[] twoColors = two.getSpans(0, two.length(), ForegroundColorSpan.class); + final int oneLength = oneColors.length; + if (oneLength != twoColors.length) { + return false; + } + for (int i = 0; i < oneLength; i++) { + if (oneColors[i].getForegroundColor() != twoColors[i].getForegroundColor()) { + return false; + } + } + return true; + } + + private static SpannableString newSpannableWithDislikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) { + return newSpanUsingStylingOfAnotherSpan(sourceStyling, + Settings.RYD_DISLIKE_PERCENTAGE.get() + ? formatDislikePercentage(voteData.getDislikePercentage()) + : formatDislikeCount(voteData.getDislikeCount())); + } + + private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull CharSequence newSpanText) { + SpannableString destination = new SpannableString(newSpanText); + Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class); + for (Object span : spans) { + destination.setSpan(span, 0, destination.length(), sourceStyle.getSpanFlags(span)); + } + return destination; + } + + private static String formatDislikeCount(long dislikeCount) { + if (isSDKAbove(24)) { + if (dislikeCountFormatter == null) { + // Note: Java number formatters will use the locale specific number characters. + // such as Arabic which formats "1.234" into "۱,۲۳٤" + // But YouTube disregards locale specific number characters + // and instead shows english number characters everywhere. + Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().getLocales().get(0); + Logger.printDebug(() -> "Locale: " + locale); + dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT); + } + return dislikeCountFormatter.format(dislikeCount); + } else { + return String.valueOf(dislikeCount); + } + } + + private static String formatDislikePercentage(float dislikePercentage) { + if (isSDKAbove(24)) { + synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize + if (dislikePercentageFormatter == null) { + Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().getLocales().get(0); + Logger.printDebug(() -> "Locale: " + locale); + dislikePercentageFormatter = NumberFormat.getPercentInstance(locale); + } + if (dislikePercentage >= 0.01) { // at least 1% + dislikePercentageFormatter.setMaximumFractionDigits(0); // show only whole percentage points + } else { + dislikePercentageFormatter.setMaximumFractionDigits(1); // show up to 1 digit precision + } + return dislikePercentageFormatter.format(dislikePercentage); + } + } else { + return String.valueOf((int) (dislikePercentage * 100)); + } + } + + @NonNull + public static ReturnYouTubeDislike getFetchForVideoId(@Nullable String videoId) { + Objects.requireNonNull(videoId); + synchronized (fetchCache) { + // Remove any expired entries. + final long now = System.currentTimeMillis(); + if (isSDKAbove(24)) { + fetchCache.values().removeIf(value -> { + final boolean expired = value.isExpired(now); + if (expired) + Logger.printDebug(() -> "Removing expired fetch: " + value.videoId); + return expired; + }); + } else { + final Iterator> itr = fetchCache.entrySet().iterator(); + while (itr.hasNext()) { + final Map.Entry entry = itr.next(); + if (entry.getValue().isExpired(now)) { + Logger.printDebug(() -> "Removing expired fetch: " + entry.getValue().videoId); + itr.remove(); + } + } + } + + ReturnYouTubeDislike fetch = fetchCache.get(videoId); + if (fetch == null) { + fetch = new ReturnYouTubeDislike(videoId); + fetchCache.put(videoId, fetch); + } + return fetch; + } + } + + /** + * Should be called if the user changes dislikes appearance settings. + */ + public static void clearAllUICaches() { + synchronized (fetchCache) { + for (ReturnYouTubeDislike fetch : fetchCache.values()) { + fetch.clearUICache(); + } + } + } + + private ReturnYouTubeDislike(@NonNull String videoId) { + this.videoId = Objects.requireNonNull(videoId); + this.timeFetched = System.currentTimeMillis(); + this.future = Utils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId)); + } + + private boolean isExpired(long now) { + final long timeSinceCreation = now - timeFetched; + if (timeSinceCreation < CACHE_TIMEOUT_FAILURE_MILLISECONDS) { + return false; // Not expired, even if the API call failed. + } + if (timeSinceCreation > CACHE_TIMEOUT_SUCCESS_MILLISECONDS) { + return true; // Always expired. + } + // Only expired if the fetch failed (API null response). + return (!fetchCompleted() || getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH) == null); + } + + @Nullable + public RYDVoteData getFetchData(long maxTimeToWait) { + try { + return future.get(maxTimeToWait, TimeUnit.MILLISECONDS); + } catch (TimeoutException ex) { + Logger.printDebug(() -> "Waited but future was not complete after: " + maxTimeToWait + "ms"); + } catch (ExecutionException | InterruptedException ex) { + Logger.printException(() -> "Future failure ", ex); // will never happen + } + return null; + } + + /** + * @return if the RYD fetch call has completed. + */ + public boolean fetchCompleted() { + return future.isDone(); + } + + private synchronized void clearUICache() { + if (replacementLikeDislikeSpan != null) { + Logger.printDebug(() -> "Clearing replacement span for: " + videoId); + } + replacementLikeDislikeSpan = null; + } + + /** + * Must call off main thread, as this will make a network call if user is not yet registered. + * + * @return ReturnYouTubeDislike user ID. If user registration has never happened + * and the network call fails, this returns NULL. + */ + @Nullable + private static String getUserId() { + Utils.verifyOffMainThread(); + + String userId = Settings.RYD_USER_ID.get(); + if (!userId.isEmpty()) { + return userId; + } + + userId = ReturnYouTubeDislikeApi.registerAsNewUser(); + if (userId != null) { + Settings.RYD_USER_ID.save(userId); + } + return userId; + } + + @NonNull + public String getVideoId() { + return videoId; + } + + /** + * @return the replacement span containing dislikes, or the original span if RYD is not available. + */ + @NonNull + public synchronized Spanned getDislikesSpan(@NonNull Spanned original, boolean isLithoText) { + return waitForFetchAndUpdateReplacementSpan(original, isLithoText); + } + + @NonNull + private Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned original, boolean isLithoText) { + try { + RYDVoteData votingData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH); + if (votingData == null) { + Logger.printDebug(() -> "Cannot add dislike to UI (RYD data not available)"); + return original; + } + + synchronized (this) { + if (originalDislikeSpan != null && replacementLikeDislikeSpan != null) { + if (spansHaveEqualTextAndColor(original, replacementLikeDislikeSpan)) { + Logger.printDebug(() -> "Ignoring previously created dislikes span of data: " + videoId); + return original; + } + if (spansHaveEqualTextAndColor(original, originalDislikeSpan)) { + Logger.printDebug(() -> "Replacing span with previously created dislike span of data: " + videoId); + return replacementLikeDislikeSpan; + } + } + if (isPreviouslyCreatedSegmentedSpan(original.toString())) { + // need to recreate using original, as original has prior outdated dislike values + if (originalDislikeSpan == null) { + // Should never happen. + Logger.printDebug(() -> "Cannot add dislikes - original span is null. videoId: " + videoId); + return original; + } + original = originalDislikeSpan; + } + + // No replacement span exist, create it now. + + if (userVote != null) { + votingData.updateUsingVote(userVote); + } + originalDislikeSpan = original; + replacementLikeDislikeSpan = createDislikeSpan(original, votingData, isLithoText); + Logger.printDebug(() -> "Replaced: '" + originalDislikeSpan + "' with: '" + + replacementLikeDislikeSpan + "'" + " using video: " + videoId); + + return replacementLikeDislikeSpan; + } + } catch (Exception e) { + Logger.printException(() -> "waitForFetchAndUpdateReplacementSpan failure", e); // should never happen + } + return original; + } + + public void sendVote(@NonNull Vote vote) { + Utils.verifyOnMainThread(); + Objects.requireNonNull(vote); + try { + setUserVote(vote); + + voteSerialExecutor.execute(() -> { + try { // Must wrap in try/catch to properly log exceptions. + ReturnYouTubeDislikeApi.sendVote(getUserId(), videoId, vote); + } catch (Exception ex) { + Logger.printException(() -> "Failed to send vote", ex); + } + }); + } catch (Exception ex) { + Logger.printException(() -> "Error trying to send vote", ex); + } + } + + /** + * Sets the current user vote value, and does not send the vote to the RYD API. + *

+ * Only used to set value if thumbs up/down is already selected on video load. + */ + public void setUserVote(@NonNull Vote vote) { + Objects.requireNonNull(vote); + try { + Logger.printDebug(() -> "setUserVote: " + vote); + + synchronized (this) { + userVote = vote; + clearUICache(); + } + + if (future.isDone()) { + // Update the fetched vote data. + RYDVoteData voteData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH); + if (voteData == null) { + // RYD fetch failed. + Logger.printDebug(() -> "Cannot update UI (vote data not available)"); + return; + } + voteData.updateUsingVote(vote); + } // Else, vote will be applied after fetch completes. + + } catch (Exception ex) { + Logger.printException(() -> "setUserVote failure", ex); + } + } +} + +/** + * Vertically centers a Spanned Drawable. + */ +class VerticallyCenteredImageSpan extends ImageSpan { + + public VerticallyCenteredImageSpan(Drawable drawable) { + super(drawable); + } + + @Override + public int getSize(@NonNull Paint paint, @NonNull CharSequence text, + int start, int end, @Nullable Paint.FontMetricsInt fontMetrics) { + Drawable drawable = getDrawable(); + Rect bounds = drawable.getBounds(); + if (fontMetrics != null) { + Paint.FontMetricsInt paintMetrics = paint.getFontMetricsInt(); + final int fontHeight = paintMetrics.descent - paintMetrics.ascent; + final int drawHeight = bounds.bottom - bounds.top; + final int halfDrawHeight = drawHeight / 2; + final int yCenter = paintMetrics.ascent + fontHeight / 2; + + fontMetrics.ascent = yCenter - halfDrawHeight; + fontMetrics.top = fontMetrics.ascent; + fontMetrics.bottom = yCenter + halfDrawHeight; + fontMetrics.descent = fontMetrics.bottom; + } + return bounds.right; + } + + @Override + public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, + float x, int top, int y, int bottom, @NonNull Paint paint) { + Drawable drawable = getDrawable(); + canvas.save(); + Paint.FontMetricsInt paintMetrics = paint.getFontMetricsInt(); + final int fontHeight = paintMetrics.descent - paintMetrics.ascent; + final int yCenter = y + paintMetrics.descent - fontHeight / 2; + final Rect drawBounds = drawable.getBounds(); + final int translateY = yCenter - (drawBounds.bottom - drawBounds.top) / 2; + canvas.translate(x, translateY); + drawable.draw(canvas); + canvas.restore(); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/ActivityHook.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/ActivityHook.java new file mode 100644 index 000000000..628c44ed6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/ActivityHook.java @@ -0,0 +1,80 @@ +package app.revanced.extension.music.settings; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; + +import androidx.annotation.NonNull; + +import java.lang.ref.WeakReference; + +import app.revanced.extension.music.settings.preference.ReVancedPreferenceFragment; +import app.revanced.extension.shared.utils.Logger; + +/** + * @noinspection ALL + */ +public class ActivityHook { + private static WeakReference activityRef = new WeakReference<>(null); + + public static Activity getActivity() { + return activityRef.get(); + } + + /** + * Injection point. + * + * @param object object is usually Activity, but sometimes object cannot be cast to Activity. + * Check whether object can be cast as Activity for a safe hook. + */ + public static void setActivity(@NonNull Object object) { + if (object instanceof Activity mActivity) { + activityRef = new WeakReference<>(mActivity); + } + } + + /** + * Injection point. + * + * @param baseActivity Activity containing intent data. + * It should be finished immediately after obtaining the dataString. + * @return Whether or not dataString is included. + */ + public static boolean initialize(@NonNull Activity baseActivity) { + try { + final Intent baseActivityIntent = baseActivity.getIntent(); + if (baseActivityIntent == null) + return false; + + // If we do not finish the activity immediately, the YT Music logo will remain on the screen. + baseActivity.finish(); + + String dataString = baseActivityIntent.getDataString(); + if (dataString == null || dataString.isEmpty()) + return false; + + // Checks whether dataString contains settings that use Intent. + if (!Settings.includeWithIntent(dataString)) + return false; + + + // Save intent data in settings activity. + Activity mActivity = activityRef.get(); + Intent intent = mActivity.getIntent(); + intent.setData(Uri.parse(dataString)); + mActivity.setIntent(intent); + + // Starts a new PreferenceFragment to handle activities freely. + mActivity.getFragmentManager() + .beginTransaction() + .add(new ReVancedPreferenceFragment(), "") + .commit(); + + return true; + } catch (Exception ex) { + Logger.printException(() -> "initializeSettings failure", ex); + } + return false; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java new file mode 100644 index 000000000..5032dc814 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java @@ -0,0 +1,270 @@ +package app.revanced.extension.music.settings; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static app.revanced.extension.music.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY; + +import androidx.annotation.NonNull; + +import app.revanced.extension.music.patches.misc.client.AppClient.ClientType; +import app.revanced.extension.music.patches.utils.PatchStatus; +import app.revanced.extension.music.sponsorblock.SponsorBlockSettings; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.settings.EnumSetting; +import app.revanced.extension.shared.settings.FloatSetting; +import app.revanced.extension.shared.settings.IntegerSetting; +import app.revanced.extension.shared.settings.LongSetting; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.settings.StringSetting; +import app.revanced.extension.shared.utils.Utils; + + +@SuppressWarnings("unused") +public class Settings extends BaseSettings { + // PreferenceScreen: Account + public static final BooleanSetting HIDE_ACCOUNT_MENU = new BooleanSetting("revanced_hide_account_menu", FALSE); + public static final StringSetting HIDE_ACCOUNT_MENU_FILTER_STRINGS = new StringSetting("revanced_hide_account_menu_filter_strings", ""); + public static final BooleanSetting HIDE_ACCOUNT_MENU_EMPTY_COMPONENT = new BooleanSetting("revanced_hide_account_menu_empty_component", FALSE); + public static final BooleanSetting HIDE_HANDLE = new BooleanSetting("revanced_hide_handle", TRUE, true); + public static final BooleanSetting HIDE_TERMS_CONTAINER = new BooleanSetting("revanced_hide_terms_container", FALSE); + + + // PreferenceScreen: Action Bar + public static final BooleanSetting HIDE_ACTION_BUTTON_LIKE_DISLIKE = new BooleanSetting("revanced_hide_action_button_like_dislike", FALSE, true); + public static final BooleanSetting HIDE_ACTION_BUTTON_COMMENT = new BooleanSetting("revanced_hide_action_button_comment", FALSE, true); + public static final BooleanSetting HIDE_ACTION_BUTTON_ADD_TO_PLAYLIST = new BooleanSetting("revanced_hide_action_button_add_to_playlist", FALSE, true); + public static final BooleanSetting HIDE_ACTION_BUTTON_DOWNLOAD = new BooleanSetting("revanced_hide_action_button_download", FALSE, true); + public static final BooleanSetting HIDE_ACTION_BUTTON_SHARE = new BooleanSetting("revanced_hide_action_button_share", FALSE, true); + public static final BooleanSetting HIDE_ACTION_BUTTON_RADIO = new BooleanSetting("revanced_hide_action_button_radio", FALSE, true); + public static final BooleanSetting HIDE_ACTION_BUTTON_LABEL = new BooleanSetting("revanced_hide_action_button_label", FALSE, true); + public static final BooleanSetting EXTERNAL_DOWNLOADER_ACTION_BUTTON = new BooleanSetting("revanced_external_downloader_action", FALSE, true); + public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME = new StringSetting("revanced_external_downloader_package_name", "com.deniscerri.ytdl"); + + + // PreferenceScreen: Ads + public static final BooleanSetting HIDE_GENERAL_ADS = new BooleanSetting("revanced_hide_general_ads", TRUE, true); + public static final BooleanSetting HIDE_MUSIC_ADS = new BooleanSetting("revanced_hide_music_ads", TRUE, true); + public static final BooleanSetting HIDE_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_paid_promotion_label", TRUE, true); + public static final BooleanSetting HIDE_PREMIUM_PROMOTION = new BooleanSetting("revanced_hide_premium_promotion", TRUE, true); + public static final BooleanSetting HIDE_PREMIUM_RENEWAL = new BooleanSetting("revanced_hide_premium_renewal", TRUE, true); + + + // PreferenceScreen: Flyout menu + public static final BooleanSetting ENABLE_COMPACT_DIALOG = new BooleanSetting("revanced_enable_compact_dialog", FALSE); + public static final BooleanSetting ENABLE_TRIM_SILENCE = new BooleanSetting("revanced_enable_trim_silence", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_LIKE_DISLIKE = new BooleanSetting("revanced_hide_flyout_menu_like_dislike", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_3_COLUMN_COMPONENT = new BooleanSetting("revanced_hide_flyout_menu_3_column_component", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_ADD_TO_QUEUE = new BooleanSetting("revanced_hide_flyout_menu_add_to_queue", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_CAPTIONS = new BooleanSetting("revanced_hide_flyout_menu_captions", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_DELETE_PLAYLIST = new BooleanSetting("revanced_hide_flyout_menu_delete_playlist", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_DISMISS_QUEUE = new BooleanSetting("revanced_hide_flyout_menu_dismiss_queue", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_DOWNLOAD = new BooleanSetting("revanced_hide_flyout_menu_download", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_EDIT_PLAYLIST = new BooleanSetting("revanced_hide_flyout_menu_edit_playlist", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_GO_TO_ALBUM = new BooleanSetting("revanced_hide_flyout_menu_go_to_album", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_GO_TO_ARTIST = new BooleanSetting("revanced_hide_flyout_menu_go_to_artist", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_GO_TO_EPISODE = new BooleanSetting("revanced_hide_flyout_menu_go_to_episode", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_GO_TO_PODCAST = new BooleanSetting("revanced_hide_flyout_menu_go_to_podcast", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_HELP = new BooleanSetting("revanced_hide_flyout_menu_help", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_PIN_TO_SPEED_DIAL = new BooleanSetting("revanced_hide_flyout_menu_pin_to_speed_dial", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_PLAY_NEXT = new BooleanSetting("revanced_hide_flyout_menu_play_next", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_QUALITY = new BooleanSetting("revanced_hide_flyout_menu_quality", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_REMOVE_FROM_LIBRARY = new BooleanSetting("revanced_hide_flyout_menu_remove_from_library", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_REMOVE_FROM_PLAYLIST = new BooleanSetting("revanced_hide_flyout_menu_remove_from_playlist", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_REPORT = new BooleanSetting("revanced_hide_flyout_menu_report", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_SAVE_EPISODE_FOR_LATER = new BooleanSetting("revanced_hide_flyout_menu_save_episode_for_later", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_SAVE_TO_LIBRARY = new BooleanSetting("revanced_hide_flyout_menu_save_to_library", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_SAVE_TO_PLAYLIST = new BooleanSetting("revanced_hide_flyout_menu_save_to_playlist", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_SHARE = new BooleanSetting("revanced_hide_flyout_menu_share", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_SHUFFLE_PLAY = new BooleanSetting("revanced_hide_flyout_menu_shuffle_play", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_SLEEP_TIMER = new BooleanSetting("revanced_hide_flyout_menu_sleep_timer", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_START_RADIO = new BooleanSetting("revanced_hide_flyout_menu_start_radio", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_STATS_FOR_NERDS = new BooleanSetting("revanced_hide_flyout_menu_stats_for_nerds", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_SUBSCRIBE = new BooleanSetting("revanced_hide_flyout_menu_subscribe", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_UNPIN_FROM_SPEED_DIAL = new BooleanSetting("revanced_hide_flyout_menu_unpin_from_speed_dial", FALSE, true); + public static final BooleanSetting HIDE_FLYOUT_MENU_VIEW_SONG_CREDIT = new BooleanSetting("revanced_hide_flyout_menu_view_song_credit", FALSE, true); + public static final BooleanSetting REPLACE_FLYOUT_MENU_DISMISS_QUEUE = new BooleanSetting("revanced_replace_flyout_menu_dismiss_queue", FALSE, true); + public static final BooleanSetting REPLACE_FLYOUT_MENU_DISMISS_QUEUE_CONTINUE_WATCH = new BooleanSetting("revanced_replace_flyout_menu_dismiss_queue_continue_watch", TRUE); + public static final BooleanSetting REPLACE_FLYOUT_MENU_REPORT = new BooleanSetting("revanced_replace_flyout_menu_report", TRUE, true); + public static final BooleanSetting REPLACE_FLYOUT_MENU_REPORT_ONLY_PLAYER = new BooleanSetting("revanced_replace_flyout_menu_report_only_player", TRUE, true); + + + // PreferenceScreen: General + public static final StringSetting CHANGE_START_PAGE = new StringSetting("revanced_change_start_page", "FEmusic_home", true); + public static final BooleanSetting DISABLE_DISLIKE_REDIRECTION = new BooleanSetting("revanced_disable_dislike_redirection", FALSE); + public static final BooleanSetting ENABLE_LANDSCAPE_MODE = new BooleanSetting("revanced_enable_landscape_mode", FALSE, true); + public static final BooleanSetting CUSTOM_FILTER = new BooleanSetting("revanced_custom_filter", FALSE); + public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true); + public static final BooleanSetting HIDE_BUTTON_SHELF = new BooleanSetting("revanced_hide_button_shelf", FALSE, true); + public static final BooleanSetting HIDE_CAROUSEL_SHELF = new BooleanSetting("revanced_hide_carousel_shelf", FALSE, true); + public static final BooleanSetting HIDE_PLAYLIST_CARD_SHELF = new BooleanSetting("revanced_hide_playlist_card_shelf", FALSE, true); + public static final BooleanSetting HIDE_SAMPLE_SHELF = new BooleanSetting("revanced_hide_samples_shelf", FALSE, true); + public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_hide_cast_button", TRUE); + public static final BooleanSetting HIDE_CATEGORY_BAR = new BooleanSetting("revanced_hide_category_bar", FALSE, true); + public static final BooleanSetting HIDE_FLOATING_BUTTON = new BooleanSetting("revanced_hide_floating_button", FALSE, true); + public static final BooleanSetting HIDE_TAP_TO_UPDATE_BUTTON = new BooleanSetting("revanced_hide_tap_to_update_button", FALSE, true); + public static final BooleanSetting HIDE_HISTORY_BUTTON = new BooleanSetting("revanced_hide_history_button", FALSE); + public static final BooleanSetting HIDE_NOTIFICATION_BUTTON = new BooleanSetting("revanced_hide_notification_button", FALSE, true); + public static final BooleanSetting HIDE_SOUND_SEARCH_BUTTON = new BooleanSetting("revanced_hide_sound_search_button", FALSE, true); + public static final BooleanSetting HIDE_VOICE_SEARCH_BUTTON = new BooleanSetting("revanced_hide_voice_search_button", FALSE, true); + public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE); + public static final BooleanSetting RESTORE_OLD_STYLE_LIBRARY_SHELF = new BooleanSetting("revanced_restore_old_style_library_shelf", FALSE, true); + public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", + PatchStatus.SpoofAppVersionDefaultBoolean(), true); + public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", + PatchStatus.SpoofAppVersionDefaultString(), true); + + + // PreferenceScreen: Navigation bar + public static final BooleanSetting ENABLE_BLACK_NAVIGATION_BAR = new BooleanSetting("revanced_enable_black_navigation_bar", TRUE); + public static final BooleanSetting HIDE_NAVIGATION_HOME_BUTTON = new BooleanSetting("revanced_hide_navigation_home_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_SAMPLES_BUTTON = new BooleanSetting("revanced_hide_navigation_samples_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_EXPLORE_BUTTON = new BooleanSetting("revanced_hide_navigation_explore_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_LIBRARY_BUTTON = new BooleanSetting("revanced_hide_navigation_library_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_UPGRADE_BUTTON = new BooleanSetting("revanced_hide_navigation_upgrade_button", TRUE, true); + public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_hide_navigation_bar", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_LABEL = new BooleanSetting("revanced_hide_navigation_label", FALSE, true); + + + // PreferenceScreen: Player + public static final BooleanSetting DISABLE_MINI_PLAYER_GESTURE = new BooleanSetting("revanced_disable_mini_player_gesture", FALSE, true); + public static final BooleanSetting DISABLE_PLAYER_GESTURE = new BooleanSetting("revanced_disable_player_gesture", FALSE, true); + public static final BooleanSetting ENABLE_BLACK_PLAYER_BACKGROUND = new BooleanSetting("revanced_enable_black_player_background", FALSE, true); + public static final BooleanSetting ENABLE_COLOR_MATCH_PLAYER = new BooleanSetting("revanced_enable_color_match_player", TRUE); + public static final BooleanSetting ENABLE_FORCE_MINIMIZED_PLAYER = new BooleanSetting("revanced_enable_force_minimized_player", TRUE); + public static final BooleanSetting ENABLE_MINI_PLAYER_NEXT_BUTTON = new BooleanSetting("revanced_enable_mini_player_next_button", TRUE, true); + public static final BooleanSetting ENABLE_MINI_PLAYER_PREVIOUS_BUTTON = new BooleanSetting("revanced_enable_mini_player_previous_button", TRUE, true); + public static final BooleanSetting ENABLE_SWIPE_TO_DISMISS_MINI_PLAYER = new BooleanSetting("revanced_enable_swipe_to_dismiss_mini_player", TRUE, true); + public static final BooleanSetting ENABLE_ZEN_MODE = new BooleanSetting("revanced_enable_zen_mode", FALSE); + public static final BooleanSetting ENABLE_ZEN_MODE_PODCAST = new BooleanSetting("revanced_enable_zen_mode_podcast", FALSE); + public static final BooleanSetting HIDE_AUDIO_VIDEO_SWITCH_TOGGLE = new BooleanSetting("revanced_hide_audio_video_switch_toggle", FALSE, true); + public static final BooleanSetting HIDE_COMMENT_CHANNEL_GUIDELINES = new BooleanSetting("revanced_hide_comment_channel_guidelines", TRUE); + public static final BooleanSetting HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS = new BooleanSetting("revanced_hide_comment_timestamp_and_emoji_buttons", FALSE); + public static final BooleanSetting HIDE_DOUBLE_TAP_OVERLAY_FILTER = new BooleanSetting("revanced_hide_double_tap_overlay_filter", FALSE, true); + public static final BooleanSetting HIDE_FULLSCREEN_SHARE_BUTTON = new BooleanSetting("revanced_hide_fullscreen_share_button", FALSE, true); + public static final BooleanSetting REMEMBER_REPEAT_SATE = new BooleanSetting("revanced_remember_repeat_state", TRUE); + public static final BooleanSetting REMEMBER_SHUFFLE_SATE = new BooleanSetting("revanced_remember_shuffle_state", TRUE); + public static final BooleanSetting ALWAYS_SHUFFLE = new BooleanSetting("revanced_always_shuffle", FALSE); + public static final BooleanSetting RESTORE_OLD_COMMENTS_POPUP_PANELS = new BooleanSetting("revanced_restore_old_comments_popup_panels", FALSE, true); + public static final BooleanSetting RESTORE_OLD_PLAYER_BACKGROUND = new BooleanSetting("revanced_restore_old_player_background", FALSE, true); + public static final BooleanSetting RESTORE_OLD_PLAYER_LAYOUT = new BooleanSetting("revanced_restore_old_player_layout", FALSE, true); + + + // PreferenceScreen: Settings menu + public static final BooleanSetting HIDE_SETTINGS_MENU_PARENT_TOOLS = new BooleanSetting("revanced_hide_settings_menu_parent_tools", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_GENERAL = new BooleanSetting("revanced_hide_settings_menu_general", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_PLAYBACK = new BooleanSetting("revanced_hide_settings_menu_playback", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_DATA_SAVING = new BooleanSetting("revanced_hide_settings_menu_data_saving", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_DOWNLOADS_AND_STORAGE = new BooleanSetting("revanced_hide_settings_menu_downloads_and_storage", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_NOTIFICATIONS = new BooleanSetting("revanced_hide_settings_menu_notification", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_PRIVACY_AND_LOCATION = new BooleanSetting("revanced_hide_settings_menu_privacy_and_location", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_RECOMMENDATIONS = new BooleanSetting("revanced_hide_settings_menu_recommendations", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_PAID_MEMBERSHIPS = new BooleanSetting("revanced_hide_settings_menu_paid_memberships", TRUE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_ABOUT = new BooleanSetting("revanced_hide_settings_menu_about", FALSE, true); + + + // PreferenceScreen: Video + public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", "0.5\n0.8\n1.0\n1.2\n1.5\n1.8\n2.0", true); + public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", TRUE); + public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE); + public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", TRUE); + public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_last_selected_toast", TRUE); + public static final FloatSetting DEFAULT_PLAYBACK_SPEED = new FloatSetting("revanced_default_playback_speed", 1.0f); + public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE = new IntegerSetting("revanced_default_video_quality_mobile", -2); + public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI = new IntegerSetting("revanced_default_video_quality_wifi", -2); + + + // PreferenceScreen: Miscellaneous + public static final BooleanSetting CHANGE_SHARE_SHEET = new BooleanSetting("revanced_change_share_sheet", FALSE, true); + public static final BooleanSetting DISABLE_CAIRO_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_cairo_splash_animation", FALSE, true); + public static final BooleanSetting DISABLE_DRC_AUDIO = new BooleanSetting("revanced_disable_drc_audio", FALSE, true); + public static final BooleanSetting ENABLE_OPUS_CODEC = new BooleanSetting("revanced_enable_opus_codec", FALSE, true); + public static final BooleanSetting SETTINGS_IMPORT_EXPORT = new BooleanSetting("revanced_extended_settings_import_export", FALSE, false); + public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true); + public static final EnumSetting SPOOF_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_client_type", ClientType.IOS_MUSIC_6_21, true); + + + // PreferenceScreen: Return YouTube Dislike + public static final BooleanSetting RYD_ENABLED = new BooleanSetting("revanced_ryd_enabled", TRUE); + public static final StringSetting RYD_USER_ID = new StringSetting("revanced_ryd_user_id", "", false, false); + public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("revanced_ryd_dislike_percentage", FALSE); + public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("revanced_ryd_compact_layout", FALSE); + public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("revanced_ryd_estimated_like", FALSE, true); + public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("revanced_ryd_toast_on_connection_error", FALSE); + + // PreferenceScreen: Return YouTube Username + public static final BooleanSetting RETURN_YOUTUBE_USERNAME_ABOUT = new BooleanSetting("revanced_return_youtube_username_youtube_data_api_v3_about", FALSE, false); + + + // PreferenceScreen: SponsorBlock + public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE); + public static final BooleanSetting SB_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("sb_toast_on_connection_error", FALSE); + public static final BooleanSetting SB_TOAST_ON_SKIP = new BooleanSetting("sb_toast_on_skip", TRUE); + public static final StringSetting SB_API_URL = new StringSetting("sb_api_url", "https://sponsor.ajay.app"); + public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id", ""); + public static final BooleanSetting SB_USER_IS_VIP = new BooleanSetting("sb_user_is_vip", FALSE); + + public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#00D400"); + public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", SKIP_AUTOMATICALLY.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#FFFF00"); + public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", SKIP_AUTOMATICALLY.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CC00FF"); + public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", SKIP_AUTOMATICALLY.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#00FFFF"); + public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", SKIP_AUTOMATICALLY.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#0202ED"); + public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", SKIP_AUTOMATICALLY.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#008FD6"); + public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", SKIP_AUTOMATICALLY.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#7300FF"); + public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", SKIP_AUTOMATICALLY.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#FF9900"); + + // SB settings not exported + public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false); + + static { + // region SB import/export callbacks + + Setting.addImportExportCallback(SponsorBlockSettings.SB_IMPORT_EXPORT_CALLBACK); + + // endregion + } + + public static final String OPEN_DEFAULT_APP_SETTINGS = "revanced_default_app_settings"; + + /** + * If a setting path has this prefix, then remove it. + */ + public static final String OPTIONAL_SPONSOR_BLOCK_SETTINGS_PREFIX = "sb_segments_"; + + /** + * Array of settings using intent + */ + private static final String[] intentSettingArray = new String[]{ + BYPASS_IMAGE_REGION_RESTRICTIONS_DOMAIN.key, + CHANGE_START_PAGE.key, + CUSTOM_FILTER_STRINGS.key, + CUSTOM_PLAYBACK_SPEEDS.key, + EXTERNAL_DOWNLOADER_PACKAGE_NAME.key, + HIDE_ACCOUNT_MENU_FILTER_STRINGS.key, + SB_API_URL.key, + SETTINGS_IMPORT_EXPORT.key, + SPOOF_APP_VERSION_TARGET.key, + SPOOF_CLIENT_TYPE.key, + SPOOF_STREAMING_DATA_TYPE.key, + RETURN_YOUTUBE_USERNAME_ABOUT.key, + RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT.key, + RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY.key, + OPEN_DEFAULT_APP_SETTINGS, + OPTIONAL_SPONSOR_BLOCK_SETTINGS_PREFIX + }; + + /** + * @return whether dataString contains settings that use Intent + */ + public static boolean includeWithIntent(@NonNull String dataString) { + return Utils.containsAny(dataString, intentSettingArray); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ExternalDownloaderPreference.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ExternalDownloaderPreference.java new file mode 100644 index 000000000..0454f1424 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ExternalDownloaderPreference.java @@ -0,0 +1,132 @@ +package app.revanced.extension.music.settings.preference; + +import static app.revanced.extension.music.utils.ExtendedUtils.getDialogBuilder; +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Intent; +import android.net.Uri; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.TypedValue; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TableLayout; +import android.widget.TableRow; + +import java.util.Arrays; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.utils.ExtendedUtils; +import app.revanced.extension.shared.settings.StringSetting; +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.shared.utils.Utils; + +/** + * @noinspection all + */ +public class ExternalDownloaderPreference { + + private static final StringSetting settings = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME; + private static final String[] mEntries = ResourceUtils.getStringArray("revanced_external_downloader_label"); + private static final String[] mEntryValues = ResourceUtils.getStringArray("revanced_external_downloader_package_name"); + private static final String[] mWebsiteEntries = ResourceUtils.getStringArray("revanced_external_downloader_website"); + private static EditText mEditText; + private static String packageName; + private static int mClickedDialogEntryIndex; + + private static final TextWatcher textWatcher = new TextWatcher() { + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + public void afterTextChanged(Editable s) { + packageName = s.toString(); + mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName); + } + }; + + public static void showDialog(Activity mActivity) { + packageName = settings.get().toString(); + mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName); + + AlertDialog.Builder builder = getDialogBuilder(mActivity); + + TableLayout table = new TableLayout(mActivity); + table.setOrientation(LinearLayout.HORIZONTAL); + table.setPadding(15, 0, 15, 0); + + TableRow row = new TableRow(mActivity); + + mEditText = new EditText(mActivity); + mEditText.setText(packageName); + mEditText.addTextChangedListener(textWatcher); + mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 9); + mEditText.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT, 1f)); + row.addView(mEditText); + + table.addView(row); + builder.setView(table); + + builder.setTitle(str("revanced_external_downloader_dialog_title")); + builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, (dialog, which) -> { + mClickedDialogEntryIndex = which; + mEditText.setText(mEntryValues[which].toString()); + }); + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { + final String packageName = mEditText.getText().toString().trim(); + settings.save(packageName); + checkPackageIsValid(mActivity, packageName); + dialog.dismiss(); + }); + builder.setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> settings.resetToDefault()); + builder.setNegativeButton(android.R.string.cancel, null); + + builder.show(); + } + + private static boolean checkPackageIsValid(Activity mActivity, String packageName) { + String appName = ""; + String website = ""; + if (mClickedDialogEntryIndex >= 0) { + appName = mEntries[mClickedDialogEntryIndex].toString(); + website = mWebsiteEntries[mClickedDialogEntryIndex].toString(); + return showToastOrOpenWebsites(mActivity, appName, packageName, website); + } else { + return showToastOrOpenWebsites(mActivity, appName, packageName, website); + } + } + + private static boolean showToastOrOpenWebsites(Activity mActivity, String appName, String packageName, String website) { + if (ExtendedUtils.isPackageEnabled(packageName)) + return true; + + if (website.isEmpty()) { + Utils.showToastShort(str("revanced_external_downloader_not_installed_warning", packageName)); + return false; + } + + getDialogBuilder(mActivity) + .setTitle(str("revanced_external_downloader_not_installed_dialog_title")) + .setMessage(str("revanced_external_downloader_not_installed_dialog_message", appName, appName)) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(website)); + mActivity.startActivity(i); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + + return false; + } + + public static boolean checkPackageIsEnabled() { + final Activity mActivity = Utils.getActivity(); + packageName = settings.get().toString(); + mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName); + return checkPackageIsValid(mActivity, packageName); + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java new file mode 100644 index 000000000..313588ef9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java @@ -0,0 +1,355 @@ +package app.revanced.extension.music.settings.preference; + +import static app.revanced.extension.music.settings.Settings.BYPASS_IMAGE_REGION_RESTRICTIONS_DOMAIN; +import static app.revanced.extension.music.settings.Settings.CHANGE_START_PAGE; +import static app.revanced.extension.music.settings.Settings.CUSTOM_FILTER_STRINGS; +import static app.revanced.extension.music.settings.Settings.CUSTOM_PLAYBACK_SPEEDS; +import static app.revanced.extension.music.settings.Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME; +import static app.revanced.extension.music.settings.Settings.HIDE_ACCOUNT_MENU_FILTER_STRINGS; +import static app.revanced.extension.music.settings.Settings.OPEN_DEFAULT_APP_SETTINGS; +import static app.revanced.extension.music.settings.Settings.OPTIONAL_SPONSOR_BLOCK_SETTINGS_PREFIX; +import static app.revanced.extension.music.settings.Settings.RETURN_YOUTUBE_USERNAME_ABOUT; +import static app.revanced.extension.music.settings.Settings.SB_API_URL; +import static app.revanced.extension.music.settings.Settings.SETTINGS_IMPORT_EXPORT; +import static app.revanced.extension.music.settings.Settings.SPOOF_APP_VERSION_TARGET; +import static app.revanced.extension.music.settings.Settings.SPOOF_CLIENT_TYPE; +import static app.revanced.extension.music.utils.ExtendedUtils.getDialogBuilder; +import static app.revanced.extension.music.utils.ExtendedUtils.getLayoutParams; +import static app.revanced.extension.music.utils.RestartUtils.showRestartDialog; +import static app.revanced.extension.shared.settings.BaseSettings.RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT; +import static app.revanced.extension.shared.settings.BaseSettings.RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY; +import static app.revanced.extension.shared.settings.BaseSettings.SPOOF_STREAMING_DATA_TYPE; +import static app.revanced.extension.shared.settings.Setting.getSettingFromPath; +import static app.revanced.extension.shared.utils.ResourceUtils.getStringArray; +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.isSDKAbove; +import static app.revanced.extension.shared.utils.Utils.showToastShort; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.icu.text.SimpleDateFormat; +import android.net.Uri; +import android.os.Bundle; +import android.preference.PreferenceFragment; +import android.text.InputType; +import android.util.TypedValue; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.Nullable; + +import com.google.android.material.textfield.TextInputLayout; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Date; +import java.util.Objects; + +import app.revanced.extension.music.patches.utils.ReturnYouTubeDislikePatch; +import app.revanced.extension.music.returnyoutubedislike.ReturnYouTubeDislike; +import app.revanced.extension.music.settings.ActivityHook; +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.utils.ExtendedUtils; +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.settings.EnumSetting; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.settings.StringSetting; +import app.revanced.extension.shared.settings.preference.YouTubeDataAPIDialogBuilder; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings("all") +public class ReVancedPreferenceFragment extends PreferenceFragment { + + private static final String IMPORT_EXPORT_SETTINGS_ENTRY_KEY = "revanced_extended_settings_import_export_entries"; + private static final int READ_REQUEST_CODE = 42; + private static final int WRITE_REQUEST_CODE = 43; + + private static String existingSettings; + + + public ReVancedPreferenceFragment() { + } + + /** + * Injection point. + */ + public static void onPreferenceChanged(@Nullable String key, boolean newValue) { + if (key == null || key.isEmpty()) + return; + + if (key.equals(Settings.RESTORE_OLD_PLAYER_LAYOUT.key) && newValue) { + Settings.RESTORE_OLD_PLAYER_BACKGROUND.save(newValue); + } else if (key.equals(Settings.RYD_ENABLED.key)) { + ReturnYouTubeDislikePatch.onRYDStatusChange(newValue); + } else if (key.equals(Settings.RYD_DISLIKE_PERCENTAGE.key) || key.equals(Settings.RYD_COMPACT_LAYOUT.key)) { + ReturnYouTubeDislike.clearAllUICaches(); + } + + for (Setting setting : Setting.allLoadedSettings()) { + if (key.equals(setting.key)) { + ((BooleanSetting) setting).save(newValue); + if (setting.rebootApp) { + showRebootDialog(); + } + break; + } + } + } + + public static void showRebootDialog() { + final Activity activity = ActivityHook.getActivity(); + if (activity == null) + return; + + showRestartDialog(activity); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + try { + final Activity baseActivity = this.getActivity(); + final Activity mActivity = ActivityHook.getActivity(); + final Intent savedInstanceStateIntent = baseActivity.getIntent(); + if (savedInstanceStateIntent == null) + return; + + final String dataString = savedInstanceStateIntent.getDataString(); + if (dataString == null || dataString.isEmpty()) + return; + + if (dataString.startsWith(OPTIONAL_SPONSOR_BLOCK_SETTINGS_PREFIX)) { + SponsorBlockCategoryPreference.showDialog(baseActivity, dataString.replaceAll(OPTIONAL_SPONSOR_BLOCK_SETTINGS_PREFIX, "")); + return; + } else if (dataString.equals(OPEN_DEFAULT_APP_SETTINGS)) { + openDefaultAppSetting(); + return; + } + + final Setting settings = getSettingFromPath(dataString); + if (settings instanceof StringSetting stringSetting) { + if (settings.equals(CHANGE_START_PAGE)) { + ResettableListPreference.showDialog(mActivity, stringSetting, 2); + } else if (settings.equals(BYPASS_IMAGE_REGION_RESTRICTIONS_DOMAIN) + || settings.equals(CUSTOM_FILTER_STRINGS) + || settings.equals(CUSTOM_PLAYBACK_SPEEDS) + || settings.equals(HIDE_ACCOUNT_MENU_FILTER_STRINGS) + || settings.equals(RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY)) { + ResettableEditTextPreference.showDialog(mActivity, stringSetting); + } else if (settings.equals(EXTERNAL_DOWNLOADER_PACKAGE_NAME)) { + ExternalDownloaderPreference.showDialog(mActivity); + } else if (settings.equals(SB_API_URL)) { + SponsorBlockApiUrlPreference.showDialog(mActivity); + } else if (settings.equals(SPOOF_APP_VERSION_TARGET)) { + ResettableListPreference.showDialog(mActivity, stringSetting, 0); + } else { + Logger.printDebug(() -> "Failed to find the right value: " + dataString); + } + } else if (settings instanceof BooleanSetting) { + if (settings.equals(SETTINGS_IMPORT_EXPORT)) { + importExportListDialogBuilder(); + } else if (settings.equals(RETURN_YOUTUBE_USERNAME_ABOUT)) { + YouTubeDataAPIDialogBuilder.showDialog(mActivity); + } else { + Logger.printDebug(() -> "Failed to find the right value: " + dataString); + } + } else if (settings instanceof EnumSetting enumSetting) { + if (settings.equals(RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT) + || settings.equals(SPOOF_CLIENT_TYPE) + || settings.equals(SPOOF_STREAMING_DATA_TYPE)) { + ResettableListPreference.showDialog(mActivity, enumSetting, 0); + } + } + } catch (Exception ex) { + Logger.printException(() -> "onCreate failure", ex); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + private void openDefaultAppSetting() { + try { + Context context = getActivity(); + final Uri uri = Uri.parse("package:" + context.getPackageName()); + final Intent intent = isSDKAbove(31) + ? new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri) + : new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri); + context.startActivity(intent); + } catch (Exception exception) { + Logger.printException(() -> "openDefaultAppSetting failed"); + } + } + + /** + * Build a ListDialog for Import / Export settings + * When importing/exporting as file, {@link #onActivityResult} is used, so declare it here. + */ + private void importExportListDialogBuilder() { + try { + final Activity activity = getActivity(); + final String[] mEntries = getStringArray(IMPORT_EXPORT_SETTINGS_ENTRY_KEY); + + getDialogBuilder(activity) + .setTitle(str("revanced_extended_settings_import_export_title")) + .setItems(mEntries, (dialog, index) -> { + switch (index) { + case 0 -> exportActivity(); + case 1 -> importActivity(); + case 2 -> importExportEditTextDialogBuilder(); + } + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } catch (Exception ex) { + Logger.printException(() -> "importExportListDialogBuilder failure", ex); + } + } + + /** + * Build a EditTextDialog for Import / Export settings + */ + private void importExportEditTextDialogBuilder() { + try { + final Activity activity = getActivity(); + final EditText textView = new EditText(activity); + existingSettings = Setting.exportToJson(null); + textView.setText(existingSettings); + textView.setInputType(textView.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + textView.setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); // Use a smaller font to reduce text wrap. + + TextInputLayout textInputLayout = new TextInputLayout(activity); + textInputLayout.setLayoutParams(getLayoutParams()); + textInputLayout.addView(textView); + + FrameLayout container = new FrameLayout(activity); + container.addView(textInputLayout); + + getDialogBuilder(activity) + .setTitle(str("revanced_extended_settings_import_export_title")) + .setView(container) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(str("revanced_extended_settings_import_copy"), (dialog, which) -> Utils.setClipboard(textView.getText().toString(), str("revanced_share_copy_settings_success"))) + .setPositiveButton(str("revanced_extended_settings_import"), (dialog, which) -> importSettings(activity, textView.getText().toString())) + .show(); + } catch (Exception ex) { + Logger.printException(() -> "importExportEditTextDialogBuilder failure", ex); + } + } + + /** + * Invoke the SAF(Storage Access Framework) to export settings + */ + private void exportActivity() { + @SuppressLint("SimpleDateFormat") + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + + var appName = ExtendedUtils.getAppLabel(); + var versionName = ExtendedUtils.getAppVersionName(); + var formatDate = dateFormat.format(new Date(System.currentTimeMillis())); + var fileName = String.format("%s_v%s_%s.txt", appName, versionName, formatDate); + + var intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TITLE, fileName); + startActivityForResult(intent, WRITE_REQUEST_CODE); + } + + /** + * Invoke the SAF(Storage Access Framework) to import settings + */ + private void importActivity() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(isSDKAbove(29) ? "text/plain" : "*/*"); + startActivityForResult(intent, READ_REQUEST_CODE); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == WRITE_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + exportText(data.getData()); + } else if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) { + importText(data.getData()); + } + } + + private void exportText(Uri uri) { + try { + final Context context = this.getContext(); + + @SuppressLint("Recycle") + FileWriter jsonFileWriter = + new FileWriter( + Objects.requireNonNull(context.getApplicationContext() + .getContentResolver() + .openFileDescriptor(uri, "w")) + .getFileDescriptor() + ); + PrintWriter printWriter = new PrintWriter(jsonFileWriter); + printWriter.write(Setting.exportToJson(null)); + printWriter.close(); + jsonFileWriter.close(); + + showToastShort(str("revanced_extended_settings_export_success")); + } catch (IOException e) { + showToastShort(str("revanced_extended_settings_export_failed")); + } + } + + private void importText(Uri uri) { + final Context context = this.getContext(); + StringBuilder sb = new StringBuilder(); + String line; + + try { + @SuppressLint("Recycle") + FileReader fileReader = + new FileReader( + Objects.requireNonNull(context.getApplicationContext() + .getContentResolver() + .openFileDescriptor(uri, "r")) + .getFileDescriptor() + ); + BufferedReader bufferedReader = new BufferedReader(fileReader); + while ((line = bufferedReader.readLine()) != null) { + sb.append(line).append("\n"); + } + bufferedReader.close(); + fileReader.close(); + + final boolean restartNeeded = Setting.importFromJSON(context, sb.toString()); + if (restartNeeded) { + ReVancedPreferenceFragment.showRebootDialog(); + } + } catch (IOException e) { + showToastShort(str("revanced_extended_settings_import_failed")); + throw new RuntimeException(e); + } + } + + private void importSettings(Activity mActivity, String replacementSettings) { + try { + existingSettings = Setting.exportToJson(null); + if (replacementSettings.equals(existingSettings)) { + return; + } + final boolean restartNeeded = Setting.importFromJSON(mActivity, replacementSettings); + if (restartNeeded) { + ReVancedPreferenceFragment.showRebootDialog(); + } + } catch (Exception ex) { + Logger.printException(() -> "importSettings failure", ex); + } + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ResettableEditTextPreference.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ResettableEditTextPreference.java new file mode 100644 index 000000000..d94bfab37 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ResettableEditTextPreference.java @@ -0,0 +1,50 @@ +package app.revanced.extension.music.settings.preference; + +import static app.revanced.extension.music.utils.ExtendedUtils.getDialogBuilder; +import static app.revanced.extension.music.utils.ExtendedUtils.getLayoutParams; +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.app.Activity; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import com.google.android.material.textfield.TextInputLayout; + +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.utils.Logger; + +public class ResettableEditTextPreference { + + public static void showDialog(Activity mActivity, @NonNull Setting setting) { + try { + final EditText textView = new EditText(mActivity); + textView.setText(setting.get()); + + TextInputLayout textInputLayout = new TextInputLayout(mActivity); + textInputLayout.setLayoutParams(getLayoutParams()); + textInputLayout.addView(textView); + + FrameLayout container = new FrameLayout(mActivity); + container.addView(textInputLayout); + + getDialogBuilder(mActivity) + .setTitle(str(setting.key + "_title")) + .setView(container) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> { + setting.resetToDefault(); + ReVancedPreferenceFragment.showRebootDialog(); + }) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + setting.save(textView.getText().toString().trim()); + ReVancedPreferenceFragment.showRebootDialog(); + }) + .show(); + } catch (Exception ex) { + Logger.printException(() -> "showDialog failure", ex); + } + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ResettableListPreference.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ResettableListPreference.java new file mode 100644 index 000000000..b01f5bf2d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ResettableListPreference.java @@ -0,0 +1,81 @@ +package app.revanced.extension.music.settings.preference; + +import static app.revanced.extension.music.utils.ExtendedUtils.getDialogBuilder; +import static app.revanced.extension.shared.utils.ResourceUtils.getStringArray; +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.app.Activity; + +import androidx.annotation.NonNull; + +import java.util.Arrays; + +import app.revanced.extension.shared.settings.EnumSetting; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.utils.Logger; + +public class ResettableListPreference { + private static int mClickedDialogEntryIndex; + + public static void showDialog(Activity mActivity, @NonNull Setting setting, int defaultIndex) { + try { + final String settingsKey = setting.key; + + final String entryKey = settingsKey + "_entries"; + final String entryValueKey = settingsKey + "_entry_values"; + final String[] mEntries = getStringArray(entryKey); + final String[] mEntryValues = getStringArray(entryValueKey); + + final int findIndex = Arrays.binarySearch(mEntryValues, setting.get()); + mClickedDialogEntryIndex = findIndex >= 0 ? findIndex : defaultIndex; + + getDialogBuilder(mActivity) + .setTitle(str(settingsKey + "_title")) + .setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, + (dialog, id) -> mClickedDialogEntryIndex = id) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> { + setting.resetToDefault(); + ReVancedPreferenceFragment.showRebootDialog(); + }) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + setting.save(mEntryValues[mClickedDialogEntryIndex]); + ReVancedPreferenceFragment.showRebootDialog(); + }) + .show(); + } catch (Exception ex) { + Logger.printException(() -> "showDialog failure", ex); + } + } + + public static void showDialog(Activity mActivity, @NonNull EnumSetting setting, int defaultIndex) { + try { + final String settingsKey = setting.key; + + final String entryKey = settingsKey + "_entries"; + final String entryValueKey = settingsKey + "_entry_values"; + final String[] mEntries = getStringArray(entryKey); + final String[] mEntryValues = getStringArray(entryValueKey); + + final int findIndex = Arrays.binarySearch(mEntryValues, setting.get().toString()); + mClickedDialogEntryIndex = findIndex >= 0 ? findIndex : defaultIndex; + + getDialogBuilder(mActivity) + .setTitle(str(settingsKey + "_title")) + .setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, + (dialog, id) -> mClickedDialogEntryIndex = id) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> { + setting.resetToDefault(); + ReVancedPreferenceFragment.showRebootDialog(); + }) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + setting.saveValueFromString(mEntryValues[mClickedDialogEntryIndex]); + ReVancedPreferenceFragment.showRebootDialog(); + }) + .show(); + } catch (Exception ex) { + Logger.printException(() -> "showDialog failure", ex); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/SponsorBlockApiUrlPreference.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/SponsorBlockApiUrlPreference.java new file mode 100644 index 000000000..9b6c9a1a7 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/SponsorBlockApiUrlPreference.java @@ -0,0 +1,70 @@ +package app.revanced.extension.music.settings.preference; + +import static app.revanced.extension.music.utils.ExtendedUtils.getDialogBuilder; +import static app.revanced.extension.music.utils.ExtendedUtils.getLayoutParams; +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.app.Activity; +import android.util.Patterns; +import android.widget.EditText; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import com.google.android.material.textfield.TextInputLayout; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.settings.StringSetting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +public class SponsorBlockApiUrlPreference { + + public static void showDialog(Activity mActivity) { + try { + final StringSetting apiUrl = Settings.SB_API_URL; + + final EditText textView = new EditText(mActivity); + textView.setText(apiUrl.get()); + + TextInputLayout textInputLayout = new TextInputLayout(mActivity); + textInputLayout.setLayoutParams(getLayoutParams()); + textInputLayout.addView(textView); + + FrameLayout container = new FrameLayout(mActivity); + container.addView(textInputLayout); + + getDialogBuilder(mActivity) + .setTitle(str("revanced_sb_api_url")) + .setView(container) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> { + apiUrl.resetToDefault(); + Utils.showToastShort(str("revanced_sb_api_url_reset")); + }) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + String serverAddress = textView.getText().toString().trim(); + if (!isValidSBServerAddress(serverAddress)) { + Utils.showToastShort(str("revanced_sb_api_url_invalid")); + } else if (!serverAddress.equals(Settings.SB_API_URL.get())) { + apiUrl.save(serverAddress); + Utils.showToastShort(str("revanced_sb_api_url_changed")); + } + }) + .show(); + } catch (Exception ex) { + Logger.printException(() -> "showDialog failure", ex); + } + } + + public static boolean isValidSBServerAddress(@NonNull String serverAddress) { + if (!Patterns.WEB_URL.matcher(serverAddress).matches()) { + return false; + } + // Verify url is only the server address and does not contain a path such as: "https://sponsor.ajay.app/api/" + // Could use Patterns.compile, but this is simpler + final int lastDotIndex = serverAddress.lastIndexOf('.'); + return lastDotIndex == -1 || !serverAddress.substring(lastDotIndex).contains("/"); + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/SponsorBlockCategoryPreference.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/SponsorBlockCategoryPreference.java new file mode 100644 index 000000000..14dda2c78 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/SponsorBlockCategoryPreference.java @@ -0,0 +1,124 @@ +package app.revanced.extension.music.settings.preference; + +import static app.revanced.extension.music.utils.ExtendedUtils.getDialogBuilder; +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.app.Activity; +import android.app.AlertDialog; +import android.graphics.Color; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TableLayout; +import android.widget.TableRow; +import android.widget.TextView; + +import java.util.Arrays; +import java.util.Objects; + +import app.revanced.extension.music.sponsorblock.objects.CategoryBehaviour; +import app.revanced.extension.music.sponsorblock.objects.SegmentCategory; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +public class SponsorBlockCategoryPreference { + private static final String[] CategoryBehaviourEntries = {str("revanced_sb_skip_automatically"), str("revanced_sb_skip_ignore")}; + private static final CategoryBehaviour[] CategoryBehaviourEntryValues = {CategoryBehaviour.SKIP_AUTOMATICALLY, CategoryBehaviour.IGNORE}; + private static int mClickedDialogEntryIndex; + + + public static void showDialog(Activity baseActivity, String categoryString) { + try { + SegmentCategory category = Objects.requireNonNull(SegmentCategory.byCategoryKey(categoryString)); + final AlertDialog.Builder builder = getDialogBuilder(baseActivity); + TableLayout table = new TableLayout(baseActivity); + table.setOrientation(LinearLayout.HORIZONTAL); + table.setPadding(70, 0, 150, 0); + + TableRow row = new TableRow(baseActivity); + + TextView colorTextLabel = new TextView(baseActivity); + colorTextLabel.setText(str("revanced_sb_color_dot_label")); + row.addView(colorTextLabel); + + TextView colorDotView = new TextView(baseActivity); + colorDotView.setText(category.getCategoryColorDot()); + colorDotView.setPadding(30, 0, 30, 0); + row.addView(colorDotView); + + final EditText mEditText = new EditText(baseActivity); + mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS); + mEditText.setText(category.colorString()); + mEditText.addTextChangedListener(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 s) { + try { + String colorString = s.toString(); + if (!colorString.startsWith("#")) { + s.insert(0, "#"); // recursively calls back into this method + return; + } + if (colorString.length() > 7) { + s.delete(7, colorString.length()); + return; + } + final int color = Color.parseColor(colorString); + colorDotView.setText(SegmentCategory.getCategoryColorDot(color)); + } catch (IllegalArgumentException ex) { + // ignore + } + } + }); + mEditText.setLayoutParams(new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f)); + row.addView(mEditText); + + table.addView(row); + builder.setView(table); + builder.setTitle(category.title.toString()); + + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { + category.behaviour = CategoryBehaviourEntryValues[mClickedDialogEntryIndex]; + category.setBehaviour(category.behaviour); + SegmentCategory.updateEnabledCategories(); + + String colorString = mEditText.getText().toString(); + try { + if (!colorString.equals(category.colorString())) { + category.setColor(colorString); + Utils.showToastShort(str("revanced_sb_color_changed")); + } + } catch (IllegalArgumentException ex) { + Utils.showToastShort(str("revanced_sb_color_invalid")); + } + }); + builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> { + try { + category.resetColor(); + Utils.showToastShort(str("revanced_sb_color_reset")); + } catch (Exception ex) { + Logger.printException(() -> "setNeutralButton failure", ex); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + + final int index = Arrays.asList(CategoryBehaviourEntryValues).indexOf(category.behaviour); + mClickedDialogEntryIndex = Math.max(index, 0); + + builder.setSingleChoiceItems(CategoryBehaviourEntries, mClickedDialogEntryIndex, + (dialog, id) -> mClickedDialogEntryIndex = id); + builder.show(); + } catch (Exception ex) { + Logger.printException(() -> "dialogBuilder failure", ex); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/shared/PlayerType.kt b/extensions/shared/src/main/java/app/revanced/extension/music/shared/PlayerType.kt new file mode 100644 index 000000000..5ca6ba944 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/shared/PlayerType.kt @@ -0,0 +1,54 @@ +package app.revanced.extension.music.shared + +import app.revanced.extension.shared.utils.Event +import app.revanced.extension.shared.utils.Logger + +/** + * WatchWhile player type + */ +enum class PlayerType { + DISMISSED, + MINIMIZED, + MAXIMIZED_NOW_PLAYING, + MAXIMIZED_PLAYER_ADDITIONAL_VIEW, + FULLSCREEN, + SLIDING_VERTICALLY, + QUEUE_EXPANDING, + SLIDING_HORIZONTALLY; + + companion object { + + private val nameToPlayerType = values().associateBy { it.name } + + @JvmStatic + fun setFromString(enumName: String) { + val newType = nameToPlayerType[enumName] + if (newType == null) { + Logger.printException { "Unknown PlayerType encountered: $enumName" } + } else if (current != newType) { + Logger.printDebug { "PlayerType changed to: $newType" } + current = newType + } + } + + /** + * The current player type. + */ + @JvmStatic + var current + get() = currentPlayerType + private set(value) { + currentPlayerType = value + onChange(currentPlayerType) + } + + @Volatile // value is read/write from different threads + private var currentPlayerType = MINIMIZED + + /** + * player type change listener + */ + @JvmStatic + val onChange = Event() + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoInformation.java b/extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoInformation.java new file mode 100644 index 000000000..12ce65258 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoInformation.java @@ -0,0 +1,319 @@ +package app.revanced.extension.music.shared; + +import static app.revanced.extension.shared.utils.ResourceUtils.getString; +import static app.revanced.extension.shared.utils.Utils.getFormattedTimeStamp; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +/** + * Hooking class for the current playing video. + */ +@SuppressWarnings("unused") +public final class VideoInformation { + private static final float DEFAULT_YOUTUBE_MUSIC_PLAYBACK_SPEED = 1.0f; + private static final int DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY = -2; + private static final String DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY_STRING = getString("quality_auto"); + @NonNull + private static String videoId = ""; + + private static long videoLength = 0; + private static long videoTime = -1; + + /** + * The current playback speed + */ + private static float playbackSpeed = DEFAULT_YOUTUBE_MUSIC_PLAYBACK_SPEED; + /** + * The current video quality + */ + private static int videoQuality = DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY; + /** + * The current video quality string + */ + private static String videoQualityString = DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY_STRING; + /** + * The available qualities of the current video in human readable form: [1080, 720, 480] + */ + @Nullable + private static List videoQualities; + + /** + * Injection point. + */ + public static void initialize() { + videoTime = -1; + videoLength = 0; + Logger.printDebug(() -> "Initialized Player"); + } + + /** + * Injection point. + */ + public static void initializeMdx() { + Logger.printDebug(() -> "Initialized Mdx Player"); + } + + /** + * Id of the current video playing. Includes Shorts and YouTube Stories. + * + * @return The id of the video. Empty string if not set yet. + */ + @NonNull + public static String getVideoId() { + return videoId; + } + + /** + * Injection point. + * + * @param newlyLoadedVideoId id of the current video + */ + public static void setVideoId(@NonNull String newlyLoadedVideoId) { + if (Objects.equals(newlyLoadedVideoId, videoId)) { + return; + } + Logger.printDebug(() -> "New video id: " + newlyLoadedVideoId); + videoId = newlyLoadedVideoId; + } + + /** + * Seek on the current video. + * Does not function for playback of Shorts. + *

+ * Caution: If called from a videoTimeHook() callback, + * this will cause a recursive call into the same videoTimeHook() callback. + * + * @param seekTime The millisecond to seek the video to. + * @return if the seek was successful + */ + public static boolean seekTo(final long seekTime) { + Utils.verifyOnMainThread(); + try { + final long videoLength = getVideoLength(); + final long videoTime = getVideoTime(); + final long adjustedSeekTime = getAdjustedSeekTime(seekTime, videoLength); + + if (videoTime <= 0 || videoLength <= 0) { + Logger.printDebug(() -> "Skipping seekTo as the video is not initialized"); + return false; + } + + Logger.printDebug(() -> "Seeking to: " + getFormattedTimeStamp(adjustedSeekTime)); + + // Try regular playback controller first, and it will not succeed if casting. + if (overrideVideoTime(adjustedSeekTime)) return true; + Logger.printDebug(() -> "seekTo did not succeeded. Trying MXD."); + // Else the video is loading or changing videos, or video is casting to a different device. + + // Try calling the seekTo method of the MDX player director (called when casting). + // The difference has to be a different second mark in order to avoid infinite skip loops + // as the Lounge API only supports seconds. + if (adjustedSeekTime / 1000 == videoTime / 1000) { + Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small " + + "(" + (adjustedSeekTime - videoTime) + "ms)"); + return false; + } + + return overrideMDXVideoTime(adjustedSeekTime); + } catch (Exception ex) { + Logger.printException(() -> "Failed to seek", ex); + return false; + } + } + + // Prevent issues such as play/pause button or autoplay not working. + private static long getAdjustedSeekTime(final long seekTime, final long videoLength) { + // If the user skips to a section that is 500 ms before the video length, + // it will get stuck in a loop. + if (videoLength - seekTime > 500) { + return seekTime; + } else { + // Otherwise, just skips to a time longer than the video length. + // Paradoxically, if user skips to a section much longer than the video length, does not get stuck in a loop. + return Integer.MAX_VALUE; + } + } + + /** + * @return The current playback speed. + */ + public static float getPlaybackSpeed() { + return playbackSpeed; + } + + /** + * Injection point. + * + * @param newlyLoadedPlaybackSpeed The current playback speed. + */ + public static void setPlaybackSpeed(float newlyLoadedPlaybackSpeed) { + playbackSpeed = newlyLoadedPlaybackSpeed; + } + + /** + * @return The current video quality. + */ + public static int getVideoQuality() { + return videoQuality; + } + + /** + * @return The current video quality string. + */ + public static String getVideoQualityString() { + return videoQualityString; + } + + /** + * Injection point. + * + * @param newlyLoadedQuality The current video quality string. + */ + public static void setVideoQuality(String newlyLoadedQuality) { + if (newlyLoadedQuality == null) { + return; + } + try { + String splitVideoQuality; + if (newlyLoadedQuality.contains("p")) { + splitVideoQuality = newlyLoadedQuality.split("p")[0]; + videoQuality = Integer.parseInt(splitVideoQuality); + videoQualityString = splitVideoQuality + "p"; + } else if (newlyLoadedQuality.contains("s")) { + splitVideoQuality = newlyLoadedQuality.split("s")[0]; + videoQuality = Integer.parseInt(splitVideoQuality); + videoQualityString = splitVideoQuality + "s"; + } else { + videoQuality = DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY; + videoQualityString = DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY_STRING; + } + } catch (NumberFormatException ignored) { + } + } + + /** + * @return available video quality. + */ + public static int getAvailableVideoQuality(int preferredQuality) { + if (videoQualities != null) { + int qualityToUse = videoQualities.get(0); // first element is automatic mode + for (Integer quality : videoQualities) { + if (quality <= preferredQuality && qualityToUse < quality) { + qualityToUse = quality; + } + } + preferredQuality = qualityToUse; + } + return preferredQuality; + } + + /** + * Injection point. + * + * @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2 + */ + public static void setVideoQualityList(Object[] qualities) { + try { + if (videoQualities == null || videoQualities.size() != qualities.length) { + videoQualities = new ArrayList<>(qualities.length); + for (Object streamQuality : qualities) { + for (Field field : streamQuality.getClass().getFields()) { + if (field.getType().isAssignableFrom(Integer.TYPE) + && field.getName().length() <= 2) { + videoQualities.add(field.getInt(streamQuality)); + } + } + } + Logger.printDebug(() -> "videoQualities: " + videoQualities); + } + } catch (Exception ex) { + Logger.printException(() -> "Failed to set quality list", ex); + } + } + + /** + * Length of the current video playing. Includes Shorts. + * + * @return The length of the video in milliseconds. + * If the video is not yet loaded, or if the video is playing in the background with no video visible, + * then this returns zero. + */ + public static long getVideoLength() { + return videoLength; + } + + /** + * Injection point. + * + * @param length The length of the video in milliseconds. + */ + public static void setVideoLength(final long length) { + if (videoLength != length) { + videoLength = length; + } + } + + /** + * Playback time of the current video playing. Includes Shorts. + *

+ * Value will lag behind the actual playback time by a variable amount based on the playback speed. + *

+ * If playback speed is 2.0x, this value may be up to 2000ms behind the actual playback time. + * If playback speed is 1.0x, this value may be up to 1000ms behind the actual playback time. + * If playback speed is 0.5x, this value may be up to 500ms behind the actual playback time. + * Etc. + * + * @return The time of the video in milliseconds. -1 if not set yet. + */ + public static long getVideoTime() { + return videoTime; + } + + /** + * Injection point. + * Called on the main thread every 1000ms. + * + * @param currentPlaybackTime The current playback time of the video in milliseconds. + */ + public static void setVideoTime(final long currentPlaybackTime) { + videoTime = currentPlaybackTime; + } + + /** + * Overrides the current quality. + * Rest of the implementation added by patch. + */ + public static void overrideVideoQuality(int qualityOverride) { + Logger.printDebug(() -> "Overriding video quality to: " + qualityOverride); + } + + /** + * Overrides the current video time by seeking. + * Rest of the implementation added by patch. + */ + public static boolean overrideVideoTime(final long seekTime) { + // These instructions are ignored by patch. + Logger.printDebug(() -> "Seeking to " + seekTime); + return false; + } + + /** + * Overrides the current video time by seeking. (MDX player) + * Rest of the implementation added by patch. + */ + public static boolean overrideMDXVideoTime(final long seekTime) { + // These instructions are ignored by patch. + Logger.printDebug(() -> "Seeking to " + seekTime); + return false; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoType.kt b/extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoType.kt new file mode 100644 index 000000000..87711a27e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoType.kt @@ -0,0 +1,63 @@ +package app.revanced.extension.music.shared + +import app.revanced.extension.shared.utils.Event +import app.revanced.extension.shared.utils.Logger + +/** + * Music video type + */ +enum class VideoType { + MUSIC_VIDEO_TYPE_UNKNOWN, + MUSIC_VIDEO_TYPE_ATV, + MUSIC_VIDEO_TYPE_OMV, + MUSIC_VIDEO_TYPE_UGC, + MUSIC_VIDEO_TYPE_SHOULDER, + MUSIC_VIDEO_TYPE_OFFICIAL_SOURCE_MUSIC, + MUSIC_VIDEO_TYPE_PRIVATELY_OWNED_TRACK, + MUSIC_VIDEO_TYPE_LIVE_STREAM, + MUSIC_VIDEO_TYPE_PODCAST_EPISODE; + + companion object { + + private val nameToVideoType = values().associateBy { it.name } + + @JvmStatic + fun setFromString(enumName: String) { + val newType = nameToVideoType[enumName] + if (newType == null) { + Logger.printException { "Unknown VideoType encountered: $enumName" } + } else if (current != newType) { + Logger.printDebug { "VideoType changed to: $newType" } + current = newType + } + } + + /** + * The current video type. + */ + @JvmStatic + var current + get() = currentVideoType + private set(value) { + currentVideoType = value + onChange(currentVideoType) + } + + @Volatile // value is read/write from different threads + private var currentVideoType = MUSIC_VIDEO_TYPE_UNKNOWN + + /** + * player type change listener + */ + @JvmStatic + val onChange = Event() + } + + fun isMusicVideo(): Boolean { + return this == MUSIC_VIDEO_TYPE_OMV + } + + fun isPodCast(): Boolean { + return this == MUSIC_VIDEO_TYPE_PODCAST_EPISODE + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/SegmentPlaybackController.java b/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/SegmentPlaybackController.java new file mode 100644 index 000000000..948a8a92e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/SegmentPlaybackController.java @@ -0,0 +1,472 @@ +package app.revanced.extension.music.sponsorblock; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.graphics.Canvas; +import android.graphics.Rect; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.Objects; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.shared.VideoInformation; +import app.revanced.extension.music.sponsorblock.objects.CategoryBehaviour; +import app.revanced.extension.music.sponsorblock.objects.SponsorSegment; +import app.revanced.extension.music.sponsorblock.requests.SBRequester; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +/** + * Handles showing, scheduling, and skipping of all {@link SponsorSegment} for the current video. + *

+ * Class is not thread safe. All methods must be called on the main thread unless otherwise specified. + */ +@SuppressWarnings("unused") +public class SegmentPlaybackController { + @Nullable + private static String currentVideoId; + @Nullable + private static SponsorSegment[] segments; + /** + * Currently playing (non-highlight) segment that user can manually skip. + */ + @Nullable + private static SponsorSegment segmentCurrentlyPlaying; + /** + * Currently playing manual skip segment that is scheduled to hide. + * This will always be NULL or equal to {@link #segmentCurrentlyPlaying}. + */ + @Nullable + private static SponsorSegment scheduledHideSegment; + /** + * Upcoming segment that is scheduled to either autoskip or show the manual skip button. + */ + @Nullable + private static SponsorSegment scheduledUpcomingSegment; + /** + * System time (in milliseconds) of when to hide the skip button of {@link #segmentCurrentlyPlaying}. + * Value is zero if playback is not inside a segment ({@link #segmentCurrentlyPlaying} is null), + */ + private static long skipSegmentButtonEndTime; + + private static int sponsorBarAbsoluteLeft; + private static int sponsorAbsoluteBarRight; + private static int sponsorBarThickness = 7; + private static SponsorSegment lastSegmentSkipped; + private static long lastSegmentSkippedTime; + private static int toastNumberOfSegmentsSkipped; + @Nullable + private static SponsorSegment toastSegmentSkipped; + + private static void setSegments(@NonNull SponsorSegment[] videoSegments) { + Arrays.sort(videoSegments); + segments = videoSegments; + } + + /** + * Clears all downloaded data. + */ + private static void clearData() { + SponsorBlockSettings.initialize(); + currentVideoId = null; + segments = null; + segmentCurrentlyPlaying = null; + scheduledUpcomingSegment = null; + scheduledHideSegment = null; + skipSegmentButtonEndTime = 0; + toastSegmentSkipped = null; + toastNumberOfSegmentsSkipped = 0; + } + + /** + * Injection point. + */ + public static void setVideoId(@NonNull String videoId) { + try { + if (Objects.equals(currentVideoId, videoId)) { + return; + } + clearData(); + if (!Settings.SB_ENABLED.get()) { + return; + } + if (Utils.isNetworkNotConnected()) { + Logger.printDebug(() -> "Network not connected, ignoring video"); + return; + } + + currentVideoId = videoId; + Logger.printDebug(() -> "setCurrentVideoId: " + videoId); + + Utils.runOnBackgroundThread(() -> { + try { + executeDownloadSegments(videoId); + } catch (Exception e) { + Logger.printException(() -> "Failed to download segments", e); + } + }); + } catch (Exception ex) { + Logger.printException(() -> "setCurrentVideoId failure", ex); + } + } + + /** + * Must be called off main thread + */ + static void executeDownloadSegments(@NonNull String videoId) { + Objects.requireNonNull(videoId); + try { + SponsorSegment[] segments = SBRequester.getSegments(videoId); + + Utils.runOnMainThread(() -> { + if (!videoId.equals(currentVideoId)) { + // user changed videos before get segments network call could complete + Logger.printDebug(() -> "Ignoring segments for prior video: " + videoId); + return; + } + setSegments(segments); + + // check for any skips now, instead of waiting for the next update to setVideoTime() + setVideoTime(VideoInformation.getVideoTime()); + }); + } catch (Exception ex) { + Logger.printException(() -> "executeDownloadSegments failure", ex); + } + } + + /** + * Injection point. + * Updates SponsorBlock every 1000ms. + * When changing videos, this is first called with value 0 and then the video is changed. + */ + public static void setVideoTime(long millis) { + try { + if (!Settings.SB_ENABLED.get() || segments == null || segments.length == 0) { + return; + } + Logger.printDebug(() -> "setVideoTime: " + millis); + + final float playbackSpeed = VideoInformation.getPlaybackSpeed(); + // Amount of time to look ahead for the next segment, + // and the threshold to determine if a scheduled show/hide is at the correct video time when it's run. + // + // This value must be greater than largest time between calls to this method (1000ms), + // and must be adjusted for the video speed. + // + // To debug the stale skip logic, set this to a very large value (5000 or more) + // then try manually seeking just before playback reaches a segment skip. + final long speedAdjustedTimeThreshold = (long) (playbackSpeed * 1200); + final long startTimerLookAheadThreshold = millis + speedAdjustedTimeThreshold; + + SponsorSegment foundSegmentCurrentlyPlaying = null; + SponsorSegment foundUpcomingSegment = null; + + for (final SponsorSegment segment : segments) { + if (segment.category.behaviour == CategoryBehaviour.IGNORE) { + continue; + } + if (segment.end <= millis) { + continue; // past this segment + } + + if (segment.start <= millis) { + // we are in the segment! + if (segment.shouldAutoSkip()) { + skipSegment(segment); + return; // must return, as skipping causes a recursive call back into this method + } + + // first found segment, or it's an embedded segment and fully inside the outer segment + if (foundSegmentCurrentlyPlaying == null || foundSegmentCurrentlyPlaying.containsSegment(segment)) { + // If the found segment is not currently displayed, then do not show if the segment is nearly over. + // This check prevents the skip button text from rapidly changing when multiple segments end at nearly the same time. + // Also prevents showing the skip button if user seeks into the last 800ms of the segment. + final long minMillisOfSegmentRemainingThreshold = 800; + if (segmentCurrentlyPlaying == segment + || !segment.endIsNear(millis, minMillisOfSegmentRemainingThreshold)) { + foundSegmentCurrentlyPlaying = segment; + } else { + Logger.printDebug(() -> "Ignoring segment that ends very soon: " + segment); + } + } + // Keep iterating and looking. There may be an upcoming autoskip, + // or there may be another smaller segment nested inside this segment + continue; + } + + // segment is upcoming + if (startTimerLookAheadThreshold < segment.start) { + break; // segment is not close enough to schedule, and no segments after this are of interest + } + if (segment.shouldAutoSkip()) { // upcoming autoskip + foundUpcomingSegment = segment; + break; // must stop here + } + + // upcoming manual skip + + // do not schedule upcoming segment, if it is not fully contained inside the current segment + if ((foundSegmentCurrentlyPlaying == null || foundSegmentCurrentlyPlaying.containsSegment(segment)) + // use the most inner upcoming segment + && (foundUpcomingSegment == null || foundUpcomingSegment.containsSegment(segment))) { + + // Only schedule, if the segment start time is not near the end time of the current segment. + // This check is needed to prevent scheduled hide and show from clashing with each other. + // Instead the upcoming segment will be handled when the current segment scheduled hide calls back into this method. + final long minTimeBetweenStartEndOfSegments = 1000; + if (foundSegmentCurrentlyPlaying == null + || !foundSegmentCurrentlyPlaying.endIsNear(segment.start, minTimeBetweenStartEndOfSegments)) { + foundUpcomingSegment = segment; + } else { + Logger.printDebug(() -> "Not scheduling segment (start time is near end of current segment): " + segment); + } + } + } + + if (segmentCurrentlyPlaying != foundSegmentCurrentlyPlaying) { + setSegmentCurrentlyPlaying(foundSegmentCurrentlyPlaying); + } else if (foundSegmentCurrentlyPlaying != null + && skipSegmentButtonEndTime != 0 && skipSegmentButtonEndTime <= System.currentTimeMillis()) { + Logger.printDebug(() -> "Auto hiding skip button for segment: " + segmentCurrentlyPlaying); + skipSegmentButtonEndTime = 0; + } + + // schedule a hide, only if the segment end is near + final SponsorSegment segmentToHide = + (foundSegmentCurrentlyPlaying != null && foundSegmentCurrentlyPlaying.endIsNear(millis, speedAdjustedTimeThreshold)) + ? foundSegmentCurrentlyPlaying + : null; + + if (scheduledHideSegment != segmentToHide) { + if (segmentToHide == null) { + Logger.printDebug(() -> "Clearing scheduled hide: " + scheduledHideSegment); + scheduledHideSegment = null; + } else { + scheduledHideSegment = segmentToHide; + Logger.printDebug(() -> "Scheduling hide segment: " + segmentToHide + " playbackSpeed: " + playbackSpeed); + final long delayUntilHide = (long) ((segmentToHide.end - millis) / playbackSpeed); + Utils.runOnMainThreadDelayed(() -> { + if (scheduledHideSegment != segmentToHide) { + Logger.printDebug(() -> "Ignoring old scheduled hide segment: " + segmentToHide); + return; + } + scheduledHideSegment = null; + + final long videoTime = VideoInformation.getVideoTime(); + if (!segmentToHide.endIsNear(videoTime, speedAdjustedTimeThreshold)) { + // current video time is not what's expected. User paused playback + Logger.printDebug(() -> "Ignoring outdated scheduled hide: " + segmentToHide + + " videoInformation time: " + videoTime); + return; + } + Logger.printDebug(() -> "Running scheduled hide segment: " + segmentToHide); + // Need more than just hide the skip button, as this may have been an embedded segment + // Instead call back into setVideoTime to check everything again. + // Should not use VideoInformation time as it is less accurate, + // but this scheduled handler was scheduled precisely so we can just use the segment end time + setSegmentCurrentlyPlaying(null); + setVideoTime(segmentToHide.end); + }, delayUntilHide); + } + } + + if (scheduledUpcomingSegment != foundUpcomingSegment) { + if (foundUpcomingSegment == null) { + Logger.printDebug(() -> "Clearing scheduled segment: " + scheduledUpcomingSegment); + scheduledUpcomingSegment = null; + } else { + scheduledUpcomingSegment = foundUpcomingSegment; + final SponsorSegment segmentToSkip = foundUpcomingSegment; + + Logger.printDebug(() -> "Scheduling segment: " + segmentToSkip + " playbackSpeed: " + playbackSpeed); + final long delayUntilSkip = (long) ((segmentToSkip.start - millis) / playbackSpeed); + Utils.runOnMainThreadDelayed(() -> { + if (scheduledUpcomingSegment != segmentToSkip) { + Logger.printDebug(() -> "Ignoring old scheduled segment: " + segmentToSkip); + return; + } + scheduledUpcomingSegment = null; + + final long videoTime = VideoInformation.getVideoTime(); + if (!segmentToSkip.startIsNear(videoTime, speedAdjustedTimeThreshold)) { + // current video time is not what's expected. User paused playback + Logger.printDebug(() -> "Ignoring outdated scheduled segment: " + segmentToSkip + + " videoInformation time: " + videoTime); + return; + } + if (segmentToSkip.shouldAutoSkip()) { + Logger.printDebug(() -> "Running scheduled skip segment: " + segmentToSkip); + skipSegment(segmentToSkip); + } else { + Logger.printDebug(() -> "Running scheduled show segment: " + segmentToSkip); + setSegmentCurrentlyPlaying(segmentToSkip); + } + }, delayUntilSkip); + } + } + } catch (Exception e) { + Logger.printException(() -> "setVideoTime failure", e); + } + } + + private static void setSegmentCurrentlyPlaying(@Nullable SponsorSegment segment) { + if (segment == null) { + if (segmentCurrentlyPlaying != null) + Logger.printDebug(() -> "Hiding segment: " + segmentCurrentlyPlaying); + segmentCurrentlyPlaying = null; + skipSegmentButtonEndTime = 0; + return; + } + segmentCurrentlyPlaying = segment; + skipSegmentButtonEndTime = 0; + Logger.printDebug(() -> "Showing segment: " + segment); + } + + private static void skipSegment(@NonNull SponsorSegment segmentToSkip) { + try { + // If trying to seek to end of the video, YouTube can seek just before of the actual end. + // (especially if the video does not end on a whole second boundary). + // This causes additional segment skip attempts, even though it cannot seek any closer to the desired time. + // Check for and ignore repeated skip attempts of the same segment over a small time period. + final long now = System.currentTimeMillis(); + final long minimumMillisecondsBetweenSkippingSameSegment = 500; + if ((lastSegmentSkipped == segmentToSkip) && (now - lastSegmentSkippedTime < minimumMillisecondsBetweenSkippingSameSegment)) { + Logger.printDebug(() -> "Ignoring skip segment request (already skipped as close as possible): " + segmentToSkip); + return; + } + + Logger.printDebug(() -> "Skipping segment: " + segmentToSkip); + lastSegmentSkipped = segmentToSkip; + lastSegmentSkippedTime = now; + setSegmentCurrentlyPlaying(null); + scheduledHideSegment = null; + scheduledUpcomingSegment = null; + + // If the seek is successful, then the seek causes a recursive call back into this class. + final boolean seekSuccessful = VideoInformation.seekTo(segmentToSkip.end); + if (!seekSuccessful) { + // can happen when switching videos and is normal + Logger.printDebug(() -> "Could not skip segment (seek unsuccessful): " + segmentToSkip); + return; + } + + // check for any smaller embedded segments, and count those as autoskipped + final boolean showSkipToast = Settings.SB_TOAST_ON_SKIP.get(); + for (final SponsorSegment otherSegment : Objects.requireNonNull(segments)) { + if (segmentToSkip.end < otherSegment.start) { + break; // no other segments can be contained + } + if (otherSegment == segmentToSkip || + segmentToSkip.containsSegment(otherSegment)) { + otherSegment.didAutoSkipped = true; + // Do not show a toast if the user is scrubbing thru a paused video. + // Cannot do this video state check in setTime or earlier in this method, as the video state may not be up to date. + // So instead, only hide toasts because all other skip logic done while paused causes no harm. + if (showSkipToast) { + showSkippedSegmentToast(otherSegment); + } + } + } + } catch (Exception ex) { + Logger.printException(() -> "skipSegment failure", ex); + } + } + + private static void showSkippedSegmentToast(@NonNull SponsorSegment segment) { + Utils.verifyOnMainThread(); + toastNumberOfSegmentsSkipped++; + if (toastNumberOfSegmentsSkipped > 1) { + return; // toast already scheduled + } + toastSegmentSkipped = segment; + + final long delayToToastMilliseconds = 250; // also the maximum time between skips to be considered skipping multiple segments + Utils.runOnMainThreadDelayed(() -> { + try { + if (toastSegmentSkipped == null) { // video was changed just after skipping segment + Logger.printDebug(() -> "Ignoring old scheduled show toast"); + return; + } + Utils.showToastShort(toastNumberOfSegmentsSkipped == 1 + ? toastSegmentSkipped.getSkippedToastText() + : str("revanced_sb_skipped_multiple_segments")); + } catch (Exception ex) { + Logger.printException(() -> "showSkippedSegmentToast failure", ex); + } finally { + toastNumberOfSegmentsSkipped = 0; + toastSegmentSkipped = null; + } + }, delayToToastMilliseconds); + } + + /** + * Injection point + */ + public static void setSponsorBarRect(final Object self, final String fieldName) { + try { + Field field = self.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + Rect rect = (Rect) Objects.requireNonNull(field.get(self)); + setSponsorBarAbsoluteLeft(rect); + setSponsorBarAbsoluteRight(rect); + } catch (Exception ex) { + Logger.printException(() -> "setSponsorBarRect failure", ex); + } + } + + private static void setSponsorBarAbsoluteLeft(Rect rect) { + final int left = rect.left; + if (sponsorBarAbsoluteLeft != left) { + Logger.printDebug(() -> "setSponsorBarAbsoluteLeft: " + left); + sponsorBarAbsoluteLeft = left; + } + } + + private static void setSponsorBarAbsoluteRight(Rect rect) { + final int right = rect.right; + if (sponsorAbsoluteBarRight != right) { + Logger.printDebug(() -> "setSponsorBarAbsoluteRight: " + right); + sponsorAbsoluteBarRight = right; + } + } + + /** + * Injection point + */ + public static void setSponsorBarThickness(int thickness) { + if (sponsorBarThickness != thickness) { + sponsorBarThickness = (int) Math.round(thickness * 1.2); + Logger.printDebug(() -> "setSponsorBarThickness: " + sponsorBarThickness); + } + } + + /** + * Injection point. + */ + public static void drawSponsorTimeBars(final Canvas canvas, final float posY) { + try { + if (segments == null) return; + final long videoLength = VideoInformation.getVideoLength(); + if (videoLength <= 0) return; + + final int thicknessDiv2 = sponsorBarThickness / 2; // rounds down + final float top = posY - (sponsorBarThickness - thicknessDiv2); + final float bottom = posY + thicknessDiv2; + final float videoMillisecondsToPixels = (1f / videoLength) * (sponsorAbsoluteBarRight - sponsorBarAbsoluteLeft); + final float leftPadding = sponsorBarAbsoluteLeft; + + for (SponsorSegment segment : segments) { + final float left = leftPadding + segment.start * videoMillisecondsToPixels; + final float right = leftPadding + segment.end * videoMillisecondsToPixels; + canvas.drawRect(left, top, right, bottom, segment.category.paint); + } + } catch (Exception ex) { + Logger.printException(() -> "drawSponsorTimeBars failure", ex); + } + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/SponsorBlockSettings.java b/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/SponsorBlockSettings.java new file mode 100644 index 000000000..eef25d14d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/SponsorBlockSettings.java @@ -0,0 +1,60 @@ +package app.revanced.extension.music.sponsorblock; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.UUID; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.sponsorblock.objects.SegmentCategory; +import app.revanced.extension.shared.settings.Setting; + +public class SponsorBlockSettings { + + public static final Setting.ImportExportCallback SB_IMPORT_EXPORT_CALLBACK = new Setting.ImportExportCallback() { + @Override + public void settingsImported(@Nullable Context context) { + SegmentCategory.loadAllCategoriesFromSettings(); + } + + @Override + public void settingsExported(@Nullable Context context) { + } + }; + + private static boolean initialized; + + /** + * @return if the user has ever voted, created a segment, or imported existing SB settings. + */ + public static boolean userHasSBPrivateId() { + return !Settings.SB_PRIVATE_USER_ID.get().isEmpty(); + } + + /** + * Use this only if a user id is required (creating segments, voting). + */ + @NonNull + public static String getSBPrivateUserID() { + String uuid = Settings.SB_PRIVATE_USER_ID.get(); + if (uuid.isEmpty()) { + uuid = (UUID.randomUUID().toString() + + UUID.randomUUID().toString() + + UUID.randomUUID().toString()) + .replace("-", ""); + Settings.SB_PRIVATE_USER_ID.save(uuid); + } + return uuid; + } + + public static void initialize() { + if (initialized) { + return; + } + initialized = true; + + SegmentCategory.updateEnabledCategories(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/objects/CategoryBehaviour.java b/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/objects/CategoryBehaviour.java new file mode 100644 index 000000000..bba2334dc --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/objects/CategoryBehaviour.java @@ -0,0 +1,49 @@ +package app.revanced.extension.music.sponsorblock.objects; + +import static app.revanced.extension.shared.utils.StringRef.sf; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +import app.revanced.extension.shared.utils.StringRef; + +public enum CategoryBehaviour { + SKIP_AUTOMATICALLY("skip", 2, true, sf("revanced_sb_skip_automatically")), + // ignored categories are not exported to json, and ignore is the default behavior when importing + IGNORE("ignore", -1, false, sf("revanced_sb_skip_ignore")); + + /** + * ReVanced specific value. + */ + @NonNull + public final String reVancedKeyValue; + /** + * Desktop specific value. + */ + public final int desktopKeyValue; + /** + * If the segment should skip automatically + */ + public final boolean skipAutomatically; + @NonNull + public final StringRef description; + + CategoryBehaviour(String reVancedKeyValue, int desktopKeyValue, boolean skipAutomatically, StringRef description) { + this.reVancedKeyValue = Objects.requireNonNull(reVancedKeyValue); + this.desktopKeyValue = desktopKeyValue; + this.skipAutomatically = skipAutomatically; + this.description = Objects.requireNonNull(description); + } + + @Nullable + public static CategoryBehaviour byReVancedKeyValue(@NonNull String keyValue) { + for (CategoryBehaviour behaviour : values()) { + if (behaviour.reVancedKeyValue.equals(keyValue)) { + return behaviour; + } + } + return null; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/objects/SegmentCategory.java b/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/objects/SegmentCategory.java new file mode 100644 index 000000000..d20827e6f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/objects/SegmentCategory.java @@ -0,0 +1,293 @@ +package app.revanced.extension.music.sponsorblock.objects; + +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_FILLER; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_FILLER_COLOR; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_INTERACTION; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_INTERACTION_COLOR; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_INTRO; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_INTRO_COLOR; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC_COLOR; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_OUTRO; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_OUTRO_COLOR; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_PREVIEW; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_PREVIEW_COLOR; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_SELF_PROMO; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_SELF_PROMO_COLOR; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_SPONSOR; +import static app.revanced.extension.music.settings.Settings.SB_CATEGORY_SPONSOR_COLOR; +import static app.revanced.extension.shared.utils.StringRef.sf; + +import android.graphics.Color; +import android.graphics.Paint; +import android.text.Html; +import android.text.Spanned; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import app.revanced.extension.shared.settings.StringSetting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.StringRef; +import app.revanced.extension.shared.utils.Utils; + +public enum SegmentCategory { + SPONSOR("sponsor", sf("revanced_sb_segments_sponsor"), sf("revanced_sb_segments_sponsor_sum"), sf("revanced_sb_skip_button_sponsor"), sf("revanced_sb_skipped_sponsor"), + SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR), + SELF_PROMO("selfpromo", sf("revanced_sb_segments_selfpromo"), sf("revanced_sb_segments_selfpromo_sum"), sf("revanced_sb_skip_button_selfpromo"), sf("revanced_sb_skipped_selfpromo"), + SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR), + INTERACTION("interaction", sf("revanced_sb_segments_interaction"), sf("revanced_sb_segments_interaction_sum"), sf("revanced_sb_skip_button_interaction"), sf("revanced_sb_skipped_interaction"), + SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR), + INTRO("intro", sf("revanced_sb_segments_intro"), sf("revanced_sb_segments_intro_sum"), + sf("revanced_sb_skip_button_intro_beginning"), sf("revanced_sb_skip_button_intro_middle"), sf("revanced_sb_skip_button_intro_end"), + sf("revanced_sb_skipped_intro_beginning"), sf("revanced_sb_skipped_intro_middle"), sf("revanced_sb_skipped_intro_end"), + SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR), + OUTRO("outro", sf("revanced_sb_segments_outro"), sf("revanced_sb_segments_outro_sum"), sf("revanced_sb_skip_button_outro"), sf("revanced_sb_skipped_outro"), + SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR), + PREVIEW("preview", sf("revanced_sb_segments_preview"), sf("revanced_sb_segments_preview_sum"), + sf("revanced_sb_skip_button_preview_beginning"), sf("revanced_sb_skip_button_preview_middle"), sf("revanced_sb_skip_button_preview_end"), + sf("revanced_sb_skipped_preview_beginning"), sf("revanced_sb_skipped_preview_middle"), sf("revanced_sb_skipped_preview_end"), + SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR), + FILLER("filler", sf("revanced_sb_segments_filler"), sf("revanced_sb_segments_filler_sum"), sf("revanced_sb_skip_button_filler"), sf("revanced_sb_skipped_filler"), + SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR), + MUSIC_OFFTOPIC("music_offtopic", sf("revanced_sb_segments_nomusic"), sf("revanced_sb_segments_nomusic_sum"), sf("revanced_sb_skip_button_nomusic"), sf("revanced_sb_skipped_nomusic"), + SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR); + + private static final SegmentCategory[] categoriesWithoutUnsubmitted = new SegmentCategory[]{ + SPONSOR, + SELF_PROMO, + INTERACTION, + INTRO, + OUTRO, + PREVIEW, + FILLER, + MUSIC_OFFTOPIC, + }; + private static final Map mValuesMap = new HashMap<>(2 * categoriesWithoutUnsubmitted.length); + + /** + * Categories currently enabled, formatted for an API call + */ + public static String sponsorBlockAPIFetchCategories = "[]"; + + static { + for (SegmentCategory value : categoriesWithoutUnsubmitted) + mValuesMap.put(value.keyValue, value); + } + + @NonNull + public static SegmentCategory[] categoriesWithoutUnsubmitted() { + return categoriesWithoutUnsubmitted; + } + + @Nullable + public static SegmentCategory byCategoryKey(@NonNull String key) { + return mValuesMap.get(key); + } + + /** + * Must be called if behavior of any category is changed + */ + public static void updateEnabledCategories() { + Utils.verifyOnMainThread(); + Logger.printDebug(() -> "updateEnabledCategories"); + SegmentCategory[] categories = categoriesWithoutUnsubmitted(); + List enabledCategories = new ArrayList<>(categories.length); + for (SegmentCategory category : categories) { + if (category.behaviour != CategoryBehaviour.IGNORE) { + enabledCategories.add(category.keyValue); + } + } + + //"[%22sponsor%22,%22outro%22,%22music_offtopic%22,%22intro%22,%22selfpromo%22,%22interaction%22,%22preview%22]"; + if (enabledCategories.isEmpty()) + sponsorBlockAPIFetchCategories = "[]"; + else + sponsorBlockAPIFetchCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]"; + } + + public static void loadAllCategoriesFromSettings() { + for (SegmentCategory category : values()) { + category.loadFromSettings(); + } + updateEnabledCategories(); + } + + @NonNull + public final String keyValue; + @NonNull + private final StringSetting behaviorSetting; + @NonNull + private final StringSetting colorSetting; + + @NonNull + public final StringRef title; + @NonNull + public final StringRef description; + + /** + * Skip button text, if the skip occurs in the first quarter of the video + */ + @NonNull + public final StringRef skipButtonTextBeginning; + /** + * Skip button text, if the skip occurs in the middle half of the video + */ + @NonNull + public final StringRef skipButtonTextMiddle; + /** + * Skip button text, if the skip occurs in the last quarter of the video + */ + @NonNull + public final StringRef skipButtonTextEnd; + /** + * Skipped segment toast, if the skip occurred in the first quarter of the video + */ + @NonNull + public final StringRef skippedToastBeginning; + /** + * Skipped segment toast, if the skip occurred in the middle half of the video + */ + @NonNull + public final StringRef skippedToastMiddle; + /** + * Skipped segment toast, if the skip occurred in the last quarter of the video + */ + @NonNull + public final StringRef skippedToastEnd; + + @NonNull + public final Paint paint; + + /** + * Value must be changed using {@link #setColor(String)}. + */ + public int color; + + /** + * Value must be changed using {@link #setBehaviour(CategoryBehaviour)}. + * Caller must also {@link #updateEnabledCategories()}. + */ + @NonNull + public CategoryBehaviour behaviour = CategoryBehaviour.SKIP_AUTOMATICALLY; + + SegmentCategory(String keyValue, StringRef title, StringRef description, + StringRef skipButtonText, + StringRef skippedToastText, + StringSetting behavior, StringSetting color) { + this(keyValue, title, description, + skipButtonText, skipButtonText, skipButtonText, + skippedToastText, skippedToastText, skippedToastText, + behavior, color); + } + + SegmentCategory(String keyValue, StringRef title, StringRef description, + StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd, + StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd, + StringSetting behavior, StringSetting color) { + this.keyValue = Objects.requireNonNull(keyValue); + this.title = Objects.requireNonNull(title); + this.description = Objects.requireNonNull(description); + this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning); + this.skipButtonTextMiddle = Objects.requireNonNull(skipButtonTextMiddle); + this.skipButtonTextEnd = Objects.requireNonNull(skipButtonTextEnd); + this.skippedToastBeginning = Objects.requireNonNull(skippedToastBeginning); + this.skippedToastMiddle = Objects.requireNonNull(skippedToastMiddle); + this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd); + this.behaviorSetting = Objects.requireNonNull(behavior); + this.colorSetting = Objects.requireNonNull(color); + this.paint = new Paint(); + loadFromSettings(); + } + + private void loadFromSettings() { + String behaviorString = behaviorSetting.get(); + CategoryBehaviour savedBehavior = CategoryBehaviour.byReVancedKeyValue(behaviorString); + if (savedBehavior == null) { + Logger.printException(() -> "Invalid behavior: " + behaviorString); + behaviorSetting.resetToDefault(); + loadFromSettings(); + return; + } + this.behaviour = savedBehavior; + + String colorString = colorSetting.get(); + try { + setColor(colorString); + } catch (Exception ex) { + Logger.printException(() -> "Invalid color: " + colorString, ex); + colorSetting.resetToDefault(); + loadFromSettings(); + } + } + + public void setBehaviour(@NonNull CategoryBehaviour behaviour) { + this.behaviour = Objects.requireNonNull(behaviour); + this.behaviorSetting.save(behaviour.reVancedKeyValue); + } + + /** + * @return HTML color format string + */ + @NonNull + public String colorString() { + return String.format("#%06X", color); + } + + public void setColor(@NonNull String colorString) throws IllegalArgumentException { + final int color = Color.parseColor(colorString) & 0xFFFFFF; + this.color = color; + paint.setColor(color); + paint.setAlpha(255); + colorSetting.save(colorString); // Save after parsing. + } + + public void resetColor() { + setColor(colorSetting.defaultValue); + } + + @NonNull + private static String getCategoryColorDotHTML(int color) { + color &= 0xFFFFFF; + return String.format("", color); + } + + /** + * @noinspection deprecation + */ + @NonNull + public static Spanned getCategoryColorDot(int color) { + return Html.fromHtml(getCategoryColorDotHTML(color)); + } + + @NonNull + public Spanned getCategoryColorDot() { + return getCategoryColorDot(color); + } + + /** + * @param segmentStartTime video time the segment category started + * @param videoLength length of the video + * @return 'skipped segment' toast message + */ + @NonNull + StringRef getSkippedToastText(long segmentStartTime, long videoLength) { + if (videoLength == 0) { + return skippedToastBeginning; // video is still loading. Assume it's the beginning + } + final float position = segmentStartTime / (float) videoLength; + if (position < 0.25f) { + return skippedToastBeginning; + } else if (position < 0.75f) { + return skippedToastMiddle; + } + return skippedToastEnd; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/objects/SponsorSegment.java b/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/objects/SponsorSegment.java new file mode 100644 index 000000000..85c2e0c26 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/objects/SponsorSegment.java @@ -0,0 +1,102 @@ +package app.revanced.extension.music.sponsorblock.objects; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +import app.revanced.extension.music.shared.VideoInformation; + +public class SponsorSegment implements Comparable { + @NonNull + public final SegmentCategory category; + /** + * NULL if segment is unsubmitted + */ + @Nullable + public final String UUID; + public final long start; + public final long end; + public final boolean isLocked; + public boolean didAutoSkipped = false; + + public SponsorSegment(@NonNull SegmentCategory category, @Nullable String UUID, long start, long end, boolean isLocked) { + this.category = category; + this.UUID = UUID; + this.start = start; + this.end = end; + this.isLocked = isLocked; + } + + public boolean shouldAutoSkip() { + return category.behaviour.skipAutomatically; + } + + /** + * @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number + */ + public boolean startIsNear(long videoTime, long nearThreshold) { + return Math.abs(start - videoTime) <= nearThreshold; + } + + /** + * @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number + */ + public boolean endIsNear(long videoTime, long nearThreshold) { + return Math.abs(end - videoTime) <= nearThreshold; + } + + /** + * @return if the segment is completely contained inside this segment + */ + public boolean containsSegment(SponsorSegment other) { + return start <= other.start && other.end <= end; + } + + /** + * @return the length of this segment, in milliseconds. Always a positive number. + */ + public long length() { + return end - start; + } + + /** + * @return 'skipped segment' toast message + */ + @NonNull + public String getSkippedToastText() { + return category.getSkippedToastText(start, VideoInformation.getVideoLength()).toString(); + } + + @Override + public int compareTo(SponsorSegment o) { + // If both segments start at the same time, then sort with the longer segment first. + // This keeps the seekbar drawing correct since it draws the segments using the sorted order. + return start == o.start ? Long.compare(o.length(), length()) : Long.compare(start, o.start); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SponsorSegment other)) return false; + return Objects.equals(UUID, other.UUID) + && category == other.category + && start == other.start + && end == other.end; + } + + @Override + public int hashCode() { + return Objects.hashCode(UUID); + } + + @NonNull + @Override + public String toString() { + return "SponsorSegment{" + + "category=" + category + + ", start=" + start + + ", end=" + end + + '}'; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/requests/SBRequester.java b/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/requests/SBRequester.java new file mode 100644 index 000000000..0b520fbfc --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/requests/SBRequester.java @@ -0,0 +1,145 @@ +package app.revanced.extension.music.sponsorblock.requests; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.sponsorblock.SponsorBlockSettings; +import app.revanced.extension.music.sponsorblock.objects.SegmentCategory; +import app.revanced.extension.music.sponsorblock.objects.SponsorSegment; +import app.revanced.extension.shared.requests.Requester; +import app.revanced.extension.shared.requests.Route; +import app.revanced.extension.shared.sponsorblock.requests.SBRoutes; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +public class SBRequester { + /** + * TCP timeout + */ + private static final int TIMEOUT_TCP_DEFAULT_MILLISECONDS = 7000; + + /** + * HTTP response timeout + */ + private static final int TIMEOUT_HTTP_DEFAULT_MILLISECONDS = 10000; + + /** + * Response code of a successful API call + */ + private static final int HTTP_STATUS_CODE_SUCCESS = 200; + + private SBRequester() { + } + + private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) { + if (Settings.SB_TOAST_ON_CONNECTION_ERROR.get()) { + Utils.showToastShort(toastMessage); + } + if (ex != null) { + Logger.printInfo(() -> toastMessage, ex); + } + } + + @NonNull + public static SponsorSegment[] getSegments(@NonNull String videoId) { + Utils.verifyOffMainThread(); + List segments = new ArrayList<>(); + try { + HttpURLConnection connection = getConnectionFromRoute(SBRoutes.GET_SEGMENTS, videoId, SegmentCategory.sponsorBlockAPIFetchCategories); + final int responseCode = connection.getResponseCode(); + + if (responseCode == HTTP_STATUS_CODE_SUCCESS) { + JSONArray responseArray = Requester.parseJSONArray(connection); + final long minSegmentDuration = 0; + for (int i = 0, length = responseArray.length(); i < length; i++) { + JSONObject obj = (JSONObject) responseArray.get(i); + JSONArray segment = obj.getJSONArray("segment"); + final long start = (long) (segment.getDouble(0) * 1000); + final long end = (long) (segment.getDouble(1) * 1000); + + String uuid = obj.getString("UUID"); + final boolean locked = obj.getInt("locked") == 1; + String categoryKey = obj.getString("category"); + SegmentCategory category = SegmentCategory.byCategoryKey(categoryKey); + if (category == null) { + Logger.printException(() -> "Received unknown category: " + categoryKey); // should never happen + } else if ((end - start) >= minSegmentDuration) { + segments.add(new SponsorSegment(category, uuid, start, end, locked)); + } + } + Logger.printDebug(() -> { + StringBuilder builder = new StringBuilder("Downloaded segments:"); + for (SponsorSegment segment : segments) { + builder.append('\n').append(segment); + } + return builder.toString(); + }); + runVipCheckInBackgroundIfNeeded(); + } else if (responseCode == 404) { + // no segments are found. a normal response + Logger.printDebug(() -> "No segments found for video: " + videoId); + } else { + handleConnectionError(str("revanced_sb_sponsorblock_connection_failure_status", responseCode), null); + connection.disconnect(); // something went wrong, might as well disconnect + } + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_sb_sponsorblock_connection_failure_timeout"), ex); + } catch (IOException ex) { + handleConnectionError(str("revanced_sb_sponsorblock_connection_failure_generic"), ex); + } catch (Exception ex) { + // Should never happen + Logger.printException(() -> "getSegments failure", ex); + } + + return segments.toArray(new SponsorSegment[0]); + } + + public static void runVipCheckInBackgroundIfNeeded() { + if (!SponsorBlockSettings.userHasSBPrivateId()) { + return; // User cannot be a VIP. User has never voted, created any segments, or has imported a SB user id. + } + long now = System.currentTimeMillis(); + if (now < (Settings.SB_LAST_VIP_CHECK.get() + TimeUnit.DAYS.toMillis(3))) { + return; + } + Utils.runOnBackgroundThread(() -> { + try { + JSONObject json = getJSONObject(SponsorBlockSettings.getSBPrivateUserID()); + boolean vip = json.getBoolean("vip"); + Settings.SB_USER_IS_VIP.save(vip); + Settings.SB_LAST_VIP_CHECK.save(now); + } catch (IOException ex) { + Logger.printInfo(() -> "Failed to check VIP (network error)", ex); // info, so no error toast is shown + } catch (Exception ex) { + Logger.printException(() -> "Failed to check VIP", ex); // should never happen + } + }); + } + + // helpers + + private static HttpURLConnection getConnectionFromRoute(@NonNull Route route, String... params) throws IOException { + HttpURLConnection connection = Requester.getConnectionFromRoute(Settings.SB_API_URL.get(), route, params); + connection.setConnectTimeout(TIMEOUT_TCP_DEFAULT_MILLISECONDS); + connection.setReadTimeout(TIMEOUT_HTTP_DEFAULT_MILLISECONDS); + return connection; + } + + private static JSONObject getJSONObject(String... params) throws IOException, JSONException { + return Requester.parseJSONObject(getConnectionFromRoute(SBRoutes.IS_USER_VIP, params)); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/utils/ExtendedUtils.java b/extensions/shared/src/main/java/app/revanced/extension/music/utils/ExtendedUtils.java new file mode 100644 index 000000000..5de71744e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/utils/ExtendedUtils.java @@ -0,0 +1,41 @@ +package app.revanced.extension.music.utils; + +import android.app.AlertDialog; +import android.content.Context; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.shared.utils.PackageUtils; + +public class ExtendedUtils extends PackageUtils { + + public static boolean isSpoofingToLessThan(@NonNull String versionName) { + if (!Settings.SPOOF_APP_VERSION.get()) + return false; + + return isVersionToLessThan(Settings.SPOOF_APP_VERSION_TARGET.get(), versionName); + } + + @SuppressWarnings("deprecation") + public static AlertDialog.Builder getDialogBuilder(@NonNull Context context) { + return new AlertDialog.Builder(context, isSDKAbove(22) + ? android.R.style.Theme_DeviceDefault_Dialog_Alert + : AlertDialog.THEME_DEVICE_DEFAULT_DARK + ); + } + + public static FrameLayout.LayoutParams getLayoutParams() { + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + + int left_margin = dpToPx(20); + int top_margin = dpToPx(10); + int right_margin = dpToPx(20); + int bottom_margin = dpToPx(4); + params.setMargins(left_margin, top_margin, right_margin, bottom_margin); + + return params; + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/utils/RestartUtils.java b/extensions/shared/src/main/java/app/revanced/extension/music/utils/RestartUtils.java new file mode 100644 index 000000000..a4ca37641 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/utils/RestartUtils.java @@ -0,0 +1,36 @@ +package app.revanced.extension.music.utils; + +import static app.revanced.extension.music.utils.ExtendedUtils.getDialogBuilder; +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.runOnMainThreadDelayed; + +import android.app.Activity; +import android.content.Intent; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +public class RestartUtils { + + public static void restartApp(@NonNull Activity activity) { + final Intent intent = Objects.requireNonNull(activity.getPackageManager().getLaunchIntentForPackage(activity.getPackageName())); + final Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent()); + + activity.finishAffinity(); + activity.startActivity(mainIntent); + Runtime.getRuntime().exit(0); + } + + public static void showRestartDialog(@NonNull Activity activity) { + showRestartDialog(activity, "revanced_extended_restart_message", 0); + } + + public static void showRestartDialog(@NonNull Activity activity, @NonNull String message, long delay) { + getDialogBuilder(activity) + .setMessage(str(message)) + .setPositiveButton(android.R.string.ok, (dialog, id) -> runOnMainThreadDelayed(() -> restartApp(activity), delay)) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/utils/VideoUtils.java b/extensions/shared/src/main/java/app/revanced/extension/music/utils/VideoUtils.java new file mode 100644 index 000000000..059c311bd --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/utils/VideoUtils.java @@ -0,0 +1,87 @@ +package app.revanced.extension.music.utils; + +import static app.revanced.extension.music.settings.preference.ExternalDownloaderPreference.checkPackageIsEnabled; +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.media.AudioManager; +import android.util.Log; + +import androidx.annotation.NonNull; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.shared.VideoInformation; +import app.revanced.extension.shared.settings.StringSetting; +import app.revanced.extension.shared.utils.IntentUtils; +import app.revanced.extension.shared.utils.Logger; + +@SuppressWarnings("unused") +public class VideoUtils extends IntentUtils { + private static final StringSetting externalDownloaderPackageName = + Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME; + + public static void launchExternalDownloader() { + launchExternalDownloader(VideoInformation.getVideoId()); + } + + public static void launchExternalDownloader(@NonNull String videoId) { + try { + String downloaderPackageName = externalDownloaderPackageName.get().trim(); + + if (downloaderPackageName.isEmpty()) { + externalDownloaderPackageName.resetToDefault(); + downloaderPackageName = externalDownloaderPackageName.defaultValue; + } + + if (!checkPackageIsEnabled()) { + return; + } + + final String content = String.format("https://music.youtube.com/watch?v=%s", videoId); + launchExternalDownloader(content, downloaderPackageName); + } catch (Exception ex) { + Logger.printException(() -> "launchExternalDownloader failure", ex); + } + } + + @SuppressLint("IntentReset") + public static void openInYouTube() { + final String videoId = VideoInformation.getVideoId(); + if (videoId.isEmpty()) { + showToastShort(str("revanced_replace_flyout_menu_dismiss_queue_watch_on_youtube_warning")); + return; + } + + if (context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE) instanceof AudioManager audioManager) { + audioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + } + + String url = String.format("vnd.youtube://%s", videoId); + if (Settings.REPLACE_FLYOUT_MENU_DISMISS_QUEUE_CONTINUE_WATCH.get()) { + long seconds = VideoInformation.getVideoTime() / 1000; + url += String.format("?t=%s", seconds); + } + + launchView(url); + } + + public static void openInYouTubeMusic(@NonNull String songId) { + final String url = String.format("vnd.youtube.music://%s", songId); + launchView(url, context.getPackageName()); + } + + /** + * Rest of the implementation added by patch. + */ + public static void shuffleTracks() { + Log.d("Extended: VideoUtils", "Tracks are shuffled"); + } + + /** + * Rest of the implementation added by patch. + */ + public static void showPlaybackSpeedFlyoutMenu() { + Logger.printDebug(() -> "Playback speed flyout menu opened"); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/GeneralAdsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/GeneralAdsPatch.java new file mode 100644 index 000000000..f108a49d7 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/GeneralAdsPatch.java @@ -0,0 +1,42 @@ +package app.revanced.extension.reddit.patches; + +import com.reddit.domain.model.ILink; + +import java.util.ArrayList; +import java.util.List; + +import app.revanced.extension.reddit.settings.Settings; + +@SuppressWarnings("unused") +public final class GeneralAdsPatch { + + private static List filterChildren(final Iterable links) { + final List filteredList = new ArrayList<>(); + + for (Object item : links) { + if (item instanceof ILink iLink && iLink.getPromoted()) continue; + + filteredList.add(item); + } + + return filteredList; + } + + public static boolean hideCommentAds() { + return Settings.HIDE_COMMENT_ADS.get(); + } + + public static List hideOldPostAds(List list) { + if (!Settings.HIDE_OLD_POST_ADS.get()) + return list; + + return filterChildren(list); + } + + public static void hideNewPostAds(ArrayList arrayList, Object object) { + if (Settings.HIDE_NEW_POST_ADS.get()) + return; + + arrayList.add(object); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/NavigationButtonsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/NavigationButtonsPatch.java new file mode 100644 index 000000000..301616c3e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/NavigationButtonsPatch.java @@ -0,0 +1,53 @@ +package app.revanced.extension.reddit.patches; + +import android.view.View; +import android.view.ViewGroup; + +import java.util.List; + +import app.revanced.extension.reddit.settings.Settings; +import app.revanced.extension.shared.utils.Logger; + +@SuppressWarnings("unused") +public final class NavigationButtonsPatch { + + public static List hideNavigationButtons(List list) { + try { + for (NavigationButton button : NavigationButton.values()) { + if (button.enabled && list.size() > button.index) { + list.remove(button.index); + } + } + } catch (Exception exception) { + Logger.printException(() -> "Failed to remove button list", exception); + } + return list; + } + + public static void hideNavigationButtons(ViewGroup viewGroup) { + try { + if (viewGroup == null) return; + for (NavigationButton button : NavigationButton.values()) { + if (button.enabled && viewGroup.getChildCount() > button.index) { + View view = viewGroup.getChildAt(button.index); + if (view != null) view.setVisibility(View.GONE); + } + } + } catch (Exception exception) { + Logger.printException(() -> "Failed to remove button view", exception); + } + } + + private enum NavigationButton { + CHAT(Settings.HIDE_CHAT_BUTTON.get(), 3), + CREATE(Settings.HIDE_CREATE_BUTTON.get(), 2), + DISCOVER(Settings.HIDE_DISCOVER_BUTTON.get(), 1); + private final boolean enabled; + private final int index; + + NavigationButton(final boolean enabled, final int index) { + this.enabled = enabled; + this.index = index; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/OpenLinksDirectlyPatch.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/OpenLinksDirectlyPatch.java new file mode 100644 index 000000000..caab44f0e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/OpenLinksDirectlyPatch.java @@ -0,0 +1,30 @@ +package app.revanced.extension.reddit.patches; + +import android.net.Uri; + +import app.revanced.extension.reddit.settings.Settings; +import app.revanced.extension.shared.utils.Logger; + +@SuppressWarnings("unused") +public final class OpenLinksDirectlyPatch { + + /** + * Parses the given Reddit redirect uri by extracting the redirect query. + * + * @param uri The Reddit redirect uri. + * @return The redirect query. + */ + public static Uri parseRedirectUri(Uri uri) { + try { + if (Settings.OPEN_LINKS_DIRECTLY.get()) { + final String parsedUri = uri.getQueryParameter("url"); + if (parsedUri != null && !parsedUri.isEmpty()) + return Uri.parse(parsedUri); + } + } catch (Exception e) { + Logger.printException(() -> "Can not parse URL: " + uri, e); + } + return uri; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/OpenLinksExternallyPatch.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/OpenLinksExternallyPatch.java new file mode 100644 index 000000000..387f120ad --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/OpenLinksExternallyPatch.java @@ -0,0 +1,33 @@ +package app.revanced.extension.reddit.patches; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; + +import app.revanced.extension.reddit.settings.Settings; +import app.revanced.extension.shared.utils.Logger; + +@SuppressWarnings("unused") +public class OpenLinksExternallyPatch { + + /** + * Override 'CustomTabsIntent', in order to open links in the default browser. + * Instead of doing CustomTabsActivity, + * + * @param activity The activity, to start an Intent. + * @param uri The URL to be opened in the default browser. + */ + public static boolean openLinksExternally(Activity activity, Uri uri) { + try { + if (activity != null && uri != null && Settings.OPEN_LINKS_EXTERNALLY.get()) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(uri); + activity.startActivity(intent); + return true; + } + } catch (Exception e) { + Logger.printException(() -> "Can not open URL: " + uri, e); + } + return false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/RecentlyVisitedShelfPatch.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/RecentlyVisitedShelfPatch.java new file mode 100644 index 000000000..5363688df --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/RecentlyVisitedShelfPatch.java @@ -0,0 +1,14 @@ +package app.revanced.extension.reddit.patches; + +import java.util.Collections; +import java.util.List; + +import app.revanced.extension.reddit.settings.Settings; + +@SuppressWarnings("unused") +public final class RecentlyVisitedShelfPatch { + + public static List hideRecentlyVisitedShelf(List list) { + return Settings.HIDE_RECENTLY_VISITED_SHELF.get() ? Collections.emptyList() : list; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/RecommendedCommunitiesPatch.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/RecommendedCommunitiesPatch.java new file mode 100644 index 000000000..126d79761 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/RecommendedCommunitiesPatch.java @@ -0,0 +1,12 @@ +package app.revanced.extension.reddit.patches; + +import app.revanced.extension.reddit.settings.Settings; + +@SuppressWarnings("unused") +public final class RecommendedCommunitiesPatch { + + public static boolean hideRecommendedCommunitiesShelf() { + return Settings.HIDE_RECOMMENDED_COMMUNITIES_SHELF.get(); + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/RemoveSubRedditDialogPatch.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/RemoveSubRedditDialogPatch.java new file mode 100644 index 000000000..98dd6c53b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/RemoveSubRedditDialogPatch.java @@ -0,0 +1,41 @@ +package app.revanced.extension.reddit.patches; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import app.revanced.extension.reddit.settings.Settings; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings("unused") +public class RemoveSubRedditDialogPatch { + + public static void confirmDialog(@NonNull TextView textView) { + if (!Settings.REMOVE_NSFW_DIALOG.get()) + return; + + if (!textView.getText().toString().equals(str("nsfw_continue_non_anonymously"))) + return; + + clickViewDelayed(textView); + } + + public static void dismissDialog(View cancelButtonView) { + if (!Settings.REMOVE_NOTIFICATION_DIALOG.get()) + return; + + clickViewDelayed(cancelButtonView); + } + + private static void clickViewDelayed(View view) { + Utils.runOnMainThreadDelayed(() -> { + if (view != null) { + view.setSoundEffectsEnabled(false); + view.performClick(); + } + }, 0); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/SanitizeUrlQueryPatch.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/SanitizeUrlQueryPatch.java new file mode 100644 index 000000000..f19398376 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/SanitizeUrlQueryPatch.java @@ -0,0 +1,12 @@ +package app.revanced.extension.reddit.patches; + +import app.revanced.extension.reddit.settings.Settings; + +@SuppressWarnings("unused") +public final class SanitizeUrlQueryPatch { + + public static boolean stripQueryParameters() { + return Settings.SANITIZE_URL_QUERY.get(); + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/ScreenshotPopupPatch.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/ScreenshotPopupPatch.java new file mode 100644 index 000000000..7216ea55c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/ScreenshotPopupPatch.java @@ -0,0 +1,11 @@ +package app.revanced.extension.reddit.patches; + +import app.revanced.extension.reddit.settings.Settings; + +@SuppressWarnings("unused") +public class ScreenshotPopupPatch { + + public static boolean disableScreenshotPopup() { + return Settings.DISABLE_SCREENSHOT_POPUP.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/ToolBarButtonPatch.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/ToolBarButtonPatch.java new file mode 100644 index 000000000..46a82cd0d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/ToolBarButtonPatch.java @@ -0,0 +1,16 @@ +package app.revanced.extension.reddit.patches; + +import android.view.View; + +import app.revanced.extension.reddit.settings.Settings; + +@SuppressWarnings("unused") +public class ToolBarButtonPatch { + + public static void hideToolBarButton(View view) { + if (!Settings.HIDE_TOOLBAR_BUTTON.get()) + return; + + view.setVisibility(View.GONE); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/ActivityHook.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/ActivityHook.java new file mode 100644 index 000000000..ffe8bfd7d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/ActivityHook.java @@ -0,0 +1,35 @@ +package app.revanced.extension.reddit.settings; + +import android.app.Activity; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import app.revanced.extension.reddit.settings.preference.ReVancedPreferenceFragment; + +/** + * @noinspection ALL + */ +public class ActivityHook { + public static void initialize(Activity activity) { + SettingsStatus.load(); + + final int fragmentId = View.generateViewId(); + final FrameLayout fragment = new FrameLayout(activity); + fragment.setLayoutParams(new FrameLayout.LayoutParams(-1, -1)); + fragment.setId(fragmentId); + + final LinearLayout linearLayout = new LinearLayout(activity); + linearLayout.setLayoutParams(new LinearLayout.LayoutParams(-1, -1)); + linearLayout.setOrientation(LinearLayout.VERTICAL); + linearLayout.setFitsSystemWindows(true); + linearLayout.setTransitionGroup(true); + linearLayout.addView(fragment); + activity.setContentView(linearLayout); + + activity.getFragmentManager() + .beginTransaction() + .replace(fragmentId, new ReVancedPreferenceFragment()) + .commit(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/Settings.java new file mode 100644 index 000000000..2efc2eb37 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/Settings.java @@ -0,0 +1,30 @@ +package app.revanced.extension.reddit.settings; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.settings.BooleanSetting; + +public class Settings extends BaseSettings { + // Ads + public static final BooleanSetting HIDE_COMMENT_ADS = new BooleanSetting("revanced_hide_comment_ads", TRUE, true); + public static final BooleanSetting HIDE_OLD_POST_ADS = new BooleanSetting("revanced_hide_old_post_ads", TRUE, true); + public static final BooleanSetting HIDE_NEW_POST_ADS = new BooleanSetting("revanced_hide_new_post_ads", TRUE, true); + + // Layout + public static final BooleanSetting DISABLE_SCREENSHOT_POPUP = new BooleanSetting("revanced_disable_screenshot_popup", TRUE); + public static final BooleanSetting HIDE_CHAT_BUTTON = new BooleanSetting("revanced_hide_chat_button", FALSE, true); + public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", FALSE, true); + public static final BooleanSetting HIDE_DISCOVER_BUTTON = new BooleanSetting("revanced_hide_discover_button", FALSE, true); + public static final BooleanSetting HIDE_RECENTLY_VISITED_SHELF = new BooleanSetting("revanced_hide_recently_visited_shelf", FALSE); + public static final BooleanSetting HIDE_RECOMMENDED_COMMUNITIES_SHELF = new BooleanSetting("revanced_hide_recommended_communities_shelf", FALSE, true); + public static final BooleanSetting HIDE_TOOLBAR_BUTTON = new BooleanSetting("revanced_hide_toolbar_button", FALSE, true); + public static final BooleanSetting REMOVE_NSFW_DIALOG = new BooleanSetting("revanced_remove_nsfw_dialog", FALSE, true); + public static final BooleanSetting REMOVE_NOTIFICATION_DIALOG = new BooleanSetting("revanced_remove_notification_dialog", FALSE, true); + + // Miscellaneous + public static final BooleanSetting OPEN_LINKS_DIRECTLY = new BooleanSetting("revanced_open_links_directly", TRUE); + public static final BooleanSetting OPEN_LINKS_EXTERNALLY = new BooleanSetting("revanced_open_links_externally", TRUE); + public static final BooleanSetting SANITIZE_URL_QUERY = new BooleanSetting("revanced_sanitize_url_query", TRUE); +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/SettingsStatus.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/SettingsStatus.java new file mode 100644 index 000000000..a71521dab --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/SettingsStatus.java @@ -0,0 +1,78 @@ +package app.revanced.extension.reddit.settings; + +@SuppressWarnings("unused") +public class SettingsStatus { + public static boolean generalAdsEnabled = false; + public static boolean navigationButtonsEnabled = false; + public static boolean openLinksDirectlyEnabled = false; + public static boolean openLinksExternallyEnabled = false; + public static boolean recentlyVisitedShelfEnabled = false; + public static boolean recommendedCommunitiesShelfEnabled = false; + public static boolean sanitizeUrlQueryEnabled = false; + public static boolean screenshotPopupEnabled = false; + public static boolean subRedditDialogEnabled = false; + public static boolean toolBarButtonEnabled = false; + + + public static void enableGeneralAds() { + generalAdsEnabled = true; + } + + public static void enableNavigationButtons() { + navigationButtonsEnabled = true; + } + + public static void enableOpenLinksDirectly() { + openLinksDirectlyEnabled = true; + } + + public static void enableOpenLinksExternally() { + openLinksExternallyEnabled = true; + } + + public static void enableRecentlyVisitedShelf() { + recentlyVisitedShelfEnabled = true; + } + + public static void enableRecommendedCommunitiesShelf() { + recommendedCommunitiesShelfEnabled = true; + } + + public static void enableSubRedditDialog() { + subRedditDialogEnabled = true; + } + + public static void enableSanitizeUrlQuery() { + sanitizeUrlQueryEnabled = true; + } + + public static void enableScreenshotPopup() { + screenshotPopupEnabled = true; + } + + public static void enableToolBarButton() { + toolBarButtonEnabled = true; + } + + public static boolean adsCategoryEnabled() { + return generalAdsEnabled; + } + + public static boolean layoutCategoryEnabled() { + return navigationButtonsEnabled || + recentlyVisitedShelfEnabled || + screenshotPopupEnabled || + subRedditDialogEnabled || + toolBarButtonEnabled; + } + + public static boolean miscellaneousCategoryEnabled() { + return openLinksDirectlyEnabled || + openLinksExternallyEnabled || + sanitizeUrlQueryEnabled; + } + + public static void load() { + + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/ReVancedPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/ReVancedPreferenceFragment.java new file mode 100644 index 000000000..8451a5819 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/ReVancedPreferenceFragment.java @@ -0,0 +1,46 @@ +package app.revanced.extension.reddit.settings.preference; + +import android.content.Context; +import android.preference.Preference; +import android.preference.PreferenceScreen; + +import androidx.annotation.NonNull; + +import org.jetbrains.annotations.NotNull; + +import app.revanced.extension.reddit.settings.preference.categories.AdsPreferenceCategory; +import app.revanced.extension.reddit.settings.preference.categories.LayoutPreferenceCategory; +import app.revanced.extension.reddit.settings.preference.categories.MiscellaneousPreferenceCategory; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment; + +/** + * Preference fragment for ReVanced settings + */ +@SuppressWarnings("deprecation") +public class ReVancedPreferenceFragment extends AbstractPreferenceFragment { + + @Override + protected void syncSettingWithPreference(@NonNull @NotNull Preference pref, + @NonNull @NotNull Setting setting, + boolean applySettingToPreference) { + super.syncSettingWithPreference(pref, setting, applySettingToPreference); + } + + @Override + protected void initialize() { + final Context context = getContext(); + + // Currently no resources can be compiled for Reddit (fails with aapt error). + // So all Reddit Strings are hard coded in integrations. + restartDialogMessage = "Refresh and restart"; + + PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context); + setPreferenceScreen(preferenceScreen); + + // Custom categories reference app specific Settings class. + new AdsPreferenceCategory(context, preferenceScreen); + new LayoutPreferenceCategory(context, preferenceScreen); + new MiscellaneousPreferenceCategory(context, preferenceScreen); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/TogglePreference.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/TogglePreference.java new file mode 100644 index 000000000..fed5a7c4b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/TogglePreference.java @@ -0,0 +1,17 @@ +package app.revanced.extension.reddit.settings.preference; + +import android.content.Context; +import android.preference.SwitchPreference; + +import app.revanced.extension.shared.settings.BooleanSetting; + +@SuppressWarnings("deprecation") +public class TogglePreference extends SwitchPreference { + public TogglePreference(Context context, String title, String summary, BooleanSetting setting) { + super(context); + this.setTitle(title); + this.setSummary(summary); + this.setKey(setting.key); + this.setChecked(setting.get()); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/categories/AdsPreferenceCategory.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/categories/AdsPreferenceCategory.java new file mode 100644 index 000000000..a51fc397d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/categories/AdsPreferenceCategory.java @@ -0,0 +1,43 @@ +package app.revanced.extension.reddit.settings.preference.categories; + +import android.content.Context; +import android.preference.PreferenceScreen; + +import app.revanced.extension.reddit.settings.Settings; +import app.revanced.extension.reddit.settings.SettingsStatus; +import app.revanced.extension.reddit.settings.preference.TogglePreference; + +@SuppressWarnings("deprecation") +public class AdsPreferenceCategory extends ConditionalPreferenceCategory { + public AdsPreferenceCategory(Context context, PreferenceScreen screen) { + super(context, screen); + setTitle("Ads"); + } + + @Override + public boolean getSettingsStatus() { + return SettingsStatus.adsCategoryEnabled(); + } + + @Override + public void addPreferences(Context context) { + addPreference(new TogglePreference( + context, + "Hide comment ads", + "Hides ads in the comments section.", + Settings.HIDE_COMMENT_ADS + )); + addPreference(new TogglePreference( + context, + "Hide feed ads", + "Hides ads in the feed (old method).", + Settings.HIDE_OLD_POST_ADS + )); + addPreference(new TogglePreference( + context, + "Hide feed ads", + "Hides ads in the feed (new method).", + Settings.HIDE_NEW_POST_ADS + )); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/categories/ConditionalPreferenceCategory.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/categories/ConditionalPreferenceCategory.java new file mode 100644 index 000000000..c82b7c129 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/categories/ConditionalPreferenceCategory.java @@ -0,0 +1,22 @@ +package app.revanced.extension.reddit.settings.preference.categories; + +import android.content.Context; +import android.preference.PreferenceCategory; +import android.preference.PreferenceScreen; + +@SuppressWarnings("deprecation") +public abstract class ConditionalPreferenceCategory extends PreferenceCategory { + public ConditionalPreferenceCategory(Context context, PreferenceScreen screen) { + super(context); + + if (getSettingsStatus()) { + screen.addPreference(this); + addPreferences(context); + } + } + + public abstract boolean getSettingsStatus(); + + public abstract void addPreferences(Context context); +} + diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/categories/LayoutPreferenceCategory.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/categories/LayoutPreferenceCategory.java new file mode 100644 index 000000000..18dfd3349 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/categories/LayoutPreferenceCategory.java @@ -0,0 +1,91 @@ +package app.revanced.extension.reddit.settings.preference.categories; + +import android.content.Context; +import android.preference.PreferenceScreen; + +import app.revanced.extension.reddit.settings.Settings; +import app.revanced.extension.reddit.settings.SettingsStatus; +import app.revanced.extension.reddit.settings.preference.TogglePreference; + +@SuppressWarnings("deprecation") +public class LayoutPreferenceCategory extends ConditionalPreferenceCategory { + public LayoutPreferenceCategory(Context context, PreferenceScreen screen) { + super(context, screen); + setTitle("Layout"); + } + + @Override + public boolean getSettingsStatus() { + return SettingsStatus.layoutCategoryEnabled(); + } + + @Override + public void addPreferences(Context context) { + if (SettingsStatus.screenshotPopupEnabled) { + addPreference(new TogglePreference( + context, + "Disable screenshot popup", + "Disables the popup that appears when taking a screenshot.", + Settings.DISABLE_SCREENSHOT_POPUP + )); + } + if (SettingsStatus.navigationButtonsEnabled) { + addPreference(new TogglePreference( + context, + "Hide Chat button", + "Hides the Chat button in the navigation bar.", + Settings.HIDE_CHAT_BUTTON + )); + addPreference(new TogglePreference( + context, + "Hide Create button", + "Hides the Create button in the navigation bar.", + Settings.HIDE_CREATE_BUTTON + )); + addPreference(new TogglePreference( + context, + "Hide Discover or Communities button", + "Hides the Discover or Communities button in the navigation bar.", + Settings.HIDE_DISCOVER_BUTTON + )); + } + if (SettingsStatus.recentlyVisitedShelfEnabled) { + addPreference(new TogglePreference( + context, + "Hide Recently Visited shelf", + "Hides the Recently Visited shelf in the sidebar.", + Settings.HIDE_RECENTLY_VISITED_SHELF + )); + } + if (SettingsStatus.recommendedCommunitiesShelfEnabled) { + addPreference(new TogglePreference( + context, + "Hide recommended communities", + "Hides the recommended communities shelves in subreddits.", + Settings.HIDE_RECOMMENDED_COMMUNITIES_SHELF + )); + } + if (SettingsStatus.toolBarButtonEnabled) { + addPreference(new TogglePreference( + context, + "Hide toolbar button", + "Hide toolbar button", + Settings.HIDE_TOOLBAR_BUTTON + )); + } + if (SettingsStatus.subRedditDialogEnabled) { + addPreference(new TogglePreference( + context, + "Remove NSFW warning dialog", + "Removes the NSFW warning dialog that appears when visiting a subreddit by accepting it automatically.", + Settings.REMOVE_NSFW_DIALOG + )); + addPreference(new TogglePreference( + context, + "Remove notification suggestion dialog", + "Removes the notifications suggestion dialog that appears when visiting a subreddit by dismissing it automatically.", + Settings.REMOVE_NOTIFICATION_DIALOG + )); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/categories/MiscellaneousPreferenceCategory.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/categories/MiscellaneousPreferenceCategory.java new file mode 100644 index 000000000..5e16cf5b8 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/settings/preference/categories/MiscellaneousPreferenceCategory.java @@ -0,0 +1,49 @@ +package app.revanced.extension.reddit.settings.preference.categories; + +import android.content.Context; +import android.preference.PreferenceScreen; + +import app.revanced.extension.reddit.settings.Settings; +import app.revanced.extension.reddit.settings.SettingsStatus; +import app.revanced.extension.reddit.settings.preference.TogglePreference; + +@SuppressWarnings("deprecation") +public class MiscellaneousPreferenceCategory extends ConditionalPreferenceCategory { + public MiscellaneousPreferenceCategory(Context context, PreferenceScreen screen) { + super(context, screen); + setTitle("Miscellaneous"); + } + + @Override + public boolean getSettingsStatus() { + return SettingsStatus.miscellaneousCategoryEnabled(); + } + + @Override + public void addPreferences(Context context) { + if (SettingsStatus.openLinksDirectlyEnabled) { + addPreference(new TogglePreference( + context, + "Open links directly", + "Skips over redirection URLs in external links.", + Settings.OPEN_LINKS_DIRECTLY + )); + } + if (SettingsStatus.openLinksExternallyEnabled) { + addPreference(new TogglePreference( + context, + "Open links externally", + "Opens links in your browser instead of in the in-app-browser.", + Settings.OPEN_LINKS_EXTERNALLY + )); + } + if (SettingsStatus.sanitizeUrlQueryEnabled) { + addPreference(new TogglePreference( + context, + "Sanitize sharing links", + "Removes tracking query parameters from URLs when sharing links.", + Settings.SANITIZE_URL_QUERY + )); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/AutoCaptionsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/AutoCaptionsPatch.java new file mode 100644 index 000000000..2a9752df8 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/AutoCaptionsPatch.java @@ -0,0 +1,18 @@ +package app.revanced.extension.shared.patches; + +import app.revanced.extension.shared.settings.BaseSettings; + +@SuppressWarnings("unused") +public final class AutoCaptionsPatch { + + private static boolean captionsButtonStatus; + + public static boolean disableAutoCaptions() { + return BaseSettings.DISABLE_AUTO_CAPTIONS.get() && + !captionsButtonStatus; + } + + public static void setCaptionsButtonStatus(boolean status) { + captionsButtonStatus = status; + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/BaseSettingsMenuPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/BaseSettingsMenuPatch.java new file mode 100644 index 000000000..4ce7e63a8 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/BaseSettingsMenuPatch.java @@ -0,0 +1,16 @@ +package app.revanced.extension.shared.patches; + +import android.util.Log; + +import androidx.preference.PreferenceScreen; + +@SuppressWarnings("unused") +public class BaseSettingsMenuPatch { + + /** + * Rest of the implementation added by patch. + */ + public static void removePreference(PreferenceScreen mPreferenceScreen, String key) { + Log.d("Extended: SettingsMenuPatch", "key: " + key); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/BypassImageRegionRestrictionsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/BypassImageRegionRestrictionsPatch.java new file mode 100644 index 000000000..a43849f40 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/BypassImageRegionRestrictionsPatch.java @@ -0,0 +1,34 @@ +package app.revanced.extension.shared.patches; + +import java.util.regex.Pattern; + +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.utils.Logger; + +@SuppressWarnings("unused") +public final class BypassImageRegionRestrictionsPatch { + + private static final boolean BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED = BaseSettings.BYPASS_IMAGE_REGION_RESTRICTIONS.get(); + private static final String REPLACEMENT_IMAGE_DOMAIN = BaseSettings.BYPASS_IMAGE_REGION_RESTRICTIONS_DOMAIN.get(); + + /** + * YouTube static images domain. Includes user and channel avatar images and community post images. + */ + private static final Pattern YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN = Pattern.compile("(ap[1-2]|gm[1-4]|gz0|(cp|ci|gp|lh)[3-6]|sp[1-3]|yt[3-4]|(play|ccp)-lh)\\.(ggpht|googleusercontent)\\.com"); + + public static String overrideImageURL(String originalUrl) { + try { + if (BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED) { + final String replacement = YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN + .matcher(originalUrl).replaceFirst(REPLACEMENT_IMAGE_DOMAIN); + if (!replacement.equals(originalUrl)) { + Logger.printDebug(() -> "Replaced: '" + originalUrl + "' with: '" + replacement + "'"); + } + return replacement; + } + } catch (Exception ex) { + Logger.printException(() -> "overrideImageURL failure", ex); + } + return originalUrl; + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/FullscreenAdsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/FullscreenAdsPatch.java new file mode 100644 index 000000000..341f8748e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/FullscreenAdsPatch.java @@ -0,0 +1,74 @@ +package app.revanced.extension.shared.patches; + +import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition; + +import android.view.View; + +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.utils.Logger; + +@SuppressWarnings("unused") +public class FullscreenAdsPatch { + private static final boolean hideFullscreenAdsEnabled = BaseSettings.HIDE_FULLSCREEN_ADS.get(); + private static final ByteArrayFilterGroup exception = + new ByteArrayFilterGroup( + null, + "post_image_lightbox.eml" // Community post image in fullscreen + ); + + public static boolean disableFullscreenAds(final byte[] bytes, int type) { + if (!hideFullscreenAdsEnabled) { + return false; + } + + final DialogType dialogType = DialogType.getDialogType(type); + final String dialogName = dialogType.name(); + + // The dialog type of a fullscreen dialog is always {@code DialogType.FULLSCREEN} + if (dialogType != DialogType.FULLSCREEN) { + Logger.printDebug(() -> "Ignoring dialogType " + dialogName); + return false; + } + + // Image in community post in fullscreen is not filtered + final boolean isException = bytes != null && + exception.check(bytes).isFiltered(); + + if (isException) { + Logger.printDebug(() -> "Ignoring exception"); + } else { + Logger.printDebug(() -> "Blocked fullscreen ads"); + } + + return !isException; + } + + public static void hideFullscreenAds(View view) { + hideViewBy0dpUnderCondition( + hideFullscreenAdsEnabled, + view + ); + } + + private enum DialogType { + NULL(0), + ALERT(1), + FULLSCREEN(2), + LAYOUT_FULLSCREEN(3); + + private final int type; + + DialogType(int type) { + this.type = type; + } + + private static DialogType getDialogType(int type) { + for (DialogType val : values()) + if (type == val.type) return val; + + return DialogType.NULL; + } + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/GmsCoreSupport.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/GmsCoreSupport.java new file mode 100644 index 000000000..6fdd09645 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/GmsCoreSupport.java @@ -0,0 +1,250 @@ +package app.revanced.extension.shared.patches; + +import static app.revanced.extension.shared.settings.BaseSettings.GMS_SHOW_DIALOG; +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.isSDKAbove; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.SearchManager; +import android.content.ContentProviderClient; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.PowerManager; +import android.provider.Settings; + +import org.apache.commons.lang3.StringUtils; + +import java.net.MalformedURLException; +import java.net.URL; + +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings("unused") +public class GmsCoreSupport { + private static final String PACKAGE_NAME_YOUTUBE = "com.google.android.youtube"; + private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music"; + + private static final String GMS_CORE_PACKAGE_NAME + = getGmsCoreVendorGroupId() + ".android.gms"; + private static final Uri GMS_CORE_PROVIDER + = Uri.parse("content://" + getGmsCoreVendorGroupId() + ".android.gsf.gservices/prefix"); + private static final String DONT_KILL_MY_APP_LINK + = "https://dontkillmyapp.com"; + + private static final String META_SPOOF_PACKAGE_NAME = + GMS_CORE_PACKAGE_NAME + ".SPOOFED_PACKAGE_NAME"; + + private static void open(Activity mActivity, String queryOrLink) { + Intent intent; + try { + // Check if queryOrLink is a valid URL. + new URL(queryOrLink); + + intent = new Intent(Intent.ACTION_VIEW, Uri.parse(queryOrLink)); + } catch (MalformedURLException e) { + intent = new Intent(Intent.ACTION_WEB_SEARCH); + intent.putExtra(SearchManager.QUERY, queryOrLink); + } + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + mActivity.startActivity(intent); + + // Gracefully exit, otherwise the broken app will continue to run. + System.exit(0); + } + + private static void showBatteryOptimizationDialog(Activity context, + String dialogMessageRef, + String positiveButtonTextRef, + BooleanSetting setting, + DialogInterface.OnClickListener onPositiveClickListener, + boolean showNegativeButton) { + // Use a delay to allow the activity to finish initializing. + // Otherwise, if device is in dark mode the dialog is shown with wrong color scheme. + Utils.runOnMainThreadDelayed(() -> { + // Do not set cancelable to false, to allow using back button to skip the action, + // just in case the battery change can never be satisfied. + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setTitle(str("gms_core_dialog_title")) + .setMessage(str(dialogMessageRef)) + .setPositiveButton(str(positiveButtonTextRef), onPositiveClickListener); + + if (showNegativeButton) { + dialogBuilder.setNegativeButton(str("gms_core_dialog_dismiss_text"), (dialog, which) -> setting.save(false)); + } + + dialogBuilder.show(); + }, 100); + } + + /** + * Injection point. + */ + public static void checkGmsCore(Activity mActivity) { + try { + // Verify the user has not included GmsCore for a root installation. + // GmsCore Support changes the package name, but with a mounted installation + // all manifest changes are ignored and the original package name is used. + if (StringUtils.equalsAny(mActivity.getPackageName(), PACKAGE_NAME_YOUTUBE, PACKAGE_NAME_YOUTUBE_MUSIC)) { + Logger.printInfo(() -> "App is mounted with root, but GmsCore patch was included"); + // Cannot use localize text here, since the app will load + // resources from the unpatched app and all patch strings are missing. + Utils.showToastLong("The 'GmsCore support' patch breaks mount installations"); + + // Do not exit. If the app exits before launch completes (and without + // opening another activity), then on some devices such as Pixel phone Android 10 + // no toast will be shown and the app will continually be relaunched + // with the appearance of a hung app. + } + + // Verify GmsCore is installed. + try { + PackageManager manager = mActivity.getPackageManager(); + manager.getPackageInfo(GMS_CORE_PACKAGE_NAME, PackageManager.GET_ACTIVITIES); + } catch (PackageManager.NameNotFoundException exception) { + Logger.printInfo(() -> "GmsCore was not found"); + // Cannot show a dialog and must show a toast, + // because on some installations the app crashes before a dialog can be displayed. + Utils.showToastLong(str("gms_core_toast_not_installed_message")); + open(mActivity, getGmsCoreDownload()); + return; + } + + if (contentProviderClientUnAvailable(mActivity)) { + Logger.printInfo(() -> "GmsCore is not running in the background"); + + showBatteryOptimizationDialog(mActivity, + "gms_core_dialog_not_whitelisted_not_allowed_in_background_message", + "gms_core_dialog_open_website_text", + null, + (dialog, id) -> open(mActivity, DONT_KILL_MY_APP_LINK), + false); // Do not show the negative button + return; + } + + // Check if GmsCore is whitelisted from battery optimizations. + if (batteryOptimizationsEnabled(mActivity)) { + Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations"); + if (GMS_SHOW_DIALOG.get()) { + showBatteryOptimizationDialog(mActivity, + "gms_core_dialog_not_whitelisted_using_battery_optimizations_message", + "gms_core_dialog_continue_text", + GMS_SHOW_DIALOG, + (dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(mActivity), + true); // Show the negative button + } + } + } catch (Exception ex) { + Logger.printException(() -> "checkGmsCore failure", ex); + } + } + + /** + * @return If GmsCore is not running in the background. + */ + @SuppressWarnings("deprecation") + private static boolean contentProviderClientUnAvailable(Context context) { + // Check if GmsCore is running in the background. + // Do this check before the battery optimization check. + if (isSDKAbove(24)) { + try (ContentProviderClient client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) { + return client == null; + } + } else { + ContentProviderClient client = null; + try { + //noinspection resource + client = context.getContentResolver() + .acquireContentProviderClient(GMS_CORE_PROVIDER); + return client == null; + } finally { + if (client != null) client.release(); + } + } + } + + @SuppressLint("BatteryLife") // Permission is part of GmsCore + private static void openGmsCoreDisableBatteryOptimizationsIntent(Activity mActivity) { + if (!isSDKAbove(23)) return; + Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + intent.setData(Uri.fromParts("package", GMS_CORE_PACKAGE_NAME, null)); + mActivity.startActivityForResult(intent, 0); + } + + /** + * @return If GmsCore is not whitelisted from battery optimizations. + */ + private static boolean batteryOptimizationsEnabled(Context context) { + if (isSDKAbove(23) && context.getSystemService(Context.POWER_SERVICE) instanceof PowerManager powerManager) { + return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME); + } + return false; + } + + /** + * Injection point. + */ + public static String spoofPackageName(Context context) { + // Package name of ReVanced. + final String packageName = context.getPackageName(); + + try { + final PackageManager packageManager = context.getPackageManager(); + + // Package name of YouTube or YouTube Music. + String originalPackageName; + + try { + originalPackageName = packageManager + .getPackageInfo(packageName, PackageManager.GET_META_DATA) + .applicationInfo + .metaData + .getString(META_SPOOF_PACKAGE_NAME); + } catch (PackageManager.NameNotFoundException exception) { + Logger.printDebug(() -> "Failed to parsing metadata"); + return packageName; + } + + if (StringUtils.isBlank(originalPackageName)) { + Logger.printDebug(() -> "Failed to parsing spoofed package name"); + return packageName; + } + + try { + packageManager.getPackageInfo(originalPackageName, PackageManager.GET_ACTIVITIES); + } catch (PackageManager.NameNotFoundException exception) { + Logger.printDebug(() -> "Original app '" + originalPackageName + "' was not found"); + return packageName; + } + + Logger.printDebug(() -> "Package name of '" + packageName + "' spoofed to '" + originalPackageName + "'"); + + return originalPackageName; + } catch (Exception ex) { + Logger.printException(() -> "spoofPackageName failure", ex); + } + + return packageName; + } + + private static String getGmsCoreDownload() { + final String vendorGroupId = getGmsCoreVendorGroupId(); + return switch (vendorGroupId) { + case "app.revanced" -> "https://github.com/revanced/gmscore/releases/latest"; + case "com.mgoogle" -> "https://github.com/inotia00/VancedMicroG/releases/latest"; + default -> vendorGroupId + ".android.gms"; + }; + } + + // Modified by a patch. Do not touch. + private static String getGmsCoreVendorGroupId() { + return "app.revanced"; + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java new file mode 100644 index 000000000..f5bcef97f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java @@ -0,0 +1,18 @@ +package app.revanced.extension.shared.patches; + +@SuppressWarnings("unused") +public class PatchStatus { + public static boolean HideFullscreenAdsDefaultBoolean() { + return false; + } + + public static boolean SpoofStreamingData() { + // Replace this with true If the Spoof streaming data patch succeeds + return false; + } + + public static boolean SpoofStreamingDataAndroidOnlyDefaultBoolean() { + // Replace this with true If the Spoof streaming data patch succeeds in YouTube + return false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/ReturnYouTubeUsernamePatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/ReturnYouTubeUsernamePatch.java new file mode 100644 index 000000000..45a79db40 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/ReturnYouTubeUsernamePatch.java @@ -0,0 +1,113 @@ +package app.revanced.extension.shared.patches; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + +import android.text.SpannableString; +import android.text.Spanned; + +import androidx.annotation.NonNull; + +import app.revanced.extension.shared.returnyoutubeusername.requests.ChannelRequest; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.utils.Logger; + +@SuppressWarnings("unused") +public class ReturnYouTubeUsernamePatch { + private static final boolean RETURN_YOUTUBE_USERNAME_ENABLED = BaseSettings.RETURN_YOUTUBE_USERNAME_ENABLED.get(); + private static final Boolean RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT = BaseSettings.RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT.get().userNameFirst; + private static final String YOUTUBE_API_KEY = BaseSettings.RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY.get(); + + private static final String AUTHOR_BADGE_PATH = "|author_badge.eml|"; + private static volatile String lastFetchedHandle = ""; + + /** + * Injection point. + * + * @param original The original string before the SpannableString is built. + */ + public static CharSequence preFetchLithoText(@NonNull Object conversionContext, + @NonNull CharSequence original) { + onLithoTextLoaded(conversionContext, original, true); + return original; + } + + /** + * Injection point. + * + * @param original The original string after the SpannableString is built. + */ + @NonNull + public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, + @NonNull CharSequence original) { + return onLithoTextLoaded(conversionContext, original, false); + } + + @NonNull + private static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, + @NonNull CharSequence original, + boolean fetchNeeded) { + try { + if (!RETURN_YOUTUBE_USERNAME_ENABLED) { + return original; + } + if (YOUTUBE_API_KEY.isEmpty()) { + Logger.printDebug(() -> "API key is empty"); + return original; + } + // In comments, the path to YouTube Handle(@youtube) always includes [AUTHOR_BADGE_PATH]. + if (!conversionContext.toString().contains(AUTHOR_BADGE_PATH)) { + return original; + } + String handle = original.toString(); + if (fetchNeeded && !handle.equals(lastFetchedHandle)) { + lastFetchedHandle = handle; + // Get the original username using YouTube Data API v3. + ChannelRequest.fetchRequestIfNeeded(handle, YOUTUBE_API_KEY, RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT); + return original; + } + // If the username is not in the cache, put it in the cache. + ChannelRequest channelRequest = ChannelRequest.getRequestForHandle(handle); + if (channelRequest == null) { + Logger.printDebug(() -> "ChannelRequest is null, handle:" + handle); + return original; + } + final String userName = channelRequest.getStream(); + if (userName == null) { + Logger.printDebug(() -> "ChannelRequest Stream is null, handle:" + handle); + return original; + } + final CharSequence copiedSpannableString = copySpannableString(original, userName); + Logger.printDebug(() -> "Replaced: '" + original + "' with: '" + copiedSpannableString + "'"); + return copiedSpannableString; + } catch (Exception ex) { + Logger.printException(() -> "onLithoTextLoaded failure", ex); + } + return original; + } + + private static CharSequence copySpannableString(CharSequence original, String userName) { + if (original instanceof Spanned spanned) { + SpannableString newString = new SpannableString(userName); + Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); + for (Object span : spans) { + int flags = spanned.getSpanFlags(span); + newString.setSpan(span, 0, newString.length(), flags); + } + return newString; + } + return original; + } + + public enum DisplayFormat { + USERNAME_ONLY(null), + USERNAME_HANDLE(TRUE), + HANDLE_USERNAME(FALSE); + + final Boolean userNameFirst; + + DisplayFormat(Boolean userNameFirst) { + this.userNameFirst = userNameFirst; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/SanitizeUrlQueryPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/SanitizeUrlQueryPatch.java new file mode 100644 index 000000000..c9e6c5d40 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/SanitizeUrlQueryPatch.java @@ -0,0 +1,50 @@ +package app.revanced.extension.shared.patches; + +import android.content.Intent; + +import app.revanced.extension.shared.settings.BaseSettings; + +@SuppressWarnings("all") +public final class SanitizeUrlQueryPatch { + /** + * This tracking parameter is mainly used. + */ + private static final String NEW_TRACKING_REGEX = ".si=.+"; + /** + * This tracking parameter is outdated. + * Used when patching old versions or enabling spoof app version. + */ + private static final String OLD_TRACKING_REGEX = ".feature=.+"; + private static final String URL_PROTOCOL = "http"; + + /** + * Strip query parameters from a given URL string. + *

+ * URL example containing tracking parameter: + * https://youtu.be/ZWgr7qP6yhY?si=kKA_-9cygieuFY7R + * https://youtu.be/ZWgr7qP6yhY?feature=shared + * https://youtube.com/watch?v=ZWgr7qP6yhY&si=s_PZAxnJHKX1Mc8C + * https://youtube.com/watch?v=ZWgr7qP6yhY&feature=shared + * https://youtube.com/playlist?list=PLBsP89CPrMeO7uztAu6YxSB10cRMpjgiY&si=N0U8xncY2ZmQoSMp + * https://youtube.com/playlist?list=PLBsP89CPrMeO7uztAu6YxSB10cRMpjgiY&feature=shared + *

+ * Since we need to support support all these examples, + * We cannot use [URL.getpath()] or [Uri.getQueryParameter()]. + * + * @param urlString URL string to strip query parameters from. + * @return URL string without query parameters if possible, otherwise the original string. + */ + public static String stripQueryParameters(final String urlString) { + if (!BaseSettings.SANITIZE_SHARING_LINKS.get()) + return urlString; + + return urlString.replaceAll(NEW_TRACKING_REGEX, "").replaceAll(OLD_TRACKING_REGEX, ""); + } + + public static void stripQueryParameters(final Intent intent, final String extraName, final String extraValue) { + intent.putExtra(extraName, extraValue.startsWith(URL_PROTOCOL) + ? stripQueryParameters(extraValue) + : extraValue + ); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/AppClient.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/AppClient.java new file mode 100644 index 000000000..67eee0854 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/client/AppClient.java @@ -0,0 +1,313 @@ +package app.revanced.extension.shared.patches.client; + +import static app.revanced.extension.shared.utils.ResourceUtils.getString; + +import android.os.Build; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.settings.BaseSettings; + +public class AppClient { + // IOS + /** + * Video not playable: Paid / Movie / Private / Age-restricted + * Note: Audio track available + */ + private static final String PACKAGE_NAME_IOS = "com.google.ios.youtube"; + /** + * The hardcoded client version of the iOS app used for InnerTube requests with this client. + * + *

+ * It can be extracted by getting the latest release version of the app on + * the App + * Store page of the YouTube app, in the {@code What’s New} section. + *

+ */ + private static final String CLIENT_VERSION_IOS = "19.29.1"; + /** + * The device machine id for the iPhone 15 Pro Max (iPhone16,2), used to get HDR with AV1 hardware decoding. + * + *

+ * See this GitHub Gist for more + * information. + *

+ */ + private static final String DEVICE_MODEL_IOS = "iPhone16,2"; + private static final String OS_VERSION_IOS = "17.7.2.21H221"; + private static final String USER_AGENT_VERSION_IOS = "17_7_2"; + private static final String USER_AGENT_IOS = + iOSUserAgent(PACKAGE_NAME_IOS, CLIENT_VERSION_IOS); + + + // IOS UNPLUGGED + /** + * Video not playable: Paid / Movie + * Note: Audio track available + */ + private static final String PACKAGE_NAME_IOS_UNPLUGGED = "com.google.ios.youtubeunplugged"; + /** + * The hardcoded client version of the iOS app used for InnerTube requests with this client. + * + *

+ * It can be extracted by getting the latest release version of the app on + * the App + * Store page of the YouTube TV app, in the {@code What’s New} section. + *

+ */ + private static final String CLIENT_VERSION_IOS_UNPLUGGED = "8.33"; + private static final String USER_AGENT_IOS_UNPLUGGED = + iOSUserAgent(PACKAGE_NAME_IOS_UNPLUGGED, CLIENT_VERSION_IOS_UNPLUGGED); + + + // IOS MUSIC + /** + * Video not playable: All videos that can't be played on YouTube Music + */ + private static final String PACKAGE_NAME_IOS_MUSIC = "com.google.ios.youtubemusic"; + /** + * The hardcoded client version of the iOS app used for InnerTube requests with this client. + * + *

+ * It can be extracted by getting the latest release version of the app on + * the App + * Store page of the YouTube Music app, in the {@code What’s New} section. + *

+ */ + private static final String CLIENT_VERSION_IOS_MUSIC = "7.04"; + private static final String USER_AGENT_IOS_MUSIC = + iOSUserAgent(PACKAGE_NAME_IOS_MUSIC, CLIENT_VERSION_IOS_MUSIC); + + + // ANDROID VR + /** + * Video not playable: Kids + * Note: Audio track is not available + *

+ * Package name for YouTube VR (Google DayDream): com.google.android.apps.youtube.vr (Deprecated) + * Package name for YouTube VR (Meta Quests): com.google.android.apps.youtube.vr.oculus + * Package name for YouTube VR (ByteDance Pico 4): com.google.android.apps.youtube.vr.pico + */ + private static final String PACKAGE_NAME_ANDROID_VR = "com.google.android.apps.youtube.vr.oculus"; + /** + * The hardcoded client version of the Android VR app used for InnerTube requests with this client. + * + *

+ * It can be extracted by getting the latest release version of the app on + * the App + * Store page of the YouTube app, in the {@code Additional details} section. + *

+ */ + private static final String CLIENT_VERSION_ANDROID_VR = "1.61.48"; + /** + * The device machine id for the Meta Quest 3, used to get opus codec with the Android VR client. + * + *

+ * See this GitLab for more + * information. + *

+ */ + private static final String DEVICE_MODEL_ANDROID_VR = "Quest 3"; + private static final String OS_VERSION_ANDROID_VR = "12"; + /** + * The SDK version for Android 12 is 31, + * but for some reason the build.props for the {@code Quest 3} state that the SDK version is 32. + */ + private static final String ANDROID_SDK_VERSION_ANDROID_VR = "32"; + private static final String USER_AGENT_ANDROID_VR = + androidUserAgent(PACKAGE_NAME_ANDROID_VR, CLIENT_VERSION_ANDROID_VR, OS_VERSION_ANDROID_VR); + + + // ANDROID UNPLUGGED + /** + * Video not playable: Playlists / Music + * Note: Audio track is not available + */ + private static final String PACKAGE_NAME_ANDROID_UNPLUGGED = "com.google.android.apps.youtube.unplugged"; + private static final String CLIENT_VERSION_ANDROID_UNPLUGGED = "8.16.0"; + /** + * The device machine id for the Chromecast with Google TV 4K. + * + *

+ * See this GitLab for more + * information. + *

+ */ + private static final String DEVICE_MODEL_ANDROID_UNPLUGGED = "Google TV Streamer"; + private static final String OS_VERSION_ANDROID_UNPLUGGED = "14"; + private static final String ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = "34"; + private static final String USER_AGENT_ANDROID_UNPLUGGED = + androidUserAgent(PACKAGE_NAME_ANDROID_UNPLUGGED, CLIENT_VERSION_ANDROID_UNPLUGGED, OS_VERSION_ANDROID_UNPLUGGED); + + + // ANDROID CREATOR + /** + * Video not playable: Livestream + * Note: Audio track is not available + */ + private static final String PACKAGE_NAME_ANDROID_CREATOR = "com.google.android.apps.youtube.creator"; + private static final String CLIENT_VERSION_ANDROID_CREATOR = "24.14.101"; + private static final String DEVICE_MODEL_ANDROID_CREATOR = Build.MODEL; + private static final String OS_VERSION_ANDROID_CREATOR = Build.VERSION.RELEASE; + private static final String ANDROID_SDK_VERSION_ANDROID_CREATOR = String.valueOf(Build.VERSION.SDK_INT); + private static final String USER_AGENT_ANDROID_CREATOR = + androidUserAgent(PACKAGE_NAME_ANDROID_CREATOR, CLIENT_VERSION_ANDROID_CREATOR, OS_VERSION_ANDROID_CREATOR); + + + private AppClient() { + } + + private static String androidUserAgent(String packageName, String clientVersion, String osVersion) { + return packageName + + "/" + + clientVersion + + " (Linux; U; Android " + + osVersion + + "; GB) gzip"; + } + + private static String iOSUserAgent(String packageName, String clientVersion) { + return packageName + + "/" + + clientVersion + + "(" + + DEVICE_MODEL_IOS + + "; U; CPU iOS " + + USER_AGENT_VERSION_IOS + + " like Mac OS X)"; + } + + public enum ClientType { + IOS(5, + DEVICE_MODEL_IOS, + OS_VERSION_IOS, + USER_AGENT_IOS, + null, + CLIENT_VERSION_IOS, + false + ), + ANDROID_VR(28, + DEVICE_MODEL_ANDROID_VR, + OS_VERSION_ANDROID_VR, + USER_AGENT_ANDROID_VR, + ANDROID_SDK_VERSION_ANDROID_VR, + CLIENT_VERSION_ANDROID_VR, + true + ), + ANDROID_UNPLUGGED(29, + DEVICE_MODEL_ANDROID_UNPLUGGED, + OS_VERSION_ANDROID_UNPLUGGED, + USER_AGENT_ANDROID_UNPLUGGED, + ANDROID_SDK_VERSION_ANDROID_UNPLUGGED, + CLIENT_VERSION_ANDROID_UNPLUGGED, + true + ), + ANDROID_CREATOR(14, + DEVICE_MODEL_ANDROID_CREATOR, + OS_VERSION_ANDROID_CREATOR, + USER_AGENT_ANDROID_CREATOR, + ANDROID_SDK_VERSION_ANDROID_CREATOR, + CLIENT_VERSION_ANDROID_CREATOR, + true + ), + IOS_UNPLUGGED(33, + DEVICE_MODEL_IOS, + OS_VERSION_IOS, + USER_AGENT_IOS_UNPLUGGED, + null, + CLIENT_VERSION_IOS_UNPLUGGED, + true + ), + IOS_MUSIC( + 26, + DEVICE_MODEL_IOS, + OS_VERSION_IOS, + USER_AGENT_IOS_MUSIC, + null, + CLIENT_VERSION_IOS_MUSIC, + true + ); + + /** + * YouTube + * client type + */ + public final int id; + + public final String clientName; + + /** + * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) + */ + public final String deviceModel; + + /** + * Device OS version. + */ + public final String osVersion; + + /** + * Player user-agent. + */ + public final String userAgent; + + /** + * Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk) + * Field is null if not applicable. + */ + @Nullable + public final String androidSdkVersion; + + /** + * App version. + */ + public final String clientVersion; + + /** + * If the client can access the API logged in. + */ + public final boolean canLogin; + + ClientType(int id, + String deviceModel, + String osVersion, + String userAgent, + @Nullable String androidSdkVersion, + String clientVersion, + boolean canLogin + ) { + this.id = id; + this.clientName = name(); + this.deviceModel = deviceModel; + this.clientVersion = clientVersion; + this.osVersion = osVersion; + this.androidSdkVersion = androidSdkVersion; + this.userAgent = userAgent; + this.canLogin = canLogin; + } + + private static final ClientType[] CLIENT_ORDER_TO_USE_ANDROID = { + ANDROID_VR, + ANDROID_UNPLUGGED, + ANDROID_CREATOR, + }; + + private static final ClientType[] CLIENT_ORDER_TO_USE_DEFAULT = { + IOS, + ANDROID_VR, + ANDROID_UNPLUGGED, + IOS_UNPLUGGED, + IOS_MUSIC, + }; + + public final String getFriendlyName() { + return getString("revanced_spoof_streaming_data_type_entry_" + name().toLowerCase()); + } + } + + public static ClientType[] getAvailableClientTypes() { + return BaseSettings.SPOOF_STREAMING_DATA_ANDROID_ONLY.get() + ? ClientType.CLIENT_ORDER_TO_USE_ANDROID + : ClientType.CLIENT_ORDER_TO_USE_DEFAULT; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/ByteArrayFilterGroup.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/ByteArrayFilterGroup.java new file mode 100644 index 000000000..18a94365c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/ByteArrayFilterGroup.java @@ -0,0 +1,98 @@ +package app.revanced.extension.shared.patches.components; + +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.utils.ByteTrieSearch; +import app.revanced.extension.shared.utils.Logger; + +/** + * If you have more than 1 filter patterns, then all instances of + * this class should filtered using {@link ByteArrayFilterGroupList#check(byte[])}, + * which uses a prefix tree to give better performance. + */ +@SuppressWarnings("unused") +public class ByteArrayFilterGroup extends FilterGroup { + + private volatile int[][] failurePatterns; + + // Modified implementation from https://stackoverflow.com/a/1507813 + private static int indexOf(final byte[] data, final byte[] pattern, final int[] failure) { + // Finds the first occurrence of the pattern in the byte array using + // KMP matching algorithm. + int patternLength = pattern.length; + for (int i = 0, j = 0, dataLength = data.length; i < dataLength; i++) { + while (j > 0 && pattern[j] != data[i]) { + j = failure[j - 1]; + } + if (pattern[j] == data[i]) { + j++; + } + if (j == patternLength) { + return i - patternLength + 1; + } + } + return -1; + } + + private static int[] createFailurePattern(byte[] pattern) { + // Computes the failure function using a boot-strapping process, + // where the pattern is matched against itself. + final int patternLength = pattern.length; + final int[] failure = new int[patternLength]; + + for (int i = 1, j = 0; i < patternLength; i++) { + while (j > 0 && pattern[j] != pattern[i]) { + j = failure[j - 1]; + } + if (pattern[j] == pattern[i]) { + j++; + } + failure[i] = j; + } + return failure; + } + + public ByteArrayFilterGroup(BooleanSetting setting, byte[]... filters) { + super(setting, filters); + } + + /** + * Converts the Strings into byte arrays. Used to search for text in binary data. + */ + public ByteArrayFilterGroup(BooleanSetting setting, String... filters) { + super(setting, ByteTrieSearch.convertStringsToBytes(filters)); + } + + private synchronized void buildFailurePatterns() { + if (failurePatterns != null) + return; // Thread race and another thread already initialized the search. + Logger.printDebug(() -> "Building failure array for: " + this); + int[][] failurePatterns = new int[filters.length][]; + int i = 0; + for (byte[] pattern : filters) { + failurePatterns[i++] = createFailurePattern(pattern); + } + this.failurePatterns = failurePatterns; // Must set after initialization finishes. + } + + @Override + public FilterGroupResult check(final byte[] bytes) { + int matchedLength = 0; + int matchedIndex = -1; + if (isEnabled()) { + int[][] failures = failurePatterns; + if (failures == null) { + buildFailurePatterns(); // Lazy load. + failures = failurePatterns; + } + for (int i = 0, length = filters.length; i < length; i++) { + byte[] filter = filters[i]; + matchedIndex = indexOf(bytes, filter, failures[i]); + if (matchedIndex >= 0) { + matchedLength = filter.length; + break; + } + } + } + return new FilterGroupResult(setting, matchedIndex, matchedLength); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/ByteArrayFilterGroupList.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/ByteArrayFilterGroupList.java new file mode 100644 index 000000000..52bbbbab0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/ByteArrayFilterGroupList.java @@ -0,0 +1,14 @@ +package app.revanced.extension.shared.patches.components; + +import app.revanced.extension.shared.utils.ByteTrieSearch; + +/** + * If searching for a single byte pattern, then it is slightly better to use + * {@link ByteArrayFilterGroup#check(byte[])} as it uses KMP which is faster + * than a prefix tree to search for only 1 pattern. + */ +public final class ByteArrayFilterGroupList extends FilterGroupList { + protected ByteTrieSearch createSearchGraph() { + return new ByteTrieSearch(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/Filter.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/Filter.java new file mode 100644 index 000000000..77123be16 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/Filter.java @@ -0,0 +1,106 @@ +package app.revanced.extension.shared.patches.components; + +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.utils.Logger; + +/** + * Filters litho based components. + *

+ * Callbacks to filter content are added using {@link #addIdentifierCallbacks(StringFilterGroup...)} + * and {@link #addPathCallbacks(StringFilterGroup...)}. + *

+ * To filter {@link FilterContentType#PROTOBUFFER}, first add a callback to + * either an identifier or a path. + * Then inside {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)} + * search for the buffer content using either a {@link ByteArrayFilterGroup} (if searching for 1 pattern) + * or a {@link ByteArrayFilterGroupList} (if searching for more than 1 pattern). + *

+ * All callbacks must be registered before the constructor completes. + */ +@SuppressWarnings("unused") +public abstract class Filter { + + public enum FilterContentType { + IDENTIFIER, + PATH, + ALLVALUE, + PROTOBUFFER + } + + /** + * Identifier callbacks. Do not add to this instance, + * and instead use {@link #addIdentifierCallbacks(StringFilterGroup...)}. + */ + protected final List identifierCallbacks = new ArrayList<>(); + /** + * Path callbacks. Do not add to this instance, + * and instead use {@link #addPathCallbacks(StringFilterGroup...)}. + */ + protected final List pathCallbacks = new ArrayList<>(); + /** + * Path callbacks. Do not add to this instance, + * and instead use {@link #addAllValueCallbacks(StringFilterGroup...)}. + */ + protected final List allValueCallbacks = new ArrayList<>(); + + /** + * Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)} + * if any of the groups are found. + */ + protected final void addIdentifierCallbacks(StringFilterGroup... groups) { + identifierCallbacks.addAll(Arrays.asList(groups)); + } + + /** + * Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)} + * if any of the groups are found. + */ + protected final void addPathCallbacks(StringFilterGroup... groups) { + pathCallbacks.addAll(Arrays.asList(groups)); + } + + /** + * Adds callbacks to {@link #isFiltered(String, String, String, byte[], StringFilterGroup, FilterContentType, int)} + * if any of the groups are found. + */ + protected final void addAllValueCallbacks(StringFilterGroup... groups) { + allValueCallbacks.addAll(Arrays.asList(groups)); + } + + /** + * Called after an enabled filter has been matched. + * Default implementation is to always filter the matched component and log the action. + * Subclasses can perform additional or different checks if needed. + *

+ * 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. + *

+ * Method is called off the main thread. + * + * @param matchedGroup The actual filter that matched. + * @param contentType The type of content matched. + * @param contentIndex Matched index of the identifier or path. + * @return True if the litho component should be filtered out. + */ + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (BaseSettings.ENABLE_DEBUG_LOGGING.get()) { + String filterSimpleName = getClass().getSimpleName(); + if (contentType == FilterContentType.IDENTIFIER) { + Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier); + } else if (contentType == FilterContentType.PATH) { + Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path); + } else if (contentType == FilterContentType.ALLVALUE) { + Logger.printDebug(() -> filterSimpleName + " Filtered object: " + allValue); + } + } + return true; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/FilterGroup.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/FilterGroup.java new file mode 100644 index 000000000..e580ea5ce --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/FilterGroup.java @@ -0,0 +1,95 @@ +package app.revanced.extension.shared.patches.components; + +import androidx.annotation.NonNull; + +import app.revanced.extension.shared.settings.BooleanSetting; + +@SuppressWarnings("unused") +public abstract class FilterGroup { + public final static class FilterGroupResult { + private BooleanSetting setting; + private int matchedIndex; + private int matchedLength; + // In the future it might be useful to include which pattern matched, + // but for now that is not needed. + + FilterGroupResult() { + this(null, -1, 0); + } + + FilterGroupResult(BooleanSetting setting, int matchedIndex, int matchedLength) { + setValues(setting, matchedIndex, matchedLength); + } + + public void setValues(BooleanSetting setting, int matchedIndex, int matchedLength) { + this.setting = setting; + this.matchedIndex = matchedIndex; + this.matchedLength = matchedLength; + } + + /** + * A null value if the group has no setting, + * or if no match is returned from {@link FilterGroupList#check(Object)}. + */ + public BooleanSetting getSetting() { + return setting; + } + + public boolean isFiltered() { + return matchedIndex >= 0; + } + + /** + * Matched index of first pattern that matched, or -1 if nothing matched. + */ + public int getMatchedIndex() { + return matchedIndex; + } + + /** + * Length of the matched filter pattern. + */ + public int getMatchedLength() { + return matchedLength; + } + } + + protected final BooleanSetting setting; + protected final T[] filters; + + /** + * Initialize a new filter group. + * + * @param setting The associated setting. + * @param filters The filters. + */ + @SafeVarargs + public FilterGroup(final BooleanSetting setting, final T... filters) { + this.setting = setting; + this.filters = filters; + if (filters.length == 0) { + throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)"); + } + } + + public boolean isEnabled() { + return setting == null || setting.get(); + } + + /** + * @return If {@link FilterGroupList} should include this group when searching. + * By default, all filters are included except non enabled settings that require reboot. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean includeInSearch() { + return isEnabled() || !setting.rebootApp; + } + + @NonNull + @Override + public String toString() { + return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting); + } + + public abstract FilterGroupResult check(final T stack); +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/FilterGroupList.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/FilterGroupList.java new file mode 100644 index 000000000..62e08a7e2 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/FilterGroupList.java @@ -0,0 +1,69 @@ +package app.revanced.extension.shared.patches.components; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.function.Consumer; + +import app.revanced.extension.shared.utils.TrieSearch; + +@SuppressWarnings("unused") +public abstract class FilterGroupList> implements Iterable { + + private final List filterGroups = new ArrayList<>(); + private final TrieSearch search = createSearchGraph(); + + @SafeVarargs + public final void addAll(final T... groups) { + filterGroups.addAll(Arrays.asList(groups)); + + for (T group : groups) { + if (!group.includeInSearch()) { + continue; + } + for (V pattern : group.filters) { + search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { + if (group.isEnabled()) { + FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter; + result.setValues(group.setting, matchedStartIndex, matchedLength); + return true; + } + return false; + }); + } + } + } + + @NonNull + @Override + public Iterator iterator() { + return filterGroups.iterator(); + } + + @RequiresApi(24) + @Override + public void forEach(@NonNull Consumer action) { + filterGroups.forEach(action); + } + + @RequiresApi(24) + @NonNull + @Override + public Spliterator spliterator() { + return filterGroups.spliterator(); + } + + public FilterGroup.FilterGroupResult check(V stack) { + FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult(); + search.matches(stack, result); + return result; + + } + + protected abstract TrieSearch createSearchGraph(); +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/LithoFilterPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/LithoFilterPatch.java new file mode 100644 index 000000000..6e59379af --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/LithoFilterPatch.java @@ -0,0 +1,191 @@ +package app.revanced.extension.shared.patches.components; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.nio.ByteBuffer; +import java.util.List; + +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.StringTrieSearch; + +@SuppressWarnings("unused") +public final class LithoFilterPatch { + /** + * Simple wrapper to pass the litho parameters through the prefix search. + */ + private static final class LithoFilterParameters { + @Nullable + final String identifier; + final String path; + final String allValue; + final byte[] protoBuffer; + + LithoFilterParameters(String lithoPath, @Nullable String lithoIdentifier, String allValues, byte[] bufferArray) { + this.path = lithoPath; + this.identifier = lithoIdentifier; + this.allValue = allValues; + this.protoBuffer = bufferArray; + } + + @NonNull + @Override + public String toString() { + // Estimate the percentage of the buffer that are Strings. + StringBuilder builder = new StringBuilder(Math.max(100, protoBuffer.length / 2)); + builder.append("\nID: "); + builder.append(identifier); + builder.append("\nPath: "); + builder.append(path); + if (BaseSettings.ENABLE_DEBUG_BUFFER_LOGGING.get()) { + builder.append("\nBufferStrings: "); + findAsciiStrings(builder, protoBuffer); + } + + return builder.toString(); + } + + /** + * Search through a byte array for all ASCII strings. + */ + private static void findAsciiStrings(StringBuilder builder, byte[] buffer) { + // Valid ASCII values (ignore control characters). + final int minimumAscii = 32; // 32 = space character + final int maximumAscii = 126; // 127 = delete character + final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include. + String delimitingCharacter = "❙"; // Non ascii character, to allow easier log filtering. + + final int length = buffer.length; + int start = 0; + int end = 0; + while (end < length) { + int value = buffer[end]; + if (value < minimumAscii || value > maximumAscii || end == length - 1) { + if (end - start >= minimumAsciiStringLength) { + for (int i = start; i < end; i++) { + builder.append((char) buffer[i]); + } + builder.append(delimitingCharacter); + } + start = end + 1; + } + end++; + } + } + } + + private static final Filter[] filters = new Filter[]{ + new DummyFilter() // Replaced by patch. + }; + + private static final StringTrieSearch pathSearchTree = new StringTrieSearch(); + private static final StringTrieSearch identifierSearchTree = new StringTrieSearch(); + private static final StringTrieSearch allValueSearchTree = new StringTrieSearch(); + + private static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + + /** + * Because litho filtering is multi-threaded and the buffer is passed in from a different injection point, + * the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads. + */ + private static final ThreadLocal bufferThreadLocal = new ThreadLocal<>(); + + static { + for (Filter filter : filters) { + filterUsingCallbacks(identifierSearchTree, filter, + filter.identifierCallbacks, Filter.FilterContentType.IDENTIFIER); + filterUsingCallbacks(pathSearchTree, filter, + filter.pathCallbacks, Filter.FilterContentType.PATH); + filterUsingCallbacks(allValueSearchTree, filter, + filter.allValueCallbacks, Filter.FilterContentType.ALLVALUE); + } + + Logger.printDebug(() -> "Using: " + + identifierSearchTree.numberOfPatterns() + " identifier filters" + + " (" + identifierSearchTree.getEstimatedMemorySize() + " KB), " + + pathSearchTree.numberOfPatterns() + " path filters" + + " (" + pathSearchTree.getEstimatedMemorySize() + " KB)"); + } + + private static void filterUsingCallbacks(StringTrieSearch pathSearchTree, + Filter filter, List groups, + Filter.FilterContentType type) { + for (StringFilterGroup group : groups) { + if (!group.includeInSearch()) { + continue; + } + for (String pattern : group.filters) { + pathSearchTree.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { + if (!group.isEnabled()) return false; + LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter; + return filter.isFiltered(parameters.path, parameters.identifier, parameters.allValue, parameters.protoBuffer, + group, type, matchedStartIndex); + } + ); + } + } + } + + /** + * Injection point. Called off the main thread. + */ + public static void setProtoBuffer(@Nullable ByteBuffer protobufBuffer) { + // Set the buffer to a thread local. The buffer will remain in memory, even after the call to #filter completes. + // This is intentional, as it appears the buffer can be set once and then filtered multiple times. + // The buffer will be cleared from memory after a new buffer is set by the same thread, + // or when the calling thread eventually dies. + bufferThreadLocal.set(protobufBuffer); + } + + /** + * Injection point. Called off the main thread, and commonly called by multiple threads at the same time. + */ + public static boolean filter(@NonNull StringBuilder pathBuilder, @Nullable String identifier, @NonNull Object object) { + try { + if (pathBuilder.length() == 0) { + return false; + } + + ByteBuffer protobufBuffer = bufferThreadLocal.get(); + final byte[] bufferArray; + // Potentially the buffer may have been null or never set up until now. + // Use an empty buffer so the litho id or path filters still work correctly. + if (protobufBuffer == null) { + Logger.printDebug(() -> "Proto buffer is null, using an empty buffer array"); + bufferArray = EMPTY_BYTE_ARRAY; + } else if (!protobufBuffer.hasArray()) { + Logger.printDebug(() -> "Proto buffer does not have an array, using an empty buffer array"); + bufferArray = EMPTY_BYTE_ARRAY; + } else { + bufferArray = protobufBuffer.array(); + } + + LithoFilterParameters parameter = new LithoFilterParameters(pathBuilder.toString(), identifier, + object.toString(), bufferArray); + Logger.printDebug(() -> "Searching " + parameter); + + if (parameter.identifier != null && identifierSearchTree.matches(parameter.identifier, parameter)) { + return true; + } + + if (pathSearchTree.matches(parameter.path, parameter)) { + return true; + } + + if (allValueSearchTree.matches(parameter.allValue, parameter)) { + return true; + } + } catch (Exception ex) { + Logger.printException(() -> "Litho filter failure", ex); + } + + return false; + } +} + +/** + * Placeholder for actual filters. + */ +final class DummyFilter extends Filter { +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/StringFilterGroup.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/StringFilterGroup.java new file mode 100644 index 000000000..9ac111cf9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/StringFilterGroup.java @@ -0,0 +1,29 @@ +package app.revanced.extension.shared.patches.components; + +import app.revanced.extension.shared.settings.BooleanSetting; + +public class StringFilterGroup extends FilterGroup { + + public StringFilterGroup(final BooleanSetting setting, final String... filters) { + super(setting, filters); + } + + @Override + public FilterGroupResult check(final String string) { + int matchedIndex = -1; + int matchedLength = 0; + if (isEnabled()) { + for (String pattern : filters) { + if (!string.isEmpty()) { + final int indexOf = string.indexOf(pattern); + if (indexOf >= 0) { + matchedIndex = indexOf; + matchedLength = pattern.length(); + break; + } + } + } + } + return new FilterGroupResult(setting, matchedIndex, matchedLength); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/StringFilterGroupList.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/StringFilterGroupList.java new file mode 100644 index 000000000..ae6c189e9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/components/StringFilterGroupList.java @@ -0,0 +1,9 @@ +package app.revanced.extension.shared.patches.components; + +import app.revanced.extension.shared.utils.StringTrieSearch; + +public final class StringFilterGroupList extends FilterGroupList { + protected StringTrieSearch createSearchGraph() { + return new StringTrieSearch(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/Filter.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/Filter.java new file mode 100644 index 000000000..566fb96e0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/Filter.java @@ -0,0 +1,70 @@ +package app.revanced.extension.shared.patches.spans; + +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.text.SpannableString; +import android.text.style.ImageSpan; +import android.text.style.RelativeSizeSpan; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.utils.Logger; + +/** + * Filters litho based components. + *

+ * All callbacks must be registered before the constructor completes. + */ +public abstract class Filter { + private static final RelativeSizeSpan relativeSizeSpanDummy = new RelativeSizeSpan(0f); + private static final Drawable transparentDrawable = new ColorDrawable(Color.TRANSPARENT); + private static final ImageSpan imageSpanDummy = new ImageSpan(transparentDrawable); + + /** + * Path callbacks. Do not add to this instance, + * and instead use {@link #addCallbacks(StringFilterGroup...)}. + */ + protected final List callbacks = new ArrayList<>(); + + /** + * Adds callbacks to {@link #skip(String, SpannableString, Object, int, int, int, boolean, SpanType, StringFilterGroup)} + * if any of the groups are found. + */ + protected final void addCallbacks(StringFilterGroup... groups) { + callbacks.addAll(Arrays.asList(groups)); + } + + protected final void hideSpan(SpannableString spannableString, int start, int end, int flags) { + spannableString.setSpan(relativeSizeSpanDummy, start, end, flags); + } + + protected final void hideImageSpan(SpannableString spannableString, int start, int end, int flags) { + spannableString.setSpan(imageSpanDummy, start, end, flags); + } + + /** + * Called after an enabled filter has been matched. + * Default implementation is to always filter the matched component and log the action. + * Subclasses can perform additional or different checks if needed. + *

+ * 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. + *

+ * Method is called off the main thread. + * + * @param matchedGroup The actual filter that matched. + */ + public boolean skip(String conversionContext, SpannableString spannableString, Object span, int start, int end, + int flags, boolean isWord, SpanType spanType, StringFilterGroup matchedGroup) { + if (BaseSettings.ENABLE_DEBUG_LOGGING.get()) { + String filterSimpleName = getClass().getSimpleName(); + Logger.printDebug(() -> filterSimpleName + " Removed setSpan: " + spanType.type); + } + return true; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/FilterGroup.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/FilterGroup.java new file mode 100644 index 000000000..d1dc3c2a0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/FilterGroup.java @@ -0,0 +1,78 @@ +package app.revanced.extension.shared.patches.spans; + +import androidx.annotation.NonNull; + +import app.revanced.extension.shared.settings.BooleanSetting; + +public abstract class FilterGroup { + final static class FilterGroupResult { + private BooleanSetting setting; + private int matchedIndex; + // In the future it might be useful to include which pattern matched, + // but for now that is not needed. + + FilterGroupResult() { + this(null, -1); + } + + FilterGroupResult(BooleanSetting setting, int matchedIndex) { + setValues(setting, matchedIndex); + } + + public void setValues(BooleanSetting setting, int matchedIndex) { + this.setting = setting; + this.matchedIndex = matchedIndex; + } + + /** + * A null value if the group has no setting, + * or if no match is returned from {@link FilterGroupList#check(Object)}. + */ + public BooleanSetting getSetting() { + return setting; + } + + public boolean isFiltered() { + return matchedIndex >= 0; + } + } + + protected final BooleanSetting setting; + protected final T[] filters; + + /** + * Initialize a new filter group. + * + * @param setting The associated setting. + * @param filters The filters. + */ + @SafeVarargs + public FilterGroup(final BooleanSetting setting, final T... filters) { + this.setting = setting; + this.filters = filters; + if (filters.length == 0) { + throw new IllegalArgumentException("Must use one or more filter patterns (zero specified)"); + } + } + + public boolean isEnabled() { + return setting == null || setting.get(); + } + + /** + * @return If {@link FilterGroupList} should include this group when searching. + * By default, all filters are included except non enabled settings that require reboot. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public boolean includeInSearch() { + return isEnabled() || !setting.rebootApp; + } + + @NonNull + @Override + public String toString() { + return getClass().getSimpleName() + ": " + (setting == null ? "(null setting)" : setting); + } + + public abstract FilterGroupResult check(final T stack); +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/FilterGroupList.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/FilterGroupList.java new file mode 100644 index 000000000..16c82cf61 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/FilterGroupList.java @@ -0,0 +1,65 @@ +package app.revanced.extension.shared.patches.spans; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Spliterator; +import java.util.function.Consumer; + +import app.revanced.extension.shared.utils.TrieSearch; + +public abstract class FilterGroupList> implements Iterable { + + private final List filterGroups = new ArrayList<>(); + private final TrieSearch search = createSearchGraph(); + + @SafeVarargs + protected final void addAll(final T... groups) { + filterGroups.addAll(Arrays.asList(groups)); + + for (T group : groups) { + if (!group.includeInSearch()) { + continue; + } + for (V pattern : group.filters) { + search.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { + if (group.isEnabled()) { + FilterGroup.FilterGroupResult result = (FilterGroup.FilterGroupResult) callbackParameter; + result.setValues(group.setting, matchedStartIndex); + return true; + } + return false; + }); + } + } + } + + @NonNull + @Override + public Iterator iterator() { + return filterGroups.iterator(); + } + + @Override + public void forEach(@NonNull Consumer action) { + filterGroups.forEach(action); + } + + @NonNull + @Override + public Spliterator spliterator() { + return filterGroups.spliterator(); + } + + protected FilterGroup.FilterGroupResult check(V stack) { + FilterGroup.FilterGroupResult result = new FilterGroup.FilterGroupResult(); + search.matches(stack, result); + return result; + + } + + protected abstract TrieSearch createSearchGraph(); +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/InclusiveSpanPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/InclusiveSpanPatch.java new file mode 100644 index 000000000..fead03f7e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/InclusiveSpanPatch.java @@ -0,0 +1,202 @@ +package app.revanced.extension.shared.patches.spans; + +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.AbsoluteSizeSpan; +import android.text.style.CharacterStyle; +import android.text.style.ClickableSpan; +import android.text.style.ForegroundColorSpan; +import android.text.style.ImageSpan; +import android.text.style.TypefaceSpan; + +import androidx.annotation.NonNull; + +import java.util.List; + +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.StringTrieSearch; + + +/** + * Placeholder for actual filters. + */ +final class DummyFilter extends Filter { +} + +@SuppressWarnings("unused") +public final class InclusiveSpanPatch { + private static final BooleanSetting ENABLE_DEBUG_LOGGING = BaseSettings.ENABLE_DEBUG_LOGGING; + + /** + * Simple wrapper to pass the litho parameters through the prefix search. + */ + private static final class LithoFilterParameters { + final String conversionContext; + final SpannableString spannableString; + final Object span; + final int start; + final int end; + final int flags; + final String originalString; + final int originalLength; + final SpanType spanType; + final boolean isWord; + + public LithoFilterParameters(String conversionContext, SpannableString spannableString, + Object span, int start, int end, int flags) { + this.conversionContext = conversionContext; + this.spannableString = spannableString; + this.span = span; + this.start = start; + this.end = end; + this.flags = flags; + this.originalString = spannableString.toString(); + this.originalLength = spannableString.length(); + this.spanType = getSpanType(span); + this.isWord = !(start == 0 && end == originalLength); + } + + @NonNull + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("CharSequence:'") + .append(originalString) + .append("'\nSpanType:'") + .append(getSpanType(spanType, span)) + .append("'\nLength:'") + .append(originalLength) + .append("'\nStart:'") + .append(start) + .append("'\nEnd:'") + .append(end) + .append("'\nisWord:'") + .append(isWord) + .append("'"); + if (isWord) { + builder.append("\nWord:'") + .append(originalString.substring(start, end)) + .append("'"); + } + return builder.toString(); + } + } + + private static SpanType getSpanType(Object span) { + if (span instanceof ClickableSpan) { + return SpanType.CLICKABLE; + } else if (span instanceof ForegroundColorSpan) { + return SpanType.FOREGROUND_COLOR; + } else if (span instanceof AbsoluteSizeSpan) { + return SpanType.ABSOLUTE_SIZE; + } else if (span instanceof TypefaceSpan) { + return SpanType.TYPEFACE; + } else if (span instanceof ImageSpan) { + return SpanType.IMAGE; + } else if (span instanceof CharacterStyle) { // Replaced by patch. + return SpanType.CUSTOM_CHARACTER_STYLE; + } else { + return SpanType.UNKNOWN; + } + } + + private static String getSpanType(SpanType spanType, Object span) { + return spanType == SpanType.UNKNOWN + ? span.getClass().getSimpleName() + : spanType.type; + } + + private static final Filter[] filters = new Filter[]{ + new DummyFilter() // Replaced by patch. + }; + + private static final StringTrieSearch searchTree = new StringTrieSearch(); + + + /** + * Because litho filtering is multi-threaded and the buffer is passed in from a different injection point, + * the buffer is saved to a ThreadLocal so each calling thread does not interfere with other threads. + */ + private static final ThreadLocal conversionContextThreadLocal = new ThreadLocal<>(); + + static { + for (Filter filter : filters) { + filterUsingCallbacks(filter, filter.callbacks); + } + + if (ENABLE_DEBUG_LOGGING.get()) { + Logger.printDebug(() -> "Using: " + + searchTree.numberOfPatterns() + " conversion context filters" + + " (" + searchTree.getEstimatedMemorySize() + " KB)"); + } + } + + private static void filterUsingCallbacks(Filter filter, List groups) { + for (StringFilterGroup group : groups) { + if (!group.includeInSearch()) { + continue; + } + for (String pattern : group.filters) { + InclusiveSpanPatch.searchTree.addPattern(pattern, (textSearched, matchedStartIndex, matchedLength, callbackParameter) -> { + if (!group.isEnabled()) return false; + LithoFilterParameters parameters = (LithoFilterParameters) callbackParameter; + return filter.skip(parameters.conversionContext, parameters.spannableString, parameters.span, + parameters.start, parameters.end, parameters.flags, parameters.isWord, parameters.spanType, group); + } + ); + } + } + } + + /** + * Injection point. + * + * @param conversionContext ConversionContext is used to identify whether it is a comment thread or not. + */ + public static CharSequence setConversionContext(@NonNull Object conversionContext, + @NonNull CharSequence original) { + conversionContextThreadLocal.set(conversionContext.toString()); + return original; + } + + private static boolean returnEarly(SpannableString spannableString, Object span, int start, int end, int flags) { + try { + final String conversionContext = conversionContextThreadLocal.get(); + if (conversionContext == null || conversionContext.isEmpty()) { + return false; + } + + LithoFilterParameters parameter = + new LithoFilterParameters(conversionContext, spannableString, span, start, end, flags); + + if (ENABLE_DEBUG_LOGGING.get()) { + Logger.printDebug(() -> "Searching...\n\u200B\n" + parameter); + } + + return searchTree.matches(parameter.conversionContext, parameter); + } catch (Exception ex) { + Logger.printException(() -> "Spans filter failure", ex); + } + + return false; + } + + /** + * Injection point. + * + * @param spannableString Original SpannableString. + * @param span Span such as {@link ClickableSpan}, {@link ForegroundColorSpan}, + * {@link AbsoluteSizeSpan}, {@link TypefaceSpan}, {@link ImageSpan}. + * @param start Start index of {@link Spannable#setSpan(Object, int, int, int)}. + * @param end End index of {@link Spannable#setSpan(Object, int, int, int)}. + * @param flags Flags of {@link Spannable#setSpan(Object, int, int, int)}. + */ + public static void setSpan(SpannableString spannableString, Object span, int start, int end, int flags) { + if (returnEarly(spannableString, span, start, end, flags)) { + return; + } + spannableString.setSpan(span, start, end, flags); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/SpanType.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/SpanType.java new file mode 100644 index 000000000..0ba705410 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/SpanType.java @@ -0,0 +1,20 @@ +package app.revanced.extension.shared.patches.spans; + +import androidx.annotation.NonNull; + +public enum SpanType { + CLICKABLE("ClickableSpan"), + FOREGROUND_COLOR("ForegroundColorSpan"), + ABSOLUTE_SIZE("AbsoluteSizeSpan"), + TYPEFACE("TypefaceSpan"), + IMAGE("ImageSpan"), + CUSTOM_CHARACTER_STYLE("CustomCharacterStyle"), + UNKNOWN("Unknown"); + + @NonNull + public final String type; + + SpanType(@NonNull String type) { + this.type = type; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/StringFilterGroup.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/StringFilterGroup.java new file mode 100644 index 000000000..384153318 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/StringFilterGroup.java @@ -0,0 +1,27 @@ +package app.revanced.extension.shared.patches.spans; + +import app.revanced.extension.shared.settings.BooleanSetting; + +public class StringFilterGroup extends FilterGroup { + + public StringFilterGroup(final BooleanSetting setting, final String... filters) { + super(setting, filters); + } + + @Override + public FilterGroupResult check(final String string) { + int matchedIndex = -1; + if (isEnabled()) { + for (String pattern : filters) { + if (!string.isEmpty()) { + final int indexOf = string.indexOf(pattern); + if (indexOf >= 0) { + matchedIndex = indexOf; + break; + } + } + } + } + return new FilterGroupResult(setting, matchedIndex); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java new file mode 100644 index 000000000..f1ca98e32 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/SpoofStreamingDataPatch.java @@ -0,0 +1,256 @@ +package app.revanced.extension.shared.patches.spoof; + +import static app.revanced.extension.shared.patches.PatchStatus.SpoofStreamingData; + +import android.net.Uri; +import android.text.TextUtils; + +import androidx.annotation.Nullable; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import app.revanced.extension.shared.patches.spoof.requests.StreamingDataRequest; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings("unused") +public class SpoofStreamingDataPatch { + public static final boolean SPOOF_STREAMING_DATA = SpoofStreamingData() && BaseSettings.SPOOF_STREAMING_DATA.get(); + + /** + * Any unreachable ip address. Used to intentionally fail requests. + */ + private static final String UNREACHABLE_HOST_URI_STRING = "https://127.0.0.0"; + private static final Uri UNREACHABLE_HOST_URI = Uri.parse(UNREACHABLE_HOST_URI_STRING); + + /** + * Key: video id + * Value: original video length [streamingData.formats.approxDurationMs] + */ + private static final Map approxDurationMsMap = Collections.synchronizedMap( + new LinkedHashMap<>(10) { + private static final int CACHE_LIMIT = 5; + + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit. + } + }); + + /** + * Injection point. + * Blocks /get_watch requests by returning an unreachable URI. + * + * @param playerRequestUri The URI of the player request. + * @return An unreachable URI if the request is a /get_watch request, otherwise the original URI. + */ + public static Uri blockGetWatchRequest(Uri playerRequestUri) { + if (SPOOF_STREAMING_DATA) { + try { + String path = playerRequestUri.getPath(); + + if (path != null && path.contains("get_watch")) { + Logger.printDebug(() -> "Blocking 'get_watch' by returning unreachable uri"); + + return UNREACHABLE_HOST_URI; + } + } catch (Exception ex) { + Logger.printException(() -> "blockGetWatchRequest failure", ex); + } + } + + return playerRequestUri; + } + + /** + * Injection point. + *

+ * Blocks /initplayback requests. + */ + public static String blockInitPlaybackRequest(String originalUrlString) { + if (SPOOF_STREAMING_DATA) { + try { + var originalUri = Uri.parse(originalUrlString); + String path = originalUri.getPath(); + + if (path != null && path.contains("initplayback")) { + Logger.printDebug(() -> "Blocking 'initplayback' by clearing query"); + + return originalUri.buildUpon().clearQuery().build().toString(); + } + } catch (Exception ex) { + Logger.printException(() -> "blockInitPlaybackRequest failure", ex); + } + } + + return originalUrlString; + } + + /** + * Injection point. + */ + public static boolean isSpoofingEnabled() { + return SPOOF_STREAMING_DATA; + } + + /** + * Injection point. + * This method is only invoked when playing a livestream on an iOS client. + */ + public static boolean fixHLSCurrentTime(boolean original) { + if (!SPOOF_STREAMING_DATA) { + return original; + } + return false; + } + + /** + * Injection point. + */ + public static void fetchStreams(String url, Map requestHeaders) { + if (SPOOF_STREAMING_DATA) { + try { + Uri uri = Uri.parse(url); + String path = uri.getPath(); + + // 'heartbeat' has no video id and appears to be only after playback has started. + // 'refresh' has no video id and appears to happen when waiting for a livestream to start. + if (path != null && path.contains("player") && !path.contains("heartbeat") + && !path.contains("refresh")) { + String id = uri.getQueryParameter("id"); + if (id == null) { + Logger.printException(() -> "Ignoring request that has no video id." + + " Url: " + url + " headers: " + requestHeaders); + return; + } + + StreamingDataRequest.fetchRequest(id, requestHeaders); + } + } catch (Exception ex) { + Logger.printException(() -> "buildRequest failure", ex); + } + } + } + + /** + * Injection point. + * Fix playback by replace the streaming data. + * Called after {@link #fetchStreams(String, Map)}. + */ + @Nullable + public static ByteBuffer getStreamingData(String videoId) { + if (SPOOF_STREAMING_DATA) { + try { + StreamingDataRequest request = StreamingDataRequest.getRequestForVideoId(videoId); + if (request != null) { + // This hook is always called off the main thread, + // but this can later be called for the same video id from the main thread. + // This is not a concern, since the fetch will always be finished + // and never block the main thread. + // But if debugging, then still verify this is the situation. + if (BaseSettings.ENABLE_DEBUG_LOGGING.get() && !request.fetchCompleted() && Utils.isCurrentlyOnMainThread()) { + Logger.printException(() -> "Error: Blocking main thread"); + } + + var stream = request.getStream(); + if (stream != null) { + Logger.printDebug(() -> "Overriding video stream: " + videoId); + return stream; + } + } + + Logger.printDebug(() -> "Not overriding streaming data (video stream is null): " + videoId); + } catch (Exception ex) { + Logger.printException(() -> "getStreamingData failure", ex); + } + } + + return null; + } + + /** + * Injection point. + *

+ * If spoofed [streamingData.formats] is empty, + * Put the original [streamingData.formats.approxDurationMs] into the HashMap. + *

+ * Called after {@link #getStreamingData(String)}. + */ + public static void setApproxDurationMs(String videoId, long approxDurationMs) { + if (approxDurationMs != Long.MAX_VALUE) { + approxDurationMsMap.put(videoId, approxDurationMs); + Logger.printDebug(() -> "New approxDurationMs loaded, video id: " + videoId + ", video length: " + approxDurationMs); + } + } + + /** + * Injection point. + *

+ * When measuring the length of a video in an Android YouTube client, + * the client first checks if the streaming data contains [streamingData.formats.approxDurationMs]. + *

+ * If the streaming data response contains [approxDurationMs] (Long type, actual value), this value will be the video length. + *

+ * If [streamingData.formats] (List type) is empty, the [approxDurationMs] value cannot be accessed, + * So it falls back to the value of [videoDetails.lengthSeconds] (Integer type, approximate value) multiplied by 1000. + *

+ * For iOS clients, [streamingData.formats] (List type) is always empty, so it always falls back to the approximate value. + *

+ * Called after {@link #getStreamingData(String)}. + */ + public static long getApproxDurationMs(String videoId) { + if (SPOOF_STREAMING_DATA && videoId != null) { + final Long approxDurationMs = approxDurationMsMap.get(videoId); + if (approxDurationMs != null) { + Logger.printDebug(() -> "Replacing video length: " + approxDurationMs + " for videoId: " + videoId); + approxDurationMsMap.remove(videoId); + return approxDurationMs; + } + } + return Long.MAX_VALUE; + } + + /** + * Injection point. + * Called after {@link #getStreamingData(String)}. + */ + @Nullable + public static byte[] removeVideoPlaybackPostBody(Uri uri, int method, byte[] postData) { + if (SPOOF_STREAMING_DATA) { + try { + final int methodPost = 2; + if (method == methodPost) { + String path = uri.getPath(); + if (path != null && path.contains("videoplayback")) { + return null; + } + } + } catch (Exception ex) { + Logger.printException(() -> "removeVideoPlaybackPostBody failure", ex); + } + } + + return postData; + } + + /** + * Injection point. + */ + public static String appendSpoofedClient(String videoFormat) { + try { + if (SPOOF_STREAMING_DATA && BaseSettings.SPOOF_STREAMING_DATA_STATS_FOR_NERDS.get() + && !TextUtils.isEmpty(videoFormat)) { + // Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages + return "\u202D" + videoFormat + String.format("\u2009(%s)", StreamingDataRequest.getLastSpoofedClientName()); // u202D = left to right override + } + } catch (Exception ex) { + Logger.printException(() -> "appendSpoofedClient failure", ex); + } + + return videoFormat; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.java new file mode 100644 index 000000000..4eb16d20c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/PlayerRoutes.java @@ -0,0 +1,95 @@ +package app.revanced.extension.shared.patches.spoof.requests; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import app.revanced.extension.shared.patches.client.AppClient.ClientType; +import app.revanced.extension.shared.requests.Requester; +import app.revanced.extension.shared.requests.Route; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings({"ExtractMethodRecommender", "deprecation"}) +public final class PlayerRoutes { + public static final Route.CompiledRoute GET_PLAYLIST_PAGE = new Route( + Route.Method.POST, + "next" + + "?fields=contents.singleColumnWatchNextResults.playlist.playlist" + ).compile(); + static final Route.CompiledRoute GET_STREAMING_DATA = new Route( + Route.Method.POST, + "player" + + "?fields=streamingData" + + "&alt=proto" + ).compile(); + private static final String YT_API_URL = "https://youtubei.googleapis.com/youtubei/v1/"; + /** + * TCP connection and HTTP read timeout + */ + private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds. + + private static final String LOCALE_LANGUAGE = Utils.getContext().getResources() + .getConfiguration().locale.getLanguage(); + + private PlayerRoutes() { + } + + public static String createInnertubeBody(ClientType clientType) { + return createInnertubeBody(clientType, false); + } + + public static String createInnertubeBody(ClientType clientType, boolean playlistId) { + JSONObject innerTubeBody = new JSONObject(); + + try { + JSONObject client = new JSONObject(); + client.put("clientName", clientType.clientName); + client.put("clientVersion", clientType.clientVersion); + client.put("deviceModel", clientType.deviceModel); + client.put("osVersion", clientType.osVersion); + if (clientType.androidSdkVersion != null) { + client.put("androidSdkVersion", clientType.androidSdkVersion); + client.put("osName", "Android"); + } else { + client.put("deviceMake", "Apple"); + client.put("osName", "iOS"); + } + client.put("hl", LOCALE_LANGUAGE); + + JSONObject context = new JSONObject(); + context.put("client", client); + + innerTubeBody.put("context", context); + innerTubeBody.put("contentCheckOk", true); + innerTubeBody.put("racyCheckOk", true); + innerTubeBody.put("videoId", "%s"); + if (playlistId) { + innerTubeBody.put("playlistId", "%s"); + } + } catch (JSONException e) { + Logger.printException(() -> "Failed to create innerTubeBody", e); + } + + return innerTubeBody.toString(); + } + + /** + * @noinspection SameParameterValue + */ + public static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException { + var connection = Requester.getConnectionFromCompiledRoute(YT_API_URL, route); + + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("User-Agent", clientType.userAgent); + + connection.setUseCaches(false); + connection.setDoOutput(true); + + connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS); + connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS); + return connection; + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java new file mode 100644 index 000000000..a76c8a2df --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spoof/requests/StreamingDataRequest.java @@ -0,0 +1,241 @@ +package app.revanced.extension.shared.patches.spoof.requests; + +import static app.revanced.extension.shared.patches.client.AppClient.getAvailableClientTypes; +import static app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import app.revanced.extension.shared.patches.client.AppClient.ClientType; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +/** + * Video streaming data. Fetching is tied to the behavior YT uses, + * where this class fetches the streams only when YT fetches. + *

+ * Effectively the cache expiration of these fetches is the same as the stock app, + * since the stock app would not use expired streams and therefor + * the extension replace stream hook is called only if YT + * did use its own client streams. + */ +public class StreamingDataRequest { + + private static final ClientType[] CLIENT_ORDER_TO_USE; + private static final String AUTHORIZATION_HEADER = "Authorization"; + private static final String[] REQUEST_HEADER_KEYS = { + AUTHORIZATION_HEADER, // Available only to logged-in users. + "X-GOOG-API-FORMAT-VERSION", + "X-Goog-Visitor-Id" + }; + private static ClientType lastSpoofedClientType; + + + /** + * TCP connection and HTTP read timeout. + */ + private static final int HTTP_TIMEOUT_MILLISECONDS = 10 * 1000; + /** + * Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS} + */ + private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; + private static final Map cache = Collections.synchronizedMap( + new LinkedHashMap<>(100) { + /** + * Cache limit must be greater than the maximum number of videos open at once, + * which theoretically is more than 4 (3 Shorts + one regular minimized video). + * But instead use a much larger value, to handle if a video viewed a while ago + * is somehow still referenced. Each stream is a small array of Strings + * so memory usage is not a concern. + */ + private static final int CACHE_LIMIT = 50; + + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit. + } + }); + + public static String getLastSpoofedClientName() { + return lastSpoofedClientType == null + ? "Unknown" + : lastSpoofedClientType.getFriendlyName(); + } + + static { + ClientType[] allClientTypes = getAvailableClientTypes(); + ClientType preferredClient = BaseSettings.SPOOF_STREAMING_DATA_TYPE.get(); + + if (Arrays.stream(allClientTypes).noneMatch(preferredClient::equals)) { + CLIENT_ORDER_TO_USE = allClientTypes; + } else { + CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length]; + CLIENT_ORDER_TO_USE[0] = preferredClient; + + int i = 1; + for (ClientType c : allClientTypes) { + if (c != preferredClient) { + CLIENT_ORDER_TO_USE[i++] = c; + } + } + } + } + + private final String videoId; + private final Future future; + + private StreamingDataRequest(String videoId, Map playerHeaders) { + Objects.requireNonNull(playerHeaders); + this.videoId = videoId; + this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId, playerHeaders)); + } + + public static void fetchRequest(String videoId, Map fetchHeaders) { + // Always fetch, even if there is an existing request for the same video. + cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); + } + + @Nullable + public static StreamingDataRequest getRequestForVideoId(String videoId) { + return cache.get(videoId); + } + + private static void handleConnectionError(String toastMessage, @Nullable Exception ex) { + Logger.printInfo(() -> toastMessage, ex); + } + + @Nullable + private static HttpURLConnection send(ClientType clientType, String videoId, + Map playerHeaders) { + Objects.requireNonNull(clientType); + Objects.requireNonNull(videoId); + Objects.requireNonNull(playerHeaders); + + final long startTime = System.currentTimeMillis(); + Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType); + + try { + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); + connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS); + connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS); + + for (String key : REQUEST_HEADER_KEYS) { + String value = playerHeaders.get(key); + if (value != null) { + if (key.equals(AUTHORIZATION_HEADER)) { + if (!clientType.canLogin) { + Logger.printDebug(() -> "Not including request header: " + key); + continue; + } + } + + connection.setRequestProperty(key, value); + } + } + + String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); + byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(requestBody.length); + connection.getOutputStream().write(requestBody); + + final int responseCode = connection.getResponseCode(); + if (responseCode == 200) return connection; + + // This situation likely means the patches are outdated. + // Use a toast message that suggests updating. + handleConnectionError("Playback error (App is outdated?) " + clientType + ": " + + responseCode + " response: " + connection.getResponseMessage(), + null); + } catch (SocketTimeoutException ex) { + handleConnectionError("Connection timeout", ex); + } catch (IOException ex) { + handleConnectionError("Network error", ex); + } catch (Exception ex) { + Logger.printException(() -> "send failed", ex); + } finally { + Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms"); + } + + return null; + } + + private static ByteBuffer fetch(String videoId, Map playerHeaders) { + lastSpoofedClientType = null; + + // Retry with different client if empty response body is received. + for (ClientType clientType : CLIENT_ORDER_TO_USE) { + HttpURLConnection connection = send(clientType, videoId, playerHeaders); + if (connection != null) { + try { + // gzip encoding doesn't response with content length (-1), + // but empty response body does. + if (connection.getContentLength() == 0) { + Logger.printDebug(() -> "Received empty response" + "\nClient: " + clientType + "\nVideo: " + videoId); + } else { + try (InputStream inputStream = new BufferedInputStream(connection.getInputStream()); + ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + byte[] buffer = new byte[2048]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) >= 0) { + baos.write(buffer, 0, bytesRead); + } + lastSpoofedClientType = clientType; + + return ByteBuffer.wrap(baos.toByteArray()); + } + } + } catch (IOException ex) { + Logger.printException(() -> "Fetch failed while processing response data", ex); + } + } + } + + handleConnectionError("Could not fetch any client streams", null); + return null; + } + + public boolean fetchCompleted() { + return future.isDone(); + } + + @Nullable + public ByteBuffer getStream() { + try { + return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS); + } catch (TimeoutException ex) { + Logger.printInfo(() -> "getStream timed out", ex); + } catch (InterruptedException ex) { + Logger.printException(() -> "getStream interrupted", ex); + Thread.currentThread().interrupt(); // Restore interrupt status flag. + } catch (ExecutionException ex) { + Logger.printException(() -> "getStream failure", ex); + } + + return null; + } + + @NonNull + @Override + public String toString() { + return "StreamingDataRequest{" + "videoId='" + videoId + '\'' + '}'; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Requester.java b/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Requester.java new file mode 100644 index 000000000..831b8bf63 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Requester.java @@ -0,0 +1,146 @@ +package app.revanced.extension.shared.requests; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +import app.revanced.extension.shared.utils.PackageUtils; + +@SuppressWarnings("unused") +public class Requester { + private Requester() { + } + + public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException { + return getConnectionFromCompiledRoute(apiUrl, route.compile(params)); + } + + public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException { + String url = apiUrl + route.getCompiledRoute(); + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + // Request data is in the URL parameters and no body is sent. + // The calling code must set a length if using a request body. + connection.setFixedLengthStreamingMode(0); + connection.setRequestMethod(route.getMethod().name()); + String agentString = System.getProperty("http.agent") + + "; RVX/" + PackageUtils.getAppVersionName(); + connection.setRequestProperty("User-Agent", agentString); + + return connection; + } + + /** + * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. + */ + private static String parseInputStreamAndClose(InputStream inputStream) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + StringBuilder jsonBuilder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + jsonBuilder.append(line); + jsonBuilder.append('\n'); + } + return jsonBuilder.toString(); + } + } + + /** + * Parse the {@link HttpURLConnection} response as a String. + * This does not close the url connection. If further requests to this host are unlikely + * in the near future, then instead use {@link #parseStringAndDisconnect(HttpURLConnection)}. + */ + public static String parseString(HttpURLConnection connection) throws IOException { + return parseInputStreamAndClose(connection.getInputStream()); + } + + /** + * Parse the {@link HttpURLConnection} response as a String, and disconnect. + *

+ * Should only be used if other requests to the server in the near future are unlikely + * + * @see #parseString(HttpURLConnection) + */ + public static String parseStringAndDisconnect(HttpURLConnection connection) throws IOException { + String result = parseString(connection); + connection.disconnect(); + return result; + } + + /** + * Parse the {@link HttpURLConnection} error stream as a String. + * If the server sent no error response data, this returns an empty string. + */ + public static String parseErrorString(HttpURLConnection connection) throws IOException { + InputStream errorStream = connection.getErrorStream(); + if (errorStream == null) { + return ""; + } + return parseInputStreamAndClose(errorStream); + } + + /** + * Parse the {@link HttpURLConnection} error stream as a String, and disconnect. + * If the server sent no error response data, this returns an empty string. + *

+ * Should only be used if other requests to the server are unlikely in the near future. + * + * @see #parseErrorString(HttpURLConnection) + */ + public static String parseErrorStringAndDisconnect(HttpURLConnection connection) throws IOException { + String result = parseErrorString(connection); + connection.disconnect(); + return result; + } + + /** + * Parse the {@link HttpURLConnection} response into a JSONObject. + * This does not close the url connection. If further requests to this host are unlikely + * in the near future, then instead use {@link #parseJSONObjectAndDisconnect(HttpURLConnection)}. + */ + public static JSONObject parseJSONObject(HttpURLConnection connection) throws JSONException, IOException { + return new JSONObject(parseString(connection)); + } + + /** + * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. + *

+ * Should only be used if other requests to the server in the near future are unlikely + * + * @see #parseJSONObject(HttpURLConnection) + */ + public static JSONObject parseJSONObjectAndDisconnect(HttpURLConnection connection) throws JSONException, IOException { + JSONObject object = parseJSONObject(connection); + connection.disconnect(); + return object; + } + + /** + * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. + * This does not close the url connection. If further requests to this host are unlikely + * in the near future, then instead use {@link #parseJSONArrayAndDisconnect(HttpURLConnection)}. + */ + public static JSONArray parseJSONArray(HttpURLConnection connection) throws JSONException, IOException { + return new JSONArray(parseString(connection)); + } + + /** + * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. + *

+ * Should only be used if other requests to the server in the near future are unlikely + * + * @see #parseJSONArray(HttpURLConnection) + */ + public static JSONArray parseJSONArrayAndDisconnect(HttpURLConnection connection) throws JSONException, IOException { + JSONArray array = parseJSONArray(connection); + connection.disconnect(); + return array; + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Route.java b/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Route.java new file mode 100644 index 000000000..9e6f2c5a7 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Route.java @@ -0,0 +1,66 @@ +package app.revanced.extension.shared.requests; + +public class Route { + private final String route; + private final Method method; + private final int paramCount; + + public Route(Method method, String route) { + this.method = method; + this.route = route; + this.paramCount = countMatches(route, '{'); + + if (paramCount != countMatches(route, '}')) + throw new IllegalArgumentException("Not enough parameters"); + } + + public Method getMethod() { + return method; + } + + public CompiledRoute compile(String... params) { + if (params.length != paramCount) + throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " + + "Expected: " + paramCount + ", provided: " + params.length); + + StringBuilder compiledRoute = new StringBuilder(route); + for (int i = 0; i < paramCount; i++) { + int paramStart = compiledRoute.indexOf("{"); + int paramEnd = compiledRoute.indexOf("}"); + compiledRoute.replace(paramStart, paramEnd + 1, params[i]); + } + return new CompiledRoute(this, compiledRoute.toString()); + } + + public static class CompiledRoute { + private final Route baseRoute; + private final String compiledRoute; + + private CompiledRoute(Route baseRoute, String compiledRoute) { + this.baseRoute = baseRoute; + this.compiledRoute = compiledRoute; + } + + public String getCompiledRoute() { + return compiledRoute; + } + + public Method getMethod() { + return baseRoute.method; + } + } + + private int countMatches(CharSequence seq, char c) { + int count = 0; + for (int i = 0; i < seq.length(); i++) { + if (seq.charAt(i) == c) + count++; + } + return count; + } + + public enum Method { + GET, + POST + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/ReturnYouTubeDislike.java new file mode 100644 index 000000000..c7031d02f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/ReturnYouTubeDislike.java @@ -0,0 +1,17 @@ +package app.revanced.extension.shared.returnyoutubedislike; + +public class ReturnYouTubeDislike { + + public enum Vote { + LIKE(1), + DISLIKE(-1), + LIKE_REMOVE(0); + + public final int value; + + Vote(int value) { + this.value = value; + } + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/RYDVoteData.java b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/RYDVoteData.java new file mode 100644 index 000000000..a4a56de04 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/RYDVoteData.java @@ -0,0 +1,179 @@ +package app.revanced.extension.shared.returnyoutubedislike.requests; + +import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import app.revanced.extension.shared.utils.Logger; + +/** + * ReturnYouTubeDislike API estimated like/dislike/view counts. + *

+ * ReturnYouTubeDislike does not guarantee when the counts are updated. + * So these values may lag behind what YouTube shows. + */ +@SuppressWarnings("unused") +public final class RYDVoteData { + @NonNull + public final String videoId; + + /** + * Estimated number of views + */ + public final long viewCount; + + private final long fetchedLikeCount; + private volatile long likeCount; // Read/write from different threads. + /** + * Like count can be hidden by video creator, but RYD still tracks the number + * of like/dislikes it received thru it's browser extension and and API. + * The raw like/dislikes can be used to calculate a percentage. + *

+ * Raw values can be null, especially for older videos with little to no views. + */ + @Nullable + private final Long fetchedRawLikeCount; + private volatile float likePercentage; + + private final long fetchedDislikeCount; + private volatile long dislikeCount; // Read/write from different threads. + @Nullable + private final Long fetchedRawDislikeCount; + private volatile float dislikePercentage; + + @Nullable + private static Long getLongIfExist(JSONObject json, String key) throws JSONException { + return json.isNull(key) + ? null + : json.getLong(key); + } + + /** + * @throws JSONException if JSON parse error occurs, or if the values make no sense (ie: negative values) + */ + public RYDVoteData(@NonNull JSONObject json) throws JSONException { + videoId = json.getString("id"); + viewCount = json.getLong("viewCount"); + + fetchedLikeCount = json.getLong("likes"); + fetchedRawLikeCount = getLongIfExist(json, "rawLikes"); + + fetchedDislikeCount = json.getLong("dislikes"); + fetchedRawDislikeCount = getLongIfExist(json, "rawDislikes"); + + if (viewCount < 0 || fetchedLikeCount < 0 || fetchedDislikeCount < 0) { + throw new JSONException("Unexpected JSON values: " + json); + } + likeCount = fetchedLikeCount; + dislikeCount = fetchedDislikeCount; + updateUsingVote(Vote.LIKE_REMOVE); // Calculate percentages. + } + + /** + * Public like count of the video, as reported by YT when RYD last updated it's data. + *

+ * If the likes were hidden by the video creator, then this returns an + * estimated likes using the same extrapolation as the dislikes. + */ + public long getLikeCount() { + return likeCount; + } + + /** + * Estimated total dislike count, extrapolated from the public like count using RYD data. + */ + public long getDislikeCount() { + return dislikeCount; + } + + /** + * Estimated percentage of likes for all votes. Value has range of [0, 1] + *

+ * A video with 400 positive votes, and 100 negative votes, has a likePercentage of 0.8 + */ + public float getLikePercentage() { + return likePercentage; + } + + /** + * Estimated percentage of dislikes for all votes. Value has range of [0, 1] + *

+ * A video with 400 positive votes, and 100 negative votes, has a dislikePercentage of 0.2 + */ + public float getDislikePercentage() { + return dislikePercentage; + } + + public void updateUsingVote(Vote vote) { + final int likesToAdd, dislikesToAdd; + + switch (vote) { + case LIKE: + likesToAdd = 1; + dislikesToAdd = 0; + break; + case DISLIKE: + likesToAdd = 0; + dislikesToAdd = 1; + break; + case LIKE_REMOVE: + likesToAdd = 0; + dislikesToAdd = 0; + break; + default: + throw new IllegalStateException(); + } + + // If a video has no public likes but RYD has raw like data, + // then use the raw data instead. + final boolean videoHasNoPublicLikes = fetchedLikeCount == 0; + final boolean hasRawData = fetchedRawLikeCount != null && fetchedRawDislikeCount != null; + + if (videoHasNoPublicLikes && hasRawData && fetchedRawDislikeCount > 0) { + // YT creator has hidden the likes count, and this is an older video that + // RYD does not provide estimated like counts. + // + // But we can calculate the public likes the same way RYD does for newer videos with hidden likes, + // by using the same raw to estimated scale factor applied to dislikes. + // This calculation exactly matches the public likes RYD provides for newer hidden videos. + final float estimatedRawScaleFactor = (float) fetchedDislikeCount / fetchedRawDislikeCount; + likeCount = (long) (estimatedRawScaleFactor * fetchedRawLikeCount) + likesToAdd; + Logger.printDebug(() -> "Using locally calculated estimated likes since RYD did not return an estimate"); + } else { + likeCount = fetchedLikeCount + likesToAdd; + } + // RYD now always returns an estimated dislike count, even if the likes are hidden. + dislikeCount = fetchedDislikeCount + dislikesToAdd; + + // Update percentages. + + final float totalCount = likeCount + dislikeCount; + if (totalCount == 0) { + likePercentage = 0; + dislikePercentage = 0; + } else { + likePercentage = likeCount / totalCount; + dislikePercentage = dislikeCount / totalCount; + } + } + + @NonNull + @Override + public String toString() { + return "RYDVoteData{" + + "videoId=" + videoId + + ", viewCount=" + viewCount + + ", likeCount=" + likeCount + + ", dislikeCount=" + dislikeCount + + ", likePercentage=" + likePercentage + + ", dislikePercentage=" + dislikePercentage + + '}'; + } + + // equals and hashcode is not implemented (currently not needed) + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java new file mode 100644 index 000000000..df1e503b5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java @@ -0,0 +1,476 @@ +package app.revanced.extension.shared.returnyoutubedislike.requests; + +import static app.revanced.extension.shared.returnyoutubedislike.requests.ReturnYouTubeDislikeRoutes.getRYDConnectionFromRoute; +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.util.Base64; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.ProtocolException; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Objects; + +import app.revanced.extension.shared.requests.Requester; +import app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +public class ReturnYouTubeDislikeApi { + /** + * {@link #fetchVotes(String)} TCP connection timeout + */ + private static final int API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS = 2 * 1000; // 2 Seconds. + + /** + * {@link #fetchVotes(String)} HTTP read timeout. + * To locally debug and force timeouts, change this to a very small number (ie: 100) + */ + private static final int API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS = 4 * 1000; // 4 Seconds. + + /** + * Default connection and response timeout for voting and registration. + *

+ * Voting and user registration runs in the background and has has no urgency + * so this can be a larger value. + */ + private static final int API_REGISTER_VOTE_TIMEOUT_MILLISECONDS = 60 * 1000; // 60 Seconds. + + /** + * Response code of a successful API call + */ + private static final int HTTP_STATUS_CODE_SUCCESS = 200; + + /** + * Indicates a client rate limit has been reached and the client must back off. + */ + private static final int HTTP_STATUS_CODE_RATE_LIMIT = 429; + + /** + * How long to wait until API calls are resumed, if the API requested a back off. + * No clear guideline of how long to wait until resuming. + */ + private static final int BACKOFF_RATE_LIMIT_MILLISECONDS = 10 * 60 * 1000; // 10 Minutes. + + /** + * How long to wait until API calls are resumed, if any connection error occurs. + */ + private static final int BACKOFF_CONNECTION_ERROR_MILLISECONDS = 2 * 60 * 1000; // 2 Minutes. + + /** + * If non zero, then the system time of when API calls can resume. + */ + private static volatile long timeToResumeAPICalls; // must be volatile, since different threads read/write to this + + /** + * If the last API getVotes call failed for any reason (including server requested rate limit). + * Used to prevent showing repeat connection toasts when the API is down. + */ + private static volatile boolean lastApiCallFailed; + + public static boolean toastOnConnectionError = false; + + private ReturnYouTubeDislikeApi() { + } // utility class + + /** + * Clears any backoff rate limits in effect. + * Should be called if RYD is turned on/off. + */ + public static void resetRateLimits() { + if (lastApiCallFailed || timeToResumeAPICalls != 0) { + Logger.printDebug(() -> "Reset rate limit"); + } + lastApiCallFailed = false; + timeToResumeAPICalls = 0; + } + + /** + * @return True, if api rate limit is in effect. + */ + private static boolean checkIfRateLimitInEffect(String apiEndPointName) { + if (timeToResumeAPICalls == 0) { + return false; + } + final long now = System.currentTimeMillis(); + if (now > timeToResumeAPICalls) { + timeToResumeAPICalls = 0; + return false; + } + Logger.printDebug(() -> "Ignoring api call " + apiEndPointName + " as rate limit is in effect"); + return true; + } + + /** + * @return True, if a client rate limit was requested + */ + private static boolean checkIfRateLimitWasHit(int httpResponseCode) { + return httpResponseCode == HTTP_STATUS_CODE_RATE_LIMIT; + } + + private static void updateRateLimitAndStats(boolean connectionError, boolean rateLimitHit) { + if (connectionError && rateLimitHit) { + throw new IllegalArgumentException(); + } + if (connectionError) { + timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_CONNECTION_ERROR_MILLISECONDS; + lastApiCallFailed = true; + } else if (rateLimitHit) { + Logger.printDebug(() -> "API rate limit was hit. Stopping API calls for the next " + + BACKOFF_RATE_LIMIT_MILLISECONDS + " seconds"); + timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_RATE_LIMIT_MILLISECONDS; + if (!lastApiCallFailed && toastOnConnectionError) { + Utils.showToastLong(str("revanced_ryd_failure_client_rate_limit_requested")); + } + lastApiCallFailed = true; + } else { + lastApiCallFailed = false; + } + } + + private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) { + if (!lastApiCallFailed && toastOnConnectionError) { + Utils.showToastShort(toastMessage); + } + if (ex != null) { + Logger.printInfo(() -> toastMessage, ex); + } + } + + /** + * @return NULL if fetch failed, or if a rate limit is in effect. + */ + @Nullable + public static RYDVoteData fetchVotes(String videoId) { + Utils.verifyOffMainThread(); + Objects.requireNonNull(videoId); + + if (checkIfRateLimitInEffect("fetchVotes")) { + return null; + } + Logger.printDebug(() -> "Fetching votes for: " + videoId); + + try { + HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_DISLIKES, videoId); + // request headers, as per https://returnyoutubedislike.com/docs/fetching + // the documentation says to use 'Accept:text/html', but the RYD browser plugin uses 'Accept:application/json' + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("Connection", "keep-alive"); // keep-alive is on by default with http 1.1, but specify anyways + connection.setRequestProperty("Pragma", "no-cache"); + connection.setRequestProperty("Cache-Control", "no-cache"); + connection.setUseCaches(false); + connection.setConnectTimeout(API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS); // timeout for TCP connection to server + connection.setReadTimeout(API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS); // timeout for server response + + final int responseCode = connection.getResponseCode(); + if (checkIfRateLimitWasHit(responseCode)) { + connection.disconnect(); // rate limit hit, should disconnect + updateRateLimitAndStats(false, true); + return null; + } + + if (responseCode == HTTP_STATUS_CODE_SUCCESS) { + // Do not disconnect, the same server connection will likely be used again soon. + JSONObject json = Requester.parseJSONObject(connection); + try { + RYDVoteData votingData = new RYDVoteData(json); + updateRateLimitAndStats(false, false); + Logger.printDebug(() -> "Voting data fetched: " + votingData); + return votingData; + } catch (JSONException ex) { + Logger.printException(() -> "Failed to parse video: " + videoId + " json: " + json, ex); + // fall thru to update statistics + } + } else { + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + } + connection.disconnect(); // something went wrong, might as well disconnect + } catch ( + SocketTimeoutException ex) { // connection timed out, response timeout, or some other network error + handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex); + } catch (IOException ex) { + handleConnectionError((str("revanced_ryd_failure_generic", ex.getMessage())), ex); + } catch (Exception ex) { + // should never happen + Logger.printException(() -> "fetchVotes failure", ex); + } + + updateRateLimitAndStats(true, false); + return null; + } + + /** + * @return The newly created and registered user id. Returns NULL if registration failed. + */ + @Nullable + public static String registerAsNewUser() { + Utils.verifyOffMainThread(); + try { + if (checkIfRateLimitInEffect("registerAsNewUser")) { + return null; + } + String userId = randomString(); + Logger.printDebug(() -> "Trying to register new user"); + + HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.GET_REGISTRATION, userId); + connection.setRequestProperty("Accept", "application/json"); + connection.setConnectTimeout(API_REGISTER_VOTE_TIMEOUT_MILLISECONDS); + connection.setReadTimeout(API_REGISTER_VOTE_TIMEOUT_MILLISECONDS); + + final int responseCode = connection.getResponseCode(); + if (checkIfRateLimitWasHit(responseCode)) { + connection.disconnect(); // disconnect, as no more connections will be made for a little while + return null; + } + if (responseCode == HTTP_STATUS_CODE_SUCCESS) { + JSONObject json = Requester.parseJSONObject(connection); + String challenge = json.getString("challenge"); + int difficulty = json.getInt("difficulty"); + + String solution = solvePuzzle(challenge, difficulty); + return confirmRegistration(userId, solution); + } + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + connection.disconnect(); + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + } catch (IOException ex) { + handleConnectionError(str("revanced_ryd_failure_generic", "registration failed"), ex); + } catch (Exception ex) { + Logger.printException(() -> "Failed to register user", ex); // should never happen + } + return null; + } + + @Nullable + private static String confirmRegistration(String userId, String solution) { + Utils.verifyOffMainThread(); + Objects.requireNonNull(userId); + Objects.requireNonNull(solution); + try { + if (checkIfRateLimitInEffect("confirmRegistration")) { + return null; + } + Logger.printDebug(() -> "Trying to confirm registration with solution: " + solution); + + HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_REGISTRATION, userId); + applyCommonPostRequestSettings(connection); + + String jsonInputString = "{\"solution\": \"" + solution + "\"}"; + byte[] body = jsonInputString.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(body.length); + try (OutputStream os = connection.getOutputStream()) { + os.write(body); + } + + final int responseCode = connection.getResponseCode(); + if (checkIfRateLimitWasHit(responseCode)) { + connection.disconnect(); // disconnect, as no more connections will be made for a little while + return null; + } + if (responseCode == HTTP_STATUS_CODE_SUCCESS) { + Logger.printDebug(() -> "Registration confirmation successful"); + return userId; + } + // Something went wrong, might as well disconnect. + String response = Requester.parseStringAndDisconnect(connection); + Logger.printInfo(() -> "Failed to confirm registration for user: " + userId + + " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "''"); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + } catch (IOException ex) { + handleConnectionError(str("revanced_ryd_failure_generic", "confirm registration failed"), ex); + } catch (Exception ex) { + Logger.printException(() -> "Failed to confirm registration for user: " + userId + + "solution: " + solution, ex); + } + return null; + } + + public static void sendVote(String userId, String videoId, ReturnYouTubeDislike.Vote vote) { + Utils.verifyOffMainThread(); + Objects.requireNonNull(videoId); + Objects.requireNonNull(vote); + + try { + if (userId == null) return; + + if (checkIfRateLimitInEffect("sendVote")) { + return; + } + Logger.printDebug(() -> "Trying to vote for video: " + videoId + " with vote: " + vote); + + HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.SEND_VOTE); + applyCommonPostRequestSettings(connection); + + String voteJsonString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"value\": \"" + vote.value + "\"}"; + byte[] body = voteJsonString.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(body.length); + try (OutputStream os = connection.getOutputStream()) { + os.write(body); + } + + final int responseCode = connection.getResponseCode(); + if (checkIfRateLimitWasHit(responseCode)) { + connection.disconnect(); // disconnect, as no more connections will be made for a little while + return; + } + if (responseCode == HTTP_STATUS_CODE_SUCCESS) { + JSONObject json = Requester.parseJSONObject(connection); + String challenge = json.getString("challenge"); + int difficulty = json.getInt("difficulty"); + + String solution = solvePuzzle(challenge, difficulty); + confirmVote(videoId, userId, solution); + return; + } + + Logger.printInfo(() -> "Failed to send vote for video: " + videoId + " vote: " + vote + + " response code was: " + responseCode); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + connection.disconnect(); // something went wrong, might as well disconnect + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + } catch (IOException ex) { + handleConnectionError(str("revanced_ryd_failure_generic", "send vote failed"), ex); + } catch (Exception ex) { + // should never happen + Logger.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex); + } + } + + private static void confirmVote(String videoId, String userId, String solution) { + Utils.verifyOffMainThread(); + Objects.requireNonNull(videoId); + Objects.requireNonNull(userId); + Objects.requireNonNull(solution); + + try { + if (checkIfRateLimitInEffect("confirmVote")) { + return; + } + Logger.printDebug(() -> "Trying to confirm vote for video: " + videoId + " solution: " + solution); + HttpURLConnection connection = getRYDConnectionFromRoute(ReturnYouTubeDislikeRoutes.CONFIRM_VOTE); + applyCommonPostRequestSettings(connection); + + String jsonInputString = "{\"userId\": \"" + userId + "\", \"videoId\": \"" + videoId + "\", \"solution\": \"" + solution + "\"}"; + byte[] body = jsonInputString.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(body.length); + try (OutputStream os = connection.getOutputStream()) { + os.write(body); + } + + final int responseCode = connection.getResponseCode(); + if (checkIfRateLimitWasHit(responseCode)) { + connection.disconnect(); // disconnect, as no more connections will be made for a little while + return; + } + if (responseCode == HTTP_STATUS_CODE_SUCCESS) { + Logger.printDebug(() -> "Vote confirm successful for video: " + videoId); + return; + } + // Something went wrong, might as well disconnect. + String response = Requester.parseStringAndDisconnect(connection); + Logger.printInfo(() -> "Failed to confirm vote for video: " + videoId + + " solution: " + solution + " responseCode: " + responseCode + " response: '" + response + "'"); + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), null); + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex); + } catch (IOException ex) { + handleConnectionError(str("revanced_ryd_failure_generic", "confirm vote failed"), ex); + } catch (Exception ex) { + Logger.printException(() -> "Failed to confirm vote for video: " + videoId + + " solution: " + solution, ex); // should never happen + } + } + + private static void applyCommonPostRequestSettings(HttpURLConnection connection) throws ProtocolException { + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Accept", "application/json"); + connection.setRequestProperty("Pragma", "no-cache"); + connection.setRequestProperty("Cache-Control", "no-cache"); + connection.setUseCaches(false); + connection.setDoOutput(true); + connection.setConnectTimeout(API_REGISTER_VOTE_TIMEOUT_MILLISECONDS); // timeout for TCP connection to server + connection.setReadTimeout(API_REGISTER_VOTE_TIMEOUT_MILLISECONDS); // timeout for server response + } + + + private static String solvePuzzle(String challenge, int difficulty) { + byte[] decodedChallenge = Base64.decode(challenge, Base64.NO_WRAP); + + byte[] buffer = new byte[20]; + System.arraycopy(decodedChallenge, 0, buffer, 4, 16); + + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-512"); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException(ex); // should never happen + } + + final int maxCount = (int) (Math.pow(2, difficulty + 1) * 5); + for (int i = 0; i < maxCount; i++) { + buffer[0] = (byte) i; + buffer[1] = (byte) (i >> 8); + buffer[2] = (byte) (i >> 16); + buffer[3] = (byte) (i >> 24); + byte[] messageDigest = md.digest(buffer); + + if (countLeadingZeroes(messageDigest) >= difficulty) { + return Base64.encodeToString(new byte[]{buffer[0], buffer[1], buffer[2], buffer[3]}, Base64.NO_WRAP); + } + } + + // should never be reached + throw new IllegalStateException("Failed to solve puzzle challenge: " + challenge + " of difficulty: " + difficulty); + } + + // https://stackoverflow.com/a/157202 + private static String randomString() { + String AB = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + SecureRandom rnd = new SecureRandom(); + + StringBuilder sb = new StringBuilder(36); + for (int i = 0; i < 36; i++) + sb.append(AB.charAt(rnd.nextInt(AB.length()))); + return sb.toString(); + } + + private static int countLeadingZeroes(byte[] uInt8View) { + int zeroes = 0; + int value; + for (byte b : uInt8View) { + value = b & 0xFF; + if (value == 0) { + zeroes += 8; + } else { + int count = 1; + if (value >>> 4 == 0) { + count += 4; + value <<= 4; + } + if (value >>> 6 == 0) { + count += 2; + value <<= 2; + } + zeroes += count - (value >>> 7); + break; + } + } + return zeroes; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java new file mode 100644 index 000000000..98c9fe676 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java @@ -0,0 +1,28 @@ +package app.revanced.extension.shared.returnyoutubedislike.requests; + +import static app.revanced.extension.shared.requests.Route.Method.GET; +import static app.revanced.extension.shared.requests.Route.Method.POST; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import app.revanced.extension.shared.requests.Requester; +import app.revanced.extension.shared.requests.Route; + +public class ReturnYouTubeDislikeRoutes { + public static final String RYD_API_URL = "https://returnyoutubedislikeapi.com/"; + + public static final Route SEND_VOTE = new Route(POST, "interact/vote"); + public static final Route CONFIRM_VOTE = new Route(POST, "interact/confirmVote"); + public static final Route GET_DISLIKES = new Route(GET, "votes?videoId={video_id}"); + public static final Route GET_REGISTRATION = new Route(GET, "puzzle/registration?userId={user_id}"); + public static final Route CONFIRM_REGISTRATION = new Route(POST, "puzzle/registration?userId={user_id}"); + + public ReturnYouTubeDislikeRoutes() { + } + + public static HttpURLConnection getRYDConnectionFromRoute(Route route, String... params) throws IOException { + return Requester.getConnectionFromRoute(RYD_API_URL, route, params); + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubeusername/requests/ChannelRequest.java b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubeusername/requests/ChannelRequest.java new file mode 100644 index 000000000..84e02755b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubeusername/requests/ChannelRequest.java @@ -0,0 +1,167 @@ +package app.revanced.extension.shared.returnyoutubeusername.requests; + +import static java.lang.Boolean.TRUE; +import static app.revanced.extension.shared.returnyoutubeusername.requests.ChannelRoutes.GET_CHANNEL_DETAILS; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import app.revanced.extension.shared.requests.Requester; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +public class ChannelRequest { + /** + * TCP connection and HTTP read timeout. + */ + private static final int HTTP_TIMEOUT_MILLISECONDS = 3 * 1000; + + /** + * Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS} + */ + private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 6 * 1000; + + @GuardedBy("itself") + private static final Map cache = Collections.synchronizedMap( + new LinkedHashMap<>(200) { + private static final int CACHE_LIMIT = 100; + + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit. + } + }); + + public static void fetchRequestIfNeeded(@NonNull String handle, @NonNull String apiKey, Boolean userNameFirst) { + if (!cache.containsKey(handle)) { + cache.put(handle, new ChannelRequest(handle, apiKey, userNameFirst)); + } + } + + @Nullable + public static ChannelRequest getRequestForHandle(@NonNull String handle) { + return cache.get(handle); + } + + private static void handleConnectionError(String toastMessage, @Nullable Exception ex) { + Logger.printInfo(() -> toastMessage, ex); + } + + @Nullable + private static JSONObject send(String handle, String apiKey) { + Objects.requireNonNull(handle); + Objects.requireNonNull(apiKey); + + final long startTime = System.currentTimeMillis(); + Logger.printDebug(() -> "Fetching channel handle for: " + handle); + + try { + HttpURLConnection connection = ChannelRoutes.getChannelConnectionFromRoute(GET_CHANNEL_DETAILS, handle, apiKey); + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("Connection", "keep-alive"); // keep-alive is on by default with http 1.1, but specify anyways + connection.setRequestProperty("Pragma", "no-cache"); + connection.setRequestProperty("Cache-Control", "no-cache"); + connection.setUseCaches(false); + connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS); + connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS); + + final int responseCode = connection.getResponseCode(); + if (responseCode == 200) return Requester.parseJSONObject(connection); + + handleConnectionError("API not available with response code: " + + responseCode + " message: " + connection.getResponseMessage(), + null); + } catch (SocketTimeoutException ex) { + handleConnectionError("Connection timeout", ex); + } catch (IOException ex) { + handleConnectionError("Network error", ex); + } catch (Exception ex) { + Logger.printException(() -> "send failed", ex); + } finally { + Logger.printDebug(() -> "handle: " + handle + " took: " + (System.currentTimeMillis() - startTime) + "ms"); + } + + return null; + } + + private static String fetch(@NonNull String handle, @NonNull String apiKey, Boolean userNameFirst) { + final JSONObject channelJsonObject = send(handle, apiKey); + if (channelJsonObject != null) { + try { + final String userName = channelJsonObject + .getJSONArray("items") + .getJSONObject(0) + .getJSONObject("snippet") + .getString("title"); + return authorBadgeBuilder(handle, userName, userNameFirst); + } catch (JSONException e) { + Logger.printDebug(() -> "Fetch failed while processing response data for response: " + channelJsonObject); + } + } + return null; + } + + private static final String AUTHOR_BADGE_FORMAT = "\u202D%s\u2009%s"; + private static final String PARENTHESES_FORMAT = "(%s)"; + + private static String authorBadgeBuilder(@NonNull String handle, @NonNull String userName, Boolean userNameFirst) { + if (userNameFirst == null) { + return userName; + } else if (TRUE.equals(userNameFirst)) { + handle = String.format(Locale.ENGLISH, PARENTHESES_FORMAT, handle); + if (!Utils.isRightToLeftTextLayout()) { + return String.format(Locale.ENGLISH, AUTHOR_BADGE_FORMAT, userName, handle); + } + } else { + userName = String.format(Locale.ENGLISH, PARENTHESES_FORMAT, userName); + } + return String.format(Locale.ENGLISH, AUTHOR_BADGE_FORMAT, handle, userName); + } + + private final String handle; + private final Future future; + + private ChannelRequest(String handle, String apiKey, Boolean append) { + this.handle = handle; + this.future = Utils.submitOnBackgroundThread(() -> fetch(handle, apiKey, append)); + } + + @Nullable + public String getStream() { + try { + return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS); + } catch (TimeoutException ex) { + Logger.printInfo(() -> "getStream timed out", ex); + } catch (InterruptedException ex) { + Logger.printException(() -> "getStream interrupted", ex); + Thread.currentThread().interrupt(); // Restore interrupt status flag. + } catch (ExecutionException ex) { + Logger.printException(() -> "getStream failure", ex); + } + + return null; + } + + @NonNull + @Override + public String toString() { + return "ChannelRequest{" + "handle='" + handle + '\'' + '}'; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubeusername/requests/ChannelRoutes.java b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubeusername/requests/ChannelRoutes.java new file mode 100644 index 000000000..14da59603 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/returnyoutubeusername/requests/ChannelRoutes.java @@ -0,0 +1,22 @@ +package app.revanced.extension.shared.returnyoutubeusername.requests; + +import static app.revanced.extension.shared.requests.Route.Method.GET; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import app.revanced.extension.shared.requests.Requester; +import app.revanced.extension.shared.requests.Route; + +public class ChannelRoutes { + public static final String YOUTUBEI_V3_GAPIS_URL = "https://www.googleapis.com/youtube/v3/"; + + public static final Route GET_CHANNEL_DETAILS = new Route(GET, "channels?part=snippet&forHandle={handle}&key={api_key}"); + + public ChannelRoutes() { + } + + public static HttpURLConnection getChannelConnectionFromRoute(Route route, String... params) throws IOException { + return Requester.getConnectionFromRoute(YOUTUBEI_V3_GAPIS_URL, route, params); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java new file mode 100644 index 000000000..d654366e9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java @@ -0,0 +1,53 @@ +package app.revanced.extension.shared.settings; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static app.revanced.extension.shared.patches.PatchStatus.HideFullscreenAdsDefaultBoolean; +import static app.revanced.extension.shared.patches.PatchStatus.SpoofStreamingDataAndroidOnlyDefaultBoolean; + +import app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat; +import app.revanced.extension.shared.patches.client.AppClient.ClientType; + +/** + * Settings shared across multiple apps. + *

+ * To ensure this class is loaded when the UI is created, app specific setting bundles should extend + * or reference this class. + */ +public class BaseSettings { + public static final BooleanSetting ENABLE_DEBUG_LOGGING = new BooleanSetting("revanced_enable_debug_logging", FALSE); + /** + * When enabled, share the debug logs with care. + * The buffer contains select user data, including the client ip address and information that could identify the end user. + */ + public static final BooleanSetting ENABLE_DEBUG_BUFFER_LOGGING = new BooleanSetting("revanced_enable_debug_buffer_logging", FALSE); + public static final BooleanSetting SETTINGS_INITIALIZED = new BooleanSetting("revanced_settings_initialized", FALSE, false, false); + public static final BooleanSetting GMS_SHOW_DIALOG = new BooleanSetting("revanced_gms_show_dialog", TRUE); + + /** + * These settings are used by YouTube and YouTube Music. + */ + public static final BooleanSetting HIDE_FULLSCREEN_ADS = new BooleanSetting("revanced_hide_fullscreen_ads", HideFullscreenAdsDefaultBoolean(), true); + public static final BooleanSetting HIDE_PROMOTION_ALERT_BANNER = new BooleanSetting("revanced_hide_promotion_alert_banner", TRUE); + + public static final BooleanSetting DISABLE_AUTO_CAPTIONS = new BooleanSetting("revanced_disable_auto_captions", FALSE, true); + + public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true); + public static final BooleanSetting RETURN_YOUTUBE_USERNAME_ENABLED = new BooleanSetting("revanced_return_youtube_username_enabled", FALSE, true); + public static final EnumSetting RETURN_YOUTUBE_USERNAME_DISPLAY_FORMAT = new EnumSetting<>("revanced_return_youtube_username_display_format", DisplayFormat.USERNAME_ONLY, true); + public static final StringSetting RETURN_YOUTUBE_USERNAME_YOUTUBE_DATA_API_V3_DEVELOPER_KEY = new StringSetting("revanced_return_youtube_username_youtube_data_api_v3_developer_key", "", true, false); + + public static final BooleanSetting SPOOF_STREAMING_DATA = new BooleanSetting("revanced_spoof_streaming_data", TRUE, true, "revanced_spoof_streaming_data_user_dialog_message"); + public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.ANDROID_VR, true); + public static final BooleanSetting SPOOF_STREAMING_DATA_ANDROID_ONLY = new BooleanSetting("revanced_spoof_streaming_data_android_only", SpoofStreamingDataAndroidOnlyDefaultBoolean(), true, "revanced_spoof_streaming_data_android_only_user_dialog_message"); + public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE); + + /** + * @noinspection DeprecatedIsStillUsed + */ + @Deprecated + // The official ReVanced does not offer this, so it has been removed from the settings only. Users can still access settings through import / export settings. + public static final StringSetting BYPASS_IMAGE_REGION_RESTRICTIONS_DOMAIN = new StringSetting("revanced_bypass_image_region_restrictions_domain", "yt4.ggpht.com", true); + + public static final BooleanSetting SANITIZE_SHARING_LINKS = new BooleanSetting("revanced_sanitize_sharing_links", TRUE, true); +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java new file mode 100644 index 000000000..b517924b4 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java @@ -0,0 +1,93 @@ +package app.revanced.extension.shared.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Objects; + +@SuppressWarnings("unused") +public class BooleanSetting extends Setting { + public BooleanSetting(String key, Boolean defaultValue) { + super(key, defaultValue); + } + + public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp) { + super(key, defaultValue, rebootApp); + } + + public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport) { + super(key, defaultValue, rebootApp, includeWithImportExport); + } + + public BooleanSetting(String key, Boolean defaultValue, String userDialogMessage) { + super(key, defaultValue, userDialogMessage); + } + + public BooleanSetting(String key, Boolean defaultValue, Availability availability) { + super(key, defaultValue, availability); + } + + public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage) { + super(key, defaultValue, rebootApp, userDialogMessage); + } + + public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, Availability availability) { + super(key, defaultValue, rebootApp, availability); + } + + public BooleanSetting(String key, Boolean defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + super(key, defaultValue, rebootApp, userDialogMessage, availability); + } + + public BooleanSetting(@NonNull String key, @NonNull Boolean defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { + super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); + } + + /** + * Sets, but does _not_ persistently save the value. + * This method is only to be used by the Settings preference code. + *

+ * This intentionally is a static method to deter + * accidental usage when {@link #save(Boolean)} was intnded. + */ + public static void privateSetValue(@NonNull BooleanSetting setting, @NonNull Boolean newValue) { + setting.value = Objects.requireNonNull(newValue); + } + + @Override + protected void load() { + value = preferences.getBoolean(key, defaultValue); + } + + @Override + protected Boolean readFromJSON(JSONObject json, String importExportKey) throws JSONException { + return json.getBoolean(importExportKey); + } + + @Override + protected void setValueFromString(@NonNull String newValue) { + value = Boolean.valueOf(Objects.requireNonNull(newValue)); + } + + @Override + public void save(@NonNull Boolean newValue) { + // Must set before saving to preferences (otherwise importing fails to update UI correctly). + value = Objects.requireNonNull(newValue); + preferences.saveBoolean(key, newValue); + } + + @Override + public void saveValueFromString(@NonNull String newValue) { + setValueFromString(newValue); + preferences.saveString(key, newValue); + } + + @NonNull + @Override + public Boolean get() { + return value; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java new file mode 100644 index 000000000..36c6ebfb7 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java @@ -0,0 +1,131 @@ +package app.revanced.extension.shared.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Locale; +import java.util.Objects; + +import app.revanced.extension.shared.utils.Logger; + +/** + * If an Enum value is removed or changed, any saved or imported data using the + * non-existent value will be reverted to the default value + * (the event is logged, but no user error is displayed). + *

+ * All saved JSON text is converted to lowercase to keep the output less obnoxious. + */ +@SuppressWarnings("unused") +public class EnumSetting> extends Setting { + public EnumSetting(String key, T defaultValue) { + super(key, defaultValue); + } + + public EnumSetting(String key, T defaultValue, boolean rebootApp) { + super(key, defaultValue, rebootApp); + } + + public EnumSetting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) { + super(key, defaultValue, rebootApp, includeWithImportExport); + } + + public EnumSetting(String key, T defaultValue, String userDialogMessage) { + super(key, defaultValue, userDialogMessage); + } + + public EnumSetting(String key, T defaultValue, Availability availability) { + super(key, defaultValue, availability); + } + + public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) { + super(key, defaultValue, rebootApp, userDialogMessage); + } + + public EnumSetting(String key, T defaultValue, boolean rebootApp, Availability availability) { + super(key, defaultValue, rebootApp, availability); + } + + public EnumSetting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + super(key, defaultValue, rebootApp, userDialogMessage, availability); + } + + public EnumSetting(@NonNull String key, @NonNull T defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { + super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); + } + + @Override + protected void load() { + value = preferences.getEnum(key, defaultValue); + } + + @Override + protected T readFromJSON(JSONObject json, String importExportKey) throws JSONException { + String enumName = json.getString(importExportKey); + try { + return getEnumFromString(enumName); + } catch (IllegalArgumentException ex) { + // Info level to allow removing enum values in the future without showing any user errors. + Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName, ex); + return defaultValue; + } + } + + @Override + protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException { + // Use lowercase to keep the output less ugly. + json.put(importExportKey, value.name().toLowerCase(Locale.ENGLISH)); + } + + @NonNull + private T getEnumFromString(String enumName) { + //noinspection ConstantConditions + for (Enum value : defaultValue.getClass().getEnumConstants()) { + if (value.name().equalsIgnoreCase(enumName)) { + // noinspection unchecked + return (T) value; + } + } + throw new IllegalArgumentException("Unknown enum value: " + enumName); + } + + @Override + protected void setValueFromString(@NonNull String newValue) { + value = getEnumFromString(Objects.requireNonNull(newValue)); + } + + @Override + public void save(@NonNull T newValue) { + // Must set before saving to preferences (otherwise importing fails to update UI correctly). + value = Objects.requireNonNull(newValue); + preferences.saveEnumAsString(key, newValue); + } + + @Override + public void saveValueFromString(@NonNull String newValue) { + setValueFromString(newValue); + preferences.saveString(key, newValue); + } + + @NonNull + @Override + public T get() { + return value; + } + + /** + * Availability based on if this setting is currently set to any of the provided types. + */ + @SafeVarargs + public final Setting.Availability availability(@NonNull T... types) { + return () -> { + T currentEnumType = get(); + for (T enumType : types) { + if (currentEnumType == enumType) return true; + } + return false; + }; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/FloatSetting.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/FloatSetting.java new file mode 100644 index 000000000..fe6190d65 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/FloatSetting.java @@ -0,0 +1,83 @@ +package app.revanced.extension.shared.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Objects; + +@SuppressWarnings("unused") +public class FloatSetting extends Setting { + + public FloatSetting(String key, Float defaultValue) { + super(key, defaultValue); + } + + public FloatSetting(String key, Float defaultValue, boolean rebootApp) { + super(key, defaultValue, rebootApp); + } + + public FloatSetting(String key, Float defaultValue, boolean rebootApp, boolean includeWithImportExport) { + super(key, defaultValue, rebootApp, includeWithImportExport); + } + + public FloatSetting(String key, Float defaultValue, String userDialogMessage) { + super(key, defaultValue, userDialogMessage); + } + + public FloatSetting(String key, Float defaultValue, Availability availability) { + super(key, defaultValue, availability); + } + + public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage) { + super(key, defaultValue, rebootApp, userDialogMessage); + } + + public FloatSetting(String key, Float defaultValue, boolean rebootApp, Availability availability) { + super(key, defaultValue, rebootApp, availability); + } + + public FloatSetting(String key, Float defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + super(key, defaultValue, rebootApp, userDialogMessage, availability); + } + + public FloatSetting(@NonNull String key, @NonNull Float defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { + super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); + } + + @Override + protected void load() { + value = preferences.getFloatString(key, defaultValue); + } + + @Override + protected Float readFromJSON(JSONObject json, String importExportKey) throws JSONException { + return (float) json.getDouble(importExportKey); + } + + @Override + protected void setValueFromString(@NonNull String newValue) { + value = Float.valueOf(Objects.requireNonNull(newValue)); + } + + @Override + public void save(@NonNull Float newValue) { + // Must set before saving to preferences (otherwise importing fails to update UI correctly). + value = Objects.requireNonNull(newValue); + preferences.saveFloatString(key, newValue); + } + + @Override + public void saveValueFromString(@NonNull String newValue) { + setValueFromString(newValue); + preferences.saveString(key, newValue); + } + + @NonNull + @Override + public Float get() { + return value; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/IntegerSetting.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/IntegerSetting.java new file mode 100644 index 000000000..d4d34728f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/IntegerSetting.java @@ -0,0 +1,83 @@ +package app.revanced.extension.shared.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Objects; + +@SuppressWarnings("unused") +public class IntegerSetting extends Setting { + + public IntegerSetting(String key, Integer defaultValue) { + super(key, defaultValue); + } + + public IntegerSetting(String key, Integer defaultValue, boolean rebootApp) { + super(key, defaultValue, rebootApp); + } + + public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, boolean includeWithImportExport) { + super(key, defaultValue, rebootApp, includeWithImportExport); + } + + public IntegerSetting(String key, Integer defaultValue, String userDialogMessage) { + super(key, defaultValue, userDialogMessage); + } + + public IntegerSetting(String key, Integer defaultValue, Availability availability) { + super(key, defaultValue, availability); + } + + public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage) { + super(key, defaultValue, rebootApp, userDialogMessage); + } + + public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, Availability availability) { + super(key, defaultValue, rebootApp, availability); + } + + public IntegerSetting(String key, Integer defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + super(key, defaultValue, rebootApp, userDialogMessage, availability); + } + + public IntegerSetting(@NonNull String key, @NonNull Integer defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { + super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); + } + + @Override + protected void load() { + value = preferences.getIntegerString(key, defaultValue); + } + + @Override + protected Integer readFromJSON(JSONObject json, String importExportKey) throws JSONException { + return json.getInt(importExportKey); + } + + @Override + protected void setValueFromString(@NonNull String newValue) { + value = Integer.valueOf(Objects.requireNonNull(newValue)); + } + + @Override + public void save(@NonNull Integer newValue) { + // Must set before saving to preferences (otherwise importing fails to update UI correctly). + value = Objects.requireNonNull(newValue); + preferences.saveIntegerString(key, newValue); + } + + @Override + public void saveValueFromString(@NonNull String newValue) { + setValueFromString(newValue); + preferences.saveString(key, newValue); + } + + @NonNull + @Override + public Integer get() { + return value; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/LongSetting.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/LongSetting.java new file mode 100644 index 000000000..91d1b5a93 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/LongSetting.java @@ -0,0 +1,83 @@ +package app.revanced.extension.shared.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Objects; + +@SuppressWarnings("unused") +public class LongSetting extends Setting { + + public LongSetting(String key, Long defaultValue) { + super(key, defaultValue); + } + + public LongSetting(String key, Long defaultValue, boolean rebootApp) { + super(key, defaultValue, rebootApp); + } + + public LongSetting(String key, Long defaultValue, boolean rebootApp, boolean includeWithImportExport) { + super(key, defaultValue, rebootApp, includeWithImportExport); + } + + public LongSetting(String key, Long defaultValue, String userDialogMessage) { + super(key, defaultValue, userDialogMessage); + } + + public LongSetting(String key, Long defaultValue, Availability availability) { + super(key, defaultValue, availability); + } + + public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage) { + super(key, defaultValue, rebootApp, userDialogMessage); + } + + public LongSetting(String key, Long defaultValue, boolean rebootApp, Availability availability) { + super(key, defaultValue, rebootApp, availability); + } + + public LongSetting(String key, Long defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + super(key, defaultValue, rebootApp, userDialogMessage, availability); + } + + public LongSetting(@NonNull String key, @NonNull Long defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { + super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); + } + + @Override + protected void load() { + value = preferences.getLongString(key, defaultValue); + } + + @Override + protected Long readFromJSON(JSONObject json, String importExportKey) throws JSONException { + return json.getLong(importExportKey); + } + + @Override + protected void setValueFromString(@NonNull String newValue) { + value = Long.valueOf(Objects.requireNonNull(newValue)); + } + + @Override + public void save(@NonNull Long newValue) { + // Must set before saving to preferences (otherwise importing fails to update UI correctly). + value = Objects.requireNonNull(newValue); + preferences.saveLongString(key, newValue); + } + + @Override + public void saveValueFromString(@NonNull String newValue) { + setValueFromString(newValue); + preferences.saveString(key, newValue); + } + + @NonNull + @Override + public Long get() { + return value; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/Setting.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/Setting.java new file mode 100644 index 000000000..e3a769f67 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/Setting.java @@ -0,0 +1,492 @@ +package app.revanced.extension.shared.settings; + +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.isSDKAbove; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import app.revanced.extension.shared.settings.preference.SharedPrefCategory; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.StringRef; +import app.revanced.extension.shared.utils.Utils; + +/** + * @noinspection rawtypes + */ +@SuppressWarnings("unused") +public abstract class Setting { + + /** + * Indicates if a {@link Setting} is available to edit and use. + * Typically this is dependent upon other BooleanSetting(s) set to 'true', + * but this can be used to call into integrations code and check other conditions. + */ + public interface Availability { + boolean isAvailable(); + } + + /** + * Availability based on a single parent setting being enabled. + */ + @NonNull + public static Availability parent(@NonNull BooleanSetting parent) { + return parent::get; + } + + /** + * Availability based on all parents being enabled. + */ + @NonNull + public static Availability parentsAll(@NonNull BooleanSetting... parents) { + return () -> { + for (BooleanSetting parent : parents) { + if (!parent.get()) return false; + } + return true; + }; + } + + /** + * Availability based on any parent being enabled. + */ + @NonNull + public static Availability parentsAny(@NonNull BooleanSetting... parents) { + return () -> { + for (BooleanSetting parent : parents) { + if (parent.get()) return true; + } + return false; + }; + } + + /** + * Callback for importing/exporting settings. + */ + public interface ImportExportCallback { + /** + * Called after all settings have been imported. + */ + void settingsImported(@Nullable Context context); + + /** + * Called after all settings have been exported. + */ + void settingsExported(@Nullable Context context); + } + + private static final List importExportCallbacks = new ArrayList<>(); + + /** + * Adds a callback for {@link #importFromJSON(Context, String)} and {@link #exportToJson(Context)}. + */ + public static void addImportExportCallback(@NonNull ImportExportCallback callback) { + importExportCallbacks.add(Objects.requireNonNull(callback)); + } + + /** + * All settings that were instantiated. + * When a new setting is created, it is automatically added to this list. + */ + private static final List> SETTINGS = new ArrayList<>(); + + /** + * Map of setting path to setting object. + */ + private static final Map> PATH_TO_SETTINGS = new HashMap<>(); + + /** + * Preference all instances are saved to. + */ + public static final SharedPrefCategory preferences = new SharedPrefCategory("revanced"); + + @Nullable + public static Setting getSettingFromPath(@NonNull String str) { + return PATH_TO_SETTINGS.get(str); + } + + /** + * @return All settings that have been created. + */ + @NonNull + public static List> allLoadedSettings() { + return Collections.unmodifiableList(SETTINGS); + } + + /** + * @return All settings that have been created, sorted by keys. + */ + @NonNull + private static List> allLoadedSettingsSorted() { + if (isSDKAbove(24)) { + SETTINGS.sort(Comparator.comparing((Setting o) -> o.key)); + } else { + //noinspection ComparatorCombinators + Collections.sort(SETTINGS, (o1, o2) -> o1.key.compareTo(o2.key)); + } + return allLoadedSettings(); + } + + /** + * The key used to store the value in the shared preferences. + */ + @NonNull + public final String key; + + /** + * The default value of the setting. + */ + @NonNull + public final T defaultValue; + + /** + * If the app should be rebooted, if this setting is changed + */ + public final boolean rebootApp; + + /** + * If this setting should be included when importing/exporting settings. + */ + public final boolean includeWithImportExport; + + /** + * If this setting is available to edit and use. + * Not to be confused with it's status returned from {@link #get()}. + */ + @Nullable + private final Availability availability; + + /** + * Confirmation message to display, if the user tries to change the setting from the default value. + * Currently this works only for Boolean setting types. + */ + @Nullable + public final StringRef userDialogMessage; + + // Must be volatile, as some settings are read/write from different threads. + // Of note, the object value is persistently stored using SharedPreferences (which is thread safe). + /** + * The value of the setting. + */ + @NonNull + protected volatile T value; + + public Setting(String key, T defaultValue) { + this(key, defaultValue, false, true, null, null); + } + + public Setting(String key, T defaultValue, boolean rebootApp) { + this(key, defaultValue, rebootApp, true, null, null); + } + + public Setting(String key, T defaultValue, boolean rebootApp, boolean includeWithImportExport) { + this(key, defaultValue, rebootApp, includeWithImportExport, null, null); + } + + public Setting(String key, T defaultValue, String userDialogMessage) { + this(key, defaultValue, false, true, userDialogMessage, null); + } + + public Setting(String key, T defaultValue, Availability availability) { + this(key, defaultValue, false, true, null, availability); + } + + public Setting(String key, T defaultValue, boolean rebootApp, String userDialogMessage) { + this(key, defaultValue, rebootApp, true, userDialogMessage, null); + } + + public Setting(String key, T defaultValue, boolean rebootApp, Availability availability) { + this(key, defaultValue, rebootApp, true, null, availability); + } + + public Setting(String key, T defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + this(key, defaultValue, rebootApp, true, userDialogMessage, availability); + } + + /** + * A setting backed by a shared preference. + * + * @param key The key used to store the value in the shared preferences. + * @param defaultValue The default value of the setting. + * @param rebootApp If the app should be rebooted, if this setting is changed. + * @param includeWithImportExport If this setting should be shown in the import/export dialog. + * @param userDialogMessage Confirmation message to display, if the user tries to change the setting from the default value. + * @param availability Condition that must be true, for this setting to be available to configure. + */ + public Setting(@NonNull String key, + @NonNull T defaultValue, + boolean rebootApp, + boolean includeWithImportExport, + @Nullable String userDialogMessage, + @Nullable Availability availability + ) { + this.key = Objects.requireNonNull(key); + this.value = this.defaultValue = Objects.requireNonNull(defaultValue); + this.rebootApp = rebootApp; + this.includeWithImportExport = includeWithImportExport; + this.userDialogMessage = (userDialogMessage == null) ? null : new StringRef(userDialogMessage); + this.availability = availability; + + SETTINGS.add(this); + if (PATH_TO_SETTINGS.put(key, this) != null) { + // Debug setting may not be created yet so using Logger may cause an initialization crash. + // Show a toast instead. + Utils.showToastShort(this.getClass().getSimpleName() + + " error: Duplicate Setting key found: " + key); + } + + load(); + } + + /** + * Migrate a setting value if the path is renamed but otherwise the old and new settings are identical. + */ + public static void migrateOldSettingToNew(@NonNull Setting oldSetting, @NonNull Setting newSetting) { + if (oldSetting == newSetting) { + throw new IllegalArgumentException(); + } + + if (!oldSetting.isSetToDefault()) { + Logger.printInfo(() -> "Migrating old setting value: " + oldSetting + " into replacement setting: " + newSetting); + newSetting.save(oldSetting.value); + oldSetting.resetToDefault(); + } + } + + /** + * Migrate an old Setting value previously stored in a different SharedPreference. + *

+ * This method will be deleted in the future. + */ + public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) { + if (!oldPrefs.preferences.contains(settingKey)) { + return; // Nothing to do. + } + + Object newValue = setting.get(); + final Object migratedValue; + if (setting instanceof BooleanSetting) { + migratedValue = oldPrefs.getBoolean(settingKey, (Boolean) newValue); + } else if (setting instanceof IntegerSetting) { + migratedValue = oldPrefs.getIntegerString(settingKey, (Integer) newValue); + } else if (setting instanceof LongSetting) { + migratedValue = oldPrefs.getLongString(settingKey, (Long) newValue); + } else if (setting instanceof FloatSetting) { + migratedValue = oldPrefs.getFloatString(settingKey, (Float) newValue); + } else if (setting instanceof StringSetting) { + migratedValue = oldPrefs.getString(settingKey, (String) newValue); + } else { + Logger.printException(() -> "Unknown setting: " + setting); + // Remove otherwise it'll show a toast on every launch + oldPrefs.preferences.edit().remove(settingKey).apply(); + return; + } + + oldPrefs.preferences.edit().remove(settingKey).apply(); // Remove the old setting. + if (migratedValue.equals(newValue)) { + Logger.printDebug(() -> "Value does not need migrating: " + settingKey); + return; // Old value is already equal to the new setting value. + } + + Logger.printDebug(() -> "Migrating old preference value into current preference: " + settingKey); + //noinspection unchecked + setting.save(migratedValue); + } + + /** + * Sets, but does _not_ persistently save the value. + * This method is only to be used by the Settings preference code. + *

+ * This intentionally is a static method to deter + * accidental usage when {@link #save(Object)} was intended. + */ + public static void privateSetValueFromString(@NonNull Setting setting, @NonNull String newValue) { + setting.setValueFromString(newValue); + } + + /** + * Sets the value of {@link #value}, but do not save to {@link #preferences}. + */ + protected abstract void setValueFromString(@NonNull String newValue); + + /** + * Load and set the value of {@link #value}. + */ + protected abstract void load(); + + /** + * Persistently saves the value. + */ + public abstract void save(@NonNull T newValue); + + /** + * Persistently saves the value using strings. + */ + public abstract void saveValueFromString(@NonNull String newValue); + + @NonNull + public abstract T get(); + + /** + * Identical to calling {@link #save(Object)} using {@link #defaultValue}. + */ + public void resetToDefault() { + save(defaultValue); + } + + /** + * @return if this setting can be configured and used. + */ + public boolean isAvailable() { + return availability == null || availability.isAvailable(); + } + + /** + * @return if the currently set value is the same as {@link #defaultValue} + * @noinspection BooleanMethodIsAlwaysInverted + */ + public boolean isSetToDefault() { + return value.equals(defaultValue); + } + + @NonNull + @Override + public String toString() { + return key + "=" + get(); + } + + // region Import / export + + /** + * If a setting path has this prefix, then remove it before importing/exporting. + */ + private static final String OPTIONAL_REVANCED_SETTINGS_PREFIX = "revanced_"; + + /** + * The path, minus any 'revanced' prefix to keep json concise. + */ + private String getImportExportKey() { + if (key.startsWith(OPTIONAL_REVANCED_SETTINGS_PREFIX)) { + return key.substring(OPTIONAL_REVANCED_SETTINGS_PREFIX.length()); + } + return key; + } + + /** + * @param importExportKey The JSON key. The JSONObject parameter will contain data for this key. + * @return the value stored using the import/export key. Do not set any values in this method. + */ + protected abstract T readFromJSON(JSONObject json, String importExportKey) throws JSONException; + + /** + * Saves this instance to JSON. + *

+ * To keep the JSON simple and readable, + * subclasses should not write out any embedded types (such as JSON Array or Dictionaries). + *

+ * If this instance is not a type supported natively by JSON (ie: it's not a String/Integer/Float/Long), + * then subclasses can override this method and write out a String value representing the value. + */ + protected void writeToJSON(JSONObject json, String importExportKey) throws JSONException { + json.put(importExportKey, value); + } + + @NonNull + public static String exportToJson(@Nullable Context alertDialogContext) { + try { + JSONObject json = new JSONObject(); + for (Setting setting : allLoadedSettingsSorted()) { + String importExportKey = setting.getImportExportKey(); + if (json.has(importExportKey)) { + throw new IllegalArgumentException("duplicate key found: " + importExportKey); + } + + final boolean exportDefaultValues = false; // Enable to see what all settings looks like in the UI. + //noinspection ConstantValue + if (setting.includeWithImportExport && (!setting.isSetToDefault() || exportDefaultValues)) { + setting.writeToJSON(json, importExportKey); + } + } + + for (ImportExportCallback callback : importExportCallbacks) { + callback.settingsExported(alertDialogContext); + } + + if (json.length() == 0) { + return ""; + } + + String export = json.toString(0); + + // Remove the outer JSON braces to make the output more compact, + // and leave less chance of the user forgetting to copy it + return export.substring(2, export.length() - 2); + } catch (JSONException e) { + Logger.printException(() -> "Export failure", e); // should never happen + return ""; + } + } + + /** + * @return if any settings that require a reboot were changed. + */ + public static boolean importFromJSON(@NonNull Context alertDialogContext, @NonNull String settingsJsonString) { + try { + if (!settingsJsonString.matches("[\\s\\S]*\\{")) { + settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces + } + JSONObject json = new JSONObject(settingsJsonString); + + boolean rebootSettingChanged = false; + int numberOfSettingsImported = 0; + for (Setting setting : SETTINGS) { + String key = setting.getImportExportKey(); + if (json.has(key)) { + Object value = setting.readFromJSON(json, key); + if (!setting.get().equals(value)) { + rebootSettingChanged |= setting.rebootApp; + //noinspection unchecked + setting.save(value); + } + numberOfSettingsImported++; + } else if (setting.includeWithImportExport && !setting.isSetToDefault()) { + Logger.printDebug(() -> "Resetting to default: " + setting); + rebootSettingChanged |= setting.rebootApp; + setting.resetToDefault(); + } + } + + for (ImportExportCallback callback : importExportCallbacks) { + callback.settingsImported(alertDialogContext); + } + + Utils.showToastLong(numberOfSettingsImported == 0 + ? str("revanced_extended_settings_import_reset") + : str("revanced_extended_settings_import_success", numberOfSettingsImported)); + + return rebootSettingChanged; + } catch (JSONException | IllegalArgumentException ex) { + Utils.showToastLong(str("revanced_extended_settings_import_failed", ex.getMessage())); + Logger.printInfo(() -> "", ex); + } catch (Exception ex) { + Logger.printException(() -> "Import failure: " + ex.getMessage(), ex); // should never happen + } + return false; + } + + // End import / export + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/StringSetting.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/StringSetting.java new file mode 100644 index 000000000..fda7e516c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/StringSetting.java @@ -0,0 +1,83 @@ +package app.revanced.extension.shared.settings; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Objects; + +@SuppressWarnings("unused") +public class StringSetting extends Setting { + + public StringSetting(String key, String defaultValue) { + super(key, defaultValue); + } + + public StringSetting(String key, String defaultValue, boolean rebootApp) { + super(key, defaultValue, rebootApp); + } + + public StringSetting(String key, String defaultValue, boolean rebootApp, boolean includeWithImportExport) { + super(key, defaultValue, rebootApp, includeWithImportExport); + } + + public StringSetting(String key, String defaultValue, String userDialogMessage) { + super(key, defaultValue, userDialogMessage); + } + + public StringSetting(String key, String defaultValue, Availability availability) { + super(key, defaultValue, availability); + } + + public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage) { + super(key, defaultValue, rebootApp, userDialogMessage); + } + + public StringSetting(String key, String defaultValue, boolean rebootApp, Availability availability) { + super(key, defaultValue, rebootApp, availability); + } + + public StringSetting(String key, String defaultValue, boolean rebootApp, String userDialogMessage, Availability availability) { + super(key, defaultValue, rebootApp, userDialogMessage, availability); + } + + public StringSetting(@NonNull String key, @NonNull String defaultValue, boolean rebootApp, boolean includeWithImportExport, @Nullable String userDialogMessage, @Nullable Availability availability) { + super(key, defaultValue, rebootApp, includeWithImportExport, userDialogMessage, availability); + } + + @Override + protected void load() { + value = preferences.getString(key, defaultValue); + } + + @Override + protected String readFromJSON(JSONObject json, String importExportKey) throws JSONException { + return json.getString(importExportKey); + } + + @Override + protected void setValueFromString(@NonNull String newValue) { + value = Objects.requireNonNull(newValue); + } + + @Override + public void save(@NonNull String newValue) { + // Must set before saving to preferences (otherwise importing fails to update UI correctly). + value = Objects.requireNonNull(newValue); + preferences.saveString(key, newValue); + } + + @Override + public void saveValueFromString(@NonNull String newValue) { + setValueFromString(newValue); + preferences.saveString(key, newValue); + } + + @NonNull + @Override + public String get() { + return value; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java new file mode 100644 index 000000000..b2bac3d67 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java @@ -0,0 +1,287 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.utils.ResourceUtils.getXmlIdentifier; +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.view.View; +import android.widget.ListView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings({"unused", "deprecation"}) +public abstract class AbstractPreferenceFragment extends PreferenceFragment { + /** + * Indicates that if a preference changes, + * to apply the change from the Setting to the UI component. + */ + public static boolean settingImportInProgress; + + /** + * Confirm and restart dialog button text and title. + * Set by subclasses if Strings cannot be added as a resource. + */ + @Nullable + protected static String restartDialogMessage; + + /** + * Used to prevent showing reboot dialog, if user cancels a setting user dialog. + */ + private boolean showingUserDialogMessage; + + private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { + try { + if (str == null) { + return; + } + Setting setting = Setting.getSettingFromPath(str); + if (setting == null) { + return; + } + Preference pref = findPreference(str); + if (pref == null) { + return; + } + + // Apply 'Setting <- Preference', unless during importing when it needs to be 'Setting -> Preference'. + updatePreference(pref, setting, true, settingImportInProgress); + // Update any other preference availability that may now be different. + updateUIAvailability(); + + if (settingImportInProgress) { + return; + } + + if (!showingUserDialogMessage) { + if (setting.userDialogMessage != null && ((SwitchPreference) pref).isChecked() != (Boolean) setting.defaultValue) { + showSettingUserDialogConfirmation((SwitchPreference) pref, (BooleanSetting) setting); + } else if (setting.rebootApp) { + showRestartDialog(getActivity()); + } + } + + } catch (Exception ex) { + Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex); + } + }; + + /** + * Initialize this instance, and do any custom behavior. + *

+ * To ensure all {@link Setting} instances are correctly synced to the UI, + * it is important that subclasses make a call or otherwise reference their Settings class bundle + * so all app specific {@link Setting} instances are loaded before this method returns. + */ + protected void initialize() { + final int id = getXmlIdentifier("revanced_prefs"); + + if (id == 0) return; + addPreferencesFromResource(id); + Utils.sortPreferenceGroups(getPreferenceScreen()); + } + + private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) { + Utils.verifyOnMainThread(); + + final var context = getActivity(); + showingUserDialogMessage = true; + assert setting.userDialogMessage != null; + new AlertDialog.Builder(context) + .setTitle(android.R.string.dialog_alert_title) + .setMessage(setting.userDialogMessage.toString()) + .setPositiveButton(android.R.string.ok, (dialog, id) -> { + if (setting.rebootApp) { + showRestartDialog(context); + } + }) + .setNegativeButton(android.R.string.cancel, (dialog, id) -> { + switchPref.setChecked(setting.defaultValue); // Recursive call that resets the Setting value. + }) + .setOnDismissListener(dialog -> showingUserDialogMessage = false) + .setCancelable(false) + .show(); + } + + /** + * Updates all Preferences values and their availability using the current values in {@link Setting}. + */ + protected void updateUIToSettingValues() { + updatePreferenceScreen(getPreferenceScreen(), true, true); + } + + /** + * Updates Preferences availability only using the status of {@link Setting}. + */ + protected void updateUIAvailability() { + updatePreferenceScreen(getPreferenceScreen(), false, false); + } + + /** + * Syncs all UI Preferences to any {@link Setting} they represent. + */ + private void updatePreferenceScreen(@NonNull PreferenceScreen screen, + boolean syncSettingValue, + boolean applySettingToPreference) { + // Alternatively this could iterate thru all Settings and check for any matching Preferences, + // but there are many more Settings than UI preferences so it's more efficient to only check + // the Preferences. + for (int i = 0, prefCount = screen.getPreferenceCount(); i < prefCount; i++) { + Preference pref = screen.getPreference(i); + if (pref instanceof PreferenceScreen preferenceScreen) { + updatePreferenceScreen(preferenceScreen, syncSettingValue, applySettingToPreference); + } else if (pref.hasKey()) { + String key = pref.getKey(); + Setting setting = Setting.getSettingFromPath(key); + if (setting != null) { + updatePreference(pref, setting, syncSettingValue, applySettingToPreference); + } + } + } + } + + /** + * Handles syncing a UI Preference with the {@link Setting} that backs it. + * If needed, subclasses can override this to handle additional UI Preference types. + * + * @param applySettingToPreference If true, then apply {@link Setting} -> Preference. + * If false, then apply {@link Setting} <- Preference. + */ + protected void syncSettingWithPreference(@NonNull Preference pref, + @NonNull Setting setting, + boolean applySettingToPreference) { + if (pref instanceof SwitchPreference switchPreference) { + BooleanSetting boolSetting = (BooleanSetting) setting; + if (applySettingToPreference) { + switchPreference.setChecked(boolSetting.get()); + } else { + BooleanSetting.privateSetValue(boolSetting, switchPreference.isChecked()); + } + } else if (pref instanceof EditTextPreference editTextPreference) { + if (applySettingToPreference) { + editTextPreference.setText(setting.get().toString()); + } else { + Setting.privateSetValueFromString(setting, editTextPreference.getText()); + } + } else if (pref instanceof ListPreference listPreference) { + if (applySettingToPreference) { + listPreference.setValue(setting.get().toString()); + } else { + Setting.privateSetValueFromString(setting, listPreference.getValue()); + } + updateListPreferenceSummary(listPreference, setting); + } else { + Logger.printException(() -> "Setting cannot be handled: " + pref.getClass() + ": " + pref); + } + } + + /** + * Updates a UI Preference with the {@link Setting} that backs it. + * + * @param syncSetting If the UI should be synced {@link Setting} <-> Preference + * @param applySettingToPreference If true, then apply {@link Setting} -> Preference. + * If false, then apply {@link Setting} <- Preference. + */ + private void updatePreference(@NonNull Preference pref, @NonNull Setting setting, + boolean syncSetting, boolean applySettingToPreference) { + if (!syncSetting && applySettingToPreference) { + throw new IllegalArgumentException(); + } + + if (syncSetting) { + syncSettingWithPreference(pref, setting, applySettingToPreference); + } + + updatePreferenceAvailability(pref, setting); + } + + protected void updatePreferenceAvailability(@NonNull Preference pref, @NonNull Setting setting) { + pref.setEnabled(setting.isAvailable()); + } + + public static void updateListPreferenceSummary(ListPreference listPreference, Setting setting) { + String objectStringValue = setting.get().toString(); + int entryIndex = listPreference.findIndexOfValue(objectStringValue); + if (entryIndex >= 0) { + listPreference.setValue(objectStringValue); + objectStringValue = listPreference.getEntries()[entryIndex].toString(); + } + listPreference.setSummary(objectStringValue); + } + + public static void showRestartDialog(@NonNull final Context context) { + if (restartDialogMessage == null) { + restartDialogMessage = str("revanced_extended_restart_message"); + } + showRestartDialog(context, restartDialogMessage); + } + + public static void showRestartDialog(@NonNull final Context context, String message) { + showRestartDialog(context, message, 0); + } + + public static void showRestartDialog(@NonNull final Context context, String message, long delay) { + Utils.verifyOnMainThread(); + + new AlertDialog.Builder(context) + .setMessage(message) + .setPositiveButton(android.R.string.ok, (dialog, id) + -> Utils.runOnMainThreadDelayed(() -> Utils.restartApp(context), delay)) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + @SuppressLint("ResourceType") + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + try { + PreferenceManager preferenceManager = getPreferenceManager(); + preferenceManager.setSharedPreferencesName(Setting.preferences.name); + + // Must initialize before adding change listener, + // otherwise the syncing of Setting -> UI + // causes a callback to the listener even though nothing changed. + initialize(); + updateUIToSettingValues(); + + preferenceManager.getSharedPreferences().registerOnSharedPreferenceChangeListener(listener); + } catch (Exception ex) { + Logger.printException(() -> "onCreate() failure", ex); + } + } + + @Override + public void onResume() { + super.onResume(); + + final View rootView = getView(); + if (rootView == null) return; + ListView listView = getView().findViewById(android.R.id.list); + if (listView == null) return; + listView.setDivider(null); + listView.setDividerHeight(0); + } + + @Override + public void onDestroy() { + getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener); + super.onDestroy(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/HtmlPreference.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/HtmlPreference.java new file mode 100644 index 000000000..3023ee2aa --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/HtmlPreference.java @@ -0,0 +1,34 @@ +package app.revanced.extension.shared.settings.preference; + +import static android.text.Html.FROM_HTML_MODE_COMPACT; + +import android.content.Context; +import android.preference.Preference; +import android.text.Html; +import android.util.AttributeSet; + +/** + * Allows using basic html for the summary text. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class HtmlPreference extends Preference { + { + setSummary(Html.fromHtml(getSummary().toString(), FROM_HTML_MODE_COMPACT)); + } + + public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public HtmlPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public HtmlPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public HtmlPreference(Context context) { + super(context); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java new file mode 100644 index 000000000..b155c84ad --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java @@ -0,0 +1,104 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.content.Context; +import android.preference.EditTextPreference; +import android.preference.Preference; +import android.text.InputType; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.EditText; + +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings({"unused", "deprecation"}) +public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener { + + private String existingSettings; + + @TargetApi(26) + private void init() { + setSelectable(true); + + EditText editText = getEditText(); + editText.setTextIsSelectable(true); + editText.setAutofillHints((String) null); + editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 7); // Use a smaller font to reduce text wrap. + + setOnPreferenceClickListener(this); + } + + public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public ImportExportPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ImportExportPreference(Context context) { + super(context); + init(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + try { + // Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened. + existingSettings = Setting.exportToJson(getContext()); + getEditText().setText(existingSettings); + } catch (Exception ex) { + Logger.printException(() -> "showDialog failure", ex); + } + return true; + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + try { + Utils.setEditTextDialogTheme(builder, true); + super.onPrepareDialogBuilder(builder); + // Show the user the settings in JSON format. + builder.setNeutralButton( + str("revanced_extended_settings_import_copy"), (dialog, which) -> + Utils.setClipboard(getEditText().getText().toString()) + ).setPositiveButton( + str("revanced_extended_settings_import"), (dialog, which) -> + importSettings(builder.getContext(), getEditText().getText().toString()) + ); + } catch (Exception ex) { + Logger.printException(() -> "onPrepareDialogBuilder failure", ex); + } + } + + private void importSettings(Context context, String replacementSettings) { + try { + if (replacementSettings.equals(existingSettings)) { + return; + } + AbstractPreferenceFragment.settingImportInProgress = true; + final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings); + if (rebootNeeded) { + AbstractPreferenceFragment.showRestartDialog(getContext()); + } + } catch (Exception ex) { + Logger.printException(() -> "importSettings failure", ex); + } finally { + AbstractPreferenceFragment.settingImportInProgress = false; + } + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java new file mode 100644 index 000000000..43305c23c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java @@ -0,0 +1,78 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.app.AlertDialog; +import android.content.Context; +import android.os.Bundle; +import android.preference.EditTextPreference; +import android.util.AttributeSet; +import android.widget.Button; +import android.widget.EditText; + +import java.util.Objects; + +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings({"unused", "deprecation"}) +public class ResettableEditTextPreference extends EditTextPreference { + + public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public ResettableEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public ResettableEditTextPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public ResettableEditTextPreference(Context context) { + super(context); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + Utils.setEditTextDialogTheme(builder); + super.onPrepareDialogBuilder(builder); + + final CharSequence title = getTitle(); + if (title != null) { + builder.setTitle(getTitle()); + } + final Setting setting = Setting.getSettingFromPath(getKey()); + if (setting != null) { + builder.setNeutralButton(str("revanced_extended_settings_reset"), null); + } + } + + @Override + protected void showDialog(Bundle state) { + super.showDialog(state); + + if (!(getDialog() instanceof AlertDialog alertDialog)) { + return; + } + + // Override the button click listener to prevent dismissing the dialog. + Button button = alertDialog.getButton(AlertDialog.BUTTON_NEUTRAL); + if (button == null) { + return; + } + button.setOnClickListener(v -> { + try { + Setting setting = Objects.requireNonNull(Setting.getSettingFromPath(getKey())); + String defaultStringValue = setting.defaultValue.toString(); + EditText editText = getEditText(); + editText.setText(defaultStringValue); + editText.setSelection(defaultStringValue.length()); // move cursor to end of text + } catch (Exception ex) { + Logger.printException(() -> "reset failure", ex); + } + }); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java new file mode 100644 index 000000000..5122ba191 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java @@ -0,0 +1,193 @@ +package app.revanced.extension.shared.settings.preference; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceFragment; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +/** + * Shared categories, and helper methods. + *

+ * The various save methods store numbers as Strings, + * which is required if using {@link PreferenceFragment}. + *

+ * If saved numbers will not be used with a preference fragment, + * then store the primitive numbers using the {@link #preferences} itself. + */ +public class SharedPrefCategory { + @NonNull + public final String name; + @NonNull + public final SharedPreferences preferences; + + public SharedPrefCategory(@NonNull String name) { + this.name = Objects.requireNonNull(name); + preferences = Objects.requireNonNull(Utils.getContext()).getSharedPreferences(name, Context.MODE_PRIVATE); + } + + private void removeConflictingPreferenceKeyValue(@NonNull String key) { + Logger.printException(() -> "Found conflicting preference: " + key); + removeKey(key); + } + + private void saveObjectAsString(@NonNull String key, @Nullable Object value) { + preferences.edit().putString(key, (value == null ? null : value.toString())).apply(); + } + + /** + * Removes any preference data type that has the specified key. + */ + public void removeKey(@NonNull String key) { + preferences.edit().remove(Objects.requireNonNull(key)).apply(); + } + + public void saveBoolean(@NonNull String key, boolean value) { + preferences.edit().putBoolean(key, value).apply(); + } + + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveEnumAsString(@NonNull String key, @Nullable Enum value) { + saveObjectAsString(key, value); + } + + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveIntegerString(@NonNull String key, @Nullable Integer value) { + saveObjectAsString(key, value); + } + + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveLongString(@NonNull String key, @Nullable Long value) { + saveObjectAsString(key, value); + } + + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveFloatString(@NonNull String key, @Nullable Float value) { + saveObjectAsString(key, value); + } + + /** + * @param value a NULL parameter removes the value from the preferences + */ + public void saveString(@NonNull String key, @Nullable String value) { + saveObjectAsString(key, value); + } + + @NonNull + public String getString(@NonNull String key, @NonNull String _default) { + Objects.requireNonNull(_default); + try { + return preferences.getString(key, _default); + } catch (ClassCastException ex) { + // Value stored is a completely different type (should never happen). + removeConflictingPreferenceKeyValue(key); + return _default; + } + } + + @NonNull + public > T getEnum(@NonNull String key, @NonNull T _default) { + Objects.requireNonNull(_default); + try { + String enumName = preferences.getString(key, null); + if (enumName != null) { + try { + // noinspection unchecked + return (T) Enum.valueOf(_default.getClass(), enumName); + } catch (IllegalArgumentException ex) { + // Info level to allow removing enum values in the future without showing any user errors. + Logger.printInfo(() -> "Using default, and ignoring unknown enum value: " + enumName); + removeKey(key); + } + } + } catch (ClassCastException ex) { + // Value stored is a completely different type (should never happen). + removeConflictingPreferenceKeyValue(key); + } + return _default; + } + + public boolean getBoolean(@NonNull String key, boolean _default) { + try { + return preferences.getBoolean(key, _default); + } catch (ClassCastException ex) { + // Value stored is a completely different type (should never happen). + removeConflictingPreferenceKeyValue(key); + return _default; + } + } + + @NonNull + public Integer getIntegerString(@NonNull String key, @NonNull Integer _default) { + try { + String value = preferences.getString(key, null); + if (value != null) { + return Integer.valueOf(value); + } + return _default; + } catch (ClassCastException | NumberFormatException ex) { + try { + // Old data previously stored as primitive. + return preferences.getInt(key, _default); + } catch (ClassCastException ex2) { + // Value stored is a completely different type (should never happen). + removeConflictingPreferenceKeyValue(key); + } + } + return _default; + } + + @NonNull + public Long getLongString(@NonNull String key, @NonNull Long _default) { + try { + String value = preferences.getString(key, null); + if (value != null) { + return Long.valueOf(value); + } + } catch (ClassCastException | NumberFormatException ex) { + try { + return preferences.getLong(key, _default); + } catch (ClassCastException ex2) { + removeConflictingPreferenceKeyValue(key); + } + } + return _default; + } + + @NonNull + public Float getFloatString(@NonNull String key, @NonNull Float _default) { + try { + String value = preferences.getString(key, null); + if (value != null) { + return Float.valueOf(value); + } + } catch (ClassCastException | NumberFormatException ex) { + try { + return preferences.getFloat(key, _default); + } catch (ClassCastException ex2) { + removeConflictingPreferenceKeyValue(key); + } + } + return _default; + } + + @NonNull + @Override + public String toString() { + return name; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/WebViewDialog.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/WebViewDialog.java new file mode 100644 index 000000000..d971540ee --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/WebViewDialog.java @@ -0,0 +1,62 @@ +package app.revanced.extension.shared.settings.preference; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.view.Window; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import androidx.annotation.NonNull; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +/** + * Displays html content as a dialog. Any links a user taps on are opened in an external browser. + */ +@SuppressWarnings("deprecation") +public class WebViewDialog extends Dialog { + + private final String htmlContent; + + public WebViewDialog(@NonNull Context context, @NonNull String htmlContent) { + super(context); + this.htmlContent = htmlContent; + } + + // JS required to hide any broken images. No remote javascript is ever loaded. + @SuppressLint("SetJavaScriptEnabled") + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + requestWindowFeature(Window.FEATURE_NO_TITLE); + + WebView webView = new WebView(getContext()); + webView.getSettings().setJavaScriptEnabled(true); + webView.setWebViewClient(new OpenLinksExternallyWebClient()); + webView.loadDataWithBaseURL(null, htmlContent, "text/html", "utf-8", null); + + setContentView(webView); + } + + private class OpenLinksExternallyWebClient extends WebViewClient { + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + try { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + getContext().startActivity(intent); + } catch (Exception ex) { + Logger.printException(() -> "Open link failure", ex); + } + // Dismiss the about dialog using a delay, + // otherwise without a delay the UI looks hectic with the dialog dismissing + // to show the settings while simultaneously a web browser is opening. + Utils.runOnMainThreadDelayed(WebViewDialog.this::dismiss, 500); + return true; + } + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/WideListPreference.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/WideListPreference.java new file mode 100644 index 000000000..08bee7bf6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/WideListPreference.java @@ -0,0 +1,34 @@ +package app.revanced.extension.shared.settings.preference; + +import android.app.AlertDialog; +import android.content.Context; +import android.preference.ListPreference; +import android.util.AttributeSet; + +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings({"unused", "deprecation"}) +public class WideListPreference extends ListPreference { + + public WideListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public WideListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public WideListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public WideListPreference(Context context) { + super(context); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + Utils.setEditTextDialogTheme(builder, true); + super.onPrepareDialogBuilder(builder); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/YouTubeDataAPIDialogBuilder.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/YouTubeDataAPIDialogBuilder.java new file mode 100644 index 000000000..872183e97 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/YouTubeDataAPIDialogBuilder.java @@ -0,0 +1,61 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.app.Activity; +import android.graphics.Point; +import android.view.Display; +import android.view.Window; +import android.view.WindowManager; + +import app.revanced.extension.shared.utils.BaseThemeUtils; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +/** + * Used by YouTube and YouTube Music. + */ +public class YouTubeDataAPIDialogBuilder { + private static final String URL_CREATE_PROJECT = "https://console.cloud.google.com/projectcreate"; + private static final String URL_MARKET_PLACE = "https://console.cloud.google.com/marketplace/product/google/youtube.googleapis.com"; + + public static void showDialog(Activity mActivity) { + try { + final String backgroundColorHex = BaseThemeUtils.getBackgroundColorHexString(); + final String foregroundColorHex = BaseThemeUtils.getForegroundColorHexString(); + + final String htmlDialog = "" + + "

" + + String.format( + "", + backgroundColorHex, foregroundColorHex, foregroundColorHex) + + "

" + + str("revanced_return_youtube_username_youtube_data_api_v3_dialog_title") + + "

" + + String.format( + str("revanced_return_youtube_username_youtube_data_api_v3_dialog_message"), + URL_CREATE_PROJECT, + URL_MARKET_PLACE + ) + + "

"; + + Utils.runOnMainThreadNowOrLater(() -> { + WebViewDialog webViewDialog = new WebViewDialog(mActivity, htmlDialog); + webViewDialog.show(); + + final Window window = webViewDialog.getWindow(); + if (window == null) return; + Display display = mActivity.getWindowManager().getDefaultDisplay(); + Point size = new Point(); + display.getSize(size); + + WindowManager.LayoutParams params = window.getAttributes(); + params.height = (int) (size.y * 0.6); + + window.setAttributes(params); + }); + } catch (Exception ex) { + Logger.printException(() -> "dialogBuilder failure", ex); + } + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/sponsorblock/requests/SBRoutes.java b/extensions/shared/src/main/java/app/revanced/extension/shared/sponsorblock/requests/SBRoutes.java new file mode 100644 index 000000000..5e972d585 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/sponsorblock/requests/SBRoutes.java @@ -0,0 +1,20 @@ +package app.revanced.extension.shared.sponsorblock.requests; + +import static app.revanced.extension.shared.requests.Route.Method.GET; +import static app.revanced.extension.shared.requests.Route.Method.POST; + +import app.revanced.extension.shared.requests.Route; + +public class SBRoutes { + public static final Route IS_USER_VIP = new Route(GET, "/api/isUserVIP?userID={user_id}"); + public static final Route GET_SEGMENTS = new Route(GET, "/api/skipSegments?videoID={video_id}&categories={categories}"); + public static final Route VIEWED_SEGMENT = new Route(POST, "/api/viewedVideoSponsorTime?UUID={segment_id}"); + public static final Route GET_USER_STATS = new Route(GET, "/api/userInfo?userID={user_id}&values=[\"userID\",\"userName\",\"reputation\",\"segmentCount\",\"ignoredSegmentCount\",\"viewCount\",\"minutesSaved\"]"); + public static final Route CHANGE_USERNAME = new Route(POST, "/api/setUsername?userID={user_id}&username={username}"); + public static final Route SUBMIT_SEGMENTS = new Route(POST, "/api/skipSegments?userID={user_id}&videoID={video_id}&category={category}&startTime={start_time}&endTime={end_time}&videoDuration={duration}"); + public static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&type={type}"); + public static final Route VOTE_ON_SEGMENT_CATEGORY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&category={category}"); + + public SBRoutes() { + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/BaseThemeUtils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/BaseThemeUtils.java new file mode 100644 index 000000000..ffc80d19a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/BaseThemeUtils.java @@ -0,0 +1,73 @@ +package app.revanced.extension.shared.utils; + +import static app.revanced.extension.shared.utils.ResourceUtils.getColor; +import static app.revanced.extension.shared.utils.ResourceUtils.getColorIdentifier; + +import android.graphics.Color; + +@SuppressWarnings("unused") +public class BaseThemeUtils { + private static int themeValue = 1; + + /** + * Injection point. + */ + public static void setTheme(Enum value) { + final int newOrdinalValue = value.ordinal(); + if (themeValue != newOrdinalValue) { + themeValue = newOrdinalValue; + Logger.printDebug(() -> "Theme value: " + newOrdinalValue); + } + } + + public static boolean isDarkTheme() { + return themeValue == 1; + } + + public static String getColorHexString(int color) { + return String.format("#%06X", (0xFFFFFF & color)); + } + + /** + * Subclasses can override this and provide a themed color. + */ + public static int getLightColor() { + return Color.WHITE; + } + + /** + * Subclasses can override this and provide a themed color. + */ + public static int getDarkColor() { + return Color.BLACK; + } + + public static String getBackgroundColorHexString() { + return getColorHexString(getBackgroundColor()); + } + + public static String getForegroundColorHexString() { + return getColorHexString(getForegroundColor()); + } + + public static int getBackgroundColor() { + final String colorName = isDarkTheme() ? "yt_black1" : "yt_white1"; + final int colorIdentifier = getColorIdentifier(colorName); + if (colorIdentifier != 0) { + return getColor(colorName); + } else { + return isDarkTheme() ? getDarkColor() : getLightColor(); + } + } + + public static int getForegroundColor() { + final String colorName = isDarkTheme() ? "yt_white1" : "yt_black1"; + final int colorIdentifier = getColorIdentifier(colorName); + if (colorIdentifier != 0) { + return getColor(colorName); + } else { + return isDarkTheme() ? getLightColor() : getDarkColor(); + } + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/ByteTrieSearch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/ByteTrieSearch.java new file mode 100644 index 000000000..1708f567c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/ByteTrieSearch.java @@ -0,0 +1,50 @@ +package app.revanced.extension.shared.utils; + +import androidx.annotation.NonNull; + +import java.nio.charset.StandardCharsets; + +public final class ByteTrieSearch extends TrieSearch { + + private static final class ByteTrieNode extends TrieNode { + ByteTrieNode() { + super(); + } + + ByteTrieNode(char nodeCharacterValue) { + super(nodeCharacterValue); + } + + @Override + TrieNode createNode(char nodeCharacterValue) { + return new ByteTrieNode(nodeCharacterValue); + } + + @Override + char getCharValue(byte[] text, int index) { + return (char) text[index]; + } + + @Override + int getTextLength(byte[] text) { + return text.length; + } + } + + /** + * Helper method for the common usage of converting Strings to raw UTF-8 bytes. + */ + public static byte[][] convertStringsToBytes(String... strings) { + final int length = strings.length; + byte[][] replacement = new byte[length][]; + for (int i = 0; i < length; i++) { + replacement[i] = strings[i].getBytes(StandardCharsets.UTF_8); + } + return replacement; + } + + public ByteTrieSearch(@NonNull byte[]... patterns) { + super(new ByteTrieNode(), patterns); + } +} + diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Event.kt b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Event.kt new file mode 100644 index 000000000..a4f76152a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Event.kt @@ -0,0 +1,30 @@ +package app.revanced.extension.shared.utils + +/** + * generic event provider class + */ +class Event { + private val eventListeners = mutableSetOf<(T) -> Unit>() + + operator fun plusAssign(observer: (T) -> Unit) { + addObserver(observer) + } + + fun addObserver(observer: (T) -> Unit) { + eventListeners.add(observer) + } + + operator fun minusAssign(observer: (T) -> Unit) { + removeObserver(observer) + } + + private fun removeObserver(observer: (T) -> Unit) { + eventListeners.remove(observer) + } + + operator fun invoke(value: T) { + for (observer in eventListeners) + observer.invoke(value) + } +} + diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/IntentUtils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/IntentUtils.java new file mode 100644 index 000000000..6c15a67ee --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/IntentUtils.java @@ -0,0 +1,44 @@ +package app.revanced.extension.shared.utils; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class IntentUtils extends Utils { + + public static void launchExternalDownloader(@NonNull String content, @NonNull String downloaderPackageName) { + Intent intent = new Intent("android.intent.action.SEND"); + intent.setType("text/plain"); + intent.setPackage(downloaderPackageName); + intent.putExtra("android.intent.extra.TEXT", content); + launchIntent(intent); + } + + private static void launchIntent(@NonNull Intent intent) { + // If possible, use the main activity as the context. + // Otherwise fall back on using the application context. + Context mContext = getActivity(); + if (mContext == null) { + // Utils context is the application context, and not an activity context. + mContext = context; + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + mContext.startActivity(intent); + } + + public static void launchView(@NonNull String content) { + launchView(content, null); + } + + public static void launchView(@NonNull String content, @Nullable String packageName) { + Intent intent = new Intent("android.intent.action.VIEW", Uri.parse(content)); + if (packageName != null) { + intent.setPackage(packageName); + } + launchIntent(intent); + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Logger.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Logger.java new file mode 100644 index 000000000..38ac18660 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Logger.java @@ -0,0 +1,142 @@ +package app.revanced.extension.shared.utils; + +import static app.revanced.extension.shared.settings.BaseSettings.ENABLE_DEBUG_LOGGING; + +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.settings.BaseSettings; + +public class Logger { + + /** + * Log messages using lambdas. + */ + public interface LogMessage { + @NonNull + String buildMessageString(); + + /** + * @return For outer classes, this returns {@link Class#getSimpleName()}. + * For static, inner, or anonymous classes, this returns the simple name of the enclosing class. + *
+ * For example, each of these classes return 'SomethingView': + * + * com.company.SomethingView + * com.company.SomethingView$StaticClass + * com.company.SomethingView$1 + * + */ + default String findOuterClassSimpleName() { + Class selfClass = this.getClass(); + + String fullClassName = selfClass.getName(); + final int dollarSignIndex = fullClassName.indexOf('$'); + if (dollarSignIndex < 0) { + return selfClass.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); + } + } + + private static final String REVANCED_LOG_PREFIX = "Extended: "; + + /** + * Logs debug messages under the outer class name of the code calling this method. + * Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()} + * so the performance cost of building strings is paid only if {@link BaseSettings#ENABLE_DEBUG_LOGGING} is enabled. + */ + public static void printDebug(@NonNull LogMessage message) { + printDebug(message, null); + } + + /** + * Logs debug messages under the outer class name of the code calling this method. + * Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()} + * so the performance cost of building strings is paid only if {@link BaseSettings#ENABLE_DEBUG_LOGGING} is enabled. + */ + public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) { + if (ENABLE_DEBUG_LOGGING.get()) { + String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName(); + String logMessage = message.buildMessageString(); + + 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. + */ + public static void printInfo(@NonNull LogMessage message) { + printInfo(message, null); + } + + /** + * Logs information messages using the outer class name of the code calling this method. + */ + public static void printInfo(@NonNull LogMessage message, @Nullable Exception ex) { + String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName(); + String logMessage = message.buildMessageString(); + if (ex == null) { + Log.i(logTag, logMessage); + } else { + Log.i(logTag, logMessage, ex); + } + } + + /** + * Logs exceptions under the outer class name of the code calling this method. + */ + public static void printException(@NonNull LogMessage message) { + printException(message, null); + } + + /** + * Logs exceptions under the outer class name of the code calling this method. + *

+ * If the calling code is showing it's own error toast, + * instead use {@link #printInfo(LogMessage, Exception)} + * + * @param message log message + * @param ex exception (optional) + */ + public static void printException(@NonNull LogMessage message, @Nullable Throwable ex) { + String messageString = message.buildMessageString(); + String outerClassSimpleName = message.findOuterClassSimpleName(); + String logMessage = REVANCED_LOG_PREFIX + outerClassSimpleName; + if (ex == null) { + Log.e(logMessage, messageString); + } else { + Log.e(logMessage, messageString, ex); + } + } + + /** + * Logging to use if {@link BaseSettings#ENABLE_DEBUG_LOGGING} or {@link Utils#getContext()} may not be initialized. + * Normally this method should not be used. + */ + public static void initializationInfo(@NonNull Class callingClass, @NonNull String message) { + Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message); + } + + /** + * Logging to use if {@link BaseSettings#ENABLE_DEBUG_LOGGING} or {@link Utils#getContext()} may not be initialized. + * Normally this method should not be used. + */ + public static void initializationException(@NonNull Class callingClass, @NonNull String message, + @Nullable Exception ex) { + Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex); + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/PackageUtils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/PackageUtils.java new file mode 100644 index 000000000..1db1f137e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/PackageUtils.java @@ -0,0 +1,79 @@ +package app.revanced.extension.shared.utils; + +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class PackageUtils extends Utils { + + public static String getAppLabel() { + final PackageInfo packageInfo = getPackageInfo(); + if (packageInfo != null) { + final ApplicationInfo applicationInfo = packageInfo.applicationInfo; + if (applicationInfo != null && applicationInfo.loadLabel(getPackageManager()) instanceof String applicationLabel) { + return applicationLabel; + } + } + return ""; + } + + public static String getAppVersionName() { + final PackageInfo packageInfo = getPackageInfo(); + if (packageInfo != null) { + return packageInfo.versionName; + } else { + return ""; + } + } + + public static boolean isPackageEnabled(@NonNull String packageName) { + try { + return context.getPackageManager().getApplicationInfo(packageName, 0).enabled; + } catch (PackageManager.NameNotFoundException ignored) { + } + + return false; + } + + public static boolean isTablet() { + return getSmallestScreenWidthDp() >= 600; + } + + public static int getSmallestScreenWidthDp() { + return context.getResources().getConfiguration().smallestScreenWidthDp; + } + + // utils + @Nullable + private static PackageInfo getPackageInfo() { + try { + final PackageManager packageManager = getPackageManager(); + final String packageName = context.getPackageName(); + return isSDKAbove(33) + ? packageManager.getPackageInfo(packageName, PackageManager.PackageInfoFlags.of(0)) + : packageManager.getPackageInfo(packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + Logger.printException(() -> "Failed to get package Info!" + e); + } + return null; + } + + @NonNull + private static PackageManager getPackageManager() { + return context.getPackageManager(); + } + + public static boolean isVersionToLessThan(@NonNull String compareVersion, @NonNull String targetVersion) { + try { + final int compareVersionNumber = Integer.parseInt(compareVersion.replaceAll("\\.", "")); + final int targetVersionNumber = Integer.parseInt(targetVersion.replaceAll("\\.", "")); + return compareVersionNumber < targetVersionNumber; + } catch (NumberFormatException ex) { + Logger.printException(() -> "Failed to compare version: " + compareVersion + ", " + targetVersion, ex); + } + return false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/ResourceUtils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/ResourceUtils.java new file mode 100644 index 000000000..55b7c1ac6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/ResourceUtils.java @@ -0,0 +1,186 @@ +package app.revanced.extension.shared.utils; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; + +import androidx.annotation.NonNull; + +/** + * @noinspection ALL + */ +public class ResourceUtils extends Utils { + + private ResourceUtils() { + } // utility class + + public static int getIdentifier(@NonNull String str, @NonNull ResourceType resourceType) { + return getIdentifier(str, resourceType, getContext()); + } + + public static int getIdentifier(@NonNull String str, @NonNull ResourceType resourceType, + @NonNull Context context) { + return getResources().getIdentifier(str, resourceType.getType(), context.getPackageName()); + } + + public static int getAnimIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.ANIM); + } + + public static int getArrayIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.ARRAY); + } + + public static int getAttrIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.ATTR); + } + + public static int getColorIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.COLOR); + } + + public static int getDimenIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.DIMEN); + } + + public static int getDrawableIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.DRAWABLE); + } + + public static int getFontIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.FONT); + } + + public static int getIdIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.ID); + } + + public static int getIntegerIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.INTEGER); + } + + public static int getLayoutIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.LAYOUT); + } + + public static int getMenuIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.MENU); + } + + public static int getMipmapIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.MIPMAP); + } + + public static int getRawIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.RAW); + } + + public static int getStringIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.STRING); + } + + public static int getStyleIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.STYLE); + } + + public static int getXmlIdentifier(@NonNull String str) { + return getIdentifier(str, ResourceType.XML); + } + + public static Animation getAnimation(@NonNull String str) { + int identifier = getAnimIdentifier(str); + if (identifier == 0) { + handleException(str, ResourceType.ANIM); + identifier = android.R.anim.fade_in; + } + return AnimationUtils.loadAnimation(getContext(), identifier); + } + + public static int getColor(@NonNull String str) { + final int identifier = getColorIdentifier(str); + if (identifier == 0) { + handleException(str, ResourceType.COLOR); + return 0; + } + return getResources().getColor(identifier); + } + + public static int getDimension(@NonNull String str) { + final int identifier = getDimenIdentifier(str); + if (identifier == 0) { + handleException(str, ResourceType.DIMEN); + return 0; + } + return getResources().getDimensionPixelSize(identifier); + } + + public static Drawable getDrawable(@NonNull String str) { + final int identifier = getDrawableIdentifier(str); + if (identifier == 0) { + handleException(str, ResourceType.DRAWABLE); + return null; + } + return getResources().getDrawable(identifier); + } + + public static String getString(@NonNull String str) { + final int identifier = getStringIdentifier(str); + if (identifier == 0) { + handleException(str, ResourceType.STRING); + return str; + } + return getResources().getString(identifier); + } + + public static String[] getStringArray(@NonNull String str) { + final int identifier = getArrayIdentifier(str); + if (identifier == 0) { + handleException(str, ResourceType.ARRAY); + return new String[0]; + } + return getResources().getStringArray(identifier); + } + + public static int getInteger(@NonNull String str) { + final int identifier = getIntegerIdentifier(str); + if (identifier == 0) { + handleException(str, ResourceType.INTEGER); + return 0; + } + return getResources().getInteger(identifier); + } + + private static void handleException(@NonNull String str, ResourceType resourceType) { + Logger.printException(() -> "R." + resourceType.getType() + "." + str + " is null"); + } + + public enum ResourceType { + ANIM("anim"), + ARRAY("array"), + ATTR("attr"), + COLOR("color"), + DIMEN("dimen"), + DRAWABLE("drawable"), + FONT("font"), + ID("id"), + INTEGER("integer"), + LAYOUT("layout"), + MENU("menu"), + MIPMAP("mipmap"), + RAW("raw"), + STRING("string"), + STYLE("style"), + XML("xml"); + + private final String type; + + ResourceType(String type) { + this.type = type; + } + + public final String getType() { + return type; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/StringRef.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/StringRef.java new file mode 100644 index 000000000..f51b49ed0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/StringRef.java @@ -0,0 +1,135 @@ +package app.revanced.extension.shared.utils; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.Resources; + +import androidx.annotation.NonNull; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +@SuppressLint("DiscouragedApi") +public class StringRef extends Utils { + private static Resources resources; + + // must use a thread safe map, as this class is used both on and off the main thread + private static final Map strings = Collections.synchronizedMap(new HashMap<>()); + + /** + * Returns a cached instance. + * Should be used if the same String could be loaded more than once. + * + * @param id string resource name/id + * @see #sf(String) + */ + @NonNull + public static StringRef sfc(@NonNull String id) { + StringRef ref = strings.get(id); + if (ref == null) { + ref = new StringRef(id); + strings.put(id, ref); + } + return ref; + } + + /** + * Creates a new instance, but does not cache the value. + * Should be used for Strings that are loaded exactly once. + * + * @param id string resource name/id + * @see #sfc(String) + */ + @NonNull + public static StringRef sf(@NonNull String id) { + return new StringRef(id); + } + + /** + * Gets string value by string id, shorthand for sfc(id).toString() + * + * @param id string resource name/id + * @return String value from string.xml + */ + @NonNull + public static String str(@NonNull String id) { + return sfc(id).toString(); + } + + /** + * Gets string value by string id, shorthand for sfc(id).toString() and formats the string + * with given args. + * + * @param id string resource name/id + * @param args the args to format the string with + * @return String value from string.xml formatted with given args + */ + @NonNull + public static String str(@NonNull String id, Object... args) { + return String.format(str(id), args); + } + + /** + * Creates a StringRef object that'll not change it's value + * + * @param value value which toString() method returns when invoked on returned object + * @return Unique StringRef instance, its value will never change + */ + @NonNull + public static StringRef constant(@NonNull String value) { + final StringRef ref = new StringRef(value); + ref.resolved = true; + return ref; + } + + /** + * Shorthand for constant("") + * Its value always resolves to empty string + */ + @SuppressLint("StaticFieldLeak") + @NonNull + public static final StringRef empty = constant(""); + + @NonNull + private String value; + private boolean resolved; + + public StringRef(@NonNull String resName) { + this.value = resName; + } + + @Override + @NonNull + public String toString() { + if (!resolved) { + try { + Context context = getContext(); + if (resources == null) { + resources = getResources(); + } + if (resources != null) { + value = ResourceUtils.getString(value); + resolved = true; + return value; + } + resources = context.getResources(); + if (resources != null) { + final String packageName = context.getPackageName(); + final int identifier = resources.getIdentifier(value, "string", packageName); + if (identifier == 0) + Logger.printException(() -> "Resource not found: " + value); + else + value = resources.getString(identifier); + resolved = true; + } else { + Logger.printException(() -> "Could not resolve resources!"); + } + } catch (Exception ex) { + Logger.initializationException(StringRef.class, "Context is null!", ex); + } + } + + return value; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/StringTrieSearch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/StringTrieSearch.java new file mode 100644 index 000000000..e4df4a57b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/StringTrieSearch.java @@ -0,0 +1,38 @@ +package app.revanced.extension.shared.utils; + +import androidx.annotation.NonNull; + +/** + * Text pattern searching using a prefix tree (trie). + */ +public final class StringTrieSearch extends TrieSearch { + + private static final class StringTrieNode extends TrieNode { + StringTrieNode() { + super(); + } + + StringTrieNode(char nodeCharacterValue) { + super(nodeCharacterValue); + } + + @Override + TrieNode createNode(char nodeValue) { + return new StringTrieNode(nodeValue); + } + + @Override + char getCharValue(String text, int index) { + return text.charAt(index); + } + + @Override + int getTextLength(String text) { + return text.length(); + } + } + + public StringTrieSearch(@NonNull String... patterns) { + super(new StringTrieNode(), patterns); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/TrieSearch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/TrieSearch.java new file mode 100644 index 000000000..01ecf28c5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/TrieSearch.java @@ -0,0 +1,416 @@ +package app.revanced.extension.shared.utils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * Searches for a group of different patterns using a trie (prefix tree). + * Can significantly speed up searching for multiple patterns. + */ +public abstract class TrieSearch { + + public interface TriePatternMatchedCallback { + /** + * Called when a pattern is matched. + * + * @param textSearched Text that was searched. + * @param matchedStartIndex Start index of the search text, where the pattern was matched. + * @param matchedLength Length of the match. + * @param callbackParameter Optional parameter passed into {@link TrieSearch#matches(Object, Object)}. + * @return True, if the search should stop here. + * If false, searching will continue to look for other matches. + */ + boolean patternMatched(T textSearched, int matchedStartIndex, int matchedLength, Object callbackParameter); + } + + /** + * Represents a compressed tree path for a single pattern that shares no sibling nodes. + *

+ * For example, if a tree contains the patterns: "foobar", "football", "feet", + * it would contain 3 compressed paths of: "bar", "tball", "eet". + *

+ * And the tree would contain children arrays only for the first level containing 'f', + * the second level containing 'o', + * and the third level containing 'o'. + *

+ * This is done to reduce memory usage, which can be substantial if many long patterns are used. + */ + private static final class TrieCompressedPath { + final T pattern; + final int patternStartIndex; + final int patternLength; + final TriePatternMatchedCallback callback; + + TrieCompressedPath(T pattern, int patternStartIndex, int patternLength, TriePatternMatchedCallback callback) { + this.pattern = pattern; + this.patternStartIndex = patternStartIndex; + this.patternLength = patternLength; + this.callback = callback; + } + + boolean matches(TrieNode enclosingNode, // Used only for the get character method. + T searchText, int searchTextLength, int searchTextIndex, Object callbackParameter) { + if (searchTextLength - searchTextIndex < patternLength - patternStartIndex) { + return false; // Remaining search text is shorter than the remaining leaf pattern and they cannot match. + } + for (int i = searchTextIndex, j = patternStartIndex; j < patternLength; i++, j++) { + if (enclosingNode.getCharValue(searchText, i) != enclosingNode.getCharValue(pattern, j)) { + return false; + } + } + return callback == null || callback.patternMatched(searchText, + searchTextIndex - patternStartIndex, patternLength, callbackParameter); + } + } + + static abstract class TrieNode { + /** + * Dummy value used for root node. Value can be anything as it's never referenced. + */ + private static final char ROOT_NODE_CHARACTER_VALUE = 0; // ASCII null character. + + /** + * How much to expand the children array when resizing. + */ + private static final int CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT = 2; + + /** + * Character this node represents. + * This field is ignored for the root node (which does not represent any character). + */ + private final char nodeValue; + + /** + * A compressed graph path that represents the remaining pattern characters of a single child node. + *

+ * If present then child array is always null, although callbacks for other + * end of patterns can also exist on this same node. + */ + @Nullable + private TrieCompressedPath leaf; + + /** + * All child nodes. Only present if no compressed leaf exist. + *

+ * Array is dynamically increased in size as needed, + * and uses perfect hashing for the elements it contains. + *

+ * So if the array contains a given character, + * the character will always map to the node with index: (character % arraySize). + *

+ * Elements not contained can collide with elements the array does contain, + * so must compare the nodes character value. + *

+ * Alternatively this array could be a sorted and densely packed array, + * and lookup is done using binary search. + * That would save a small amount of memory because there's no null children entries, + * but would give a worst case search of O(nlog(m)) where n is the number of + * characters in the searched text and m is the maximum size of the sorted character arrays. + * Using a hash table array always gives O(n) search time. + * The memory usage here is very small (all Litho filters use ~10KB of memory), + * so the more performant hash implementation is chosen. + */ + @Nullable + private TrieNode[] children; + + /** + * Callbacks for all patterns that end at this node. + */ + @Nullable + private List> endOfPatternCallback; + + TrieNode() { + this.nodeValue = ROOT_NODE_CHARACTER_VALUE; + } + + TrieNode(char nodeCharacterValue) { + this.nodeValue = nodeCharacterValue; + } + + /** + * @param pattern Pattern to add. + * @param patternIndex Current recursive index of the pattern. + * @param patternLength Length of the pattern. + * @param callback Callback, where a value of NULL indicates to always accept a pattern match. + */ + private void addPattern(@NonNull T pattern, int patternIndex, int patternLength, + @Nullable TriePatternMatchedCallback callback) { + if (patternIndex == patternLength) { // Reached the end of the pattern. + if (endOfPatternCallback == null) { + endOfPatternCallback = new ArrayList<>(1); + } + endOfPatternCallback.add(callback); + return; + } + if (leaf != null) { + // Reached end of the graph and a leaf exist. + // Recursively call back into this method and push the existing leaf down 1 level. + if (children != null) throw new IllegalStateException(); + //noinspection unchecked + children = new TrieNode[1]; + TrieCompressedPath temp = leaf; + leaf = null; + addPattern(temp.pattern, temp.patternStartIndex, temp.patternLength, temp.callback); + // Continue onward and add the parameter pattern. + } else if (children == null) { + leaf = new TrieCompressedPath<>(pattern, patternIndex, patternLength, callback); + return; + } + final char character = getCharValue(pattern, patternIndex); + final int arrayIndex = hashIndexForTableSize(children.length, character); + TrieNode child = children[arrayIndex]; + if (child == null) { + child = createNode(character); + children[arrayIndex] = child; + } else if (child.nodeValue != character) { + // Hash collision. Resize the table until perfect hashing is found. + child = createNode(character); + expandChildArray(child); + } + child.addPattern(pattern, patternIndex + 1, patternLength, callback); + } + + /** + * Resizes the children table until all nodes hash to exactly one array index. + */ + private void expandChildArray(TrieNode child) { + int replacementArraySize = Objects.requireNonNull(children).length; + while (true) { + replacementArraySize += CHILDREN_ARRAY_INCREASE_SIZE_INCREMENT; + //noinspection unchecked + TrieNode[] replacement = new TrieNode[replacementArraySize]; + addNodeToArray(replacement, child); + boolean collision = false; + for (TrieNode existingChild : children) { + if (existingChild != null) { + if (!addNodeToArray(replacement, existingChild)) { + collision = true; + break; + } + } + } + if (collision) { + continue; + } + children = replacement; + return; + } + } + + private static boolean addNodeToArray(TrieNode[] array, TrieNode childToAdd) { + final int insertIndex = hashIndexForTableSize(array.length, childToAdd.nodeValue); + if (array[insertIndex] != null) { + return false; // Collision. + } + array[insertIndex] = childToAdd; + return true; + } + + private static int hashIndexForTableSize(int arraySize, char nodeValue) { + return nodeValue % arraySize; + } + + /** + * This method is static and uses a loop to avoid all recursion. + * This is done for performance since the JVM does not optimize tail recursion. + * + * @param startNode Node to start the search from. + * @param searchText Text to search for patterns in. + * @param searchTextIndex Start index, inclusive. + * @param searchTextEndIndex End index, exclusive. + * @return If any pattern matches, and it's associated callback halted the search. + */ + private static boolean matches(final TrieNode startNode, final T searchText, + int searchTextIndex, final int searchTextEndIndex, + final Object callbackParameter) { + TrieNode node = startNode; + int currentMatchLength = 0; + + while (true) { + TrieCompressedPath leaf = node.leaf; + if (leaf != null && leaf.matches(startNode, searchText, searchTextEndIndex, searchTextIndex, callbackParameter)) { + return true; // Leaf exists and it matched the search text. + } + List> endOfPatternCallback = node.endOfPatternCallback; + if (endOfPatternCallback != null) { + final int matchStartIndex = searchTextIndex - currentMatchLength; + for (@Nullable TriePatternMatchedCallback callback : endOfPatternCallback) { + if (callback == null) { + return true; // No callback and all matches are valid. + } + if (callback.patternMatched(searchText, matchStartIndex, currentMatchLength, callbackParameter)) { + return true; // Callback confirmed the match. + } + } + } + TrieNode[] children = node.children; + if (children == null) { + return false; // Reached a graph end point and there's no further patterns to search. + } + if (searchTextIndex == searchTextEndIndex) { + return false; // Reached end of the search text and found no matches. + } + + // Use the start node to reduce VM method lookup, since all nodes are the same class type. + final char character = startNode.getCharValue(searchText, searchTextIndex); + final int arrayIndex = hashIndexForTableSize(children.length, character); + TrieNode child = children[arrayIndex]; + if (child == null || child.nodeValue != character) { + return false; + } + + node = child; + searchTextIndex++; + currentMatchLength++; + } + } + + /** + * Gives an approximate memory usage. + * + * @return Estimated number of memory pointers used, starting from this node and including all children. + */ + private int estimatedNumberOfPointersUsed() { + int numberOfPointers = 4; // Number of fields in this class. + if (leaf != null) { + numberOfPointers += 4; // Number of fields in leaf node. + } + if (endOfPatternCallback != null) { + numberOfPointers += endOfPatternCallback.size(); + } + if (children != null) { + numberOfPointers += children.length; + for (TrieNode child : children) { + if (child != null) { + numberOfPointers += child.estimatedNumberOfPointersUsed(); + } + } + } + return numberOfPointers; + } + + abstract TrieNode createNode(char nodeValue); + + abstract char getCharValue(T text, int index); + + abstract int getTextLength(T text); + } + + /** + * Root node, and it's children represent the first pattern characters. + */ + private final TrieNode root; + + /** + * Patterns to match. + */ + private final List patterns = new ArrayList<>(); + + @SafeVarargs + TrieSearch(@NonNull TrieNode root, @NonNull T... patterns) { + this.root = Objects.requireNonNull(root); + addPatterns(patterns); + } + + @SafeVarargs + public final void addPatterns(@NonNull T... patterns) { + for (T pattern : patterns) { + addPattern(pattern); + } + } + + /** + * Adds a pattern that will always return a positive match if found. + * + * @param pattern Pattern to add. Calling this with a zero length pattern does nothing. + */ + public void addPattern(@NonNull T pattern) { + addPattern(pattern, root.getTextLength(pattern), null); + } + + /** + * @param pattern Pattern to add. Calling this with a zero length pattern does nothing. + * @param callback Callback to determine if searching should halt when a match is found. + */ + public void addPattern(@NonNull T pattern, @NonNull TriePatternMatchedCallback callback) { + addPattern(pattern, root.getTextLength(pattern), Objects.requireNonNull(callback)); + } + + void addPattern(@NonNull T pattern, int patternLength, @Nullable TriePatternMatchedCallback callback) { + if (patternLength == 0) return; // Nothing to match + + patterns.add(pattern); + root.addPattern(pattern, 0, patternLength, callback); + } + + public final boolean matches(@NonNull T textToSearch) { + return matches(textToSearch, 0); + } + + public boolean matches(@NonNull T textToSearch, @NonNull Object callbackParameter) { + return matches(textToSearch, 0, root.getTextLength(textToSearch), + Objects.requireNonNull(callbackParameter)); + } + + public boolean matches(@NonNull T textToSearch, int startIndex) { + return matches(textToSearch, startIndex, root.getTextLength(textToSearch)); + } + + public final boolean matches(@NonNull T textToSearch, int startIndex, int endIndex) { + return matches(textToSearch, startIndex, endIndex, null); + } + + /** + * Searches through text, looking for any substring that matches any pattern in this tree. + * + * @param textToSearch Text to search through. + * @param startIndex Index to start searching, inclusive value. + * @param endIndex Index to stop matching, exclusive value. + * @param callbackParameter Optional parameter passed to the callbacks. + * @return If any pattern matched, and it's callback halted searching. + */ + public boolean matches(@NonNull T textToSearch, int startIndex, int endIndex, @Nullable Object callbackParameter) { + return matches(textToSearch, root.getTextLength(textToSearch), startIndex, endIndex, callbackParameter); + } + + private boolean matches(@NonNull T textToSearch, int textToSearchLength, int startIndex, int endIndex, + @Nullable Object callbackParameter) { + if (endIndex > textToSearchLength) { + throw new IllegalArgumentException("endIndex: " + endIndex + + " is greater than texToSearchLength: " + textToSearchLength); + } + if (patterns.isEmpty()) { + return false; // No patterns were added. + } + for (int i = startIndex; i < endIndex; i++) { + if (TrieNode.matches(root, textToSearch, i, endIndex, callbackParameter)) return true; + } + return false; + } + + /** + * @return Estimated memory size (in kilobytes) of this instance. + */ + public int getEstimatedMemorySize() { + if (patterns.isEmpty()) { + return 0; + } + // Assume the device has less than 32GB of ram (and can use pointer compression), + // or the device is 32-bit. + final int numberOfBytesPerPointer = 4; + return (int) Math.ceil((numberOfBytesPerPointer * root.estimatedNumberOfPointersUsed()) / 1024.0); + } + + public int numberOfPatterns() { + return patterns.size(); + } + + public List getPatterns() { + return Collections.unmodifiableList(patterns); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java new file mode 100644 index 000000000..d8e6a0afc --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java @@ -0,0 +1,756 @@ +package app.revanced.extension.shared.utils; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Fragment; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; +import android.preference.Preference; +import android.preference.PreferenceGroup; +import android.preference.PreferenceScreen; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.Toast; +import android.widget.Toolbar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; +import java.text.Bidi; +import java.time.Duration; +import java.util.Locale; +import java.util.Objects; +import java.util.SortedMap; +import java.util.TreeMap; +import java.util.concurrent.Callable; +import java.util.concurrent.Future; +import java.util.concurrent.SynchronousQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import app.revanced.extension.shared.settings.BooleanSetting; +import kotlin.text.Regex; + +@SuppressWarnings("deprecation") +public class Utils { + + private static WeakReference activityRef = new WeakReference<>(null); + + @SuppressLint("StaticFieldLeak") + public static Context context; + + private static Resources resources; + + protected Utils() { + } // utility class + + public static void clickView(View view) { + if (view == null) return; + view.callOnClick(); + } + + /** + * Hide a view by setting its layout height and width to 1dp. + * + * @param condition The setting to check for hiding the view. + * @param view The view to hide. + */ + public static void hideViewBy0dpUnderCondition(BooleanSetting condition, View view) { + hideViewBy0dpUnderCondition(condition.get(), view); + } + + public static void hideViewBy0dpUnderCondition(boolean enabled, View view) { + if (!enabled) return; + + hideViewByLayoutParams(view); + } + + /** + * Hide a view by setting its visibility to GONE. + * + * @param condition The setting to check for hiding the view. + * @param view The view to hide. + */ + public static void hideViewUnderCondition(BooleanSetting condition, View view) { + hideViewUnderCondition(condition.get(), view); + } + + /** + * Hide a view by setting its visibility to GONE. + * + * @param condition The setting to check for hiding the view. + * @param view The view to hide. + */ + public static void hideViewUnderCondition(boolean condition, View view) { + if (!condition) return; + if (view == null) return; + + view.setVisibility(View.GONE); + } + + @SuppressWarnings("unused") + public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting condition, View view) { + hideViewByRemovingFromParentUnderCondition(condition.get(), view); + } + + public static void hideViewByRemovingFromParentUnderCondition(boolean condition, View view) { + if (!condition) return; + if (view == null) return; + if (!(view.getParent() instanceof ViewGroup viewGroup)) + return; + + viewGroup.removeView(view); + } + + /** + * General purpose pool for network calls and other background tasks. + * All tasks run at max thread priority. + */ + private static final ThreadPoolExecutor backgroundThreadPool = new ThreadPoolExecutor( + 3, // 3 threads always ready to go + Integer.MAX_VALUE, + 10, // For any threads over the minimum, keep them alive 10 seconds after they go idle + TimeUnit.SECONDS, + new SynchronousQueue<>(), + r -> { // ThreadFactory + Thread t = new Thread(r); + t.setPriority(Thread.MAX_PRIORITY); // run at max priority + return t; + }); + + public static void runOnBackgroundThread(@NonNull Runnable task) { + backgroundThreadPool.execute(task); + } + + @NonNull + public static Future submitOnBackgroundThread(@NonNull Callable call) { + return backgroundThreadPool.submit(call); + } + + + public static boolean containsAny(@NonNull String value, @NonNull String... targets) { + return indexOfFirstFound(value, targets) >= 0; + } + + public static int indexOfFirstFound(@NonNull String value, @NonNull String... targets) { + for (String string : targets) { + if (!string.isEmpty()) { + final int indexOf = value.indexOf(string); + if (indexOf >= 0) return indexOf; + } + } + return -1; + } + + public interface MatchFilter { + boolean matches(T object); + } + + public static R getChildView(@NonNull Activity activity, @NonNull String str) { + final View decorView = activity.getWindow().getDecorView(); + return getChildView(decorView, str); + } + + /** + * @noinspection unchecked + */ + public static R getChildView(@NonNull View view, @NonNull String str) { + view = view.findViewById(ResourceUtils.getIdIdentifier(str)); + if (view != null) { + return (R) view; + } else { + throw new IllegalArgumentException("View with name" + str + " not found"); + } + } + + /** + * @param searchRecursively If children ViewGroups should also be + * recursively searched using depth first search. + * @return The first child view that matches the filter. + */ + @Nullable + public static T getChildView(@NonNull ViewGroup viewGroup, boolean searchRecursively, + @NonNull MatchFilter filter) { + for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) { + View childAt = viewGroup.getChildAt(i); + if (filter.matches(childAt)) { + //noinspection unchecked + return (T) childAt; + } + // Must do recursive after filter check, in case the filter is looking for a ViewGroup. + if (searchRecursively && childAt instanceof ViewGroup) { + T match = getChildView((ViewGroup) childAt, true, filter); + if (match != null) return match; + } + } + return null; + } + + /** + * @return The first child view that matches the filter. + * @noinspection rawtypes, unchecked + */ + @Nullable + public static T getChildView(@NonNull ViewGroup viewGroup, @NonNull MatchFilter filter) { + for (int i = 0, childCount = viewGroup.getChildCount(); i < childCount; i++) { + View childAt = viewGroup.getChildAt(i); + if (filter.matches(childAt)) { + return (T) childAt; + } + } + return null; + } + + @Nullable + public static ViewParent getParentView(@NonNull View view, int nthParent) { + ViewParent parent = view.getParent(); + + int currentDepth = 0; + while (++currentDepth < nthParent && parent != null) { + parent = parent.getParent(); + } + + if (currentDepth == nthParent) { + return parent; + } + + final int finalDepthLog = currentDepth; + final ViewParent finalParent = parent; + Logger.printDebug(() -> "Could not find parent view of depth: " + nthParent + + " and instead found at: " + finalDepthLog + " view: " + finalParent); + return null; + } + + public static void restartApp(@NonNull Context mContext) { + String packageName = mContext.getPackageName(); + Intent intent = mContext.getPackageManager().getLaunchIntentForPackage(packageName); + if (intent == null) return; + Intent mainIntent = Intent.makeRestartActivityTask(intent.getComponent()); + // Required for API 34 and later + // Ref: https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents + mainIntent.setPackage(packageName); + if (mContext instanceof Activity mActivity) { + mActivity.finishAndRemoveTask(); + } + mContext.startActivity(mainIntent); + System.runFinalizersOnExit(true); + System.exit(0); + } + + public static Activity getActivity() { + return activityRef.get(); + } + + public static Context getContext() { + if (context == null) { + Logger.initializationException(Utils.class, "Context is null, returning null!", null); + } + return context; + } + + public static Resources getResources() { + if (resources == null) { + return getLocalizedContextAndSetResources(getContext()).getResources(); + } else { + return resources; + } + } + + /** + * Compare MainActivity's Locale and Context's Locale. + * If the Locale of MainActivity and the Locale of Context are different, the Locale of MainActivity is applied. + *

+ * If Locale changes, resources should also change and be saved locally. + * Otherwise, {@link ResourceUtils#getString(String)} will be updated to the incorrect language. + * + * @param mContext Context to check locale. + * @return Context with locale applied. + */ + public static Context getLocalizedContextAndSetResources(Context mContext) { + Activity mActivity = activityRef.get(); + if (mActivity == null) { + return mContext; + } + + // Locale of MainActivity. + Locale applicationLocale; + + // Locale of Context. + Locale contextLocale; + + if (isSDKAbove(24)) { + applicationLocale = mActivity.getResources().getConfiguration().getLocales().get(0); + contextLocale = mContext.getResources().getConfiguration().getLocales().get(0); + } else { + applicationLocale = mActivity.getResources().getConfiguration().locale; + contextLocale = mContext.getResources().getConfiguration().locale; + } + + // If they are identical, no need to override them. + if (applicationLocale == contextLocale) { + resources = mActivity.getResources(); + return mContext; + } + + // If they are different, overrides the Locale of the Context and resource. + Locale.setDefault(applicationLocale); + Configuration configuration = new Configuration(mContext.getResources().getConfiguration()); + configuration.setLocale(applicationLocale); + Context localizedContext = mContext.createConfigurationContext(configuration); + resources = localizedContext.getResources(); + return localizedContext; + } + + public static void setActivity(Activity mainActivity) { + activityRef = new WeakReference<>(mainActivity); + } + + public static void setContext(@Nullable Context appContext) { + // Typically, Context is invoked in the constructor method, so it is not null. + // Since some are invoked from methods other than the constructor method, + // it may be necessary to check whether Context is null. + if (appContext == null) { + return; + } + + context = appContext; + + // In some apps like TikTok, the Setting classes can load in weird orders due to cyclic class dependencies. + // Calling the regular printDebug method here can cause a Settings context null pointer exception, + // even though the context is already set before the call. + // + // The initialization logger methods do not directly or indirectly + // reference the Context or any Settings and are unaffected by this problem. + // + // Info level also helps debug if a patch hook is called before + // the context is set since debug logging is off by default. + Logger.initializationInfo(Utils.class, "Set context: " + appContext); + } + + public static void setClipboard(@NonNull String text) { + setClipboard(text, null); + } + + public static void setClipboard(@NonNull String text, @Nullable String toastMessage) { + if (!(context.getSystemService(Context.CLIPBOARD_SERVICE) instanceof ClipboardManager clipboard)) + return; + android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text); + clipboard.setPrimaryClip(clip); + + // Do not show a toast if using Android 13+ as it shows it's own toast. + // But if the user copied with a timestamp then show a toast. + // Unfortunately this will show 2 toasts on Android 13+, but no way around this. + if (isSDKAbove(33) || toastMessage == null) return; + showToastShort(toastMessage); + } + + public static String getFormattedTimeStamp(long videoTime) { + return "'" + videoTime + + "' (" + + getTimeStamp(videoTime) + + ")\n"; + } + + @SuppressLint("DefaultLocale") + public static String getTimeStamp(long time) { + long hours; + long minutes; + long seconds; + + if (isSDKAbove(26)) { + final Duration duration = Duration.ofMillis(time); + + hours = duration.toHours(); + minutes = duration.toMinutes() % 60; + seconds = duration.getSeconds() % 60; + } else { + final long currentVideoTimeInSeconds = time / 1000; + + hours = currentVideoTimeInSeconds / (60 * 60); + minutes = (currentVideoTimeInSeconds / 60) % 60; + seconds = currentVideoTimeInSeconds % 60; + } + + if (hours > 0) { + return String.format("%02d:%02d:%02d", hours, minutes, seconds); + } else { + return String.format("%02d:%02d", minutes, seconds); + } + } + + public static void setEditTextDialogTheme(final AlertDialog.Builder builder) { + setEditTextDialogTheme(builder, false); + } + + /** + * If {@link Fragment} uses [Android library] rather than [AndroidX library], + * the Dialog theme corresponding to [Android library] should be used. + *

+ * If not, the following issues will occur: + * ReVanced/revanced-patches#3061 + *

+ * To prevent these issues, apply the Dialog theme corresponding to [Android library]. + * + * @param builder Alertdialog builder to apply theme to. + * When used in a method containing an override, it must be called before 'super'. + * @param maxWidth Whether to use alertdialog as max width. + * It is used when there is a lot of content to show, such as an import/export dialog. + */ + public static void setEditTextDialogTheme(final AlertDialog.Builder builder, boolean maxWidth) { + final String styleIdentifier = maxWidth + ? "revanced_edit_text_dialog_max_width_style" + : "revanced_edit_text_dialog_style"; + final int editTextDialogStyle = ResourceUtils.getStyleIdentifier(styleIdentifier); + if (editTextDialogStyle != 0) { + builder.getContext().setTheme(editTextDialogStyle); + } + } + + public static AlertDialog.Builder getEditTextDialogBuilder(final Context context) { + return getEditTextDialogBuilder(context, false); + } + + public static AlertDialog.Builder getEditTextDialogBuilder(final Context context, boolean maxWidth) { + final AlertDialog.Builder builder = new AlertDialog.Builder(context); + setEditTextDialogTheme(builder, maxWidth); + return builder; + } + + @Nullable + private static Boolean isRightToLeftTextLayout; + + /** + * If the device language uses right to left text layout (hebrew, arabic, etc) + */ + public static boolean isRightToLeftTextLayout() { + if (isRightToLeftTextLayout == null) { + String displayLanguage = Locale.getDefault().getDisplayLanguage(); + isRightToLeftTextLayout = new Bidi(displayLanguage, Bidi.DIRECTION_DEFAULT_LEFT_TO_RIGHT).isRightToLeft(); + } + return isRightToLeftTextLayout; + } + + /** + * @return if the text contains at least 1 number character, + * including any unicode numbers such as Arabic. + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public static boolean containsNumber(@NonNull CharSequence text) { + for (int index = 0, length = text.length(); index < length; ) { + final int codePoint = Character.codePointAt(text, index); + if (Character.isDigit(codePoint)) { + return true; + } + index += Character.charCount(codePoint); + } + + return false; + } + + public static boolean isDarkModeEnabled() { + return isDarkModeEnabled(context); + } + + public static boolean isDarkModeEnabled(Context context) { + Configuration config = context.getResources().getConfiguration(); + final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK; + return currentNightMode == Configuration.UI_MODE_NIGHT_YES; + } + + /** + * @return whether the device's API level is higher than a specific SDK version. + */ + public static boolean isSDKAbove(int sdk) { + return Build.VERSION.SDK_INT >= sdk; + } + + public static int dpToPx(float dp) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); + } + + public static int dpToPx(int dp) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); + } + + /** + * Safe to call from any thread + */ + public static void showToastShort(@NonNull String messageToToast) { + showToast(messageToToast, Toast.LENGTH_SHORT); + } + + /** + * Safe to call from any thread + */ + public static void showToastLong(@NonNull String messageToToast) { + showToast(messageToToast, Toast.LENGTH_LONG); + } + + private static void showToast(@NonNull String messageToToast, int toastDuration) { + Objects.requireNonNull(messageToToast); + runOnMainThreadNowOrLater(() -> { + if (context == null) { + Logger.initializationException(Utils.class, "Cannot show toast (context is null): " + messageToToast, null); + } else { + Logger.printDebug(() -> "Showing toast: " + messageToToast); + Toast.makeText(context, messageToToast, toastDuration).show(); + } + } + ); + } + + /** + * Automatically logs any exceptions the runnable throws. + * + * @see #runOnMainThreadNowOrLater(Runnable) + */ + public static void runOnMainThread(@NonNull Runnable runnable) { + runOnMainThreadDelayed(runnable, 0); + } + + /** + * Automatically logs any exceptions the runnable throws + */ + public static void runOnMainThreadDelayed(@NonNull Runnable runnable, long delayMillis) { + Runnable loggingRunnable = () -> { + try { + runnable.run(); + } catch (Exception ex) { + Logger.printException(() -> runnable.getClass().getSimpleName() + ": " + ex.getMessage(), ex); + } + }; + new Handler(Looper.getMainLooper()).postDelayed(loggingRunnable, delayMillis); + } + + /** + * If called from the main thread, the code is run immediately.

+ * If called off the main thread, this is the same as {@link #runOnMainThread(Runnable)}. + */ + public static void runOnMainThreadNowOrLater(@NonNull Runnable runnable) { + if (isCurrentlyOnMainThread()) { + runnable.run(); + } else { + runOnMainThread(runnable); + } + } + + /** + * @return if the calling thread is on the main thread + */ + public static boolean isCurrentlyOnMainThread() { + if (isSDKAbove(23)) { + return Looper.getMainLooper().isCurrentThread(); + } else { + return Looper.getMainLooper().getThread() == Thread.currentThread(); + } + } + + /** + * @throws IllegalStateException if the calling thread is _off_ the main thread + */ + public static void verifyOnMainThread() throws IllegalStateException { + if (!isCurrentlyOnMainThread()) { + throw new IllegalStateException("Must call _on_ the main thread"); + } + } + + /** + * @throws IllegalStateException if the calling thread is _on_ the main thread + */ + public static void verifyOffMainThread() throws IllegalStateException { + if (isCurrentlyOnMainThread()) { + throw new IllegalStateException("Must call _off_ the main thread"); + } + } + + public enum NetworkType { + MOBILE("mobile"), + WIFI("wifi"), + NONE("none"); + + private final String name; + + NetworkType(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + public static boolean isNetworkNotConnected() { + final NetworkType networkType = getNetworkType(); + return networkType == NetworkType.NONE; + } + + @SuppressLint("MissingPermission") // permission already included in YouTube + public static NetworkType getNetworkType() { + if (context == null || !(context.getSystemService(Context.CONNECTIVITY_SERVICE) instanceof ConnectivityManager cm)) + return NetworkType.NONE; + + final NetworkInfo networkInfo = cm.getActiveNetworkInfo(); + + if (networkInfo == null || !networkInfo.isConnected()) + return NetworkType.NONE; + + return switch (networkInfo.getType()) { + case ConnectivityManager.TYPE_MOBILE, ConnectivityManager.TYPE_BLUETOOTH -> + NetworkType.MOBILE; + default -> NetworkType.WIFI; + }; + } + + /** + * Hide a view by setting its layout params to 0x0 + * + * @param view The view to hide. + */ + public static void hideViewByLayoutParams(View view) { + if (view == null) return; + + if (view instanceof LinearLayout) { + LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(0, 0); + view.setLayoutParams(layoutParams); + } else if (view instanceof FrameLayout) { + FrameLayout.LayoutParams layoutParams2 = new FrameLayout.LayoutParams(0, 0); + view.setLayoutParams(layoutParams2); + } else if (view instanceof RelativeLayout) { + RelativeLayout.LayoutParams layoutParams3 = new RelativeLayout.LayoutParams(0, 0); + view.setLayoutParams(layoutParams3); + } else if (view instanceof Toolbar) { + Toolbar.LayoutParams layoutParams4 = new Toolbar.LayoutParams(0, 0); + view.setLayoutParams(layoutParams4); + } else if (view instanceof ViewGroup) { + ViewGroup.LayoutParams layoutParams5 = new ViewGroup.LayoutParams(0, 0); + view.setLayoutParams(layoutParams5); + } else { + ViewGroup.LayoutParams params = view.getLayoutParams(); + params.width = 0; + params.height = 0; + view.setLayoutParams(params); + } + } + + public static void hideViewGroupByMarginLayoutParams(ViewGroup viewGroup) { + // Rest of the implementation added by patch. + viewGroup.setVisibility(View.GONE); + } + + /** + * {@link PreferenceScreen} and {@link PreferenceGroup} sorting styles. + */ + private enum Sort { + /** + * Sort by the localized preference title. + */ + BY_TITLE("_sort_by_title"), + + /** + * Sort by the preference keys. + */ + BY_KEY("_sort_by_key"), + + /** + * Unspecified sorting. + */ + UNSORTED("_sort_by_unsorted"); + + final String keySuffix; + + Sort(String keySuffix) { + this.keySuffix = keySuffix; + } + + @NonNull + static Sort fromKey(@Nullable String key, @NonNull Sort defaultSort) { + if (key != null) { + for (Sort sort : values()) { + if (key.endsWith(sort.keySuffix)) { + return sort; + } + } + } + return defaultSort; + } + } + + private static final Regex punctuationRegex = new Regex("\\p{P}+"); + + /** + * Strips all punctuation and converts to lower case. A null parameter returns an empty string. + */ + public static String removePunctuationConvertToLowercase(@Nullable CharSequence original) { + if (original == null) return ""; + return punctuationRegex.replace(original, "").toLowerCase(); + } + + /** + * Sort a PreferenceGroup and all it's sub groups by title or key. + *

+ * Sort order is determined by the preferences key {@link Sort} suffix. + *

+ * If a preference has no key or no {@link Sort} suffix, + * then the preferences are left unsorted. + */ + @SuppressWarnings("deprecation") + public static void sortPreferenceGroups(@NonNull PreferenceGroup group) { + Sort groupSort = Sort.fromKey(group.getKey(), Sort.UNSORTED); + SortedMap preferences = new TreeMap<>(); + + for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) { + Preference preference = group.getPreference(i); + + final Sort preferenceSort; + if (preference instanceof PreferenceGroup preferenceGroup) { + sortPreferenceGroups(preferenceGroup); + preferenceSort = groupSort; // Sort value for groups is for it's content, not itself. + } else { + // Allow individual preferences to set a key sorting. + // Used to force a preference to the top or bottom of a group. + preferenceSort = Sort.fromKey(preference.getKey(), groupSort); + } + + final String sortValue; + switch (preferenceSort) { + case BY_TITLE -> + sortValue = removePunctuationConvertToLowercase(preference.getTitle()); + case BY_KEY -> sortValue = preference.getKey(); + case UNSORTED -> { + continue; // Keep original sorting. + } + default -> throw new IllegalStateException(); + } + + preferences.put(sortValue, preference); + } + + int index = 0; + for (Preference pref : preferences.values()) { + int order = index++; + + // If the preference is a PreferenceScreen or is an intent preference, move to the top. + if (pref instanceof PreferenceScreen || pref.getIntent() != null) { + // Arbitrary high number. + order -= 1000; + } + + pref.setOrder(order); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ads/AdsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ads/AdsPatch.java new file mode 100644 index 000000000..9eb1aa7b1 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ads/AdsPatch.java @@ -0,0 +1,46 @@ +package app.revanced.extension.youtube.patches.ads; + +import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition; + +import android.view.View; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class AdsPatch { + private static final boolean hideGeneralAdsEnabled = Settings.HIDE_GENERAL_ADS.get(); + private static final boolean hideGetPremiumAdsEnabled = Settings.HIDE_GET_PREMIUM.get(); + private static final boolean hideVideoAdsEnabled = Settings.HIDE_VIDEO_ADS.get(); + + /** + * Injection point. + * Hide the view, which shows ads in the homepage. + * + * @param view The view, which shows ads. + */ + public static void hideAdAttributionView(View view) { + hideViewBy0dpUnderCondition(hideGeneralAdsEnabled, view); + } + + public static boolean hideGetPremium() { + return hideGetPremiumAdsEnabled; + } + + /** + * Injection point. + */ + public static boolean hideVideoAds() { + return !hideVideoAdsEnabled; + } + + /** + * Injection point. + *

+ * Only used by old clients. + * It is presumed to have been deprecated, and if it is confirmed that it is no longer used, remove it. + */ + public static boolean hideVideoAds(boolean original) { + return !hideVideoAdsEnabled && original; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/alternativethumbnails/AlternativeThumbnailsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/alternativethumbnails/AlternativeThumbnailsPatch.java new file mode 100644 index 000000000..aa9750853 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/alternativethumbnails/AlternativeThumbnailsPatch.java @@ -0,0 +1,721 @@ +package app.revanced.extension.youtube.patches.alternativethumbnails; + +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_HOME; +import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_LIBRARY; +import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_PLAYER; +import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_SEARCH; +import static app.revanced.extension.youtube.settings.Settings.ALT_THUMBNAIL_SUBSCRIPTIONS; +import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; + +import android.net.Uri; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.chromium.net.UrlRequest; +import org.chromium.net.UrlResponseInfo; +import org.chromium.net.impl.CronetUrlRequest; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.regex.Pattern; + +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.RootView; + +/** + * @noinspection ALL + * Alternative YouTube thumbnails. + *

+ * Can show YouTube provided screen captures of beginning/middle/end of the video. + * (ie: sd1.jpg, sd2.jpg, sd3.jpg). + *

+ * Or can show crowd-sourced thumbnails provided by DeArrow (...). + *

+ * Or can use DeArrow and fall back to screen captures if DeArrow is not available. + *

+ * Has an additional option to use 'fast' video still thumbnails, + * where it forces sd thumbnail quality and skips verifying if the alt thumbnail image exists. + * The UI loading time will be the same or better than using original thumbnails, + * but thumbnails will initially fail to load for all live streams, unreleased, and occasionally very old videos. + * If a failed thumbnail load is reloaded (ie: scroll off, then on screen), then the original thumbnail + * is reloaded instead. Fast thumbnails requires using SD or lower thumbnail resolution, + * because a noticeable number of videos do not have hq720 and too much fail to load. + */ +public final class AlternativeThumbnailsPatch { + + // These must be class declarations if declared here, + // otherwise the app will not load due to cyclic initialization errors. + public static final class DeArrowAvailability implements Setting.Availability { + public static boolean usingDeArrowAnywhere() { + return ALT_THUMBNAIL_HOME.get().useDeArrow + || ALT_THUMBNAIL_SUBSCRIPTIONS.get().useDeArrow + || ALT_THUMBNAIL_LIBRARY.get().useDeArrow + || ALT_THUMBNAIL_PLAYER.get().useDeArrow + || ALT_THUMBNAIL_SEARCH.get().useDeArrow; + } + + @Override + public boolean isAvailable() { + return usingDeArrowAnywhere(); + } + } + + public static final class StillImagesAvailability implements Setting.Availability { + public static boolean usingStillImagesAnywhere() { + return ALT_THUMBNAIL_HOME.get().useStillImages + || ALT_THUMBNAIL_SUBSCRIPTIONS.get().useStillImages + || ALT_THUMBNAIL_LIBRARY.get().useStillImages + || ALT_THUMBNAIL_PLAYER.get().useStillImages + || ALT_THUMBNAIL_SEARCH.get().useStillImages; + } + + @Override + public boolean isAvailable() { + return usingStillImagesAnywhere(); + } + } + + public enum ThumbnailOption { + ORIGINAL(false, false), + DEARROW(true, false), + DEARROW_STILL_IMAGES(true, true), + STILL_IMAGES(false, true); + + final boolean useDeArrow; + final boolean useStillImages; + + ThumbnailOption(boolean useDeArrow, boolean useStillImages) { + this.useDeArrow = useDeArrow; + this.useStillImages = useStillImages; + } + } + + public enum ThumbnailStillTime { + BEGINNING(1), + MIDDLE(2), + END(3); + + /** + * The url alt image number. Such as the 2 in 'hq720_2.jpg' + */ + final int altImageNumber; + + ThumbnailStillTime(int altImageNumber) { + this.altImageNumber = altImageNumber; + } + } + + private static final Uri dearrowApiUri; + + /** + * The scheme and host of {@link #dearrowApiUri}. + */ + private static final String deArrowApiUrlPrefix; + + /** + * How long to temporarily turn off DeArrow if it fails for any reason. + */ + private static final long DEARROW_FAILURE_API_BACKOFF_MILLISECONDS = 5 * 60 * 1000; // 5 Minutes. + + /** + * Regex to match youtube static thumbnails domain. + * Used to find and replace blocked domain with a working ones + */ + private static final String YOUTUBE_STATIC_THUMBNAILS_DOMAIN_REGEX = "(yt[3-4]|lh[3-6]|play-lh)\\.(ggpht|googleusercontent)\\.com"; + + private static final Pattern YOUTUBE_STATIC_THUMBNAILS_DOMAIN_PATTERN = Pattern.compile(YOUTUBE_STATIC_THUMBNAILS_DOMAIN_REGEX); + + /** + * If non zero, then the system time of when DeArrow API calls can resume. + */ + private static volatile long timeToResumeDeArrowAPICalls; + + static { + dearrowApiUri = validateSettings(); + final int port = dearrowApiUri.getPort(); + String portString = port == -1 ? "" : (":" + port); + deArrowApiUrlPrefix = dearrowApiUri.getScheme() + "://" + dearrowApiUri.getHost() + portString + "/"; + Logger.printDebug(() -> "Using DeArrow API address: " + deArrowApiUrlPrefix); + } + + /** + * Fix any bad imported data. + */ + private static Uri validateSettings() { + Uri apiUri = Uri.parse(Settings.ALT_THUMBNAIL_DEARROW_API_URL.get()); + // Cannot use unsecured 'http', otherwise the connections fail to start and no callbacks hooks are made. + String scheme = apiUri.getScheme(); + if (scheme == null || scheme.equals("http") || apiUri.getHost() == null) { + Utils.showToastLong(str("revanced_alt_thumbnail_dearrow_api_url_invalid_toast")); + Utils.showToastShort(str("revanced_extended_reset_to_default_toast")); + Settings.ALT_THUMBNAIL_DEARROW_API_URL.resetToDefault(); + return validateSettings(); + } + return apiUri; + } + + private static ThumbnailOption optionSettingForCurrentNavigation() { + // Must check player type first, as search bar can be active behind the player. + if (RootView.isPlayerActive()) { + return ALT_THUMBNAIL_PLAYER.get(); + } + + // Must check second, as search can be from any tab. + if (RootView.isSearchBarActive()) { + return ALT_THUMBNAIL_SEARCH.get(); + } + + // Avoid checking which navigation button is selected, if all other settings are the same. + ThumbnailOption homeOption = ALT_THUMBNAIL_HOME.get(); + ThumbnailOption subscriptionsOption = ALT_THUMBNAIL_SUBSCRIPTIONS.get(); + ThumbnailOption libraryOption = ALT_THUMBNAIL_LIBRARY.get(); + if ((homeOption == subscriptionsOption) && (homeOption == libraryOption)) { + return homeOption; // All are the same option. + } + + NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); + if (selectedNavButton == null) { + // Unknown tab, treat as the home tab; + return homeOption; + } + if (selectedNavButton == NavigationButton.HOME) { + return homeOption; + } + if (selectedNavButton == NavigationButton.SUBSCRIPTIONS || selectedNavButton == NavigationButton.NOTIFICATIONS) { + return subscriptionsOption; + } + // A library tab variant is active. + return libraryOption; + } + + /** + * Build the alternative thumbnail url using YouTube provided still video captures. + * + * @param decodedUrl Decoded original thumbnail request url. + * @return The alternative thumbnail url, or the original url. Both without tracking parameters. + */ + @NonNull + private static String buildYoutubeVideoStillURL(@NonNull DecodedThumbnailUrl decodedUrl, + @NonNull ThumbnailQuality qualityToUse) { + String sanitizedReplacement = decodedUrl.createStillsUrl(qualityToUse, false); + if (VerifiedQualities.verifyAltThumbnailExist(decodedUrl.videoId, qualityToUse, sanitizedReplacement)) { + return sanitizedReplacement; + } + return decodedUrl.sanitizedUrl; + } + + /** + * Build the alternative thumbnail url using DeArrow thumbnail cache. + * + * @param videoId ID of the video to get a thumbnail of. Can be any video (regular or Short). + * @param fallbackUrl URL to fall back to in case. + * @return The alternative thumbnail url, without tracking parameters. + */ + @NonNull + private static String buildDeArrowThumbnailURL(String videoId, String fallbackUrl) { + // Build thumbnail request url. + // See https://github.com/ajayyy/DeArrowThumbnailCache/blob/29eb4359ebdf823626c79d944a901492d760bbbc/app.py#L29. + return dearrowApiUri + .buildUpon() + .appendQueryParameter("videoID", videoId) + .appendQueryParameter("redirectUrl", fallbackUrl) + .build() + .toString(); + } + + private static boolean urlIsDeArrow(@NonNull String imageUrl) { + return imageUrl.startsWith(deArrowApiUrlPrefix); + } + + /** + * @return If this client has not recently experienced any DeArrow API errors. + */ + private static boolean canUseDeArrowAPI() { + if (timeToResumeDeArrowAPICalls == 0) { + return true; + } + if (timeToResumeDeArrowAPICalls < System.currentTimeMillis()) { + Logger.printDebug(() -> "Resuming DeArrow API calls"); + timeToResumeDeArrowAPICalls = 0; + return true; + } + return false; + } + + private static void handleDeArrowError(@NonNull String url, int statusCode) { + Logger.printDebug(() -> "Encountered DeArrow error. Url: " + url); + final long now = System.currentTimeMillis(); + if (timeToResumeDeArrowAPICalls < now) { + timeToResumeDeArrowAPICalls = now + DEARROW_FAILURE_API_BACKOFF_MILLISECONDS; + if (Settings.ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST.get()) { + String toastMessage = (statusCode != 0) + ? str("revanced_alt_thumbnail_dearrow_error", statusCode) + : str("revanced_alt_thumbnail_dearrow_error_generic"); + Utils.showToastLong(toastMessage); + } + } + } + + /** + * Injection point. Called off the main thread and by multiple threads at the same time. + * + * @param originalUrl Image url for all url images loaded, including video thumbnails. + */ + public static String overrideImageURL(String originalUrl) { + try { + ThumbnailOption option = optionSettingForCurrentNavigation(); + + if (option == ThumbnailOption.ORIGINAL) { + return originalUrl; + } + + final var decodedUrl = DecodedThumbnailUrl.decodeImageUrl(originalUrl); + if (decodedUrl == null) { + return originalUrl; // Not a thumbnail. + } + + Logger.printDebug(() -> "Original url: " + decodedUrl.sanitizedUrl); + + ThumbnailQuality qualityToUse = ThumbnailQuality.getQualityToUse(decodedUrl.imageQuality); + if (qualityToUse == null) { + // Thumbnail is a Short or a Storyboard image used for seekbar thumbnails (must not replace these). + return originalUrl; + } + + String sanitizedReplacementUrl; + final boolean includeTracking; + if (option.useDeArrow && canUseDeArrowAPI()) { + includeTracking = false; // Do not include view tracking parameters with API call. + final String fallbackUrl = option.useStillImages + ? buildYoutubeVideoStillURL(decodedUrl, qualityToUse) + : decodedUrl.sanitizedUrl; + + sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl); + } else if (option.useStillImages) { + includeTracking = true; // Include view tracking parameters if present. + sanitizedReplacementUrl = buildYoutubeVideoStillURL(decodedUrl, qualityToUse); + } else { + return originalUrl; // Recently experienced DeArrow failure and video stills are not enabled. + } + + // Do not log any tracking parameters. + Logger.printDebug(() -> "Replacement url: " + sanitizedReplacementUrl); + + return includeTracking + ? sanitizedReplacementUrl + decodedUrl.viewTrackingParameters + : sanitizedReplacementUrl; + } catch (Exception ex) { + Logger.printException(() -> "overrideImageURL failure", ex); + return originalUrl; + } + } + + /** + * Injection point. + *

+ * Cronet considers all completed connections as a success, even if the response is 404 or 5xx. + */ + public static void handleCronetSuccess(UrlRequest request, @NonNull UrlResponseInfo responseInfo) { + try { + final int statusCode = responseInfo.getHttpStatusCode(); + if (statusCode == 200) { + return; + } + + String url = responseInfo.getUrl(); + + if (urlIsDeArrow(url)) { + Logger.printDebug(() -> "handleCronetSuccess, statusCode: " + statusCode); + if (statusCode == 304) { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/304 + return; // Normal response. + } + handleDeArrowError(url, statusCode); + return; + } + + if (statusCode == 404) { + // Fast alt thumbnails is enabled and the thumbnail is not available. + // The video is: + // - live stream + // - upcoming unreleased video + // - very old + // - very low view count + // Take note of this, so if the image reloads the original thumbnail will be used. + DecodedThumbnailUrl decodedUrl = DecodedThumbnailUrl.decodeImageUrl(url); + if (decodedUrl == null) { + return; // Not a thumbnail. + } + + Logger.printDebug(() -> "handleCronetSuccess, image not available: " + url); + + ThumbnailQuality quality = ThumbnailQuality.altImageNameToQuality(decodedUrl.imageQuality); + if (quality == null) { + // Video is a short or a seekbar thumbnail, but somehow did not load. Should not happen. + Logger.printDebug(() -> "Failed to recognize image quality of url: " + decodedUrl.sanitizedUrl); + return; + } + + VerifiedQualities.setAltThumbnailDoesNotExist(decodedUrl.videoId, quality); + } + } catch (Exception ex) { + Logger.printException(() -> "Callback success error", ex); + } + } + + /** + * Injection point. + *

+ * To test failure cases, try changing the API URL to each of: + * - A non-existent domain. + * - A url path of something incorrect (ie: /v1/nonExistentEndPoint). + *

+ * Cronet uses a very timeout (several minutes), so if the API never responds this hook can take a while to be called. + * But this does not appear to be a problem, as the DeArrow API has not been observed to 'go silent' + * Instead if there's a problem it returns an error code status response, which is handled in this patch. + */ + public static void handleCronetFailure(UrlRequest request, + @Nullable UrlResponseInfo responseInfo, + IOException exception) { + try { + String url = ((CronetUrlRequest) request).getHookedUrl(); + if (urlIsDeArrow(url)) { + Logger.printDebug(() -> "handleCronetFailure, exception: " + exception); + final int statusCode = (responseInfo != null) + ? responseInfo.getHttpStatusCode() + : 0; + handleDeArrowError(url, statusCode); + } + } catch (Exception ex) { + Logger.printException(() -> "Callback failure error", ex); + } + } + + private enum ThumbnailQuality { + // In order of lowest to highest resolution. + DEFAULT("default", ""), // effective alt name is 1.jpg, 2.jpg, 3.jpg + MQDEFAULT("mqdefault", "mq"), + HQDEFAULT("hqdefault", "hq"), + SDDEFAULT("sddefault", "sd"), + HQ720("hq720", "hq720_"), + MAXRESDEFAULT("maxresdefault", "maxres"); + + /** + * Lookup map of original name to enum. + */ + private static final Map originalNameToEnum = new HashMap<>(); + + /** + * Lookup map of alt name to enum. ie: "hq720_1" to {@link #HQ720}. + */ + private static final Map altNameToEnum = new HashMap<>(); + + static { + for (ThumbnailQuality quality : values()) { + originalNameToEnum.put(quality.originalName, quality); + + for (ThumbnailStillTime time : ThumbnailStillTime.values()) { + // 'custom' thumbnails set by the content creator. + // These show up in place of regular thumbnails + // and seem to be limited to the same [1, 3] range as the still captures. + originalNameToEnum.put(quality.originalName + "_custom_" + time.altImageNumber, quality); + + altNameToEnum.put(quality.altImageName + time.altImageNumber, quality); + } + } + } + + /** + * Convert an alt image name to enum. + * ie: "hq720_2" returns {@link #HQ720}. + */ + @Nullable + static ThumbnailQuality altImageNameToQuality(@NonNull String altImageName) { + return altNameToEnum.get(altImageName); + } + + /** + * Original quality to effective alt quality to use. + * ie: If fast alt image is enabled, then "hq720" returns {@link #SDDEFAULT}. + */ + @Nullable + static ThumbnailQuality getQualityToUse(@NonNull String originalSize) { + ThumbnailQuality quality = originalNameToEnum.get(originalSize); + if (quality == null) { + return null; // Not a thumbnail for a regular video. + } + + final boolean useFastQuality = Settings.ALT_THUMBNAIL_STILLS_FAST.get(); + switch (quality) { + case SDDEFAULT: + // SD alt images have somewhat worse quality with washed out color and poor contrast. + // But the 720 images look much better and don't suffer from these issues. + // For unknown reasons, the 720 thumbnails are used only for the home feed, + // while SD is used for the search and subscription feed + // (even though search and subscriptions use the exact same layout as the home feed). + // Of note, this image quality issue only appears with the alt thumbnail images, + // and the regular thumbnails have identical color/contrast quality for all sizes. + // Fix this by falling thru and upgrading SD to 720. + case HQ720: + if (useFastQuality) { + return SDDEFAULT; // SD is max resolution for fast alt images. + } + return HQ720; + case MAXRESDEFAULT: + if (useFastQuality) { + return SDDEFAULT; + } + return MAXRESDEFAULT; + default: + return quality; + } + } + + final String originalName; + final String altImageName; + + ThumbnailQuality(String originalName, String altImageName) { + this.originalName = originalName; + this.altImageName = altImageName; + } + + String getAltImageNameToUse() { + return altImageName + Settings.ALT_THUMBNAIL_STILLS_TIME.get().altImageNumber; + } + } + + /** + * Uses HTTP HEAD requests to verify and keep track of which thumbnail sizes + * are available and not available. + */ + private static class VerifiedQualities { + /** + * After a quality is verified as not available, how long until the quality is re-verified again. + * Used only if fast mode is not enabled. Intended for live streams and unreleased videos + * that are now finished and available (and thus, the alt thumbnails are also now available). + */ + private static final long NOT_AVAILABLE_TIMEOUT_MILLISECONDS = 10 * 60 * 1000; // 10 minutes. + + /** + * Cache used to verify if an alternative thumbnails exists for a given video id. + */ + @GuardedBy("itself") + private static final Map altVideoIdLookup = new LinkedHashMap<>(100) { + private static final int CACHE_LIMIT = 1000; + + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit. + } + }; + + private static VerifiedQualities getVerifiedQualities(@NonNull String videoId, boolean returnNullIfDoesNotExist) { + synchronized (altVideoIdLookup) { + VerifiedQualities verified = altVideoIdLookup.get(videoId); + if (verified == null) { + if (returnNullIfDoesNotExist) { + return null; + } + verified = new VerifiedQualities(); + altVideoIdLookup.put(videoId, verified); + } + return verified; + } + } + + static boolean verifyAltThumbnailExist(@NonNull String videoId, @NonNull ThumbnailQuality quality, + @NonNull String imageUrl) { + VerifiedQualities verified = getVerifiedQualities(videoId, Settings.ALT_THUMBNAIL_STILLS_FAST.get()); + if (verified == null) return true; // Fast alt thumbnails is enabled. + return verified.verifyYouTubeThumbnailExists(videoId, quality, imageUrl); + } + + static void setAltThumbnailDoesNotExist(@NonNull String videoId, @NonNull ThumbnailQuality quality) { + VerifiedQualities verified = getVerifiedQualities(videoId, false); + //noinspection ConstantConditions + verified.setQualityVerified(videoId, quality, false); + } + + /** + * Highest quality verified as existing. + */ + @Nullable + private ThumbnailQuality highestQualityVerified; + /** + * Lowest quality verified as not existing. + */ + @Nullable + private ThumbnailQuality lowestQualityNotAvailable; + + /** + * System time, of when to invalidate {@link #lowestQualityNotAvailable}. + * Used only if fast mode is not enabled. + */ + private long timeToReVerifyLowestQuality; + + private synchronized void setQualityVerified(String videoId, ThumbnailQuality quality, boolean isVerified) { + if (isVerified) { + if (highestQualityVerified == null || highestQualityVerified.ordinal() < quality.ordinal()) { + highestQualityVerified = quality; + } + } else { + if (lowestQualityNotAvailable == null || lowestQualityNotAvailable.ordinal() > quality.ordinal()) { + lowestQualityNotAvailable = quality; + timeToReVerifyLowestQuality = System.currentTimeMillis() + NOT_AVAILABLE_TIMEOUT_MILLISECONDS; + } + Logger.printDebug(() -> quality + " not available for video: " + videoId); + } + } + + /** + * Verify if a video alt thumbnail exists. Does so by making a minimal HEAD http request. + */ + synchronized boolean verifyYouTubeThumbnailExists(@NonNull String videoId, @NonNull ThumbnailQuality quality, + @NonNull String imageUrl) { + if (highestQualityVerified != null && highestQualityVerified.ordinal() >= quality.ordinal()) { + return true; // Previously verified as existing. + } + + final boolean fastQuality = Settings.ALT_THUMBNAIL_STILLS_FAST.get(); + if (lowestQualityNotAvailable != null && lowestQualityNotAvailable.ordinal() <= quality.ordinal()) { + if (fastQuality || System.currentTimeMillis() < timeToReVerifyLowestQuality) { + return false; // Previously verified as not existing. + } + // Enough time has passed, and should re-verify again. + Logger.printDebug(() -> "Resetting lowest verified quality for: " + videoId); + lowestQualityNotAvailable = null; + } + + if (fastQuality) { + return true; // Unknown if it exists or not. Use the URL anyways and update afterwards if loading fails. + } + + boolean imageFileFound; + try { + // This hooked code is running on a low priority thread, and it's slightly faster + // to run the url connection thru the integrations thread pool which runs at the highest priority. + final long start = System.currentTimeMillis(); + imageFileFound = Utils.submitOnBackgroundThread(() -> { + final int connectionTimeoutMillis = 10000; // 10 seconds. + HttpURLConnection connection = (HttpURLConnection) new URL(imageUrl).openConnection(); + connection.setConnectTimeout(connectionTimeoutMillis); + connection.setReadTimeout(connectionTimeoutMillis); + connection.setRequestMethod("HEAD"); + // Even with a HEAD request, the response is the same size as a full GET request. + // Using an empty range fixes this. + connection.setRequestProperty("Range", "bytes=0-0"); + final int responseCode = connection.getResponseCode(); + if (responseCode == HttpURLConnection.HTTP_PARTIAL) { + String contentType = connection.getContentType(); + return (contentType != null && contentType.startsWith("image")); + } + if (responseCode != HttpURLConnection.HTTP_NOT_FOUND) { + Logger.printDebug(() -> "Unexpected response code: " + responseCode + " for url: " + imageUrl); + } + return false; + }).get(); + Logger.printDebug(() -> "Verification took: " + (System.currentTimeMillis() - start) + "ms for image: " + imageUrl); + } catch (ExecutionException | InterruptedException ex) { + Logger.printInfo(() -> "Could not verify alt url: " + imageUrl, ex); + imageFileFound = false; + } + + setQualityVerified(videoId, quality, imageFileFound); + return imageFileFound; + } + } + + /** + * YouTube video thumbnail url, decoded into it's relevant parts. + */ + private static class DecodedThumbnailUrl { + /** + * YouTube thumbnail URL prefix. Can be '/vi/' or '/vi_webp/' + */ + private static final String YOUTUBE_THUMBNAIL_PREFIX = "https://i.ytimg.com/vi"; + + @Nullable + static DecodedThumbnailUrl decodeImageUrl(String url) { + final int videoIdStartIndex = url.indexOf('/', YOUTUBE_THUMBNAIL_PREFIX.length()) + 1; + if (videoIdStartIndex <= 0) return null; + + final int videoIdEndIndex = url.indexOf('/', videoIdStartIndex); + if (videoIdEndIndex < 0) return null; + + final int imageSizeStartIndex = videoIdEndIndex + 1; + final int imageSizeEndIndex = url.indexOf('.', imageSizeStartIndex); + if (imageSizeEndIndex < 0) return null; + + int imageExtensionEndIndex = url.indexOf('?', imageSizeEndIndex); + if (imageExtensionEndIndex < 0) imageExtensionEndIndex = url.length(); + + return new DecodedThumbnailUrl(url, videoIdStartIndex, videoIdEndIndex, + imageSizeStartIndex, imageSizeEndIndex, imageExtensionEndIndex); + } + + final String originalFullUrl; + /** + * Full usable url, but stripped of any tracking information. + */ + final String sanitizedUrl; + /** + * Url up to the video ID. + */ + final String urlPrefix; + final String videoId; + /** + * Quality, such as hq720 or sddefault. + */ + final String imageQuality; + /** + * JPG or WEBP + */ + final String imageExtension; + /** + * User view tracking parameters, only present on some images. + */ + final String viewTrackingParameters; + + DecodedThumbnailUrl(String fullUrl, int videoIdStartIndex, int videoIdEndIndex, + int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) { + originalFullUrl = fullUrl; + sanitizedUrl = fullUrl.substring(0, imageExtensionEndIndex); + urlPrefix = fullUrl.substring(0, videoIdStartIndex); + videoId = fullUrl.substring(videoIdStartIndex, videoIdEndIndex); + imageQuality = fullUrl.substring(imageSizeStartIndex, imageSizeEndIndex); + imageExtension = fullUrl.substring(imageSizeEndIndex + 1, imageExtensionEndIndex); + viewTrackingParameters = (imageExtensionEndIndex == fullUrl.length()) + ? "" : fullUrl.substring(imageExtensionEndIndex); + } + + /** + * @noinspection SameParameterValue + */ + String createStillsUrl(@NonNull ThumbnailQuality qualityToUse, boolean includeViewTracking) { + // Images could be upgraded to webp if they are not already, but this fails quite often, + // especially for new videos uploaded in the last hour. + // And even if alt webp images do exist, sometimes they can load much slower than the original jpg alt images. + // (as much as 4x slower has been observed, despite the alt webp image being a smaller file). + StringBuilder builder = new StringBuilder(originalFullUrl.length() + 2); + builder.append(urlPrefix); + builder.append(videoId).append('/'); + builder.append(qualityToUse.getAltImageNameToUse()); + builder.append('.').append(imageExtension); + if (includeViewTracking) { + builder.append(viewTrackingParameters); + } + return builder.toString(); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ActionButtonsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ActionButtonsFilter.java new file mode 100644 index 000000000..69386f21f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ActionButtonsFilter.java @@ -0,0 +1,118 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class ActionButtonsFilter extends Filter { + private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.eml"; + private static final String ANIMATED_VECTOR_TYPE_PATH = "AnimatedVectorType"; + + private final StringFilterGroup actionBarRule; + private final StringFilterGroup bufferFilterPathRule; + private final StringFilterGroup likeSubscribeGlow; + private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList(); + + public ActionButtonsFilter() { + actionBarRule = new StringFilterGroup( + null, + VIDEO_ACTION_BAR_PATH_PREFIX + ); + addIdentifierCallbacks(actionBarRule); + + bufferFilterPathRule = new StringFilterGroup( + null, + "|ContainerType|button.eml|" + ); + likeSubscribeGlow = new StringFilterGroup( + Settings.DISABLE_LIKE_DISLIKE_GLOW, + "animated_button_border.eml" + ); + addPathCallbacks( + new StringFilterGroup( + Settings.HIDE_LIKE_DISLIKE_BUTTON, + "|segmented_like_dislike_button" + ), + new StringFilterGroup( + Settings.HIDE_DOWNLOAD_BUTTON, + "|download_button.eml|" + ), + new StringFilterGroup( + Settings.HIDE_CLIP_BUTTON, + "|clip_button.eml|" + ), + new StringFilterGroup( + Settings.HIDE_PLAYLIST_BUTTON, + "|save_to_playlist_button" + ), + new StringFilterGroup( + Settings.HIDE_REWARDS_BUTTON, + "account_link_button" + ), + bufferFilterPathRule, + likeSubscribeGlow + ); + + bufferButtonsGroupList.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_REPORT_BUTTON, + "yt_outline_flag" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHARE_BUTTON, + "yt_outline_share" + ), + new ByteArrayFilterGroup( + Settings.HIDE_REMIX_BUTTON, + "yt_outline_youtube_shorts_plus" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHOP_BUTTON, + "yt_outline_bag" + ), + new ByteArrayFilterGroup( + Settings.HIDE_THANKS_BUTTON, + "yt_outline_dollar_sign_heart" + ) + ); + } + + private boolean isEveryFilterGroupEnabled() { + for (StringFilterGroup group : pathCallbacks) + if (!group.isEnabled()) return false; + + for (ByteArrayFilterGroup group : bufferButtonsGroupList) + if (!group.isEnabled()) return false; + + return true; + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (!path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX)) { + return false; + } + if (matchedGroup == actionBarRule && !isEveryFilterGroupEnabled()) { + return false; + } + if (matchedGroup == likeSubscribeGlow) { + if (!path.contains(ANIMATED_VECTOR_TYPE_PATH)) { + return false; + } + } + if (matchedGroup == bufferFilterPathRule) { + // In case the group list has no match, return false. + if (!bufferButtonsGroupList.check(protobufBufferArray).isFiltered()) { + return false; + } + } + + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/AdsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/AdsFilter.java new file mode 100644 index 000000000..bb98225f6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/AdsFilter.java @@ -0,0 +1,161 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.youtube.settings.Settings; + +/** + * If A/B testing is applied, ad components can only be filtered by identifier + *

+ * Before A/B testing: + * Identifier: video_display_button_group_layout.eml + * Path: video_display_button_group_layout.eml|ContainerType|.... + * (Path always starts with an Identifier) + *

+ * After A/B testing: + * Identifier: video_display_button_group_layout.eml + * Path: video_lockup_with_attachment.eml|ContainerType|.... + * (Path does not contain an Identifier) + */ +@SuppressWarnings("unused") +public final class AdsFilter extends Filter { + + private final StringFilterGroup playerShoppingShelf; + private final ByteArrayFilterGroup playerShoppingShelfBuffer; + + public AdsFilter() { + + // Identifiers. + + final StringFilterGroup alertBannerPromo = new StringFilterGroup( + Settings.HIDE_PROMOTION_ALERT_BANNER, + "alert_banner_promo.eml" + ); + + // Keywords checked in 2024: + final StringFilterGroup generalAdsIdentifier = new StringFilterGroup( + Settings.HIDE_GENERAL_ADS, + // "brand_video_shelf.eml" + // "brand_video_singleton.eml" + "brand_video", + + // "carousel_footered_layout.eml" + "carousel_footered_layout", + + // "composite_concurrent_carousel_layout" + "composite_concurrent_carousel_layout", + + // "landscape_image_wide_button_layout.eml" + "landscape_image_wide_button_layout", + + // "square_image_layout.eml" + "square_image_layout", + + // "statement_banner.eml" + "statement_banner", + + // "video_display_full_layout.eml" + "video_display_full_layout", + + // "text_image_button_group_layout.eml" + // "video_display_button_group_layout.eml" + "_button_group_layout", + + // "banner_text_icon_buttoned_layout.eml" + // "video_display_compact_buttoned_layout.eml" + // "video_display_full_buttoned_layout.eml" + "_buttoned_layout", + + // "compact_landscape_image_layout.eml" + // "full_width_portrait_image_layout.eml" + // "full_width_square_image_layout.eml" + "_image_layout" + ); + + final StringFilterGroup merchandise = new StringFilterGroup( + Settings.HIDE_MERCHANDISE_SHELF, + "product_carousel", + "shopping_carousel" + ); + + final StringFilterGroup paidContent = new StringFilterGroup( + Settings.HIDE_PAID_PROMOTION_LABEL, + "paid_content_overlay" + ); + + final StringFilterGroup selfSponsor = new StringFilterGroup( + Settings.HIDE_SELF_SPONSOR_CARDS, + "cta_shelf_card" + ); + + final StringFilterGroup viewProducts = new StringFilterGroup( + Settings.HIDE_VIEW_PRODUCTS, + "product_item", + "products_in_video", + "shopping_overlay" + ); + + final StringFilterGroup webSearchPanel = new StringFilterGroup( + Settings.HIDE_WEB_SEARCH_RESULTS, + "web_link_panel", + "web_result_panel" + ); + + addIdentifierCallbacks( + alertBannerPromo, + generalAdsIdentifier, + merchandise, + paidContent, + selfSponsor, + viewProducts, + webSearchPanel + ); + + // Path. + + final StringFilterGroup generalAdsPath = new StringFilterGroup( + Settings.HIDE_GENERAL_ADS, + "carousel_ad", + "carousel_headered_layout", + "hero_promo_image", + "legal_disclosure", + "lumiere_promo_carousel", + "primetime_promo", + "product_details", + "text_image_button_layout", + "video_display_carousel_button", + "watch_metadata_app_promo" + ); + + playerShoppingShelf = new StringFilterGroup( + null, + "horizontal_shelf.eml" + ); + + playerShoppingShelfBuffer = new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_STORE_SHELF, + "shopping_item_card_list.eml" + ); + + addPathCallbacks( + generalAdsPath, + playerShoppingShelf, + viewProducts + ); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (matchedGroup == playerShoppingShelf) { + if (contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CarouselShelfFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CarouselShelfFilter.java new file mode 100644 index 000000000..4dd9ace46 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CarouselShelfFilter.java @@ -0,0 +1,107 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.StringTrieSearch; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; +import app.revanced.extension.youtube.shared.RootView; + +@SuppressWarnings("unused") +public final class CarouselShelfFilter extends Filter { + private static final String BROWSE_ID_CLIP = "FEclips"; + private static final String BROWSE_ID_HOME = "FEwhat_to_watch"; + private static final String BROWSE_ID_LIBRARY = "FElibrary"; + private static final String BROWSE_ID_NOTIFICATION = "FEactivity"; + private static final String BROWSE_ID_NOTIFICATION_INBOX = "FEnotifications_inbox"; + private static final String BROWSE_ID_PLAYLIST = "VLPL"; + private static final String BROWSE_ID_PREMIUM = "SPunlimited"; + private static final String BROWSE_ID_SUBSCRIPTION = "FEsubscriptions"; + + private static final Supplier> knownBrowseId = () -> Stream.of( + BROWSE_ID_HOME, + BROWSE_ID_NOTIFICATION, + BROWSE_ID_PLAYLIST, + BROWSE_ID_SUBSCRIPTION + ); + + private static final Supplier> whitelistBrowseId = () -> Stream.of( + BROWSE_ID_LIBRARY, + BROWSE_ID_NOTIFICATION_INBOX, + BROWSE_ID_CLIP, + BROWSE_ID_PREMIUM + ); + + private final StringTrieSearch exceptions = new StringTrieSearch(); + public final StringFilterGroup horizontalShelf; + + public CarouselShelfFilter() { + exceptions.addPattern("library_recent_shelf.eml"); + + final StringFilterGroup carouselShelf = new StringFilterGroup( + Settings.HIDE_CAROUSEL_SHELF, + "horizontal_shelf_inline.eml", + "horizontal_tile_shelf.eml", + "horizontal_video_shelf.eml" + ); + + horizontalShelf = new StringFilterGroup( + Settings.HIDE_CAROUSEL_SHELF, + "horizontal_shelf.eml" + ); + + addPathCallbacks(carouselShelf, horizontalShelf); + } + + private static boolean hideShelves(boolean playerActive, boolean searchBarActive, NavigationButton selectedNavButton, String browseId) { + // Must check player type first, as search bar can be active behind the player. + if (playerActive) { + return false; + } + // Must check second, as search can be from any tab. + if (searchBarActive) { + return true; + } + // Unknown tab, treat the same as home. + if (selectedNavButton == null) { + return true; + } + // Fixes a very rare bug in home. + if (selectedNavButton == NavigationButton.HOME && browseId.equals(BROWSE_ID_NOTIFICATION_INBOX)) { + return true; + } + // Sometimes the browserId is empty. In this case, check the navigation button. + if (browseId.isEmpty()) { + return selectedNavButton != NavigationButton.LIBRARY; + } + return knownBrowseId.get().anyMatch(browseId::equals) || whitelistBrowseId.get().noneMatch(browseId::equals); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (exceptions.matches(path)) { + return false; + } + final boolean playerActive = RootView.isPlayerActive(); + final boolean searchBarActive = RootView.isSearchBarActive(); + final NavigationButton navigationButton = NavigationButton.getSelectedNavigationButton(); + final String navigation = navigationButton == null ? "null" : navigationButton.name(); + final String browseId = RootView.getBrowseId(); + final boolean hideShelves = matchedGroup != horizontalShelf || hideShelves(playerActive, searchBarActive, navigationButton, browseId); + if (contentIndex != 0) { + return false; + } + Logger.printDebug(() -> "hideShelves: " + hideShelves + "\nplayerActive: " + playerActive + "\nsearchBarActive: " + searchBarActive + "\nbrowseId: " + browseId + "\nnavigation: " + navigation); + if (!hideShelves) { + return false; + } + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CommentsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CommentsFilter.java new file mode 100644 index 000000000..d4525cff6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CommentsFilter.java @@ -0,0 +1,133 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import java.util.regex.Pattern; + +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.utils.StringTrieSearch; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class CommentsFilter extends Filter { + private static final String COMMENT_COMPOSER_PATH = "comment_composer"; + private static final String COMMENT_ENTRY_POINT_TEASER_PATH = "comments_entry_point_teaser"; + private static final Pattern COMMENT_PREVIEW_TEXT_PATTERN = Pattern.compile("comments_entry_point_teaser.+ContainerType"); + private static final String FEED_VIDEO_PATH = "video_lockup_with_attachment"; + private static final String VIDEO_METADATA_CAROUSEL_PATH = "video_metadata_carousel.eml"; + + private final StringFilterGroup comments; + private final StringFilterGroup commentsPreviewDots; + private final StringFilterGroup createShorts; + private final StringFilterGroup previewCommentText; + private final StringFilterGroup thanks; + private final StringFilterGroup timeStampAndEmojiPicker; + private final StringTrieSearch exceptions = new StringTrieSearch(); + + public CommentsFilter() { + exceptions.addPatterns("macro_markers_list_item"); + + final StringFilterGroup channelGuidelines = new StringFilterGroup( + Settings.HIDE_CHANNEL_GUIDELINES, + "channel_guidelines_entry_banner", + "community_guidelines", + "sponsorships_comments_upsell" + ); + + comments = new StringFilterGroup( + null, + VIDEO_METADATA_CAROUSEL_PATH, + "comments_" + ); + + commentsPreviewDots = new StringFilterGroup( + Settings.HIDE_PREVIEW_COMMENT_OLD_METHOD, + "|ContainerType|ContainerType|ContainerType|" + ); + + createShorts = new StringFilterGroup( + Settings.HIDE_COMMENT_CREATE_SHORTS_BUTTON, + "composer_short_creation_button" + ); + + final StringFilterGroup membersBanner = new StringFilterGroup( + Settings.HIDE_COMMENTS_BY_MEMBERS, + "sponsorships_comments_header.eml", + "sponsorships_comments_footer.eml" + ); + + final StringFilterGroup previewComment = new StringFilterGroup( + Settings.HIDE_PREVIEW_COMMENT_OLD_METHOD, + "|carousel_item.", + "|carousel_listener", + COMMENT_ENTRY_POINT_TEASER_PATH, + "comments_entry_point_simplebox" + ); + + previewCommentText = new StringFilterGroup( + Settings.HIDE_PREVIEW_COMMENT_NEW_METHOD, + COMMENT_ENTRY_POINT_TEASER_PATH + ); + + thanks = new StringFilterGroup( + Settings.HIDE_COMMENT_THANKS_BUTTON, + "|super_thanks_button.eml" + ); + + timeStampAndEmojiPicker = new StringFilterGroup( + Settings.HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS, + "|CellType|ContainerType|ContainerType|ContainerType|ContainerType|ContainerType|" + ); + + + addIdentifierCallbacks(channelGuidelines); + + addPathCallbacks( + comments, + commentsPreviewDots, + createShorts, + membersBanner, + previewComment, + previewCommentText, + thanks, + timeStampAndEmojiPicker + ); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (exceptions.matches(path)) + return false; + + if (matchedGroup == createShorts || matchedGroup == thanks || matchedGroup == timeStampAndEmojiPicker) { + if (path.startsWith(COMMENT_COMPOSER_PATH)) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } else if (matchedGroup == comments) { + if (path.startsWith(FEED_VIDEO_PATH)) { + if (Settings.HIDE_COMMENTS_SECTION_IN_HOME_FEED.get()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } else if (Settings.HIDE_COMMENTS_SECTION.get()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } else if (matchedGroup == commentsPreviewDots) { + if (path.startsWith(VIDEO_METADATA_CAROUSEL_PATH)) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } else if (matchedGroup == previewCommentText) { + if (COMMENT_PREVIEW_TEXT_PATTERN.matcher(path).find()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } + + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CustomFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CustomFilter.java new file mode 100644 index 000000000..2c165c084 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CustomFilter.java @@ -0,0 +1,164 @@ +package app.revanced.extension.youtube.patches.components; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.utils.ByteTrieSearch; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; + +/** + * Allows custom filtering using a path and optionally a proto buffer string. + */ +@SuppressWarnings("unused") +public final class CustomFilter extends Filter { + + private static void showInvalidSyntaxToast(@NonNull String expression) { + Utils.showToastLong(str("revanced_custom_filter_toast_invalid_syntax", expression)); + } + + private static class CustomFilterGroup extends StringFilterGroup { + /** + * Optional character for the path that indicates the custom filter path must match the start. + * Must be the first character of the expression. + */ + public static final String SYNTAX_STARTS_WITH = "^"; + + /** + * Optional character that separates the path from a proto buffer string pattern. + */ + public static final String SYNTAX_BUFFER_SYMBOL = "$"; + + /** + * @return the parsed objects + */ + @NonNull + @SuppressWarnings("ConstantConditions") + static Collection parseCustomFilterGroups() { + String rawCustomFilterText = Settings.CUSTOM_FILTER_STRINGS.get(); + if (rawCustomFilterText.isBlank()) { + return Collections.emptyList(); + } + + // Map key is the path including optional special characters (^ and/or $) + Map result = new HashMap<>(); + Pattern pattern = Pattern.compile( + "(" // map key group + + "(\\Q" + SYNTAX_STARTS_WITH + "\\E?)" // optional starts with + + "([^\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E]*)" // path + + "(\\Q" + SYNTAX_BUFFER_SYMBOL + "\\E?)" // optional buffer symbol + + ")" // end map key group + + "(.*)"); // optional buffer string + + for (String expression : rawCustomFilterText.split("\n")) { + if (expression.isBlank()) continue; + + Matcher matcher = pattern.matcher(expression); + if (!matcher.find()) { + showInvalidSyntaxToast(expression); + continue; + } + + final String mapKey = matcher.group(1); + final boolean pathStartsWith = !matcher.group(2).isEmpty(); + final String path = matcher.group(3); + final boolean hasBufferSymbol = !matcher.group(4).isEmpty(); + final String bufferString = matcher.group(5); + + if (path.isBlank() || (hasBufferSymbol && bufferString.isBlank())) { + showInvalidSyntaxToast(expression); + continue; + } + + // Use one group object for all expressions with the same path. + // This ensures the buffer is searched exactly once + // when multiple paths are used with different buffer strings. + CustomFilterGroup group = result.get(mapKey); + if (group == null) { + group = new CustomFilterGroup(pathStartsWith, path); + result.put(mapKey, group); + } + if (hasBufferSymbol) { + group.addBufferString(bufferString); + } + } + + return result.values(); + } + + final boolean startsWith; + ByteTrieSearch bufferSearch; + + CustomFilterGroup(boolean startsWith, @NonNull String path) { + super(Settings.CUSTOM_FILTER, path); + this.startsWith = startsWith; + } + + void addBufferString(@NonNull String bufferString) { + if (bufferSearch == null) { + bufferSearch = new ByteTrieSearch(); + } + bufferSearch.addPattern(bufferString.getBytes()); + } + + @NonNull + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("CustomFilterGroup{"); + builder.append("path="); + if (startsWith) builder.append(SYNTAX_STARTS_WITH); + builder.append(filters[0]); + + if (bufferSearch != null) { + String delimitingCharacter = "❙"; + builder.append(", bufferStrings="); + builder.append(delimitingCharacter); + for (byte[] bufferString : bufferSearch.getPatterns()) { + builder.append(new String(bufferString)); + builder.append(delimitingCharacter); + } + } + builder.append("}"); + return builder.toString(); + } + } + + public CustomFilter() { + Collection groups = CustomFilterGroup.parseCustomFilterGroups(); + + if (!groups.isEmpty()) { + CustomFilterGroup[] groupsArray = groups.toArray(new CustomFilterGroup[0]); + Logger.printDebug(() -> "Using Custom filters: " + Arrays.toString(groupsArray)); + addPathCallbacks(groupsArray); + } + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + // All callbacks are custom filter groups. + CustomFilterGroup custom = (CustomFilterGroup) matchedGroup; + if (custom.startsWith && contentIndex != 0) { + return false; + } + if (custom.bufferSearch != null && !custom.bufferSearch.matches(protobufBufferArray)) { + return false; + } + + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/DescriptionsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/DescriptionsFilter.java new file mode 100644 index 000000000..fb2224d18 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/DescriptionsFilter.java @@ -0,0 +1,111 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class DescriptionsFilter extends Filter { + private final ByteArrayFilterGroupList macroMarkerShelfGroupList = new ByteArrayFilterGroupList(); + + private final StringFilterGroup howThisWasMadeSection; + private final StringFilterGroup infoCardsSection; + private final StringFilterGroup macroMarkerShelf; + private final StringFilterGroup shoppingLinks; + + public DescriptionsFilter() { + // game section, music section and places section now use the same identifier in the latest version. + final StringFilterGroup attributesSection = new StringFilterGroup( + Settings.HIDE_ATTRIBUTES_SECTION, + "gaming_section.eml", + "music_section.eml", + "place_section.eml", + "video_attributes_section.eml" + ); + + final StringFilterGroup podcastSection = new StringFilterGroup( + Settings.HIDE_PODCAST_SECTION, + "playlist_section.eml" + ); + + final StringFilterGroup transcriptSection = new StringFilterGroup( + Settings.HIDE_TRANSCRIPT_SECTION, + "transcript_section.eml" + ); + + final StringFilterGroup videoSummarySection = new StringFilterGroup( + Settings.HIDE_AI_GENERATED_VIDEO_SUMMARY_SECTION, + "cell_expandable_metadata.eml-js" + ); + + addIdentifierCallbacks( + attributesSection, + podcastSection, + transcriptSection, + videoSummarySection + ); + + howThisWasMadeSection = new StringFilterGroup( + Settings.HIDE_CONTENTS_SECTION, + "how_this_was_made_section.eml" + ); + + infoCardsSection = new StringFilterGroup( + Settings.HIDE_INFO_CARDS_SECTION, + "infocards_section.eml" + ); + + macroMarkerShelf = new StringFilterGroup( + null, + "macro_markers_carousel.eml" + ); + + shoppingLinks = new StringFilterGroup( + Settings.HIDE_SHOPPING_LINKS, + "expandable_list.", + "shopping_description_shelf" + ); + + addPathCallbacks( + howThisWasMadeSection, + infoCardsSection, + macroMarkerShelf, + shoppingLinks + ); + + macroMarkerShelfGroupList.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_CHAPTERS_SECTION, + "chapters_horizontal_shelf" + ), + new ByteArrayFilterGroup( + Settings.HIDE_KEY_CONCEPTS_SECTION, + "learning_concept_macro_markers_carousel_shelf" + ) + ); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + // Check for the index because of likelihood of false positives. + if (matchedGroup == howThisWasMadeSection || matchedGroup == infoCardsSection || matchedGroup == shoppingLinks) { + if (contentIndex != 0) { + return false; + } + } else if (matchedGroup == macroMarkerShelf) { + if (contentIndex != 0) { + return false; + } + if (!macroMarkerShelfGroupList.check(protobufBufferArray).isFiltered()) { + return false; + } + } + + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FeedComponentsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FeedComponentsFilter.java new file mode 100644 index 000000000..68b1b9d1f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FeedComponentsFilter.java @@ -0,0 +1,279 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.patches.components.StringFilterGroupList; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.StringTrieSearch; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class FeedComponentsFilter extends Filter { + private static final String CONVERSATION_CONTEXT_FEED_IDENTIFIER = + "horizontalCollectionSwipeProtector=null"; + private static final String CONVERSATION_CONTEXT_SUBSCRIPTIONS_IDENTIFIER = + "heightConstraint=null"; + private static final String INLINE_EXPANSION_PATH = "inline_expansion"; + private static final String FEED_VIDEO_PATH = "video_lockup_with_attachment"; + + private static final ByteArrayFilterGroup inlineExpansion = + new ByteArrayFilterGroup( + Settings.HIDE_EXPANDABLE_CHIP, + "inline_expansion" + ); + + private static final ByteArrayFilterGroup mixPlaylists = + new ByteArrayFilterGroup( + null, + "&list=" + ); + private static final ByteArrayFilterGroup mixPlaylistsBufferExceptions = + new ByteArrayFilterGroup( + null, + "cell_description_body", + "channel_profile" + ); + private static final StringTrieSearch mixPlaylistsContextExceptions = new StringTrieSearch(); + + private final StringFilterGroup channelProfile; + private final StringFilterGroup communityPosts; + private final StringFilterGroup expandableChip; + private final ByteArrayFilterGroup visitStoreButton; + private final StringFilterGroup videoLockup; + + private static final StringTrieSearch communityPostsFeedGroupSearch = new StringTrieSearch(); + private final StringFilterGroupList communityPostsFeedGroup = new StringFilterGroupList(); + + + public FeedComponentsFilter() { + communityPostsFeedGroupSearch.addPatterns( + CONVERSATION_CONTEXT_FEED_IDENTIFIER, + CONVERSATION_CONTEXT_SUBSCRIPTIONS_IDENTIFIER + ); + mixPlaylistsContextExceptions.addPatterns( + "V.ED", // playlist browse id + "java.lang.ref.WeakReference" + ); + + // Identifiers. + + final StringFilterGroup chipsShelf = new StringFilterGroup( + Settings.HIDE_CHIPS_SHELF, + "chips_shelf" + ); + + communityPosts = new StringFilterGroup( + null, + "post_base_wrapper", + "images_post_root", + "images_post_slim", + "poll_post_root", + "text_post_root" + ); + + final StringFilterGroup expandableShelf = new StringFilterGroup( + Settings.HIDE_EXPANDABLE_SHELF, + "expandable_section" + ); + + final StringFilterGroup feedSearchBar = new StringFilterGroup( + Settings.HIDE_FEED_SEARCH_BAR, + "search_bar_entry_point" + ); + + final StringFilterGroup tasteBuilder = new StringFilterGroup( + Settings.HIDE_FEED_SURVEY, + "selectable_item.eml", + "cell_button.eml" + ); + + videoLockup = new StringFilterGroup( + null, + FEED_VIDEO_PATH + ); + + addIdentifierCallbacks( + chipsShelf, + communityPosts, + expandableShelf, + feedSearchBar, + tasteBuilder, + videoLockup + ); + + // Paths. + + final StringFilterGroup albumCard = new StringFilterGroup( + Settings.HIDE_ALBUM_CARDS, + "browsy_bar", + "official_card" + ); + + channelProfile = new StringFilterGroup( + Settings.HIDE_BROWSE_STORE_BUTTON, + "channel_profile.eml", + "page_header.eml" // new layout + ); + + visitStoreButton = new ByteArrayFilterGroup( + null, + "header_store_button" + ); + + final StringFilterGroup channelMemberShelf = new StringFilterGroup( + Settings.HIDE_CHANNEL_MEMBER_SHELF, + "member_recognition_shelf" + ); + + final StringFilterGroup channelProfileLinks = new StringFilterGroup( + Settings.HIDE_CHANNEL_PROFILE_LINKS, + "channel_header_links", + "attribution.eml" // new layout + ); + + expandableChip = new StringFilterGroup( + Settings.HIDE_EXPANDABLE_CHIP, + INLINE_EXPANSION_PATH, + "inline_expander", + "expandable_metadata.eml" + ); + + final StringFilterGroup feedSurvey = new StringFilterGroup( + Settings.HIDE_FEED_SURVEY, + "feed_nudge", + "_survey" + ); + + final StringFilterGroup forYouShelf = new StringFilterGroup( + Settings.HIDE_FOR_YOU_SHELF, + "mixed_content_shelf" + ); + + final StringFilterGroup imageShelf = new StringFilterGroup( + Settings.HIDE_IMAGE_SHELF, + "image_shelf" + ); + + final StringFilterGroup latestPosts = new StringFilterGroup( + Settings.HIDE_LATEST_POSTS, + "post_shelf" + ); + + final StringFilterGroup movieShelf = new StringFilterGroup( + Settings.HIDE_MOVIE_SHELF, + "compact_movie", + "horizontal_movie_shelf", + "movie_and_show_upsell_card", + "compact_tvfilm_item", + "offer_module" + ); + + final StringFilterGroup notifyMe = new StringFilterGroup( + Settings.HIDE_NOTIFY_ME_BUTTON, + "set_reminder_button" + ); + + final StringFilterGroup playables = new StringFilterGroup( + Settings.HIDE_PLAYABLES, + "horizontal_gaming_shelf.eml", + "mini_game_card.eml" + ); + + final StringFilterGroup subscriptionsChannelBar = new StringFilterGroup( + Settings.HIDE_SUBSCRIPTIONS_CAROUSEL, + "subscriptions_channel_bar" + ); + + final StringFilterGroup ticketShelf = new StringFilterGroup( + Settings.HIDE_TICKET_SHELF, + "ticket_horizontal_shelf", + "ticket_shelf" + ); + + addPathCallbacks( + albumCard, + channelProfile, + channelMemberShelf, + channelProfileLinks, + expandableChip, + feedSurvey, + forYouShelf, + imageShelf, + latestPosts, + movieShelf, + notifyMe, + playables, + subscriptionsChannelBar, + ticketShelf, + videoLockup + ); + + final StringFilterGroup communityPostsHomeAndRelatedVideos = + new StringFilterGroup( + Settings.HIDE_COMMUNITY_POSTS_HOME_RELATED_VIDEOS, + CONVERSATION_CONTEXT_FEED_IDENTIFIER + ); + + final StringFilterGroup communityPostsSubscriptions = + new StringFilterGroup( + Settings.HIDE_COMMUNITY_POSTS_SUBSCRIPTIONS, + CONVERSATION_CONTEXT_SUBSCRIPTIONS_IDENTIFIER + ); + + communityPostsFeedGroup.addAll(communityPostsHomeAndRelatedVideos, communityPostsSubscriptions); + } + + /** + * Injection point. + *

+ * Called from a different place then the other filters. + */ + public static boolean filterMixPlaylists(final Object conversionContext, @Nullable final byte[] bytes) { + try { + if (!Settings.HIDE_MIX_PLAYLISTS.get()) { + return false; + } + return bytes != null + && mixPlaylists.check(bytes).isFiltered() + && !mixPlaylistsBufferExceptions.check(bytes).isFiltered() + && !mixPlaylistsContextExceptions.matches(conversionContext.toString()); + } catch (Exception ex) { + Logger.printException(() -> "filterMixPlaylists failure", ex); + } + + return false; + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (matchedGroup == channelProfile) { + if (contentIndex == 0 && visitStoreButton.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } else if (matchedGroup == communityPosts) { + if (!communityPostsFeedGroupSearch.matches(allValue) && Settings.HIDE_COMMUNITY_POSTS_CHANNEL.get()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + if (!communityPostsFeedGroup.check(allValue).isFiltered()) { + return false; + } + } else if (matchedGroup == expandableChip) { + if (path.startsWith(FEED_VIDEO_PATH)) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } else if (matchedGroup == videoLockup) { + if (contentIndex == 0 && path.startsWith("CellType|") && inlineExpansion.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } + + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FeedVideoFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FeedVideoFilter.java new file mode 100644 index 000000000..6a3587cff --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FeedVideoFilter.java @@ -0,0 +1,99 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.patches.components.StringFilterGroupList; +import app.revanced.extension.shared.utils.StringTrieSearch; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.RootView; + +@SuppressWarnings("unused") +public final class FeedVideoFilter extends Filter { + private static final String CONVERSATION_CONTEXT_FEED_IDENTIFIER = + "horizontalCollectionSwipeProtector=null"; + private static final String ENDORSEMENT_FOOTER_PATH = "endorsement_header_footer"; + + private static final StringTrieSearch feedOnlyVideoPattern = new StringTrieSearch(); + // In search results, vertical video with shorts labels mostly include videos with gray descriptions. + // Filters without check process. + private final StringFilterGroup inlineShorts; + // Used for home, related videos, subscriptions, and search results. + private final StringFilterGroup videoLockup = new StringFilterGroup( + null, + "video_lockup_with_attachment.eml" + ); + private final ByteArrayFilterGroupList feedAndDrawerGroupList = new ByteArrayFilterGroupList(); + private final ByteArrayFilterGroupList feedOnlyGroupList = new ByteArrayFilterGroupList(); + private final StringFilterGroupList videoLockupFilterGroup = new StringFilterGroupList(); + private static final ByteArrayFilterGroup relatedVideo = + new ByteArrayFilterGroup( + Settings.HIDE_RELATED_VIDEOS, + "relatedH" + ); + + public FeedVideoFilter() { + feedOnlyVideoPattern.addPattern(CONVERSATION_CONTEXT_FEED_IDENTIFIER); + + inlineShorts = new StringFilterGroup( + Settings.HIDE_RECOMMENDED_VIDEO, + "inline_shorts.eml" // vertical video with shorts label + ); + + addIdentifierCallbacks(inlineShorts); + + addPathCallbacks(videoLockup); + + feedAndDrawerGroupList.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_RECOMMENDED_VIDEO, + ENDORSEMENT_FOOTER_PATH, // videos with gray descriptions + "high-ptsZ" // videos for membership only + ) + ); + + feedOnlyGroupList.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_LOW_VIEWS_VIDEO, + "g-highZ" // videos with less than 1000 views + ) + ); + + videoLockupFilterGroup.addAll( + new StringFilterGroup( + Settings.HIDE_RECOMMENDED_VIDEO, + ENDORSEMENT_FOOTER_PATH + ) + ); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (matchedGroup == inlineShorts) { + if (RootView.isSearchBarActive()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } else if (matchedGroup == videoLockup) { + if (relatedVideo.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + if (feedOnlyVideoPattern.matches(allValue)) { + if (feedOnlyGroupList.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } else if (videoLockupFilterGroup.check(allValue).isFiltered()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + } else { + if (feedAndDrawerGroupList.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + } + } + return false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FeedVideoViewsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FeedVideoViewsFilter.java new file mode 100644 index 000000000..eb336c71a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FeedVideoViewsFilter.java @@ -0,0 +1,195 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.NavigationBar; +import app.revanced.extension.youtube.shared.RootView; + +@SuppressWarnings("all") +public final class FeedVideoViewsFilter extends Filter { + + private static final String ARROW = " -> "; + private static final String VIEWS = "views"; + private final StringFilterGroup feedVideoFilter = new StringFilterGroup( + null, + "video_lockup_with_attachment.eml" + ); + private final String[] parts = Settings.HIDE_VIDEO_VIEW_COUNTS_MULTIPLIER.get().split("\\n"); + private Pattern viewCountPattern = null; + + public FeedVideoViewsFilter() { + addPathCallbacks(feedVideoFilter); + } + + private boolean hideFeedVideoViewsSettingIsActive() { + final boolean hideHome = Settings.HIDE_VIDEO_BY_VIEW_COUNTS_HOME.get(); + final boolean hideSearch = Settings.HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH.get(); + final boolean hideSubscriptions = Settings.HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS.get(); + + if (!hideHome && !hideSearch && !hideSubscriptions) { + return false; + } else if (hideHome && hideSearch && hideSubscriptions) { + return true; + } + + // Must check player type first, as search bar can be active behind the player. + if (RootView.isPlayerActive()) { + // For now, consider the under video results the same as the home feed. + return hideHome; + } + + // Must check second, as search can be from any tab. + if (RootView.isSearchBarActive()) { + return hideSearch; + } + + NavigationBar.NavigationButton selectedNavButton = NavigationBar.NavigationButton.getSelectedNavigationButton(); + if (selectedNavButton == null) { + return hideHome; // Unknown tab, treat the same as home. + } else if (selectedNavButton == NavigationBar.NavigationButton.HOME) { + return hideHome; + } else if (selectedNavButton == NavigationBar.NavigationButton.SUBSCRIPTIONS) { + return hideSubscriptions; + } + // User is in the Library or Notifications tab. + return false; + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (hideFeedVideoViewsSettingIsActive() && filterByViews(protobufBufferArray)) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } + + /** + * Hide videos based on views count + */ + private synchronized boolean filterByViews(byte[] protobufBufferArray) { + final String protobufString = new String(protobufBufferArray); + final long lessThan = Settings.HIDE_VIDEO_VIEW_COUNTS_LESS_THAN.get(); + final long greaterThan = Settings.HIDE_VIDEO_VIEW_COUNTS_GREATER_THAN.get(); + + if (viewCountPattern == null) { + viewCountPattern = getViewCountPattern(parts); + } + + final Matcher matcher = viewCountPattern.matcher(protobufString); + if (matcher.find()) { + String numString = Objects.requireNonNull(matcher.group(1)); + double num = parseNumber(numString); + String multiplierKey = matcher.group(2); + long multiplierValue = getMultiplierValue(parts, multiplierKey); + boolean shouldFilter = num * multiplierValue < lessThan || num * multiplierValue > greaterThan; + + final boolean finalShouldFilter = shouldFilter; + Logger.printDebug(() -> { + StringBuilder builder = new StringBuilder(); + builder.append("FeedVideoViewsFilter: Should Filter: ").append(finalShouldFilter); + builder.append("\n").append(num * multiplierValue).append(" < ").append(lessThan); + builder.append(" || ").append(num * multiplierValue).append(" > ").append(greaterThan); + builder.append("\nRegex pattern: ").append(viewCountPattern.pattern()); + builder.append("\nText: ").append(matcher.group(0)); + return builder.toString(); + }); + return shouldFilter; + } + return false; + } + + private synchronized double parseNumber(String numString) { + /** + * Some languages have comma (,) as a decimal separator. + * In order to detect those numbers as doubles in Java + * we convert commas (,) to dots (.). + * Unless we find a language that has commas used in + * a different manner, it should work. + */ + numString = numString.replace(",", "."); + + /** + * Some languages have dot (.) as a kilo separator. + * So we check with regex if there is a number with 3+ + * digits after dot (.), we replace it with nothing + * to make Java understand the number as a whole. + */ + if (numString.matches("\\d+\\.\\d{3,}")) { + numString = numString.replace(".", ""); + } + return Double.parseDouble(numString); + } + + private synchronized Pattern getViewCountPattern(String[] parts) { + // Regex: (\d+[.,]?\d*)\s?(K|M|B)?(\u200F\u202C)?\s*views + StringBuilder prefixPatternBuilder = new StringBuilder("(\\d+[.,]?\\d*)\\s?("); + StringBuilder suffixBuilder = getSuffixBuilder(parts, prefixPatternBuilder); + + prefixPatternBuilder.deleteCharAt(prefixPatternBuilder.length() - 1); // Remove the trailing | + prefixPatternBuilder.append(")?(\\u200F\\u202C)?\\s*"); + prefixPatternBuilder.append(suffixBuilder.toString()); + + Pattern pattern = Pattern.compile(prefixPatternBuilder.toString()); + Logger.printDebug(() -> "FeedVideoViewsFilter: pattern: " + pattern.pattern()); + return pattern; + } + + + @NonNull + private synchronized StringBuilder getSuffixBuilder(String[] parts, StringBuilder prefixPatternBuilder) { + StringBuilder suffixBuilder = new StringBuilder(); + + for (String part : parts) { + final String[] pair = part.split(ARROW); + if (pair.length != 2) { + Logger.printDebug(() -> "FeedVideoViewsFilter: Invalid multiplier setting: " + part); + continue; // Skip invalid entries + } + final String pair0 = pair[0].trim(); + final String pair1 = pair[1].trim(); + + if (!pair1.equals(VIEWS)) { + prefixPatternBuilder.append(pair0).append("|"); + } else { + suffixBuilder.append(pair0); + } + } + return suffixBuilder; + } + + private synchronized long getMultiplierValue(String[] parts, String multiplier) { + if (multiplier == null || multiplier.isEmpty()) { + return 1L; + } + for (String part : parts) { + final String[] pair = part.split(ARROW); + if (pair.length != 2) { + Logger.printDebug(() -> "FeedVideoViewsFilter: Invalid multiplier setting: " + part); + continue; // Skip invalid entries + } + final String pair0 = pair[0].trim(); + final String pair1 = pair[1].trim(); + + + if (pair0.equals(multiplier) && !pair1.equals(VIEWS)) { + try { + return Long.parseLong(pair1.replaceAll("[^\\d]", "")); + } catch (NumberFormatException e) { + Logger.printException(() -> "Error parsing multiplier value for " + multiplier + ": " + pair1, e); + return 1L; // Default value on error + } + } + } + return 1L; // Default value if not found + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java new file mode 100644 index 000000000..bef4712de --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java @@ -0,0 +1,632 @@ +package app.revanced.extension.youtube.patches.components; + +import static java.lang.Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS; +import static java.lang.Character.UnicodeBlock.HIRAGANA; +import static java.lang.Character.UnicodeBlock.KATAKANA; +import static java.lang.Character.UnicodeBlock.KHMER; +import static java.lang.Character.UnicodeBlock.LAO; +import static java.lang.Character.UnicodeBlock.MYANMAR; +import static java.lang.Character.UnicodeBlock.THAI; +import static java.lang.Character.UnicodeBlock.TIBETAN; +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.utils.ByteTrieSearch; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.StringTrieSearch; +import app.revanced.extension.shared.utils.TrieSearch; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.RootView; + +/** + *

+ * Allows hiding home feed and search results based on video title keywords and/or channel names.
+ *
+ * Limitations:
+ * - Searching for a keyword phrase will give no search results.
+ *   This is because the buffer for each video contains the text the user searched for, and everything
+ *   will be filtered away (even if that video title/channel does not contain any keywords).
+ * - Filtering a channel name can still show Shorts from that channel in the search results.
+ *   The most common Shorts layouts do not include the channel name, so they will not be filtered.
+ * - Some layout component residue will remain, such as the video chapter previews for some search results.
+ *   These components do not include the video title or channel name, and they
+ *   appear outside the filtered components so they are not caught.
+ * - Keywords are case sensitive, but some casing variation is manually added.
+ *   (ie: "mr beast" automatically filters "Mr Beast" and "MR BEAST").
+ * - Keywords present in the layout or video data cannot be used as filters, otherwise all videos
+ *   will always be hidden.  This patch checks for some words of these words.
+ * - When using whole word syntax, some keywords may need additional pluralized variations.
+ */
+@SuppressWarnings("unused")
+public final class KeywordContentFilter extends Filter {
+
+    /**
+     * Strings found in the buffer for every videos.  Full strings should be specified.
+     * 

+ * This list does not include every common buffer string, and this can be added/changed as needed. + * Words must be entered with the exact casing as found in the buffer. + */ + private static final String[] STRINGS_IN_EVERY_BUFFER = { + // Video playback data. + "googlevideo.com/initplayback?source=youtube", // Video url. + "ANDROID", // Video url parameter. + "https://i.ytimg.com/vi/", // Thumbnail url. + "mqdefault.jpg", + "hqdefault.jpg", + "sddefault.jpg", + "hq720.jpg", + "webp", + "_custom_", // Custom thumbnail set by video creator. + // Video decoders. + "OMX.ffmpeg.vp9.decoder", + "OMX.Intel.sw_vd.vp9", + "OMX.MTK.VIDEO.DECODER.SW.VP9", + "OMX.google.vp9.decoder", + "OMX.google.av1.decoder", + "OMX.sprd.av1.decoder", + "c2.android.av1.decoder", + "c2.android.av1-dav1d.decoder", + "c2.android.vp9.decoder", + "c2.mtk.sw.vp9.decoder", + // Analytics. + "searchR", + "browse-feed", + "FEwhat_to_watch", + "FEsubscriptions", + "search_vwc_description_transition_key", + "g-high-recZ", + // Text and litho components found in the buffer that belong to path filters. + "expandable_metadata.eml", + "thumbnail.eml", + "avatar.eml", + "overflow_button.eml", + "shorts-lockup-image", + "shorts-lockup.overlay-metadata.secondary-text", + "YouTubeSans-SemiBold", + "sans-serif" + }; + + /** + * Substrings that are always first in the identifier. + */ + private final StringFilterGroup startsWithFilter = new StringFilterGroup( + null, // Multiple settings are used and must be individually checked if active. + "video_lockup_with_attachment.eml", + "compact_video.eml", + "inline_shorts", + "shorts_video_cell", + "shorts_pivot_item.eml" + ); + + /** + * Substrings that are never at the start of the path. + */ + @SuppressWarnings("FieldCanBeLocal") + private final StringFilterGroup containsFilter = new StringFilterGroup( + null, + "modern_type_shelf_header_content.eml", + "shorts_lockup_cell.eml", // Part of 'shorts_shelf_carousel.eml' + "video_card.eml" // Shorts that appear in a horizontal shelf. + ); + + /** + * Path components to not filter. Cannot filter the buffer when these are present, + * otherwise text in UI controls can be filtered as a keyword (such as using "Playlist" as a keyword). + *

+ * This is also a small performance improvement since + * the buffer of the parent component was already searched and passed. + */ + private final StringTrieSearch exceptions = new StringTrieSearch( + "metadata.eml", + "thumbnail.eml", + "avatar.eml", + "overflow_button.eml" + ); + + /** + * Minimum keyword/phrase length to prevent excessively broad content filtering. + * Only applies when not using whole word syntax. + */ + private static final int MINIMUM_KEYWORD_LENGTH = 3; + + /** + * Threshold for {@link #filteredVideosPercentage} + * that indicates all or nearly all videos have been filtered. + * This should be close to 100% to reduce false positives. + */ + private static final float ALL_VIDEOS_FILTERED_THRESHOLD = 0.95f; + + private static final float ALL_VIDEOS_FILTERED_SAMPLE_SIZE = 50; + + private static final long ALL_VIDEOS_FILTERED_BACKOFF_MILLISECONDS = 60 * 1000; // 60 seconds + + private static final int UTF8_MAX_BYTE_COUNT = 4; + + /** + * Rolling average of how many videos were filtered by a keyword. + * Used to detect if a keyword passes the initial check against {@link #STRINGS_IN_EVERY_BUFFER} + * but a keyword is still hiding all videos. + *

+ * This check can still fail if some extra UI elements pass the keywords, + * such as the video chapter preview or any other elements. + *

+ * To test this, add a filter that appears in all videos (such as 'ovd='), + * and open the subscription feed. In practice this does not always identify problems + * in the home feed and search, because the home feed has a finite amount of content and + * search results have a lot of extra video junk that is not hidden and interferes with the detection. + */ + private volatile float filteredVideosPercentage; + + /** + * If filtering is temporarily turned off, the time to resume filtering. + * Field is zero if no timeout is in effect. + */ + private volatile long timeToResumeFiltering; + + private final StringFilterGroup commentsFilter; + + private final StringTrieSearch commentsFilterExceptions = new StringTrieSearch(); + + /** + * The last value of {@link Settings#HIDE_KEYWORD_CONTENT_PHRASES} + * parsed and loaded into {@link #bufferSearch}. + * Allows changing the keywords without restarting the app. + */ + private volatile String lastKeywordPhrasesParsed; + + private volatile ByteTrieSearch bufferSearch; + + private static void logNavigationState(String state) { + // Enable locally to debug filtering. Default off to reduce log spam. + final boolean LOG_NAVIGATION_STATE = false; + // noinspection ConstantValue + if (LOG_NAVIGATION_STATE) { + Logger.printDebug(() -> "Navigation state: " + state); + } + } + + /** + * Change first letter of the first word to use title case. + */ + private static String titleCaseFirstWordOnly(String sentence) { + if (sentence.isEmpty()) { + return sentence; + } + final int firstCodePoint = sentence.codePointAt(0); + // In some non English languages title case is different than uppercase. + return new StringBuilder() + .appendCodePoint(Character.toTitleCase(firstCodePoint)) + .append(sentence, Character.charCount(firstCodePoint), sentence.length()) + .toString(); + } + + /** + * Uppercase the first letter of each word. + */ + private static String capitalizeAllFirstLetters(String sentence) { + if (sentence.isEmpty()) { + return sentence; + } + + final int delimiter = ' '; + // Use code points and not characters to handle unicode surrogates. + int[] codePoints = sentence.codePoints().toArray(); + boolean capitalizeNext = true; + for (int i = 0, length = codePoints.length; i < length; i++) { + final int codePoint = codePoints[i]; + if (codePoint == delimiter) { + capitalizeNext = true; + } else if (capitalizeNext) { + codePoints[i] = Character.toUpperCase(codePoint); + capitalizeNext = false; + } + } + return new String(codePoints, 0, codePoints.length); + } + + /** + * @return If the string contains any characters from languages that do not use spaces between words. + */ + private static boolean isLanguageWithNoSpaces(String text) { + for (int i = 0, length = text.length(); i < length; ) { + final int codePoint = text.codePointAt(i); + + Character.UnicodeBlock block = Character.UnicodeBlock.of(codePoint); + if (block == CJK_UNIFIED_IDEOGRAPHS // Chinese and Kanji + || block == HIRAGANA // Japanese Hiragana + || block == KATAKANA // Japanese Katakana + || block == THAI + || block == LAO + || block == MYANMAR + || block == KHMER + || block == TIBETAN) { + return true; + } + + i += Character.charCount(codePoint); + } + + return false; + } + + + /** + * @return If the phrase will hide all videos. Not an exhaustive check. + */ + private static boolean phrasesWillHideAllVideos(@NonNull String[] phrases, boolean matchWholeWords) { + for (String phrase : phrases) { + for (String commonString : STRINGS_IN_EVERY_BUFFER) { + if (matchWholeWords) { + byte[] commonStringBytes = commonString.getBytes(StandardCharsets.UTF_8); + int matchIndex = 0; + while (true) { + matchIndex = commonString.indexOf(phrase, matchIndex); + if (matchIndex < 0) break; + + if (keywordMatchIsWholeWord(commonStringBytes, matchIndex, phrase.length())) { + return true; + } + + matchIndex++; + } + } else if (Utils.containsAny(commonString, phrases)) { + return true; + } + } + } + + return false; + } + + /** + * @return If the start and end indexes are not surrounded by other letters. + * If the indexes are surrounded by numbers/symbols/punctuation it is considered a whole word. + */ + private static boolean keywordMatchIsWholeWord(byte[] text, int keywordStartIndex, int keywordLength) { + final Integer codePointBefore = getUtf8CodePointBefore(text, keywordStartIndex); + if (codePointBefore != null && Character.isLetter(codePointBefore)) { + return false; + } + + final Integer codePointAfter = getUtf8CodePointAt(text, keywordStartIndex + keywordLength); + //noinspection RedundantIfStatement + if (codePointAfter != null && Character.isLetter(codePointAfter)) { + return false; + } + + return true; + } + + /** + * @return The UTF8 character point immediately before the index, + * or null if the bytes before the index is not a valid UTF8 character. + */ + @Nullable + private static Integer getUtf8CodePointBefore(byte[] data, int index) { + int characterByteCount = 0; + while (--index >= 0 && ++characterByteCount <= UTF8_MAX_BYTE_COUNT) { + if (isValidUtf8(data, index, characterByteCount)) { + return decodeUtf8ToCodePoint(data, index, characterByteCount); + } + } + + return null; + } + + /** + * @return The UTF8 character point at the index, + * or null if the index holds no valid UTF8 character. + */ + @Nullable + private static Integer getUtf8CodePointAt(byte[] data, int index) { + int characterByteCount = 0; + final int dataLength = data.length; + while (index + characterByteCount < dataLength && ++characterByteCount <= UTF8_MAX_BYTE_COUNT) { + if (isValidUtf8(data, index, characterByteCount)) { + return decodeUtf8ToCodePoint(data, index, characterByteCount); + } + } + + return null; + } + + public static boolean isValidUtf8(byte[] data, int startIndex, int numberOfBytes) { + switch (numberOfBytes) { + case 1 -> { // 0xxxxxxx (ASCII) + return (data[startIndex] & 0x80) == 0; + } + case 2 -> { // 110xxxxx, 10xxxxxx + return (data[startIndex] & 0xE0) == 0xC0 + && (data[startIndex + 1] & 0xC0) == 0x80; + } + case 3 -> { // 1110xxxx, 10xxxxxx, 10xxxxxx + return (data[startIndex] & 0xF0) == 0xE0 + && (data[startIndex + 1] & 0xC0) == 0x80 + && (data[startIndex + 2] & 0xC0) == 0x80; + } + case 4 -> { // 11110xxx, 10xxxxxx, 10xxxxxx, 10xxxxxx + return (data[startIndex] & 0xF8) == 0xF0 + && (data[startIndex + 1] & 0xC0) == 0x80 + && (data[startIndex + 2] & 0xC0) == 0x80 + && (data[startIndex + 3] & 0xC0) == 0x80; + } + } + + throw new IllegalArgumentException("numberOfBytes: " + numberOfBytes); + } + + public static int decodeUtf8ToCodePoint(byte[] data, int startIndex, int numberOfBytes) { + switch (numberOfBytes) { + case 1 -> { + return data[startIndex]; + } + case 2 -> { + return ((data[startIndex] & 0x1F) << 6) | + (data[startIndex + 1] & 0x3F); + } + case 3 -> { + return ((data[startIndex] & 0x0F) << 12) | + ((data[startIndex + 1] & 0x3F) << 6) | + (data[startIndex + 2] & 0x3F); + } + case 4 -> { + return ((data[startIndex] & 0x07) << 18) | + ((data[startIndex + 1] & 0x3F) << 12) | + ((data[startIndex + 2] & 0x3F) << 6) | + (data[startIndex + 3] & 0x3F); + } + } + throw new IllegalArgumentException("numberOfBytes: " + numberOfBytes); + } + + private static boolean phraseUsesWholeWordSyntax(String phrase) { + return phrase.startsWith("\"") && phrase.endsWith("\""); + } + + private static String stripWholeWordSyntax(String phrase) { + return phrase.substring(1, phrase.length() - 1); + } + + private synchronized void parseKeywords() { // Must be synchronized since Litho is multi-threaded. + String rawKeywords = Settings.HIDE_KEYWORD_CONTENT_PHRASES.get(); + + //noinspection StringEquality + if (rawKeywords == lastKeywordPhrasesParsed) { + Logger.printDebug(() -> "Using previously initialized search"); + return; // Another thread won the race, and search is already initialized. + } + + ByteTrieSearch search = new ByteTrieSearch(); + String[] split = rawKeywords.split("\n"); + if (split.length != 0) { + // Linked Set so log statement are more organized and easier to read. + // Map is: Phrase -> isWholeWord + Map keywords = new LinkedHashMap<>(10 * split.length); + + for (String phrase : split) { + // Remove any trailing spaces the user may have accidentally included. + phrase = phrase.stripTrailing(); + if (phrase.isBlank()) continue; + + final boolean wholeWordMatching; + if (phraseUsesWholeWordSyntax(phrase)) { + if (phrase.length() == 2) { + continue; // Empty "" phrase + } + phrase = stripWholeWordSyntax(phrase); + wholeWordMatching = true; + } else if (phrase.length() < MINIMUM_KEYWORD_LENGTH && !isLanguageWithNoSpaces(phrase)) { + // Allow phrases of 1 and 2 characters if using a + // language that does not use spaces between words. + + // Do not reset the setting. Keep the invalid keywords so the user can fix the mistake. + Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_length", phrase, MINIMUM_KEYWORD_LENGTH)); + continue; + } else { + wholeWordMatching = false; + } + + // Common casing that might appear. + // + // This could be simplified by adding case insensitive search to the prefix search, + // which is very simple to add to StringTreSearch for Unicode and ByteTrieSearch for ASCII. + // + // But to support Unicode with ByteTrieSearch would require major changes because + // UTF-8 characters can be different byte lengths, which does + // not allow comparing two different byte arrays using simple plain array indexes. + // + // Instead use all common case variations of the words. + String[] phraseVariations = { + phrase, + phrase.toLowerCase(), + titleCaseFirstWordOnly(phrase), + capitalizeAllFirstLetters(phrase), + phrase.toUpperCase() + }; + if (phrasesWillHideAllVideos(phraseVariations, wholeWordMatching)) { + String toastMessage; + // If whole word matching is off, but would pass with on, then show a different toast. + if (!wholeWordMatching && !phrasesWillHideAllVideos(phraseVariations, true)) { + toastMessage = "revanced_hide_keyword_toast_invalid_common_whole_word_required"; + } else { + toastMessage = "revanced_hide_keyword_toast_invalid_common"; + } + + Utils.showToastLong(str(toastMessage, phrase)); + continue; + } + + for (String variation : phraseVariations) { + // Check if the same phrase is declared both with and without quotes. + Boolean existing = keywords.get(variation); + if (existing == null) { + keywords.put(variation, wholeWordMatching); + } else if (existing != wholeWordMatching) { + Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_conflicting", phrase)); + break; + } + } + } + + for (Map.Entry entry : keywords.entrySet()) { + String keyword = entry.getKey(); + //noinspection ExtractMethodRecommender + final boolean isWholeWord = entry.getValue(); + TrieSearch.TriePatternMatchedCallback callback = + (textSearched, startIndex, matchLength, callbackParameter) -> { + if (isWholeWord && !keywordMatchIsWholeWord(textSearched, startIndex, matchLength)) { + return false; + } + + Logger.printDebug(() -> (isWholeWord ? "Matched whole keyword: '" + : "Matched keyword: '") + keyword + "'"); + // noinspection unchecked + ((MutableReference) callbackParameter).value = keyword; + return true; + }; + byte[] stringBytes = keyword.getBytes(StandardCharsets.UTF_8); + search.addPattern(stringBytes, callback); + } + + Logger.printDebug(() -> "Search using: (" + search.getEstimatedMemorySize() + " KB) keywords: " + keywords.keySet()); + } + + bufferSearch = search; + timeToResumeFiltering = 0; + filteredVideosPercentage = 0; + lastKeywordPhrasesParsed = rawKeywords; // Must set last. + } + + public KeywordContentFilter() { + commentsFilterExceptions.addPatterns("engagement_toolbar"); + + commentsFilter = new StringFilterGroup( + Settings.HIDE_KEYWORD_CONTENT_COMMENTS, + "comment_thread.eml" + ); + + // Keywords are parsed on first call to isFiltered() + addPathCallbacks(startsWithFilter, containsFilter, commentsFilter); + } + + private boolean hideKeywordSettingIsActive() { + if (timeToResumeFiltering != 0) { + if (System.currentTimeMillis() < timeToResumeFiltering) { + return false; + } + + timeToResumeFiltering = 0; + filteredVideosPercentage = 0; + Logger.printDebug(() -> "Resuming keyword filtering"); + } + + final boolean hideHome = Settings.HIDE_KEYWORD_CONTENT_HOME.get(); + final boolean hideSearch = Settings.HIDE_KEYWORD_CONTENT_SEARCH.get(); + final boolean hideSubscriptions = Settings.HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS.get(); + + if (!hideHome && !hideSearch && !hideSubscriptions) { + return false; + } else if (hideHome && hideSearch && hideSubscriptions) { + return true; + } + + // Must check player type first, as search bar can be active behind the player. + if (RootView.isPlayerActive()) { + // For now, consider the under video results the same as the home feed. + return hideHome; + } + + // Must check second, as search can be from any tab. + if (RootView.isSearchBarActive()) { + return hideSearch; + } + + NavigationButton selectedNavButton = NavigationButton.getSelectedNavigationButton(); + if (selectedNavButton == null) { + return hideHome; // Unknown tab, treat the same as home. + } + if (selectedNavButton == NavigationButton.HOME) { + return hideHome; + } + if (selectedNavButton == NavigationButton.SUBSCRIPTIONS) { + return hideSubscriptions; + } + // User is in the Library or Notifications tab. + return false; + } + + private void updateStats(boolean videoWasHidden, @Nullable String keyword) { + float updatedAverage = filteredVideosPercentage + * ((ALL_VIDEOS_FILTERED_SAMPLE_SIZE - 1) / ALL_VIDEOS_FILTERED_SAMPLE_SIZE); + if (videoWasHidden) { + updatedAverage += 1 / ALL_VIDEOS_FILTERED_SAMPLE_SIZE; + } + + if (updatedAverage <= ALL_VIDEOS_FILTERED_THRESHOLD) { + filteredVideosPercentage = updatedAverage; + return; + } + + // A keyword is hiding everything. + // Inform the user, and temporarily turn off filtering. + timeToResumeFiltering = System.currentTimeMillis() + ALL_VIDEOS_FILTERED_BACKOFF_MILLISECONDS; + + Logger.printDebug(() -> "Temporarily turning off filtering due to excessively broad filter: " + keyword); + Utils.showToastLong(str("revanced_hide_keyword_toast_invalid_broad", keyword)); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (contentIndex != 0 && matchedGroup == startsWithFilter) { + return false; + } + + // Do not filter if comments path includes an engagement toolbar (like, dislike...) + if (matchedGroup == commentsFilter && commentsFilterExceptions.matches(path)) { + return false; + } + + // Field is intentionally compared using reference equality. + //noinspection StringEquality + if (Settings.HIDE_KEYWORD_CONTENT_PHRASES.get() != lastKeywordPhrasesParsed) { + // User changed the keywords or whole word setting. + parseKeywords(); + } + + if (matchedGroup != commentsFilter && !hideKeywordSettingIsActive()) { + return false; + } + + if (exceptions.matches(path)) { + return false; // Do not update statistics. + } + + MutableReference matchRef = new MutableReference<>(); + if (bufferSearch.matches(protobufBufferArray, matchRef)) { + updateStats(true, matchRef.value); + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + + updateStats(false, null); + return false; + } +} + +/** + * Simple non-atomic wrapper since {@link AtomicReference#setPlain(Object)} is not available with Android 8.0. + */ +final class MutableReference { + T value; +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java new file mode 100644 index 000000000..f124060f0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java @@ -0,0 +1,38 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class LayoutComponentsFilter extends Filter { + private static final String ACCOUNT_HEADER_PATH = "account_header.eml"; + + public LayoutComponentsFilter() { + addIdentifierCallbacks( + new StringFilterGroup( + Settings.HIDE_GRAY_SEPARATOR, + "cell_divider" + ) + ); + + addPathCallbacks( + new StringFilterGroup( + Settings.HIDE_HANDLE, + "|CellType|ContainerType|ContainerType|ContainerType|TextType|" + ) + ); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (contentType == FilterContentType.PATH && !path.startsWith(ACCOUNT_HEADER_PATH)) { + return false; + } + + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilter.java new file mode 100644 index 000000000..87a047299 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilter.java @@ -0,0 +1,52 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.youtube.patches.video.CustomPlaybackSpeedPatch; +import app.revanced.extension.youtube.settings.Settings; + +/** + * Abuse LithoFilter for {@link CustomPlaybackSpeedPatch}. + */ +public final class PlaybackSpeedMenuFilter extends Filter { + /** + * Old litho based speed selection menu. + */ + public static volatile boolean isOldPlaybackSpeedMenuVisible; + + /** + * 0.05x speed selection menu. + */ + public static volatile boolean isPlaybackRateSelectorMenuVisible; + + private final StringFilterGroup oldPlaybackMenuGroup; + + public PlaybackSpeedMenuFilter() { + // 0.05x litho speed menu. + final StringFilterGroup playbackRateSelectorGroup = new StringFilterGroup( + Settings.ENABLE_CUSTOM_PLAYBACK_SPEED, + "playback_rate_selector_menu_sheet.eml-js" + ); + + // Old litho based speed menu. + oldPlaybackMenuGroup = new StringFilterGroup( + Settings.ENABLE_CUSTOM_PLAYBACK_SPEED, + "playback_speed_sheet_content.eml-js"); + + addPathCallbacks(playbackRateSelectorGroup, oldPlaybackMenuGroup); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (matchedGroup == oldPlaybackMenuGroup) { + isOldPlaybackSpeedMenuVisible = true; + } else { + isPlaybackRateSelectorMenuVisible = true; + } + + return false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlayerComponentsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlayerComponentsFilter.java new file mode 100644 index 000000000..8ece2b72a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlayerComponentsFilter.java @@ -0,0 +1,141 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.patches.components.StringFilterGroupList; +import app.revanced.extension.shared.utils.StringTrieSearch; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; + +@SuppressWarnings("unused") +public final class PlayerComponentsFilter extends Filter { + private final StringFilterGroupList channelBarGroupList = new StringFilterGroupList(); + private final StringFilterGroup channelBar; + private final StringTrieSearch suggestedActionsException = new StringTrieSearch(); + private final StringFilterGroup suggestedActions; + + public PlayerComponentsFilter() { + suggestedActionsException.addPatterns( + "channel_bar", + "shorts" + ); + + // The player audio track button does the exact same function as the audio track flyout menu option. + // But if the copy url button is shown, these button clashes and the the audio button does not work. + // Previously this was a setting to show/hide the player button. + // But it was decided it's simpler to always hide this button because: + // - it doesn't work with copy video url feature + // - the button is rare + // - always hiding makes the ReVanced settings simpler and easier to understand + // - nobody is going to notice the redundant button is always hidden + final StringFilterGroup audioTrackButton = new StringFilterGroup( + null, + "multi_feed_icon_button" + ); + + channelBar = new StringFilterGroup( + null, + "channel_bar_inner" + ); + + final StringFilterGroup channelWaterMark = new StringFilterGroup( + Settings.HIDE_CHANNEL_WATERMARK, + "featured_channel_watermark_overlay.eml" + ); + + final StringFilterGroup infoCards = new StringFilterGroup( + Settings.HIDE_INFO_CARDS, + "info_card_teaser_overlay.eml" + ); + + final StringFilterGroup infoPanel = new StringFilterGroup( + Settings.HIDE_INFO_PANEL, + "compact_banner", + "publisher_transparency_panel", + "single_item_information_panel" + ); + + final StringFilterGroup liveChatMessages = new StringFilterGroup( + Settings.HIDE_LIVE_CHAT_MESSAGES, + "live_chat_text_message", + "viewer_engagement_message" // message about poll, not poll itself + ); + + final StringFilterGroup liveChatSummary = new StringFilterGroup( + Settings.HIDE_LIVE_CHAT_SUMMARY, + "live_chat_summary_banner" + ); + + final StringFilterGroup medicalPanel = new StringFilterGroup( + Settings.HIDE_MEDICAL_PANEL, + "emergency_onebox", + "medical_panel" + ); + + final StringFilterGroup seekMessage = new StringFilterGroup( + Settings.HIDE_SEEK_MESSAGE, + "seek_edu_overlay" + ); + + suggestedActions = new StringFilterGroup( + Settings.HIDE_SUGGESTED_ACTION, + "|suggested_action.eml|" + ); + + final StringFilterGroup timedReactions = new StringFilterGroup( + Settings.HIDE_TIMED_REACTIONS, + "emoji_control_panel", + "timed_reaction" + ); + + addPathCallbacks( + audioTrackButton, + channelBar, + channelWaterMark, + infoCards, + infoPanel, + liveChatMessages, + liveChatSummary, + medicalPanel, + seekMessage, + suggestedActions, + timedReactions + ); + + final StringFilterGroup joinMembership = new StringFilterGroup( + Settings.HIDE_JOIN_BUTTON, + "compact_sponsor_button", + "|ContainerType|button.eml|" + ); + + final StringFilterGroup startTrial = new StringFilterGroup( + Settings.HIDE_START_TRIAL_BUTTON, + "channel_purchase_button" + ); + + channelBarGroupList.addAll( + joinMembership, + startTrial + ); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (matchedGroup == suggestedActions) { + // suggested actions button on shorts and the suggested actions button on video players use the same path builder. + // Check PlayerType to make each setting work independently. + if (suggestedActionsException.matches(path) || PlayerType.getCurrent().isNoneOrHidden()) { + return false; + } + } else if (matchedGroup == channelBar) { + if (!channelBarGroupList.check(path).isFiltered()) { + return false; + } + } + + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuFilter.java new file mode 100644 index 000000000..a3bbafdfd --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuFilter.java @@ -0,0 +1,170 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.utils.StringTrieSearch; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; + +@SuppressWarnings("unused") +public final class PlayerFlyoutMenuFilter extends Filter { + private final ByteArrayFilterGroupList flyoutFilterGroupList = new ByteArrayFilterGroupList(); + + private final ByteArrayFilterGroup byteArrayException; + private final StringTrieSearch pathBuilderException = new StringTrieSearch(); + private final StringTrieSearch playerFlyoutMenuFooter = new StringTrieSearch(); + private final StringFilterGroup playerFlyoutMenu; + private final StringFilterGroup qualityHeader; + + public PlayerFlyoutMenuFilter() { + byteArrayException = new ByteArrayFilterGroup( + null, + "quality_sheet" + ); + pathBuilderException.addPattern( + "bottom_sheet_list_option" + ); + playerFlyoutMenuFooter.addPatterns( + "captions_sheet_content.eml", + "quality_sheet_content.eml" + ); + + final StringFilterGroup captionsFooter = new StringFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_CAPTIONS_FOOTER, + "|ContainerType|ContainerType|ContainerType|TextType|", + "|divider.eml|" + ); + + final StringFilterGroup qualityFooter = new StringFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_QUALITY_FOOTER, + "quality_sheet_footer.eml", + "|divider.eml|" + ); + + qualityHeader = new StringFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_QUALITY_HEADER, + "quality_sheet_header.eml" + ); + + playerFlyoutMenu = new StringFilterGroup(null, "overflow_menu_item.eml|"); + + // Using pathFilterGroupList due to new flyout panel(A/B) + addPathCallbacks( + captionsFooter, + qualityFooter, + qualityHeader, + playerFlyoutMenu + ); + + flyoutFilterGroupList.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_AMBIENT, + "yt_outline_screen_light" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_AUDIO_TRACK, + "yt_outline_person_radar" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_CAPTIONS, + "closed_caption" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_HELP, + "yt_outline_question_circle" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_LOCK_SCREEN, + "yt_outline_lock" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_LOOP, + "yt_outline_arrow_repeat_1_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_MORE, + "yt_outline_info_circle" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_PIP, + "yt_fill_picture_in_picture", + "yt_outline_picture_in_picture" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_PLAYBACK_SPEED, + "yt_outline_play_arrow_half_circle" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_PREMIUM_CONTROLS, + "yt_outline_adjust" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_ADDITIONAL_SETTINGS, + "yt_outline_gear" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_REPORT, + "yt_outline_flag" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_STABLE_VOLUME, + "volume_stable" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_SLEEP_TIMER, + "yt_outline_moon_z_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_STATS_FOR_NERDS, + "yt_outline_statistics_graph" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_WATCH_IN_VR, + "yt_outline_vr" + ), + new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_FLYOUT_MENU_YT_MUSIC, + "yt_outline_open_new" + ) + ); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (matchedGroup == playerFlyoutMenu) { + // Overflow menu is always the start of the path. + if (contentIndex != 0) { + return false; + } + // Shorts also use this player flyout panel + if (PlayerType.getCurrent().isNoneOrHidden() || byteArrayException.check(protobufBufferArray).isFiltered()) { + return false; + } + if (flyoutFilterGroupList.check(protobufBufferArray).isFiltered()) { + // Super class handles logging. + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + } else if (matchedGroup == qualityHeader) { + // Quality header is always the start of the path. + if (contentIndex != 0) { + return false; + } + // Super class handles logging. + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } else { + // Components other than the footer separator are not filtered. + if (pathBuilderException.matches(path) || !playerFlyoutMenuFooter.matches(path)) { + return false; + } + // Super class handles logging. + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + + return false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/QuickActionFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/QuickActionFilter.java new file mode 100644 index 000000000..f86e2dfce --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/QuickActionFilter.java @@ -0,0 +1,117 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class QuickActionFilter extends Filter { + private static final String QUICK_ACTION_PATH = "quick_actions.eml"; + private final StringFilterGroup quickActionRule; + + private final StringFilterGroup bufferFilterPathRule; + private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList(); + + private final StringFilterGroup liveChatReplay; + + public QuickActionFilter() { + quickActionRule = new StringFilterGroup(null, QUICK_ACTION_PATH); + addIdentifierCallbacks(quickActionRule); + bufferFilterPathRule = new StringFilterGroup( + null, + "|ContainerType|button.eml|", + "|fullscreen_video_action_button.eml|" + ); + + liveChatReplay = new StringFilterGroup( + Settings.HIDE_LIVE_CHAT_REPLAY_BUTTON, + "live_chat_ep_entrypoint.eml" + ); + + addIdentifierCallbacks(liveChatReplay); + + addPathCallbacks( + new StringFilterGroup( + Settings.HIDE_QUICK_ACTIONS_LIKE_BUTTON, + "|like_button" + ), + new StringFilterGroup( + Settings.HIDE_QUICK_ACTIONS_DISLIKE_BUTTON, + "dislike_button" + ), + new StringFilterGroup( + Settings.HIDE_QUICK_ACTIONS_COMMENT_BUTTON, + "comments_entry_point_button" + ), + new StringFilterGroup( + Settings.HIDE_QUICK_ACTIONS_SAVE_TO_PLAYLIST_BUTTON, + "|save_to_playlist_button" + ), + new StringFilterGroup( + Settings.HIDE_QUICK_ACTIONS_MORE_BUTTON, + "|overflow_menu_button" + ), + new StringFilterGroup( + Settings.HIDE_RELATED_VIDEO_OVERLAY, + "fullscreen_related_videos" + ), + bufferFilterPathRule + ); + + bufferButtonsGroupList.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_QUICK_ACTIONS_COMMENT_BUTTON, + "yt_outline_message_bubble_right" + ), + new ByteArrayFilterGroup( + Settings.HIDE_QUICK_ACTIONS_LIVE_CHAT_BUTTON, + "yt_outline_message_bubble_overlap" + ), + new ByteArrayFilterGroup( + Settings.HIDE_QUICK_ACTIONS_OPEN_MIX_PLAYLIST_BUTTON, + "yt_outline_youtube_mix" + ), + new ByteArrayFilterGroup( + Settings.HIDE_QUICK_ACTIONS_OPEN_PLAYLIST_BUTTON, + "yt_outline_list_play_arrow" + ), + new ByteArrayFilterGroup( + Settings.HIDE_QUICK_ACTIONS_SHARE_BUTTON, + "yt_outline_share" + ) + ); + } + + private boolean isEveryFilterGroupEnabled() { + for (StringFilterGroup group : pathCallbacks) + if (!group.isEnabled()) return false; + + for (ByteArrayFilterGroup group : bufferButtonsGroupList) + if (!group.isEnabled()) return false; + + return true; + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (matchedGroup == liveChatReplay) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + if (!path.startsWith(QUICK_ACTION_PATH)) { + return false; + } + if (matchedGroup == quickActionRule && !isEveryFilterGroupEnabled()) { + return false; + } + if (matchedGroup == bufferFilterPathRule) { + return bufferButtonsGroupList.check(protobufBufferArray).isFiltered(); + } + + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/RelatedVideoFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/RelatedVideoFilter.java new file mode 100644 index 000000000..af9a2fc4c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/RelatedVideoFilter.java @@ -0,0 +1,55 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import java.util.concurrent.atomic.AtomicBoolean; + +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.shared.PlayerType; + +/** + * Here is an unintended behavior: + *

+ * 1. The user does not hide Shorts in the Subscriptions tab, but hides them otherwise. + * 2. Goes to the Subscriptions tab and scrolls to where Shorts is. + * 3. Opens a regular video. + * 4. Minimizes the video and turns off the screen. + * 5. Turns the screen on and maximizes the video. + * 6. Shorts belonging to related videos are not hidden. + *

+ * Here is an explanation of this special issue: + *

+ * When the user minimizes the video, turns off the screen, and then turns it back on, + * the components below the player are reloaded, and at this moment the PlayerType is [WATCH_WHILE_MINIMIZED]. + * (Shorts belonging to related videos are also reloaded) + * Since the PlayerType is [WATCH_WHILE_MINIMIZED] at this moment, the navigation tab is checked. + * (Even though PlayerType is [WATCH_WHILE_MINIMIZED], this is a Shorts belonging to a related video) + *

+ * As a workaround for this special issue, if a video actionbar is detected, which is one of the components below the player, + * it is treated as being in the same state as [WATCH_WHILE_MAXIMIZED]. + */ +public final class RelatedVideoFilter extends Filter { + // Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread. + public static final AtomicBoolean isActionBarVisible = new AtomicBoolean(false); + + public RelatedVideoFilter() { + addIdentifierCallbacks( + new StringFilterGroup( + null, + "video_action_bar.eml" + ) + ); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (PlayerType.getCurrent() == PlayerType.WATCH_WHILE_MINIMIZED && + isActionBarVisible.compareAndSet(false, true)) + Utils.runOnMainThreadDelayed(() -> isActionBarVisible.compareAndSet(true, false), 750); + + return false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ReturnYouTubeChannelNameFilterPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ReturnYouTubeChannelNameFilterPatch.java new file mode 100644 index 000000000..a78ba0a71 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ReturnYouTubeChannelNameFilterPatch.java @@ -0,0 +1,105 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import java.net.URLDecoder; + +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.patches.utils.ReturnYouTubeChannelNamePatch; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings({"unused", "CharsetObjectCanBeUsed"}) +public final class ReturnYouTubeChannelNameFilterPatch extends Filter { + private static final String DELIMITING_CHARACTER = "❙"; + private static final String CHANNEL_ID_IDENTIFIER_CHARACTER = "UC"; + private static final String CHANNEL_ID_IDENTIFIER_WITH_DELIMITING_CHARACTER = + DELIMITING_CHARACTER + CHANNEL_ID_IDENTIFIER_CHARACTER; + private static final String HANDLE_IDENTIFIER_CHARACTER = "@"; + private static final String HANDLE_IDENTIFIER_WITH_DELIMITING_CHARACTER = + HANDLE_IDENTIFIER_CHARACTER + CHANNEL_ID_IDENTIFIER_CHARACTER; + + private final ByteArrayFilterGroupList shortsChannelBarAvatarFilterGroup = new ByteArrayFilterGroupList(); + + public ReturnYouTubeChannelNameFilterPatch() { + addPathCallbacks( + new StringFilterGroup(Settings.REPLACE_CHANNEL_HANDLE, "|reel_channel_bar_inner.eml|") + ); + shortsChannelBarAvatarFilterGroup.addAll( + new ByteArrayFilterGroup(Settings.REPLACE_CHANNEL_HANDLE, "/@") + ); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (shortsChannelBarAvatarFilterGroup.check(protobufBufferArray).isFiltered()) { + setLastShortsChannelId(protobufBufferArray); + } + + return false; + } + + private void setLastShortsChannelId(byte[] protobufBufferArray) { + try { + String[] splitArr; + final String bufferString = findAsciiStrings(protobufBufferArray); + splitArr = bufferString.split(CHANNEL_ID_IDENTIFIER_WITH_DELIMITING_CHARACTER); + if (splitArr.length < 2) { + return; + } + final String splitedBufferString = CHANNEL_ID_IDENTIFIER_CHARACTER + splitArr[1]; + splitArr = splitedBufferString.split(HANDLE_IDENTIFIER_WITH_DELIMITING_CHARACTER); + if (splitArr.length < 2) { + return; + } + splitArr = splitArr[1].split(DELIMITING_CHARACTER); + if (splitArr.length < 1) { + return; + } + final String cachedHandle = HANDLE_IDENTIFIER_CHARACTER + splitArr[0]; + splitArr = splitedBufferString.split(DELIMITING_CHARACTER); + if (splitArr.length < 1) { + return; + } + final String channelId = splitArr[0].replaceAll("\"", "").trim(); + final String handle = URLDecoder.decode(cachedHandle, "UTF-8").trim(); + + ReturnYouTubeChannelNamePatch.setLastShortsChannelId(handle, channelId); + } catch (Exception ex) { + Logger.printException(() -> "setLastShortsChannelId failed", ex); + } + } + + private String findAsciiStrings(byte[] buffer) { + StringBuilder builder = new StringBuilder(Math.max(100, buffer.length / 2)); + builder.append(""); + + // Valid ASCII values (ignore control characters). + final int minimumAscii = 32; // 32 = space character + final int maximumAscii = 126; // 127 = delete character + final int minimumAsciiStringLength = 4; // Minimum length of an ASCII string to include. + String delimitingCharacter = "❙"; // Non ascii character, to allow easier log filtering. + + final int length = buffer.length; + int start = 0; + int end = 0; + while (end < length) { + int value = buffer[end]; + if (value < minimumAscii || value > maximumAscii || end == length - 1) { + if (end - start >= minimumAsciiStringLength) { + for (int i = start; i < end; i++) { + builder.append((char) buffer[i]); + } + builder.append(delimitingCharacter); + } + start = end + 1; + } + end++; + } + return builder.toString(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java new file mode 100644 index 000000000..cc668821e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java @@ -0,0 +1,166 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; + +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.FilterGroup; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.TrieSearch; +import app.revanced.extension.youtube.patches.utils.ReturnYouTubeDislikePatch; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.VideoInformation; + +/** + * @noinspection ALL + *

+ * Searches for video id's in the proto buffer of Shorts dislike. + *

+ * Because multiple litho dislike spans are created in the background + * (and also anytime litho refreshes the components, which is somewhat arbitrary), + * that makes the value of {@link VideoInformation#getVideoId()} and {@link VideoInformation#getPlayerResponseVideoId()} + * unreliable to determine which video id a Shorts litho span belongs to. + *

+ * But the correct video id does appear in the protobuffer just before a Shorts litho span is created. + *

+ * Once a way to asynchronously update litho text is found, this strategy will no longer be needed. + */ +public final class ReturnYouTubeDislikeFilterPatch extends Filter { + + /** + * Last unique video id's loaded. Value is ignored and Map is treated as a Set. + * Cannot use {@link LinkedHashSet} because it's missing #removeEldestEntry(). + */ + @GuardedBy("itself") + private static final Map lastVideoIds = new LinkedHashMap<>() { + /** + * Number of video id's to keep track of for searching thru the buffer. + * A minimum value of 3 should be sufficient, but check a few more just in case. + */ + private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 5; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK; + } + }; + private final ByteArrayFilterGroupList videoIdFilterGroup = new ByteArrayFilterGroupList(); + + public ReturnYouTubeDislikeFilterPatch() { + // When a new Short is opened, the like buttons always seem to load before the dislike. + // But if swiping back to a previous video and liking/disliking, then only that single button reloads. + // So must check for both buttons. + addPathCallbacks( + new StringFilterGroup(null, "|shorts_like_button.eml"), + new StringFilterGroup(null, "|shorts_dislike_button.eml") + ); + + // After the button identifiers is binary data and then the video id for that specific short. + videoIdFilterGroup.addAll( + new ByteArrayFilterGroup(null, "id.reel_like_button"), + new ByteArrayFilterGroup(null, "id.reel_dislike_button") + ); + } + + private volatile static String shortsVideoId = ""; + + public static String getShortsVideoId() { + return shortsVideoId; + } + + /** + * Injection point. + */ + public static void newShortsVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, + @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, + final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { + if (!Settings.RYD_SHORTS.get()) { + return; + } + if (shortsVideoId.equals(newlyLoadedVideoId)) { + return; + } + Logger.printDebug(() -> "newShortsVideoStarted: " + newlyLoadedVideoId); + shortsVideoId = newlyLoadedVideoId; + } + + /** + * Injection point. + */ + public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) { + try { + if (!isShortAndOpeningOrPlaying || !Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) { + return; + } + synchronized (lastVideoIds) { + if (lastVideoIds.put(videoId, Boolean.TRUE) == null) { + Logger.printDebug(() -> "New Short video id: " + videoId); + } + } + } catch (Exception ex) { + Logger.printException(() -> "newPlayerResponseVideoId failure", ex); + } + } + + /** + * This could use {@link TrieSearch}, but since the patterns are constantly changing + * the overhead of updating the Trie might negate the search performance gain. + */ + private static boolean byteArrayContainsString(@NonNull byte[] array, @NonNull String text) { + for (int i = 0, lastArrayStartIndex = array.length - text.length(); i <= lastArrayStartIndex; i++) { + boolean found = true; + for (int j = 0, textLength = text.length(); j < textLength; j++) { + if (array[i + j] != (byte) text.charAt(j)) { + found = false; + break; + } + } + if (found) { + return true; + } + } + + return false; + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (!Settings.RYD_ENABLED.get() || !Settings.RYD_SHORTS.get()) { + return false; + } + + FilterGroup.FilterGroupResult result = videoIdFilterGroup.check(protobufBufferArray); + if (result.isFiltered()) { + String matchedVideoId = findVideoId(protobufBufferArray); + // Matched video will be null if in incognito mode. + // Must pass a null id to correctly clear out the current video data. + // Otherwise if a Short is opened in non-incognito, then incognito is enabled and another Short is opened, + // the new incognito Short will show the old prior data. + ReturnYouTubeDislikePatch.setLastLithoShortsVideoId(matchedVideoId); + } + + return false; + } + + @Nullable + private String findVideoId(byte[] protobufBufferArray) { + synchronized (lastVideoIds) { + for (String videoId : lastVideoIds.keySet()) { + if (byteArrayContainsString(protobufBufferArray, videoId)) { + return videoId; + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShareSheetMenuFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShareSheetMenuFilter.java new file mode 100644 index 000000000..316b0db39 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShareSheetMenuFilter.java @@ -0,0 +1,33 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.youtube.patches.misc.ShareSheetPatch; +import app.revanced.extension.youtube.settings.Settings; + +/** + * Abuse LithoFilter for {@link ShareSheetPatch}. + */ +public final class ShareSheetMenuFilter extends Filter { + // Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread. + public static volatile boolean isShareSheetMenuVisible; + + public ShareSheetMenuFilter() { + addIdentifierCallbacks( + new StringFilterGroup( + Settings.CHANGE_SHARE_SHEET, + "share_sheet_container.eml" + ) + ); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + isShareSheetMenuVisible = true; + + return false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsButtonFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsButtonFilter.java new file mode 100644 index 000000000..463d9ece3 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsButtonFilter.java @@ -0,0 +1,295 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import org.apache.commons.lang3.StringUtils; + +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.youtube.settings.Settings; + +import java.util.regex.Pattern; + +@SuppressWarnings("unused") +public final class ShortsButtonFilter extends Filter { + // Pattern: reel_comment_button … number (of comments) + space? + character / letter (for comments) … 4 (random number), + // probably unstable. + // If comment button does not have number of comments, then it is disabled or with label "0". + private static final Pattern REEL_COMMENTS_DISABLED_PATTERN = Pattern.compile("reel_comment_button.+\\d\\s?\\p{L}.+4"); + private static final String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml"; + private static final String REEL_LIVE_HEADER_PATH = "immersive_live_header.eml"; + /** + * For paid promotion label and subscribe button that appears in the channel bar. + */ + private static final String REEL_METAPANEL_PATH = "reel_metapanel.eml"; + + private static final String SHORTS_PAUSED_STATE_BUTTON_PATH = "|ScrollableContainerType|ContainerType|button.eml|"; + + private final StringFilterGroup subscribeButton; + private final StringFilterGroup joinButton; + private final StringFilterGroup pausedOverlayButtons; + private final StringFilterGroup metaPanelButton; + private final ByteArrayFilterGroupList pausedOverlayButtonsGroupList = new ByteArrayFilterGroupList(); + + private final ByteArrayFilterGroup shortsCommentDisabled; + + private final StringFilterGroup suggestedAction; + private final ByteArrayFilterGroupList suggestedActionsGroupList = new ByteArrayFilterGroupList(); + + private final StringFilterGroup actionButton; + private final ByteArrayFilterGroupList videoActionButtonGroupList = new ByteArrayFilterGroupList(); + + private final ByteArrayFilterGroup useThisSoundButton = new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_USE_THIS_SOUND_BUTTON, + "yt_outline_camera" + ); + + public ShortsButtonFilter() { + StringFilterGroup floatingButton = new StringFilterGroup( + Settings.HIDE_SHORTS_FLOATING_BUTTON, + "floating_action_button" + ); + + addIdentifierCallbacks(floatingButton); + + pausedOverlayButtons = new StringFilterGroup( + null, + "shorts_paused_state" + ); + + StringFilterGroup channelBar = new StringFilterGroup( + Settings.HIDE_SHORTS_CHANNEL_BAR, + REEL_CHANNEL_BAR_PATH + ); + + StringFilterGroup fullVideoLinkLabel = new StringFilterGroup( + Settings.HIDE_SHORTS_FULL_VIDEO_LINK_LABEL, + "reel_multi_format_link" + ); + + StringFilterGroup videoTitle = new StringFilterGroup( + Settings.HIDE_SHORTS_VIDEO_TITLE, + "shorts_video_title_item" + ); + + StringFilterGroup reelSoundMetadata = new StringFilterGroup( + Settings.HIDE_SHORTS_SOUND_METADATA_LABEL, + "reel_sound_metadata" + ); + + StringFilterGroup infoPanel = new StringFilterGroup( + Settings.HIDE_SHORTS_INFO_PANEL, + "shorts_info_panel_overview" + ); + + StringFilterGroup stickers = new StringFilterGroup( + Settings.HIDE_SHORTS_STICKERS, + "stickers_layer.eml" + ); + + StringFilterGroup liveHeader = new StringFilterGroup( + Settings.HIDE_SHORTS_LIVE_HEADER, + "immersive_live_header" + ); + + StringFilterGroup paidPromotionButton = new StringFilterGroup( + Settings.HIDE_SHORTS_PAID_PROMOTION_LABEL, + "reel_player_disclosure.eml" + ); + + metaPanelButton = new StringFilterGroup( + null, + "|ContainerType|button.eml|" + ); + + joinButton = new StringFilterGroup( + Settings.HIDE_SHORTS_JOIN_BUTTON, + "sponsor_button" + ); + + subscribeButton = new StringFilterGroup( + Settings.HIDE_SHORTS_SUBSCRIBE_BUTTON, + "subscribe_button" + ); + + actionButton = new StringFilterGroup( + null, + "shorts_video_action_button.eml" + ); + + suggestedAction = new StringFilterGroup( + null, + "|suggested_action_inner.eml|" + ); + + addPathCallbacks( + suggestedAction, actionButton, joinButton, subscribeButton, metaPanelButton, + paidPromotionButton, pausedOverlayButtons, channelBar, fullVideoLinkLabel, + videoTitle, reelSoundMetadata, infoPanel, liveHeader, stickers + ); + + // + // Action buttons + // + shortsCommentDisabled = + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_COMMENTS_DISABLED_BUTTON, + "reel_comment_button" + ); + + videoActionButtonGroupList.addAll( + // This also appears as the path item 'shorts_like_button.eml' + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_LIKE_BUTTON, + "reel_like_button", + "reel_like_toggled_button" + ), + // This also appears as the path item 'shorts_dislike_button.eml' + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_DISLIKE_BUTTON, + "reel_dislike_button", + "reel_dislike_toggled_button" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_COMMENTS_BUTTON, + "reel_comment_button" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_SHARE_BUTTON, + "reel_share_button" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_REMIX_BUTTON, + "reel_remix_button" + ), + new ByteArrayFilterGroup( + Settings.DISABLE_SHORTS_LIKE_BUTTON_FOUNTAIN_ANIMATION, + "shorts_like_fountain" + ) + ); + + // + // Paused overlay buttons. + // + pausedOverlayButtonsGroupList.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_TRENDS_BUTTON, + "yt_outline_fire_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_SHOPPING_BUTTON, + "yt_outline_bag_" + ) + ); + + // + // Suggested actions. + // + suggestedActionsGroupList.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_TAGGED_PRODUCTS, + // Product buttons show pictures of the products, and does not have any unique icons to identify. + // Instead, use a unique identifier found in the buffer. + "PAproduct_listZ" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_SHOP_BUTTON, + "yt_outline_bag_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_LOCATION_BUTTON, + "yt_outline_location_point_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_SAVE_MUSIC_BUTTON, + "yt_outline_list_add_", + "yt_outline_bookmark_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_SEARCH_SUGGESTIONS_BUTTON, + "yt_outline_search_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_SUPER_THANKS_BUTTON, + "yt_outline_dollar_sign_heart_" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_USE_TEMPLATE_BUTTON, + "yt_outline_template_add" + ), + new ByteArrayFilterGroup( + Settings.HIDE_SHORTS_GREEN_SCREEN_BUTTON, + "shorts_green_screen" + ), + useThisSoundButton + ); + } + + private boolean isEverySuggestedActionFilterEnabled() { + for (ByteArrayFilterGroup group : suggestedActionsGroupList) + if (!group.isEnabled()) return false; + + return true; + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (matchedGroup == subscribeButton || matchedGroup == joinButton) { + // Selectively filter to avoid false positive filtering of other subscribe/join buttons. + if (StringUtils.startsWithAny(path, REEL_CHANNEL_BAR_PATH, REEL_LIVE_HEADER_PATH, REEL_METAPANEL_PATH)) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } + + if (matchedGroup == metaPanelButton) { + if (path.startsWith(REEL_METAPANEL_PATH) && useThisSoundButton.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } + + // Video action buttons (like, dislike, comment, share, remix) have the same path. + if (matchedGroup == actionButton) { + // If the Comment button is hidden, there is no need to check {@code REEL_COMMENTS_DISABLED_PATTERN}. + // Check {@code videoActionButtonGroupList} first. + if (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + + String protobufString = new String(protobufBufferArray); + if (shortsCommentDisabled.check(protobufBufferArray).isFiltered()) { + return !REEL_COMMENTS_DISABLED_PATTERN.matcher(protobufString).find(); + } + return false; + } + + if (matchedGroup == suggestedAction) { + if (isEverySuggestedActionFilterEnabled()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + // Suggested actions can be at the start or in the middle of a path. + if (suggestedActionsGroupList.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } + + if (matchedGroup == pausedOverlayButtons) { + if (Settings.HIDE_SHORTS_PAUSED_OVERLAY_BUTTONS.get()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } else if (StringUtils.contains(path, SHORTS_PAUSED_STATE_BUTTON_PATH)) { + if (pausedOverlayButtonsGroupList.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + } + return false; + } + + // Super class handles logging. + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsCustomActionsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsCustomActionsFilter.java new file mode 100644 index 000000000..bbd515167 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsCustomActionsFilter.java @@ -0,0 +1,174 @@ +package app.revanced.extension.youtube.patches.components; + +import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; + +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroupList; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.TrieSearch; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class ShortsCustomActionsFilter extends Filter { + private static final boolean IS_SPOOFING_TO_YOUTUBE_2023 = + isSpoofingToLessThan("19.00.00"); + private static final boolean SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED = + !IS_SPOOFING_TO_YOUTUBE_2023 && Settings.ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU.get(); + private static final boolean SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED = + Settings.ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR.get(); + private static final boolean SHORTS_CUSTOM_ACTIONS_ENABLED = + SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED || SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED; + + /** + * Last unique video id's loaded. Value is ignored and Map is treated as a Set. + * Cannot use {@link LinkedHashSet} because it's missing #removeEldestEntry(). + */ + @GuardedBy("itself") + private static final Map lastVideoIds = new LinkedHashMap<>() { + /** + * Number of video id's to keep track of for searching thru the buffer. + * A minimum value of 3 should be sufficient, but check a few more just in case. + */ + private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 5; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK; + } + }; + private final ByteArrayFilterGroupList videoIdFilterGroup = new ByteArrayFilterGroupList(); + + private final StringFilterGroup playerFlyoutMenu; + + private final StringFilterGroup likeDislikeButton; + + public static volatile boolean isShortsFlyoutMenuVisible; + + public ShortsCustomActionsFilter() { + likeDislikeButton = new StringFilterGroup( + null, + "|shorts_like_button.eml", + "|shorts_dislike_button.eml" + ); + playerFlyoutMenu = new StringFilterGroup( + null, + "overflow_menu_item.eml|" + ); + + addIdentifierCallbacks(playerFlyoutMenu); + addPathCallbacks(likeDislikeButton); + + // After the button identifiers is binary data and then the video id for that specific short. + videoIdFilterGroup.addAll( + new ByteArrayFilterGroup(null, "id.reel_like_button"), + new ByteArrayFilterGroup(null, "id.reel_dislike_button") + ); + } + + private volatile static String shortsVideoId = ""; + + private static void setShortsVideoId(@NonNull String videoId, boolean isLive) { + if (shortsVideoId.equals(videoId)) { + return; + } + final String prefix = isLive ? "New Short livestream video id: " : "New Short video id: "; + Logger.printDebug(() -> prefix + videoId); + shortsVideoId = videoId; + } + + public static String getShortsVideoId() { + return shortsVideoId; + } + + /** + * Injection point. + */ + public static void newShortsVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, + @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, + final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { + if (!SHORTS_CUSTOM_ACTIONS_ENABLED) { + return; + } + if (!newlyLoadedLiveStreamValue) { + return; + } + setShortsVideoId(newlyLoadedVideoId, true); + } + + /** + * Injection point. + */ + public static void newPlayerResponseVideoId(String videoId, boolean isShortAndOpeningOrPlaying) { + try { + if (!SHORTS_CUSTOM_ACTIONS_ENABLED) { + return; + } + if (!isShortAndOpeningOrPlaying) { + return; + } + synchronized (lastVideoIds) { + lastVideoIds.putIfAbsent(videoId, Boolean.TRUE); + } + } catch (Exception ex) { + Logger.printException(() -> "newPlayerResponseVideoId failure", ex); + } + } + + + /** + * This could use {@link TrieSearch}, but since the patterns are constantly changing + * the overhead of updating the Trie might negate the search performance gain. + */ + private static boolean byteArrayContainsString(@NonNull byte[] array, @NonNull String text) { + for (int i = 0, lastArrayStartIndex = array.length - text.length(); i <= lastArrayStartIndex; i++) { + boolean found = true; + for (int j = 0, textLength = text.length(); j < textLength; j++) { + if (array[i + j] != (byte) text.charAt(j)) { + found = false; + break; + } + } + if (found) { + return true; + } + } + + return false; + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (!SHORTS_CUSTOM_ACTIONS_ENABLED) { + return false; + } + if (matchedGroup == playerFlyoutMenu) { + isShortsFlyoutMenuVisible = true; + findVideoId(protobufBufferArray); + } else if (matchedGroup == likeDislikeButton && videoIdFilterGroup.check(protobufBufferArray).isFiltered()) { + findVideoId(protobufBufferArray); + } + + return false; + } + + private void findVideoId(byte[] protobufBufferArray) { + synchronized (lastVideoIds) { + for (String videoId : lastVideoIds.keySet()) { + if (byteArrayContainsString(protobufBufferArray, videoId)) { + setShortsVideoId(videoId, false); + } + } + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsShelfFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsShelfFilter.java new file mode 100644 index 000000000..50924fae9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsShelfFilter.java @@ -0,0 +1,195 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.patches.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.StringTrieSearch; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; +import app.revanced.extension.youtube.shared.RootView; + +@SuppressWarnings("unused") +public final class ShortsShelfFilter extends Filter { + private static final String BROWSE_ID_HISTORY = "FEhistory"; + private static final String BROWSE_ID_LIBRARY = "FElibrary"; + private static final String BROWSE_ID_NOTIFICATION_INBOX = "FEnotifications_inbox"; + private static final String BROWSE_ID_SUBSCRIPTIONS = "FEsubscriptions"; + private static final String CONVERSATION_CONTEXT_FEED_IDENTIFIER = + "horizontalCollectionSwipeProtector=null"; + private static final String SHELF_HEADER_PATH = "shelf_header.eml"; + private final StringFilterGroup channelProfile; + private final StringFilterGroup compactFeedVideoPath; + private final ByteArrayFilterGroup compactFeedVideoBuffer; + private final StringFilterGroup shelfHeaderIdentifier; + private final StringFilterGroup shelfHeaderPath; + private static final StringTrieSearch feedGroup = new StringTrieSearch(); + private static final BooleanSetting hideShortsShelf = Settings.HIDE_SHORTS_SHELF; + private static final BooleanSetting hideChannel = Settings.HIDE_SHORTS_SHELF_CHANNEL; + private static final ByteArrayFilterGroup channelProfileShelfHeader = + new ByteArrayFilterGroup( + hideChannel, + "Shorts" + ); + + public ShortsShelfFilter() { + feedGroup.addPattern(CONVERSATION_CONTEXT_FEED_IDENTIFIER); + + channelProfile = new StringFilterGroup( + hideChannel, + "shorts_pivot_item" + ); + + final StringFilterGroup shortsIdentifiers = new StringFilterGroup( + hideShortsShelf, + "shorts_shelf", + "inline_shorts", + "shorts_grid", + "shorts_video_cell" + ); + + shelfHeaderIdentifier = new StringFilterGroup( + hideShortsShelf, + SHELF_HEADER_PATH + ); + + addIdentifierCallbacks(channelProfile, shortsIdentifiers, shelfHeaderIdentifier); + + compactFeedVideoPath = new StringFilterGroup( + hideShortsShelf, + // Shorts that appear in the feed/search when the device is using tablet layout. + "compact_video.eml", + // 'video_lockup_with_attachment.eml' is used instead of 'compact_video.eml' for some users. (A/B tests) + "video_lockup_with_attachment.eml", + // Search results that appear in a horizontal shelf. + "video_card.eml" + ); + + // Filter out items that use the 'frame0' thumbnail. + // This is a valid thumbnail for both regular videos and Shorts, + // but it appears these thumbnails are used only for Shorts. + compactFeedVideoBuffer = new ByteArrayFilterGroup( + hideShortsShelf, + "/frame0.jpg" + ); + + // Feed Shorts shelf header. + // Use a different filter group for this pattern, as it requires an additional check after matching. + shelfHeaderPath = new StringFilterGroup( + hideShortsShelf, + SHELF_HEADER_PATH + ); + + addPathCallbacks(compactFeedVideoPath, shelfHeaderPath); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + final boolean playerActive = RootView.isPlayerActive(); + final boolean searchBarActive = RootView.isSearchBarActive(); + final NavigationButton navigationButton = NavigationButton.getSelectedNavigationButton(); + final String navigation = navigationButton == null ? "null" : navigationButton.name(); + final String browseId = RootView.getBrowseId(); + final boolean hideShelves = shouldHideShortsFeedItems(playerActive, searchBarActive, navigationButton, browseId); + Logger.printDebug(() -> "hideShelves: " + hideShelves + "\nplayerActive: " + playerActive + "\nsearchBarActive: " + searchBarActive + "\nbrowseId: " + browseId + "\nnavigation: " + navigation); + if (contentType == FilterContentType.PATH) { + if (matchedGroup == compactFeedVideoPath) { + if (hideShelves && compactFeedVideoBuffer.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } else if (matchedGroup == shelfHeaderPath) { + // 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 (!channelProfileShelfHeader.check(protobufBufferArray).isFiltered()) { + return false; + } + if (feedGroup.matches(allValue)) { + return false; + } + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + } else if (contentType == FilterContentType.IDENTIFIER) { + // Feed/search identifier components. + if (matchedGroup == shelfHeaderIdentifier) { + // Check ConversationContext to not hide shelf header in channel profile + // This value does not exist in the shelf header in the channel profile + if (!feedGroup.matches(allValue)) { + return false; + } + } else if (matchedGroup == channelProfile) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + if (!hideShelves) { + return false; + } + } + + // Super class handles logging. + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + + private static boolean shouldHideShortsFeedItems(boolean playerActive, boolean searchBarActive, NavigationButton selectedNavButton, String browseId) { + final boolean hideHomeAndRelatedVideos = Settings.HIDE_SHORTS_SHELF_HOME_RELATED_VIDEOS.get(); + final boolean hideSubscriptions = Settings.HIDE_SHORTS_SHELF_SUBSCRIPTIONS.get(); + final boolean hideSearch = Settings.HIDE_SHORTS_SHELF_SEARCH.get(); + final boolean hideHistory = Settings.HIDE_SHORTS_SHELF_HISTORY.get(); + + if (hideHomeAndRelatedVideos && hideSubscriptions && hideSearch && hideHistory) { + // Shorts suggestions can load in the background if a video is opened and + // then immediately minimized before any suggestions are loaded. + // In this state the player type will show minimized, which makes it not possible to + // distinguish between Shorts suggestions loading in the player and between + // scrolling thru search/home/subscription tabs while a player is minimized. + // + // To avoid this situation for users that never want to show Shorts (all hide Shorts options are enabled) + // then hide all Shorts everywhere including the Library history and Library playlists. + return true; + } + + // Must check player type first, as search bar can be active behind the player. + if (playerActive) { + // For now, consider the under video results the same as the home feed. + return hideHomeAndRelatedVideos; + } + + // Must check second, as search can be from any tab. + if (searchBarActive) { + return hideSearch; + } + + // Avoid checking navigation button status if all other Shorts should show. + if (!hideHomeAndRelatedVideos && !hideSubscriptions && !hideHistory) { + return false; + } + + // Unknown tab, treat the same as home. + if (selectedNavButton == null) { + return hideHomeAndRelatedVideos; + } + + // Fixes a very rare bug in home. + if (selectedNavButton == NavigationButton.HOME && browseId.equals(BROWSE_ID_NOTIFICATION_INBOX)) { + return true; + } + + switch (browseId) { + case BROWSE_ID_HISTORY, BROWSE_ID_LIBRARY, BROWSE_ID_NOTIFICATION_INBOX -> { + return hideHistory; + } + case BROWSE_ID_SUBSCRIPTIONS -> { + return hideSubscriptions; + } + default -> { + return hideHomeAndRelatedVideos; + } + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/VideoQualityMenuFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/VideoQualityMenuFilter.java new file mode 100644 index 000000000..812eabbc4 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/VideoQualityMenuFilter.java @@ -0,0 +1,33 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.patches.components.Filter; +import app.revanced.extension.shared.patches.components.StringFilterGroup; +import app.revanced.extension.youtube.patches.video.RestoreOldVideoQualityMenuPatch; +import app.revanced.extension.youtube.settings.Settings; + +/** + * Abuse LithoFilter for {@link RestoreOldVideoQualityMenuPatch}. + */ +public final class VideoQualityMenuFilter extends Filter { + // Must be volatile or synchronized, as litho filtering runs off main thread and this field is then access from the main thread. + public static volatile boolean isVideoQualityMenuVisible; + + public VideoQualityMenuFilter() { + addPathCallbacks( + new StringFilterGroup( + Settings.RESTORE_OLD_VIDEO_QUALITY_MENU, + "quick_quality_sheet_content.eml-js" + ) + ); + } + + @Override + public boolean isFiltered(String path, @Nullable String identifier, String allValue, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + isVideoQualityMenuVisible = true; + + return false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/feed/FeedPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/feed/FeedPatch.java new file mode 100644 index 000000000..9ab4dd81d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/feed/FeedPatch.java @@ -0,0 +1,221 @@ +package app.revanced.extension.youtube.patches.feed; + +import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition; +import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class FeedPatch { + + // region [Hide feed components] patch + + public static int hideCategoryBarInFeed(final int height) { + return Settings.HIDE_CATEGORY_BAR_IN_FEED.get() ? 0 : height; + } + + public static void hideCategoryBarInRelatedVideos(final View chipView) { + Utils.hideViewBy0dpUnderCondition( + Settings.HIDE_CATEGORY_BAR_IN_RELATED_VIDEOS.get() || Settings.HIDE_RELATED_VIDEOS.get(), + chipView + ); + } + + public static int hideCategoryBarInSearch(final int height) { + return Settings.HIDE_CATEGORY_BAR_IN_SEARCH.get() ? 0 : height; + } + + /** + * Rather than simply hiding the channel tab view, completely removes channel tab from list. + * If a channel tab is removed from the list, users will not be able to open it by swiping. + * + * @param channelTabText Text to be assigned to channel tab, such as 'Shorts', 'Playlists', 'Community', 'Store'. + * This text is hardcoded, so it follows the user's language. + * @return Whether to remove the channel tab from the list. + */ + public static boolean hideChannelTab(String channelTabText) { + if (!Settings.HIDE_CHANNEL_TAB.get()) { + return false; + } + if (channelTabText == null || channelTabText.isEmpty()) { + return false; + } + + String[] blockList = Settings.HIDE_CHANNEL_TAB_FILTER_STRINGS.get().split("\\n"); + + for (String filter : blockList) { + if (!filter.isEmpty() && channelTabText.equals(filter)) { + return true; + } + } + + return false; + } + + public static void hideBreakingNewsShelf(View view) { + hideViewBy0dpUnderCondition( + Settings.HIDE_CAROUSEL_SHELF.get(), + view + ); + } + + public static View hideCaptionsButton(View view) { + return Settings.HIDE_FEED_CAPTIONS_BUTTON.get() ? null : view; + } + + public static void hideCaptionsButtonContainer(View view) { + hideViewUnderCondition( + Settings.HIDE_FEED_CAPTIONS_BUTTON, + view + ); + } + + public static String hideFloatingButton(String fab) { + return Settings.HIDE_FLOATING_BUTTON.get() ? null : fab; + } + + public static void hideLatestVideosButton(View view) { + hideViewUnderCondition(Settings.HIDE_LATEST_VIDEOS_BUTTON.get(), view); + } + + public static boolean hideSubscriptionsChannelSection() { + return Settings.HIDE_SUBSCRIPTIONS_CAROUSEL.get(); + } + + public static void hideSubscriptionsChannelSection(View view) { + hideViewUnderCondition(Settings.HIDE_SUBSCRIPTIONS_CAROUSEL, view); + } + + private static FrameLayout.LayoutParams layoutParams; + private static int minimumHeight = -1; + private static int paddingLeft = 12; + private static int paddingTop = 0; + private static int paddingRight = 12; + private static int paddingBottom = 0; + + /** + * expandButtonContainer is used in channel profiles as well as search results. + * We need to hide expandButtonContainer only in search results, not in channel profile. + *

+ * If we hide expandButtonContainer with setVisibility, the empty space occupied by expandButtonContainer will still be left. + * Therefore, we need to dynamically resize the View with LayoutParams. + *

+ * Unlike other Views, expandButtonContainer cannot make a View invisible using the normal {@link Utils#hideViewByLayoutParams} method. + * We should set the parent view's padding and MinimumHeight to 0 to completely hide the expandButtonContainer. + * + * @param parentView Parent view of expandButtonContainer. + */ + public static void hideShowMoreButton(View parentView) { + if (!Settings.HIDE_SHOW_MORE_BUTTON.get()) + return; + + if (!(parentView instanceof ViewGroup viewGroup)) + return; + + if (!(viewGroup.getChildAt(0) instanceof ViewGroup expandButtonContainer)) + return; + + if (layoutParams == null) { + // We need to get the original LayoutParams and paddings applied to expandButtonContainer. + // Theses are used to make the expandButtonContainer visible again. + if (expandButtonContainer.getLayoutParams() instanceof FrameLayout.LayoutParams lp) { + layoutParams = lp; + paddingLeft = parentView.getPaddingLeft(); + paddingTop = parentView.getPaddingTop(); + paddingRight = parentView.getPaddingRight(); + paddingBottom = parentView.getPaddingBottom(); + } + } + + // I'm not sure if 'Utils.runOnMainThreadDelayed' is absolutely necessary. + Utils.runOnMainThreadDelayed(() -> { + // MinimumHeight is also needed to make expandButtonContainer visible again. + // Get original MinimumHeight. + if (minimumHeight == -1) { + minimumHeight = parentView.getMinimumHeight(); + } + + // In the search results, the child view structure of expandButtonContainer is as follows: + // expandButtonContainer + // L TextView (first child view is SHOWN, 'Show more' text) + // L ImageView (second child view is shown, dropdown arrow icon) + + // In the channel profiles, the child view structure of expandButtonContainer is as follows: + // expandButtonContainer + // L TextView (first child view is HIDDEN, 'Show more' text) + // L ImageView (second child view is shown, dropdown arrow icon) + + if (expandButtonContainer.getChildAt(0).getVisibility() != View.VISIBLE && layoutParams != null) { + // If the first child view (TextView) is HIDDEN, the channel profile is open. + // Restore parent view's padding and MinimumHeight to make them visible. + parentView.setMinimumHeight(minimumHeight); + parentView.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom); + expandButtonContainer.setLayoutParams(layoutParams); + } else { + // If the first child view (TextView) is SHOWN, the search results is open. + // Set the parent view's padding and MinimumHeight to 0 to completely hide the expandButtonContainer. + parentView.setMinimumHeight(0); + parentView.setPadding(0, 0, 0, 0); + expandButtonContainer.setLayoutParams(new FrameLayout.LayoutParams(0, 0)); + } + }, 0 + ); + } + + // endregion + + // region [Hide feed flyout menu] patch + + /** + * hide feed flyout menu for phone + * + * @param menuTitleCharSequence menu title + */ + @Nullable + public static CharSequence hideFlyoutMenu(@Nullable CharSequence menuTitleCharSequence) { + if (menuTitleCharSequence != null && Settings.HIDE_FEED_FLYOUT_MENU.get()) { + String[] blockList = Settings.HIDE_FEED_FLYOUT_MENU_FILTER_STRINGS.get().split("\\n"); + String menuTitleString = menuTitleCharSequence.toString(); + + for (String filter : blockList) { + if (menuTitleString.equals(filter) && !filter.isEmpty()) + return null; + } + } + + return menuTitleCharSequence; + } + + /** + * hide feed flyout panel for tablet + * + * @param menuTextView flyout text view + * @param menuTitleCharSequence raw text + */ + public static void hideFlyoutMenu(TextView menuTextView, CharSequence menuTitleCharSequence) { + if (menuTitleCharSequence == null || !Settings.HIDE_FEED_FLYOUT_MENU.get()) + return; + + if (!(menuTextView.getParent() instanceof View parentView)) + return; + + String[] blockList = Settings.HIDE_FEED_FLYOUT_MENU_FILTER_STRINGS.get().split("\\n"); + String menuTitleString = menuTitleCharSequence.toString(); + + for (String filter : blockList) { + if (menuTitleString.equals(filter) && !filter.isEmpty()) + Utils.hideViewByLayoutParams(parentView); + } + } + + // endregion + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/feed/RelatedVideoPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/feed/RelatedVideoPatch.java new file mode 100644 index 000000000..ccc20a631 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/feed/RelatedVideoPatch.java @@ -0,0 +1,49 @@ +package app.revanced.extension.youtube.patches.feed; + +import androidx.annotation.Nullable; + +import java.util.concurrent.atomic.AtomicBoolean; + +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.BottomSheetState; +import app.revanced.extension.youtube.shared.RootView; + +@SuppressWarnings("unused") +public final class RelatedVideoPatch { + private static final boolean HIDE_RELATED_VIDEOS = Settings.HIDE_RELATED_VIDEOS.get(); + + private static final int OFFSET = Settings.RELATED_VIDEOS_OFFSET.get(); + + // video title,channel bar, video action bar, comment + private static final int MAX_ITEM_COUNT = 4 + OFFSET; + + private static final AtomicBoolean engagementPanelOpen = new AtomicBoolean(false); + + public static void showEngagementPanel(@Nullable Object object) { + engagementPanelOpen.set(object != null); + } + + public static void hideEngagementPanel() { + engagementPanelOpen.compareAndSet(true, false); + } + + public static int overrideItemCounts(int itemCounts) { + if (!HIDE_RELATED_VIDEOS) { + return itemCounts; + } + if (itemCounts < MAX_ITEM_COUNT) { + return itemCounts; + } + if (!RootView.isPlayerActive()) { + return itemCounts; + } + if (BottomSheetState.getCurrent().isOpen()) { + return itemCounts; + } + if (engagementPanelOpen.get()) { + return itemCounts; + } + return MAX_ITEM_COUNT; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeStartPagePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeStartPagePatch.java new file mode 100644 index 000000000..5ee65fc0b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeStartPagePatch.java @@ -0,0 +1,135 @@ +package app.revanced.extension.youtube.patches.general; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + +import android.content.Intent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; + +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class ChangeStartPagePatch { + + public enum StartPage { + /** + * Unmodified type, and same as un-patched. + */ + ORIGINAL("", null), + + /** + * Browse id. + */ + BROWSE("FEguide_builder", TRUE), + EXPLORE("FEexplore", TRUE), + HISTORY("FEhistory", TRUE), + LIBRARY("FElibrary", TRUE), + MOVIE("FEstorefront", TRUE), + SUBSCRIPTIONS("FEsubscriptions", TRUE), + TRENDING("FEtrending", TRUE), + + /** + * Channel id, this can be used as a browseId. + */ + GAMING("UCOpNcN46UbXVtpKMrmU4Abg", TRUE), + LIVE("UC4R8DWoMoI7CAwX8_LjQHig", TRUE), + MUSIC("UC-9-kyTW8ZkZNDHQJ6FgpwQ", TRUE), + SPORTS("UCEgdi0XIXXZ-qJOFPf4JSKw", TRUE), + COURSES("UCtFRv9O2AHqOZjjynzrv-xg", TRUE), + + /** + * Playlist id, this can be used as a browseId. + */ + LIKED_VIDEO("VLLL", TRUE), + WATCH_LATER("VLWL", TRUE), + + /** + * Intent action. + */ + SEARCH("com.google.android.youtube.action.open.search", FALSE), + SHORTS("com.google.android.youtube.action.open.shorts", FALSE); + + @Nullable + final Boolean isBrowseId; + + @NonNull + final String id; + + StartPage(@NonNull String id, @Nullable Boolean isBrowseId) { + this.id = id; + this.isBrowseId = isBrowseId; + } + + private boolean isBrowseId() { + return BooleanUtils.isTrue(isBrowseId); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean isIntentAction() { + return BooleanUtils.isFalse(isBrowseId); + } + } + + /** + * Intent action when YouTube is cold started from the launcher. + *

+ * If you don't check this, the hooking will also apply in the following cases: + * Case 1. The user clicked Shorts button on the YouTube shortcut. + * Case 2. The user clicked Shorts button on the YouTube widget. + * In this case, instead of opening Shorts, the start page specified by the user is opened. + */ + private static final String ACTION_MAIN = "android.intent.action.MAIN"; + + private static final StartPage START_PAGE = Settings.CHANGE_START_PAGE.get(); + private static final boolean ALWAYS_CHANGE_START_PAGE = Settings.CHANGE_START_PAGE_TYPE.get(); + + /** + * There is an issue where the back button on the toolbar doesn't work properly. + * As a workaround for this issue, instead of overriding the browserId multiple times, just override it once. + */ + private static boolean appLaunched = false; + + public static String overrideBrowseId(@NonNull String original) { + if (!START_PAGE.isBrowseId()) { + return original; + } + if (!ALWAYS_CHANGE_START_PAGE && appLaunched) { + Logger.printDebug(() -> "Ignore override browseId as the app already launched"); + return original; + } + appLaunched = true; + + final String browseId = START_PAGE.id; + Logger.printDebug(() -> "Changing browseId to " + browseId); + return browseId; + } + + public static void overrideIntentAction(@NonNull Intent intent) { + if (!START_PAGE.isIntentAction()) { + return; + } + if (!StringUtils.equals(intent.getAction(), ACTION_MAIN)) { + Logger.printDebug(() -> "Ignore override intent action" + + " as the current activity is not the entry point of the application"); + return; + } + + final String intentAction = START_PAGE.id; + Logger.printDebug(() -> "Changing intent action to " + intentAction); + intent.setAction(intentAction); + } + + public static final class ChangeStartPageTypeAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return Settings.CHANGE_START_PAGE.get() != StartPage.ORIGINAL; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/DownloadActionsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/DownloadActionsPatch.java new file mode 100644 index 000000000..0c1607561 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/DownloadActionsPatch.java @@ -0,0 +1,98 @@ +package app.revanced.extension.youtube.patches.general; + +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.VideoUtils; + +@SuppressWarnings("unused") +public final class DownloadActionsPatch extends VideoUtils { + + private static final BooleanSetting overrideVideoDownloadButton = + Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON; + + private static final BooleanSetting overridePlaylistDownloadButton = + Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON; + + /** + * Injection point. + *

+ * Called from the in app download hook, + * for both the player action button (below the video) + * and the 'Download video' flyout option for feed videos. + *

+ * Appears to always be called from the main thread. + */ + public static boolean inAppVideoDownloadButtonOnClick(String videoId) { + try { + if (!overrideVideoDownloadButton.get()) { + return false; + } + if (videoId == null || videoId.isEmpty()) { + return false; + } + launchVideoExternalDownloader(videoId); + + return true; + } catch (Exception ex) { + Logger.printException(() -> "inAppVideoDownloadButtonOnClick failure", ex); + } + return false; + } + + /** + * Injection point. + *

+ * Called from the in app playlist download hook. + *

+ * Appears to always be called from the main thread. + */ + public static String inAppPlaylistDownloadButtonOnClick(String playlistId) { + try { + if (!overridePlaylistDownloadButton.get()) { + return playlistId; + } + if (playlistId == null || playlistId.isEmpty()) { + return playlistId; + } + launchPlaylistExternalDownloader(playlistId); + + return ""; + } catch (Exception ex) { + Logger.printException(() -> "inAppPlaylistDownloadButtonOnClick failure", ex); + } + return playlistId; + } + + /** + * Injection point. + *

+ * Called from the 'Download playlist' flyout option. + *

+ * Appears to always be called from the main thread. + */ + public static boolean inAppPlaylistDownloadMenuOnClick(String playlistId) { + try { + if (!overridePlaylistDownloadButton.get()) { + return false; + } + if (playlistId == null || playlistId.isEmpty()) { + return false; + } + launchPlaylistExternalDownloader(playlistId); + + return true; + } catch (Exception ex) { + Logger.printException(() -> "inAppPlaylistDownloadMenuOnClick failure", ex); + } + return false; + } + + /** + * Injection point. + */ + public static boolean overridePlaylistDownloadButtonVisibility() { + return overridePlaylistDownloadButton.get(); + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java new file mode 100644 index 000000000..881a49a30 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java @@ -0,0 +1,650 @@ +package app.revanced.extension.youtube.patches.general; + +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.getChildView; +import static app.revanced.extension.shared.utils.Utils.hideViewByLayoutParams; +import static app.revanced.extension.shared.utils.Utils.hideViewGroupByMarginLayoutParams; +import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition; +import static app.revanced.extension.shared.utils.Utils.isSDKAbove; +import static app.revanced.extension.youtube.patches.utils.PatchStatus.ImageSearchButton; +import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.util.TypedValue; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewGroup.LayoutParams; +import android.view.ViewGroup.MarginLayoutParams; +import android.view.Window; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.google.android.apps.youtube.app.application.Shell_SettingsActivity; +import com.google.android.apps.youtube.app.settings.SettingsActivity; +import com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity; + +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.EnumMap; +import java.util.Map; +import java.util.Objects; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.ThemeUtils; + +@SuppressWarnings("unused") +public class GeneralPatch { + + // region [Disable auto audio tracks] patch + + private static final String DEFAULT_AUDIO_TRACKS_IDENTIFIER = "original"; + private static ArrayList formatStreamModelArray; + + /** + * Find the stream format containing the parameter {@link GeneralPatch#DEFAULT_AUDIO_TRACKS_IDENTIFIER}, and save to the array. + * + * @param formatStreamModel stream format model including audio tracks. + */ + public static void setFormatStreamModelArray(final Object formatStreamModel) { + if (!Settings.DISABLE_AUTO_AUDIO_TRACKS.get()) { + return; + } + + // Ignoring, as the stream format model array has already been added. + if (formatStreamModelArray != null) { + return; + } + + // Ignoring, as it is not an original audio track. + if (!formatStreamModel.toString().contains(DEFAULT_AUDIO_TRACKS_IDENTIFIER)) { + return; + } + + // For some reason, when YouTube handles formatStreamModelArray, + // it uses an array with duplicate values at the first and second indices. + formatStreamModelArray = new ArrayList<>(); + formatStreamModelArray.add(formatStreamModel); + formatStreamModelArray.add(formatStreamModel); + } + + /** + * Returns an array of stream format models containing the default audio tracks. + * + * @param localizedFormatStreamModelArray stream format model array consisting of audio tracks in the system's language. + * @return stream format model array consisting of original audio tracks. + */ + public static ArrayList getFormatStreamModelArray(final ArrayList localizedFormatStreamModelArray) { + if (!Settings.DISABLE_AUTO_AUDIO_TRACKS.get()) { + return localizedFormatStreamModelArray; + } + + // Ignoring, as the stream format model array is empty. + if (formatStreamModelArray == null || formatStreamModelArray.isEmpty()) { + return localizedFormatStreamModelArray; + } + + // Initialize the array before returning it. + ArrayList defaultFormatStreamModelArray = formatStreamModelArray; + formatStreamModelArray = null; + return defaultFormatStreamModelArray; + } + + // endregion + + // region [Disable splash animation] patch + + public static boolean disableSplashAnimation(boolean original) { + try { + return !Settings.DISABLE_SPLASH_ANIMATION.get() && original; + } catch (Exception ex) { + Logger.printException(() -> "Failed to load disableSplashAnimation", ex); + } + return original; + } + + // endregion + + // region [Enable gradient loading screen] patch + + public static boolean enableGradientLoadingScreen() { + return Settings.ENABLE_GRADIENT_LOADING_SCREEN.get(); + } + + // endregion + + // region [Hide layout components] patch + + private static String[] accountMenuBlockList; + + static { + accountMenuBlockList = Settings.HIDE_ACCOUNT_MENU_FILTER_STRINGS.get().split("\\n"); + // Some settings should not be hidden. + accountMenuBlockList = Arrays.stream(accountMenuBlockList) + .filter(item -> !Objects.equals(item, str("settings"))) + .toArray(String[]::new); + } + + /** + * hide account menu in you tab + * + * @param menuTitleCharSequence menu title + */ + public static void hideAccountList(View view, CharSequence menuTitleCharSequence) { + if (!Settings.HIDE_ACCOUNT_MENU.get()) + return; + if (menuTitleCharSequence == null) + return; + if (!(view.getParent().getParent().getParent() instanceof ViewGroup viewGroup)) + return; + + hideAccountMenu(viewGroup, menuTitleCharSequence.toString()); + } + + /** + * hide account menu for tablet and old clients + * + * @param menuTitleCharSequence menu title + */ + public static void hideAccountMenu(View view, CharSequence menuTitleCharSequence) { + if (!Settings.HIDE_ACCOUNT_MENU.get()) + return; + if (menuTitleCharSequence == null) + return; + if (!(view.getParent().getParent() instanceof ViewGroup viewGroup)) + return; + + hideAccountMenu(viewGroup, menuTitleCharSequence.toString()); + } + + private static void hideAccountMenu(ViewGroup viewGroup, String menuTitleString) { + for (String filter : accountMenuBlockList) { + if (!filter.isEmpty() && menuTitleString.equals(filter)) { + if (viewGroup.getLayoutParams() instanceof MarginLayoutParams) + hideViewGroupByMarginLayoutParams(viewGroup); + else + viewGroup.setLayoutParams(new LayoutParams(0, 0)); + } + } + } + + public static int hideHandle(int originalValue) { + return Settings.HIDE_HANDLE.get() ? 8 : originalValue; + } + + public static boolean hideFloatingMicrophone(boolean original) { + return Settings.HIDE_FLOATING_MICROPHONE.get() || original; + } + + public static boolean hideSnackBar() { + return Settings.HIDE_SNACK_BAR.get(); + } + + // endregion + + // region [Hide navigation bar components] patch + + private static final int fillBellCairoBlack = ResourceUtils.getDrawableIdentifier("yt_fill_bell_cairo_black_24"); + + private static final Map shouldHideMap = new EnumMap<>(NavigationButton.class) { + { + put(NavigationButton.HOME, Settings.HIDE_NAVIGATION_HOME_BUTTON.get()); + put(NavigationButton.SHORTS, Settings.HIDE_NAVIGATION_SHORTS_BUTTON.get()); + put(NavigationButton.SUBSCRIPTIONS, Settings.HIDE_NAVIGATION_SUBSCRIPTIONS_BUTTON.get()); + put(NavigationButton.CREATE, Settings.HIDE_NAVIGATION_CREATE_BUTTON.get()); + put(NavigationButton.NOTIFICATIONS, Settings.HIDE_NAVIGATION_NOTIFICATIONS_BUTTON.get()); + put(NavigationButton.LIBRARY, Settings.HIDE_NAVIGATION_LIBRARY_BUTTON.get()); + } + }; + + public static boolean enableNarrowNavigationButton(boolean original) { + return Settings.ENABLE_NARROW_NAVIGATION_BUTTONS.get() || original; + } + + /** + * @noinspection ALL + */ + public static void setCairoNotificationFilledIcon(EnumMap enumMap, Enum tabActivityCairo) { + if (fillBellCairoBlack != 0) { + // It's very unlikely, but Google might fix this issue someday. + // If so, [fillBellCairoBlack] might already be in enumMap. + // That's why 'EnumMap.putIfAbsent()' is used instead of 'EnumMap.put()'. + enumMap.putIfAbsent(tabActivityCairo, Integer.valueOf(fillBellCairoBlack)); + } + } + + public static boolean switchCreateWithNotificationButton(boolean original) { + return Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get() || original; + } + + public static void navigationTabCreated(NavigationButton button, View tabView) { + if (BooleanUtils.isTrue(shouldHideMap.get(button))) { + tabView.setVisibility(View.GONE); + } + } + + public static void hideNavigationLabel(TextView view) { + hideViewUnderCondition(Settings.HIDE_NAVIGATION_LABEL.get(), view); + } + + public static void hideNavigationBar(View view) { + hideViewUnderCondition(Settings.HIDE_NAVIGATION_BAR.get(), view); + } + + public static boolean useTranslucentNavigationStatusBar(boolean original) { + try { + if (Settings.DISABLE_TRANSLUCENT_STATUS_BAR.get()) { + return false; + } + } catch (Exception ex) { + Logger.printException(() -> "Failed to load useTranslucentNavigationStatusBar", ex); + } + + return original; + } + + public static boolean enableTranslucentNavigationBar() { + return Settings.ENABLE_TRANSLUCENT_NAVIGATION_BAR.get(); + } + + public static boolean enableTranslucentStatusBar() { + return Settings.ENABLE_TRANSLUCENT_STATUS_BAR.get(); + } + + private static final Boolean DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT + = Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT.get(); + + private static final Boolean DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK + = Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK.get(); + + public static boolean useTranslucentNavigationButtons(boolean original) { + try { + // Feature requires Android 13+ + if (!isSDKAbove(33)) { + return original; + } + + if (!DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK && !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT) { + return original; + } + + if (DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK && DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT) { + return false; + } + + return Utils.isDarkModeEnabled() + ? !DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK + : !DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT; + } catch (Exception ex) { + Logger.printException(() -> "Failed to load useTranslucentNavigationButtons", ex); + } + return original; + } + + // endregion + + // region [Remove viewer discretion dialog] patch + + /** + * Injection point. + *

+ * The {@link AlertDialog#getButton(int)} method must be used after {@link AlertDialog#show()} is called. + * Otherwise {@link AlertDialog#getButton(int)} method will always return null. + * + *

+ * That's why {@link AlertDialog#show()} is absolutely necessary. + * Instead, use two tricks to hide Alertdialog. + *

+ * 1. Change the size of AlertDialog to 0. + * 2. Disable AlertDialog's background dim. + *

+ * This way, AlertDialog will be completely hidden, + * and {@link AlertDialog#getButton(int)} method can be used without issue. + */ + public static void confirmDialog(final AlertDialog dialog) { + if (!Settings.REMOVE_VIEWER_DISCRETION_DIALOG.get()) { + return; + } + + // This method is called after AlertDialog#show(), + // So we need to hide the AlertDialog before pressing the possitive button. + final Window window = dialog.getWindow(); + final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + if (window != null && button != null) { + WindowManager.LayoutParams params = window.getAttributes(); + params.height = 0; + params.width = 0; + + // Change the size of AlertDialog to 0. + window.setAttributes(params); + + // Disable AlertDialog's background dim. + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + Utils.clickView(button); + } + } + + public static void confirmDialogAgeVerified(final AlertDialog dialog) { + final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + if (!button.getText().toString().equals(str("og_continue"))) + return; + + confirmDialog(dialog); + } + + // endregion + + // region [Spoof app version] patch + + public static String getVersionOverride(String appVersion) { + return Settings.SPOOF_APP_VERSION.get() + ? Settings.SPOOF_APP_VERSION_TARGET.get() + : appVersion; + } + + // endregion + + // region [Toolbar components] patch + + private static final int generalHeaderAttributeId = ResourceUtils.getAttrIdentifier("ytWordmarkHeader"); + private static final int premiumHeaderAttributeId = ResourceUtils.getAttrIdentifier("ytPremiumWordmarkHeader"); + + public static void setDrawerNavigationHeader(View lithoView) { + final int headerAttributeId = getHeaderAttributeId(); + + lithoView.getViewTreeObserver().addOnDrawListener(() -> { + if (!(lithoView instanceof ViewGroup viewGroup)) + return; + if (!(viewGroup.getChildAt(0) instanceof ImageView imageView)) + return; + final Activity mActivity = Utils.getActivity(); + if (mActivity == null) + return; + imageView.setImageDrawable(getHeaderDrawable(mActivity, headerAttributeId)); + }); + } + + public static int getHeaderAttributeId() { + return Settings.CHANGE_YOUTUBE_HEADER.get() + ? premiumHeaderAttributeId + : generalHeaderAttributeId; + } + + public static boolean overridePremiumHeader() { + return Settings.CHANGE_YOUTUBE_HEADER.get(); + } + + private static Drawable getHeaderDrawable(Activity mActivity, int resourceId) { + // Rest of the implementation added by patch. + return ResourceUtils.getDrawable(""); + } + + private static final int searchBarId = ResourceUtils.getIdIdentifier("search_bar"); + private static final int youtubeTextId = ResourceUtils.getIdIdentifier("youtube_text"); + private static final int searchBoxId = ResourceUtils.getIdIdentifier("search_box"); + private static final int searchIconId = ResourceUtils.getIdIdentifier("search_icon"); + + private static final boolean wideSearchbarEnabled = Settings.ENABLE_WIDE_SEARCH_BAR.get(); + // Loads the search bar deprecated by Google. + private static final boolean wideSearchbarWithHeaderEnabled = Settings.ENABLE_WIDE_SEARCH_BAR_WITH_HEADER.get(); + private static final boolean wideSearchbarYouTabEnabled = Settings.ENABLE_WIDE_SEARCH_BAR_IN_YOU_TAB.get(); + + public static boolean enableWideSearchBar(boolean original) { + return wideSearchbarEnabled || original; + } + + /** + * Limitation: Premium header will not be applied for YouTube Premium users if the user uses the 'Wide search bar with header' option. + * This is because it forces the deprecated search bar to be loaded. + * As a solution to this limitation, 'Change YouTube header' patch is required. + */ + public static boolean enableWideSearchBarWithHeader(boolean original) { + if (!wideSearchbarEnabled) + return original; + else + return wideSearchbarWithHeaderEnabled || original; + } + + public static boolean enableWideSearchBarWithHeaderInverse(boolean original) { + if (!wideSearchbarEnabled) + return original; + else + return !wideSearchbarWithHeaderEnabled && original; + } + + public static boolean enableWideSearchBarInYouTab(boolean original) { + if (!wideSearchbarEnabled) + return original; + else + return !wideSearchbarYouTabEnabled && original; + } + + public static void setWideSearchBarLayout(View view) { + if (!wideSearchbarEnabled) + return; + if (!(view.findViewById(searchBarId) instanceof RelativeLayout searchBarView)) + return; + + // When the deprecated search bar is loaded, two search bars overlap. + // Manually hides another search bar. + if (wideSearchbarWithHeaderEnabled) { + final View searchIconView = searchBarView.findViewById(searchIconId); + final View searchBoxView = searchBarView.findViewById(searchBoxId); + final View textView = searchBarView.findViewById(youtubeTextId); + if (textView != null) { + RelativeLayout.LayoutParams layoutParams = new RelativeLayout.LayoutParams(0, 0); + layoutParams.setMargins(0, 0, 0, 0); + textView.setLayoutParams(layoutParams); + } + // The search icon in the deprecated search bar is clickable, but onClickListener is not assigned. + // Assign onClickListener and disable the effect when clicked. + if (searchIconView != null && searchBoxView != null) { + searchIconView.setOnClickListener(view1 -> searchBoxView.callOnClick()); + searchIconView.getBackground().setAlpha(0); + } + } else { + // This is the legacy method - Wide search bar without YouTube header. + // Since the padding start is 0, it does not look good. + // Add a padding start of 8.0 dip. + final int paddingLeft = searchBarView.getPaddingLeft(); + final int paddingRight = searchBarView.getPaddingRight(); + final int paddingTop = searchBarView.getPaddingTop(); + final int paddingBottom = searchBarView.getPaddingBottom(); + final int paddingStart = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8f, Utils.getResources().getDisplayMetrics()); + + // In RelativeLayout, paddingStart cannot be assigned programmatically. + // Check RTL layout and set left padding or right padding. + if (Utils.isRightToLeftTextLayout()) { + searchBarView.setPadding(paddingLeft, paddingTop, paddingStart, paddingBottom); + } else { + searchBarView.setPadding(paddingStart, paddingTop, paddingRight, paddingBottom); + } + } + } + + public static boolean hideCastButton(boolean original) { + return !Settings.HIDE_TOOLBAR_CAST_BUTTON.get() && original; + } + + public static void hideCastButton(MenuItem menuItem) { + if (!Settings.HIDE_TOOLBAR_CAST_BUTTON.get()) + return; + + menuItem.setVisible(false); + menuItem.setEnabled(false); + } + + public static void hideCreateButton(String enumString, View view) { + if (!Settings.HIDE_TOOLBAR_CREATE_BUTTON.get()) + return; + + hideViewUnderCondition(isCreateButton(enumString), view); + } + + public static void hideNotificationButton(String enumString, View view) { + if (!Settings.HIDE_TOOLBAR_NOTIFICATION_BUTTON.get()) + return; + + hideViewUnderCondition(isNotificationButton(enumString), view); + } + + public static boolean hideSearchTermThumbnail() { + return Settings.HIDE_SEARCH_TERM_THUMBNAIL.get(); + } + + private static final boolean hideImageSearchButton = Settings.HIDE_IMAGE_SEARCH_BUTTON.get(); + private static final boolean hideVoiceSearchButton = Settings.HIDE_VOICE_SEARCH_BUTTON.get(); + + /** + * If the user does not hide the Image search button but only the Voice search button, + * {@link View#setVisibility(int)} cannot be used on the Voice search button. + * (This breaks the search bar layout.) + *

+ * In this case, {@link Utils#hideViewByLayoutParams(View)} should be used. + */ + private static final boolean showImageSearchButtonAndHideVoiceSearchButton = !hideImageSearchButton && hideVoiceSearchButton && ImageSearchButton(); + + public static boolean hideImageSearchButton(boolean original) { + return !hideImageSearchButton && original; + } + + public static void hideVoiceSearchButton(View view) { + if (showImageSearchButtonAndHideVoiceSearchButton) { + hideViewByLayoutParams(view); + } else { + hideViewUnderCondition(hideVoiceSearchButton, view); + } + } + + public static void hideVoiceSearchButton(View view, int visibility) { + if (showImageSearchButtonAndHideVoiceSearchButton) { + view.setVisibility(visibility); + hideViewByLayoutParams(view); + } else { + view.setVisibility( + hideVoiceSearchButton + ? View.GONE : visibility + ); + } + } + + /** + * In ReVanced, image files are replaced to change the header, + * Whereas in RVX, the header is changed programmatically. + * There is an issue where the header is not changed in RVX when YouTube Doodles are hidden. + * As a workaround, manually set the header when YouTube Doodles are hidden. + */ + public static void hideYouTubeDoodles(ImageView imageView, Drawable drawable) { + final Activity mActivity = Utils.getActivity(); + if (Settings.HIDE_YOUTUBE_DOODLES.get() && mActivity != null) { + drawable = getHeaderDrawable(mActivity, getHeaderAttributeId()); + } + imageView.setImageDrawable(drawable); + } + + private static final int settingsDrawableId = + ResourceUtils.getDrawableIdentifier("yt_outline_gear_black_24"); + + public static int getCreateButtonDrawableId(int original) { + return Settings.REPLACE_TOOLBAR_CREATE_BUTTON.get() && + settingsDrawableId != 0 + ? settingsDrawableId + : original; + } + + public static void replaceCreateButton(String enumString, View toolbarView) { + if (!Settings.REPLACE_TOOLBAR_CREATE_BUTTON.get()) + return; + // Check if the button is a create button. + if (!isCreateButton(enumString)) + return; + ImageView imageView = getChildView((ViewGroup) toolbarView, view -> view instanceof ImageView); + if (imageView == null) + return; + + // Overriding is possible only after OnClickListener is assigned to the create button. + Utils.runOnMainThreadDelayed(() -> { + if (Settings.REPLACE_TOOLBAR_CREATE_BUTTON_TYPE.get()) { + imageView.setOnClickListener(GeneralPatch::openRVXSettings); + imageView.setOnLongClickListener(button -> { + openYouTubeSettings(button); + return true; + }); + } else { + imageView.setOnClickListener(GeneralPatch::openYouTubeSettings); + imageView.setOnLongClickListener(button -> { + openRVXSettings(button); + return true; + }); + } + }, 0); + } + + private static void openYouTubeSettings(View view) { + Context context = view.getContext(); + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setPackage(context.getPackageName()); + intent.setClass(context, Shell_SettingsActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); + context.startActivity(intent); + } + + private static void openRVXSettings(View view) { + Context context = view.getContext(); + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.setPackage(context.getPackageName()); + intent.setData(Uri.parse("revanced_extended_settings_intent")); + intent.setClass(context, VideoQualitySettingsActivity.class); + context.startActivity(intent); + } + + /** + * The theme of {@link Shell_SettingsActivity} is dark theme. + * Since this theme is hardcoded, we should manually specify the theme for the activity. + *

+ * Since {@link Shell_SettingsActivity} only invokes {@link SettingsActivity}, finish activity after specifying a theme. + * + * @param base {@link Shell_SettingsActivity} + */ + public static void setShellActivityTheme(Activity base) { + if (!Settings.REPLACE_TOOLBAR_CREATE_BUTTON.get()) + return; + + base.setTheme(ThemeUtils.getThemeId()); + Utils.runOnMainThreadDelayed(base::finish, 0); + } + + + private static boolean isCreateButton(String enumString) { + return StringUtils.equalsAny( + enumString, + "CREATION_ENTRY", // Create button for Phone layout + "FAB_CAMERA" // Create button for Tablet layout + ); + } + + private static boolean isNotificationButton(String enumString) { + return StringUtils.equalsAny( + enumString, + "TAB_ACTIVITY", // Notification button + "TAB_ACTIVITY_CAIRO" // Notification button (new layout) + ); + } + + // endregion + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/LayoutSwitchPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/LayoutSwitchPatch.java new file mode 100644 index 000000000..56d343080 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/LayoutSwitchPatch.java @@ -0,0 +1,79 @@ +package app.revanced.extension.youtube.patches.general; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + +import androidx.annotation.Nullable; + +import org.apache.commons.lang3.BooleanUtils; + +import java.util.Objects; + +import app.revanced.extension.shared.utils.PackageUtils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class LayoutSwitchPatch { + + public enum FormFactor { + /** + * Unmodified type, and same as un-patched. + */ + ORIGINAL(null, null, null), + SMALL_FORM_FACTOR(1, null, TRUE), + SMALL_FORM_FACTOR_WIDTH_DP(1, 480, TRUE), + LARGE_FORM_FACTOR(2, null, FALSE), + LARGE_FORM_FACTOR_WIDTH_DP(2, 600, FALSE); + + @Nullable + final Integer formFactorType; + + @Nullable + final Integer widthDp; + + @Nullable + final Boolean setMinimumDp; + + FormFactor(@Nullable Integer formFactorType, @Nullable Integer widthDp, @Nullable Boolean setMinimumDp) { + this.formFactorType = formFactorType; + this.widthDp = widthDp; + this.setMinimumDp = setMinimumDp; + } + + private boolean setMinimumDp() { + return BooleanUtils.isTrue(setMinimumDp); + } + } + + private static final FormFactor FORM_FACTOR = Settings.CHANGE_LAYOUT.get(); + + public static int getFormFactor(int original) { + Integer formFactorType = FORM_FACTOR.formFactorType; + return formFactorType == null + ? original + : formFactorType; + } + + public static int getWidthDp(int original) { + Integer widthDp = FORM_FACTOR.widthDp; + if (widthDp == null) { + return original; + } + final int smallestScreenWidthDp = PackageUtils.getSmallestScreenWidthDp(); + if (smallestScreenWidthDp == 0) { + return original; + } + return FORM_FACTOR.setMinimumDp() + ? Math.min(smallestScreenWidthDp, widthDp) + : Math.max(smallestScreenWidthDp, widthDp); + } + + public static boolean phoneLayoutEnabled() { + return Objects.equals(FORM_FACTOR.formFactorType, 1); + } + + public static boolean tabletLayoutEnabled() { + return Objects.equals(FORM_FACTOR.formFactorType, 2); + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/MiniplayerPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/MiniplayerPatch.java new file mode 100644 index 000000000..b231f24d9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/MiniplayerPatch.java @@ -0,0 +1,363 @@ +package app.revanced.extension.youtube.patches.general; + +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.DISABLED; +import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_1; +import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_2; +import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_3; +import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.ORIGINAL; +import static app.revanced.extension.youtube.utils.ExtendedUtils.IS_19_20_OR_GREATER; +import static app.revanced.extension.youtube.utils.ExtendedUtils.IS_19_21_OR_GREATER; +import static app.revanced.extension.youtube.utils.ExtendedUtils.IS_19_26_OR_GREATER; +import static app.revanced.extension.youtube.utils.ExtendedUtils.IS_19_29_OR_GREATER; +import static app.revanced.extension.youtube.utils.ExtendedUtils.validateValue; + +import android.util.DisplayMetrics; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.ExtendedUtils; + +@SuppressWarnings({"unused", "SpellCheckingInspection"}) +public final class MiniplayerPatch { + + /** + * Mini player type. Null fields indicates to use the original un-patched value. + */ + public enum MiniplayerType { + /** + * Disabled. When swiped down the miniplayer is immediately closed. + * Only available with 19.43+ + */ + DISABLED(false, null), + /** + * Unmodified type, and same as un-patched. + */ + ORIGINAL(null, null), + /** + * Exactly the same as MINIMAL and only here for migration of user settings. + * Eventually this should be deleted. + */ + @Deprecated + PHONE(false, null), + MINIMAL(false, null), + TABLET(true, null), + MODERN_1(null, 1), + MODERN_2(null, 2), + MODERN_3(null, 3), + /** + * Half broken miniplayer, that might be work in progress or left over abandoned code. + * Can force this type by editing the import/export settings. + */ + MODERN_4(null, 4); + + /** + * Legacy tablet hook value. + */ + @Nullable + final Boolean legacyTabletOverride; + + /** + * Modern player type used by YT. + */ + @Nullable + final Integer modernPlayerType; + + MiniplayerType(@Nullable Boolean legacyTabletOverride, @Nullable Integer modernPlayerType) { + this.legacyTabletOverride = legacyTabletOverride; + this.modernPlayerType = modernPlayerType; + } + + public boolean isModern() { + return modernPlayerType != null; + } + } + + private static final int MINIPLAYER_SIZE; + + static { + // YT appears to use the device screen dip width, plus an unknown fixed horizontal padding size. + DisplayMetrics displayMetrics = Utils.getContext().getResources().getDisplayMetrics(); + final int deviceDipWidth = (int) (displayMetrics.widthPixels / displayMetrics.density); + + // YT seems to use a minimum height to calculate the minimum miniplayer width based on the video. + // 170 seems to be the smallest that can be used and using less makes no difference. + final int WIDTH_DIP_MIN = 170; // Seems to be the smallest that works. + final int HORIZONTAL_PADDING_DIP = 15; // Estimated padding. + // Round down to the nearest 5 pixels, to keep any error toasts easier to read. + final int estimatedWidthDipMax = 5 * ((deviceDipWidth - HORIZONTAL_PADDING_DIP) / 5); + // On some ultra low end devices the pixel width and density are the same number, + // which causes the estimate to always give a value of 1. + // Fix this by using a fixed size of double the min width. + final int WIDTH_DIP_MAX = estimatedWidthDipMax <= WIDTH_DIP_MIN + ? 2 * WIDTH_DIP_MIN + : estimatedWidthDipMax; + Logger.printDebug(() -> "Screen dip width: " + deviceDipWidth + " maxWidth: " + WIDTH_DIP_MAX); + + int dipWidth = Settings.MINIPLAYER_WIDTH_DIP.get(); + + if (dipWidth < WIDTH_DIP_MIN || dipWidth > WIDTH_DIP_MAX) { + Utils.showToastShort(str("revanced_miniplayer_width_dip_invalid_toast", + WIDTH_DIP_MIN, WIDTH_DIP_MAX)); + Utils.showToastShort(str("revanced_extended_reset_to_default_toast")); + + // Instead of resetting, clamp the size at the bounds. + dipWidth = Math.max(WIDTH_DIP_MIN, Math.min(dipWidth, WIDTH_DIP_MAX)); + Settings.MINIPLAYER_WIDTH_DIP.save(dipWidth); + } + + MINIPLAYER_SIZE = dipWidth; + + final int opacity = validateValue( + Settings.MINIPLAYER_OPACITY, + 0, + 100, + "revanced_miniplayer_opacity_invalid_toast" + ); + + OPACITY_LEVEL = (opacity * 255) / 100; + } + + /** + * Modern subtitle overlay for {@link MiniplayerType#MODERN_2}. + * Resource is not present in older targets, and this field will be zero. + */ + private static final int MODERN_OVERLAY_SUBTITLE_TEXT + = ResourceUtils.getIdIdentifier("modern_miniplayer_subtitle_text"); + + private static final MiniplayerType CURRENT_TYPE = Settings.MINIPLAYER_TYPE.get(); + + /** + * Cannot turn off double tap with modern 2 or 3 with later targets, + * as forcing it off breakings tapping the miniplayer. + */ + private static final boolean DOUBLE_TAP_ACTION_ENABLED = + // 19.29+ is very broken if double tap is not enabled. + IS_19_29_OR_GREATER || + (CURRENT_TYPE.isModern() && Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get()); + + private static final boolean DRAG_AND_DROP_ENABLED = + CURRENT_TYPE.isModern() && Settings.MINIPLAYER_DRAG_AND_DROP.get(); + + private static final boolean HIDE_EXPAND_CLOSE_ENABLED = + Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get() + && Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.isAvailable(); + + private static final boolean HIDE_SUBTEXT_ENABLED = + (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_SUBTEXT.get(); + + // 19.25 is last version that has forward/back buttons for phones, + // but buttons still show for tablets/foldable devices and they don't work well so always hide. + private static final boolean HIDE_REWIND_FORWARD_ENABLED = CURRENT_TYPE == MODERN_1 + && (ExtendedUtils.IS_19_34_OR_GREATER || Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get()); + + private static final boolean MINIPLAYER_ROUNDED_CORNERS_ENABLED = + Settings.MINIPLAYER_ROUNDED_CORNERS.get(); + + private static final boolean MINIPLAYER_HORIZONTAL_DRAG_ENABLED = + DRAG_AND_DROP_ENABLED && Settings.MINIPLAYER_HORIZONTAL_DRAG.get(); + + /** + * Remove a broken and always present subtitle text that is only + * present with {@link MiniplayerType#MODERN_2}. Bug was fixed in 19.21. + */ + private static final boolean HIDE_BROKEN_MODERN_2_SUBTITLE = + CURRENT_TYPE == MODERN_2 && !IS_19_21_OR_GREATER; + + private static final int OPACITY_LEVEL; + + public static final class MiniplayerHorizontalDragAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return Settings.MINIPLAYER_TYPE.get().isModern() && Settings.MINIPLAYER_DRAG_AND_DROP.get(); + } + } + + public static final class MiniplayerHideExpandCloseAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + MiniplayerType type = Settings.MINIPLAYER_TYPE.get(); + return (!IS_19_20_OR_GREATER && (type == MODERN_1 || type == MODERN_3)) + || (!IS_19_26_OR_GREATER && type == MODERN_1 + && !Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get() && !Settings.MINIPLAYER_DRAG_AND_DROP.get()) + || (IS_19_29_OR_GREATER && type == MODERN_3); + } + } + + /** + * Injection point. + *

+ * Enables a handler that immediately closes the miniplayer when the video is minimized, + * effectively disabling the miniplayer. + */ + public static boolean getMiniplayerOnCloseHandler(boolean original) { + return CURRENT_TYPE == ORIGINAL + ? original + : CURRENT_TYPE == DISABLED; + } + + /** + * Injection point. + */ + public static boolean getLegacyTabletMiniplayerOverride(boolean original) { + Boolean isTablet = CURRENT_TYPE.legacyTabletOverride; + return isTablet == null + ? original + : isTablet; + } + + /** + * Injection point. + */ + public static boolean getModernMiniplayerOverride(boolean original) { + return CURRENT_TYPE == ORIGINAL + ? original + : CURRENT_TYPE.isModern(); + } + + /** + * Injection point. + */ + public static int getModernMiniplayerOverrideType(int original) { + Integer modernValue = CURRENT_TYPE.modernPlayerType; + return modernValue == null + ? original + : modernValue; + } + + /** + * Injection point. + */ + public static void adjustMiniplayerOpacity(ImageView view) { + if (CURRENT_TYPE == MODERN_1) { + view.setImageAlpha(OPACITY_LEVEL); + } + } + + /** + * Injection point. + */ + public static boolean getModernFeatureFlagsActiveOverride(boolean original) { + if (CURRENT_TYPE == ORIGINAL) { + return original; + } + + return CURRENT_TYPE.isModern(); + } + + /** + * Injection point. + */ + public static boolean enableMiniplayerDoubleTapAction(boolean original) { + if (CURRENT_TYPE == ORIGINAL) { + return original; + } + + return DOUBLE_TAP_ACTION_ENABLED; + } + + /** + * Injection point. + */ + public static boolean enableMiniplayerDragAndDrop(boolean original) { + if (CURRENT_TYPE == ORIGINAL) { + return original; + } + + return DRAG_AND_DROP_ENABLED; + } + + + /** + * Injection point. + */ + public static boolean setRoundedCorners(boolean original) { + if (CURRENT_TYPE.isModern()) { + return MINIPLAYER_ROUNDED_CORNERS_ENABLED; + } + + return original; + } + + /** + * Injection point. + */ + public static int setMiniplayerDefaultSize(int original) { + if (CURRENT_TYPE.isModern()) { + return MINIPLAYER_SIZE; + } + + return original; + } + + + /** + * Injection point. + */ + public static boolean setHorizontalDrag(boolean original) { + if (CURRENT_TYPE.isModern()) { + return MINIPLAYER_HORIZONTAL_DRAG_ENABLED; + } + + return original; + } + + /** + * Injection point. + */ + public static void hideMiniplayerExpandClose(ImageView view) { + Utils.hideViewByRemovingFromParentUnderCondition(HIDE_EXPAND_CLOSE_ENABLED, view); + } + + /** + * Injection point. + */ + public static void hideMiniplayerRewindForward(ImageView view) { + Utils.hideViewByRemovingFromParentUnderCondition(HIDE_REWIND_FORWARD_ENABLED, view); + } + + /** + * Injection point. + */ + public static void hideMiniplayerSubTexts(View view) { + try { + // Different subviews are passed in, but only TextView is of interest here. + if (HIDE_SUBTEXT_ENABLED && view instanceof TextView) { + Logger.printDebug(() -> "Hiding subtext view"); + Utils.hideViewByRemovingFromParentUnderCondition(true, view); + } + } catch (Exception ex) { + Logger.printException(() -> "hideMiniplayerSubTexts failure", ex); + } + } + + /** + * Injection point. + */ + public static void playerOverlayGroupCreated(View group) { + try { + if (HIDE_BROKEN_MODERN_2_SUBTITLE && MODERN_OVERLAY_SUBTITLE_TEXT != 0) { + if (group instanceof ViewGroup) { + View subtitleText = Utils.getChildView((ViewGroup) group, true, + view -> view.getId() == MODERN_OVERLAY_SUBTITLE_TEXT); + + if (subtitleText != null) { + subtitleText.setVisibility(View.GONE); + Logger.printDebug(() -> "Modern overlay subtitle view set to hidden"); + } + } + } + } catch (Exception ex) { + Logger.printException(() -> "playerOverlayGroupCreated failure", ex); + } + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/SettingsMenuPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/SettingsMenuPatch.java new file mode 100644 index 000000000..792fe4635 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/SettingsMenuPatch.java @@ -0,0 +1,52 @@ +package app.revanced.extension.youtube.patches.general; + +import androidx.preference.PreferenceScreen; + +import app.revanced.extension.shared.patches.BaseSettingsMenuPatch; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class SettingsMenuPatch extends BaseSettingsMenuPatch { + + public static void hideSettingsMenu(PreferenceScreen mPreferenceScreen) { + if (mPreferenceScreen == null) return; + for (SettingsMenuComponent component : SettingsMenuComponent.values()) + if (component.enabled) + removePreference(mPreferenceScreen, component.key); + } + + private enum SettingsMenuComponent { + YOUTUBE_TV("yt_unplugged_pref_key", Settings.HIDE_SETTINGS_MENU_YOUTUBE_TV.get()), + PARENT_TOOLS("parent_tools_key", Settings.HIDE_SETTINGS_MENU_PARENT_TOOLS.get()), + PRE_PURCHASE("yt_unlimited_pre_purchase_key", Settings.HIDE_SETTINGS_MENU_PRE_PURCHASE.get()), + GENERAL("general_key", Settings.HIDE_SETTINGS_MENU_GENERAL.get()), + ACCOUNT("account_switcher_key", Settings.HIDE_SETTINGS_MENU_ACCOUNT.get()), + DATA_SAVING("data_saving_settings_key", Settings.HIDE_SETTINGS_MENU_DATA_SAVING.get()), + AUTOPLAY("auto_play_key", Settings.HIDE_SETTINGS_MENU_AUTOPLAY.get()), + VIDEO_QUALITY_PREFERENCES("video_quality_settings_key", Settings.HIDE_SETTINGS_MENU_VIDEO_QUALITY_PREFERENCES.get()), + POST_PURCHASE("yt_unlimited_post_purchase_key", Settings.HIDE_SETTINGS_MENU_POST_PURCHASE.get()), + OFFLINE("offline_key", Settings.HIDE_SETTINGS_MENU_OFFLINE.get()), + WATCH_ON_TV("pair_with_tv_key", Settings.HIDE_SETTINGS_MENU_WATCH_ON_TV.get()), + MANAGE_ALL_HISTORY("history_key", Settings.HIDE_SETTINGS_MENU_MANAGE_ALL_HISTORY.get()), + YOUR_DATA_IN_YOUTUBE("your_data_key", Settings.HIDE_SETTINGS_MENU_YOUR_DATA_IN_YOUTUBE.get()), + PRIVACY("privacy_key", Settings.HIDE_SETTINGS_MENU_PRIVACY.get()), + TRY_EXPERIMENTAL_NEW_FEATURES("premium_early_access_browse_page_key", Settings.HIDE_SETTINGS_MENU_TRY_EXPERIMENTAL_NEW_FEATURES.get()), + PURCHASES_AND_MEMBERSHIPS("subscription_product_setting_key", Settings.HIDE_SETTINGS_MENU_PURCHASES_AND_MEMBERSHIPS.get()), + BILLING_AND_PAYMENTS("billing_and_payment_key", Settings.HIDE_SETTINGS_MENU_BILLING_AND_PAYMENTS.get()), + NOTIFICATIONS("notification_key", Settings.HIDE_SETTINGS_MENU_NOTIFICATIONS.get()), + THIRD_PARTY("third_party_key", Settings.HIDE_SETTINGS_MENU_THIRD_PARTY.get()), + CONNECTED_APPS("connected_accounts_browse_page_key", Settings.HIDE_SETTINGS_MENU_CONNECTED_APPS.get()), + LIVE_CHAT("live_chat_key", Settings.HIDE_SETTINGS_MENU_LIVE_CHAT.get()), + CAPTIONS("captions_key", Settings.HIDE_SETTINGS_MENU_CAPTIONS.get()), + ACCESSIBILITY("accessibility_settings_key", Settings.HIDE_SETTINGS_MENU_ACCESSIBILITY.get()), + ABOUT("about_key", Settings.HIDE_SETTINGS_MENU_ABOUT.get()); + + private final String key; + private final boolean enabled; + + SettingsMenuComponent(String key, boolean enabled) { + this.key = key; + this.enabled = enabled; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/YouTubeMusicActionsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/YouTubeMusicActionsPatch.java new file mode 100644 index 000000000..52c0da246 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/YouTubeMusicActionsPatch.java @@ -0,0 +1,55 @@ +package app.revanced.extension.youtube.patches.general; + +import androidx.annotation.NonNull; + +import org.apache.commons.lang3.StringUtils; + +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.ExtendedUtils; +import app.revanced.extension.youtube.utils.VideoUtils; + +@SuppressWarnings("unused") +public final class YouTubeMusicActionsPatch extends VideoUtils { + + private static final String PACKAGE_NAME_YOUTUBE_MUSIC = "com.google.android.apps.youtube.music"; + + private static final boolean isOverrideYouTubeMusicEnabled = + Settings.OVERRIDE_YOUTUBE_MUSIC_BUTTON.get(); + + private static final boolean overrideYouTubeMusicEnabled = + isOverrideYouTubeMusicEnabled && isYouTubeMusicEnabled(); + + public static String overridePackageName(@NonNull String packageName) { + if (!overrideYouTubeMusicEnabled) { + return packageName; + } + if (!StringUtils.equals(PACKAGE_NAME_YOUTUBE_MUSIC, packageName)) { + return packageName; + } + final String thirdPartyPackageName = Settings.THIRD_PARTY_YOUTUBE_MUSIC_PACKAGE_NAME.get(); + if (!ExtendedUtils.isPackageEnabled(thirdPartyPackageName)) { + return packageName; + } + return thirdPartyPackageName; + } + + private static boolean isYouTubeMusicEnabled() { + return ExtendedUtils.isPackageEnabled(PACKAGE_NAME_YOUTUBE_MUSIC); + } + + public static final class HookYouTubeMusicAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return isYouTubeMusicEnabled(); + } + } + + public static final class HookYouTubeMusicPackageNameAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return isOverrideYouTubeMusicEnabled && isYouTubeMusicEnabled(); + } + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/BackgroundPlaybackPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/BackgroundPlaybackPatch.java new file mode 100644 index 000000000..39dcff1a7 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/BackgroundPlaybackPatch.java @@ -0,0 +1,24 @@ +package app.revanced.extension.youtube.patches.misc; + +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.ShortsPlayerState; + +@SuppressWarnings("unused") +public class BackgroundPlaybackPatch { + + /** + * Injection point. + */ + public static boolean isBackgroundPlaybackAllowed(boolean original) { + if (original) return true; + return ShortsPlayerState.getCurrent().isClosed(); + } + + /** + * Injection point. + */ + public static boolean isBackgroundShortsPlaybackAllowed(boolean original) { + return !Settings.DISABLE_SHORTS_BACKGROUND_PLAYBACK.get(); + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/ExternalBrowserPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/ExternalBrowserPatch.java new file mode 100644 index 000000000..794fd93e0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/ExternalBrowserPatch.java @@ -0,0 +1,14 @@ +package app.revanced.extension.youtube.patches.misc; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class ExternalBrowserPatch { + + public static String enableExternalBrowser(final String original) { + if (!Settings.ENABLE_EXTERNAL_BROWSER.get()) + return original; + + return ""; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/OpenLinksDirectlyPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/OpenLinksDirectlyPatch.java new file mode 100644 index 000000000..a3e9b9658 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/OpenLinksDirectlyPatch.java @@ -0,0 +1,24 @@ +package app.revanced.extension.youtube.patches.misc; + +import android.net.Uri; + +import java.util.Objects; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class OpenLinksDirectlyPatch { + private static final String YOUTUBE_REDIRECT_PATH = "/redirect"; + + public static Uri enableBypassRedirect(String uri) { + final Uri parsed = Uri.parse(uri); + if (!Settings.ENABLE_OPEN_LINKS_DIRECTLY.get()) + return parsed; + + if (Objects.equals(parsed.getPath(), YOUTUBE_REDIRECT_PATH)) { + return Uri.parse(Uri.decode(parsed.getQueryParameter("q"))); + } + + return parsed; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/OpusCodecPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/OpusCodecPatch.java new file mode 100644 index 000000000..32696151a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/OpusCodecPatch.java @@ -0,0 +1,11 @@ +package app.revanced.extension.youtube.patches.misc; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class OpusCodecPatch { + + public static boolean enableOpusCodec() { + return Settings.ENABLE_OPUS_CODEC.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/QUICProtocolPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/QUICProtocolPatch.java new file mode 100644 index 000000000..b8e099b91 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/QUICProtocolPatch.java @@ -0,0 +1,17 @@ +package app.revanced.extension.youtube.patches.misc; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class QUICProtocolPatch { + + public static boolean disableQUICProtocol(boolean original) { + try { + return !Settings.DISABLE_QUIC_PROTOCOL.get() && original; + } catch (Exception ex) { + Logger.printException(() -> "Failed to load disableQUICProtocol", ex); + } + return original; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/ShareSheetPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/ShareSheetPatch.java new file mode 100644 index 000000000..a1236f479 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/ShareSheetPatch.java @@ -0,0 +1,62 @@ +package app.revanced.extension.youtube.patches.misc; + +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.components.ShareSheetMenuFilter; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class ShareSheetPatch { + private static final boolean changeShareSheetEnabled = Settings.CHANGE_SHARE_SHEET.get(); + + private static void clickSystemShareButton(final RecyclerView bottomSheetRecyclerView, + final RecyclerView appsContainerRecyclerView) { + if (appsContainerRecyclerView.getChildAt(appsContainerRecyclerView.getChildCount() - 1) instanceof ViewGroup parentView && + parentView.getChildAt(0) instanceof ViewGroup shareWithOtherAppsView) { + ShareSheetMenuFilter.isShareSheetMenuVisible = false; + + bottomSheetRecyclerView.setVisibility(View.GONE); + Utils.clickView(shareWithOtherAppsView); + } + } + + /** + * Injection point. + */ + public static void onShareSheetMenuCreate(final RecyclerView recyclerView) { + if (!changeShareSheetEnabled) + return; + + recyclerView.getViewTreeObserver().addOnDrawListener(() -> { + try { + if (!ShareSheetMenuFilter.isShareSheetMenuVisible) { + return; + } + if (!(recyclerView.getChildAt(0) instanceof ViewGroup parentView4th)) { + return; + } + if (parentView4th.getChildAt(0) instanceof ViewGroup parentView3rd && + parentView3rd.getChildAt(0) instanceof RecyclerView appsContainerRecyclerView) { + clickSystemShareButton(recyclerView, appsContainerRecyclerView); + } else if (parentView4th.getChildAt(1) instanceof ViewGroup parentView3rd && + parentView3rd.getChildAt(0) instanceof RecyclerView appsContainerRecyclerView) { + clickSystemShareButton(recyclerView, appsContainerRecyclerView); + } + } catch (Exception ex) { + Logger.printException(() -> "onShareSheetMenuCreate failure", ex); + } + }); + } + + /** + * Injection point. + */ + public static String overridePackageName(String original) { + return changeShareSheetEnabled ? "" : original; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/WatchHistoryPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/WatchHistoryPatch.java new file mode 100644 index 000000000..01a002be4 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/WatchHistoryPatch.java @@ -0,0 +1,37 @@ +package app.revanced.extension.youtube.patches.misc; + +import android.net.Uri; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class WatchHistoryPatch { + + public enum WatchHistoryType { + ORIGINAL, + REPLACE, + BLOCK + } + + private static final Uri UNREACHABLE_HOST_URI = Uri.parse("https://127.0.0.0"); + private static final String WWW_TRACKING_URL_AUTHORITY = "www.youtube.com"; + + public static Uri replaceTrackingUrl(Uri trackingUrl) { + final WatchHistoryType watchHistoryType = Settings.WATCH_HISTORY_TYPE.get(); + if (watchHistoryType != WatchHistoryType.ORIGINAL) { + try { + if (watchHistoryType == WatchHistoryType.REPLACE) { + return trackingUrl.buildUpon().authority(WWW_TRACKING_URL_AUTHORITY).build(); + } else if (watchHistoryType == WatchHistoryType.BLOCK) { + return UNREACHABLE_HOST_URI; + } + } catch (Exception ex) { + Logger.printException(() -> "replaceTrackingUrl failure", ex); + } + } + + return trackingUrl; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/AlwaysRepeat.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/AlwaysRepeat.java new file mode 100644 index 000000000..777831a1e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/AlwaysRepeat.java @@ -0,0 +1,59 @@ +package app.revanced.extension.youtube.patches.overlaybutton; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class AlwaysRepeat extends BottomControlButton { + @Nullable + private static AlwaysRepeat instance; + + public AlwaysRepeat(ViewGroup bottomControlsViewGroup) { + super( + bottomControlsViewGroup, + "always_repeat_button", + Settings.OVERLAY_BUTTON_ALWAYS_REPEAT, + Settings.ALWAYS_REPEAT, + Settings.ALWAYS_REPEAT_PAUSE, + view -> { + if (instance != null) + instance.changeSelected(!view.isSelected()); + }, + view -> { + if (instance != null) + instance.changeColorFilter(); + return true; + } + ); + } + + /** + * Injection point. + */ + public static void initialize(View bottomControlsViewGroup) { + try { + if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { + instance = new AlwaysRepeat(viewGroup); + } + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + /** + * Injection point. + */ + public static void changeVisibility(boolean showing, boolean animation) { + if (instance != null) instance.setVisibility(showing, animation); + } + + public static void changeVisibilityNegatedImmediate() { + if (instance != null) instance.setVisibilityNegatedImmediate(); + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/BottomControlButton.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/BottomControlButton.java new file mode 100644 index 000000000..da4744f5a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/BottomControlButton.java @@ -0,0 +1,174 @@ +package app.revanced.extension.youtube.patches.overlaybutton; + +import static app.revanced.extension.shared.utils.ResourceUtils.getAnimation; +import static app.revanced.extension.shared.utils.ResourceUtils.getInteger; +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.getChildView; + +import android.graphics.Color; +import android.graphics.ColorFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +public abstract class BottomControlButton { + private static final Animation fadeIn; + private static final Animation fadeOut; + private static final Animation fadeOutImmediate; + + private final ColorFilter cf = + new PorterDuffColorFilter(Color.parseColor("#fffffc79"), PorterDuff.Mode.SRC_ATOP); + + private final WeakReference buttonRef; + private final BooleanSetting setting; + private final BooleanSetting primaryInteractionSetting; + private final BooleanSetting secondaryInteractionSetting; + protected boolean isVisible; + + static { + fadeIn = getAnimation("fade_in"); + // android.R.integer.config_shortAnimTime, 200 + fadeIn.setDuration(getInteger("fade_duration_fast")); + + fadeOut = getAnimation("fade_out"); + // android.R.integer.config_mediumAnimTime, 400 + fadeOut.setDuration(getInteger("fade_overlay_fade_duration")); + + fadeOutImmediate = getAnimation("abc_fade_out"); + // android.R.integer.config_shortAnimTime, 200 + fadeOutImmediate.setDuration(getInteger("fade_duration_fast")); + } + + @NonNull + public static Animation getButtonFadeIn() { + return fadeIn; + } + + @NonNull + public static Animation getButtonFadeOut() { + return fadeOut; + } + + @NonNull + public static Animation getButtonFadeOutImmediate() { + return fadeOutImmediate; + } + + public BottomControlButton(@NonNull ViewGroup bottomControlsViewGroup, @NonNull String imageViewButtonId, @NonNull BooleanSetting booleanSetting, + @NonNull View.OnClickListener onClickListener, @Nullable View.OnLongClickListener longClickListener) { + this(bottomControlsViewGroup, imageViewButtonId, booleanSetting, null, null, onClickListener, longClickListener); + } + + @SuppressWarnings("unused") + public BottomControlButton(@NonNull ViewGroup bottomControlsViewGroup, @NonNull String imageViewButtonId, @NonNull BooleanSetting booleanSetting, @Nullable BooleanSetting primaryInteractionSetting, + @NonNull View.OnClickListener onClickListener, @Nullable View.OnLongClickListener longClickListener) { + this(bottomControlsViewGroup, imageViewButtonId, booleanSetting, primaryInteractionSetting, null, onClickListener, longClickListener); + } + + public BottomControlButton(@NonNull ViewGroup bottomControlsViewGroup, @NonNull String imageViewButtonId, @NonNull BooleanSetting booleanSetting, + @Nullable BooleanSetting primaryInteractionSetting, @Nullable BooleanSetting secondaryInteractionSetting, + @NonNull View.OnClickListener onClickListener, @Nullable View.OnLongClickListener longClickListener) { + Logger.printDebug(() -> "Initializing button: " + imageViewButtonId); + + setting = booleanSetting; + + // Create the button. + ImageView imageView = Objects.requireNonNull(getChildView(bottomControlsViewGroup, imageViewButtonId)); + imageView.setOnClickListener(onClickListener); + this.primaryInteractionSetting = primaryInteractionSetting; + this.secondaryInteractionSetting = secondaryInteractionSetting; + if (primaryInteractionSetting != null) { + imageView.setSelected(primaryInteractionSetting.get()); + } + if (secondaryInteractionSetting != null) { + setColorFilter(imageView, secondaryInteractionSetting.get()); + } + if (longClickListener != null) { + imageView.setOnLongClickListener(longClickListener); + } + imageView.setVisibility(View.GONE); + buttonRef = new WeakReference<>(imageView); + } + + public void changeActivated(boolean activated) { + ImageView imageView = buttonRef.get(); + if (imageView == null) + return; + imageView.setActivated(activated); + } + + public void changeSelected(boolean selected) { + ImageView imageView = buttonRef.get(); + if (imageView == null || primaryInteractionSetting == null) + return; + + if (imageView.getColorFilter() == cf) { + Utils.showToastShort(str("revanced_overlay_button_not_allowed_warning")); + return; + } + + imageView.setSelected(selected); + primaryInteractionSetting.save(selected); + } + + public void changeColorFilter() { + ImageView imageView = buttonRef.get(); + if (imageView == null) return; + if (primaryInteractionSetting == null || secondaryInteractionSetting == null) + return; + + imageView.setSelected(true); + primaryInteractionSetting.save(true); + + final boolean newValue = !secondaryInteractionSetting.get(); + secondaryInteractionSetting.save(newValue); + setColorFilter(imageView, newValue); + } + + public void setColorFilter(ImageView imageView, boolean selected) { + if (selected) + imageView.setColorFilter(cf); + else + imageView.clearColorFilter(); + } + + public void setVisibility(boolean visible, boolean animation) { + ImageView imageView = buttonRef.get(); + if (imageView == null || isVisible == visible) return; + isVisible = visible; + + imageView.clearAnimation(); + if (visible && setting.get()) { + imageView.setVisibility(View.VISIBLE); + if (animation) imageView.startAnimation(fadeIn); + return; + } + if (imageView.getVisibility() == View.VISIBLE) { + if (animation) imageView.startAnimation(fadeOut); + imageView.setVisibility(View.GONE); + } + } + + public void setVisibilityNegatedImmediate() { + ImageView imageView = buttonRef.get(); + if (imageView == null) return; + if (!setting.get()) return; + + imageView.clearAnimation(); + imageView.startAnimation(fadeOutImmediate); + imageView.setVisibility(View.GONE); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/CopyVideoUrl.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/CopyVideoUrl.java new file mode 100644 index 000000000..33e7e88bb --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/CopyVideoUrl.java @@ -0,0 +1,55 @@ +package app.revanced.extension.youtube.patches.overlaybutton; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.VideoUtils; + +@SuppressWarnings("unused") +public class CopyVideoUrl extends BottomControlButton { + @Nullable + private static CopyVideoUrl instance; + + public CopyVideoUrl(ViewGroup bottomControlsViewGroup) { + super( + bottomControlsViewGroup, + "copy_video_url_button", + Settings.OVERLAY_BUTTON_COPY_VIDEO_URL, + view -> VideoUtils.copyUrl(false), + view -> { + VideoUtils.copyUrl(true); + return true; + } + ); + } + + /** + * Injection point. + */ + public static void initialize(View bottomControlsViewGroup) { + try { + if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { + instance = new CopyVideoUrl(viewGroup); + } + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + /** + * Injection point. + */ + public static void changeVisibility(boolean showing, boolean animation) { + if (instance != null) instance.setVisibility(showing, animation); + } + + public static void changeVisibilityNegatedImmediate() { + if (instance != null) instance.setVisibilityNegatedImmediate(); + } + + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/CopyVideoUrlTimestamp.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/CopyVideoUrlTimestamp.java new file mode 100644 index 000000000..bfda8216b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/CopyVideoUrlTimestamp.java @@ -0,0 +1,55 @@ +package app.revanced.extension.youtube.patches.overlaybutton; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.VideoUtils; + +@SuppressWarnings("unused") +public class CopyVideoUrlTimestamp extends BottomControlButton { + @Nullable + private static CopyVideoUrlTimestamp instance; + + public CopyVideoUrlTimestamp(ViewGroup bottomControlsViewGroup) { + super( + bottomControlsViewGroup, + "copy_video_url_timestamp_button", + Settings.OVERLAY_BUTTON_COPY_VIDEO_URL_TIMESTAMP, + view -> VideoUtils.copyUrl(true), + view -> { + VideoUtils.copyTimeStamp(); + return true; + } + ); + } + + /** + * Injection point. + */ + public static void initialize(View bottomControlsViewGroup) { + try { + if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { + instance = new CopyVideoUrlTimestamp(viewGroup); + } + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + /** + * Injection point. + */ + public static void changeVisibility(boolean showing, boolean animation) { + if (instance != null) instance.setVisibility(showing, animation); + } + + public static void changeVisibilityNegatedImmediate() { + if (instance != null) instance.setVisibilityNegatedImmediate(); + } + + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/ExternalDownload.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/ExternalDownload.java new file mode 100644 index 000000000..5bd1c39da --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/ExternalDownload.java @@ -0,0 +1,55 @@ +package app.revanced.extension.youtube.patches.overlaybutton; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.VideoUtils; + +@SuppressWarnings("unused") +public class ExternalDownload extends BottomControlButton { + @Nullable + private static ExternalDownload instance; + + public ExternalDownload(ViewGroup bottomControlsViewGroup) { + super( + bottomControlsViewGroup, + "external_download_button", + Settings.OVERLAY_BUTTON_EXTERNAL_DOWNLOADER, + view -> VideoUtils.launchVideoExternalDownloader(), + view -> { + VideoUtils.launchLongPressVideoExternalDownloader(); + return true; + } + ); + } + + /** + * Injection point. + */ + public static void initialize(View bottomControlsViewGroup) { + try { + if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { + instance = new ExternalDownload(viewGroup); + } + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + /** + * Injection point. + */ + public static void changeVisibility(boolean showing, boolean animation) { + if (instance != null) instance.setVisibility(showing, animation); + } + + public static void changeVisibilityNegatedImmediate() { + if (instance != null) instance.setVisibilityNegatedImmediate(); + } + + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/MuteVolume.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/MuteVolume.java new file mode 100644 index 000000000..532bc0a62 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/MuteVolume.java @@ -0,0 +1,76 @@ +package app.revanced.extension.youtube.patches.overlaybutton; + +import android.content.Context; +import android.media.AudioManager; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings({"deprecation", "unused"}) +public class MuteVolume extends BottomControlButton { + @Nullable + private static MuteVolume instance; + private static AudioManager audioManager; + private static final int stream = AudioManager.STREAM_MUSIC; + + public MuteVolume(ViewGroup bottomControlsViewGroup) { + super( + bottomControlsViewGroup, + "mute_volume_button", + Settings.OVERLAY_BUTTON_MUTE_VOLUME, + view -> { + if (instance != null && audioManager != null) { + boolean unMuted = !audioManager.isStreamMute(stream); + audioManager.setStreamMute(stream, unMuted); + instance.changeActivated(unMuted); + } + }, + null + ); + } + + /** + * Injection point. + */ + public static void initialize(View bottomControlsViewGroup) { + try { + if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { + instance = new MuteVolume(viewGroup); + } + if (bottomControlsViewGroup.getContext().getSystemService(Context.AUDIO_SERVICE) instanceof AudioManager am) { + audioManager = am; + } + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + /** + * Injection point. + */ + public static void changeVisibility(boolean showing, boolean animation) { + if (instance != null) { + instance.setVisibility(showing, animation); + changeActivated(instance); + } + } + + public static void changeVisibilityNegatedImmediate() { + if (instance != null) { + instance.setVisibilityNegatedImmediate(); + changeActivated(instance); + } + } + + private static void changeActivated(MuteVolume instance) { + if (audioManager != null) { + boolean muted = audioManager.isStreamMute(stream); + instance.changeActivated(muted); + } + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/PlayAll.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/PlayAll.java new file mode 100644 index 000000000..25df9ae4b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/PlayAll.java @@ -0,0 +1,55 @@ +package app.revanced.extension.youtube.patches.overlaybutton; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.VideoUtils; + +@SuppressWarnings("unused") +public class PlayAll extends BottomControlButton { + + @Nullable + private static PlayAll instance; + + public PlayAll(ViewGroup bottomControlsViewGroup) { + super( + bottomControlsViewGroup, + "play_all_button", + Settings.OVERLAY_BUTTON_PLAY_ALL, + view -> VideoUtils.openVideo(Settings.OVERLAY_BUTTON_PLAY_ALL_TYPE.get()), + view -> { + VideoUtils.openVideo(); + return true; + } + ); + } + + /** + * Injection point. + */ + public static void initialize(View bottomControlsViewGroup) { + try { + if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { + instance = new PlayAll(viewGroup); + } + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + /** + * Injection point. + */ + public static void changeVisibility(boolean showing, boolean animation) { + if (instance != null) instance.setVisibility(showing, animation); + } + + public static void changeVisibilityNegatedImmediate() { + if (instance != null) instance.setVisibilityNegatedImmediate(); + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/SpeedDialog.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/SpeedDialog.java new file mode 100644 index 000000000..a091f36c6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/SpeedDialog.java @@ -0,0 +1,68 @@ +package app.revanced.extension.youtube.patches.overlaybutton; + +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.showToastShort; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.VideoInformation; +import app.revanced.extension.youtube.utils.VideoUtils; + +@SuppressWarnings("unused") +public class SpeedDialog extends BottomControlButton { + @Nullable + private static SpeedDialog instance; + + public SpeedDialog(ViewGroup bottomControlsViewGroup) { + super( + bottomControlsViewGroup, + "speed_dialog_button", + Settings.OVERLAY_BUTTON_SPEED_DIALOG, + view -> VideoUtils.showPlaybackSpeedDialog(view.getContext()), + view -> { + if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get() || + VideoInformation.getPlaybackSpeed() == Settings.DEFAULT_PLAYBACK_SPEED.get()) { + VideoInformation.overridePlaybackSpeed(1.0f); + showToastShort(str("revanced_overlay_button_speed_dialog_reset", "1.0")); + } else { + float defaultSpeed = Settings.DEFAULT_PLAYBACK_SPEED.get(); + VideoInformation.overridePlaybackSpeed(defaultSpeed); + showToastShort(str("revanced_overlay_button_speed_dialog_reset", defaultSpeed)); + } + + return true; + } + ); + } + + /** + * Injection point. + */ + public static void initialize(View bottomControlsViewGroup) { + try { + if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { + instance = new SpeedDialog(viewGroup); + } + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + /** + * Injection point. + */ + public static void changeVisibility(boolean showing, boolean animation) { + if (instance != null) instance.setVisibility(showing, animation); + } + + public static void changeVisibilityNegatedImmediate() { + if (instance != null) instance.setVisibilityNegatedImmediate(); + } + + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/Whitelists.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/Whitelists.java new file mode 100644 index 000000000..e88cacd00 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/Whitelists.java @@ -0,0 +1,55 @@ +package app.revanced.extension.youtube.patches.overlaybutton; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.settings.preference.WhitelistedChannelsPreference; +import app.revanced.extension.youtube.whitelist.Whitelist; + +@SuppressWarnings("unused") +public class Whitelists extends BottomControlButton { + @Nullable + private static Whitelists instance; + + public Whitelists(ViewGroup bottomControlsViewGroup) { + super( + bottomControlsViewGroup, + "whitelist_button", + Settings.OVERLAY_BUTTON_WHITELIST, + view -> Whitelist.showWhitelistDialog(view.getContext()), + view -> { + WhitelistedChannelsPreference.showWhitelistedChannelDialog(view.getContext()); + return true; + } + ); + } + + /** + * Injection point. + */ + public static void initialize(View bottomControlsViewGroup) { + try { + if (bottomControlsViewGroup instanceof ViewGroup viewGroup) { + instance = new Whitelists(viewGroup); + } + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + /** + * Injection point. + */ + public static void changeVisibility(boolean showing, boolean animation) { + if (instance != null) instance.setVisibility(showing, animation); + } + + public static void changeVisibilityNegatedImmediate() { + if (instance != null) instance.setVisibilityNegatedImmediate(); + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java new file mode 100644 index 000000000..defbd23a9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java @@ -0,0 +1,743 @@ +package app.revanced.extension.youtube.patches.player; + +import static app.revanced.extension.shared.utils.Utils.hideViewBy0dpUnderCondition; +import static app.revanced.extension.shared.utils.Utils.hideViewByRemovingFromParentUnderCondition; +import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition; +import static app.revanced.extension.youtube.utils.ExtendedUtils.validateValue; + +import android.app.Activity; +import android.content.pm.ActivityInfo; +import android.support.v7.widget.RecyclerView; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.coordinatorlayout.widget.CoordinatorLayout; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.settings.IntegerSetting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.utils.InitializationPatch; +import app.revanced.extension.youtube.patches.utils.PatchStatus; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; +import app.revanced.extension.youtube.shared.RootView; +import app.revanced.extension.youtube.shared.ShortsPlayerState; +import app.revanced.extension.youtube.shared.VideoInformation; +import app.revanced.extension.youtube.utils.VideoUtils; + +@SuppressWarnings("unused") +public class PlayerPatch { + private static final IntegerSetting quickActionsMarginTopSetting = Settings.QUICK_ACTIONS_TOP_MARGIN; + + private static final int PLAYER_OVERLAY_OPACITY_LEVEL; + private static final int QUICK_ACTIONS_MARGIN_TOP; + private static final float SPEED_OVERLAY_VALUE; + + static { + final int opacity = validateValue( + Settings.CUSTOM_PLAYER_OVERLAY_OPACITY, + 0, + 100, + "revanced_custom_player_overlay_opacity_invalid_toast" + ); + PLAYER_OVERLAY_OPACITY_LEVEL = (opacity * 255) / 100; + + SPEED_OVERLAY_VALUE = validateValue( + Settings.SPEED_OVERLAY_VALUE, + 0.0f, + 8.0f, + "revanced_speed_overlay_value_invalid_toast" + ); + + final int topMargin = validateValue( + Settings.QUICK_ACTIONS_TOP_MARGIN, + 0, + 32, + "revanced_quick_actions_top_margin_invalid_toast" + ); + + QUICK_ACTIONS_MARGIN_TOP = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (float) topMargin, Utils.getResources().getDisplayMetrics()); + } + + // region [Ambient mode control] patch + + /** + * Constant found in: androidx.window.embedding.DividerAttributes + */ + private static final int DIVIDER_ATTRIBUTES_COLOR_SYSTEM_DEFAULT = -16777216; + + public static boolean bypassAmbientModeRestrictions(boolean original) { + return (!Settings.BYPASS_AMBIENT_MODE_RESTRICTIONS.get() && original) || Settings.DISABLE_AMBIENT_MODE.get(); + } + + public static boolean disableAmbientModeInFullscreen() { + return !Settings.DISABLE_AMBIENT_MODE_IN_FULLSCREEN.get(); + } + + public static int getFullScreenBackgroundColor(int originalColor) { + if (Settings.DISABLE_AMBIENT_MODE_IN_FULLSCREEN.get()) { + return DIVIDER_ATTRIBUTES_COLOR_SYSTEM_DEFAULT; + } + + return originalColor; + } + + // endregion + + // region [Change player flyout menu toggles] patch + + public static boolean changeSwitchToggle(boolean original) { + return !Settings.CHANGE_PLAYER_FLYOUT_MENU_TOGGLE.get() && original; + } + + public static String getToggleString(String str) { + return ResourceUtils.getString(str); + } + + // endregion + + // region [Description components] patch + + public static boolean disableRollingNumberAnimations() { + return Settings.DISABLE_ROLLING_NUMBER_ANIMATIONS.get(); + } + + /** + * view id R.id.content + */ + private static final int contentId = ResourceUtils.getIdIdentifier("content"); + private static final boolean expandDescriptionEnabled = Settings.EXPAND_VIDEO_DESCRIPTION.get(); + private static final String descriptionString = Settings.EXPAND_VIDEO_DESCRIPTION_STRINGS.get(); + + private static boolean isDescriptionPanel = false; + + public static void setContentDescription(String contentDescription) { + if (!expandDescriptionEnabled) { + return; + } + if (contentDescription == null || contentDescription.isEmpty()) { + isDescriptionPanel = false; + return; + } + if (descriptionString.isEmpty()) { + isDescriptionPanel = false; + return; + } + isDescriptionPanel = descriptionString.equals(contentDescription); + } + + /** + * The last time the clickDescriptionView method was called. + */ + private static long lastTimeDescriptionViewInvoked; + + + public static void onVideoDescriptionCreate(RecyclerView recyclerView) { + if (!expandDescriptionEnabled) + return; + + recyclerView.getViewTreeObserver().addOnDrawListener(() -> { + try { + // Video description panel is only open when the player is active. + if (!RootView.isPlayerActive()) { + return; + } + // Video description's recyclerView is a child view of [contentId]. + if (!(recyclerView.getParent().getParent() instanceof View contentView)) { + return; + } + if (contentView.getId() != contentId) { + return; + } + // This method is invoked whenever the Engagement panel is opened. (Description, Chapters, Comments, etc.) + // Check the title of the Engagement panel to prevent unnecessary clicking. + if (!isDescriptionPanel) { + return; + } + // The first view group contains information such as the video's title, like count, and number of views. + if (!(recyclerView.getChildAt(0) instanceof ViewGroup primaryViewGroup)) { + return; + } + if (primaryViewGroup.getChildCount() < 2) { + return; + } + // Typically, descriptionView is placed as the second child of recyclerView. + if (recyclerView.getChildAt(1) instanceof ViewGroup viewGroup) { + clickDescriptionView(viewGroup); + } + // In some videos, descriptionView is placed as the third child of recyclerView. + if (recyclerView.getChildAt(2) instanceof ViewGroup viewGroup) { + clickDescriptionView(viewGroup); + } + // Even if both methods are performed, there is no major issue with the operation of the patch. + } catch (Exception ex) { + Logger.printException(() -> "onVideoDescriptionCreate failed.", ex); + } + }); + } + + private static void clickDescriptionView(@NonNull ViewGroup descriptionViewGroup) { + final View descriptionView = descriptionViewGroup.getChildAt(0); + if (descriptionView == null) { + return; + } + // This method is sometimes used multiple times. + // To prevent this, ignore method reuse within 1 second. + final long now = System.currentTimeMillis(); + if (now - lastTimeDescriptionViewInvoked < 1000) { + return; + } + lastTimeDescriptionViewInvoked = now; + + // The type of descriptionView can be either ViewGroup or TextView. (A/B tests) + // If the type of descriptionView is TextView, longer delay is required. + final long delayMillis = descriptionView instanceof TextView + ? 500 + : 100; + + Utils.runOnMainThreadDelayed(() -> Utils.clickView(descriptionView), delayMillis); + } + + /** + * This method is invoked only when the view type of descriptionView is {@link TextView}. (A/B tests) + * + * @param textView descriptionView. + * @param original Whether to apply {@link TextView#setTextIsSelectable}. + * Patch replaces the {@link TextView#setTextIsSelectable} method invoke. + */ + public static void disableVideoDescriptionInteraction(TextView textView, boolean original) { + if (textView != null) { + textView.setTextIsSelectable( + !Settings.DISABLE_VIDEO_DESCRIPTION_INTERACTION.get() && original + ); + } + } + + // endregion + + // region [Disable haptic feedback] patch + + public static boolean disableChapterVibrate() { + return Settings.DISABLE_HAPTIC_FEEDBACK_CHAPTERS.get(); + } + + + public static boolean disableSeekVibrate() { + return Settings.DISABLE_HAPTIC_FEEDBACK_SEEK.get(); + } + + public static boolean disableSeekUndoVibrate() { + return Settings.DISABLE_HAPTIC_FEEDBACK_SEEK_UNDO.get(); + } + + public static boolean disableScrubbingVibrate() { + return Settings.DISABLE_HAPTIC_FEEDBACK_SCRUBBING.get(); + } + + public static boolean disableZoomVibrate() { + return Settings.DISABLE_HAPTIC_FEEDBACK_ZOOM.get(); + } + + // endregion + + // region [Fullscreen components] patch + + public static void disableEngagementPanels(CoordinatorLayout coordinatorLayout) { + if (!Settings.DISABLE_ENGAGEMENT_PANEL.get()) return; + coordinatorLayout.setVisibility(View.GONE); + } + + public static void showVideoTitleSection(FrameLayout frameLayout, View view) { + final boolean isEnabled = Settings.SHOW_VIDEO_TITLE_SECTION.get() || !Settings.DISABLE_ENGAGEMENT_PANEL.get(); + + if (isEnabled) { + frameLayout.addView(view); + } + } + + public static boolean hideAutoPlayPreview() { + return Settings.HIDE_AUTOPLAY_PREVIEW.get(); + } + + public static boolean hideRelatedVideoOverlay() { + return Settings.HIDE_RELATED_VIDEO_OVERLAY.get(); + } + + public static void hideQuickActions(View view) { + final boolean isEnabled = Settings.DISABLE_ENGAGEMENT_PANEL.get() || Settings.HIDE_QUICK_ACTIONS.get(); + + Utils.hideViewBy0dpUnderCondition( + isEnabled, + view + ); + } + + public static void setQuickActionMargin(View view) { + int topMarginPx = getQuickActionsTopMargin(); + if (topMarginPx == 0) { + return; + } + + if (!(view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams mlp)) + return; + + mlp.setMargins( + mlp.leftMargin, + topMarginPx, + mlp.rightMargin, + mlp.bottomMargin + ); + view.requestLayout(); + } + + public static boolean enableCompactControlsOverlay(boolean original) { + return Settings.ENABLE_COMPACT_CONTROLS_OVERLAY.get() || original; + } + + public static boolean disableLandScapeMode(boolean original) { + return Settings.DISABLE_LANDSCAPE_MODE.get() || original; + } + + private static volatile boolean isScreenOn; + + public static boolean keepFullscreen(boolean original) { + if (!Settings.KEEP_LANDSCAPE_MODE.get()) + return original; + + return isScreenOn; + } + + public static void setScreenOn() { + if (!Settings.KEEP_LANDSCAPE_MODE.get()) + return; + + isScreenOn = true; + Utils.runOnMainThreadDelayed(() -> isScreenOn = false, Settings.KEEP_LANDSCAPE_MODE_TIMEOUT.get()); + } + + private static WeakReference watchDescriptorActivityRef = new WeakReference<>(null); + private static volatile boolean isLandScapeVideo = true; + + public static void setWatchDescriptorActivity(Activity activity) { + watchDescriptorActivityRef = new WeakReference<>(activity); + } + + public static boolean forceFullscreen(boolean original) { + if (!Settings.FORCE_FULLSCREEN.get()) + return original; + + Utils.runOnMainThreadDelayed(PlayerPatch::setOrientation, 1000); + return true; + } + + private static void setOrientation() { + final Activity watchDescriptorActivity = watchDescriptorActivityRef.get(); + final int requestedOrientation = isLandScapeVideo + ? ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + : watchDescriptorActivity.getRequestedOrientation(); + + watchDescriptorActivity.setRequestedOrientation(requestedOrientation); + } + + public static void setVideoPortrait(int width, int height) { + if (!Settings.FORCE_FULLSCREEN.get()) + return; + + isLandScapeVideo = width > height; + } + + // endregion + + // region [Hide comments component] patch + + public static void changeEmojiPickerOpacity(ImageView imageView) { + if (!Settings.HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS.get()) + return; + + imageView.setImageAlpha(0); + } + + @Nullable + public static Object disableEmojiPickerOnClickListener(@Nullable Object object) { + return Settings.HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS.get() ? null : object; + } + + // endregion + + // region [Hide player buttons] patch + + public static boolean hideAutoPlayButton() { + return Settings.HIDE_PLAYER_AUTOPLAY_BUTTON.get(); + } + + public static boolean hideCaptionsButton(boolean original) { + return !Settings.HIDE_PLAYER_CAPTIONS_BUTTON.get() && original; + } + + public static int hideCastButton(int original) { + return Settings.HIDE_PLAYER_CAST_BUTTON.get() + ? View.GONE + : original; + } + + public static void hideCaptionsButton(View view) { + Utils.hideViewUnderCondition(Settings.HIDE_PLAYER_CAPTIONS_BUTTON, view); + } + + public static void hideCollapseButton(ImageView imageView) { + if (!Settings.HIDE_PLAYER_COLLAPSE_BUTTON.get()) + return; + + imageView.setImageResource(android.R.color.transparent); + imageView.setImageAlpha(0); + imageView.setEnabled(false); + + var layoutParams = imageView.getLayoutParams(); + if (layoutParams instanceof RelativeLayout.LayoutParams) { + RelativeLayout.LayoutParams lp = new RelativeLayout.LayoutParams(0, 0); + imageView.setLayoutParams(lp); + } else { + Logger.printDebug(() -> "Unknown collapse button layout params: " + layoutParams); + } + } + + public static void setTitleAnchorStartMargin(View titleAnchorView) { + if (!Settings.HIDE_PLAYER_COLLAPSE_BUTTON.get()) + return; + + var layoutParams = titleAnchorView.getLayoutParams(); + if (titleAnchorView.getLayoutParams() instanceof RelativeLayout.LayoutParams lp) { + lp.setMarginStart(0); + } else { + Logger.printDebug(() -> "Unknown title anchor layout params: " + layoutParams); + } + } + + public static ImageView hideFullscreenButton(ImageView imageView) { + final boolean hideView = Settings.HIDE_PLAYER_FULLSCREEN_BUTTON.get(); + + Utils.hideViewUnderCondition(hideView, imageView); + return hideView ? null : imageView; + } + + public static boolean hidePreviousNextButton(boolean previousOrNextButtonVisible) { + return !Settings.HIDE_PLAYER_PREVIOUS_NEXT_BUTTON.get() && previousOrNextButtonVisible; + } + + private static final int playerControlPreviousButtonTouchAreaId = + ResourceUtils.getIdIdentifier("player_control_previous_button_touch_area"); + private static final int playerControlNextButtonTouchAreaId = + ResourceUtils.getIdIdentifier("player_control_next_button_touch_area"); + + public static void hidePreviousNextButtons(View parentView) { + if (!Settings.HIDE_PLAYER_PREVIOUS_NEXT_BUTTON.get()) { + return; + } + + // Must use a deferred call to main thread to hide the button. + // Otherwise the layout crashes if set to hidden now. + Utils.runOnMainThread(() -> { + hideView(parentView, playerControlPreviousButtonTouchAreaId); + hideView(parentView, playerControlNextButtonTouchAreaId); + }); + } + + private static void hideView(View parentView, int resourceId) { + View nextPreviousButton = parentView.findViewById(resourceId); + + if (nextPreviousButton == null) { + Logger.printException(() -> "Could not find player previous / next button"); + return; + } + + Utils.hideViewByRemovingFromParentUnderCondition(true, nextPreviousButton); + } + + public static boolean hideMusicButton() { + return Settings.HIDE_PLAYER_YOUTUBE_MUSIC_BUTTON.get(); + } + + // endregion + + // region [Player components] patch + + public static void changeOpacity(ImageView imageView) { + imageView.setImageAlpha(PLAYER_OVERLAY_OPACITY_LEVEL); + } + + private static boolean isAutoPopupPanel; + + public static boolean disableAutoPlayerPopupPanels(boolean isLiveChatOrPlaylistPanel) { + if (!Settings.DISABLE_AUTO_PLAYER_POPUP_PANELS.get()) { + return false; + } + if (isLiveChatOrPlaylistPanel) { + return true; + } + return isAutoPopupPanel && ShortsPlayerState.getCurrent().isClosed(); + } + + public static void setInitVideoPanel(boolean initVideoPanel) { + isAutoPopupPanel = initVideoPanel; + } + + @NonNull + public static String videoId = ""; + + public static void disableAutoSwitchMixPlaylists(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, + @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, + final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { + if (!Settings.DISABLE_AUTO_SWITCH_MIX_PLAYLISTS.get()) { + return; + } + if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL) { + return; + } + if (Objects.equals(newlyLoadedVideoId, videoId)) { + return; + } + videoId = newlyLoadedVideoId; + + if (!VideoInformation.lastPlayerResponseIsAutoGeneratedMixPlaylist()) { + return; + } + VideoUtils.pauseMedia(); + VideoUtils.openVideo(videoId); + } + + public static boolean disableSpeedOverlay() { + return disableSpeedOverlay(true); + } + + public static boolean disableSpeedOverlay(boolean original) { + return !Settings.DISABLE_SPEED_OVERLAY.get() && original; + } + + public static double speedOverlayValue() { + return speedOverlayValue(2.0f); + } + + public static float speedOverlayValue(float original) { + return SPEED_OVERLAY_VALUE; + } + + public static boolean hideChannelWatermark(boolean original) { + return !Settings.HIDE_CHANNEL_WATERMARK.get() && original; + } + + public static void hideCrowdfundingBox(View view) { + hideViewBy0dpUnderCondition(Settings.HIDE_CROWDFUNDING_BOX.get(), view); + } + + public static void hideDoubleTapOverlayFilter(View view) { + hideViewByRemovingFromParentUnderCondition(Settings.HIDE_DOUBLE_TAP_OVERLAY_FILTER, view); + } + + public static void hideEndScreenCards(View view) { + if (Settings.HIDE_END_SCREEN_CARDS.get()) { + view.setVisibility(View.GONE); + } + } + + public static boolean hideFilmstripOverlay() { + return Settings.HIDE_FILMSTRIP_OVERLAY.get(); + } + + public static boolean hideInfoCard(boolean original) { + return !Settings.HIDE_INFO_CARDS.get() && original; + } + + public static boolean hideSeekMessage() { + return Settings.HIDE_SEEK_MESSAGE.get(); + } + + public static boolean hideSeekUndoMessage() { + return Settings.HIDE_SEEK_UNDO_MESSAGE.get(); + } + + public static void hideSuggestedActions(View view) { + hideViewUnderCondition(Settings.HIDE_SUGGESTED_ACTION.get(), view); + } + + public static boolean hideSuggestedVideoEndScreen() { + return Settings.HIDE_SUGGESTED_VIDEO_END_SCREEN.get(); + } + + public static void skipAutoPlayCountdown(View view) { + if (!hideSuggestedVideoEndScreen()) + return; + if (!Settings.SKIP_AUTOPLAY_COUNTDOWN.get()) + return; + + Utils.clickView(view); + } + + public static boolean hideZoomOverlay() { + return Settings.HIDE_ZOOM_OVERLAY.get(); + } + + // endregion + + // region [Hide player flyout menu] patch + + private static final String QUALITY_LABEL_PREMIUM = "1080p Premium"; + + public static String hidePlayerFlyoutMenuEnhancedBitrate(String qualityLabel) { + return Settings.HIDE_PLAYER_FLYOUT_MENU_ENHANCED_BITRATE.get() && + Objects.equals(QUALITY_LABEL_PREMIUM, qualityLabel) + ? null + : qualityLabel; + } + + public static void hidePlayerFlyoutMenuCaptionsFooter(View view) { + Utils.hideViewUnderCondition( + Settings.HIDE_PLAYER_FLYOUT_MENU_CAPTIONS_FOOTER.get(), + view + ); + } + + public static void hidePlayerFlyoutMenuQualityFooter(View view) { + Utils.hideViewUnderCondition( + Settings.HIDE_PLAYER_FLYOUT_MENU_QUALITY_FOOTER.get(), + view + ); + } + + public static View hidePlayerFlyoutMenuQualityHeader(View view) { + return Settings.HIDE_PLAYER_FLYOUT_MENU_QUALITY_HEADER.get() + ? new View(view.getContext()) // empty view + : view; + } + + /** + * Overriding this values is possible only after the litho component has been loaded. + * Otherwise, crash will occur. + * See {@link InitializationPatch#onCreate}. + * + * @param original original value. + * @return whether to enable PiP Mode in the player flyout menu. + */ + public static boolean hidePiPModeMenu(boolean original) { + if (!BaseSettings.SETTINGS_INITIALIZED.get()) { + return original; + } + + return !Settings.HIDE_PLAYER_FLYOUT_MENU_PIP.get(); + } + + /** + * Overriding this values is possible only after the litho component has been loaded. + * Otherwise, crash will occur. + * See {@link InitializationPatch#onCreate}. + * + * @param original original value. + * @return whether to enable Sleep timer Mode in the player flyout menu. + */ + public static boolean hideDeprecatedSleepTimerMenu(boolean original) { + if (!BaseSettings.SETTINGS_INITIALIZED.get()) { + return original; + } + + return !Settings.HIDE_PLAYER_FLYOUT_MENU_SLEEP_TIMER.get(); + } + + // endregion + + // region [Seekbar components] patch + + public static String appendTimeStampInformation(String original) { + if (!Settings.APPEND_TIME_STAMP_INFORMATION.get()) return original; + + String appendString = Settings.APPEND_TIME_STAMP_INFORMATION_TYPE.get() + ? VideoUtils.getFormattedQualityString(null) + : VideoUtils.getFormattedSpeedString(null); + + // Encapsulate the entire appendString with bidi control characters + appendString = "\u2066" + appendString + "\u2069"; + + // Format the original string with the appended timestamp information + return String.format( + "%s\u2009•\u2009%s", // Add the separator and the appended information + original, appendString + ); + } + + public static void setContainerClickListener(View view) { + if (!Settings.APPEND_TIME_STAMP_INFORMATION.get()) + return; + + if (!(view.getParent() instanceof View containerView)) + return; + + final BooleanSetting appendTypeSetting = Settings.APPEND_TIME_STAMP_INFORMATION_TYPE; + final boolean previousBoolean = appendTypeSetting.get(); + + containerView.setOnLongClickListener(timeStampContainerView -> { + appendTypeSetting.save(!previousBoolean); + return true; + } + ); + + if (Settings.REPLACE_TIME_STAMP_ACTION.get()) { + containerView.setOnClickListener(timeStampContainerView -> VideoUtils.showFlyoutMenu()); + } + } + + public static boolean enableSeekbarTapping() { + return Settings.ENABLE_SEEKBAR_TAPPING.get(); + } + + public static boolean enableHighQualityFullscreenThumbnails() { + return Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get(); + } + + private static final int timeBarChapterViewId = + ResourceUtils.getIdIdentifier("time_bar_chapter_title"); + + public static boolean hideSeekbar() { + return Settings.HIDE_SEEKBAR.get(); + } + + public static boolean disableSeekbarChapters() { + return Settings.DISABLE_SEEKBAR_CHAPTERS.get(); + } + + public static boolean hideSeekbarChapterLabel(View view) { + return Settings.HIDE_SEEKBAR_CHAPTER_LABEL.get() && view.getId() == timeBarChapterViewId; + } + + public static boolean hideTimeStamp() { + return Settings.HIDE_TIME_STAMP.get(); + } + + public static boolean restoreOldSeekbarThumbnails() { + return !Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get(); + } + + public static boolean enableCairoSeekbar() { + return Settings.ENABLE_CAIRO_SEEKBAR.get(); + } + + // endregion + + public static int getQuickActionsTopMargin() { + if (!PatchStatus.QuickActions()) { + return 0; + } + return QUICK_ACTIONS_MARGIN_TOP; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/SeekbarColorPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/SeekbarColorPatch.java new file mode 100644 index 000000000..d31788832 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/SeekbarColorPatch.java @@ -0,0 +1,264 @@ +package app.revanced.extension.youtube.patches.player; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.content.res.Resources; +import android.graphics.Color; +import android.graphics.drawable.AnimatedVectorDrawable; + +import java.util.Arrays; +import java.util.Locale; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class SeekbarColorPatch { + + private static final boolean CUSTOM_SEEKBAR_COLOR_ENABLED = + Settings.ENABLE_CUSTOM_SEEKBAR_COLOR.get(); + + /** + * Default color of the litho seekbar. + * Differs slightly from the default custom seekbar color setting. + */ + private static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0000; + + /** + * Default colors of the gradient seekbar. + */ + private static final int[] ORIGINAL_SEEKBAR_GRADIENT_COLORS = {0xFFFF0033, 0xFFFF2791}; + + /** + * Default positions of the gradient seekbar. + */ + private static final float[] ORIGINAL_SEEKBAR_GRADIENT_POSITIONS = {0.8f, 1.0f}; + + /** + * Default YouTube seekbar color brightness. + */ + private static final float ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS; + + /** + * If {@link Settings#ENABLE_CUSTOM_SEEKBAR_COLOR} is enabled, + * this is the color value of {@link Settings#ENABLE_CUSTOM_SEEKBAR_COLOR_VALUE}. + * Otherwise this is {@link #ORIGINAL_SEEKBAR_COLOR}. + */ + private static int seekbarColor = ORIGINAL_SEEKBAR_COLOR; + + /** + * Custom seekbar hue, saturation, and brightness values. + */ + private static final float[] customSeekbarColorHSV = new float[3]; + + static { + float[] hsv = new float[3]; + Color.colorToHSV(ORIGINAL_SEEKBAR_COLOR, hsv); + ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS = hsv[2]; + + if (CUSTOM_SEEKBAR_COLOR_ENABLED) { + loadCustomSeekbarColor(); + } + } + + private static void loadCustomSeekbarColor() { + try { + seekbarColor = Color.parseColor(Settings.ENABLE_CUSTOM_SEEKBAR_COLOR_VALUE.get()); + Color.colorToHSV(seekbarColor, customSeekbarColorHSV); + } catch (Exception ex) { + Utils.showToastShort(str("revanced_custom_seekbar_color_value_invalid_invalid_toast")); + Utils.showToastShort(str("revanced_extended_reset_to_default_toast")); + Settings.ENABLE_CUSTOM_SEEKBAR_COLOR_VALUE.resetToDefault(); + loadCustomSeekbarColor(); + } + } + + public static int getSeekbarColor() { + return seekbarColor; + } + + /** + * Injection point + */ + public static boolean playerSeekbarGradientEnabled(boolean original) { + if (CUSTOM_SEEKBAR_COLOR_ENABLED) return false; + + return original; + } + + /** + * Injection point + */ + public static boolean useLotteLaunchSplashScreen(boolean original) { + Logger.printDebug(() -> "useLotteLaunchSplashScreen original: " + original); + + if (CUSTOM_SEEKBAR_COLOR_ENABLED) return false; + + return original; + } + + private static int colorChannelTo3Bits(int channel8Bits) { + final float channel3Bits = channel8Bits * 7 / 255f; + + // If a color channel is near zero, then allow rounding up so values between + // 0x12 and 0x23 will show as 0x24. But always round down when the channel is + // near full saturation, otherwise rounding to nearest will cause all values + // between 0xEC and 0xFE to always show as full saturation (0xFF). + return channel3Bits < 6 + ? Math.round(channel3Bits) + : (int) channel3Bits; + } + + private static String get9BitStyleIdentifier(int color24Bit) { + final int r3 = colorChannelTo3Bits(Color.red(color24Bit)); + final int g3 = colorChannelTo3Bits(Color.green(color24Bit)); + final int b3 = colorChannelTo3Bits(Color.blue(color24Bit)); + + return String.format(Locale.US, "splash_seekbar_color_style_%d_%d_%d", r3, g3, b3); + } + + /** + * Injection point + */ + public static void setSplashAnimationDrawableTheme(AnimatedVectorDrawable vectorDrawable) { + // Alternatively a ColorMatrixColorFilter can be used to change the color of the drawable + // without using any styles, but a color filter cannot selectively change the seekbar + // while keeping the red YT logo untouched. + // Even if the seekbar color xml value is changed to a completely different color (such as green), + // a color filter still cannot be selectively applied when the drawable has more than 1 color. + try { + String seekbarStyle = get9BitStyleIdentifier(seekbarColor); + Logger.printDebug(() -> "Using splash seekbar style: " + seekbarStyle); + + final int styleIdentifierDefault = ResourceUtils.getStyleIdentifier(seekbarStyle); + if (styleIdentifierDefault == 0) { + throw new RuntimeException("Seekbar style not found: " + seekbarStyle); + } + + Resources.Theme theme = Utils.getContext().getResources().newTheme(); + theme.applyStyle(styleIdentifierDefault, true); + + vectorDrawable.applyTheme(theme); + } catch (Exception ex) { + Logger.printException(() -> "setSplashAnimationDrawableTheme failure", ex); + } + } + + /** + * Injection point. + *

+ * Overrides all Litho components that use the YouTube seekbar color. + * Used only for the video thumbnails seekbar. + *

+ * If {@link Settings#HIDE_SEEKBAR_THUMBNAIL} is enabled, this returns a fully transparent color. + */ + public static int getLithoColor(int colorValue) { + if (colorValue == ORIGINAL_SEEKBAR_COLOR) { + if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) { + return 0x00000000; + } + + return getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR); + } + return colorValue; + } + + /** + * Injection point. + */ + public static void setLinearGradient(int[] colors, float[] positions) { + final boolean hideSeekbar = Settings.HIDE_SEEKBAR_THUMBNAIL.get(); + + if (CUSTOM_SEEKBAR_COLOR_ENABLED || hideSeekbar) { + // Most litho usage of linear gradients is hooked here, + // so must only change if the values are those for the seekbar. + if (Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_COLORS, colors) + && Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_POSITIONS, positions)) { + Arrays.fill(colors, hideSeekbar + ? 0x00000000 + : seekbarColor); + return; + } + + Logger.printDebug(() -> "Ignoring gradient colors: " + Arrays.toString(colors) + + " positions: " + Arrays.toString(positions)); + } + } + + /** + * Injection point. + *

+ * Overrides color when video player seekbar is clicked. + */ + public static int getVideoPlayerSeekbarClickedColor(int colorValue) { + if (!CUSTOM_SEEKBAR_COLOR_ENABLED) { + return colorValue; + } + + return colorValue == ORIGINAL_SEEKBAR_COLOR + ? getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR) + : colorValue; + } + + /** + * Injection point. + *

+ * Overrides color used for the video player seekbar. + */ + public static int getVideoPlayerSeekbarColor(int originalColor) { + if (!CUSTOM_SEEKBAR_COLOR_ENABLED) { + return originalColor; + } + + return getSeekbarColorValue(originalColor); + } + + /** + * Color parameter is changed to the custom seekbar color, while retaining + * the brightness and alpha changes of the parameter value compared to the original seekbar color. + */ + private static int getSeekbarColorValue(int originalColor) { + try { + if (!CUSTOM_SEEKBAR_COLOR_ENABLED || originalColor == seekbarColor) { + return originalColor; // nothing to do + } + + final int alphaDifference = Color.alpha(originalColor) - Color.alpha(ORIGINAL_SEEKBAR_COLOR); + + // The seekbar uses the same color but different brightness for different situations. + float[] hsv = new float[3]; + Color.colorToHSV(originalColor, hsv); + final float brightnessDifference = hsv[2] - ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS; + + // Apply the brightness difference to the custom seekbar color. + hsv[0] = customSeekbarColorHSV[0]; + hsv[1] = customSeekbarColorHSV[1]; + hsv[2] = clamp(customSeekbarColorHSV[2] + brightnessDifference, 0, 1); + + final int replacementAlpha = clamp(Color.alpha(seekbarColor) + alphaDifference, 0, 255); + final int replacementColor = Color.HSVToColor(replacementAlpha, hsv); + Logger.printDebug(() -> String.format("Original color: #%08X replacement color: #%08X", + originalColor, replacementColor)); + return replacementColor; + } catch (Exception ex) { + Logger.printException(() -> "getSeekbarColorValue failure", ex); + 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)); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/AnimationFeedbackPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/AnimationFeedbackPatch.java new file mode 100644 index 000000000..e21d61a0b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/AnimationFeedbackPatch.java @@ -0,0 +1,84 @@ +package app.revanced.extension.youtube.patches.shorts; + +import static app.revanced.extension.shared.utils.ResourceUtils.getRawIdentifier; +import static app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType.ORIGINAL; + +import androidx.annotation.Nullable; + +import com.airbnb.lottie.LottieAnimationView; + +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.youtube.patches.utils.LottieAnimationViewPatch; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class AnimationFeedbackPatch { + + public enum AnimationType { + /** + * Unmodified type, and same as un-patched. + */ + ORIGINAL(null), + THUMBS_UP("like_tap_feedback"), + THUMBS_UP_CAIRO("like_tap_feedback_cairo"), + HEART("like_tap_feedback_heart"), + HEART_TINT("like_tap_feedback_heart_tint"), + HIDDEN("like_tap_feedback_hidden"); + + /** + * Animation id. + */ + final int rawRes; + + AnimationType(@Nullable String jsonName) { + this.rawRes = jsonName != null + ? getRawIdentifier(jsonName) + : 0; + } + } + + private static final AnimationType CURRENT_TYPE = Settings.ANIMATION_TYPE.get(); + + private static final boolean HIDE_PLAY_PAUSE_FEEDBACK = Settings.HIDE_SHORTS_PLAY_PAUSE_BUTTON_BACKGROUND.get(); + + private static final int PAUSE_TAP_FEEDBACK_HIDDEN + = ResourceUtils.getRawIdentifier("pause_tap_feedback_hidden"); + + private static final int PLAY_TAP_FEEDBACK_HIDDEN + = ResourceUtils.getRawIdentifier("play_tap_feedback_hidden"); + + + /** + * Injection point. + */ + public static void setShortsLikeFeedback(LottieAnimationView lottieAnimationView) { + if (CURRENT_TYPE == ORIGINAL) { + return; + } + + LottieAnimationViewPatch.setLottieAnimationRawResources(lottieAnimationView, CURRENT_TYPE.rawRes); + } + + /** + * Injection point. + */ + public static void setShortsPauseFeedback(LottieAnimationView lottieAnimationView) { + if (!HIDE_PLAY_PAUSE_FEEDBACK) { + return; + } + + LottieAnimationViewPatch.setLottieAnimationRawResources(lottieAnimationView, PAUSE_TAP_FEEDBACK_HIDDEN); + } + + /** + * Injection point. + */ + public static void setShortsPlayFeedback(LottieAnimationView lottieAnimationView) { + if (!HIDE_PLAY_PAUSE_FEEDBACK) { + return; + } + + LottieAnimationViewPatch.setLottieAnimationRawResources(lottieAnimationView, PLAY_TAP_FEEDBACK_HIDDEN); + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/CustomActionsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/CustomActionsPatch.java new file mode 100644 index 000000000..4bf010129 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/CustomActionsPatch.java @@ -0,0 +1,471 @@ +package app.revanced.extension.youtube.patches.shorts; + +import static app.revanced.extension.shared.utils.ResourceUtils.getString; +import static app.revanced.extension.shared.utils.Utils.dpToPx; +import static app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter.isShortsFlyoutMenuVisible; +import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan; + +import android.app.AlertDialog; +import android.content.Context; +import android.graphics.ColorFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; +import android.graphics.drawable.StateListDrawable; +import android.support.v7.widget.RecyclerView; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.apache.commons.lang3.StringUtils; + +import java.lang.ref.WeakReference; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.components.ShortsCustomActionsFilter; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.ShortsPlayerState; +import app.revanced.extension.youtube.utils.ThemeUtils; +import app.revanced.extension.youtube.utils.VideoUtils; + +@SuppressWarnings("unused") +public final class CustomActionsPatch { + private static final boolean IS_SPOOFING_TO_YOUTUBE_2023 = + isSpoofingToLessThan("19.00.00"); + private static final boolean SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED = + !IS_SPOOFING_TO_YOUTUBE_2023 && Settings.ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU.get(); + private static final boolean SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED = + Settings.ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR.get(); + + private static final int arrSize = CustomAction.values().length; + private static final Map flyoutMenuMap = new LinkedHashMap<>(arrSize); + private static WeakReference contextRef = new WeakReference<>(null); + private static WeakReference recyclerViewRef = new WeakReference<>(null); + + /** + * Injection point. + */ + public static void setToolbarMenu(String enumString, View toolbarView) { + if (!SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED) { + return; + } + if (ShortsPlayerState.getCurrent().isClosed()) { + return; + } + if (!isMoreButton(enumString)) { + return; + } + setToolbarMenuOnLongClickListener((ViewGroup) toolbarView); + } + + private static void setToolbarMenuOnLongClickListener(ViewGroup parentView) { + ImageView imageView = Utils.getChildView(parentView, v -> v instanceof ImageView); + if (imageView == null) { + return; + } + Context context = imageView.getContext(); + contextRef = new WeakReference<>(context); + + // Overriding is possible only after OnClickListener is assigned to the more button. + Utils.runOnMainThreadDelayed(() -> imageView.setOnLongClickListener(button -> { + showMoreButtonDialog(context); + return true; + }), 0); + } + + private static void showMoreButtonDialog(Context context) { + ScrollView scrollView = new ScrollView(context); + LinearLayout container = new LinearLayout(context); + + container.setOrientation(LinearLayout.VERTICAL); + container.setPadding(0, 0, 0, 0); + + Map toolbarMap = new LinkedHashMap<>(arrSize); + + for (CustomAction customAction : CustomAction.values()) { + if (customAction.settings.get()) { + String title = customAction.getLabel(); + int iconId = customAction.getDrawableId(); + Runnable action = customAction.getOnClickAction(); + LinearLayout itemLayout = createItemLayout(context, title, iconId); + toolbarMap.putIfAbsent(itemLayout, action); + container.addView(itemLayout); + } + } + + scrollView.addView(container); + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setView(scrollView); + + AlertDialog dialog = builder.create(); + dialog.show(); + + toolbarMap.forEach((view, action) -> + view.setOnClickListener(v -> { + action.run(); + dialog.dismiss(); + }) + ); + toolbarMap.clear(); + + Window window = dialog.getWindow(); + if (window == null) { + return; + } + + // round corners + GradientDrawable dialogBackground = new GradientDrawable(); + dialogBackground.setCornerRadius(32); + window.setBackgroundDrawable(dialogBackground); + + // fit screen width + int dialogWidth = (int) (context.getResources().getDisplayMetrics().widthPixels * 0.95); + window.setLayout(dialogWidth, ViewGroup.LayoutParams.WRAP_CONTENT); + + // move dialog to bottom + WindowManager.LayoutParams layoutParams = window.getAttributes(); + layoutParams.gravity = Gravity.BOTTOM; + + // adjust the vertical offset + layoutParams.y = dpToPx(5); + + window.setAttributes(layoutParams); + } + + private static LinearLayout createItemLayout(Context context, String title, int iconId) { + // Item Layout + LinearLayout itemLayout = new LinearLayout(context); + itemLayout.setOrientation(LinearLayout.HORIZONTAL); + itemLayout.setPadding(dpToPx(16), dpToPx(12), dpToPx(16), dpToPx(12)); + itemLayout.setGravity(Gravity.CENTER_VERTICAL); + itemLayout.setClickable(true); + itemLayout.setFocusable(true); + + // Create a StateListDrawable for the background + StateListDrawable background = new StateListDrawable(); + ColorDrawable pressedDrawable = new ColorDrawable(ThemeUtils.getPressedElementColor()); + ColorDrawable defaultDrawable = new ColorDrawable(ThemeUtils.getBackgroundColor()); + background.addState(new int[]{android.R.attr.state_pressed}, pressedDrawable); + background.addState(new int[]{}, defaultDrawable); + itemLayout.setBackground(background); + + // Icon + ColorFilter cf = new PorterDuffColorFilter(ThemeUtils.getForegroundColor(), PorterDuff.Mode.SRC_ATOP); + ImageView iconView = new ImageView(context); + iconView.setImageResource(iconId); + iconView.setColorFilter(cf); + LinearLayout.LayoutParams iconParams = new LinearLayout.LayoutParams(dpToPx(24), dpToPx(24)); + iconParams.setMarginEnd(dpToPx(16)); + iconView.setLayoutParams(iconParams); + itemLayout.addView(iconView); + + // Text container + LinearLayout textContainer = new LinearLayout(context); + textContainer.setOrientation(LinearLayout.VERTICAL); + TextView titleView = new TextView(context); + titleView.setText(title); + titleView.setTextSize(16); + titleView.setTextColor(ThemeUtils.getForegroundColor()); + textContainer.addView(titleView); + + itemLayout.addView(textContainer); + + return itemLayout; + } + + private static boolean isMoreButton(String enumString) { + return StringUtils.equalsAny( + enumString, + "MORE_VERT", + "MORE_VERT_BOLD" + ); + } + + /** + * Injection point. + */ + public static void setFlyoutMenuObject(Object bottomSheetMenuObject) { + if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) { + return; + } + if (ShortsPlayerState.getCurrent().isClosed()) { + return; + } + if (bottomSheetMenuObject == null) { + return; + } + for (CustomAction customAction : CustomAction.values()) { + flyoutMenuMap.putIfAbsent(customAction, bottomSheetMenuObject); + } + } + + /** + * Injection point. + */ + public static void addFlyoutMenu(Object bottomSheetMenuClass, Object bottomSheetMenuList) { + if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) { + return; + } + if (ShortsPlayerState.getCurrent().isClosed()) { + return; + } + for (CustomAction customAction : CustomAction.values()) { + if (customAction.settings.get()) { + addFlyoutMenu(bottomSheetMenuClass, bottomSheetMenuList, customAction); + } + } + } + + /** + * Rest of the implementation added by patch. + */ + private static void addFlyoutMenu(Object bottomSheetMenuClass, Object bottomSheetMenuList, CustomAction customAction) { + Object bottomSheetMenuObject = flyoutMenuMap.get(customAction); + // These instructions are ignored by patch. + Logger.printInfo(() -> customAction.name() + bottomSheetMenuClass + bottomSheetMenuList + bottomSheetMenuObject); + } + + /** + * Injection point. + */ + public static void onFlyoutMenuCreate(final RecyclerView recyclerView) { + if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) { + return; + } + recyclerView.getViewTreeObserver().addOnDrawListener(() -> { + try { + if (ShortsPlayerState.getCurrent().isClosed()) { + return; + } + contextRef = new WeakReference<>(recyclerView.getContext()); + if (!isShortsFlyoutMenuVisible) { + return; + } + int childCount = recyclerView.getChildCount(); + if (childCount < arrSize + 1) { + return; + } + for (int i = 0; i < arrSize; i++) { + if (recyclerView.getChildAt(childCount - i - 1) instanceof ViewGroup parentViewGroup) { + childCount = recyclerView.getChildCount(); + if (childCount > 3 && parentViewGroup.getChildAt(1) instanceof TextView textView) { + for (CustomAction customAction : CustomAction.values()) { + if (customAction.getLabel().equals(textView.getText().toString())) { + View.OnClickListener onClick = customAction.getOnClickListener(); + View.OnLongClickListener onLongClick = customAction.getOnLongClickListener(); + recyclerViewRef = new WeakReference<>(recyclerView); + parentViewGroup.setOnClickListener(onClick); + if (onLongClick != null) { + parentViewGroup.setOnLongClickListener(onLongClick); + } + } + } + } + } + } + isShortsFlyoutMenuVisible = false; + } catch (Exception ex) { + Logger.printException(() -> "onFlyoutMenuCreate failure", ex); + } + }); + } + + /** + * Injection point. + */ + public static void onLiveHeaderElementsContainerCreate(final View view) { + if (!SHORTS_CUSTOM_ACTIONS_TOOLBAR_ENABLED) { + return; + } + view.getViewTreeObserver().addOnDrawListener(() -> { + try { + if (view instanceof ViewGroup viewGroup) { + setToolbarMenuOnLongClickListener(viewGroup); + } + } catch (Exception ex) { + Logger.printException(() -> "onFlyoutMenuCreate failure", ex); + } + }); + } + + private static void hideFlyoutMenu() { + if (!SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU_ENABLED) { + return; + } + RecyclerView recyclerView = recyclerViewRef.get(); + if (recyclerView == null) { + return; + } + + if (!(Utils.getParentView(recyclerView, 3) instanceof ViewGroup parentView3rd)) { + return; + } + + if (!(parentView3rd.getParent() instanceof ViewGroup parentView4th)) { + return; + } + + // Dismiss View [R.id.touch_outside] is the 1st ChildView of the 4th ParentView. + // This only shows in phone layout. + Utils.clickView(parentView4th.getChildAt(0)); + + // In tablet layout there is no Dismiss View, instead we just hide all two parent views. + parentView3rd.setVisibility(View.GONE); + parentView4th.setVisibility(View.GONE); + } + + public enum CustomAction { + COPY_URL( + Settings.SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, + "yt_outline_link_black_24", + () -> VideoUtils.copyUrl( + VideoUtils.getVideoUrl( + ShortsCustomActionsFilter.getShortsVideoId(), + false + ), + false + ), + () -> VideoUtils.copyUrl( + VideoUtils.getVideoUrl( + ShortsCustomActionsFilter.getShortsVideoId(), + true + ), + true + ) + ), + COPY_URL_WITH_TIMESTAMP( + Settings.SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, + "yt_outline_arrow_time_black_24", + () -> VideoUtils.copyUrl( + VideoUtils.getVideoUrl( + ShortsCustomActionsFilter.getShortsVideoId(), + true + ), + true + ), + () -> VideoUtils.copyUrl( + VideoUtils.getVideoUrl( + ShortsCustomActionsFilter.getShortsVideoId(), + false + ), + false + ) + ), + EXTERNAL_DOWNLOADER( + Settings.SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, + "yt_outline_download_black_24", + () -> VideoUtils.launchVideoExternalDownloader( + ShortsCustomActionsFilter.getShortsVideoId() + ) + ), + OPEN_VIDEO( + Settings.SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, + "yt_outline_youtube_logo_icon_black_24", + () -> VideoUtils.openVideo( + ShortsCustomActionsFilter.getShortsVideoId(), + true + ) + ), + REPEAT_STATE( + Settings.SHORTS_CUSTOM_ACTIONS_REPEAT_STATE, + "yt_outline_arrow_repeat_1_black_24", + () -> VideoUtils.showShortsRepeatDialog(contextRef.get()) + ); + + @NonNull + private final BooleanSetting settings; + + @NonNull + private final Drawable drawable; + + private final int drawableId; + + @NonNull + private final String label; + + @NonNull + private final Runnable onClickAction; + + @Nullable + private final Runnable onLongClickAction; + + CustomAction(@NonNull BooleanSetting settings, + @NonNull String icon, + @NonNull Runnable onClickAction + ) { + this(settings, icon, onClickAction, null); + } + + CustomAction(@NonNull BooleanSetting settings, + @NonNull String icon, + @NonNull Runnable onClickAction, + @Nullable Runnable onLongClickAction + ) { + this.drawable = Objects.requireNonNull(ResourceUtils.getDrawable(icon)); + this.drawableId = ResourceUtils.getDrawableIdentifier(icon); + this.label = getString(settings.key + "_label"); + this.settings = settings; + this.onClickAction = onClickAction; + this.onLongClickAction = onLongClickAction; + } + + @NonNull + public Drawable getDrawable() { + return drawable; + } + + public int getDrawableId() { + return drawableId; + } + + @NonNull + public String getLabel() { + return label; + } + + @NonNull + public Runnable getOnClickAction() { + return onClickAction; + } + + @NonNull + public View.OnClickListener getOnClickListener() { + return v -> { + hideFlyoutMenu(); + onClickAction.run(); + }; + } + + @Nullable + public View.OnLongClickListener getOnLongClickListener() { + if (onLongClickAction == null) { + return null; + } else { + return v -> { + hideFlyoutMenu(); + onLongClickAction.run(); + return true; + }; + } + } + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/ShortsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/ShortsPatch.java new file mode 100644 index 000000000..9125a2138 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/ShortsPatch.java @@ -0,0 +1,214 @@ +package app.revanced.extension.youtube.patches.shorts; + +import static app.revanced.extension.shared.utils.Utils.hideViewUnderCondition; +import static app.revanced.extension.youtube.utils.ExtendedUtils.validateValue; + +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar; + +import java.lang.ref.WeakReference; + +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.ShortsPlayerState; +import app.revanced.extension.youtube.utils.VideoUtils; + +@SuppressWarnings("unused") +public class ShortsPatch { + private static final boolean ENABLE_TIME_STAMP = Settings.ENABLE_TIME_STAMP.get(); + public static final boolean HIDE_SHORTS_NAVIGATION_BAR = Settings.HIDE_SHORTS_NAVIGATION_BAR.get(); + + private static final int META_PANEL_BOTTOM_MARGIN; + private static final double NAVIGATION_BAR_HEIGHT_PERCENTAGE; + + static { + if (HIDE_SHORTS_NAVIGATION_BAR) { + ShortsPlayerState.getOnChange().addObserver((ShortsPlayerState state) -> { + setNavigationBarLayoutParams(state); + return null; + }); + } + final int bottomMargin = validateValue( + Settings.META_PANEL_BOTTOM_MARGIN, + 0, + 64, + "revanced_shorts_meta_panel_bottom_margin_invalid_toast" + ); + + META_PANEL_BOTTOM_MARGIN = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, (float) bottomMargin, Utils.getResources().getDisplayMetrics()); + + final int heightPercentage = validateValue( + Settings.SHORTS_NAVIGATION_BAR_HEIGHT_PERCENTAGE, + 0, + 100, + "revanced_shorts_navigation_bar_height_percentage_invalid_toast" + ); + + NAVIGATION_BAR_HEIGHT_PERCENTAGE = heightPercentage / 100d; + } + + public static boolean disableResumingStartupShortsPlayer() { + return Settings.DISABLE_RESUMING_SHORTS_PLAYER.get(); + } + + public static boolean enableShortsTimeStamp(boolean original) { + return ENABLE_TIME_STAMP || original; + } + + public static int enableShortsTimeStamp(int original) { + return ENABLE_TIME_STAMP ? 10010 : original; + } + + public static void setShortsMetaPanelBottomMargin(View view) { + if (!ENABLE_TIME_STAMP) + return; + + if (!(view.getLayoutParams() instanceof RelativeLayout.LayoutParams lp)) + return; + + lp.setMargins(0, 0, 0, META_PANEL_BOTTOM_MARGIN); + lp.setMarginEnd(ResourceUtils.getDimension("reel_player_right_dyn_bar_width")); + } + + public static void setShortsTimeStampChangeRepeatState(View view) { + if (!ENABLE_TIME_STAMP) + return; + if (!Settings.TIME_STAMP_CHANGE_REPEAT_STATE.get()) + return; + if (view == null) + return; + + view.setLongClickable(true); + view.setOnLongClickListener(view1 -> { + VideoUtils.showShortsRepeatDialog(view1.getContext()); + return true; + }); + } + + public static void hideShortsCommentsButton(View view) { + hideViewUnderCondition(Settings.HIDE_SHORTS_COMMENTS_BUTTON.get(), view); + } + + public static boolean hideShortsDislikeButton() { + return Settings.HIDE_SHORTS_DISLIKE_BUTTON.get(); + } + + public static ViewGroup hideShortsInfoPanel(ViewGroup viewGroup) { + return Settings.HIDE_SHORTS_INFO_PANEL.get() ? null : viewGroup; + } + + public static boolean hideShortsLikeButton() { + return Settings.HIDE_SHORTS_LIKE_BUTTON.get(); + } + + public static boolean hideShortsPaidPromotionLabel() { + return Settings.HIDE_SHORTS_PAID_PROMOTION_LABEL.get(); + } + + public static void hideShortsPaidPromotionLabel(TextView textView) { + hideViewUnderCondition(Settings.HIDE_SHORTS_PAID_PROMOTION_LABEL.get(), textView); + } + + public static void hideShortsRemixButton(View view) { + hideViewUnderCondition(Settings.HIDE_SHORTS_REMIX_BUTTON.get(), view); + } + + public static void hideShortsShareButton(View view) { + hideViewUnderCondition(Settings.HIDE_SHORTS_SHARE_BUTTON.get(), view); + } + + public static boolean hideShortsSoundButton() { + return Settings.HIDE_SHORTS_SOUND_BUTTON.get(); + } + + private static final int zeroPaddingDimenId = + ResourceUtils.getDimenIdentifier("revanced_zero_padding"); + + public static int getShortsSoundButtonDimenId(int dimenId) { + return Settings.HIDE_SHORTS_SOUND_BUTTON.get() + ? zeroPaddingDimenId + : dimenId; + } + + public static int hideShortsSubscribeButton(int original) { + return Settings.HIDE_SHORTS_SUBSCRIBE_BUTTON.get() ? 0 : original; + } + + // YouTube 18.29.38 ~ YouTube 19.28.42 + public static boolean hideShortsPausedHeader() { + return Settings.HIDE_SHORTS_PAUSED_HEADER.get(); + } + + // YouTube 19.29.42 ~ + public static boolean hideShortsPausedHeader(boolean original) { + return Settings.HIDE_SHORTS_PAUSED_HEADER.get() || original; + } + + public static boolean hideShortsToolBar(boolean original) { + return !Settings.HIDE_SHORTS_TOOLBAR.get() && original; + } + + /** + * BottomBarContainer is the parent view of {@link PivotBar}, + * And can be hidden using {@link View#setVisibility} only when it is initialized. + *

+ * If it was not hidden with {@link View#setVisibility} when it was initialized, + * it should be hidden with {@link FrameLayout.LayoutParams}. + *

+ * When Shorts is opened, {@link FrameLayout.LayoutParams} should be changed to 0dp, + * When Shorts is closed, {@link FrameLayout.LayoutParams} should be changed to the original. + */ + private static WeakReference bottomBarContainerRef = new WeakReference<>(null); + + private static FrameLayout.LayoutParams originalLayoutParams; + private static final FrameLayout.LayoutParams zeroLayoutParams = + new FrameLayout.LayoutParams(0, 0); + + public static void setNavigationBar(View view) { + if (!HIDE_SHORTS_NAVIGATION_BAR) { + return; + } + bottomBarContainerRef = new WeakReference<>(view); + if (!(view.getLayoutParams() instanceof FrameLayout.LayoutParams lp)) { + return; + } + if (originalLayoutParams == null) { + originalLayoutParams = lp; + } + } + + public static int setNavigationBarHeight(int original) { + return HIDE_SHORTS_NAVIGATION_BAR + ? (int) Math.round(original * NAVIGATION_BAR_HEIGHT_PERCENTAGE) + : original; + } + + private static void setNavigationBarLayoutParams(@NonNull ShortsPlayerState shortsPlayerState) { + final View navigationBar = bottomBarContainerRef.get(); + if (navigationBar == null) { + return; + } + if (!(navigationBar.getLayoutParams() instanceof FrameLayout.LayoutParams lp)) { + return; + } + navigationBar.setLayoutParams( + shortsPlayerState.isClosed() + ? originalLayoutParams + : zeroLayoutParams + ); + } + + public static boolean restoreShortsOldPlayerLayout() { + return !Settings.RESTORE_SHORTS_OLD_PLAYER_LAYOUT.get(); + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/ShortsRepeatStatePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/ShortsRepeatStatePatch.java new file mode 100644 index 000000000..50933c1f7 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/ShortsRepeatStatePatch.java @@ -0,0 +1,101 @@ +package app.revanced.extension.youtube.patches.shorts; + +import android.app.Activity; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.ExtendedUtils; + +@SuppressWarnings("unused") +public class ShortsRepeatStatePatch { + + public enum ShortsLoopBehavior { + UNKNOWN, + /** + * Repeat the same Short forever! + */ + REPEAT, + /** + * Play once, then advanced to the next Short. + */ + SINGLE_PLAY, + /** + * Pause playback after 1 play. + */ + END_SCREEN; + + static void setYTEnumValue(Enum ytBehavior) { + for (ShortsLoopBehavior rvBehavior : values()) { + if (ytBehavior.name().endsWith(rvBehavior.name())) { + rvBehavior.ytEnumValue = ytBehavior; + + Logger.printDebug(() -> rvBehavior + " set to YT enum: " + ytBehavior.name()); + return; + } + } + + Logger.printException(() -> "Unknown Shorts loop behavior: " + ytBehavior.name()); + } + + /** + * YouTube enum value of the obfuscated enum type. + */ + private Enum ytEnumValue; + } + + private static WeakReference mainActivityRef = new WeakReference<>(null); + + + public static void setMainActivity(Activity activity) { + mainActivityRef = new WeakReference<>(activity); + } + + /** + * @return If the app is currently in background PiP mode. + */ + private static boolean isAppInBackgroundPiPMode() { + Activity activity = mainActivityRef.get(); + return activity != null && activity.isInPictureInPictureMode(); + } + + /** + * Injection point. + */ + public static void setYTShortsRepeatEnum(Enum ytEnum) { + try { + for (Enum ytBehavior : Objects.requireNonNull(ytEnum.getClass().getEnumConstants())) { + ShortsLoopBehavior.setYTEnumValue(ytBehavior); + } + } catch (Exception ex) { + Logger.printException(() -> "setYTShortsRepeatEnum failure", ex); + } + } + + /** + * Injection point. + */ + public static Enum changeShortsRepeatBehavior(Enum original) { + try { + final ShortsLoopBehavior behavior = ExtendedUtils.IS_19_34_OR_GREATER && + isAppInBackgroundPiPMode() + ? Settings.CHANGE_SHORTS_BACKGROUND_REPEAT_STATE.get() + : Settings.CHANGE_SHORTS_REPEAT_STATE.get(); + + if (behavior != ShortsLoopBehavior.UNKNOWN && behavior.ytEnumValue != null) { + Logger.printDebug(() -> behavior.ytEnumValue == original + ? "Changing Shorts repeat behavior from: " + original.name() + " to: " + behavior.ytEnumValue + : "Behavior setting is same as original. Using original: " + original.name() + ); + + return behavior.ytEnumValue; + } + } catch (Exception ex) { + Logger.printException(() -> "changeShortsRepeatState failure", ex); + } + + return original; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spans/SanitizeVideoSubtitleFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spans/SanitizeVideoSubtitleFilter.java new file mode 100644 index 000000000..fc4daf177 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spans/SanitizeVideoSubtitleFilter.java @@ -0,0 +1,36 @@ +package app.revanced.extension.youtube.patches.spans; + +import android.text.SpannableString; + +import app.revanced.extension.shared.patches.spans.Filter; +import app.revanced.extension.shared.patches.spans.SpanType; +import app.revanced.extension.shared.patches.spans.StringFilterGroup; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings({"unused", "ConstantValue", "FieldCanBeLocal"}) +public final class SanitizeVideoSubtitleFilter extends Filter { + + public SanitizeVideoSubtitleFilter() { + addCallbacks( + new StringFilterGroup( + Settings.SANITIZE_VIDEO_SUBTITLE, + "|video_subtitle.eml|" + ) + ); + } + + @Override + public boolean skip(String conversionContext, SpannableString spannableString, Object span, + int start, int end, int flags, boolean isWord, SpanType spanType, StringFilterGroup matchedGroup) { + if (isWord) { + if (spanType == SpanType.IMAGE) { + hideImageSpan(spannableString, start, end, flags); + return super.skip(conversionContext, spannableString, span, start, end, flags, isWord, spanType, matchedGroup); + } else if (spanType == SpanType.CUSTOM_CHARACTER_STYLE) { + hideSpan(spannableString, start, end, flags); + return super.skip(conversionContext, spannableString, span, start, end, flags, isWord, spanType, matchedGroup); + } + } + return false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spans/SearchLinksFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spans/SearchLinksFilter.java new file mode 100644 index 000000000..2e6babc82 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spans/SearchLinksFilter.java @@ -0,0 +1,52 @@ +package app.revanced.extension.youtube.patches.spans; + +import android.text.SpannableString; + +import app.revanced.extension.shared.patches.spans.Filter; +import app.revanced.extension.shared.patches.spans.SpanType; +import app.revanced.extension.shared.patches.spans.StringFilterGroup; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings({"unused", "ConstantValue", "FieldCanBeLocal"}) +public final class SearchLinksFilter extends Filter { + /** + * Located in front of the search icon. + */ + private final String WORD_JOINER_CHARACTER = "\u2060"; + + public SearchLinksFilter() { + addCallbacks( + new StringFilterGroup( + Settings.HIDE_COMMENT_HIGHLIGHTED_SEARCH_LINKS, + "|comment." + ) + ); + } + + /** + * @return Whether the word contains a search icon or not. + */ + private boolean isSearchLinks(SpannableString original, int end) { + String originalString = original.toString(); + int wordJoinerIndex = originalString.indexOf(WORD_JOINER_CHARACTER); + // There may be more than one highlight keyword in the comment. + // Check the index of all highlight keywords. + while (wordJoinerIndex != -1) { + if (end - wordJoinerIndex == 2) return true; + wordJoinerIndex = originalString.indexOf(WORD_JOINER_CHARACTER, wordJoinerIndex + 1); + } + return false; + } + + @Override + public boolean skip(String conversionContext, SpannableString spannableString, Object span, + int start, int end, int flags, boolean isWord, SpanType spanType, StringFilterGroup matchedGroup) { + if (isWord && isSearchLinks(spannableString, end)) { + if (spanType == SpanType.IMAGE) { + hideSpan(spannableString, start, end, flags); + } + return super.skip(conversionContext, spannableString, span, start, end, flags, isWord, spanType, matchedGroup); + } + return false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java new file mode 100644 index 000000000..01b302675 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/swipe/SwipeControlsPatch.java @@ -0,0 +1,48 @@ +package app.revanced.extension.youtube.patches.swipe; + +import android.view.View; + +import java.lang.ref.WeakReference; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings({"unused", "deprecation"}) +public class SwipeControlsPatch { + private static WeakReference fullscreenEngagementOverlayViewRef = new WeakReference<>(null); + + /** + * Injection point. + */ + public static boolean disableHDRAutoBrightness() { + return Settings.DISABLE_HDR_AUTO_BRIGHTNESS.get(); + } + + /** + * Injection point. + */ + public static boolean disableSwipeToSwitchVideo() { + return !Settings.DISABLE_SWIPE_TO_SWITCH_VIDEO.get(); + } + + /** + * Injection point. + */ + public static boolean disableWatchPanelGestures() { + return !Settings.DISABLE_WATCH_PANEL_GESTURES.get(); + } + + /** + * Injection point. + * + * @param fullscreenEngagementOverlayView R.layout.fullscreen_engagement_overlay + */ + public static void setFullscreenEngagementOverlayView(View fullscreenEngagementOverlayView) { + fullscreenEngagementOverlayViewRef = new WeakReference<>(fullscreenEngagementOverlayView); + } + + public static boolean isEngagementOverlayVisible() { + final View engagementOverlayView = fullscreenEngagementOverlayViewRef.get(); + return engagementOverlayView != null && engagementOverlayView.getVisibility() == View.VISIBLE; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/AlwaysRepeatPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/AlwaysRepeatPatch.java new file mode 100644 index 000000000..41b8ea4d9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/AlwaysRepeatPatch.java @@ -0,0 +1,29 @@ +package app.revanced.extension.youtube.patches.utils; + +import static app.revanced.extension.youtube.utils.VideoUtils.pauseMedia; + +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.VideoInformation; + +@SuppressWarnings("unused") +public class AlwaysRepeatPatch extends Utils { + + /** + * Injection point. + * + * @return video is repeated. + */ + public static boolean alwaysRepeat() { + return alwaysRepeatEnabled() && VideoInformation.overrideVideoTime(0); + } + + public static boolean alwaysRepeatEnabled() { + final boolean alwaysRepeat = Settings.ALWAYS_REPEAT.get(); + final boolean alwaysRepeatPause = Settings.ALWAYS_REPEAT_PAUSE.get(); + + if (alwaysRepeat && alwaysRepeatPause) pauseMedia(); + return alwaysRepeat; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/BottomSheetHookPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/BottomSheetHookPatch.java new file mode 100644 index 000000000..b7c5c1c08 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/BottomSheetHookPatch.java @@ -0,0 +1,21 @@ +package app.revanced.extension.youtube.patches.utils; + +import app.revanced.extension.youtube.shared.BottomSheetState; + +@SuppressWarnings("unused") +public class BottomSheetHookPatch { + /** + * Injection point. + */ + public static void onAttachedToWindow() { + BottomSheetState.set(BottomSheetState.OPEN); + } + + /** + * Injection point. + */ + public static void onDetachedFromWindow() { + BottomSheetState.set(BottomSheetState.CLOSED); + } +} + diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/CastButtonPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/CastButtonPatch.java new file mode 100644 index 000000000..bd206dcc9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/CastButtonPatch.java @@ -0,0 +1,25 @@ +package app.revanced.extension.youtube.patches.utils; + +import android.view.View; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class CastButtonPatch { + + /** + * The [Hide cast button] setting is separated into the [Hide cast button in player] setting and the [Hide cast button in toolbar] setting. + * Always hide the cast button when both settings are true. + *

+ * These two settings belong to different patches, and since the default value for this setting is true, + * it is essential to ensure that each patch is included to ensure independent operation. + */ + public static int hideCastButton(int original) { + return Settings.HIDE_TOOLBAR_CAST_BUTTON.get() + && PatchStatus.ToolBarComponents() + && Settings.HIDE_PLAYER_CAST_BUTTON.get() + && PatchStatus.PlayerButtons() + ? View.GONE + : original; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/DoubleBackToClosePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/DoubleBackToClosePatch.java new file mode 100644 index 000000000..fdf4ba163 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/DoubleBackToClosePatch.java @@ -0,0 +1,66 @@ +package app.revanced.extension.youtube.patches.utils; + +import android.app.Activity; + +import androidx.annotation.NonNull; + +import app.revanced.extension.youtube.settings.Settings; + +/** + * @noinspection ALL + */ +public class DoubleBackToClosePatch { + /** + * Time between two back button presses + */ + private static final long PRESSED_TIMEOUT_MILLISECONDS = Settings.DOUBLE_BACK_TO_CLOSE_TIMEOUT.get(); + + /** + * Last time back button was pressed + */ + private static long lastTimeBackPressed = 0; + + /** + * State whether scroll position reaches the top + */ + private static boolean isScrollTop = false; + + /** + * Detect event when back button is pressed + * + * @param activity is used when closing the app + */ + public static void closeActivityOnBackPressed(@NonNull Activity activity) { + // Check scroll position reaches the top in home feed + if (!isScrollTop) + return; + + final long currentTime = System.currentTimeMillis(); + + // If the time between two back button presses does not reach PRESSED_TIMEOUT_MILLISECONDS, + // set lastTimeBackPressed to the current time. + if (currentTime - lastTimeBackPressed < PRESSED_TIMEOUT_MILLISECONDS || + PRESSED_TIMEOUT_MILLISECONDS == 0) + activity.finish(); + else + lastTimeBackPressed = currentTime; + } + + /** + * Detect event when ScrollView is created by RecyclerView + *

+ * start of ScrollView + */ + public static void onStartScrollView() { + isScrollTop = false; + } + + /** + * Detect event when the scroll position reaches the top by the back button + *

+ * stop of ScrollView + */ + public static void onStopScrollView() { + isScrollTop = true; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/DrawableColorPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/DrawableColorPatch.java new file mode 100644 index 000000000..772dcd3e6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/DrawableColorPatch.java @@ -0,0 +1,50 @@ +package app.revanced.extension.youtube.patches.utils; + +import app.revanced.extension.shared.utils.ResourceUtils; + +@SuppressWarnings("unused") +public class DrawableColorPatch { + private static final int[] WHITE_VALUES = { + -1, // comments chip background + -394759, // music related results panel background + -83886081 // video chapters list background + }; + + private static final int[] DARK_VALUES = { + -14145496, // drawer content view background + -14606047, // comments chip background + -15198184, // music related results panel background + -15790321, // comments chip background (new layout) + -98492127 // video chapters list background + }; + + // background colors + private static int whiteColor = 0; + private static int blackColor = 0; + + public static int getLithoColor(int originalValue) { + if (anyEquals(originalValue, DARK_VALUES)) { + return getBlackColor(); + } else if (anyEquals(originalValue, WHITE_VALUES)) { + return getWhiteColor(); + } + return originalValue; + } + + private static int getBlackColor() { + if (blackColor == 0) blackColor = ResourceUtils.getColor("yt_black1"); + return blackColor; + } + + private static int getWhiteColor() { + if (whiteColor == 0) whiteColor = ResourceUtils.getColor("yt_white1"); + return whiteColor; + } + + private static boolean anyEquals(int value, int... of) { + for (int v : of) if (value == v) return true; + return false; + } +} + + diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/InitializationPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/InitializationPatch.java new file mode 100644 index 000000000..680ff105a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/InitializationPatch.java @@ -0,0 +1,36 @@ +package app.revanced.extension.youtube.patches.utils; + +import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.showRestartDialog; +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.runOnMainThreadDelayed; + +import android.app.Activity; + +import androidx.annotation.NonNull; + +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.youtube.utils.ExtendedUtils; + +@SuppressWarnings("unused") +public class InitializationPatch { + private static final BooleanSetting SETTINGS_INITIALIZED = BaseSettings.SETTINGS_INITIALIZED; + + /** + * Some layouts that depend on litho do not load when the app is first installed. + * (Also reproduced on unPatched YouTube) + *

+ * To fix this, show the restart dialog when the app is installed for the first time. + */ + public static void onCreate(@NonNull Activity mActivity) { + if (SETTINGS_INITIALIZED.get()) { + return; + } + runOnMainThreadDelayed(() -> showRestartDialog(mActivity, str("revanced_extended_restart_first_run"), 3500), 500); + runOnMainThreadDelayed(() -> SETTINGS_INITIALIZED.save(true), 1000); + } + + public static void setExtendedUtils(@NonNull Activity mActivity) { + ExtendedUtils.setPlayerFlyoutMenuAdditionalSettings(); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/LockModeStateHookPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/LockModeStateHookPatch.java new file mode 100644 index 000000000..96baec1a1 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/LockModeStateHookPatch.java @@ -0,0 +1,18 @@ +package app.revanced.extension.youtube.patches.utils; + +import androidx.annotation.Nullable; + +import app.revanced.extension.youtube.shared.LockModeState; + +@SuppressWarnings("unused") +public class LockModeStateHookPatch { + /** + * Injection point. + */ + public static void setLockModeState(@Nullable Enum lockModeState) { + if (lockModeState == null) return; + + LockModeState.setFromString(lockModeState.name()); + } +} + diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/LottieAnimationViewPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/LottieAnimationViewPatch.java new file mode 100644 index 000000000..68323f843 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/LottieAnimationViewPatch.java @@ -0,0 +1,25 @@ +package app.revanced.extension.youtube.patches.utils; + +import com.airbnb.lottie.LottieAnimationView; + +import app.revanced.extension.shared.utils.Logger; + +public class LottieAnimationViewPatch { + + public static void setLottieAnimationRawResources(LottieAnimationView lottieAnimationView, int rawRes) { + if (lottieAnimationView == null) { + Logger.printDebug(() -> "View is null"); + return; + } + if (rawRes == 0) { + Logger.printDebug(() -> "Resource is not found"); + return; + } + setAnimation(lottieAnimationView, rawRes); + } + + @SuppressWarnings("unused") + private static void setAnimation(LottieAnimationView lottieAnimationView, int rawRes) { + // Rest of the implementation added by patch. + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PatchStatus.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PatchStatus.java new file mode 100644 index 000000000..309415c0d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PatchStatus.java @@ -0,0 +1,50 @@ +package app.revanced.extension.youtube.patches.utils; + +public class PatchStatus { + + public static boolean ImageSearchButton() { + // Replace this with true if the Hide image search buttons patch succeeds + return false; + } + + public static boolean MinimalHeader() { + // Replace this with true If the Custom header patch succeeds and the patch option was `youtube_minimal_header` + return false; + } + + public static boolean PlayerButtons() { + // Replace this with true if the Hide player buttons patch succeeds + return false; + } + + public static boolean QuickActions() { + // Replace this with true if the Fullscreen components patch succeeds + return false; + } + + public static boolean RememberPlaybackSpeed() { + // Replace this with true if the Video playback patch succeeds + return false; + } + + public static boolean SponsorBlock() { + // Replace this with true if the SponsorBlock patch succeeds + return false; + } + + public static boolean ToolBarComponents() { + // Replace this with true if the Toolbar components patch succeeds + return false; + } + + // Modified by a patch. Do not touch. + public static String RVXMusicPackageName() { + return "com.google.android.apps.youtube.music"; + } + + // Modified by a patch. Do not touch. + public static boolean OldSeekbarThumbnailsDefaultBoolean() { + return false; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlaybackSpeedWhilePlayingPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlaybackSpeedWhilePlayingPatch.java new file mode 100644 index 000000000..257c92644 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlaybackSpeedWhilePlayingPatch.java @@ -0,0 +1,26 @@ +package app.revanced.extension.youtube.patches.utils; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.shared.PlayerType; + +@SuppressWarnings("unused") +public class PlaybackSpeedWhilePlayingPatch { + private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f; + + public static boolean playbackSpeedChanged(float playbackSpeed) { + if (playbackSpeed == DEFAULT_YOUTUBE_PLAYBACK_SPEED && + PlayerType.getCurrent().isMaximizedOrFullscreen()) { + + Logger.printDebug(() -> "Even though playback has already started and the user has not changed the playback speed, " + + "the app attempts to change the playback speed to 1.0x." + + "\nIgnore changing playback speed, as it is invalid request."); + + return true; + } + + return false; + } + +} + + diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerControlsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerControlsPatch.java new file mode 100644 index 000000000..5a6e56f6a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerControlsPatch.java @@ -0,0 +1,129 @@ +package app.revanced.extension.youtube.patches.utils; + +import static app.revanced.extension.shared.utils.ResourceUtils.getIdIdentifier; + +import android.view.View; + +import androidx.annotation.NonNull; + +import java.lang.ref.WeakReference; + +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.shared.PlayerControlsVisibility; + +/** + * @noinspection ALL + */ +public class PlayerControlsPatch { + private static WeakReference playerOverflowButtonViewRef = new WeakReference<>(null); + private static final int playerOverflowButtonId = + getIdIdentifier("player_overflow_button"); + + /** + * Injection point. + */ + public static void initializeBottomControlButton(View bottomControlsViewGroup) { + // AlwaysRepeat.initialize(bottomControlsViewGroup); + // CopyVideoUrl.initialize(bottomControlsViewGroup); + // CopyVideoUrlTimestamp.initialize(bottomControlsViewGroup); + // MuteVolume.initialize(bottomControlsViewGroup); + // ExternalDownload.initialize(bottomControlsViewGroup); + // SpeedDialog.initialize(bottomControlsViewGroup); + // TimeOrderedPlaylist.initialize(bottomControlsViewGroup); + // Whitelists.initialize(bottomControlsViewGroup); + } + + /** + * Injection point. + */ + public static void initializeTopControlButton(View youtubeControlsLayout) { + // CreateSegmentButtonController.initialize(youtubeControlsLayout); + // VotingButtonController.initialize(youtubeControlsLayout); + } + + /** + * Injection point. + * Legacy method. + *

+ * Player overflow button view does not attach to windows immediately after cold start. + * Player overflow button view is not attached to the windows until the user touches the player at least once, and the overlay buttons are hidden until then. + * To prevent this, uses the legacy method to show the overlay button until the player overflow button view is attached to the windows. + */ + public static void changeVisibility(boolean showing) { + if (playerOverflowButtonViewRef.get() != null) { + return; + } + changeVisibility(showing, false); + } + + private static void changeVisibility(boolean showing, boolean animation) { + // AlwaysRepeat.changeVisibility(showing, animation); + // CopyVideoUrl.changeVisibility(showing, animation); + // CopyVideoUrlTimestamp.changeVisibility(showing, animation); + // MuteVolume.changeVisibility(showing, animation); + // ExternalDownload.changeVisibility(showing, animation); + // SpeedDialog.changeVisibility(showing, animation); + // TimeOrderedPlaylist.changeVisibility(showing, animation); + // Whitelists.changeVisibility(showing, animation); + + // CreateSegmentButtonController.changeVisibility(showing, animation); + // VotingButtonController.changeVisibility(showing, animation); + } + + /** + * Injection point. + * New method. + *

+ * Show or hide the overlay button when the player overflow button view is visible and hidden, respectively. + *

+ * Inject the current view into {@link PlayerControlsPatch#playerOverflowButtonView} to check that the player overflow button view is attached to the window. + * From this point on, the legacy method is deprecated. + */ + public static void changeVisibility(boolean showing, boolean animation, @NonNull View view) { + if (view.getId() != playerOverflowButtonId) { + return; + } + if (playerOverflowButtonViewRef.get() == null) { + Utils.runOnMainThreadDelayed(() -> playerOverflowButtonViewRef = new WeakReference<>(view), 1400); + } + changeVisibility(showing, animation); + } + + /** + * Injection point. + *

+ * Called whenever a motion event occurs on the player controller. + *

+ * When the user touches the player overlay (motion event occurs), the player overlay disappears immediately. + * In this case, the overlay buttons should also disappear immediately. + *

+ * In other words, this method detects when the player overlay disappears immediately upon the user's touch, + * and quickly fades out all overlay buttons. + */ + public static void changeVisibilityNegatedImmediate() { + if (PlayerControlsVisibility.getCurrent() == PlayerControlsVisibility.PLAYER_CONTROLS_VISIBILITY_HIDDEN) { + changeVisibilityNegatedImmediately(); + } + } + + private static void changeVisibilityNegatedImmediately() { + // AlwaysRepeat.changeVisibilityNegatedImmediate(); + // CopyVideoUrl.changeVisibilityNegatedImmediate(); + // CopyVideoUrlTimestamp.changeVisibilityNegatedImmediate(); + // MuteVolume.changeVisibilityNegatedImmediate(); + // ExternalDownload.changeVisibilityNegatedImmediate(); + // SpeedDialog.changeVisibilityNegatedImmediate(); + // TimeOrderedPlaylist.changeVisibilityNegatedImmediate(); + // Whitelists.changeVisibilityNegatedImmediate(); + + // CreateSegmentButtonController.changeVisibilityNegatedImmediate(); + // VotingButtonController.changeVisibilityNegatedImmediate(); + } + + /** + * Injection point. + */ + public static String getPlayerTopControlsLayoutResourceName(String original) { + return "default"; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerControlsVisibilityHookPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerControlsVisibilityHookPatch.java new file mode 100644 index 000000000..b71059449 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerControlsVisibilityHookPatch.java @@ -0,0 +1,18 @@ +package app.revanced.extension.youtube.patches.utils; + +import androidx.annotation.Nullable; + +import app.revanced.extension.youtube.shared.PlayerControlsVisibility; + +@SuppressWarnings("unused") +public class PlayerControlsVisibilityHookPatch { + /** + * Injection point. + */ + public static void setPlayerControlsVisibility(@Nullable Enum youTubePlayerControlsVisibility) { + if (youTubePlayerControlsVisibility == null) return; + + PlayerControlsVisibility.setFromString(youTubePlayerControlsVisibility.name()); + } +} + diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerTypeHookPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerTypeHookPatch.java new file mode 100644 index 000000000..38d479512 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerTypeHookPatch.java @@ -0,0 +1,53 @@ +package app.revanced.extension.youtube.patches.utils; + +import android.view.View; + +import androidx.annotation.Nullable; + +import app.revanced.extension.youtube.shared.PlayerType; +import app.revanced.extension.youtube.shared.ShortsPlayerState; +import app.revanced.extension.youtube.shared.VideoState; + +@SuppressWarnings("unused") +public class PlayerTypeHookPatch { + /** + * Injection point. + */ + public static void setPlayerType(@Nullable Enum youTubePlayerType) { + if (youTubePlayerType == null) return; + + PlayerType.setFromString(youTubePlayerType.name()); + } + + /** + * Injection point. + */ + public static void setVideoState(@Nullable Enum youTubeVideoState) { + if (youTubeVideoState == null) return; + + VideoState.setFromString(youTubeVideoState.name()); + } + + /** + * Injection point. + *

+ * Add a listener to the shorts player overlay View. + * Triggered when a shorts player is attached or detached to Windows. + * + * @param view shorts player overlay (R.id.reel_watch_player). + */ + public static void onShortsCreate(View view) { + view.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(@Nullable View v) { + ShortsPlayerState.set(ShortsPlayerState.OPEN); + } + + @Override + public void onViewDetachedFromWindow(@Nullable View v) { + ShortsPlayerState.set(ShortsPlayerState.CLOSED); + } + }); + } +} + diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ProgressBarDrawable.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ProgressBarDrawable.java new file mode 100644 index 000000000..b3d4543cd --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ProgressBarDrawable.java @@ -0,0 +1,44 @@ +package app.revanced.extension.youtube.patches.utils; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import app.revanced.extension.youtube.patches.player.SeekbarColorPatch; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class ProgressBarDrawable extends Drawable { + + private final Paint paint = new Paint(); + + @Override + public void draw(@NonNull Canvas canvas) { + if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) { + return; + } + paint.setColor(SeekbarColorPatch.getSeekbarColor()); + canvas.drawRect(getBounds(), paint); + } + + @Override + public void setAlpha(int alpha) { + paint.setAlpha(alpha); + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + paint.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ReturnYouTubeChannelNamePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ReturnYouTubeChannelNamePatch.java new file mode 100644 index 000000000..fca94b6b0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ReturnYouTubeChannelNamePatch.java @@ -0,0 +1,130 @@ +package app.revanced.extension.youtube.patches.utils; + +import androidx.annotation.NonNull; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class ReturnYouTubeChannelNamePatch { + + private static final boolean REPLACE_CHANNEL_HANDLE = Settings.REPLACE_CHANNEL_HANDLE.get(); + /** + * The last character of some handles is an official channel certification mark. + * This was in the form of nonBreakSpaceCharacter before SpannableString was made. + */ + private static final String NON_BREAK_SPACE_CHARACTER = "\u00A0"; + private volatile static String channelName = ""; + + /** + * Key: channelId, Value: channelName. + */ + private static final Map channelIdMap = Collections.synchronizedMap( + new LinkedHashMap<>(20) { + private static final int CACHE_LIMIT = 10; + + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit. + } + }); + + /** + * Key: handle, Value: channelName. + */ + private static final Map channelHandleMap = Collections.synchronizedMap( + new LinkedHashMap<>(20) { + private static final int CACHE_LIMIT = 10; + + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit. + } + }); + + /** + * This method is only invoked on Shorts and is updated whenever the user swipes up or down on the Shorts. + */ + public static void newShortsVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, + @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, + final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { + if (!REPLACE_CHANNEL_HANDLE) { + return; + } + if (channelIdMap.get(newlyLoadedChannelId) != null) { + return; + } + if (channelIdMap.put(newlyLoadedChannelId, newlyLoadedChannelName) == null) { + channelName = newlyLoadedChannelName; + Logger.printDebug(() -> "New video started, ChannelId " + newlyLoadedChannelId + ", Channel Name: " + newlyLoadedChannelName); + } + } + + /** + * Injection point. + */ + public static CharSequence onCharSequenceLoaded(@NonNull Object conversionContext, + @NonNull CharSequence charSequence) { + try { + if (!REPLACE_CHANNEL_HANDLE) { + return charSequence; + } + final String conversionContextString = conversionContext.toString(); + if (!conversionContextString.contains("|reel_channel_bar_inner.eml|")) { + return charSequence; + } + final String originalString = charSequence.toString(); + if (!originalString.startsWith("@")) { + return charSequence; + } + return getChannelName(originalString); + } catch (Exception ex) { + Logger.printException(() -> "onCharSequenceLoaded failed", ex); + } + return charSequence; + } + + private static CharSequence getChannelName(@NonNull String handle) { + final String trimmedHandle = handle.replaceAll(NON_BREAK_SPACE_CHARACTER, ""); + + String cachedChannelName = channelHandleMap.get(trimmedHandle); + if (cachedChannelName == null) { + if (!channelName.isEmpty() && channelHandleMap.put(handle, channelName) == null) { + Logger.printDebug(() -> "Set Handle from last fetched Channel Name, Handle: " + handle + ", Channel Name: " + channelName); + cachedChannelName = channelName; + } else { + Logger.printDebug(() -> "Channel handle is not found: " + trimmedHandle); + return handle; + } + } + + if (handle.contains(NON_BREAK_SPACE_CHARACTER)) { + cachedChannelName += NON_BREAK_SPACE_CHARACTER; + } + String replacedChannelName = cachedChannelName; + Logger.printDebug(() -> "Replace Handle " + handle + " to " + replacedChannelName); + return replacedChannelName; + } + + public synchronized static void setLastShortsChannelId(@NonNull String handle, @NonNull String channelId) { + try { + if (channelHandleMap.get(handle) != null) { + return; + } + final String channelName = channelIdMap.get(channelId); + if (channelName == null) { + Logger.printDebug(() -> "Channel name is not found!"); + return; + } + if (channelHandleMap.put(handle, channelName) == null) { + Logger.printDebug(() -> "Set Handle from Shorts, Handle: " + handle + ", Channel Name: " + channelName); + } + } catch (Exception ex) { + Logger.printException(() -> "setLastShortsChannelId failure ", ex); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ReturnYouTubeDislikePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ReturnYouTubeDislikePatch.java new file mode 100644 index 000000000..8e29174e5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ReturnYouTubeDislikePatch.java @@ -0,0 +1,690 @@ +package app.revanced.extension.youtube.patches.utils; + +import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote; +import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan; + +import android.graphics.Rect; +import android.graphics.drawable.ShapeDrawable; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import app.revanced.extension.shared.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilterPatch; +import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; +import app.revanced.extension.youtube.shared.VideoInformation; + +/** + * Handles all interaction of UI patch components. + *

+ * Known limitation: + * The implementation of Shorts litho requires blocking the loading the first Short until RYD has completed. + * This is because it modifies the dislikes text synchronously, and if the RYD fetch has + * not completed yet then the UI will be temporarily frozen. + *

+ * A (yet to be implemented) solution that fixes this problem. Any one of: + * - Modify patch to hook onto the Shorts Litho TextView, and update the dislikes text asynchronously. + * - Find a way to force Litho to rebuild it's component tree, + * and use that hook to force the shorts dislikes to update after the fetch is completed. + * - Hook into the dislikes button image view, and replace the dislikes thumb down image with a + * generated image of the number of dislikes, then update the image asynchronously. This Could + * also be used for the regular video player to give a better UI layout and completely remove + * the need for the Rolling Number patches. + */ +@SuppressWarnings("unused") +public class ReturnYouTubeDislikePatch { + + public static final boolean IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER = + isSpoofingToLessThan("18.34.00"); + + /** + * RYD data for the current video on screen. + */ + @Nullable + private static volatile ReturnYouTubeDislike currentVideoData; + + /** + * The last litho based Shorts loaded. + * May be the same value as {@link #currentVideoData}, but usually is the next short to swipe to. + */ + @Nullable + private static volatile ReturnYouTubeDislike lastLithoShortsVideoData; + + /** + * Because the litho Shorts spans are created after {@link ReturnYouTubeDislikeFilterPatch} + * detects the video ids, after the user votes the litho will update + * but {@link #lastLithoShortsVideoData} is not the correct data to use. + * If this is true, then instead use {@link #currentVideoData}. + */ + private static volatile boolean lithoShortsShouldUseCurrentData; + + /** + * Last video id prefetched. Field is to prevent prefetching the same video id multiple times in a row. + */ + @Nullable + private static volatile String lastPrefetchedVideoId; + + public static void onRYDStatusChange() { + ReturnYouTubeDislikeApi.resetRateLimits(); + // Must remove all values to protect against using stale data + // if the user enables RYD while a video is on screen. + clearData(); + } + + private static void clearData() { + currentVideoData = null; + lastLithoShortsVideoData = null; + lithoShortsShouldUseCurrentData = false; + // Rolling number text should not be cleared, + // as it's used if incognito Short is opened/closed + // while a regular video is on screen. + } + + // + // Litho player for both regular videos and Shorts. + // + + /** + * Injection point. + *

+ * For Litho segmented buttons and Litho Shorts player. + */ + @NonNull + public static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, + @NonNull CharSequence original) { + return onLithoTextLoaded(conversionContext, original, false); + } + + /** + * Injection point. + *

+ * Called when a litho text component is initially created, + * and also when a Span is later reused again (such as scrolling off/on screen). + *

+ * This method is sometimes called on the main thread, but it usually is called _off_ the main thread. + * This method can be called multiple times for the same UI element (including after dislikes was added). + * + * @param original Original char sequence was created or reused by Litho. + * @param isRollingNumber If the span is for a Rolling Number. + * @return The original char sequence (if nothing should change), or a replacement char sequence that contains dislikes. + */ + @NonNull + private static CharSequence onLithoTextLoaded(@NonNull Object conversionContext, + @NonNull CharSequence original, + boolean isRollingNumber) { + try { + if (!Settings.RYD_ENABLED.get()) { + return original; + } + + String conversionContextString = conversionContext.toString(); + + if (isRollingNumber && !conversionContextString.contains("video_action_bar.eml")) { + return original; + } + + if (conversionContextString.contains("segmented_like_dislike_button.eml")) { + // Regular video. + ReturnYouTubeDislike videoData = currentVideoData; + if (videoData == null) { + return original; // User enabled RYD while a video was on screen. + } + if (!(original instanceof Spanned)) { + original = new SpannableString(original); + } + return videoData.getDislikesSpanForRegularVideo((Spanned) original, + true, isRollingNumber); + } + + if (isRollingNumber) { + return original; // No need to check for Shorts in the context. + } + + if (conversionContextString.contains("|shorts_dislike_button.eml")) { + return getShortsSpan(original, true); + } + + if (conversionContextString.contains("|shorts_like_button.eml") + && !Utils.containsNumber(original)) { + Logger.printDebug(() -> "Replacing hidden likes count"); + return getShortsSpan(original, false); + } + } catch (Exception ex) { + Logger.printException(() -> "onLithoTextLoaded failure", ex); + } + return original; + } + + // + // Litho Shorts player in the incognito mode / live stream. + // + + /** + * Injection point. + *

+ * This method is used in the following situations. + *

+ * 1. When the dislike counts are fetched in the Incognito mode. + * 2. When the dislike counts are fetched in the live stream. + * + * @param original Original span that was created or reused by Litho. + * @return The original span (if nothing should change), or a replacement span that contains dislikes. + */ + public static CharSequence onCharSequenceLoaded(@NonNull Object conversionContext, + @NonNull CharSequence original) { + try { + String conversionContextString = conversionContext.toString(); + if (!Settings.RYD_ENABLED.get()) { + return original; + } + if (!Settings.RYD_SHORTS.get()) { + return original; + } + + final boolean fetchDislikeLiveStream = + conversionContextString.contains("immersive_live_video_action_bar.eml") + && conversionContextString.contains("|dislike_button.eml|"); + + if (!fetchDislikeLiveStream) { + return original; + } + + ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(ReturnYouTubeDislikeFilterPatch.getShortsVideoId()); + videoData.setVideoIdIsShort(true); + lastLithoShortsVideoData = videoData; + lithoShortsShouldUseCurrentData = false; + + return videoData.getDislikeSpanForShort(SHORTS_LOADING_SPAN); + } catch (Exception ex) { + Logger.printException(() -> "onCharSequenceLoaded failure", ex); + } + return original; + } + + + private static CharSequence getShortsSpan(@NonNull CharSequence original, boolean isDislikesSpan) { + // Litho Shorts player. + if (!Settings.RYD_SHORTS.get() || (isDislikesSpan && Settings.HIDE_SHORTS_DISLIKE_BUTTON.get()) + || (!isDislikesSpan && Settings.HIDE_SHORTS_LIKE_BUTTON.get())) { + return original; + } + + ReturnYouTubeDislike videoData = lastLithoShortsVideoData; + if (videoData == null) { + // The Shorts litho video id filter did not detect the video id. + // This is normal in incognito mode, but otherwise is abnormal. + Logger.printDebug(() -> "Cannot modify Shorts litho span, data is null"); + return original; + } + + // Use the correct dislikes data after voting. + if (lithoShortsShouldUseCurrentData) { + if (isDislikesSpan) { + lithoShortsShouldUseCurrentData = false; + } + videoData = currentVideoData; + if (videoData == null) { + Logger.printException(() -> "currentVideoData is null"); // Should never happen + return original; + } + Logger.printDebug(() -> "Using current video data for litho span"); + } + + return isDislikesSpan + ? videoData.getDislikeSpanForShort((Spanned) original) + : videoData.getLikeSpanForShort((Spanned) original); + } + + // + // Rolling Number + // + + /** + * Current regular video rolling number text, if rolling number is in use. + * This is saved to a field as it's used in every draw() call. + */ + @Nullable + private static volatile CharSequence rollingNumberSpan; + + /** + * Injection point. + */ + public static String onRollingNumberLoaded(@NonNull Object conversionContext, + @NonNull String original) { + try { + CharSequence replacement = onLithoTextLoaded(conversionContext, original, true); + + String replacementString = replacement.toString(); + if (!replacementString.equals(original)) { + rollingNumberSpan = replacement; + return replacementString; + } // Else, the text was not a likes count but instead the view count or something else. + } catch (Exception ex) { + Logger.printException(() -> "onRollingNumberLoaded failure", ex); + } + return original; + } + + /** + * Injection point. + *

+ * Called for all usage of Rolling Number. + * Modifies the measured String text width to include the left separator and padding, if needed. + */ + public static float onRollingNumberMeasured(String text, float measuredTextWidth) { + try { + if (Settings.RYD_ENABLED.get()) { + if (ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(text)) { + // +1 pixel is needed for some foreign languages that measure + // the text different from what is used for layout (Greek in particular). + // Probably a bug in Android, but who knows. + // Single line mode is also used as an additional fix for this issue. + if (Settings.RYD_COMPACT_LAYOUT.get()) { + return measuredTextWidth + 1; + } + + return measuredTextWidth + 1 + + ReturnYouTubeDislike.leftSeparatorBounds.right + + ReturnYouTubeDislike.leftSeparatorShapePaddingPixels; + } + } + } catch (Exception ex) { + Logger.printException(() -> "onRollingNumberMeasured failure", ex); + } + + return measuredTextWidth; + } + + /** + * Add Rolling Number text view modifications. + */ + private static void addRollingNumberPatchChanges(TextView view) { + // YouTube Rolling Numbers do not use compound drawables or drawable padding. + if (view.getCompoundDrawablePadding() == 0) { + Logger.printDebug(() -> "Adding rolling number TextView changes"); + view.setCompoundDrawablePadding(ReturnYouTubeDislike.leftSeparatorShapePaddingPixels); + ShapeDrawable separator = ReturnYouTubeDislike.getLeftSeparatorDrawable(); + if (Utils.isRightToLeftTextLayout()) { + view.setCompoundDrawables(null, null, separator, null); + } else { + view.setCompoundDrawables(separator, null, null, null); + } + + // Disliking can cause the span to grow in size, which is ok and is laid out correctly, + // but if the user then removes their dislike the layout will not adjust to the new shorter width. + // Use a center alignment to take up any extra space. + view.setTextAlignment(View.TEXT_ALIGNMENT_CENTER); + + // Single line mode does not clip words if the span is larger than the view bounds. + // The styled span applied to the view should always have the same bounds, + // but use this feature just in case the measurements are somehow off by a few pixels. + view.setSingleLine(true); + } + } + + /** + * Remove Rolling Number text view modifications made by this patch. + * Required as it appears text views can be reused for other rolling numbers (view count, upload time, etc). + */ + private static void removeRollingNumberPatchChanges(TextView view) { + if (view.getCompoundDrawablePadding() != 0) { + Logger.printDebug(() -> "Removing rolling number TextView changes"); + view.setCompoundDrawablePadding(0); + view.setCompoundDrawables(null, null, null, null); + view.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY); // Default alignment + view.setSingleLine(false); + } + } + + /** + * Injection point. + */ + public static CharSequence updateRollingNumber(TextView view, CharSequence original) { + try { + if (!Settings.RYD_ENABLED.get()) { + removeRollingNumberPatchChanges(view); + return original; + } + final boolean isDescriptionPanel = view.getParent() instanceof ViewGroup viewGroupParent + && viewGroupParent.getChildCount() < 2; + // Called for all instances of RollingNumber, so must check if text is for a dislikes. + // Text will already have the correct content but it's missing the drawable separators. + if (!ReturnYouTubeDislike.isPreviouslyCreatedSegmentedSpan(original.toString()) || isDescriptionPanel) { + // The text is the video view count, upload time, or some other text. + removeRollingNumberPatchChanges(view); + return original; + } + + CharSequence replacement = rollingNumberSpan; + if (replacement == null) { + // User enabled RYD while a video was open, + // or user opened/closed a Short while a regular video was opened. + Logger.printDebug(() -> "Cannot update rolling number (field is null)"); + removeRollingNumberPatchChanges(view); + return original; + } + + if (Settings.RYD_COMPACT_LAYOUT.get()) { + removeRollingNumberPatchChanges(view); + } else { + addRollingNumberPatchChanges(view); + } + + // Remove any padding set by Rolling Number. + view.setPadding(0, 0, 0, 0); + + // When displaying dislikes, the rolling animation is not visually correct + // and the dislikes always animate (even though the dislike count has not changed). + // The animation is caused by an image span attached to the span, + // and using only the modified segmented span prevents the animation from showing. + return replacement; + } catch (Exception ex) { + Logger.printException(() -> "updateRollingNumber failure", ex); + return original; + } + } + + // + // Non litho Shorts player. + // + + /** + * Replacement text to use for "Dislikes" while RYD is fetching. + */ + private static final Spannable SHORTS_LOADING_SPAN = new SpannableString("-"); + + /** + * Dislikes TextViews used by Shorts. + *

+ * Multiple TextViews are loaded at once (for the prior and next videos to swipe to). + * Keep track of all of them, and later pick out the correct one based on their on screen position. + */ + private static final List> shortsTextViewRefs = new ArrayList<>(); + + private static void clearRemovedShortsTextViews() { + shortsTextViewRefs.removeIf(ref -> ref.get() == null); + } + + /** + * Injection point. Called when a Shorts dislike is updated. Always on main thread. + * Handles update asynchronously, otherwise Shorts video will be frozen while the UI thread is blocked. + * + * @return if RYD is enabled and the TextView was updated. + */ + public static boolean setShortsDislikes(@NonNull View likeDislikeView) { + try { + if (!Settings.RYD_ENABLED.get()) { + return false; + } + if (!Settings.RYD_SHORTS.get() || Settings.HIDE_SHORTS_DISLIKE_BUTTON.get()) { + // Must clear the data here, in case a new video was loaded while PlayerType + // suggested the video was not a short (can happen when spoofing to an old app version). + clearData(); + return false; + } + Logger.printDebug(() -> "setShortsDislikes"); + + TextView textView = (TextView) likeDislikeView; + textView.setText(SHORTS_LOADING_SPAN); // Change 'Dislike' text to the loading text. + shortsTextViewRefs.add(new WeakReference<>(textView)); + + if (likeDislikeView.isSelected() && isShortTextViewOnScreen(textView)) { + Logger.printDebug(() -> "Shorts dislike is already selected"); + ReturnYouTubeDislike videoData = currentVideoData; + if (videoData != null) videoData.setUserVote(Vote.DISLIKE); + } + + // For the first short played, the Shorts dislike hook is called after the video id hook. + // But for most other times this hook is called before the video id (which is not ideal). + // Must update the TextViews here, and also after the videoId changes. + updateOnScreenShortsTextViews(false); + + return true; + } catch (Exception ex) { + Logger.printException(() -> "setShortsDislikes failure", ex); + return false; + } + } + + /** + * @param forceUpdate if false, then only update the 'loading text views. + * If true, update all on screen text views. + */ + private static void updateOnScreenShortsTextViews(boolean forceUpdate) { + try { + clearRemovedShortsTextViews(); + if (shortsTextViewRefs.isEmpty()) { + return; + } + ReturnYouTubeDislike videoData = currentVideoData; + if (videoData == null) { + return; + } + + Logger.printDebug(() -> "updateShortsTextViews"); + + Runnable update = () -> { + Spanned shortsDislikesSpan = videoData.getDislikeSpanForShort(SHORTS_LOADING_SPAN); + Utils.runOnMainThreadNowOrLater(() -> { + String videoId = videoData.getVideoId(); + if (!videoId.equals(VideoInformation.getVideoId())) { + // User swiped to new video before fetch completed + Logger.printDebug(() -> "Ignoring stale dislikes data for short: " + videoId); + return; + } + + // Update text views that appear to be visible on screen. + // Only 1 will be the actual textview for the current Short, + // but discarded and not yet garbage collected views can remain. + // So must set the dislike span on all views that match. + for (WeakReference textViewRef : shortsTextViewRefs) { + TextView textView = textViewRef.get(); + if (textView == null) { + continue; + } + if (isShortTextViewOnScreen(textView) + && (forceUpdate || textView.getText().toString().equals(SHORTS_LOADING_SPAN.toString()))) { + Logger.printDebug(() -> "Setting Shorts TextView to: " + shortsDislikesSpan); + textView.setText(shortsDislikesSpan); + } + } + }); + }; + if (videoData.fetchCompleted()) { + update.run(); // Network call is completed, no need to wait on background thread. + } else { + Utils.runOnBackgroundThread(update); + } + } catch (Exception ex) { + Logger.printException(() -> "updateOnScreenShortsTextViews failure", ex); + } + } + + /** + * Check if a view is within the screen bounds. + */ + private static boolean isShortTextViewOnScreen(@NonNull View view) { + final int[] location = new int[2]; + view.getLocationInWindow(location); + if (location[0] <= 0 && location[1] <= 0) { // Lower bound + return false; + } + Rect windowRect = new Rect(); + view.getWindowVisibleDisplayFrame(windowRect); // Upper bound + return location[0] < windowRect.width() && location[1] < windowRect.height(); + } + + + // + // Video Id and voting hooks (all players). + // + + private static volatile boolean lastPlayerResponseWasShort; + + /** + * Injection point. Uses 'playback response' video id hook to preload RYD. + */ + public static void preloadVideoId(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) { + try { + if (!Settings.RYD_ENABLED.get()) { + return; + } + if (videoId.equals(lastPrefetchedVideoId)) { + return; + } + + final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort(); + // Shorts shelf in home and subscription feed causes player response hook to be called, + // and the 'is opening/playing' parameter will be false. + // This hook will be called again when the Short is actually opened. + if (videoIdIsShort && (!isShortAndOpeningOrPlaying || !Settings.RYD_SHORTS.get())) { + return; + } + final boolean waitForFetchToComplete = !IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER + && videoIdIsShort && !lastPlayerResponseWasShort; + + Logger.printDebug(() -> "Prefetching RYD for video: " + videoId); + ReturnYouTubeDislike fetch = ReturnYouTubeDislike.getFetchForVideoId(videoId); + if (waitForFetchToComplete && !fetch.fetchCompleted()) { + // This call is off the main thread, so wait until the RYD fetch completely finishes, + // otherwise if this returns before the fetch completes then the UI can + // become frozen when the main thread tries to modify the litho Shorts dislikes and + // it must wait for the fetch. + // Only need to do this for the first Short opened, as the next Short to swipe to + // are preloaded in the background. + // + // If an asynchronous litho Shorts solution is found, then this blocking call should be removed. + Logger.printDebug(() -> "Waiting for prefetch to complete: " + videoId); + fetch.getFetchData(20000); // Any arbitrarily large max wait time. + } + + // Set the fields after the fetch completes, so any concurrent calls will also wait. + lastPlayerResponseWasShort = videoIdIsShort; + lastPrefetchedVideoId = videoId; + } catch (Exception ex) { + Logger.printException(() -> "preloadVideoId failure", ex); + } + } + + /** + * Injection point. Uses 'current playing' video id hook. Always called on main thread. + */ + public static void newVideoLoaded(@NonNull String videoId) { + try { + if (!Settings.RYD_ENABLED.get()) { + return; + } + Objects.requireNonNull(videoId); + + final PlayerType currentPlayerType = PlayerType.getCurrent(); + final boolean isNoneHiddenOrSlidingMinimized = currentPlayerType.isNoneHiddenOrSlidingMinimized(); + if (isNoneHiddenOrSlidingMinimized && !Settings.RYD_SHORTS.get()) { + // Must clear here, otherwise the wrong data can be used for a minimized regular video. + clearData(); + return; + } + + if (videoIdIsSame(currentVideoData, videoId)) { + return; + } + Logger.printDebug(() -> "New video id: " + videoId + " playerType: " + currentPlayerType); + + ReturnYouTubeDislike data = ReturnYouTubeDislike.getFetchForVideoId(videoId); + // Pre-emptively set the data to short status. + // Required to prevent Shorts data from being used on a minimized video in incognito mode. + if (isNoneHiddenOrSlidingMinimized) { + data.setVideoIdIsShort(true); + } + currentVideoData = data; + + // Current video id hook can be called out of order with the non litho Shorts text view hook. + // Must manually update again here. + if (isNoneHiddenOrSlidingMinimized) { + updateOnScreenShortsTextViews(true); + } + } catch (Exception ex) { + Logger.printException(() -> "newVideoLoaded failure", ex); + } + } + + public static void setLastLithoShortsVideoId(@Nullable String videoId) { + if (videoIdIsSame(lastLithoShortsVideoData, videoId)) { + return; + } + + if (videoId == null) { + // Litho filter did not detect the video id. App is in incognito mode, + // or the proto buffer structure was changed and the video id is no longer present. + // Must clear both currently playing and last litho data otherwise the + // next regular video may use the wrong data. + Logger.printDebug(() -> "Litho filter did not find any video ids"); + clearData(); + return; + } + + Logger.printDebug(() -> "New litho Shorts video id: " + videoId); + ReturnYouTubeDislike videoData = ReturnYouTubeDislike.getFetchForVideoId(videoId); + videoData.setVideoIdIsShort(true); + lastLithoShortsVideoData = videoData; + lithoShortsShouldUseCurrentData = false; + } + + private static boolean videoIdIsSame(@Nullable ReturnYouTubeDislike fetch, @Nullable String videoId) { + return (fetch == null && videoId == null) + || (fetch != null && fetch.getVideoId().equals(videoId)); + } + + /** + * Injection point. + *

+ * Called when the user likes or dislikes. + * + * @param vote int that matches {@link Vote#value} + */ + public static void sendVote(int vote) { + try { + if (!Settings.RYD_ENABLED.get()) { + return; + } + final boolean isNoneHiddenOrMinimized = PlayerType.getCurrent().isNoneHiddenOrMinimized(); + if (isNoneHiddenOrMinimized && !Settings.RYD_SHORTS.get()) { + return; + } + ReturnYouTubeDislike videoData = currentVideoData; + if (videoData == null) { + Logger.printDebug(() -> "Cannot send vote, as current video data is null"); + return; // User enabled RYD while a regular video was minimized. + } + + for (Vote v : Vote.values()) { + if (v.value == vote) { + videoData.sendVote(v); + + if (isNoneHiddenOrMinimized && lastLithoShortsVideoData != null) { + lithoShortsShouldUseCurrentData = true; + } + + return; + } + } + Logger.printException(() -> "Unknown vote type: " + vote); + } catch (Exception ex) { + Logger.printException(() -> "sendVote failure", ex); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ToolBarPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ToolBarPatch.java new file mode 100644 index 000000000..2d681133f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ToolBarPatch.java @@ -0,0 +1,28 @@ +package app.revanced.extension.youtube.patches.utils; + +import android.view.View; +import android.widget.ImageView; + +import app.revanced.extension.shared.utils.Logger; + +@SuppressWarnings("unused") +public class ToolBarPatch { + + public static void hookToolBar(Enum buttonEnum, ImageView imageView) { + final String enumString = buttonEnum.name(); + if (enumString.isEmpty() || + imageView == null || + !(imageView.getParent() instanceof View view)) { + return; + } + + Logger.printDebug(() -> "enumString: " + enumString); + + hookToolBar(enumString, view); + } + + private static void hookToolBar(String enumString, View parentView) { + } +} + + diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/AV1CodecPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/AV1CodecPatch.java new file mode 100644 index 000000000..71f5dd9d6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/AV1CodecPatch.java @@ -0,0 +1,53 @@ +package app.revanced.extension.youtube.patches.video; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class AV1CodecPatch { + private static final int LITERAL_VALUE_AV01 = 1635135811; + private static final int LITERAL_VALUE_DOLBY_VISION = 1685485123; + private static final String VP9_CODEC = "video/x-vnd.on2.vp9"; + private static long lastTimeResponse = 0; + + /** + * Replace the SW AV01 codec to VP9 codec. + * May not be valid on some clients. + * + * @param original hardcoded value - "video/av01" + */ + public static String replaceCodec(String original) { + return Settings.REPLACE_AV1_CODEC.get() ? VP9_CODEC : original; + } + + /** + * Replace the SW AV01 codec request with a Dolby Vision codec request. + * This request is invalid, so it falls back to codecs other than AV01. + *

+ * Limitation: Fallback process causes about 15-20 seconds of buffering. + * + * @param literalValue literal value of the codec + */ + public static int rejectResponse(int literalValue) { + if (!Settings.REJECT_AV1_CODEC.get()) + return literalValue; + + Logger.printDebug(() -> "Response: " + literalValue); + + if (literalValue != LITERAL_VALUE_AV01) + return literalValue; + + final long currentTime = System.currentTimeMillis(); + + // Ignore the invoke within 20 seconds. + if (currentTime - lastTimeResponse > 20000) { + lastTimeResponse = currentTime; + Utils.showToastShort(str("revanced_reject_av1_codec_toast")); + } + + return LITERAL_VALUE_DOLBY_VISION; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/CustomPlaybackSpeedPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/CustomPlaybackSpeedPatch.java new file mode 100644 index 000000000..cca3c4ff4 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/CustomPlaybackSpeedPatch.java @@ -0,0 +1,268 @@ +package app.revanced.extension.youtube.patches.video; + +import static app.revanced.extension.shared.utils.ResourceUtils.getString; +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.content.Context; +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +import java.util.Arrays; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.components.PlaybackSpeedMenuFilter; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.VideoUtils; + +@SuppressWarnings("unused") +public class CustomPlaybackSpeedPatch { + private static final float PLAYBACK_SPEED_AUTO = Settings.DEFAULT_PLAYBACK_SPEED.defaultValue; + + /** + * Maximum playback speed, exclusive value. Custom speeds must be less than this value. + *

+ * Going over 8x does not increase the actual playback speed any higher, + * and the UI selector starts flickering and acting weird. + * Over 10x and the speeds show up out of order in the UI selector. + */ + public static final float PLAYBACK_SPEED_MAXIMUM = 8; + private static final String[] defaultSpeedEntries; + private static final String[] defaultSpeedEntryValues; + /** + * Custom playback speeds. + */ + private static float[] playbackSpeeds; + private static String[] customSpeedEntries; + private static String[] customSpeedEntryValues; + + private static String[] playbackSpeedEntries; + private static String[] playbackSpeedEntryValues; + + /** + * The last time the old playback menu was forcefully called. + */ + private static long lastTimeOldPlaybackMenuInvoked; + + static { + defaultSpeedEntries = new String[]{getString("quality_auto"), "0.25x", "0.5x", "0.75x", getString("revanced_playback_speed_normal"), "1.25x", "1.5x", "1.75x", "2.0x"}; + defaultSpeedEntryValues = new String[]{"-2.0", "0.25", "0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0"}; + + loadCustomSpeeds(); + } + + /** + * Injection point. + */ + public static float[] getArray(float[] original) { + return isCustomPlaybackSpeedEnabled() ? playbackSpeeds : original; + } + + /** + * Injection point. + */ + public static int getLength(int original) { + return isCustomPlaybackSpeedEnabled() ? playbackSpeeds.length : original; + } + + /** + * Injection point. + */ + public static int getSize(int original) { + return isCustomPlaybackSpeedEnabled() ? 0 : original; + } + + public static String[] getListEntries() { + return isCustomPlaybackSpeedEnabled() + ? customSpeedEntries + : defaultSpeedEntries; + } + + public static String[] getListEntryValues() { + return isCustomPlaybackSpeedEnabled() + ? customSpeedEntryValues + : defaultSpeedEntryValues; + } + + public static String[] getTrimmedListEntries() { + if (playbackSpeedEntries == null) { + final String[] playbackSpeedWithAutoEntries = getListEntries(); + playbackSpeedEntries = Arrays.copyOfRange(playbackSpeedWithAutoEntries, 1, playbackSpeedWithAutoEntries.length); + } + + return playbackSpeedEntries; + } + + public static String[] getTrimmedListEntryValues() { + if (playbackSpeedEntryValues == null) { + final String[] playbackSpeedWithAutoEntryValues = getListEntryValues(); + playbackSpeedEntryValues = Arrays.copyOfRange(playbackSpeedWithAutoEntryValues, 1, playbackSpeedWithAutoEntryValues.length); + } + + return playbackSpeedEntryValues; + } + + private static void resetCustomSpeeds(@NonNull String toastMessage) { + Utils.showToastLong(toastMessage); + Utils.showToastShort(str("revanced_extended_reset_to_default_toast")); + Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault(); + } + + private static void loadCustomSpeeds() { + try { + if (!Settings.ENABLE_CUSTOM_PLAYBACK_SPEED.get()) { + return; + } + + String[] speedStrings = Settings.CUSTOM_PLAYBACK_SPEEDS.get().split("\\s+"); + Arrays.sort(speedStrings); + if (speedStrings.length == 0) { + throw new IllegalArgumentException(); + } + playbackSpeeds = new float[speedStrings.length]; + int i = 0; + for (String speedString : speedStrings) { + final float speedFloat = Float.parseFloat(speedString); + if (speedFloat <= 0 || arrayContains(playbackSpeeds, speedFloat)) { + throw new IllegalArgumentException(); + } + + if (speedFloat > PLAYBACK_SPEED_MAXIMUM) { + resetCustomSpeeds(str("revanced_custom_playback_speeds_invalid", PLAYBACK_SPEED_MAXIMUM)); + loadCustomSpeeds(); + return; + } + + playbackSpeeds[i] = speedFloat; + i++; + } + + if (customSpeedEntries != null) return; + + customSpeedEntries = new String[playbackSpeeds.length + 1]; + customSpeedEntryValues = new String[playbackSpeeds.length + 1]; + customSpeedEntries[0] = getString("quality_auto"); + customSpeedEntryValues[0] = "-2.0"; + + i = 1; + for (float speed : playbackSpeeds) { + String speedString = String.valueOf(speed); + customSpeedEntries[i] = speed != 1.0f + ? speedString + "x" + : getString("revanced_playback_speed_normal"); + customSpeedEntryValues[i] = speedString; + i++; + } + } catch (Exception ex) { + Logger.printInfo(() -> "parse error", ex); + resetCustomSpeeds(str("revanced_custom_playback_speeds_parse_exception")); + loadCustomSpeeds(); + } + } + + private static boolean arrayContains(float[] array, float value) { + for (float arrayValue : array) { + if (arrayValue == value) return true; + } + return false; + } + + private static boolean isCustomPlaybackSpeedEnabled() { + return Settings.ENABLE_CUSTOM_PLAYBACK_SPEED.get() && playbackSpeeds != null; + } + + /** + * Injection point. + */ + public static void onFlyoutMenuCreate(RecyclerView recyclerView) { + if (!Settings.ENABLE_CUSTOM_PLAYBACK_SPEED.get()) { + return; + } + + recyclerView.getViewTreeObserver().addOnDrawListener(() -> { + try { + if (PlaybackSpeedMenuFilter.isOldPlaybackSpeedMenuVisible) { + if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 8)) { + PlaybackSpeedMenuFilter.isOldPlaybackSpeedMenuVisible = false; + } + return; + } + } catch (Exception ex) { + Logger.printException(() -> "isOldPlaybackSpeedMenuVisible failure", ex); + } + + try { + if (PlaybackSpeedMenuFilter.isPlaybackRateSelectorMenuVisible) { + if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 5)) { + PlaybackSpeedMenuFilter.isPlaybackRateSelectorMenuVisible = false; + } + } + } catch (Exception ex) { + Logger.printException(() -> "isPlaybackRateSelectorMenuVisible failure", ex); + } + }); + } + + private static boolean hideLithoMenuAndShowOldSpeedMenu(RecyclerView recyclerView, int expectedChildCount) { + if (recyclerView.getChildCount() == 0) { + return false; + } + + if (!(recyclerView.getChildAt(0) instanceof ViewGroup PlaybackSpeedParentView)) { + return false; + } + + if (PlaybackSpeedParentView.getChildCount() != expectedChildCount) { + return false; + } + + if (!(Utils.getParentView(recyclerView, 3) instanceof ViewGroup parentView3rd)) { + return false; + } + + if (!(parentView3rd.getParent() instanceof ViewGroup parentView4th)) { + return false; + } + + // Dismiss View [R.id.touch_outside] is the 1st ChildView of the 4th ParentView. + // This only shows in phone layout. + Utils.clickView(parentView4th.getChildAt(0)); + + // In tablet layout there is no Dismiss View, instead we just hide all two parent views. + parentView3rd.setVisibility(View.GONE); + parentView4th.setVisibility(View.GONE); + + // Show old playback speed menu. + showCustomPlaybackSpeedMenu(recyclerView.getContext()); + + return true; + } + + /** + * This method is sometimes used multiple times + * To prevent this, ignore method reuse within 1 second. + * + * @param context Context for [playbackSpeedDialogListener] + */ + private static void showCustomPlaybackSpeedMenu(@NonNull Context context) { + // This method is sometimes used multiple times. + // To prevent this, ignore method reuse within 1 second. + final long now = System.currentTimeMillis(); + if (now - lastTimeOldPlaybackMenuInvoked < 1000) { + return; + } + lastTimeOldPlaybackMenuInvoked = now; + + if (Settings.CUSTOM_PLAYBACK_SPEED_MENU_TYPE.get()) { + // Open playback speed dialog + VideoUtils.showPlaybackSpeedDialog(context); + } else { + // Open old style flyout menu + VideoUtils.showPlaybackSpeedFlyoutMenu(); + } + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/HDRVideoPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/HDRVideoPatch.java new file mode 100644 index 000000000..0ad3758c3 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/HDRVideoPatch.java @@ -0,0 +1,11 @@ +package app.revanced.extension.youtube.patches.video; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class HDRVideoPatch { + + public static boolean disableHDRVideo() { + return !Settings.DISABLE_HDR_VIDEO.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java new file mode 100644 index 000000000..ca2ad8fc1 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java @@ -0,0 +1,143 @@ +package app.revanced.extension.youtube.patches.video; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.apache.commons.lang3.BooleanUtils; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.utils.PatchStatus; +import app.revanced.extension.youtube.patches.video.requests.PlaylistRequest; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.VideoInformation; +import app.revanced.extension.youtube.whitelist.Whitelist; + +@SuppressWarnings("unused") +public class PlaybackSpeedPatch { + private static final long TOAST_DELAY_MILLISECONDS = 750; + private static long lastTimeSpeedChanged; + private static boolean isLiveStream; + + /** + * Injection point. + */ + public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, + @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, + final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { + isLiveStream = newlyLoadedLiveStreamValue; + Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId); + + final float defaultPlaybackSpeed = getDefaultPlaybackSpeed(newlyLoadedChannelId, newlyLoadedVideoId); + Logger.printDebug(() -> "overridePlaybackSpeed: " + defaultPlaybackSpeed); + + VideoInformation.overridePlaybackSpeed(defaultPlaybackSpeed); + } + + /** + * Injection point. + */ + public static void fetchPlaylistData(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) { + if (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get()) { + try { + final boolean videoIdIsShort = VideoInformation.lastPlayerResponseIsShort(); + // Shorts shelf in home and subscription feed causes player response hook to be called, + // and the 'is opening/playing' parameter will be false. + // This hook will be called again when the Short is actually opened. + if (videoIdIsShort && !isShortAndOpeningOrPlaying) { + return; + } + + PlaylistRequest.fetchRequestIfNeeded(videoId); + } catch (Exception ex) { + Logger.printException(() -> "fetchPlaylistData failure", ex); + } + } + } + + /** + * Injection point. + */ + public static float getPlaybackSpeedInShorts(final float playbackSpeed) { + if (!VideoInformation.lastPlayerResponseIsShort()) + return playbackSpeed; + if (!Settings.ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS.get()) + return playbackSpeed; + + float defaultPlaybackSpeed = getDefaultPlaybackSpeed(VideoInformation.getChannelId(), null); + Logger.printDebug(() -> "overridePlaybackSpeed in Shorts: " + defaultPlaybackSpeed); + + return defaultPlaybackSpeed; + } + + /** + * Injection point. + * Called when user selects a playback speed. + * + * @param playbackSpeed The playback speed the user selected + */ + public static void userSelectedPlaybackSpeed(float playbackSpeed) { + try { + if (PatchStatus.RememberPlaybackSpeed() && + Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) { + // With the 0.05x menu, if the speed is set by integrations to higher than 2.0x + // then the menu will allow increasing without bounds but the max speed is + // still capped to under 8.0x. + playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.PLAYBACK_SPEED_MAXIMUM - 0.05f); + + // Prevent toast spamming if using the 0.05x adjustments. + // Show exactly one toast after the user stops interacting with the speed menu. + final long now = System.currentTimeMillis(); + lastTimeSpeedChanged = now; + + final float finalPlaybackSpeed = playbackSpeed; + Utils.runOnMainThreadDelayed(() -> { + if (lastTimeSpeedChanged != now) { + // The user made additional speed adjustments and this call is outdated. + return; + } + + if (Settings.DEFAULT_PLAYBACK_SPEED.get() == finalPlaybackSpeed) { + // User changed to a different speed and immediately changed back. + // Or the user is going past 8.0x in the glitched out 0.05x menu. + return; + } + Settings.DEFAULT_PLAYBACK_SPEED.save(finalPlaybackSpeed); + + if (!Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST.get()) { + return; + } + Utils.showToastShort(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x"))); + }, TOAST_DELAY_MILLISECONDS); + } + } catch (Exception ex) { + Logger.printException(() -> "userSelectedPlaybackSpeed failure", ex); + } + } + + private static float getDefaultPlaybackSpeed(@NonNull String channelId, @Nullable String videoId) { + return (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_LIVE.get() && isLiveStream) || + Whitelist.isChannelWhitelistedPlaybackSpeed(channelId) || + getPlaylistData(videoId) + ? 1.0f + : Settings.DEFAULT_PLAYBACK_SPEED.get(); + } + + private static boolean getPlaylistData(@Nullable String videoId) { + if (Settings.DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC.get() && videoId != null) { + try { + PlaylistRequest request = PlaylistRequest.getRequestForVideoId(videoId); + final boolean isPlaylist = request != null && BooleanUtils.toBoolean(request.getStream()); + Logger.printDebug(() -> "isPlaylist: " + isPlaylist); + + return isPlaylist; + } catch (Exception ex) { + Logger.printException(() -> "getPlaylistData failure", ex); + } + } + + return false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/ReloadVideoPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/ReloadVideoPatch.java new file mode 100644 index 000000000..b2516e7ce --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/ReloadVideoPatch.java @@ -0,0 +1,53 @@ +package app.revanced.extension.youtube.patches.video; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import androidx.annotation.NonNull; + +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; +import app.revanced.extension.youtube.shared.VideoInformation; + +@SuppressWarnings("unused") +public class ReloadVideoPatch { + private static final long RELOAD_VIDEO_TIME_MILLISECONDS = 15000L; + + @NonNull + public static String videoId = ""; + + /** + * Injection point. + */ + public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, + @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, + final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { + if (!Settings.SKIP_PRELOADED_BUFFER.get()) + return; + if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL) + return; + if (videoId.equals(newlyLoadedVideoId)) + return; + videoId = newlyLoadedVideoId; + + if (newlyLoadedVideoLength < RELOAD_VIDEO_TIME_MILLISECONDS || newlyLoadedLiveStreamValue) + return; + + final long seekTime = Math.max(RELOAD_VIDEO_TIME_MILLISECONDS, (long) (newlyLoadedVideoLength * 0.5)); + + Utils.runOnMainThreadDelayed(() -> reloadVideo(seekTime), 250); + } + + private static void reloadVideo(final long videoLength) { + final long lastVideoTime = VideoInformation.getVideoTime(); + final float playbackSpeed = VideoInformation.getPlaybackSpeed(); + final long speedAdjustedTimeThreshold = (long) (playbackSpeed * 300); + VideoInformation.overrideVideoTime(videoLength); + VideoInformation.overrideVideoTime(lastVideoTime + speedAdjustedTimeThreshold); + + if (!Settings.SKIP_PRELOADED_BUFFER_TOAST.get()) + return; + + Utils.showToastShort(str("revanced_skipped_preloaded_buffer")); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/RestoreOldVideoQualityMenuPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/RestoreOldVideoQualityMenuPatch.java new file mode 100644 index 000000000..b1ac0c979 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/RestoreOldVideoQualityMenuPatch.java @@ -0,0 +1,73 @@ +package app.revanced.extension.youtube.patches.video; + +import android.support.v7.widget.RecyclerView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ListView; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.components.VideoQualityMenuFilter; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class RestoreOldVideoQualityMenuPatch { + + public static boolean restoreOldVideoQualityMenu() { + return Settings.RESTORE_OLD_VIDEO_QUALITY_MENU.get(); + } + + public static void restoreOldVideoQualityMenu(ListView listView) { + if (!Settings.RESTORE_OLD_VIDEO_QUALITY_MENU.get()) + return; + + listView.setVisibility(View.GONE); + + Utils.runOnMainThreadDelayed(() -> { + listView.setSoundEffectsEnabled(false); + listView.performItemClick(null, 2, 0); + }, + 1 + ); + } + + public static void onFlyoutMenuCreate(final RecyclerView recyclerView) { + if (!Settings.RESTORE_OLD_VIDEO_QUALITY_MENU.get()) + return; + + recyclerView.getViewTreeObserver().addOnDrawListener(() -> { + try { + // Check if the current view is the quality menu. + if (!VideoQualityMenuFilter.isVideoQualityMenuVisible || recyclerView.getChildCount() == 0) { + return; + } + + if (!(Utils.getParentView(recyclerView, 3) instanceof ViewGroup quickQualityViewParent)) { + return; + } + + if (!(recyclerView.getChildAt(0) instanceof ViewGroup advancedQualityParentView)) { + return; + } + + if (advancedQualityParentView.getChildCount() < 4) { + return; + } + + View advancedQualityView = advancedQualityParentView.getChildAt(3); + if (advancedQualityView == null) { + return; + } + + quickQualityViewParent.setVisibility(View.GONE); + + // Click the "Advanced" quality menu to show the "old" quality menu. + advancedQualityView.callOnClick(); + + VideoQualityMenuFilter.isVideoQualityMenuVisible = false; + } catch (Exception ex) { + Logger.printException(() -> "onFlyoutMenuCreate failure", ex); + } + }); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/SpoofDeviceDimensionsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/SpoofDeviceDimensionsPatch.java new file mode 100644 index 000000000..d5e4e2801 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/SpoofDeviceDimensionsPatch.java @@ -0,0 +1,16 @@ +package app.revanced.extension.youtube.patches.video; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class SpoofDeviceDimensionsPatch { + private static final boolean SPOOF = Settings.SPOOF_DEVICE_DIMENSIONS.get(); + + public static int getMinHeightOrWidth(int minHeightOrWidth) { + return SPOOF ? 64 : minHeightOrWidth; + } + + public static int getMaxHeightOrWidth(int maxHeightOrWidth) { + return SPOOF ? 4096 : maxHeightOrWidth; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/VP9CodecPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/VP9CodecPatch.java new file mode 100644 index 000000000..6052c55ef --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/VP9CodecPatch.java @@ -0,0 +1,11 @@ +package app.revanced.extension.youtube.patches.video; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class VP9CodecPatch { + + public static boolean disableVP9Codec() { + return !Settings.DISABLE_VP9_CODEC.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/VideoQualityPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/VideoQualityPatch.java new file mode 100644 index 000000000..c42125c0d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/VideoQualityPatch.java @@ -0,0 +1,92 @@ +package app.revanced.extension.youtube.patches.video; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import androidx.annotation.NonNull; + +import app.revanced.extension.shared.settings.IntegerSetting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; +import app.revanced.extension.youtube.shared.VideoInformation; + +@SuppressWarnings("unused") +public class VideoQualityPatch { + private static final int DEFAULT_YOUTUBE_VIDEO_QUALITY = -2; + private static final IntegerSetting mobileQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_MOBILE; + private static final IntegerSetting wifiQualitySetting = Settings.DEFAULT_VIDEO_QUALITY_WIFI; + + @NonNull + public static String videoId = ""; + + /** + * Injection point. + */ + public static void newVideoStarted() { + setVideoQuality(0); + } + + /** + * Injection point. + */ + public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, + @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, + final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { + if (PlayerType.getCurrent() == PlayerType.INLINE_MINIMAL) + return; + if (videoId.equals(newlyLoadedVideoId)) + return; + videoId = newlyLoadedVideoId; + setVideoQuality(Settings.SKIP_PRELOADED_BUFFER.get() ? 250 : 500); + } + + /** + * Injection point. + */ + public static void userSelectedVideoQuality() { + Utils.runOnMainThreadDelayed(() -> + userSelectedVideoQuality(VideoInformation.getVideoQuality()), + 300 + ); + } + + private static void setVideoQuality(final long delayMillis) { + final int defaultQuality = Utils.getNetworkType() == Utils.NetworkType.MOBILE + ? mobileQualitySetting.get() + : wifiQualitySetting.get(); + + if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY) + return; + + Utils.runOnMainThreadDelayed(() -> { + final int qualityToUseFinal = VideoInformation.getAvailableVideoQuality(defaultQuality); + Logger.printDebug(() -> "Changing video quality to: " + qualityToUseFinal); + VideoInformation.overrideVideoQuality(qualityToUseFinal); + }, delayMillis + ); + } + + private static void userSelectedVideoQuality(final int defaultQuality) { + if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get()) + return; + if (defaultQuality == DEFAULT_YOUTUBE_VIDEO_QUALITY) + return; + + final Utils.NetworkType networkType = Utils.getNetworkType(); + + switch (networkType) { + case NONE -> { + Utils.showToastShort(str("revanced_remember_video_quality_none")); + return; + } + case MOBILE -> mobileQualitySetting.save(defaultQuality); + default -> wifiQualitySetting.save(defaultQuality); + } + + if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST.get()) + return; + + Utils.showToastShort(str("revanced_remember_video_quality_" + networkType.getName(), defaultQuality + "p")); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/PlaylistRequest.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/PlaylistRequest.java new file mode 100644 index 000000000..b7c69cd0a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/requests/PlaylistRequest.java @@ -0,0 +1,202 @@ +package app.revanced.extension.youtube.patches.video.requests; + +import static app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes.GET_PLAYLIST_PAGE; + +import android.annotation.SuppressLint; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import app.revanced.extension.shared.patches.client.AppClient.ClientType; +import app.revanced.extension.shared.patches.spoof.requests.PlayerRoutes; +import app.revanced.extension.shared.requests.Requester; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.shared.VideoInformation; + +public class PlaylistRequest { + + /** + * How long to keep fetches until they are expired. + */ + private static final long CACHE_RETENTION_TIME_MILLISECONDS = 60 * 1000; // 1 Minute + + private static final long MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; // 20 seconds + + @GuardedBy("itself") + private static final Map cache = new HashMap<>(); + + @SuppressLint("ObsoleteSdkInt") + public static void fetchRequestIfNeeded(@Nullable String videoId) { + Objects.requireNonNull(videoId); + synchronized (cache) { + final long now = System.currentTimeMillis(); + + cache.values().removeIf(request -> { + final boolean expired = request.isExpired(now); + if (expired) Logger.printDebug(() -> "Removing expired stream: " + request.videoId); + return expired; + }); + + if (!cache.containsKey(videoId)) { + cache.put(videoId, new PlaylistRequest(videoId)); + } + } + } + + @Nullable + public static PlaylistRequest getRequestForVideoId(@Nullable String videoId) { + synchronized (cache) { + return cache.get(videoId); + } + } + + private static void handleConnectionError(String toastMessage, @Nullable Exception ex) { + Logger.printInfo(() -> toastMessage, ex); + } + + @Nullable + private static JSONObject send(ClientType clientType, String videoId) { + Objects.requireNonNull(clientType); + Objects.requireNonNull(videoId); + + final long startTime = System.currentTimeMillis(); + String clientTypeName = clientType.name(); + Logger.printDebug(() -> "Fetching playlist request for: " + videoId + " using client: " + clientTypeName); + + try { + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_PLAYLIST_PAGE, clientType); + + String innerTubeBody = String.format( + Locale.ENGLISH, + PlayerRoutes.createInnertubeBody(clientType, true), + videoId, + "RD" + videoId + ); + byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(requestBody.length); + connection.getOutputStream().write(requestBody); + + final int responseCode = connection.getResponseCode(); + if (responseCode == 200) return Requester.parseJSONObject(connection); + + handleConnectionError(clientTypeName + " not available with response code: " + + responseCode + " message: " + connection.getResponseMessage(), + null); + } catch (SocketTimeoutException ex) { + handleConnectionError("Connection timeout", ex); + } catch (IOException ex) { + handleConnectionError("Network error", ex); + } catch (Exception ex) { + Logger.printException(() -> "send failed", ex); + } finally { + Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms"); + } + + return null; + } + + private static Boolean fetch(@NonNull String videoId) { + final ClientType clientType = ClientType.ANDROID_VR; + final JSONObject playlistJson = send(clientType, videoId); + if (playlistJson != null) { + try { + final JSONObject singleColumnWatchNextResultsJsonObject = playlistJson + .getJSONObject("contents") + .getJSONObject("singleColumnWatchNextResults"); + + if (!singleColumnWatchNextResultsJsonObject.has("playlist")) { + return false; + } + + final JSONObject playlistJsonObject = singleColumnWatchNextResultsJsonObject + .getJSONObject("playlist") + .getJSONObject("playlist"); + + final Object currentStreamObject = playlistJsonObject + .getJSONArray("contents") + .get(0); + + if (!(currentStreamObject instanceof JSONObject currentStreamJsonObject)) { + return false; + } + + final JSONObject watchEndpointJsonObject = currentStreamJsonObject + .getJSONObject("playlistPanelVideoRenderer") + .getJSONObject("navigationEndpoint") + .getJSONObject("watchEndpoint"); + + Logger.printDebug(() -> "watchEndpoint: " + watchEndpointJsonObject); + + return watchEndpointJsonObject.has("playerParams") && + VideoInformation.isMixPlaylistsOpenedByUser(watchEndpointJsonObject.getString("playerParams")); + } catch (JSONException e) { + Logger.printDebug(() -> "Fetch failed while processing response data for response: " + playlistJson); + } + } + + return false; + } + + /** + * Time this instance and the fetch future was created. + */ + private final long timeFetched; + private final String videoId; + private final Future future; + + private PlaylistRequest(String videoId) { + this.timeFetched = System.currentTimeMillis(); + this.videoId = videoId; + this.future = Utils.submitOnBackgroundThread(() -> fetch(videoId)); + } + + public boolean isExpired(long now) { + final long timeSinceCreation = now - timeFetched; + if (timeSinceCreation > CACHE_RETENTION_TIME_MILLISECONDS) { + return true; + } + + // Only expired if the fetch failed (API null response). + return (fetchCompleted() && getStream() == null); + } + + /** + * @return if the fetch call has completed. + */ + public boolean fetchCompleted() { + return future.isDone(); + } + + public Boolean getStream() { + try { + return future.get(MAX_MILLISECONDS_TO_WAIT_FOR_FETCH, TimeUnit.MILLISECONDS); + } catch (TimeoutException ex) { + Logger.printInfo(() -> "getStream timed out", ex); + } catch (InterruptedException ex) { + Logger.printException(() -> "getStream interrupted", ex); + Thread.currentThread().interrupt(); // Restore interrupt status flag. + } catch (ExecutionException ex) { + Logger.printException(() -> "getStream failure", ex); + } + + return null; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java new file mode 100644 index 000000000..dd478f4f0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java @@ -0,0 +1,776 @@ +package app.revanced.extension.youtube.returnyoutubedislike; + +import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote; +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.isSDKAbove; +import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.ShapeDrawable; +import android.graphics.drawable.shapes.OvalShape; +import android.graphics.drawable.shapes.RectShape; +import android.icu.text.CompactDecimalFormat; +import android.icu.text.DecimalFormat; +import android.icu.text.DecimalFormatSymbols; +import android.icu.text.NumberFormat; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.text.style.ImageSpan; +import android.text.style.ReplacementSpan; +import android.util.DisplayMetrics; +import android.util.TypedValue; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import app.revanced.extension.shared.returnyoutubedislike.requests.RYDVoteData; +import app.revanced.extension.shared.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; +import app.revanced.extension.youtube.utils.ThemeUtils; + +/** + * Handles fetching and creation/replacing of RYD dislike text spans. + *

+ * Because Litho creates spans using multiple threads, this entire class supports multithreading as well. + */ +public class ReturnYouTubeDislike { + + /** + * Maximum amount of time to block the UI from updates while waiting for network call to complete. + *

+ * Must be less than 5 seconds, as per: + * ... + */ + private static final long MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH = 4000; + + /** + * How long to retain successful RYD fetches. + */ + private static final long CACHE_TIMEOUT_SUCCESS_MILLISECONDS = 7 * 60 * 1000; // 7 Minutes + + /** + * How long to retain unsuccessful RYD fetches, + * and also the minimum time before retrying again. + */ + private static final long CACHE_TIMEOUT_FAILURE_MILLISECONDS = 3 * 60 * 1000; // 3 Minutes + + /** + * Unique placeholder character, used to detect if a segmented span already has dislikes added to it. + * Must be something YouTube is unlikely to use, as it's searched for in all usage of Rolling Number. + */ + private static final char MIDDLE_SEPARATOR_CHARACTER = '◎'; // 'bullseye' + + public static final boolean IS_SPOOFING_TO_OLD_SEPARATOR_COLOR = + isSpoofingToLessThan("18.10.00"); + + /** + * Cached lookup of all video ids. + */ + @GuardedBy("itself") + private static final Map fetchCache = new HashMap<>(); + + /** + * Used to send votes, one by one, in the same order the user created them. + */ + private static final ExecutorService voteSerialExecutor = Executors.newSingleThreadExecutor(); + + /** + * For formatting dislikes as number. + */ + @GuardedBy("ReturnYouTubeDislike.class") // not thread safe + private static CompactDecimalFormat dislikeCountFormatter; + + /** + * For formatting dislikes as percentage. + */ + @GuardedBy("ReturnYouTubeDislike.class") + private static NumberFormat dislikePercentageFormatter; + + // Used for segmented dislike spans in Litho regular player. + public static final Rect leftSeparatorBounds; + private static final Rect middleSeparatorBounds; + + /** + * Left separator horizontal padding for Rolling Number layout. + */ + public static final int leftSeparatorShapePaddingPixels; + private static final ShapeDrawable leftSeparatorShape; + public static final Locale locale; + + static { + final Resources resources = Utils.getResources(); + DisplayMetrics dp = resources.getDisplayMetrics(); + + leftSeparatorBounds = new Rect(0, 0, + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.2f, dp), + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14, dp)); + final int middleSeparatorSize = + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp); + middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize); + + leftSeparatorShapePaddingPixels = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 10.0f, dp); + + leftSeparatorShape = new ShapeDrawable(new RectShape()); + leftSeparatorShape.setBounds(leftSeparatorBounds); + locale = resources.getConfiguration().getLocales().get(0); + + ReturnYouTubeDislikeApi.toastOnConnectionError = Settings.RYD_TOAST_ON_CONNECTION_ERROR.get(); + } + + private final String videoId; + + /** + * Stores the results of the vote api fetch, and used as a barrier to wait until fetch completes. + * Absolutely cannot be holding any lock during calls to {@link Future#get()}. + */ + private final Future future; + + /** + * Time this instance and the fetch future was created. + */ + private final long timeFetched; + + /** + * If this instance was previously used for a Short. + */ + @GuardedBy("this") + private boolean isShort; + + /** + * Optional current vote status of the UI. Used to apply a user vote that was done on a previous video viewing. + */ + @Nullable + @GuardedBy("this") + private Vote userVote; + + /** + * Original dislike span, before modifications. + */ + @Nullable + @GuardedBy("this") + private Spanned originalDislikeSpan; + + /** + * Replacement like/dislike span that includes formatted dislikes. + * Used to prevent recreating the same span multiple times. + */ + @Nullable + @GuardedBy("this") + private SpannableString replacementLikeDislikeSpan; + + /** + * Color of the left and middle separator, based on the color of the right separator. + * It's unknown where YT gets the color from, and the values here are approximated by hand. + * Ideally, this would be the actual color YT uses at runtime. + *

+ * Older versions before the 'Me' library tab use a slightly different color. + * If spoofing was previously used and is now turned off, + * or an old version was recently upgraded then the old colors are sometimes still used. + */ + private static int getSeparatorColor() { + if (IS_SPOOFING_TO_OLD_SEPARATOR_COLOR) { + return ThemeUtils.isDarkTheme() + ? 0x29AAAAAA // transparent dark gray + : 0xFFD9D9D9; // light gray + } + + return ThemeUtils.isDarkTheme() + ? 0x33FFFFFF + : 0xFFD9D9D9; + } + + public static ShapeDrawable getLeftSeparatorDrawable() { + leftSeparatorShape.getPaint().setColor(getSeparatorColor()); + return leftSeparatorShape; + } + + /** + * @param isSegmentedButton If UI is using the segmented single UI component for both like and dislike. + */ + @NonNull + private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, + boolean isSegmentedButton, + boolean isRollingNumber, + @NonNull RYDVoteData voteData) { + if (!isSegmentedButton) { + // Simple replacement of 'dislike' with a number/percentage. + return newSpannableWithDislikes(oldSpannable, voteData); + } + + // Note: Some locales use right to left layout (Arabic, Hebrew, etc). + // If making changes to this code, change device settings to a RTL language and verify layout is correct. + CharSequence oldLikes = oldSpannable; + + // YouTube creators can hide the like count on a video, + // and the like count appears as a device language specific string that says 'Like'. + // Check if the string contains any numbers. + if (!Utils.containsNumber(oldLikes)) { + if (Settings.RYD_ESTIMATED_LIKE.get()) { + // Likes are hidden by video creator + // + // RYD does not directly provide like data, but can use an estimated likes + // using the same scale factor RYD applied to the raw dislikes. + // + // example video: https://www.youtube.com/watch?v=UnrU5vxCHxw + // RYD data: https://returnyoutubedislikeapi.com/votes?videoId=UnrU5vxCHxw + Logger.printDebug(() -> "Using estimated likes"); + oldLikes = formatDislikeCount(voteData.getLikeCount()); + } else { + // Change the "Likes" string to show that likes and dislikes are hidden. + String hiddenMessageString = str("revanced_ryd_video_likes_hidden_by_video_owner"); + return newSpanUsingStylingOfAnotherSpan(oldSpannable, hiddenMessageString); + } + } + + SpannableStringBuilder builder = new SpannableStringBuilder(); + final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get(); + + if (!compactLayout) { + String leftSeparatorString = getTextDirectionString(); + final Spannable leftSeparatorSpan; + if (isRollingNumber) { + leftSeparatorSpan = new SpannableString(leftSeparatorString); + } else { + leftSeparatorString += " "; + leftSeparatorSpan = new SpannableString(leftSeparatorString); + // Styling spans cannot overwrite RTL or LTR character. + leftSeparatorSpan.setSpan( + new VerticallyCenteredImageSpan(getLeftSeparatorDrawable(), false), + 1, 2, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + leftSeparatorSpan.setSpan( + new FixedWidthEmptySpan(leftSeparatorShapePaddingPixels), + 2, 3, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + } + builder.append(leftSeparatorSpan); + } + + // likes + builder.append(newSpanUsingStylingOfAnotherSpan(oldSpannable, oldLikes)); + + // middle separator + String middleSeparatorString = compactLayout + ? " " + MIDDLE_SEPARATOR_CHARACTER + " " + : " \u2009" + MIDDLE_SEPARATOR_CHARACTER + "\u2009 "; // u2009 = 'narrow space' character + final int shapeInsertionIndex = middleSeparatorString.length() / 2; + Spannable middleSeparatorSpan = new SpannableString(middleSeparatorString); + ShapeDrawable shapeDrawable = new ShapeDrawable(new OvalShape()); + shapeDrawable.getPaint().setColor(getSeparatorColor()); + shapeDrawable.setBounds(middleSeparatorBounds); + // Use original text width if using Rolling Number, + // to ensure the replacement styled span has the same width as the measured String, + // otherwise layout can be broken (especially on devices with small system font sizes). + middleSeparatorSpan.setSpan( + new VerticallyCenteredImageSpan(shapeDrawable, isRollingNumber), + shapeInsertionIndex, shapeInsertionIndex + 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE); + builder.append(middleSeparatorSpan); + + // dislikes + builder.append(newSpannableWithDislikes(oldSpannable, voteData)); + + 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. + */ + public static boolean isPreviouslyCreatedSegmentedSpan(@NonNull String text) { + return text.indexOf(MIDDLE_SEPARATOR_CHARACTER) >= 0; + } + + private static boolean spansHaveEqualTextAndColor(@NonNull Spanned one, @NonNull Spanned two) { + // Cannot use equals on the span, because many of the inner styling spans do not implement equals. + // Instead, compare the underlying text and the text color to handle when dark mode is changed. + // Cannot compare the status of device dark mode, as Litho components are updated just before dark mode status changes. + if (!one.toString().equals(two.toString())) { + return false; + } + ForegroundColorSpan[] oneColors = one.getSpans(0, one.length(), ForegroundColorSpan.class); + ForegroundColorSpan[] twoColors = two.getSpans(0, two.length(), ForegroundColorSpan.class); + final int oneLength = oneColors.length; + if (oneLength != twoColors.length) { + return false; + } + for (int i = 0; i < oneLength; i++) { + if (oneColors[i].getForegroundColor() != twoColors[i].getForegroundColor()) { + return false; + } + } + return true; + } + + private static SpannableString newSpannableWithLikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) { + return newSpanUsingStylingOfAnotherSpan(sourceStyling, formatDislikeCount(voteData.getLikeCount())); + } + + private static SpannableString newSpannableWithDislikes(@NonNull Spanned sourceStyling, @NonNull RYDVoteData voteData) { + return newSpanUsingStylingOfAnotherSpan(sourceStyling, + Settings.RYD_DISLIKE_PERCENTAGE.get() + ? formatDislikePercentage(voteData.getDislikePercentage()) + : formatDislikeCount(voteData.getDislikeCount())); + } + + private static SpannableString newSpanUsingStylingOfAnotherSpan(@NonNull Spanned sourceStyle, @NonNull CharSequence newSpanText) { + if (sourceStyle == newSpanText && sourceStyle instanceof SpannableString spannableString) { + return spannableString; // Nothing to do. + } + + SpannableString destination = new SpannableString(newSpanText); + Object[] spans = sourceStyle.getSpans(0, sourceStyle.length(), Object.class); + for (Object span : spans) { + destination.setSpan(span, 0, destination.length(), sourceStyle.getSpanFlags(span)); + } + + return destination; + } + + private static String formatDislikeCount(long dislikeCount) { + if (isSDKAbove(24)) { + synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize + if (dislikeCountFormatter == null) { + Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().getLocales().get(0); + dislikeCountFormatter = CompactDecimalFormat.getInstance(locale, CompactDecimalFormat.CompactStyle.SHORT); + + // YouTube disregards locale specific number characters + // and instead shows english number characters everywhere. + // To use the same behavior, override the digit characters to use English + // so languages such as Arabic will show "1.234" instead of the native "۱,۲۳٤" + if (isSDKAbove(28)) { + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale); + symbols.setDigitStrings(DecimalFormatSymbols.getInstance(Locale.ENGLISH).getDigitStrings()); + dislikeCountFormatter.setDecimalFormatSymbols(symbols); + } + } + return dislikeCountFormatter.format(dislikeCount); + } + } + + // Will never be reached, as the oldest supported YouTube app requires Android N or greater. + return String.valueOf(dislikeCount); + } + + private static String formatDislikePercentage(float dislikePercentage) { + if (isSDKAbove(24)) { + synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize + if (dislikePercentageFormatter == null) { + Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().getLocales().get(0); + dislikePercentageFormatter = NumberFormat.getPercentInstance(locale); + + // Want to set the digit strings, and the simplest way is to cast to the implementation NumberFormat returns. + if (isSDKAbove(28) && dislikePercentageFormatter instanceof DecimalFormat decimalFormat) { + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale); + symbols.setDigitStrings(DecimalFormatSymbols.getInstance(Locale.ENGLISH).getDigitStrings()); + decimalFormat.setDecimalFormatSymbols(symbols); + } + } + if (dislikePercentage >= 0.01) { // at least 1% + dislikePercentageFormatter.setMaximumFractionDigits(0); // show only whole percentage points + } else { + dislikePercentageFormatter.setMaximumFractionDigits(1); // show up to 1 digit precision + } + return dislikePercentageFormatter.format(dislikePercentage); + } + } + + // Will never be reached, as the oldest supported YouTube app requires Android N or greater. + return String.valueOf((int) (dislikePercentage * 100)); + } + + @NonNull + public static ReturnYouTubeDislike getFetchForVideoId(@Nullable String videoId) { + Objects.requireNonNull(videoId); + synchronized (fetchCache) { + // Remove any expired entries. + final long now = System.currentTimeMillis(); + if (isSDKAbove(24)) { + fetchCache.values().removeIf(value -> { + final boolean expired = value.isExpired(now); + if (expired) + Logger.printDebug(() -> "Removing expired fetch: " + value.videoId); + return expired; + }); + } else { + final Iterator> itr = fetchCache.entrySet().iterator(); + while (itr.hasNext()) { + final Map.Entry entry = itr.next(); + if (entry.getValue().isExpired(now)) { + Logger.printDebug(() -> "Removing expired fetch: " + entry.getValue().videoId); + itr.remove(); + } + } + } + + ReturnYouTubeDislike fetch = fetchCache.get(videoId); + if (fetch == null) { + fetch = new ReturnYouTubeDislike(videoId); + fetchCache.put(videoId, fetch); + } + return fetch; + } + } + + /** + * Should be called if the user changes dislikes appearance settings. + */ + public static void clearAllUICaches() { + synchronized (fetchCache) { + for (ReturnYouTubeDislike fetch : fetchCache.values()) { + fetch.clearUICache(); + } + } + } + + private ReturnYouTubeDislike(@NonNull String videoId) { + this.videoId = Objects.requireNonNull(videoId); + this.timeFetched = System.currentTimeMillis(); + this.future = Utils.submitOnBackgroundThread(() -> ReturnYouTubeDislikeApi.fetchVotes(videoId)); + } + + private boolean isExpired(long now) { + final long timeSinceCreation = now - timeFetched; + if (timeSinceCreation < CACHE_TIMEOUT_FAILURE_MILLISECONDS) { + return false; // Not expired, even if the API call failed. + } + if (timeSinceCreation > CACHE_TIMEOUT_SUCCESS_MILLISECONDS) { + return true; // Always expired. + } + // Only expired if the fetch failed (API null response). + return (!fetchCompleted() || getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH) == null); + } + + @Nullable + public RYDVoteData getFetchData(long maxTimeToWait) { + try { + return future.get(maxTimeToWait, TimeUnit.MILLISECONDS); + } catch (TimeoutException ex) { + Logger.printDebug(() -> "Waited but future was not complete after: " + maxTimeToWait + "ms"); + } catch (ExecutionException | InterruptedException ex) { + Logger.printException(() -> "Future failure ", ex); // will never happen + } + return null; + } + + /** + * @return if the RYD fetch call has completed. + */ + public boolean fetchCompleted() { + return future.isDone(); + } + + private synchronized void clearUICache() { + if (replacementLikeDislikeSpan != null) { + Logger.printDebug(() -> "Clearing replacement span for: " + videoId); + } + replacementLikeDislikeSpan = null; + } + + /** + * Must call off main thread, as this will make a network call if user is not yet registered. + * + * @return ReturnYouTubeDislike user ID. If user registration has never happened + * and the network call fails, this returns NULL. + */ + @Nullable + private static String getUserId() { + Utils.verifyOffMainThread(); + + String userId = Settings.RYD_USER_ID.get(); + if (!userId.isEmpty()) { + return userId; + } + + userId = ReturnYouTubeDislikeApi.registerAsNewUser(); + if (userId != null) { + Settings.RYD_USER_ID.save(userId); + } + return userId; + } + + @NonNull + public String getVideoId() { + return videoId; + } + + /** + * Pre-emptively set this as a Short. + */ + public synchronized void setVideoIdIsShort(boolean isShort) { + this.isShort = isShort; + } + + /** + * @return the replacement span containing dislikes, or the original span if RYD is not available. + */ + @NonNull + public synchronized Spanned getDislikesSpanForRegularVideo(@NonNull Spanned original, + boolean isSegmentedButton, + boolean isRollingNumber) { + return waitForFetchAndUpdateReplacementSpan(original, isSegmentedButton, + isRollingNumber, false, false); + } + + /** + * Called when a Shorts like Spannable is created. + */ + @NonNull + public synchronized Spanned getLikeSpanForShort(@NonNull Spanned original) { + return waitForFetchAndUpdateReplacementSpan(original, false, + false, true, true); + } + + /** + * Called when a Shorts dislike Spannable is created. + */ + @NonNull + public synchronized Spanned getDislikeSpanForShort(@NonNull Spanned original) { + return waitForFetchAndUpdateReplacementSpan(original, false, + false, true, false); + } + + @NonNull + private Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned original, + boolean isSegmentedButton, + boolean isRollingNumber, + boolean spanIsForShort, + boolean spanIsForLikes) { + try { + RYDVoteData votingData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH); + if (votingData == null) { + Logger.printDebug(() -> "Cannot add dislike to UI (RYD data not available)"); + return original; + } + + synchronized (this) { + if (spanIsForShort) { + // Cannot set this to false if span is not for a Short. + // When spoofing to an old version and a Short is opened while a regular video + // is on screen, this instance can be loaded for the minimized regular video. + // But this Shorts data won't be displayed for that call + // and when it is un-minimized it will reload again and the load will be ignored. + isShort = true; + } else if (isShort) { + // user: + // 1, opened a video + // 2. opened a short (without closing the regular video) + // 3. closed the short + // 4. regular video is now present, but the videoId and RYD data is still for the short + Logger.printDebug(() -> "Ignoring regular video dislike span," + + " as data loaded was previously used for a Short: " + videoId); + return original; + } + + // prevents reproducible bugs with the following steps: + // (user is using YouTube with RollingNumber applied) + // 1. opened a video + // 2. switched to fullscreen + // 3. click video's title to open the video description + // 4. dislike count may be replaced in the like count area or view count area of the video description + if (PlayerType.getCurrent().isFullScreenOrSlidingFullScreen()) { + Logger.printDebug(() -> "Ignoring fullscreen video description panel: " + videoId); + return original; + } + + if (spanIsForLikes) { + // Scrolling Shorts does not cause the Spans to be reloaded, + // so there is no need to cache the likes for this situations. + Logger.printDebug(() -> "Creating likes span for: " + votingData.videoId); + return newSpannableWithLikes(original, votingData); + } + + if (originalDislikeSpan != null && replacementLikeDislikeSpan != null + && spansHaveEqualTextAndColor(original, originalDislikeSpan)) { + Logger.printDebug(() -> "Replacing span with previously created dislike span of data: " + videoId); + return replacementLikeDislikeSpan; + } + + // No replacement span exist, create it now. + + if (userVote != null) { + votingData.updateUsingVote(userVote); + } + originalDislikeSpan = original; + replacementLikeDislikeSpan = createDislikeSpan(original, isSegmentedButton, isRollingNumber, votingData); + Logger.printDebug(() -> "Replaced: '" + originalDislikeSpan + "' with: '" + + replacementLikeDislikeSpan + "'" + " using video: " + videoId); + + return replacementLikeDislikeSpan; + } + } catch (Exception ex) { + Logger.printException(() -> "waitForFetchAndUpdateReplacementSpan failure", ex); + } + + return original; + } + + public void sendVote(@NonNull Vote vote) { + Utils.verifyOnMainThread(); + Objects.requireNonNull(vote); + try { + if (isShort != PlayerType.getCurrent().isNoneOrHidden()) { + // Shorts was loaded with regular video present, then Shorts was closed. + // and then user voted on the now visible original video. + // Cannot send a vote, because this instance is for the wrong video. + Utils.showToastLong(str("revanced_ryd_failure_ryd_enabled_while_playing_video_then_user_voted")); + return; + } + + setUserVote(vote); + + voteSerialExecutor.execute(() -> { + try { // Must wrap in try/catch to properly log exceptions. + ReturnYouTubeDislikeApi.sendVote(getUserId(), videoId, vote); + } catch (Exception ex) { + Logger.printException(() -> "Failed to send vote", ex); + } + }); + } catch (Exception ex) { + Logger.printException(() -> "Error trying to send vote", ex); + } + } + + /** + * Sets the current user vote value, and does not send the vote to the RYD API. + *

+ * Only used to set value if thumbs up/down is already selected on video load. + */ + public void setUserVote(@NonNull Vote vote) { + Objects.requireNonNull(vote); + try { + Logger.printDebug(() -> "setUserVote: " + vote); + + synchronized (this) { + userVote = vote; + clearUICache(); + } + + if (future.isDone()) { + // Update the fetched vote data. + RYDVoteData voteData = getFetchData(MAX_MILLISECONDS_TO_BLOCK_UI_WAITING_FOR_FETCH); + if (voteData == null) { + // RYD fetch failed. + Logger.printDebug(() -> "Cannot update UI (vote data not available)"); + return; + } + voteData.updateUsingVote(vote); + } // Else, vote will be applied after fetch completes. + + } catch (Exception ex) { + Logger.printException(() -> "setUserVote failure", ex); + } + } +} + +/** + * Styles a Spannable with an empty fixed width. + */ +class FixedWidthEmptySpan extends ReplacementSpan { + final int fixedWidth; + + /** + * @param fixedWith Fixed width in screen pixels. + */ + FixedWidthEmptySpan(int fixedWith) { + this.fixedWidth = fixedWith; + if (fixedWith < 0) throw new IllegalArgumentException(); + } + + @Override + public int getSize(@NonNull Paint paint, @NonNull CharSequence text, + int start, int end, @Nullable Paint.FontMetricsInt fontMetrics) { + return fixedWidth; + } + + @Override + public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, + float x, int top, int y, int bottom, @NonNull Paint paint) { + // Nothing to draw. + } +} + +/** + * Vertically centers a Spanned Drawable. + */ +class VerticallyCenteredImageSpan extends ImageSpan { + final boolean useOriginalWidth; + + /** + * @param useOriginalWidth Use the original layout width of the text this span is applied to, + * and not the bounds of the Drawable. Drawable is always displayed using it's own bounds, + * and this setting only affects the layout width of the entire span. + */ + public VerticallyCenteredImageSpan(Drawable drawable, boolean useOriginalWidth) { + super(drawable); + this.useOriginalWidth = useOriginalWidth; + } + + @Override + public int getSize(@NonNull Paint paint, @NonNull CharSequence text, + int start, int end, @Nullable Paint.FontMetricsInt fontMetrics) { + Drawable drawable = getDrawable(); + Rect bounds = drawable.getBounds(); + if (fontMetrics != null) { + Paint.FontMetricsInt paintMetrics = paint.getFontMetricsInt(); + final int fontHeight = paintMetrics.descent - paintMetrics.ascent; + final int drawHeight = bounds.bottom - bounds.top; + final int halfDrawHeight = drawHeight / 2; + final int yCenter = paintMetrics.ascent + fontHeight / 2; + + fontMetrics.ascent = yCenter - halfDrawHeight; + fontMetrics.top = fontMetrics.ascent; + fontMetrics.bottom = yCenter + halfDrawHeight; + fontMetrics.descent = fontMetrics.bottom; + } + if (useOriginalWidth) { + return (int) paint.measureText(text, start, end); + } + return bounds.right; + } + + @Override + public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, + float x, int top, int y, int bottom, @NonNull Paint paint) { + Drawable drawable = getDrawable(); + canvas.save(); + Paint.FontMetricsInt paintMetrics = paint.getFontMetricsInt(); + final int fontHeight = paintMetrics.descent - paintMetrics.ascent; + final int yCenter = y + paintMetrics.descent - fontHeight / 2; + final Rect drawBounds = drawable.getBounds(); + float translateX = x; + if (useOriginalWidth) { + // Horizontally center the drawable in the same space as the original text. + translateX += (paint.measureText(text, start, end) - (drawBounds.right - drawBounds.left)) / 2; + } + final int translateY = yCenter - (drawBounds.bottom - drawBounds.top) / 2; + canvas.translate(translateX, translateY); + drawable.draw(canvas); + canvas.restore(); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java new file mode 100644 index 000000000..6c06ba989 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -0,0 +1,680 @@ +package app.revanced.extension.youtube.settings; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static app.revanced.extension.shared.settings.Setting.migrateFromOldPreferences; +import static app.revanced.extension.shared.settings.Setting.parent; +import static app.revanced.extension.shared.settings.Setting.parentsAny; +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType; +import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_1; +import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_2; +import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_3; +import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_4; +import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.MANUAL_SKIP; +import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY; +import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.settings.EnumSetting; +import app.revanced.extension.shared.settings.FloatSetting; +import app.revanced.extension.shared.settings.IntegerSetting; +import app.revanced.extension.shared.settings.LongSetting; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.settings.StringSetting; +import app.revanced.extension.shared.settings.preference.SharedPrefCategory; +import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.DeArrowAvailability; +import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.StillImagesAvailability; +import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailOption; +import app.revanced.extension.youtube.patches.alternativethumbnails.AlternativeThumbnailsPatch.ThumbnailStillTime; +import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch; +import app.revanced.extension.youtube.patches.general.ChangeStartPagePatch.StartPage; +import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch.FormFactor; +import app.revanced.extension.youtube.patches.general.MiniplayerPatch; +import app.revanced.extension.youtube.patches.general.YouTubeMusicActionsPatch; +import app.revanced.extension.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType; +import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType; +import app.revanced.extension.youtube.patches.shorts.ShortsRepeatStatePatch.ShortsLoopBehavior; +import app.revanced.extension.youtube.patches.utils.PatchStatus; +import app.revanced.extension.youtube.shared.PlaylistIdPrefix; +import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings; + +@SuppressWarnings("unused") +public class Settings extends BaseSettings { + // PreferenceScreen: Ads + public static final BooleanSetting HIDE_GENERAL_ADS = new BooleanSetting("revanced_hide_general_ads", TRUE); + public static final BooleanSetting HIDE_GET_PREMIUM = new BooleanSetting("revanced_hide_get_premium", TRUE, true); + public static final BooleanSetting HIDE_MERCHANDISE_SHELF = new BooleanSetting("revanced_hide_merchandise_shelf", TRUE); + public static final BooleanSetting HIDE_PLAYER_STORE_SHELF = new BooleanSetting("revanced_hide_player_store_shelf", TRUE); + public static final BooleanSetting HIDE_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_paid_promotion_label", TRUE); + public static final BooleanSetting HIDE_SELF_SPONSOR_CARDS = new BooleanSetting("revanced_hide_self_sponsor_cards", TRUE); + public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_hide_video_ads", TRUE, true); + public static final BooleanSetting HIDE_VIEW_PRODUCTS = new BooleanSetting("revanced_hide_view_products", TRUE); + public static final BooleanSetting HIDE_WEB_SEARCH_RESULTS = new BooleanSetting("revanced_hide_web_search_results", TRUE); + + + // PreferenceScreen: Alternative Thumbnails + public static final EnumSetting ALT_THUMBNAIL_HOME = new EnumSetting<>("revanced_alt_thumbnail_home", ThumbnailOption.ORIGINAL); + public static final EnumSetting ALT_THUMBNAIL_SUBSCRIPTIONS = new EnumSetting<>("revanced_alt_thumbnail_subscriptions", ThumbnailOption.ORIGINAL); + public static final EnumSetting ALT_THUMBNAIL_LIBRARY = new EnumSetting<>("revanced_alt_thumbnail_library", ThumbnailOption.ORIGINAL); + public static final EnumSetting ALT_THUMBNAIL_PLAYER = new EnumSetting<>("revanced_alt_thumbnail_player", ThumbnailOption.ORIGINAL); + public static final EnumSetting ALT_THUMBNAIL_SEARCH = new EnumSetting<>("revanced_alt_thumbnail_search", ThumbnailOption.ORIGINAL); + public static final StringSetting ALT_THUMBNAIL_DEARROW_API_URL = new StringSetting("revanced_alt_thumbnail_dearrow_api_url", + "https://dearrow-thumb.ajay.app/api/v1/getThumbnail", true, new DeArrowAvailability()); + public static final BooleanSetting ALT_THUMBNAIL_DEARROW_CONNECTION_TOAST = new BooleanSetting("revanced_alt_thumbnail_dearrow_connection_toast", FALSE, new DeArrowAvailability()); + public static final EnumSetting ALT_THUMBNAIL_STILLS_TIME = new EnumSetting<>("revanced_alt_thumbnail_stills_time", ThumbnailStillTime.MIDDLE, new StillImagesAvailability()); + public static final BooleanSetting ALT_THUMBNAIL_STILLS_FAST = new BooleanSetting("revanced_alt_thumbnail_stills_fast", FALSE, new StillImagesAvailability()); + + + // PreferenceScreen: Feed + public static final BooleanSetting HIDE_ALBUM_CARDS = new BooleanSetting("revanced_hide_album_card", TRUE); + public static final BooleanSetting HIDE_CAROUSEL_SHELF = new BooleanSetting("revanced_hide_carousel_shelf", FALSE, true); + public static final BooleanSetting HIDE_CHIPS_SHELF = new BooleanSetting("revanced_hide_chips_shelf", TRUE); + public static final BooleanSetting HIDE_EXPANDABLE_CHIP = new BooleanSetting("revanced_hide_expandable_chip", TRUE); + public static final BooleanSetting HIDE_EXPANDABLE_SHELF = new BooleanSetting("revanced_hide_expandable_shelf", TRUE); + public static final BooleanSetting HIDE_FEED_CAPTIONS_BUTTON = new BooleanSetting("revanced_hide_feed_captions_button", FALSE, true); + public static final BooleanSetting HIDE_FEED_SEARCH_BAR = new BooleanSetting("revanced_hide_feed_search_bar", FALSE); + public static final BooleanSetting HIDE_FEED_SURVEY = new BooleanSetting("revanced_hide_feed_survey", TRUE); + public static final BooleanSetting HIDE_FLOATING_BUTTON = new BooleanSetting("revanced_hide_floating_button", FALSE, true); + public static final BooleanSetting HIDE_IMAGE_SHELF = new BooleanSetting("revanced_hide_image_shelf", TRUE); + public static final BooleanSetting HIDE_LATEST_POSTS = new BooleanSetting("revanced_hide_latest_posts", TRUE); + public static final BooleanSetting HIDE_LATEST_VIDEOS_BUTTON = new BooleanSetting("revanced_hide_latest_videos_button", TRUE); + public static final BooleanSetting HIDE_MIX_PLAYLISTS = new BooleanSetting("revanced_hide_mix_playlists", FALSE); + public static final BooleanSetting HIDE_MOVIE_SHELF = new BooleanSetting("revanced_hide_movie_shelf", FALSE); + public static final BooleanSetting HIDE_NOTIFY_ME_BUTTON = new BooleanSetting("revanced_hide_notify_me_button", FALSE); + public static final BooleanSetting HIDE_PLAYABLES = new BooleanSetting("revanced_hide_playables", TRUE); + public static final BooleanSetting HIDE_SHOW_MORE_BUTTON = new BooleanSetting("revanced_hide_show_more_button", TRUE, true); + public static final BooleanSetting HIDE_SUBSCRIPTIONS_CAROUSEL = new BooleanSetting("revanced_hide_subscriptions_carousel", FALSE, true); + public static final BooleanSetting HIDE_TICKET_SHELF = new BooleanSetting("revanced_hide_ticket_shelf", TRUE); + + + // PreferenceScreen: Feed - Category bar + public static final BooleanSetting HIDE_CATEGORY_BAR_IN_FEED = new BooleanSetting("revanced_hide_category_bar_in_feed", FALSE, true); + public static final BooleanSetting HIDE_CATEGORY_BAR_IN_SEARCH = new BooleanSetting("revanced_hide_category_bar_in_search", FALSE, true); + public static final BooleanSetting HIDE_CATEGORY_BAR_IN_RELATED_VIDEOS = new BooleanSetting("revanced_hide_category_bar_in_related_videos", FALSE, true); + + // PreferenceScreen: Feed - Channel profile + public static final BooleanSetting HIDE_CHANNEL_TAB = new BooleanSetting("revanced_hide_channel_tab", FALSE); + public static final StringSetting HIDE_CHANNEL_TAB_FILTER_STRINGS = new StringSetting("revanced_hide_channel_tab_filter_strings", "", true, parent(HIDE_CHANNEL_TAB)); + public static final BooleanSetting HIDE_BROWSE_STORE_BUTTON = new BooleanSetting("revanced_hide_browse_store_button", TRUE); + public static final BooleanSetting HIDE_CHANNEL_MEMBER_SHELF = new BooleanSetting("revanced_hide_channel_member_shelf", TRUE); + public static final BooleanSetting HIDE_CHANNEL_PROFILE_LINKS = new BooleanSetting("revanced_hide_channel_profile_links", TRUE); + public static final BooleanSetting HIDE_FOR_YOU_SHELF = new BooleanSetting("revanced_hide_for_you_shelf", TRUE); + + // PreferenceScreen: Feed - Community posts + public static final BooleanSetting HIDE_COMMUNITY_POSTS_CHANNEL = new BooleanSetting("revanced_hide_community_posts_channel", FALSE); + public static final BooleanSetting HIDE_COMMUNITY_POSTS_HOME_RELATED_VIDEOS = new BooleanSetting("revanced_hide_community_posts_home_related_videos", TRUE); + public static final BooleanSetting HIDE_COMMUNITY_POSTS_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_community_posts_subscriptions", FALSE); + + // PreferenceScreen: Feed - Flyout menu + public static final BooleanSetting HIDE_FEED_FLYOUT_MENU = new BooleanSetting("revanced_hide_feed_flyout_menu", FALSE); + public static final StringSetting HIDE_FEED_FLYOUT_MENU_FILTER_STRINGS = new StringSetting("revanced_hide_feed_flyout_menu_filter_strings", "", true, parent(HIDE_FEED_FLYOUT_MENU)); + + // PreferenceScreen: Feed - Video filter + public static final BooleanSetting HIDE_KEYWORD_CONTENT_HOME = new BooleanSetting("revanced_hide_keyword_content_home", FALSE); + public static final BooleanSetting HIDE_KEYWORD_CONTENT_SEARCH = new BooleanSetting("revanced_hide_keyword_content_search", FALSE); + public static final BooleanSetting HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_keyword_content_subscriptions", FALSE); + public static final BooleanSetting HIDE_KEYWORD_CONTENT_COMMENTS = new BooleanSetting("revanced_hide_keyword_content_comments", FALSE); + public static final StringSetting HIDE_KEYWORD_CONTENT_PHRASES = new StringSetting("revanced_hide_keyword_content_phrases", "", + parentsAny(HIDE_KEYWORD_CONTENT_HOME, HIDE_KEYWORD_CONTENT_SEARCH, HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS, HIDE_KEYWORD_CONTENT_COMMENTS)); + + public static final BooleanSetting HIDE_RECOMMENDED_VIDEO = new BooleanSetting("revanced_hide_recommended_video", FALSE); + public static final BooleanSetting HIDE_LOW_VIEWS_VIDEO = new BooleanSetting("revanced_hide_low_views_video", FALSE); + + public static final BooleanSetting HIDE_VIDEO_BY_VIEW_COUNTS_HOME = new BooleanSetting("revanced_hide_video_by_view_counts_home", FALSE); + public static final BooleanSetting HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH = new BooleanSetting("revanced_hide_video_by_view_counts_search", FALSE); + public static final BooleanSetting HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_video_by_view_counts_subscriptions", FALSE); + public static final LongSetting HIDE_VIDEO_VIEW_COUNTS_LESS_THAN = new LongSetting("revanced_hide_video_view_counts_less_than", 1000L, + parentsAny(HIDE_VIDEO_BY_VIEW_COUNTS_HOME, HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH, HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS)); + public static final LongSetting HIDE_VIDEO_VIEW_COUNTS_GREATER_THAN = new LongSetting("revanced_hide_video_view_counts_greater_than", 1_000_000_000_000L, + parentsAny(HIDE_VIDEO_BY_VIEW_COUNTS_HOME, HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH, HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS)); + public static final StringSetting HIDE_VIDEO_VIEW_COUNTS_MULTIPLIER = new StringSetting("revanced_hide_video_view_counts_multiplier", str("revanced_hide_video_view_counts_multiplier_default_value"), true, + parentsAny(HIDE_VIDEO_BY_VIEW_COUNTS_HOME, HIDE_VIDEO_BY_VIEW_COUNTS_SEARCH, HIDE_VIDEO_BY_VIEW_COUNTS_SUBSCRIPTIONS)); + + // Experimental Flags + public static final BooleanSetting HIDE_RELATED_VIDEOS = new BooleanSetting("revanced_hide_related_videos", FALSE, true, "revanced_hide_related_videos_user_dialog_message"); + public static final IntegerSetting RELATED_VIDEOS_OFFSET = new IntegerSetting("revanced_related_videos_offset", 2, true, parent(HIDE_RELATED_VIDEOS)); + + + // PreferenceScreen: General + public static final EnumSetting CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.ORIGINAL, true); + public static final BooleanSetting CHANGE_START_PAGE_TYPE = new BooleanSetting("revanced_change_start_page_type", FALSE, true, + new ChangeStartPagePatch.ChangeStartPageTypeAvailability()); + public static final BooleanSetting DISABLE_AUTO_AUDIO_TRACKS = new BooleanSetting("revanced_disable_auto_audio_tracks", FALSE); + public static final BooleanSetting DISABLE_SPLASH_ANIMATION = new BooleanSetting("revanced_disable_splash_animation", FALSE, true); + public static final BooleanSetting ENABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("revanced_enable_translucent_status_bar", FALSE, true); + public static final BooleanSetting DISABLE_TRANSLUCENT_STATUS_BAR = new BooleanSetting("revanced_disable_translucent_status_bar", FALSE, true); + public static final BooleanSetting ENABLE_GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_enable_gradient_loading_screen", FALSE, true); + public static final BooleanSetting HIDE_FLOATING_MICROPHONE = new BooleanSetting("revanced_hide_floating_microphone", TRUE, true); + public static final BooleanSetting HIDE_GRAY_SEPARATOR = new BooleanSetting("revanced_hide_gray_separator", TRUE); + public static final BooleanSetting HIDE_SNACK_BAR = new BooleanSetting("revanced_hide_snack_bar", FALSE); + public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE); + + public static final EnumSetting CHANGE_LAYOUT = new EnumSetting<>("revanced_change_layout", FormFactor.ORIGINAL, true); + public static final BooleanSetting SPOOF_APP_VERSION = new BooleanSetting("revanced_spoof_app_version", false, true, "revanced_spoof_app_version_user_dialog_message"); + public static final StringSetting SPOOF_APP_VERSION_TARGET = new StringSetting("revanced_spoof_app_version_target", "18.17.43", true, parent(SPOOF_APP_VERSION)); + + // PreferenceScreen: General - Account menu + public static final BooleanSetting HIDE_ACCOUNT_MENU = new BooleanSetting("revanced_hide_account_menu", FALSE); + public static final StringSetting HIDE_ACCOUNT_MENU_FILTER_STRINGS = new StringSetting("revanced_hide_account_menu_filter_strings", "", true, parent(HIDE_ACCOUNT_MENU)); + public static final BooleanSetting HIDE_HANDLE = new BooleanSetting("revanced_hide_handle", TRUE, true); + + // PreferenceScreen: General - Custom filter + public static final BooleanSetting CUSTOM_FILTER = new BooleanSetting("revanced_custom_filter", FALSE); + public static final StringSetting CUSTOM_FILTER_STRINGS = new StringSetting("revanced_custom_filter_strings", "", true, parent(CUSTOM_FILTER)); + + // PreferenceScreen: General - Miniplayer + public static final EnumSetting MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.ORIGINAL, true); + private static final Setting.Availability MINIPLAYER_ANY_MODERN = MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3, MODERN_4); + public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_double_tap_action", TRUE, true, MINIPLAYER_ANY_MODERN); + public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_drag_and_drop", TRUE, true, MINIPLAYER_ANY_MODERN); + public static final BooleanSetting MINIPLAYER_HORIZONTAL_DRAG = new BooleanSetting("revanced_miniplayer_horizontal_drag", FALSE, true, new MiniplayerPatch.MiniplayerHorizontalDragAvailability()); + public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true, new MiniplayerPatch.MiniplayerHideExpandCloseAvailability()); + public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3)); + public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", TRUE, true, MINIPLAYER_TYPE.availability(MODERN_1)); + public static final BooleanSetting MINIPLAYER_ROUNDED_CORNERS = new BooleanSetting("revanced_miniplayer_rounded_corners", TRUE, true, MINIPLAYER_ANY_MODERN); + public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, MINIPLAYER_ANY_MODERN); + public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, MINIPLAYER_TYPE.availability(MODERN_1)); + + // PreferenceScreen: General - Navigation bar + public static final BooleanSetting ENABLE_NARROW_NAVIGATION_BUTTONS = new BooleanSetting("revanced_enable_narrow_navigation_buttons", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_CREATE_BUTTON = new BooleanSetting("revanced_hide_navigation_create_button", TRUE, true); + public static final BooleanSetting HIDE_NAVIGATION_HOME_BUTTON = new BooleanSetting("revanced_hide_navigation_home_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_LIBRARY_BUTTON = new BooleanSetting("revanced_hide_navigation_library_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_hide_navigation_notifications_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_SHORTS_BUTTON = new BooleanSetting("revanced_hide_navigation_shorts_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_SUBSCRIPTIONS_BUTTON = new BooleanSetting("revanced_hide_navigation_subscriptions_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_LABEL = new BooleanSetting("revanced_hide_navigation_label", FALSE, true); + public static final BooleanSetting SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_switch_create_with_notifications_button", TRUE, true, "revanced_switch_create_with_notifications_button_user_dialog_message"); + public static final BooleanSetting ENABLE_TRANSLUCENT_NAVIGATION_BAR = new BooleanSetting("revanced_enable_translucent_navigation_bar", FALSE, true); + public static final BooleanSetting DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT = new BooleanSetting("revanced_disable_translucent_navigation_bar_light", FALSE, true); + public static final BooleanSetting DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK = new BooleanSetting("revanced_disable_translucent_navigation_bar_dark", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_hide_navigation_bar", FALSE, true); + + // PreferenceScreen: General - Override buttons + public static final BooleanSetting OVERRIDE_VIDEO_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_video_download_button", FALSE); + public static final BooleanSetting OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON = new BooleanSetting("revanced_override_playlist_download_button", FALSE); + public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME_VIDEO = new StringSetting("revanced_external_downloader_package_name_video", "com.deniscerri.ytdl"); + public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME_VIDEO_LONG_PRESS = new StringSetting("revanced_external_downloader_package_name_video_long_press", "com.junkfood.seal"); + public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME_PLAYLIST = new StringSetting("revanced_external_downloader_package_name_playlist", "com.deniscerri.ytdl"); + public static final BooleanSetting OVERRIDE_YOUTUBE_MUSIC_BUTTON = new BooleanSetting("revanced_override_youtube_music_button", FALSE, true + , new YouTubeMusicActionsPatch.HookYouTubeMusicAvailability()); + public static final StringSetting THIRD_PARTY_YOUTUBE_MUSIC_PACKAGE_NAME = new StringSetting("revanced_third_party_youtube_music_package_name", PatchStatus.RVXMusicPackageName(), true + , new YouTubeMusicActionsPatch.HookYouTubeMusicPackageNameAvailability()); + + // PreferenceScreen: General - Settings menu + public static final BooleanSetting HIDE_SETTINGS_MENU_PARENT_TOOLS = new BooleanSetting("revanced_hide_settings_menu_parent_tools", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_GENERAL = new BooleanSetting("revanced_hide_settings_menu_general", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_ACCOUNT = new BooleanSetting("revanced_hide_settings_menu_account", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_DATA_SAVING = new BooleanSetting("revanced_hide_settings_menu_data_saving", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_AUTOPLAY = new BooleanSetting("revanced_hide_settings_menu_auto_play", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_VIDEO_QUALITY_PREFERENCES = new BooleanSetting("revanced_hide_settings_menu_video_quality", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_OFFLINE = new BooleanSetting("revanced_hide_settings_menu_offline", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_WATCH_ON_TV = new BooleanSetting("revanced_hide_settings_menu_pair_with_tv", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_MANAGE_ALL_HISTORY = new BooleanSetting("revanced_hide_settings_menu_history", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_YOUR_DATA_IN_YOUTUBE = new BooleanSetting("revanced_hide_settings_menu_your_data", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_PRIVACY = new BooleanSetting("revanced_hide_settings_menu_privacy", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_TRY_EXPERIMENTAL_NEW_FEATURES = new BooleanSetting("revanced_hide_settings_menu_premium_early_access", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_PURCHASES_AND_MEMBERSHIPS = new BooleanSetting("revanced_hide_settings_menu_subscription_product", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_BILLING_AND_PAYMENTS = new BooleanSetting("revanced_hide_settings_menu_billing_and_payment", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_NOTIFICATIONS = new BooleanSetting("revanced_hide_settings_menu_notification", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_CONNECTED_APPS = new BooleanSetting("revanced_hide_settings_menu_connected_accounts", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_LIVE_CHAT = new BooleanSetting("revanced_hide_settings_menu_live_chat", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_CAPTIONS = new BooleanSetting("revanced_hide_settings_menu_captions", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_ACCESSIBILITY = new BooleanSetting("revanced_hide_settings_menu_accessibility", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_ABOUT = new BooleanSetting("revanced_hide_settings_menu_about", FALSE, true); + // dummy data + public static final BooleanSetting HIDE_SETTINGS_MENU_YOUTUBE_TV = new BooleanSetting("revanced_hide_settings_menu_youtube_tv", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_PRE_PURCHASE = new BooleanSetting("revanced_hide_settings_menu_pre_purchase", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_POST_PURCHASE = new BooleanSetting("revanced_hide_settings_menu_post_purchase", FALSE, true); + public static final BooleanSetting HIDE_SETTINGS_MENU_THIRD_PARTY = new BooleanSetting("revanced_hide_settings_menu_third_party", FALSE, true); + + // PreferenceScreen: General - Toolbar + public static final BooleanSetting CHANGE_YOUTUBE_HEADER = new BooleanSetting("revanced_change_youtube_header", TRUE, true); + public static final BooleanSetting ENABLE_WIDE_SEARCH_BAR = new BooleanSetting("revanced_enable_wide_search_bar", FALSE, true); + public static final BooleanSetting ENABLE_WIDE_SEARCH_BAR_WITH_HEADER = new BooleanSetting("revanced_enable_wide_search_bar_with_header", TRUE, true); + public static final BooleanSetting ENABLE_WIDE_SEARCH_BAR_IN_YOU_TAB = new BooleanSetting("revanced_enable_wide_search_bar_in_you_tab", FALSE, true); + public static final BooleanSetting HIDE_TOOLBAR_CAST_BUTTON = new BooleanSetting("revanced_hide_toolbar_cast_button", TRUE, true); + public static final BooleanSetting HIDE_TOOLBAR_CREATE_BUTTON = new BooleanSetting("revanced_hide_toolbar_create_button", FALSE, true); + public static final BooleanSetting HIDE_TOOLBAR_NOTIFICATION_BUTTON = new BooleanSetting("revanced_hide_toolbar_notification_button", FALSE, true); + public static final BooleanSetting HIDE_SEARCH_TERM_THUMBNAIL = new BooleanSetting("revanced_hide_search_term_thumbnail", FALSE); + public static final BooleanSetting HIDE_IMAGE_SEARCH_BUTTON = new BooleanSetting("revanced_hide_image_search_button", FALSE, true); + public static final BooleanSetting HIDE_VOICE_SEARCH_BUTTON = new BooleanSetting("revanced_hide_voice_search_button", FALSE, true); + public static final BooleanSetting HIDE_YOUTUBE_DOODLES = new BooleanSetting("revanced_hide_youtube_doodles", FALSE, true, "revanced_hide_youtube_doodles_user_dialog_message"); + public static final BooleanSetting REPLACE_TOOLBAR_CREATE_BUTTON = new BooleanSetting("revanced_replace_toolbar_create_button", FALSE, true); + public static final BooleanSetting REPLACE_TOOLBAR_CREATE_BUTTON_TYPE = new BooleanSetting("revanced_replace_toolbar_create_button_type", FALSE, true); + + + // PreferenceScreen: Player + public static final IntegerSetting CUSTOM_PLAYER_OVERLAY_OPACITY = new IntegerSetting("revanced_custom_player_overlay_opacity", 100, true); + public static final BooleanSetting DISABLE_AUTO_PLAYER_POPUP_PANELS = new BooleanSetting("revanced_disable_auto_player_popup_panels", TRUE, true); + public static final BooleanSetting DISABLE_AUTO_SWITCH_MIX_PLAYLISTS = new BooleanSetting("revanced_disable_auto_switch_mix_playlists", FALSE, true, "revanced_disable_auto_switch_mix_playlists_user_dialog_message"); + public static final BooleanSetting DISABLE_SPEED_OVERLAY = new BooleanSetting("revanced_disable_speed_overlay", FALSE, true); + public static final FloatSetting SPEED_OVERLAY_VALUE = new FloatSetting("revanced_speed_overlay_value", 2.0f, true); + public static final BooleanSetting HIDE_CHANNEL_WATERMARK = new BooleanSetting("revanced_hide_channel_watermark", TRUE); + public static final BooleanSetting HIDE_CROWDFUNDING_BOX = new BooleanSetting("revanced_hide_crowdfunding_box", TRUE, true); + public static final BooleanSetting HIDE_DOUBLE_TAP_OVERLAY_FILTER = new BooleanSetting("revanced_hide_double_tap_overlay_filter", FALSE, true); + public static final BooleanSetting HIDE_END_SCREEN_CARDS = new BooleanSetting("revanced_hide_end_screen_cards", FALSE, true); + public static final BooleanSetting HIDE_FILMSTRIP_OVERLAY = new BooleanSetting("revanced_hide_filmstrip_overlay", FALSE, true); + public static final BooleanSetting HIDE_INFO_CARDS = new BooleanSetting("revanced_hide_info_cards", FALSE, true); + public static final BooleanSetting HIDE_INFO_PANEL = new BooleanSetting("revanced_hide_info_panel", TRUE); + public static final BooleanSetting HIDE_LIVE_CHAT_SUMMARY = new BooleanSetting("revanced_hide_live_chat_summary", FALSE); + public static final BooleanSetting HIDE_LIVE_CHAT_MESSAGES = new BooleanSetting("revanced_hide_live_chat_messages", FALSE); + public static final BooleanSetting HIDE_MEDICAL_PANEL = new BooleanSetting("revanced_hide_medical_panel", TRUE); + public static final BooleanSetting HIDE_SEEK_MESSAGE = new BooleanSetting("revanced_hide_seek_message", FALSE, true); + public static final BooleanSetting HIDE_SEEK_UNDO_MESSAGE = new BooleanSetting("revanced_hide_seek_undo_message", FALSE, true); + public static final BooleanSetting HIDE_SUGGESTED_ACTION = new BooleanSetting("revanced_hide_suggested_actions", TRUE, true); + public static final BooleanSetting HIDE_TIMED_REACTIONS = new BooleanSetting("revanced_hide_timed_reactions", TRUE); + public static final BooleanSetting HIDE_SUGGESTED_VIDEO_END_SCREEN = new BooleanSetting("revanced_hide_suggested_video_end_screen", TRUE, true); + public static final BooleanSetting SKIP_AUTOPLAY_COUNTDOWN = new BooleanSetting("revanced_skip_autoplay_countdown", FALSE, true, parent(HIDE_SUGGESTED_VIDEO_END_SCREEN)); + public static final BooleanSetting HIDE_ZOOM_OVERLAY = new BooleanSetting("revanced_hide_zoom_overlay", FALSE, true); + public static final BooleanSetting SANITIZE_VIDEO_SUBTITLE = new BooleanSetting("revanced_sanitize_video_subtitle", FALSE); + + + // PreferenceScreen: Player - Action buttons + public static final BooleanSetting DISABLE_LIKE_DISLIKE_GLOW = new BooleanSetting("revanced_disable_like_dislike_glow", FALSE); + public static final BooleanSetting HIDE_CLIP_BUTTON = new BooleanSetting("revanced_hide_clip_button", FALSE); + public static final BooleanSetting HIDE_DOWNLOAD_BUTTON = new BooleanSetting("revanced_hide_download_button", FALSE); + public static final BooleanSetting HIDE_LIKE_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_like_dislike_button", FALSE); + public static final BooleanSetting HIDE_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_playlist_button", FALSE); + public static final BooleanSetting HIDE_REMIX_BUTTON = new BooleanSetting("revanced_hide_remix_button", FALSE); + public static final BooleanSetting HIDE_REWARDS_BUTTON = new BooleanSetting("revanced_hide_rewards_button", FALSE); + public static final BooleanSetting HIDE_REPORT_BUTTON = new BooleanSetting("revanced_hide_report_button", FALSE); + public static final BooleanSetting HIDE_SHARE_BUTTON = new BooleanSetting("revanced_hide_share_button", FALSE); + public static final BooleanSetting HIDE_SHOP_BUTTON = new BooleanSetting("revanced_hide_shop_button", FALSE); + public static final BooleanSetting HIDE_THANKS_BUTTON = new BooleanSetting("revanced_hide_thanks_button", FALSE); + + // PreferenceScreen: Player - Ambient mode + public static final BooleanSetting BYPASS_AMBIENT_MODE_RESTRICTIONS = new BooleanSetting("revanced_bypass_ambient_mode_restrictions", FALSE); + public static final BooleanSetting DISABLE_AMBIENT_MODE = new BooleanSetting("revanced_disable_ambient_mode", FALSE, true); + public static final BooleanSetting DISABLE_AMBIENT_MODE_IN_FULLSCREEN = new BooleanSetting("revanced_disable_ambient_mode_in_fullscreen", FALSE, true); + + // PreferenceScreen: Player - Channel bar + public static final BooleanSetting HIDE_JOIN_BUTTON = new BooleanSetting("revanced_hide_join_button", TRUE); + public static final BooleanSetting HIDE_START_TRIAL_BUTTON = new BooleanSetting("revanced_hide_start_trial_button", TRUE); + + // PreferenceScreen: Player - Comments + public static final BooleanSetting HIDE_CHANNEL_GUIDELINES = new BooleanSetting("revanced_hide_channel_guidelines", TRUE); + public static final BooleanSetting HIDE_COMMENTS_BY_MEMBERS = new BooleanSetting("revanced_hide_comments_by_members", FALSE); + public static final BooleanSetting HIDE_COMMENT_HIGHLIGHTED_SEARCH_LINKS = new BooleanSetting("revanced_hide_comment_highlighted_search_links", FALSE, true); + public static final BooleanSetting HIDE_COMMENTS_SECTION = new BooleanSetting("revanced_hide_comments_section", FALSE); + public static final BooleanSetting HIDE_COMMENTS_SECTION_IN_HOME_FEED = new BooleanSetting("revanced_hide_comments_section_in_home_feed", FALSE); + public static final BooleanSetting HIDE_PREVIEW_COMMENT = new BooleanSetting("revanced_hide_preview_comment", FALSE); + public static final BooleanSetting HIDE_PREVIEW_COMMENT_TYPE = new BooleanSetting("revanced_hide_preview_comment_type", FALSE); + public static final BooleanSetting HIDE_PREVIEW_COMMENT_OLD_METHOD = new BooleanSetting("revanced_hide_preview_comment_old_method", FALSE); + public static final BooleanSetting HIDE_PREVIEW_COMMENT_NEW_METHOD = new BooleanSetting("revanced_hide_preview_comment_new_method", FALSE); + public static final BooleanSetting HIDE_COMMENT_CREATE_SHORTS_BUTTON = new BooleanSetting("revanced_hide_comment_create_shorts_button", FALSE); + public static final BooleanSetting HIDE_COMMENT_THANKS_BUTTON = new BooleanSetting("revanced_hide_comment_thanks_button", FALSE, true); + public static final BooleanSetting HIDE_COMMENT_TIMESTAMP_AND_EMOJI_BUTTONS = new BooleanSetting("revanced_hide_comment_timestamp_and_emoji_buttons", FALSE); + + // PreferenceScreen: Player - Flyout menu + public static final BooleanSetting CHANGE_PLAYER_FLYOUT_MENU_TOGGLE = new BooleanSetting("revanced_change_player_flyout_menu_toggle", FALSE, true); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_ENHANCED_BITRATE = new BooleanSetting("revanced_hide_player_flyout_menu_enhanced_bitrate", TRUE, true); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_AUDIO_TRACK = new BooleanSetting("revanced_hide_player_flyout_menu_audio_track", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_CAPTIONS = new BooleanSetting("revanced_hide_player_flyout_menu_captions", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_CAPTIONS_FOOTER = new BooleanSetting("revanced_hide_player_flyout_menu_captions_footer", TRUE, true); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_LOCK_SCREEN = new BooleanSetting("revanced_hide_player_flyout_menu_lock_screen", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_MORE = new BooleanSetting("revanced_hide_player_flyout_menu_more_info", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_PLAYBACK_SPEED = new BooleanSetting("revanced_hide_player_flyout_menu_playback_speed", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_QUALITY_HEADER = new BooleanSetting("revanced_hide_player_flyout_menu_quality_header", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_QUALITY_FOOTER = new BooleanSetting("revanced_hide_player_flyout_menu_quality_footer", TRUE, true); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_REPORT = new BooleanSetting("revanced_hide_player_flyout_menu_report", TRUE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_SLEEP_TIMER = new BooleanSetting("revanced_hide_player_flyout_menu_sleep_timer", FALSE); + + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_ADDITIONAL_SETTINGS = new BooleanSetting("revanced_hide_player_flyout_menu_additional_settings", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_AMBIENT = new BooleanSetting("revanced_hide_player_flyout_menu_ambient_mode", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_HELP = new BooleanSetting("revanced_hide_player_flyout_menu_help", TRUE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_LOOP = new BooleanSetting("revanced_hide_player_flyout_menu_loop_video", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_PIP = new BooleanSetting("revanced_hide_player_flyout_menu_pip", TRUE, true); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_PREMIUM_CONTROLS = new BooleanSetting("revanced_hide_player_flyout_menu_premium_controls", TRUE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_STABLE_VOLUME = new BooleanSetting("revanced_hide_player_flyout_menu_stable_volume", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_STATS_FOR_NERDS = new BooleanSetting("revanced_hide_player_flyout_menu_stats_for_nerds", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_WATCH_IN_VR = new BooleanSetting("revanced_hide_player_flyout_menu_watch_in_vr", TRUE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MENU_YT_MUSIC = new BooleanSetting("revanced_hide_player_flyout_menu_listen_with_youtube_music", TRUE); + + // PreferenceScreen: Player - Fullscreen + public static final BooleanSetting DISABLE_ENGAGEMENT_PANEL = new BooleanSetting("revanced_disable_engagement_panel", FALSE, true); + public static final BooleanSetting SHOW_VIDEO_TITLE_SECTION = new BooleanSetting("revanced_show_video_title_section", TRUE, true, parent(DISABLE_ENGAGEMENT_PANEL)); + public static final BooleanSetting HIDE_AUTOPLAY_PREVIEW = new BooleanSetting("revanced_hide_autoplay_preview", FALSE, true); + public static final BooleanSetting HIDE_LIVE_CHAT_REPLAY_BUTTON = new BooleanSetting("revanced_hide_live_chat_replay_button", FALSE); + public static final BooleanSetting HIDE_RELATED_VIDEO_OVERLAY = new BooleanSetting("revanced_hide_related_video_overlay", FALSE, true); + + public static final BooleanSetting HIDE_QUICK_ACTIONS = new BooleanSetting("revanced_hide_quick_actions", FALSE, true); + public static final BooleanSetting HIDE_QUICK_ACTIONS_COMMENT_BUTTON = new BooleanSetting("revanced_hide_quick_actions_comment_button", FALSE); + public static final BooleanSetting HIDE_QUICK_ACTIONS_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_quick_actions_dislike_button", FALSE); + public static final BooleanSetting HIDE_QUICK_ACTIONS_LIKE_BUTTON = new BooleanSetting("revanced_hide_quick_actions_like_button", FALSE); + public static final BooleanSetting HIDE_QUICK_ACTIONS_LIVE_CHAT_BUTTON = new BooleanSetting("revanced_hide_quick_actions_live_chat_button", FALSE); + public static final BooleanSetting HIDE_QUICK_ACTIONS_MORE_BUTTON = new BooleanSetting("revanced_hide_quick_actions_more_button", FALSE); + public static final BooleanSetting HIDE_QUICK_ACTIONS_OPEN_MIX_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_quick_actions_open_mix_playlist_button", FALSE); + public static final BooleanSetting HIDE_QUICK_ACTIONS_OPEN_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_quick_actions_open_playlist_button", FALSE); + public static final BooleanSetting HIDE_QUICK_ACTIONS_SAVE_TO_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_quick_actions_save_to_playlist_button", FALSE); + public static final BooleanSetting HIDE_QUICK_ACTIONS_SHARE_BUTTON = new BooleanSetting("revanced_hide_quick_actions_share_button", FALSE); + public static final IntegerSetting QUICK_ACTIONS_TOP_MARGIN = new IntegerSetting("revanced_quick_actions_top_margin", 0, true); + + public static final BooleanSetting DISABLE_LANDSCAPE_MODE = new BooleanSetting("revanced_disable_landscape_mode", FALSE, true); + public static final BooleanSetting ENABLE_COMPACT_CONTROLS_OVERLAY = new BooleanSetting("revanced_enable_compact_controls_overlay", FALSE, true); + public static final BooleanSetting FORCE_FULLSCREEN = new BooleanSetting("revanced_force_fullscreen", FALSE, true); + public static final BooleanSetting KEEP_LANDSCAPE_MODE = new BooleanSetting("revanced_keep_landscape_mode", FALSE, true); + public static final LongSetting KEEP_LANDSCAPE_MODE_TIMEOUT = new LongSetting("revanced_keep_landscape_mode_timeout", 3000L, true); + + // PreferenceScreen: Player - Haptic feedback + public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_CHAPTERS = new BooleanSetting("revanced_disable_haptic_feedback_chapters", FALSE); + public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_SCRUBBING = new BooleanSetting("revanced_disable_haptic_feedback_scrubbing", FALSE); + public static final BooleanSetting DISABLE_HAPTIC_FEEDBACK_SEEK = new BooleanSetting("revanced_disable_haptic_feedback_seek", 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); + + // PreferenceScreen: Player - Player buttons + public static final BooleanSetting HIDE_PLAYER_AUTOPLAY_BUTTON = new BooleanSetting("revanced_hide_player_autoplay_button", TRUE, true); + public static final BooleanSetting HIDE_PLAYER_CAPTIONS_BUTTON = new BooleanSetting("revanced_hide_player_captions_button", FALSE, true); + public static final BooleanSetting HIDE_PLAYER_CAST_BUTTON = new BooleanSetting("revanced_hide_player_cast_button", TRUE, true); + public static final BooleanSetting HIDE_PLAYER_COLLAPSE_BUTTON = new BooleanSetting("revanced_hide_player_collapse_button", FALSE, true); + public static final BooleanSetting HIDE_PLAYER_FULLSCREEN_BUTTON = new BooleanSetting("revanced_hide_player_fullscreen_button", FALSE, true); + public static final BooleanSetting HIDE_PLAYER_PREVIOUS_NEXT_BUTTON = new BooleanSetting("revanced_hide_player_previous_next_button", FALSE, true); + public static final BooleanSetting HIDE_PLAYER_YOUTUBE_MUSIC_BUTTON = new BooleanSetting("revanced_hide_player_youtube_music_button", FALSE); + + public static final BooleanSetting ALWAYS_REPEAT = new BooleanSetting("revanced_always_repeat", FALSE); + public static final BooleanSetting ALWAYS_REPEAT_PAUSE = new BooleanSetting("revanced_always_repeat_pause", FALSE); + public static final BooleanSetting OVERLAY_BUTTON_ALWAYS_REPEAT = new BooleanSetting("revanced_overlay_button_always_repeat", FALSE); + public static final BooleanSetting OVERLAY_BUTTON_COPY_VIDEO_URL = new BooleanSetting("revanced_overlay_button_copy_video_url", TRUE); + public static final BooleanSetting OVERLAY_BUTTON_COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_overlay_button_copy_video_url_timestamp", FALSE); + public static final BooleanSetting OVERLAY_BUTTON_MUTE_VOLUME = new BooleanSetting("revanced_overlay_button_mute_volume", FALSE); + public static final BooleanSetting OVERLAY_BUTTON_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_overlay_button_external_downloader", FALSE); + public static final BooleanSetting OVERLAY_BUTTON_SPEED_DIALOG = new BooleanSetting("revanced_overlay_button_speed_dialog", TRUE); + public static final BooleanSetting OVERLAY_BUTTON_PLAY_ALL = new BooleanSetting("revanced_overlay_button_play_all", FALSE); + public static final EnumSetting OVERLAY_BUTTON_PLAY_ALL_TYPE = new EnumSetting<>("revanced_overlay_button_play_all_type", PlaylistIdPrefix.ALL_CONTENTS_WITH_TIME_ASCENDING); + public static final BooleanSetting OVERLAY_BUTTON_WHITELIST = new BooleanSetting("revanced_overlay_button_whitelist", FALSE); + + // PreferenceScreen: Player - Seekbar + public static final BooleanSetting APPEND_TIME_STAMP_INFORMATION = new BooleanSetting("revanced_append_time_stamp_information", TRUE, true); + public static final BooleanSetting APPEND_TIME_STAMP_INFORMATION_TYPE = new BooleanSetting("revanced_append_time_stamp_information_type", TRUE, parent(APPEND_TIME_STAMP_INFORMATION)); + public static final BooleanSetting REPLACE_TIME_STAMP_ACTION = new BooleanSetting("revanced_replace_time_stamp_action", TRUE, true, parent(APPEND_TIME_STAMP_INFORMATION)); + public static final BooleanSetting ENABLE_CUSTOM_SEEKBAR_COLOR = new BooleanSetting("revanced_enable_custom_seekbar_color", FALSE, true); + public static final StringSetting ENABLE_CUSTOM_SEEKBAR_COLOR_VALUE = new StringSetting("revanced_custom_seekbar_color_value", "#FF0033", true, parent(ENABLE_CUSTOM_SEEKBAR_COLOR)); + public static final BooleanSetting ENABLE_SEEKBAR_TAPPING = new BooleanSetting("revanced_enable_seekbar_tapping", TRUE); + public static final BooleanSetting HIDE_SEEKBAR = new BooleanSetting("revanced_hide_seekbar", FALSE, true); + public static final BooleanSetting HIDE_SEEKBAR_THUMBNAIL = new BooleanSetting("revanced_hide_seekbar_thumbnail", FALSE); + public static final BooleanSetting DISABLE_SEEKBAR_CHAPTERS = new BooleanSetting("revanced_disable_seekbar_chapters", FALSE, true); + public static final BooleanSetting HIDE_SEEKBAR_CHAPTER_LABEL = new BooleanSetting("revanced_hide_seekbar_chapter_label", FALSE, true); + public static final BooleanSetting HIDE_TIME_STAMP = new BooleanSetting("revanced_hide_time_stamp", FALSE, true); + public static final BooleanSetting RESTORE_OLD_SEEKBAR_THUMBNAILS = new BooleanSetting("revanced_restore_old_seekbar_thumbnails", + PatchStatus.OldSeekbarThumbnailsDefaultBoolean(), true); + public static final BooleanSetting ENABLE_SEEKBAR_THUMBNAILS_HIGH_QUALITY = new BooleanSetting("revanced_enable_seekbar_thumbnails_high_quality", FALSE, true, "revanced_enable_seekbar_thumbnails_high_quality_dialog_message"); + public static final BooleanSetting ENABLE_CAIRO_SEEKBAR = new BooleanSetting("revanced_enable_cairo_seekbar", FALSE, true); + + // PreferenceScreen: Player - Video description + public static final BooleanSetting DISABLE_ROLLING_NUMBER_ANIMATIONS = new BooleanSetting("revanced_disable_rolling_number_animations", 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_ATTRIBUTES_SECTION = new BooleanSetting("revanced_hide_attributes_section", FALSE); + public static final BooleanSetting HIDE_CHAPTERS_SECTION = new BooleanSetting("revanced_hide_chapters_section", FALSE); + public static final BooleanSetting HIDE_CONTENTS_SECTION = new BooleanSetting("revanced_hide_contents_section", FALSE); + public static final BooleanSetting HIDE_INFO_CARDS_SECTION = new BooleanSetting("revanced_hide_info_cards_section", FALSE); + public static final BooleanSetting HIDE_KEY_CONCEPTS_SECTION = new BooleanSetting("revanced_hide_key_concepts_section", FALSE); + public static final BooleanSetting HIDE_PODCAST_SECTION = new BooleanSetting("revanced_hide_podcast_section", FALSE); + public static final BooleanSetting HIDE_SHOPPING_LINKS = new BooleanSetting("revanced_hide_shopping_links", TRUE); + public static final BooleanSetting HIDE_TRANSCRIPT_SECTION = new BooleanSetting("revanced_hide_transcript_section", FALSE); + public static final BooleanSetting DISABLE_VIDEO_DESCRIPTION_INTERACTION = new BooleanSetting("revanced_disable_video_description_interaction", FALSE, true); + public static final BooleanSetting EXPAND_VIDEO_DESCRIPTION = new BooleanSetting("revanced_expand_video_description", FALSE, true); + public static final StringSetting EXPAND_VIDEO_DESCRIPTION_STRINGS = new StringSetting("revanced_expand_video_description_strings", str("revanced_expand_video_description_strings_default_value"), true, parent(EXPAND_VIDEO_DESCRIPTION)); + + + // PreferenceScreen: Shorts + public static final BooleanSetting DISABLE_RESUMING_SHORTS_PLAYER = new BooleanSetting("revanced_disable_resuming_shorts_player", TRUE); + public static final BooleanSetting DISABLE_SHORTS_BACKGROUND_PLAYBACK = new BooleanSetting("revanced_disable_shorts_background_playback", FALSE); + public static final BooleanSetting HIDE_SHORTS_FLOATING_BUTTON = new BooleanSetting("revanced_hide_shorts_floating_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_SHELF = new BooleanSetting("revanced_hide_shorts_shelf", TRUE, true); + public static final BooleanSetting HIDE_SHORTS_SHELF_CHANNEL = new BooleanSetting("revanced_hide_shorts_shelf_channel", FALSE); + public static final BooleanSetting HIDE_SHORTS_SHELF_HOME_RELATED_VIDEOS = new BooleanSetting("revanced_hide_shorts_shelf_home_related_videos", TRUE); + public static final BooleanSetting HIDE_SHORTS_SHELF_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_shorts_shelf_subscriptions", TRUE); + public static final BooleanSetting HIDE_SHORTS_SHELF_SEARCH = new BooleanSetting("revanced_hide_shorts_shelf_search", TRUE); + public static final BooleanSetting HIDE_SHORTS_SHELF_HISTORY = new BooleanSetting("revanced_hide_shorts_shelf_history", FALSE); + public static final EnumSetting CHANGE_SHORTS_REPEAT_STATE = new EnumSetting<>("revanced_change_shorts_repeat_state", ShortsLoopBehavior.UNKNOWN); + public static final EnumSetting CHANGE_SHORTS_BACKGROUND_REPEAT_STATE = new EnumSetting<>("revanced_change_shorts_background_repeat_state", ShortsLoopBehavior.UNKNOWN); + + // PreferenceScreen: Shorts - Shorts player components + public static final BooleanSetting HIDE_SHORTS_JOIN_BUTTON = new BooleanSetting("revanced_hide_shorts_join_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_SUBSCRIBE_BUTTON = new BooleanSetting("revanced_hide_shorts_subscribe_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_PAUSED_HEADER = new BooleanSetting("revanced_hide_shorts_paused_header", FALSE, true); + public static final BooleanSetting HIDE_SHORTS_PAUSED_OVERLAY_BUTTONS = new BooleanSetting("revanced_hide_shorts_paused_overlay_buttons", FALSE); + public static final BooleanSetting HIDE_SHORTS_TRENDS_BUTTON = new BooleanSetting("revanced_hide_shorts_trends_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_SHOPPING_BUTTON = new BooleanSetting("revanced_hide_shorts_shopping_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_STICKERS = new BooleanSetting("revanced_hide_shorts_stickers", TRUE); + public static final BooleanSetting HIDE_SHORTS_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_shorts_paid_promotion_label", TRUE, true); + public static final BooleanSetting HIDE_SHORTS_INFO_PANEL = new BooleanSetting("revanced_hide_shorts_info_panel", TRUE); + public static final BooleanSetting HIDE_SHORTS_LIVE_HEADER = new BooleanSetting("revanced_hide_shorts_live_header", FALSE); + public static final BooleanSetting HIDE_SHORTS_CHANNEL_BAR = new BooleanSetting("revanced_hide_shorts_channel_bar", FALSE); + public static final BooleanSetting HIDE_SHORTS_VIDEO_TITLE = new BooleanSetting("revanced_hide_shorts_video_title", FALSE); + public static final BooleanSetting HIDE_SHORTS_SOUND_METADATA_LABEL = new BooleanSetting("revanced_hide_shorts_sound_metadata_label", TRUE); + public static final BooleanSetting HIDE_SHORTS_FULL_VIDEO_LINK_LABEL = new BooleanSetting("revanced_hide_shorts_full_video_link_label", TRUE); + + // PreferenceScreen: Shorts - Shorts player components - Suggested actions + public static final BooleanSetting HIDE_SHORTS_GREEN_SCREEN_BUTTON = new BooleanSetting("revanced_hide_shorts_green_screen_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_SAVE_MUSIC_BUTTON = new BooleanSetting("revanced_hide_shorts_save_music_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_SHOP_BUTTON = new BooleanSetting("revanced_hide_shorts_shop_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_SUPER_THANKS_BUTTON = new BooleanSetting("revanced_hide_shorts_super_thanks_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_USE_THIS_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_use_this_sound_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_USE_TEMPLATE_BUTTON = new BooleanSetting("revanced_hide_shorts_use_template_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_LOCATION_BUTTON = new BooleanSetting("revanced_hide_shorts_location_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_SEARCH_SUGGESTIONS_BUTTON = new BooleanSetting("revanced_hide_shorts_search_suggestions_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_TAGGED_PRODUCTS = new BooleanSetting("revanced_hide_shorts_tagged_products", TRUE); + + // PreferenceScreen: Shorts - Shorts player components - Action buttons + public static final BooleanSetting HIDE_SHORTS_LIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_like_button", FALSE); + public static final BooleanSetting HIDE_SHORTS_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_shorts_dislike_button", FALSE); + public static final BooleanSetting HIDE_SHORTS_COMMENTS_BUTTON = new BooleanSetting("revanced_hide_shorts_comments_button", FALSE); + public static final BooleanSetting HIDE_SHORTS_COMMENTS_DISABLED_BUTTON = new BooleanSetting("revanced_hide_shorts_comments_disabled_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_REMIX_BUTTON = new BooleanSetting("revanced_hide_shorts_remix_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_SHARE_BUTTON = new BooleanSetting("revanced_hide_shorts_share_button", FALSE); + public static final BooleanSetting HIDE_SHORTS_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_sound_button", TRUE); + + // PreferenceScreen: Shorts - Shorts player components - Animation / Feedback + public static final BooleanSetting DISABLE_SHORTS_LIKE_BUTTON_FOUNTAIN_ANIMATION = new BooleanSetting("revanced_disable_shorts_like_button_fountain_animation", FALSE); + public static final BooleanSetting HIDE_SHORTS_PLAY_PAUSE_BUTTON_BACKGROUND = new BooleanSetting("revanced_hide_shorts_play_pause_button_background", FALSE, true); + public static final EnumSetting ANIMATION_TYPE = new EnumSetting<>("revanced_shorts_double_tap_to_like_animation", AnimationType.ORIGINAL, true); + + // PreferenceScreen: Shorts - Shorts player components - Custom actions + public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL = new BooleanSetting("revanced_shorts_custom_actions_copy_video_url", FALSE, true); + public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_shorts_custom_actions_copy_video_url_timestamp", FALSE, true); + public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_shorts_custom_actions_external_downloader", FALSE, true); + public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO = new BooleanSetting("revanced_shorts_custom_actions_open_video", FALSE, true); + public static final BooleanSetting SHORTS_CUSTOM_ACTIONS_REPEAT_STATE = new BooleanSetting("revanced_shorts_custom_actions_repeat_state", FALSE, true); + + public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU = new BooleanSetting("revanced_enable_shorts_custom_actions_flyout_menu", FALSE, true, + parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE)); + public static final BooleanSetting ENABLE_SHORTS_CUSTOM_ACTIONS_TOOLBAR = new BooleanSetting("revanced_enable_shorts_custom_actions_toolbar", FALSE, true, + parentsAny(SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL, SHORTS_CUSTOM_ACTIONS_COPY_VIDEO_URL_TIMESTAMP, SHORTS_CUSTOM_ACTIONS_EXTERNAL_DOWNLOADER, SHORTS_CUSTOM_ACTIONS_OPEN_VIDEO, SHORTS_CUSTOM_ACTIONS_REPEAT_STATE)); + + // Experimental Flags + public static final BooleanSetting ENABLE_TIME_STAMP = new BooleanSetting("revanced_enable_shorts_time_stamp", FALSE, true); + public static final BooleanSetting TIME_STAMP_CHANGE_REPEAT_STATE = new BooleanSetting("revanced_shorts_time_stamp_change_repeat_state", TRUE, true, parent(ENABLE_TIME_STAMP)); + public static final IntegerSetting META_PANEL_BOTTOM_MARGIN = new IntegerSetting("revanced_shorts_meta_panel_bottom_margin", 32, true, parent(ENABLE_TIME_STAMP)); + public static final BooleanSetting HIDE_SHORTS_TOOLBAR = new BooleanSetting("revanced_hide_shorts_toolbar", FALSE, true); + public static final BooleanSetting HIDE_SHORTS_NAVIGATION_BAR = new BooleanSetting("revanced_hide_shorts_navigation_bar", FALSE, true); + public static final IntegerSetting SHORTS_NAVIGATION_BAR_HEIGHT_PERCENTAGE = new IntegerSetting("revanced_shorts_navigation_bar_height_percentage", 45, true, parent(HIDE_SHORTS_NAVIGATION_BAR)); + public static final BooleanSetting REPLACE_CHANNEL_HANDLE = new BooleanSetting("revanced_replace_channel_handle", FALSE, true); + public static final BooleanSetting RESTORE_SHORTS_OLD_PLAYER_LAYOUT = new BooleanSetting("revanced_restore_shorts_old_player_layout", FALSE, true); + + // PreferenceScreen: Swipe controls + public static final BooleanSetting ENABLE_SWIPE_BRIGHTNESS = new BooleanSetting("revanced_enable_swipe_brightness", TRUE, true); + public static final BooleanSetting ENABLE_SWIPE_VOLUME = new BooleanSetting("revanced_enable_swipe_volume", TRUE, true); + public static final BooleanSetting ENABLE_SWIPE_LOWEST_VALUE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_enable_swipe_lowest_value_auto_brightness", TRUE, parent(ENABLE_SWIPE_BRIGHTNESS)); + public static final BooleanSetting ENABLE_SAVE_AND_RESTORE_BRIGHTNESS = new BooleanSetting("revanced_enable_save_and_restore_brightness", TRUE, true, parent(ENABLE_SWIPE_BRIGHTNESS)); + public static final BooleanSetting ENABLE_SWIPE_PRESS_TO_ENGAGE = new BooleanSetting("revanced_enable_swipe_press_to_engage", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)); + public static final BooleanSetting ENABLE_SWIPE_HAPTIC_FEEDBACK = new BooleanSetting("revanced_enable_swipe_haptic_feedback", TRUE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)); + public static final BooleanSetting SWIPE_LOCK_MODE = new BooleanSetting("revanced_swipe_gestures_lock_mode", FALSE, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)); + public static final IntegerSetting SWIPE_MAGNITUDE_THRESHOLD = new IntegerSetting("revanced_swipe_magnitude_threshold", 0, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)); + public static final IntegerSetting SWIPE_OVERLAY_BACKGROUND_ALPHA = new IntegerSetting("revanced_swipe_overlay_background_alpha", 127, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)); + public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_overlay_text_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)); + public static final IntegerSetting SWIPE_OVERLAY_RECT_SIZE = new IntegerSetting("revanced_swipe_overlay_rect_size", 20, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)); + public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true, parentsAny(ENABLE_SWIPE_BRIGHTNESS, ENABLE_SWIPE_VOLUME)); + + public static final IntegerSetting SWIPE_BRIGHTNESS_SENSITIVITY = new IntegerSetting("revanced_swipe_brightness_sensitivity", 100, true, parent(ENABLE_SWIPE_BRIGHTNESS)); + public static final IntegerSetting SWIPE_VOLUME_SENSITIVITY = new IntegerSetting("revanced_swipe_volume_sensitivity", 100, true, parent(ENABLE_SWIPE_VOLUME)); + /** + * @noinspection DeprecatedIsStillUsed + */ + @Deprecated // Patch is obsolete and no longer works with 19.09+ + public static final BooleanSetting DISABLE_HDR_AUTO_BRIGHTNESS = new BooleanSetting("revanced_disable_hdr_auto_brightness", TRUE, true, parent(ENABLE_SWIPE_BRIGHTNESS)); + public static final BooleanSetting DISABLE_SWIPE_TO_SWITCH_VIDEO = new BooleanSetting("revanced_disable_swipe_to_switch_video", FALSE, true); + public static final BooleanSetting DISABLE_WATCH_PANEL_GESTURES = new BooleanSetting("revanced_disable_watch_panel_gestures", FALSE, true); + public static final BooleanSetting SWIPE_BRIGHTNESS_AUTO = new BooleanSetting("revanced_swipe_brightness_auto", TRUE, false, false); + public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1.0f, false, false); + + + // PreferenceScreen: Video + public static final FloatSetting DEFAULT_PLAYBACK_SPEED = new FloatSetting("revanced_default_playback_speed", -2.0f); + public static final IntegerSetting DEFAULT_VIDEO_QUALITY_MOBILE = new IntegerSetting("revanced_default_video_quality_mobile", -2); + public static final IntegerSetting DEFAULT_VIDEO_QUALITY_WIFI = new IntegerSetting("revanced_default_video_quality_wifi", -2); + public static final BooleanSetting DISABLE_HDR_VIDEO = new BooleanSetting("revanced_disable_hdr_video", FALSE, true); + public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_LIVE = new BooleanSetting("revanced_disable_default_playback_speed_live", TRUE); + public static final BooleanSetting ENABLE_CUSTOM_PLAYBACK_SPEED = new BooleanSetting("revanced_enable_custom_playback_speed", FALSE, true); + public static final BooleanSetting CUSTOM_PLAYBACK_SPEED_MENU_TYPE = new BooleanSetting("revanced_custom_playback_speed_menu_type", FALSE, parent(ENABLE_CUSTOM_PLAYBACK_SPEED)); + public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", "0.25\n0.5\n0.75\n1.0\n1.25\n1.5\n1.75\n2.0\n2.25\n2.5", true, parent(ENABLE_CUSTOM_PLAYBACK_SPEED)); + public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", TRUE); + public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_playback_speed_last_selected_toast", TRUE, parent(REMEMBER_PLAYBACK_SPEED_LAST_SELECTED)); + public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", TRUE); + public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED_TOAST = new BooleanSetting("revanced_remember_video_quality_last_selected_toast", TRUE, parent(REMEMBER_VIDEO_QUALITY_LAST_SELECTED)); + public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE, true); + // Experimental Flags + public static final BooleanSetting DISABLE_DEFAULT_PLAYBACK_SPEED_MUSIC = new BooleanSetting("revanced_disable_default_playback_speed_music", TRUE, true); + public static final BooleanSetting ENABLE_DEFAULT_PLAYBACK_SPEED_SHORTS = new BooleanSetting("revanced_enable_default_playback_speed_shorts", FALSE); + public static final BooleanSetting SKIP_PRELOADED_BUFFER = new BooleanSetting("revanced_skip_preloaded_buffer", FALSE, true, "revanced_skip_preloaded_buffer_user_dialog_message"); + public static final BooleanSetting SKIP_PRELOADED_BUFFER_TOAST = new BooleanSetting("revanced_skip_preloaded_buffer_toast", TRUE); + public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true); + public static final BooleanSetting DISABLE_VP9_CODEC = new BooleanSetting("revanced_disable_vp9_codec", FALSE, true); + public static final BooleanSetting REPLACE_AV1_CODEC = new BooleanSetting("revanced_replace_av1_codec", FALSE, true); + public static final BooleanSetting REJECT_AV1_CODEC = new BooleanSetting("revanced_reject_av1_codec", FALSE, true); + + + // PreferenceScreen: Miscellaneous + public static final BooleanSetting ENABLE_EXTERNAL_BROWSER = new BooleanSetting("revanced_enable_external_browser", TRUE, true); + public static final BooleanSetting ENABLE_OPEN_LINKS_DIRECTLY = new BooleanSetting("revanced_enable_open_links_directly", TRUE); + public static final BooleanSetting DISABLE_QUIC_PROTOCOL = new BooleanSetting("revanced_disable_quic_protocol", FALSE, true); + + // Experimental Flags + public static final BooleanSetting CHANGE_SHARE_SHEET = new BooleanSetting("revanced_change_share_sheet", FALSE, true); + public static final BooleanSetting ENABLE_OPUS_CODEC = new BooleanSetting("revanced_enable_opus_codec", FALSE, true); + + /** + * @noinspection DeprecatedIsStillUsed + */ + @Deprecated + public static final LongSetting DOUBLE_BACK_TO_CLOSE_TIMEOUT = new LongSetting("revanced_double_back_to_close_timeout", 2000L); + + // PreferenceScreen: Miscellaneous - Watch history + public static final EnumSetting WATCH_HISTORY_TYPE = new EnumSetting<>("revanced_watch_history_type", WatchHistoryType.REPLACE); + + // PreferenceScreen: Miscellaneous - Spoof streaming data + + // PreferenceScreen: Return YouTube Dislike + public static final BooleanSetting RYD_ENABLED = new BooleanSetting("ryd_enabled", TRUE); + public static final StringSetting RYD_USER_ID = new StringSetting("ryd_user_id", ""); + public static final BooleanSetting RYD_SHORTS = new BooleanSetting("ryd_shorts", TRUE, parent(RYD_ENABLED)); + public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("ryd_dislike_percentage", FALSE, parent(RYD_ENABLED)); + public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("ryd_compact_layout", FALSE, parent(RYD_ENABLED)); + public static final BooleanSetting RYD_ESTIMATED_LIKE = new BooleanSetting("ryd_estimated_like", FALSE, true, parent(RYD_ENABLED)); + public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("ryd_toast_on_connection_error", FALSE, parent(RYD_ENABLED)); + + + // PreferenceScreen: SponsorBlock + public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE); + /** + * 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", "", 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_CREATE_NEW_SEGMENT = new BooleanSetting("sb_create_new_segment", FALSE, parent(SB_ENABLED)); + public static final BooleanSetting SB_COMPACT_SKIP_BUTTON = new BooleanSetting("sb_compact_skip_button", FALSE, parent(SB_ENABLED)); + public static final BooleanSetting SB_AUTO_HIDE_SKIP_BUTTON = new BooleanSetting("sb_auto_hide_skip_button", TRUE, parent(SB_ENABLED)); + public static final BooleanSetting SB_TOAST_ON_SKIP = new BooleanSetting("sb_toast_on_skip", TRUE, parent(SB_ENABLED)); + public static final BooleanSetting SB_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("sb_toast_on_connection_error", FALSE, parent(SB_ENABLED)); + public static final BooleanSetting SB_TRACK_SKIP_COUNT = new BooleanSetting("sb_track_skip_count", TRUE, parent(SB_ENABLED)); + public static final FloatSetting SB_SEGMENT_MIN_DURATION = new FloatSetting("sb_min_segment_duration", 0F, parent(SB_ENABLED)); + public static final BooleanSetting SB_VIDEO_LENGTH_WITHOUT_SEGMENTS = new BooleanSetting("sb_video_length_without_segments", FALSE, parent(SB_ENABLED)); + public static final StringSetting SB_API_URL = new StringSetting("sb_api_url", "https://sponsor.ajay.app", parent(SB_ENABLED)); + public static final BooleanSetting SB_USER_IS_VIP = new BooleanSetting("sb_user_is_vip", FALSE); + public static final IntegerSetting SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS = new IntegerSetting("sb_local_time_saved_number_segments", 0); + public static final LongSetting SB_LOCAL_TIME_SAVED_MILLISECONDS = new LongSetting("sb_local_time_saved_milliseconds", 0L); + + public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color", "#00D400"); + public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_SELF_PROMO_COLOR = new StringSetting("sb_selfpromo_color", "#FFFF00"); + public static final StringSetting SB_CATEGORY_INTERACTION = new StringSetting("sb_interaction", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_INTERACTION_COLOR = new StringSetting("sb_interaction_color", "#CC00FF"); + public static final StringSetting SB_CATEGORY_HIGHLIGHT = new StringSetting("sb_highlight", MANUAL_SKIP.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_HIGHLIGHT_COLOR = new StringSetting("sb_highlight_color", "#FF1684"); + public static final StringSetting SB_CATEGORY_INTRO = new StringSetting("sb_intro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_INTRO_COLOR = new StringSetting("sb_intro_color", "#00FFFF"); + public static final StringSetting SB_CATEGORY_OUTRO = new StringSetting("sb_outro", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_OUTRO_COLOR = new StringSetting("sb_outro_color", "#0202ED"); + public static final StringSetting SB_CATEGORY_PREVIEW = new StringSetting("sb_preview", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_PREVIEW_COLOR = new StringSetting("sb_preview_color", "#008FD6"); + public static final StringSetting SB_CATEGORY_FILLER = new StringSetting("sb_filler", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_FILLER_COLOR = new StringSetting("sb_filler_color", "#7300FF"); + public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC = new StringSetting("sb_music_offtopic", MANUAL_SKIP.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_MUSIC_OFFTOPIC_COLOR = new StringSetting("sb_music_offtopic_color", "#FF9900"); + public static final StringSetting SB_CATEGORY_UNSUBMITTED = new StringSetting("sb_unsubmitted", SKIP_AUTOMATICALLY.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_UNSUBMITTED_COLOR = new StringSetting("sb_unsubmitted_color", "#FFFFFF"); + + // SB Setting not exported + public static final LongSetting SB_LAST_VIP_CHECK = new LongSetting("sb_last_vip_check", 0L, false, false); + public static final BooleanSetting SB_HIDE_EXPORT_WARNING = new BooleanSetting("sb_hide_export_warning", FALSE, false, false); + public static final BooleanSetting SB_SEEN_GUIDELINES = new BooleanSetting("sb_seen_guidelines", FALSE, false, false); + + static { + // region Migration initialized + // Categories were previously saved without a 'sb_' key prefix, so they need an additional adjustment. + Set> sbCategories = new HashSet<>(Arrays.asList( + SB_CATEGORY_SPONSOR, + SB_CATEGORY_SPONSOR_COLOR, + SB_CATEGORY_SELF_PROMO, + SB_CATEGORY_SELF_PROMO_COLOR, + SB_CATEGORY_INTERACTION, + SB_CATEGORY_INTERACTION_COLOR, + SB_CATEGORY_HIGHLIGHT, + SB_CATEGORY_HIGHLIGHT_COLOR, + SB_CATEGORY_INTRO, + SB_CATEGORY_INTRO_COLOR, + SB_CATEGORY_OUTRO, + SB_CATEGORY_OUTRO_COLOR, + SB_CATEGORY_PREVIEW, + SB_CATEGORY_PREVIEW_COLOR, + SB_CATEGORY_FILLER, + SB_CATEGORY_FILLER_COLOR, + SB_CATEGORY_MUSIC_OFFTOPIC, + SB_CATEGORY_MUSIC_OFFTOPIC_COLOR, + SB_CATEGORY_UNSUBMITTED, + SB_CATEGORY_UNSUBMITTED_COLOR)); + + SharedPrefCategory ytPrefs = new SharedPrefCategory("youtube"); + SharedPrefCategory rydPrefs = new SharedPrefCategory("ryd"); + SharedPrefCategory sbPrefs = new SharedPrefCategory("sponsor-block"); + for (Setting setting : Setting.allLoadedSettings()) { + String key = setting.key; + if (setting.key.startsWith("sb_")) { + if (sbCategories.contains(setting)) { + key = key.substring(3); // Remove the "sb_" prefix, as old categories are saved without it. + } + migrateFromOldPreferences(sbPrefs, setting, key); + } else if (setting.key.startsWith("ryd_")) { + migrateFromOldPreferences(rydPrefs, setting, key); + } else { + migrateFromOldPreferences(ytPrefs, setting, key); + } + } + // endregion + + // region SB import/export callbacks + + Setting.addImportExportCallback(SponsorBlockSettings.SB_IMPORT_EXPORT_CALLBACK); + + // endregion + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/AboutYouTubeDataAPIPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/AboutYouTubeDataAPIPreference.java new file mode 100644 index 000000000..c8e8bd0d5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/AboutYouTubeDataAPIPreference.java @@ -0,0 +1,46 @@ +package app.revanced.extension.youtube.settings.preference; + +import android.app.Activity; +import android.content.Context; +import android.preference.Preference; +import android.util.AttributeSet; + +import app.revanced.extension.shared.settings.preference.YouTubeDataAPIDialogBuilder; + +@SuppressWarnings({"unused", "deprecation"}) +public class AboutYouTubeDataAPIPreference extends Preference implements Preference.OnPreferenceClickListener { + + private void init() { + setSelectable(true); + setOnPreferenceClickListener(this); + } + + public AboutYouTubeDataAPIPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public AboutYouTubeDataAPIPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public AboutYouTubeDataAPIPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public AboutYouTubeDataAPIPreference(Context context) { + super(context); + init(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + if (getContext() instanceof Activity mActivity) { + YouTubeDataAPIDialogBuilder.showDialog(mActivity); + } + + return true; + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java new file mode 100644 index 000000000..e979e9aca --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java @@ -0,0 +1,38 @@ +package app.revanced.extension.youtube.settings.preference; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.preference.Preference; +import android.util.AttributeSet; + +/** + * Allows tapping the DeArrow about preference to open the DeArrow website. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class AlternativeThumbnailsAboutDeArrowPreference extends Preference { + { + setOnPreferenceClickListener(pref -> { + 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) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public AlternativeThumbnailsAboutDeArrowPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AlternativeThumbnailsAboutDeArrowPreference(Context context) { + super(context); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderPlaylistPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderPlaylistPreference.java new file mode 100644 index 000000000..547eb3e34 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderPlaylistPreference.java @@ -0,0 +1,175 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.preference.Preference; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TableLayout; +import android.widget.TableRow; + +import java.util.Arrays; + +import app.revanced.extension.shared.settings.StringSetting; +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.ExtendedUtils; + +@SuppressWarnings({"unused", "deprecation"}) +public class ExternalDownloaderPlaylistPreference extends Preference implements Preference.OnPreferenceClickListener { + + private static final StringSetting settings = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME_PLAYLIST; + private static final String[] mEntries = ResourceUtils.getStringArray("revanced_external_downloader_playlist_label"); + private static final String[] mEntryValues = ResourceUtils.getStringArray("revanced_external_downloader_playlist_package_name"); + private static final String[] mWebsiteEntries = ResourceUtils.getStringArray("revanced_external_downloader_playlist_website"); + + @SuppressLint("StaticFieldLeak") + private static EditText mEditText; + private static String packageName; + private static int mClickedDialogEntryIndex; + + private final TextWatcher textWatcher = new TextWatcher() { + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + public void afterTextChanged(Editable s) { + packageName = s.toString(); + mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName); + } + }; + + private void init() { + setSelectable(true); + setOnPreferenceClickListener(this); + } + + public ExternalDownloaderPlaylistPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public ExternalDownloaderPlaylistPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public ExternalDownloaderPlaylistPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ExternalDownloaderPlaylistPreference(Context context) { + super(context); + init(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + packageName = settings.get(); + mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName); + + final Context context = getContext(); + AlertDialog.Builder builder = Utils.getEditTextDialogBuilder(context); + + TableLayout table = new TableLayout(context); + table.setOrientation(LinearLayout.HORIZONTAL); + table.setPadding(15, 0, 15, 0); + + TableRow row = new TableRow(context); + + mEditText = new EditText(context); + mEditText.setHint(settings.defaultValue); + mEditText.setText(packageName); + mEditText.addTextChangedListener(textWatcher); + mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 9); + mEditText.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT, 1f)); + row.addView(mEditText); + + table.addView(row); + builder.setView(table); + + builder.setTitle(str("revanced_external_downloader_dialog_title")); + builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, (dialog, which) -> { + mClickedDialogEntryIndex = which; + mEditText.setText(mEntryValues[which]); + }); + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { + final String packageName = mEditText.getText().toString().trim(); + settings.save(packageName); + checkPackageIsValid(context, packageName); + dialog.dismiss(); + }); + builder.setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> settings.resetToDefault()); + builder.setNegativeButton(android.R.string.cancel, null); + + builder.show(); + + return true; + } + + private static boolean checkPackageIsValid(Context context, String packageName) { + String appName = ""; + String website = ""; + + if (mClickedDialogEntryIndex >= 0) { + appName = mEntries[mClickedDialogEntryIndex]; + website = mWebsiteEntries[mClickedDialogEntryIndex]; + } + + return showToastOrOpenWebsites(context, appName, packageName, website); + } + + private static boolean showToastOrOpenWebsites(Context context, String appName, String packageName, String website) { + if (ExtendedUtils.isPackageEnabled(packageName)) + return true; + + if (website.isEmpty()) { + Utils.showToastShort(str("revanced_external_downloader_not_installed_warning", packageName)); + return false; + } + + new AlertDialog.Builder(context) + .setTitle(str("revanced_external_downloader_not_installed_dialog_title")) + .setMessage(str("revanced_external_downloader_not_installed_dialog_message", appName, appName)) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(website)); + context.startActivity(i); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + + return false; + } + + public static boolean checkPackageIsDisabled() { + final Context context = Utils.getActivity(); + packageName = settings.get(); + mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName); + return !checkPackageIsValid(context, packageName); + } + + public static String getExternalDownloaderPackageName() { + String downloaderPackageName = settings.get().trim(); + + if (downloaderPackageName.isEmpty()) { + settings.resetToDefault(); + downloaderPackageName = settings.defaultValue; + } + + return downloaderPackageName; + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderVideoLongPressPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderVideoLongPressPreference.java new file mode 100644 index 000000000..887c41019 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderVideoLongPressPreference.java @@ -0,0 +1,174 @@ +package app.revanced.extension.youtube.settings.preference; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.preference.Preference; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TableLayout; +import android.widget.TableRow; +import app.revanced.extension.shared.settings.StringSetting; +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.ExtendedUtils; + +import java.util.Arrays; + +import static app.revanced.extension.shared.utils.StringRef.str; + +@SuppressWarnings({"unused", "deprecation"}) +public class ExternalDownloaderVideoLongPressPreference extends Preference implements Preference.OnPreferenceClickListener { + + private static final StringSetting settings = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME_VIDEO_LONG_PRESS; + private static final String[] mEntries = ResourceUtils.getStringArray("revanced_external_downloader_video_long_press_label"); + private static final String[] mEntryValues = ResourceUtils.getStringArray("revanced_external_downloader_video_long_press_package_name"); + private static final String[] mWebsiteEntries = ResourceUtils.getStringArray("revanced_external_downloader_video_long_press_website"); + + @SuppressLint("StaticFieldLeak") + private static EditText mEditText; + private static String packageName; + private static int mClickedDialogEntryIndex; + + private final TextWatcher textWatcher = new TextWatcher() { + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + public void afterTextChanged(Editable s) { + packageName = s.toString(); + mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName); + } + }; + + private void init() { + setSelectable(true); + setOnPreferenceClickListener(this); + } + + public ExternalDownloaderVideoLongPressPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public ExternalDownloaderVideoLongPressPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public ExternalDownloaderVideoLongPressPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ExternalDownloaderVideoLongPressPreference(Context context) { + super(context); + init(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + packageName = settings.get(); + mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName); + + final Context context = getContext(); + AlertDialog.Builder builder = Utils.getEditTextDialogBuilder(context); + + TableLayout table = new TableLayout(context); + table.setOrientation(LinearLayout.HORIZONTAL); + table.setPadding(15, 0, 15, 0); + + TableRow row = new TableRow(context); + + mEditText = new EditText(context); + mEditText.setHint(settings.defaultValue); + mEditText.setText(packageName); + mEditText.addTextChangedListener(textWatcher); + mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 9); + mEditText.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT, 1f)); + row.addView(mEditText); + + table.addView(row); + builder.setView(table); + + builder.setTitle(str("revanced_external_downloader_dialog_title")); + builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, (dialog, which) -> { + mClickedDialogEntryIndex = which; + mEditText.setText(mEntryValues[which]); + }); + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { + final String packageName = mEditText.getText().toString().trim(); + settings.save(packageName); + checkPackageIsValid(context, packageName); + dialog.dismiss(); + }); + builder.setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> settings.resetToDefault()); + builder.setNegativeButton(android.R.string.cancel, null); + + builder.show(); + + return true; + } + + private static boolean checkPackageIsValid(Context context, String packageName) { + String appName = ""; + String website = ""; + + if (mClickedDialogEntryIndex >= 0) { + appName = mEntries[mClickedDialogEntryIndex]; + website = mWebsiteEntries[mClickedDialogEntryIndex]; + } + + return showToastOrOpenWebsites(context, appName, packageName, website); + } + + private static boolean showToastOrOpenWebsites(Context context, String appName, String packageName, String website) { + if (ExtendedUtils.isPackageEnabled(packageName)) + return true; + + if (website.isEmpty()) { + Utils.showToastShort(str("revanced_external_downloader_not_installed_warning", packageName)); + return false; + } + + new AlertDialog.Builder(context) + .setTitle(str("revanced_external_downloader_not_installed_dialog_title")) + .setMessage(str("revanced_external_downloader_not_installed_dialog_message", appName, appName)) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(website)); + context.startActivity(i); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + + return false; + } + + public static boolean checkPackageIsDisabled() { + final Context context = Utils.getActivity(); + packageName = settings.get(); + mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName); + return !checkPackageIsValid(context, packageName); + } + + public static String getExternalDownloaderPackageName() { + String downloaderPackageName = settings.get().trim(); + + if (downloaderPackageName.isEmpty()) { + settings.resetToDefault(); + downloaderPackageName = settings.defaultValue; + } + + return downloaderPackageName; + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderVideoPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderVideoPreference.java new file mode 100644 index 000000000..26a83143f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ExternalDownloaderVideoPreference.java @@ -0,0 +1,175 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.preference.Preference; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TableLayout; +import android.widget.TableRow; + +import java.util.Arrays; + +import app.revanced.extension.shared.settings.StringSetting; +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.ExtendedUtils; + +@SuppressWarnings({"unused", "deprecation"}) +public class ExternalDownloaderVideoPreference extends Preference implements Preference.OnPreferenceClickListener { + + private static final StringSetting settings = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME_VIDEO; + private static final String[] mEntries = ResourceUtils.getStringArray("revanced_external_downloader_video_label"); + private static final String[] mEntryValues = ResourceUtils.getStringArray("revanced_external_downloader_video_package_name"); + private static final String[] mWebsiteEntries = ResourceUtils.getStringArray("revanced_external_downloader_video_website"); + + @SuppressLint("StaticFieldLeak") + private static EditText mEditText; + private static String packageName; + private static int mClickedDialogEntryIndex; + + private final TextWatcher textWatcher = new TextWatcher() { + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + public void afterTextChanged(Editable s) { + packageName = s.toString(); + mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName); + } + }; + + private void init() { + setSelectable(true); + setOnPreferenceClickListener(this); + } + + public ExternalDownloaderVideoPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public ExternalDownloaderVideoPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public ExternalDownloaderVideoPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ExternalDownloaderVideoPreference(Context context) { + super(context); + init(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + packageName = settings.get(); + mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName); + + final Context context = getContext(); + AlertDialog.Builder builder = Utils.getEditTextDialogBuilder(context); + + TableLayout table = new TableLayout(context); + table.setOrientation(LinearLayout.HORIZONTAL); + table.setPadding(15, 0, 15, 0); + + TableRow row = new TableRow(context); + + mEditText = new EditText(context); + mEditText.setHint(settings.defaultValue); + mEditText.setText(packageName); + mEditText.addTextChangedListener(textWatcher); + mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 9); + mEditText.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT, 1f)); + row.addView(mEditText); + + table.addView(row); + builder.setView(table); + + builder.setTitle(str("revanced_external_downloader_dialog_title")); + builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, (dialog, which) -> { + mClickedDialogEntryIndex = which; + mEditText.setText(mEntryValues[which]); + }); + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { + final String packageName = mEditText.getText().toString().trim(); + settings.save(packageName); + checkPackageIsValid(context, packageName); + dialog.dismiss(); + }); + builder.setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> settings.resetToDefault()); + builder.setNegativeButton(android.R.string.cancel, null); + + builder.show(); + + return true; + } + + private static boolean checkPackageIsValid(Context context, String packageName) { + String appName = ""; + String website = ""; + + if (mClickedDialogEntryIndex >= 0) { + appName = mEntries[mClickedDialogEntryIndex]; + website = mWebsiteEntries[mClickedDialogEntryIndex]; + } + + return showToastOrOpenWebsites(context, appName, packageName, website); + } + + private static boolean showToastOrOpenWebsites(Context context, String appName, String packageName, String website) { + if (ExtendedUtils.isPackageEnabled(packageName)) + return true; + + if (website.isEmpty()) { + Utils.showToastShort(str("revanced_external_downloader_not_installed_warning", packageName)); + return false; + } + + new AlertDialog.Builder(context) + .setTitle(str("revanced_external_downloader_not_installed_dialog_title")) + .setMessage(str("revanced_external_downloader_not_installed_dialog_message", appName, appName)) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(website)); + context.startActivity(i); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + + return false; + } + + public static boolean checkPackageIsDisabled() { + final Context context = Utils.getActivity(); + packageName = settings.get(); + mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName); + return !checkPackageIsValid(context, packageName); + } + + public static String getExternalDownloaderPackageName() { + String downloaderPackageName = settings.get().trim(); + + if (downloaderPackageName.isEmpty()) { + settings.resetToDefault(); + downloaderPackageName = settings.defaultValue; + } + + return downloaderPackageName; + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ImportExportPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ImportExportPreference.java new file mode 100644 index 000000000..8cc0db2f7 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ImportExportPreference.java @@ -0,0 +1,102 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.content.Context; +import android.preference.EditTextPreference; +import android.preference.Preference; +import android.text.InputType; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.EditText; + +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings({"unused", "deprecation"}) +public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener { + + private String existingSettings; + + @TargetApi(26) + private void init() { + setSelectable(true); + + EditText editText = getEditText(); + editText.setTextIsSelectable(true); + editText.setAutofillHints((String) null); + editText.setInputType(editText.getInputType() | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); // Use a smaller font to reduce text wrap. + + setOnPreferenceClickListener(this); + } + + public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public ImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public ImportExportPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ImportExportPreference(Context context) { + super(context); + init(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + try { + // Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened. + existingSettings = Setting.exportToJson(null); + getEditText().setText(existingSettings); + } catch (Exception ex) { + Logger.printException(() -> "showDialog failure", ex); + } + return true; + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + try { + Utils.setEditTextDialogTheme(builder, true); + super.onPrepareDialogBuilder(builder); + // Show the user the settings in JSON format. + builder.setNeutralButton(str("revanced_extended_settings_import_copy"), (dialog, which) -> + Utils.setClipboard(getEditText().getText().toString(), str("revanced_share_copy_settings_success"))) + .setPositiveButton(str("revanced_extended_settings_import"), (dialog, which) -> + importSettings(builder.getContext(), getEditText().getText().toString())); + } catch (Exception ex) { + Logger.printException(() -> "onPrepareDialogBuilder failure", ex); + } + } + + private void importSettings(Context context, String replacementSettings) { + try { + if (replacementSettings.equals(existingSettings)) { + return; + } + ReVancedPreferenceFragment.settingImportInProgress = true; + final boolean rebootNeeded = Setting.importFromJSON(context, replacementSettings); + if (rebootNeeded) { + AbstractPreferenceFragment.showRestartDialog(getContext()); + } + } catch (Exception ex) { + Logger.printException(() -> "importSettings failure", ex); + } finally { + ReVancedPreferenceFragment.settingImportInProgress = false; + } + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/OpenDefaultAppSettingsPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/OpenDefaultAppSettingsPreference.java new file mode 100644 index 000000000..169458053 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/OpenDefaultAppSettingsPreference.java @@ -0,0 +1,47 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.utils.Utils.isSDKAbove; + +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.utils.Logger; +import app.revanced.extension.shared.utils.Utils; + +@SuppressWarnings({"unused", "deprecation"}) +public class OpenDefaultAppSettingsPreference extends Preference { + { + setOnPreferenceClickListener(pref -> { + try { + Context context = Utils.getActivity(); + final Uri uri = Uri.parse("package:" + context.getPackageName()); + final Intent intent = isSDKAbove(31) + ? new Intent(android.provider.Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri) + : new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri); + context.startActivity(intent); + } catch (Exception exception) { + Logger.printException(() -> "OpenDefaultAppSettings Failed"); + } + return false; + }); + } + + public OpenDefaultAppSettingsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public OpenDefaultAppSettingsPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public OpenDefaultAppSettingsPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public OpenDefaultAppSettingsPreference(Context context) { + super(context); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java new file mode 100644 index 000000000..cf46d1a79 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java @@ -0,0 +1,689 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.showRestartDialog; +import static app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment.updateListPreferenceSummary; +import static app.revanced.extension.shared.utils.ResourceUtils.getXmlIdentifier; +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.getChildView; +import static app.revanced.extension.shared.utils.Utils.isSDKAbove; +import static app.revanced.extension.shared.utils.Utils.showToastShort; +import static app.revanced.extension.youtube.settings.Settings.DEFAULT_PLAYBACK_SPEED; +import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT; +import static app.revanced.extension.youtube.settings.Settings.HIDE_PREVIEW_COMMENT_TYPE; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.Preference; +import android.preference.PreferenceCategory; +import android.preference.PreferenceFragment; +import android.preference.PreferenceGroup; +import android.preference.PreferenceManager; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.util.TypedValue; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toolbar; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.SortedMap; +import java.util.TreeMap; + +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.video.CustomPlaybackSpeedPatch; +import app.revanced.extension.youtube.utils.ExtendedUtils; +import app.revanced.extension.youtube.utils.ThemeUtils; + +@SuppressWarnings("deprecation") +public class ReVancedPreferenceFragment extends PreferenceFragment { + private static final int READ_REQUEST_CODE = 42; + private static final int WRITE_REQUEST_CODE = 43; + static boolean settingImportInProgress = false; + static boolean showingUserDialogMessage; + + @SuppressLint("SuspiciousIndentation") + private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { + try { + if (str == null) return; + Setting setting = Setting.getSettingFromPath(str); + + if (setting == null) return; + + Preference mPreference = findPreference(str); + + if (mPreference == null) return; + + if (mPreference instanceof SwitchPreference switchPreference) { + BooleanSetting boolSetting = (BooleanSetting) setting; + if (settingImportInProgress) { + switchPreference.setChecked(boolSetting.get()); + } else { + BooleanSetting.privateSetValue(boolSetting, switchPreference.isChecked()); + } + + if (ExtendedUtils.anyMatchSetting(setting)) { + ExtendedUtils.setPlayerFlyoutMenuAdditionalSettings(); + } else if (setting.equals(HIDE_PREVIEW_COMMENT) || setting.equals(HIDE_PREVIEW_COMMENT_TYPE)) { + ExtendedUtils.setCommentPreviewSettings(); + } + } else if (mPreference instanceof EditTextPreference editTextPreference) { + if (settingImportInProgress) { + editTextPreference.setText(setting.get().toString()); + } else { + Setting.privateSetValueFromString(setting, editTextPreference.getText()); + } + } else if (mPreference instanceof ListPreference listPreference) { + if (settingImportInProgress) { + listPreference.setValue(setting.get().toString()); + } else { + Setting.privateSetValueFromString(setting, listPreference.getValue()); + } + if (setting.equals(DEFAULT_PLAYBACK_SPEED)) { + listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries()); + listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues()); + } + if (!(mPreference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) { + updateListPreferenceSummary(listPreference, setting); + } + } else { + Logger.printException(() -> "Setting cannot be handled: " + mPreference.getClass() + " " + mPreference); + return; + } + + ReVancedSettingsPreference.initializeReVancedSettings(); + + if (settingImportInProgress) { + return; + } + + if (!showingUserDialogMessage) { + final Context context = getActivity(); + + if (setting.userDialogMessage != null + && mPreference instanceof SwitchPreference switchPreference + && setting.defaultValue instanceof Boolean defaultValue + && switchPreference.isChecked() != defaultValue) { + showSettingUserDialogConfirmation(context, switchPreference, (BooleanSetting) setting); + } else if (setting.rebootApp) { + showRestartDialog(context); + } + } + } catch (Exception ex) { + Logger.printException(() -> "OnSharedPreferenceChangeListener failure", ex); + } + }; + + private void showSettingUserDialogConfirmation(Context context, SwitchPreference switchPreference, BooleanSetting setting) { + Utils.verifyOnMainThread(); + + showingUserDialogMessage = true; + assert setting.userDialogMessage != null; + new AlertDialog.Builder(context) + .setTitle(str("revanced_extended_confirm_user_dialog_title")) + .setMessage(setting.userDialogMessage.toString()) + .setPositiveButton(android.R.string.ok, (dialog, id) -> { + if (setting.rebootApp) { + showRestartDialog(context); + } + }) + .setNegativeButton(android.R.string.cancel, (dialog, id) -> { + switchPreference.setChecked(setting.defaultValue); // Recursive call that resets the Setting value. + }) + .setOnDismissListener(dialog -> showingUserDialogMessage = false) + .setCancelable(false) + .show(); + } + + static PreferenceManager mPreferenceManager; + private SharedPreferences mSharedPreferences; + + private PreferenceScreen originalPreferenceScreen; + + public ReVancedPreferenceFragment() { + // Required empty public constructor + } + + private void putPreferenceScreenMap(SortedMap preferenceScreenMap, PreferenceGroup preferenceGroup) { + if (preferenceGroup instanceof PreferenceScreen mPreferenceScreen) { + preferenceScreenMap.put(mPreferenceScreen.getKey(), mPreferenceScreen); + } + } + + private void setPreferenceScreenToolbar() { + SortedMap preferenceScreenMap = new TreeMap<>(); + + PreferenceScreen rootPreferenceScreen = getPreferenceScreen(); + for (Preference preference : getAllPreferencesBy(rootPreferenceScreen)) { + if (!(preference instanceof PreferenceGroup preferenceGroup)) continue; + putPreferenceScreenMap(preferenceScreenMap, preferenceGroup); + for (Preference childPreference : getAllPreferencesBy(preferenceGroup)) { + if (!(childPreference instanceof PreferenceGroup nestedPreferenceGroup)) continue; + putPreferenceScreenMap(preferenceScreenMap, nestedPreferenceGroup); + for (Preference nestedPreference : getAllPreferencesBy(nestedPreferenceGroup)) { + if (!(nestedPreference instanceof PreferenceGroup childPreferenceGroup)) + continue; + putPreferenceScreenMap(preferenceScreenMap, childPreferenceGroup); + } + } + } + + for (PreferenceScreen mPreferenceScreen : preferenceScreenMap.values()) { + mPreferenceScreen.setOnPreferenceClickListener( + preferenceScreen -> { + Dialog preferenceScreenDialog = mPreferenceScreen.getDialog(); + ViewGroup rootView = (ViewGroup) preferenceScreenDialog + .findViewById(android.R.id.content) + .getParent(); + + Toolbar toolbar = new Toolbar(preferenceScreen.getContext()); + + toolbar.setTitle(preferenceScreen.getTitle()); + toolbar.setNavigationIcon(ThemeUtils.getBackButtonDrawable()); + toolbar.setNavigationOnClickListener(view -> preferenceScreenDialog.dismiss()); + + int margin = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics() + ); + + toolbar.setTitleMargin(margin, 0, margin, 0); + + TextView toolbarTextView = getChildView(toolbar, TextView.class::isInstance); + if (toolbarTextView != null) { + toolbarTextView.setTextColor(ThemeUtils.getForegroundColor()); + } + + rootView.addView(toolbar, 0); + return false; + } + ); + } + } + + // Map to store dependencies: key is the preference key, value is a list of dependent preferences + private final Map> dependencyMap = new HashMap<>(); + // Set to track already added preferences to avoid duplicates + private final Set addedPreferences = new HashSet<>(); + // Map to store preferences grouped by their parent PreferenceGroup + private final Map> groupedPreferences = new LinkedHashMap<>(); + + @SuppressLint("ResourceType") + @Override + public void onCreate(Bundle bundle) { + super.onCreate(bundle); + try { + mPreferenceManager = getPreferenceManager(); + mPreferenceManager.setSharedPreferencesName(Setting.preferences.name); + mSharedPreferences = mPreferenceManager.getSharedPreferences(); + addPreferencesFromResource(getXmlIdentifier("revanced_prefs")); + + // Initialize toolbars and other UI elements + setPreferenceScreenToolbar(); + + // Initialize ReVanced settings + ReVancedSettingsPreference.initializeReVancedSettings(); + SponsorBlockSettingsPreference.init(getActivity()); + + // Import/export + setBackupRestorePreference(); + + // Store all preferences and their dependencies + storeAllPreferences(getPreferenceScreen()); + + // Load and set initial preferences states + for (Setting setting : Setting.allLoadedSettings()) { + final Preference preference = mPreferenceManager.findPreference(setting.key); + if (preference != null && isSDKAbove(26)) { + preference.setSingleLineTitle(false); + } + + if (preference instanceof SwitchPreference switchPreference) { + BooleanSetting boolSetting = (BooleanSetting) setting; + switchPreference.setChecked(boolSetting.get()); + } else if (preference instanceof EditTextPreference editTextPreference) { + editTextPreference.setText(setting.get().toString()); + } else if (preference instanceof ListPreference listPreference) { + if (setting.equals(DEFAULT_PLAYBACK_SPEED)) { + listPreference.setEntries(CustomPlaybackSpeedPatch.getListEntries()); + listPreference.setEntryValues(CustomPlaybackSpeedPatch.getListEntryValues()); + } + if (!(preference instanceof app.revanced.extension.youtube.settings.preference.SegmentCategoryListPreference)) { + updateListPreferenceSummary(listPreference, setting); + } + } + } + + // Register preference change listener + mSharedPreferences.registerOnSharedPreferenceChangeListener(listener); + + originalPreferenceScreen = getPreferenceManager().createPreferenceScreen(getActivity()); + copyPreferences(getPreferenceScreen(), originalPreferenceScreen); + } catch (Exception th) { + Logger.printException(() -> "Error during onCreate()", th); + } + } + + private void copyPreferences(PreferenceScreen source, PreferenceScreen destination) { + for (Preference preference : getAllPreferencesBy(source)) { + destination.addPreference(preference); + } + } + + @Override + public void onDestroy() { + mSharedPreferences.unregisterOnSharedPreferenceChangeListener(listener); + super.onDestroy(); + } + + /** + * Recursively stores all preferences and their dependencies grouped by their parent PreferenceGroup. + * + * @param preferenceGroup The preference group to scan. + */ + private void storeAllPreferences(PreferenceGroup preferenceGroup) { + // Check if this is the root PreferenceScreen + boolean isRootScreen = preferenceGroup == getPreferenceScreen(); + + // Use the special top-level group only for the root PreferenceScreen + PreferenceGroup groupKey = isRootScreen + ? new PreferenceCategory(preferenceGroup.getContext()) + : preferenceGroup; + + if (isRootScreen) { + groupKey.setTitle(ResourceUtils.getString("revanced_extended_settings_title")); + } + + // Initialize a list to hold preferences of the current group + List currentGroupPreferences = groupedPreferences.computeIfAbsent(groupKey, k -> new ArrayList<>()); + + for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) { + Preference preference = preferenceGroup.getPreference(i); + + // Add preference to the current group if not already added + if (!currentGroupPreferences.contains(preference)) { + currentGroupPreferences.add(preference); + } + + // Store dependencies + if (preference.getDependency() != null) { + String dependencyKey = preference.getDependency(); + dependencyMap.computeIfAbsent(dependencyKey, k -> new ArrayList<>()).add(preference); + } + + // Recursively handle nested PreferenceGroups + if (preference instanceof PreferenceGroup nestedGroup) { + storeAllPreferences(nestedGroup); + } + } + } + + /** + * Filters preferences based on the search query, displaying grouped results with group titles. + * + * @param query The search query. + */ + public void filterPreferences(String query) { + // If the query is null or empty, reset preferences to their default state + if (query == null || query.isEmpty()) { + resetPreferences(); + return; + } + + // Convert the query to lowercase for case-insensitive search + query = query.toLowerCase(); + + // Get the preference screen to modify + PreferenceScreen preferenceScreen = getPreferenceScreen(); + // Remove all current preferences from the screen + preferenceScreen.removeAll(); + // Clear the list of added preferences to start fresh + addedPreferences.clear(); + + // Create a map to store matched preferences for each group + Map> matchedGroupPreferences = new LinkedHashMap<>(); + + // Create a set to store all keys that should be included + Set keysToInclude = new HashSet<>(); + + // First pass: identify all preferences that match the query and their dependencies + for (Map.Entry> entry : groupedPreferences.entrySet()) { + List preferences = entry.getValue(); + for (Preference preference : preferences) { + if (preferenceMatches(preference, query)) { + addPreferenceAndDependencies(preference, keysToInclude); + } + } + } + + // Second pass: add all identified preferences to matchedGroupPreferences + for (Map.Entry> entry : groupedPreferences.entrySet()) { + PreferenceGroup group = entry.getKey(); + List preferences = entry.getValue(); + List matchedPreferences = new ArrayList<>(); + + for (Preference preference : preferences) { + if (keysToInclude.contains(preference.getKey())) { + matchedPreferences.add(preference); + } + } + + if (!matchedPreferences.isEmpty()) { + matchedGroupPreferences.put(group, matchedPreferences); + } + } + + // Add matched preferences to the screen, maintaining the original order + for (Map.Entry> entry : matchedGroupPreferences.entrySet()) { + PreferenceGroup group = entry.getKey(); + List matchedPreferences = entry.getValue(); + + // Add the category for this group + PreferenceCategory category = new PreferenceCategory(preferenceScreen.getContext()); + category.setTitle(group.getTitle()); + preferenceScreen.addPreference(category); + + // Add matched preferences for this group + for (Preference preference : matchedPreferences) { + if (preference.isSelectable()) { + addPreferenceWithDependencies(category, preference); + } else { + // For non-selectable preferences, just add them directly + category.addPreference(preference); + } + } + } + } + + /** + * Checks if a preference matches the given query. + * + * @param preference The preference to check. + * @param query The search query. + * @return True if the preference matches the query, false otherwise. + */ + private boolean preferenceMatches(Preference preference, String query) { + // Check if the title contains the query string + if (preference.getTitle().toString().toLowerCase().contains(query)) { + return true; + } + + // Check if the summary contains the query string + if (preference.getSummary() != null && preference.getSummary().toString().toLowerCase().contains(query)) { + return true; + } + + // Additional checks for SwitchPreference + if (preference instanceof SwitchPreference switchPreference) { + CharSequence summaryOn = switchPreference.getSummaryOn(); + CharSequence summaryOff = switchPreference.getSummaryOff(); + + if ((summaryOn != null && summaryOn.toString().toLowerCase().contains(query)) || + (summaryOff != null && summaryOff.toString().toLowerCase().contains(query))) { + return true; + } + } + + // Additional checks for ListPreference + if (preference instanceof ListPreference listPreference) { + CharSequence[] entries = listPreference.getEntries(); + if (entries != null) { + for (CharSequence entry : entries) { + if (entry.toString().toLowerCase().contains(query)) { + return true; + } + } + } + + CharSequence[] entryValues = listPreference.getEntryValues(); + if (entryValues != null) { + for (CharSequence entryValue : entryValues) { + if (entryValue.toString().toLowerCase().contains(query)) { + return true; + } + } + } + } + + return false; + } + + /** + * Recursively adds a preference and its dependencies to the set of keys to include. + * + * @param preference The preference to add. + * @param keysToInclude The set of keys to include. + */ + private void addPreferenceAndDependencies(Preference preference, Set keysToInclude) { + String key = preference.getKey(); + if (key != null && !keysToInclude.contains(key)) { + keysToInclude.add(key); + + // Add the preference this one depends on + String dependencyKey = preference.getDependency(); + if (dependencyKey != null) { + Preference dependency = findPreferenceInAllGroups(dependencyKey); + if (dependency != null) { + addPreferenceAndDependencies(dependency, keysToInclude); + } + } + + // Add preferences that depend on this one + if (dependencyMap.containsKey(key)) { + for (Preference dependentPreference : Objects.requireNonNull(dependencyMap.get(key))) { + addPreferenceAndDependencies(dependentPreference, keysToInclude); + } + } + } + } + + /** + * Recursively adds a preference along with its dependencies + * (android:dependency attribute in XML). + * + * @param preferenceGroup The preference group to add to. + * @param preference The preference to add. + */ + private void addPreferenceWithDependencies(PreferenceGroup preferenceGroup, Preference preference) { + String key = preference.getKey(); + + // Instead of just using preference keys, we combine the category and key to ensure uniqueness + if (key != null && !addedPreferences.contains(preferenceGroup.getTitle() + ":" + key)) { + // Add dependencies first + if (preference.getDependency() != null) { + String dependencyKey = preference.getDependency(); + Preference dependency = findPreferenceInAllGroups(dependencyKey); + if (dependency != null) { + addPreferenceWithDependencies(preferenceGroup, dependency); + } else { + return; + } + } + + // Add the preference using a combination of the category and the key + preferenceGroup.addPreference(preference); + addedPreferences.add(preferenceGroup.getTitle() + ":" + key); // Track based on both category and key + + // Handle dependent preferences + if (dependencyMap.containsKey(key)) { + for (Preference dependentPreference : Objects.requireNonNull(dependencyMap.get(key))) { + addPreferenceWithDependencies(preferenceGroup, dependentPreference); + } + } + } + } + + /** + * Finds a preference in all groups based on its key. + * + * @param key The key of the preference to find. + * @return The found preference, or null if not found. + */ + private Preference findPreferenceInAllGroups(String key) { + for (List preferences : groupedPreferences.values()) { + for (Preference preference : preferences) { + if (preference.getKey() != null && preference.getKey().equals(key)) { + return preference; + } + } + } + return null; + } + + /** + * Resets the preference screen to its original state. + */ + private void resetPreferences() { + PreferenceScreen preferenceScreen = getPreferenceScreen(); + preferenceScreen.removeAll(); + for (Preference preference : getAllPreferencesBy(originalPreferenceScreen)) + preferenceScreen.addPreference(preference); + } + + private List getAllPreferencesBy(PreferenceGroup preferenceGroup) { + List preferences = new ArrayList<>(); + for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) + preferences.add(preferenceGroup.getPreference(i)); + return preferences; + } + + /** + * Add Preference to Import/Export settings submenu + */ + private void setBackupRestorePreference() { + findPreference("revanced_extended_settings_import").setOnPreferenceClickListener(pref -> { + importActivity(); + return false; + }); + + findPreference("revanced_extended_settings_export").setOnPreferenceClickListener(pref -> { + exportActivity(); + return false; + }); + } + + /** + * Invoke the SAF(Storage Access Framework) to export settings + */ + private void exportActivity() { + @SuppressLint("SimpleDateFormat") final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + + final String appName = ExtendedUtils.getAppLabel(); + final String versionName = ExtendedUtils.getAppVersionName(); + final String formatDate = dateFormat.format(new Date(System.currentTimeMillis())); + final String fileName = String.format("%s_v%s_%s.txt", appName, versionName, formatDate); + + final Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("text/plain"); + intent.putExtra(Intent.EXTRA_TITLE, fileName); + startActivityForResult(intent, WRITE_REQUEST_CODE); + } + + /** + * Invoke the SAF(Storage Access Framework) to import settings + */ + private void importActivity() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType(isSDKAbove(29) ? "text/plain" : "*/*"); + startActivityForResult(intent, READ_REQUEST_CODE); + } + + /** + * Activity should be done within the lifecycle of PreferenceFragment + */ + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == WRITE_REQUEST_CODE && resultCode == Activity.RESULT_OK) { + exportText(data.getData()); + } else if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK && data != null) { + importText(data.getData()); + } + } + + private void exportText(Uri uri) { + final Context context = this.getActivity(); + + try { + @SuppressLint("Recycle") + FileWriter jsonFileWriter = + new FileWriter( + Objects.requireNonNull(context.getApplicationContext() + .getContentResolver() + .openFileDescriptor(uri, "w")) + .getFileDescriptor() + ); + PrintWriter printWriter = new PrintWriter(jsonFileWriter); + printWriter.write(Setting.exportToJson(context)); + printWriter.close(); + jsonFileWriter.close(); + + showToastShort(str("revanced_extended_settings_export_success")); + } catch (IOException e) { + showToastShort(str("revanced_extended_settings_export_failed")); + } + } + + private void importText(Uri uri) { + final Context context = this.getActivity(); + StringBuilder sb = new StringBuilder(); + String line; + + try { + settingImportInProgress = true; + + @SuppressLint("Recycle") + FileReader fileReader = + new FileReader( + Objects.requireNonNull(context.getApplicationContext() + .getContentResolver() + .openFileDescriptor(uri, "r")) + .getFileDescriptor() + ); + BufferedReader bufferedReader = new BufferedReader(fileReader); + while ((line = bufferedReader.readLine()) != null) { + sb.append(line).append("\n"); + } + bufferedReader.close(); + fileReader.close(); + + final boolean restartNeeded = Setting.importFromJSON(context, sb.toString()); + if (restartNeeded) { + showRestartDialog(getActivity()); + } + } catch (IOException e) { + showToastShort(str("revanced_extended_settings_import_failed")); + throw new RuntimeException(e); + } finally { + settingImportInProgress = false; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java new file mode 100644 index 000000000..c89e6c118 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java @@ -0,0 +1,279 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.isSDKAbove; +import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_1; +import static app.revanced.extension.youtube.patches.general.MiniplayerPatch.MiniplayerType.MODERN_3; +import static app.revanced.extension.youtube.utils.ExtendedUtils.isSpoofingToLessThan; + +import android.preference.Preference; +import android.preference.SwitchPreference; + +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.youtube.patches.general.LayoutSwitchPatch; +import app.revanced.extension.youtube.patches.general.MiniplayerPatch; +import app.revanced.extension.youtube.patches.utils.PatchStatus; +import app.revanced.extension.youtube.patches.utils.ReturnYouTubeDislikePatch; +import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.ExtendedUtils; + +@SuppressWarnings("deprecation") +public class ReVancedSettingsPreference extends ReVancedPreferenceFragment { + + private static void enableDisablePreferences() { + for (Setting setting : Setting.allLoadedSettings()) { + final Preference preference = mPreferenceManager.findPreference(setting.key); + if (preference != null) { + preference.setEnabled(setting.isAvailable()); + } + } + } + + private static void enableDisablePreferences(final boolean isAvailable, final Setting... unavailableEnum) { + if (!isAvailable) { + return; + } + for (Setting setting : unavailableEnum) { + final Preference preference = mPreferenceManager.findPreference(setting.key); + if (preference != null) { + preference.setEnabled(false); + } + } + } + + public static void initializeReVancedSettings() { + enableDisablePreferences(); + + AmbientModePreferenceLinks(); + ChangeHeaderPreferenceLinks(); + ExternalDownloaderPreferenceLinks(); + FullScreenPanelPreferenceLinks(); + LayoutOverrideLinks(); + MiniPlayerPreferenceLinks(); + NavigationPreferenceLinks(); + RYDPreferenceLinks(); + SeekBarPreferenceLinks(); + SpeedOverlayPreferenceLinks(); + QuickActionsPreferenceLinks(); + TabletLayoutLinks(); + WhitelistPreferenceLinks(); + } + + /** + * Enable/Disable Preference related to Ambient Mode + */ + private static void AmbientModePreferenceLinks() { + enableDisablePreferences( + Settings.DISABLE_AMBIENT_MODE.get(), + Settings.BYPASS_AMBIENT_MODE_RESTRICTIONS, + Settings.DISABLE_AMBIENT_MODE_IN_FULLSCREEN + ); + } + + /** + * Enable/Disable Preference related to Change header + */ + private static void ChangeHeaderPreferenceLinks() { + enableDisablePreferences( + PatchStatus.MinimalHeader(), + Settings.CHANGE_YOUTUBE_HEADER + ); + } + + /** + * Enable/Disable Preference for External downloader settings + */ + private static void ExternalDownloaderPreferenceLinks() { + // Override download button will not work if spoofed with YouTube 18.24.xx or earlier. + enableDisablePreferences( + isSpoofingToLessThan("18.24.00"), + Settings.OVERRIDE_VIDEO_DOWNLOAD_BUTTON, + Settings.OVERRIDE_PLAYLIST_DOWNLOAD_BUTTON + ); + } + + /** + * Enable/Disable Layout Override Preference + */ + private static void LayoutOverrideLinks() { + enableDisablePreferences( + ExtendedUtils.isTablet(), + Settings.FORCE_FULLSCREEN + ); + } + + /** + * Enable/Disable Preferences not working in tablet layout + */ + private static void TabletLayoutLinks() { + final boolean isTablet = ExtendedUtils.isTablet() && + !LayoutSwitchPatch.phoneLayoutEnabled(); + + enableDisablePreferences( + isTablet, + Settings.DISABLE_ENGAGEMENT_PANEL, + Settings.HIDE_COMMUNITY_POSTS_HOME_RELATED_VIDEOS, + Settings.HIDE_COMMUNITY_POSTS_SUBSCRIPTIONS, + Settings.HIDE_MIX_PLAYLISTS, + Settings.HIDE_RELATED_VIDEO_OVERLAY, + Settings.SHOW_VIDEO_TITLE_SECTION + ); + } + + /** + * Enable/Disable Preference related to Fullscreen Panel + */ + private static void FullScreenPanelPreferenceLinks() { + enableDisablePreferences( + Settings.DISABLE_ENGAGEMENT_PANEL.get(), + Settings.HIDE_RELATED_VIDEO_OVERLAY, + Settings.HIDE_QUICK_ACTIONS, + Settings.HIDE_QUICK_ACTIONS_COMMENT_BUTTON, + Settings.HIDE_QUICK_ACTIONS_DISLIKE_BUTTON, + Settings.HIDE_QUICK_ACTIONS_LIKE_BUTTON, + Settings.HIDE_QUICK_ACTIONS_LIVE_CHAT_BUTTON, + Settings.HIDE_QUICK_ACTIONS_MORE_BUTTON, + Settings.HIDE_QUICK_ACTIONS_OPEN_MIX_PLAYLIST_BUTTON, + Settings.HIDE_QUICK_ACTIONS_OPEN_PLAYLIST_BUTTON, + Settings.HIDE_QUICK_ACTIONS_SAVE_TO_PLAYLIST_BUTTON, + Settings.HIDE_QUICK_ACTIONS_SHARE_BUTTON + ); + + enableDisablePreferences( + Settings.DISABLE_LANDSCAPE_MODE.get(), + Settings.FORCE_FULLSCREEN + ); + + enableDisablePreferences( + Settings.FORCE_FULLSCREEN.get(), + Settings.DISABLE_LANDSCAPE_MODE + ); + + } + + /** + * Enable/Disable Preference related to Hide Quick Actions + */ + private static void QuickActionsPreferenceLinks() { + final boolean isEnabled = + Settings.DISABLE_ENGAGEMENT_PANEL.get() || Settings.HIDE_QUICK_ACTIONS.get(); + + enableDisablePreferences( + isEnabled, + Settings.HIDE_QUICK_ACTIONS_COMMENT_BUTTON, + Settings.HIDE_QUICK_ACTIONS_DISLIKE_BUTTON, + Settings.HIDE_QUICK_ACTIONS_LIKE_BUTTON, + Settings.HIDE_QUICK_ACTIONS_LIVE_CHAT_BUTTON, + Settings.HIDE_QUICK_ACTIONS_MORE_BUTTON, + Settings.HIDE_QUICK_ACTIONS_OPEN_MIX_PLAYLIST_BUTTON, + Settings.HIDE_QUICK_ACTIONS_OPEN_PLAYLIST_BUTTON, + Settings.HIDE_QUICK_ACTIONS_SAVE_TO_PLAYLIST_BUTTON, + Settings.HIDE_QUICK_ACTIONS_SHARE_BUTTON + ); + } + + /** + * Enable/Disable Preference related to Miniplayer settings + */ + private static void MiniPlayerPreferenceLinks() { + final MiniplayerPatch.MiniplayerType CURRENT_TYPE = Settings.MINIPLAYER_TYPE.get(); + final boolean available = + (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && + !Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get() && + !Settings.MINIPLAYER_DRAG_AND_DROP.get(); + + enableDisablePreferences( + !available, + Settings.MINIPLAYER_HIDE_EXPAND_CLOSE + ); + } + + /** + * Enable/Disable Preference related to Navigation settings + */ + private static void NavigationPreferenceLinks() { + enableDisablePreferences( + Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get(), + Settings.HIDE_NAVIGATION_CREATE_BUTTON + ); + enableDisablePreferences( + !Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get(), + Settings.HIDE_NAVIGATION_NOTIFICATIONS_BUTTON, + Settings.REPLACE_TOOLBAR_CREATE_BUTTON, + Settings.REPLACE_TOOLBAR_CREATE_BUTTON_TYPE + ); + enableDisablePreferences( + !isSDKAbove(33), + Settings.ENABLE_TRANSLUCENT_NAVIGATION_BAR, + Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_LIGHT, + Settings.DISABLE_TRANSLUCENT_NAVIGATION_BAR_DARK + ); + } + + /** + * Enable/Disable Preference related to RYD settings + */ + private static void RYDPreferenceLinks() { + if (!(mPreferenceManager.findPreference(Settings.RYD_ENABLED.key) instanceof SwitchPreference enabledPreference)) { + return; + } + if (!(mPreferenceManager.findPreference(Settings.RYD_SHORTS.key) instanceof SwitchPreference shortsPreference)) { + return; + } + if (!(mPreferenceManager.findPreference(Settings.RYD_DISLIKE_PERCENTAGE.key) instanceof SwitchPreference percentagePreference)) { + return; + } + if (!(mPreferenceManager.findPreference(Settings.RYD_COMPACT_LAYOUT.key) instanceof SwitchPreference compactLayoutPreference)) { + return; + } + final Preference.OnPreferenceChangeListener clearAllUICaches = (pref, newValue) -> { + ReturnYouTubeDislike.clearAllUICaches(); + + return true; + }; + enabledPreference.setOnPreferenceChangeListener((pref, newValue) -> { + ReturnYouTubeDislikePatch.onRYDStatusChange(); + + return true; + }); + String shortsSummary = ReturnYouTubeDislikePatch.IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER + ? str("revanced_ryd_shorts_summary_on") + : str("revanced_ryd_shorts_summary_on_disclaimer"); + shortsPreference.setSummaryOn(shortsSummary); + percentagePreference.setOnPreferenceChangeListener(clearAllUICaches); + compactLayoutPreference.setOnPreferenceChangeListener(clearAllUICaches); + } + + /** + * Enable/Disable Preference related to Seek bar settings + */ + private static void SeekBarPreferenceLinks() { + enableDisablePreferences( + Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get(), + Settings.ENABLE_SEEKBAR_THUMBNAILS_HIGH_QUALITY + ); + } + + /** + * Enable/Disable Preference related to Speed overlay settings + */ + private static void SpeedOverlayPreferenceLinks() { + enableDisablePreferences( + Settings.DISABLE_SPEED_OVERLAY.get(), + Settings.SPEED_OVERLAY_VALUE + ); + } + + private static void WhitelistPreferenceLinks() { + final boolean enabled = PatchStatus.RememberPlaybackSpeed() || PatchStatus.SponsorBlock(); + final String[] whitelistKey = {Settings.OVERLAY_BUTTON_WHITELIST.key, "revanced_whitelist_settings"}; + + for (String key : whitelistKey) { + final Preference preference = mPreferenceManager.findPreference(key); + if (preference != null) { + preference.setEnabled(enabled); + } + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SegmentCategoryListPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SegmentCategoryListPreference.java new file mode 100644 index 000000000..b94ee3135 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SegmentCategoryListPreference.java @@ -0,0 +1,180 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.graphics.Color; +import android.preference.ListPreference; +import android.text.Editable; +import android.text.InputType; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TableLayout; +import android.widget.TableRow; +import android.widget.TextView; + +import java.util.Objects; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour; +import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory; + +@SuppressWarnings({"unused", "deprecation"}) +public class SegmentCategoryListPreference extends ListPreference { + private SegmentCategory mCategory; + private EditText mEditText; + private int mClickedDialogEntryIndex; + + private void init() { + final SegmentCategory segmentCategory = SegmentCategory.byCategoryKey(getKey()); + final boolean isHighlightCategory = segmentCategory == SegmentCategory.HIGHLIGHT; + mCategory = Objects.requireNonNull(segmentCategory); + // Edit: Using preferences to sync together multiple pieces + // of code together is messy and should be rethought. + setKey(segmentCategory.behaviorSetting.key); + setDefaultValue(segmentCategory.behaviorSetting.defaultValue); + + setEntries(isHighlightCategory + ? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce() + : CategoryBehaviour.getBehaviorDescriptions()); + setEntryValues(isHighlightCategory + ? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce() + : CategoryBehaviour.getBehaviorKeyValues()); + updateTitle(); + } + + public SegmentCategoryListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public SegmentCategoryListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public SegmentCategoryListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public SegmentCategoryListPreference(Context context) { + super(context); + init(); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + try { + Utils.setEditTextDialogTheme(builder); + super.onPrepareDialogBuilder(builder); + + Context context = builder.getContext(); + TableLayout table = new TableLayout(context); + table.setOrientation(LinearLayout.HORIZONTAL); + table.setPadding(70, 0, 150, 0); + + TableRow row = new TableRow(context); + + TextView colorTextLabel = new TextView(context); + colorTextLabel.setText(str("revanced_sb_color_dot_label")); + row.addView(colorTextLabel); + + TextView colorDotView = new TextView(context); + colorDotView.setText(mCategory.getCategoryColorDot()); + colorDotView.setPadding(30, 0, 30, 0); + row.addView(colorDotView); + + mEditText = new EditText(context); + mEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS); + mEditText.setText(mCategory.colorString()); + mEditText.addTextChangedListener(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 s) { + try { + String colorString = s.toString(); + if (!colorString.startsWith("#")) { + s.insert(0, "#"); // recursively calls back into this method + return; + } + if (colorString.length() > 7) { + s.delete(7, colorString.length()); + return; + } + final int color = Color.parseColor(colorString); + colorDotView.setText(SegmentCategory.getCategoryColorDot(color)); + } catch (IllegalArgumentException ex) { + // ignore + } + } + }); + mEditText.setLayoutParams(new TableRow.LayoutParams(0, TableRow.LayoutParams.WRAP_CONTENT, 1f)); + row.addView(mEditText); + + table.addView(row); + builder.setView(table); + builder.setTitle(mCategory.title.toString()); + + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> onClick(dialog, DialogInterface.BUTTON_POSITIVE)); + builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> { + try { + mCategory.resetColor(); + updateTitle(); + Utils.showToastShort(str("revanced_sb_color_reset")); + } catch (Exception ex) { + Logger.printException(() -> "setNeutralButton failure", ex); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + mClickedDialogEntryIndex = findIndexOfValue(getValue()); + builder.setSingleChoiceItems(getEntries(), mClickedDialogEntryIndex, (dialog, which) -> mClickedDialogEntryIndex = which); + } catch (Exception ex) { + Logger.printException(() -> "onPrepareDialogBuilder failure", ex); + } + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + try { + if (positiveResult && mClickedDialogEntryIndex >= 0 && getEntryValues() != null) { + String value = getEntryValues()[mClickedDialogEntryIndex].toString(); + if (callChangeListener(value)) { + setValue(value); + mCategory.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value))); + SegmentCategory.updateEnabledCategories(); + } + String colorString = mEditText.getText().toString(); + try { + if (!colorString.equals(mCategory.colorString())) { + mCategory.setColor(colorString); + Utils.showToastShort(str("revanced_sb_color_changed")); + } + } catch (IllegalArgumentException ex) { + Utils.showToastShort(str("revanced_sb_color_invalid")); + } + updateTitle(); + } + } catch (Exception ex) { + Logger.printException(() -> "onDialogClosed failure", ex); + } + } + + private void updateTitle() { + setTitle(mCategory.getTitleWithColorDot()); + setEnabled(Settings.SB_ENABLED.get()); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockImportExportPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockImportExportPreference.java new file mode 100644 index 000000000..1d53842dd --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockImportExportPreference.java @@ -0,0 +1,108 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.content.Context; +import android.preference.EditTextPreference; +import android.preference.Preference; +import android.text.InputType; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.EditText; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings; + +@SuppressWarnings({"unused", "deprecation"}) +public class SponsorBlockImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener { + + private String existingSettings; + + @TargetApi(26) + private void init() { + setSelectable(true); + + EditText editText = getEditText(); + editText.setTextIsSelectable(true); + editText.setAutofillHints((String) null); + editText.setInputType(editText.getInputType() + | InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_FLAG_MULTI_LINE + | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + editText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); // Use a smaller font to reduce text wrap. + + // 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"); + setSummary(importExportSummary); + + setOnPreferenceClickListener(this); + } + + public SponsorBlockImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public SponsorBlockImportExportPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public SponsorBlockImportExportPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public SponsorBlockImportExportPreference(Context context) { + super(context); + init(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + try { + // Must set text before preparing dialog, otherwise text is non selectable if this preference is later reopened. + existingSettings = SponsorBlockSettings.exportDesktopSettings(); + getEditText().setText(existingSettings); + } catch (Exception ex) { + Logger.printException(() -> "showDialog failure", ex); + } + return true; + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + try { + Utils.setEditTextDialogTheme(builder); + super.onPrepareDialogBuilder(builder); + // Show the user the settings in JSON format. + builder.setTitle(getTitle()); + builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> + Utils.setClipboard(getEditText().getText().toString(), str("revanced_sb_share_copy_settings_success"))) + .setPositiveButton(android.R.string.ok, (dialog, which) -> + importSettings(getEditText().getText().toString())); + } catch (Exception ex) { + Logger.printException(() -> "onPrepareDialogBuilder failure", ex); + } + } + + private void importSettings(String replacementSettings) { + try { + if (replacementSettings.equals(existingSettings)) { + return; + } + SponsorBlockSettings.importDesktopSettings(replacementSettings); + SponsorBlockSettingsPreference.updateSegmentCategories(); + SponsorBlockSettingsPreference.fetchAndDisplayStats(); + SponsorBlockSettingsPreference.updateUI(); + } catch (Exception ex) { + Logger.printException(() -> "importSettings failure", ex); + } + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockSettingsPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockSettingsPreference.java new file mode 100644 index 000000000..d3107381f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockSettingsPreference.java @@ -0,0 +1,432 @@ +package app.revanced.extension.youtube.settings.preference; + +import static android.text.Html.fromHtml; +import static app.revanced.extension.shared.utils.ResourceUtils.getLayoutIdentifier; +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.annotation.TargetApi; +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.preference.Preference; +import android.preference.PreferenceCategory; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.text.InputType; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TableLayout; +import android.widget.TableRow; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.settings.preference.ResettableEditTextPreference; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.utils.PatchStatus; +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.UserStats; +import app.revanced.extension.youtube.sponsorblock.requests.SBRequester; +import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController; + +@SuppressWarnings({"unused", "deprecation"}) +public class SponsorBlockSettingsPreference extends ReVancedPreferenceFragment { + + private static PreferenceCategory statsCategory; + + private static final int preferencesCategoryLayout = getLayoutIdentifier("revanced_settings_preferences_category"); + + private static final Preference.OnPreferenceChangeListener updateUI = (pref, newValue) -> { + updateUI(); + + return true; + }; + + @NonNull + private static SwitchPreference findSwitchPreference(BooleanSetting setting) { + final String key = setting.key; + if (mPreferenceManager.findPreference(key) instanceof SwitchPreference switchPreference) { + switchPreference.setOnPreferenceChangeListener(updateUI); + return switchPreference; + } else { + throw new IllegalStateException("SwitchPreference is null: " + key); + } + } + + @NonNull + private static ResettableEditTextPreference findResettableEditTextPreference(Setting setting) { + final String key = setting.key; + if (mPreferenceManager.findPreference(key) instanceof ResettableEditTextPreference switchPreference) { + switchPreference.setOnPreferenceChangeListener(updateUI); + return switchPreference; + } else { + throw new IllegalStateException("ResettableEditTextPreference is null: " + key); + } + } + + public static void updateUI() { + if (!Settings.SB_ENABLED.get()) { + SponsorBlockViewController.hideAll(); + SegmentPlaybackController.clearData(); + } else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) { + SponsorBlockViewController.hideNewSegmentLayout(); + } + } + + @TargetApi(26) + public static void init(Activity mActivity) { + if (!PatchStatus.SponsorBlock()) { + return; + } + + final SwitchPreference sbEnabled = findSwitchPreference(Settings.SB_ENABLED); + sbEnabled.setOnPreferenceClickListener(preference -> { + updateUI(); + fetchAndDisplayStats(); + updateSegmentCategories(); + return false; + }); + + if (!(sbEnabled.getParent() instanceof PreferenceScreen mPreferenceScreen)) { + return; + } + + final SwitchPreference votingEnabled = findSwitchPreference(Settings.SB_VOTING_BUTTON); + final SwitchPreference compactSkipButton = findSwitchPreference(Settings.SB_COMPACT_SKIP_BUTTON); + final SwitchPreference autoHideSkipSegmentButton = findSwitchPreference(Settings.SB_AUTO_HIDE_SKIP_BUTTON); + final SwitchPreference showSkipToast = findSwitchPreference(Settings.SB_TOAST_ON_SKIP); + showSkipToast.setOnPreferenceClickListener(preference -> { + Utils.showToastShort(str("revanced_sb_skipped_sponsor")); + return false; + }); + + final SwitchPreference showTimeWithoutSegments = findSwitchPreference(Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS); + + final SwitchPreference addNewSegment = findSwitchPreference(Settings.SB_CREATE_NEW_SEGMENT); + addNewSegment.setOnPreferenceChangeListener((preference, newValue) -> { + if ((Boolean) newValue && !Settings.SB_SEEN_GUIDELINES.get()) { + Context context = preference.getContext(); + new AlertDialog.Builder(context) + .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(context)) + .setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true)) + .setCancelable(false) + .show(); + } + updateUI(); + return true; + }); + + final ResettableEditTextPreference newSegmentStep = findResettableEditTextPreference(Settings.SB_CREATE_NEW_SEGMENT_STEP); + newSegmentStep.setOnPreferenceChangeListener((preference, 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; + }); + final Preference guidelinePreferences = Objects.requireNonNull(mPreferenceManager.findPreference("revanced_sb_guidelines_preference")); + guidelinePreferences.setDependency(Settings.SB_ENABLED.key); + guidelinePreferences.setOnPreferenceClickListener(preference -> { + openGuidelines(preference.getContext()); + return true; + }); + + final SwitchPreference toastOnConnectionError = findSwitchPreference(Settings.SB_TOAST_ON_CONNECTION_ERROR); + final SwitchPreference trackSkips = findSwitchPreference(Settings.SB_TRACK_SKIP_COUNT); + final ResettableEditTextPreference minSegmentDuration = findResettableEditTextPreference(Settings.SB_SEGMENT_MIN_DURATION); + minSegmentDuration.setOnPreferenceChangeListener((preference, 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; + }); + final ResettableEditTextPreference privateUserId = findResettableEditTextPreference(Settings.SB_PRIVATE_USER_ID); + privateUserId.setOnPreferenceChangeListener((preference, 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); + try { + updateUI(); + } catch (Exception e) { + throw new RuntimeException(e); + } + fetchAndDisplayStats(); + return true; + }); + final Preference apiUrl = mPreferenceManager.findPreference(Settings.SB_API_URL.key); + if (apiUrl != null) { + apiUrl.setOnPreferenceClickListener(preference -> { + Context context = preference.getContext(); + + TableLayout table = new TableLayout(context); + table.setOrientation(LinearLayout.HORIZONTAL); + table.setPadding(15, 0, 15, 0); + + TableRow row = new TableRow(context); + + EditText editText = new EditText(context); + editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); + editText.setText(Settings.SB_API_URL.get()); + editText.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT, 1f)); + row.addView(editText); + table.addView(row); + + 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")); + } + } + }; + Utils.getEditTextDialogBuilder(context) + .setView(table) + .setTitle(apiUrl.getTitle()) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(str("revanced_sb_reset"), urlChangeListener) + .setPositiveButton(android.R.string.ok, urlChangeListener) + .show(); + return true; + }); + } + + statsCategory = new PreferenceCategory(mActivity); + statsCategory.setLayoutResource(preferencesCategoryLayout); + statsCategory.setTitle(str("revanced_sb_stats")); + mPreferenceScreen.addPreference(statsCategory); + fetchAndDisplayStats(); + + final PreferenceCategory aboutCategory = new PreferenceCategory(mActivity); + aboutCategory.setLayoutResource(preferencesCategoryLayout); + aboutCategory.setTitle(str("revanced_sb_about")); + mPreferenceScreen.addPreference(aboutCategory); + + Preference aboutPreference = new Preference(mActivity); + aboutCategory.addPreference(aboutPreference); + aboutPreference.setTitle(str("revanced_sb_about_api")); + aboutPreference.setSummary(str("revanced_sb_about_api_sum")); + aboutPreference.setOnPreferenceClickListener(preference -> { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse("https://sponsor.ajay.app")); + preference.getContext().startActivity(i); + return false; + }); + + updateUI(); + } + + public static void updateSegmentCategories() { + try { + for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) { + final String key = category.keyValue; + if (mPreferenceManager.findPreference(key) instanceof SegmentCategoryListPreference segmentCategoryListPreference) { + segmentCategoryListPreference.setTitle(category.getTitleWithColorDot()); + segmentCategoryListPreference.setEnabled(Settings.SB_ENABLED.get()); + } + } + } catch (Exception ex) { + Logger.printException(() -> "updateSegmentCategories failure", ex); + } + } + + private static void openGuidelines(Context context) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines")); + context.startActivity(intent); + } + + public static void fetchAndDisplayStats() { + try { + if (statsCategory == null) { + return; + } + statsCategory.removeAll(); + if (!SponsorBlockSettings.userHasSBPrivateId()) { + // User has never voted or created any segments. No stats to show. + addLocalUserStats(); + return; + } + + Context context = statsCategory.getContext(); + + Preference loadingPlaceholderPreference = new Preference(context); + 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 static 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. + ResettableEditTextPreference 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 static 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; + }); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataDefaultClientListPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataDefaultClientListPreference.java new file mode 100644 index 000000000..b3fabe111 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataDefaultClientListPreference.java @@ -0,0 +1,87 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.utils.ResourceUtils.getStringArray; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.ListPreference; +import android.preference.PreferenceManager; +import android.util.AttributeSet; + +import app.revanced.extension.shared.patches.client.AppClient.ClientType; +import app.revanced.extension.shared.settings.EnumSetting; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings({"unused", "deprecation"}) +public class SpoofStreamingDataDefaultClientListPreference extends ListPreference { + + private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { + // Because this listener may run before the ReVanced settings fragment updates Settings, + // this could show the prior config and not the current. + // + // Push this call to the end of the main run queue, + // so all other listeners are done and Settings is up to date. + Utils.runOnMainThread(this::updateUI); + }; + + public SpoofStreamingDataDefaultClientListPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public SpoofStreamingDataDefaultClientListPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SpoofStreamingDataDefaultClientListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SpoofStreamingDataDefaultClientListPreference(Context context) { + super(context); + } + + private void addChangeListener() { + Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener); + } + + private void removeChangeListener() { + Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener); + } + + @Override + protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { + super.onAttachedToHierarchy(preferenceManager); + updateUI(); + addChangeListener(); + } + + @Override + protected void onPrepareForRemoval() { + super.onPrepareForRemoval(); + removeChangeListener(); + } + + private void updateUI() { + final boolean spoofStreamingDataAndroidOnly = Settings.SPOOF_STREAMING_DATA_ANDROID_ONLY.get(); + final String entryKey = spoofStreamingDataAndroidOnly + ? "revanced_spoof_streaming_data_type_android_entries" + : "revanced_spoof_streaming_data_type_android_ios_entries"; + final String entryValueKey = spoofStreamingDataAndroidOnly + ? "revanced_spoof_streaming_data_type_android_entry_values" + : "revanced_spoof_streaming_data_type_android_ios_entry_values"; + final String[] mEntries = getStringArray(entryKey); + final String[] mEntryValues = getStringArray(entryValueKey); + setEntries(mEntries); + setEntryValues(mEntryValues); + + final EnumSetting clientType = Settings.SPOOF_STREAMING_DATA_TYPE; + final boolean isAndroid = clientType.get().name().startsWith("ANDROID"); + if (spoofStreamingDataAndroidOnly && !isAndroid) { + clientType.resetToDefault(); + } + + setEnabled(Settings.SPOOF_STREAMING_DATA.get()); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java new file mode 100644 index 000000000..f8e062270 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java @@ -0,0 +1,78 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.Preference; +import android.preference.PreferenceManager; +import android.util.AttributeSet; + +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings({"deprecation", "unused"}) +public class SpoofStreamingDataSideEffectsPreference extends Preference { + + private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { + // Because this listener may run before the ReVanced settings fragment updates Settings, + // this could show the prior config and not the current. + // + // Push this call to the end of the main run queue, + // so all other listeners are done and Settings is up to date. + Utils.runOnMainThread(this::updateUI); + }; + + public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public SpoofStreamingDataSideEffectsPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SpoofStreamingDataSideEffectsPreference(Context context) { + super(context); + } + + private void addChangeListener() { + Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener); + } + + private void removeChangeListener() { + Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener); + } + + @Override + protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { + super.onAttachedToHierarchy(preferenceManager); + updateUI(); + addChangeListener(); + } + + @Override + protected void onPrepareForRemoval() { + super.onPrepareForRemoval(); + removeChangeListener(); + } + + private void updateUI() { + final String clientName = Settings.SPOOF_STREAMING_DATA_TYPE.get().name().toLowerCase(); + String summaryTextKey = "revanced_spoof_streaming_data_side_effects_"; + + if (Settings.SPOOF_STREAMING_DATA_ANDROID_ONLY.get()) { + summaryTextKey += "android"; + } else { + summaryTextKey += clientName; + } + + setSummary(str(summaryTextKey)); + setEnabled(Settings.SPOOF_STREAMING_DATA.get()); + setSelectable(false); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ThirdPartyYouTubeMusicPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ThirdPartyYouTubeMusicPreference.java new file mode 100644 index 000000000..b810cd9a5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ThirdPartyYouTubeMusicPreference.java @@ -0,0 +1,142 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.Context; +import android.preference.Preference; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TableLayout; +import android.widget.TableRow; + +import java.util.Arrays; + +import app.revanced.extension.shared.settings.StringSetting; +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.utils.ExtendedUtils; + +@SuppressWarnings({"unused", "deprecation"}) +public class ThirdPartyYouTubeMusicPreference extends Preference implements Preference.OnPreferenceClickListener { + + private static final StringSetting settings = Settings.THIRD_PARTY_YOUTUBE_MUSIC_PACKAGE_NAME; + private static final String[] mEntries = ResourceUtils.getStringArray("revanced_third_party_youtube_music_label"); + private static final String[] mEntryValues = ResourceUtils.getStringArray("revanced_third_party_youtube_music_package_name"); + + @SuppressLint("StaticFieldLeak") + private static EditText mEditText; + private static String packageName; + private static int mClickedDialogEntryIndex; + + private final TextWatcher textWatcher = new TextWatcher() { + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + } + + public void afterTextChanged(Editable s) { + packageName = s.toString(); + mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName); + } + }; + + private void init() { + setSelectable(true); + setOnPreferenceClickListener(this); + } + + public ThirdPartyYouTubeMusicPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public ThirdPartyYouTubeMusicPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public ThirdPartyYouTubeMusicPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public ThirdPartyYouTubeMusicPreference(Context context) { + super(context); + init(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + packageName = settings.get(); + mClickedDialogEntryIndex = Arrays.asList(mEntryValues).indexOf(packageName); + + final Context context = getContext(); + AlertDialog.Builder builder = Utils.getEditTextDialogBuilder(context); + + TableLayout table = new TableLayout(context); + table.setOrientation(LinearLayout.HORIZONTAL); + table.setPadding(15, 0, 15, 0); + + TableRow row = new TableRow(context); + + mEditText = new EditText(context); + mEditText.setHint(settings.defaultValue); + mEditText.setText(packageName); + mEditText.addTextChangedListener(textWatcher); + mEditText.setTextSize(TypedValue.COMPLEX_UNIT_PT, 9); + mEditText.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, TableRow.LayoutParams.WRAP_CONTENT, 1f)); + row.addView(mEditText); + + table.addView(row); + builder.setView(table); + + builder.setTitle(str("revanced_third_party_youtube_music_dialog_title")); + builder.setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, (dialog, which) -> { + mClickedDialogEntryIndex = which; + mEditText.setText(mEntryValues[which]); + }); + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> { + final String packageName = mEditText.getText().toString().trim(); + settings.save(packageName); + checkPackageIsValid(context, packageName); + dialog.dismiss(); + }); + builder.setNeutralButton(str("revanced_extended_settings_reset"), (dialog, which) -> settings.resetToDefault()); + builder.setNegativeButton(android.R.string.cancel, null); + + builder.show(); + + return true; + } + + private static void checkPackageIsValid(Context context, String packageName) { + if (packageName.isEmpty()) { + settings.resetToDefault(); + return; + } + + String appName = ""; + if (mClickedDialogEntryIndex >= 0) { + appName = mEntries[mClickedDialogEntryIndex]; + } + + showToastOrOpenWebsites(context, appName, packageName); + } + + private static void showToastOrOpenWebsites(Context context, String appName, String packageName) { + if (ExtendedUtils.isPackageEnabled(packageName)) { + return; + } + + Utils.showToastShort(str("revanced_third_party_youtube_music_not_installed_warning", appName.isEmpty() ? packageName : appName)); + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/WatchHistoryStatusPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/WatchHistoryStatusPreference.java new file mode 100644 index 000000000..104785fc1 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/WatchHistoryStatusPreference.java @@ -0,0 +1,81 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.Preference; +import android.preference.PreferenceManager; +import android.util.AttributeSet; + +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings({"deprecation", "unused"}) +public class WatchHistoryStatusPreference extends Preference { + + private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { + // Because this listener may run before the ReVanced settings fragment updates Settings, + // this could show the prior config and not the current. + // + // Push this call to the end of the main run queue, + // so all other listeners are done and Settings is up to date. + Utils.runOnMainThread(this::updateUI); + }; + + public WatchHistoryStatusPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + public WatchHistoryStatusPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public WatchHistoryStatusPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public WatchHistoryStatusPreference(Context context) { + super(context); + } + + private void addChangeListener() { + Setting.preferences.preferences.registerOnSharedPreferenceChangeListener(listener); + } + + private void removeChangeListener() { + Setting.preferences.preferences.unregisterOnSharedPreferenceChangeListener(listener); + } + + @Override + protected void onAttachedToHierarchy(PreferenceManager preferenceManager) { + super.onAttachedToHierarchy(preferenceManager); + updateUI(); + addChangeListener(); + } + + @Override + protected void onPrepareForRemoval() { + super.onPrepareForRemoval(); + removeChangeListener(); + } + + private void updateUI() { + final WatchHistoryType watchHistoryType = Settings.WATCH_HISTORY_TYPE.get(); + final boolean blockWatchHistory = watchHistoryType == WatchHistoryType.BLOCK; + final boolean replaceWatchHistory = watchHistoryType == WatchHistoryType.REPLACE; + + final String summaryTextKey; + if (blockWatchHistory) { + summaryTextKey = "revanced_watch_history_about_status_blocked"; + } else if (replaceWatchHistory) { + summaryTextKey = "revanced_watch_history_about_status_replaced"; + } else { + summaryTextKey = "revanced_watch_history_about_status_original"; + } + + setSummary(str(summaryTextKey)); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/WhitelistedChannelsPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/WhitelistedChannelsPreference.java new file mode 100644 index 000000000..ffa5d2ba4 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/WhitelistedChannelsPreference.java @@ -0,0 +1,162 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.app.AlertDialog; +import android.content.Context; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.widget.ImageButton; +import android.widget.LinearLayout; +import android.widget.TextView; + +import org.apache.commons.lang3.BooleanUtils; + +import java.util.ArrayList; + +import app.revanced.extension.youtube.patches.utils.PatchStatus; +import app.revanced.extension.youtube.utils.ThemeUtils; +import app.revanced.extension.youtube.whitelist.VideoChannel; +import app.revanced.extension.youtube.whitelist.Whitelist; +import app.revanced.extension.youtube.whitelist.Whitelist.WhitelistType; + +@SuppressWarnings({"unused", "deprecation"}) +public class WhitelistedChannelsPreference extends Preference implements Preference.OnPreferenceClickListener { + + private static final WhitelistType whitelistTypePlaybackSpeed = WhitelistType.PLAYBACK_SPEED; + private static final WhitelistType whitelistTypeSponsorBlock = WhitelistType.SPONSOR_BLOCK; + private static final boolean playbackSpeedIncluded = PatchStatus.RememberPlaybackSpeed(); + private static final boolean sponsorBlockIncluded = PatchStatus.SponsorBlock(); + private static String[] mEntries; + private static WhitelistType[] mEntryValues; + + static { + final int entrySize = BooleanUtils.toInteger(playbackSpeedIncluded) + + BooleanUtils.toInteger(sponsorBlockIncluded); + + if (entrySize != 0) { + mEntries = new String[entrySize]; + mEntryValues = new WhitelistType[entrySize]; + + int index = 0; + if (playbackSpeedIncluded) { + mEntries[index] = " " + whitelistTypePlaybackSpeed.getFriendlyName() + " "; + mEntryValues[index] = whitelistTypePlaybackSpeed; + index++; + } + if (sponsorBlockIncluded) { + mEntries[index] = " " + whitelistTypeSponsorBlock.getFriendlyName() + " "; + mEntryValues[index] = whitelistTypeSponsorBlock; + } + } + } + + private void init() { + setOnPreferenceClickListener(this); + } + + public WhitelistedChannelsPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + init(); + } + + public WhitelistedChannelsPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + public WhitelistedChannelsPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public WhitelistedChannelsPreference(Context context) { + super(context); + init(); + } + + @Override + public boolean onPreferenceClick(Preference preference) { + showWhitelistedChannelDialog(getContext()); + + return true; + } + + public static void showWhitelistedChannelDialog(Context context) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(str("revanced_whitelist_settings_title")); + builder.setItems(mEntries, (dialog, which) -> showWhitelistedChannelDialog(context, mEntryValues[which])); + builder.setNegativeButton(android.R.string.cancel, null); + builder.show(); + } + + private static void showWhitelistedChannelDialog(Context context, WhitelistType whitelistType) { + final ArrayList mEntries = Whitelist.getWhitelistedChannels(whitelistType); + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(whitelistType.getFriendlyName()); + + if (mEntries.isEmpty()) { + TextView emptyView = new TextView(context); + emptyView.setText(str("revanced_whitelist_empty")); + emptyView.setTextAlignment(View.TEXT_ALIGNMENT_TEXT_START); + emptyView.setTextSize(16); + emptyView.setPadding(60, 40, 60, 0); + builder.setView(emptyView); + } else { + LinearLayout entriesContainer = new LinearLayout(context); + entriesContainer.setOrientation(LinearLayout.VERTICAL); + for (final VideoChannel entry : mEntries) { + String author = entry.getChannelName(); + View entryView = getEntryView(context, author, v -> new AlertDialog.Builder(context) + .setMessage(str("revanced_whitelist_remove_dialog_message", author, whitelistType.getFriendlyName())) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + Whitelist.removeFromWhitelist(whitelistType, entry.getChannelId()); + entriesContainer.removeView(entriesContainer.findViewWithTag(author)); + }) + .setNegativeButton(android.R.string.cancel, null) + .show()); + entryView.setTag(author); + entriesContainer.addView(entryView); + } + builder.setView(entriesContainer); + } + + builder.setPositiveButton(android.R.string.ok, null); + builder.show(); + } + + private static View getEntryView(Context context, CharSequence entry, View.OnClickListener onDeleteClickListener) { + LinearLayout.LayoutParams entryContainerParams = new LinearLayout.LayoutParams( + new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.WRAP_CONTENT)); + entryContainerParams.setMargins(60, 40, 60, 0); + + LinearLayout.LayoutParams entryLabelLayoutParams = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1); + entryLabelLayoutParams.gravity = Gravity.CENTER; + + LinearLayout entryContainer = new LinearLayout(context); + entryContainer.setOrientation(LinearLayout.HORIZONTAL); + entryContainer.setLayoutParams(entryContainerParams); + + TextView entryLabel = new TextView(context); + entryLabel.setText(entry); + entryLabel.setLayoutParams(entryLabelLayoutParams); + entryLabel.setTextSize(16); + entryLabel.setOnClickListener(onDeleteClickListener); + + ImageButton deleteButton = new ImageButton(context); + deleteButton.setImageDrawable(ThemeUtils.getTrashButtonDrawable()); + deleteButton.setOnClickListener(onDeleteClickListener); + deleteButton.setBackground(null); + + entryContainer.addView(entryLabel); + entryContainer.addView(deleteButton); + return entryContainer; + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/BottomSheetState.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/BottomSheetState.kt new file mode 100644 index 000000000..2d8b513a3 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/BottomSheetState.kt @@ -0,0 +1,51 @@ +package app.revanced.extension.youtube.shared + +import app.revanced.extension.shared.utils.Event +import app.revanced.extension.shared.utils.Logger + +/** + * BottomSheetState bottom sheet state. + */ +enum class BottomSheetState { + CLOSED, + OPEN; + + companion object { + + @JvmStatic + fun set(enum: BottomSheetState) { + if (current != enum) { + Logger.printDebug { "BottomSheetState changed to: ${enum.name}" } + current = enum + } + } + + /** + * The current bottom sheet state. + */ + @JvmStatic + var current + get() = currentBottomSheetState + private set(value) { + currentBottomSheetState = value + onChange(currentBottomSheetState) + } + + @Volatile // value is read/write from different threads + private var currentBottomSheetState = CLOSED + + /** + * bottom sheet state change listener + */ + @JvmStatic + val onChange = Event() + } + + /** + * Check if the bottom sheet is [OPEN]. + * Useful for checking if a bottom sheet is open. + */ + fun isOpen(): Boolean { + return this == OPEN + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/LockModeState.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/LockModeState.kt new file mode 100644 index 000000000..9a330e687 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/LockModeState.kt @@ -0,0 +1,56 @@ +package app.revanced.extension.youtube.shared + +import app.revanced.extension.shared.utils.Event +import app.revanced.extension.shared.utils.Logger + +/** + * LockModeState. + */ +enum class LockModeState { + LOCK_MODE_STATE_ENUM_UNKNOWN, + LOCK_MODE_STATE_ENUM_UNLOCKED, + LOCK_MODE_STATE_ENUM_LOCKED, + LOCK_MODE_STATE_ENUM_CAN_UNLOCK, + LOCK_MODE_STATE_ENUM_UNLOCK_EXPANDED, + LOCK_MODE_STATE_ENUM_LOCKED_TEMPORARY_SUSPENSION; + + companion object { + + private val nameToLockModeState = entries.associateBy { it.name } + + @JvmStatic + fun setFromString(enumName: String) { + val newType = nameToLockModeState[enumName] + if (newType == null) { + Logger.printException { "Unknown LockModeState encountered: $enumName" } + } else if (current != newType) { + Logger.printDebug { "LockModeState changed to: $newType" } + current = newType + } + } + + /** + * The current lock mode state. + */ + @JvmStatic + var current + get() = currentLockModeState + private set(value) { + currentLockModeState = value + onChange(value) + } + + @Volatile // value is read/write from different threads + private var currentLockModeState = LOCK_MODE_STATE_ENUM_UNKNOWN + + /** + * player type change listener + */ + @JvmStatic + val onChange = Event() + } + + fun isLocked(): Boolean { + return this == LOCK_MODE_STATE_ENUM_LOCKED || this == LOCK_MODE_STATE_ENUM_UNLOCK_EXPANDED + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java new file mode 100644 index 000000000..0f3c07105 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java @@ -0,0 +1,282 @@ +package app.revanced.extension.youtube.shared; + +import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton.CREATE; + +import android.app.Activity; +import android.view.View; + +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class NavigationBar { + + /** + * How long to wait for the set nav button latch to be released. Maximum wait time must + * be as small as possible while still allowing enough time for the nav bar to update. + *

+ * YT calls it's back button handlers out of order, + * and litho starts filtering before the navigation bar is updated. + *

+ * Fixing this situation and not needlessly waiting requires somehow + * detecting if a back button key-press will cause a tab change. + *

+ * Typically after pressing the back button, the time between the first litho event and + * when the nav button is updated is about 10-20ms. Using 50-100ms here should be enough time + * and not noticeable, since YT typically takes 100-200ms (or more) to update the view anyways. + *

+ * This issue can also be avoided on a patch by patch basis, by avoiding calls to + * {@link NavigationButton#getSelectedNavigationButton()} unless absolutely necessary. + */ + private static final long LATCH_AWAIT_TIMEOUT_MILLISECONDS = 75; + + /** + * Used as a workaround to fix the issue of YT calling back button handlers out of order. + * Used to hold calls to {@link NavigationButton#getSelectedNavigationButton()} + * until the current navigation button can be determined. + *

+ * Only used when the hardware back button is pressed. + */ + @Nullable + private static volatile CountDownLatch navButtonLatch; + + /** + * Map of nav button layout views to Enum type. + * No synchronization is needed, and this is always accessed from the main thread. + */ + private static final Map viewToButtonMap = new WeakHashMap<>(); + + static { + // On app startup litho can start before the navigation bar is initialized. + // Force it to wait until the nav bar is updated. + createNavButtonLatch(); + } + + private static void createNavButtonLatch() { + navButtonLatch = new CountDownLatch(1); + } + + private static void releaseNavButtonLatch() { + CountDownLatch latch = navButtonLatch; + if (latch != null) { + navButtonLatch = null; + latch.countDown(); + } + } + + private static void waitForNavButtonLatchIfNeeded() { + CountDownLatch latch = navButtonLatch; + if (latch == null) { + return; + } + + if (Utils.isCurrentlyOnMainThread()) { + // The latch is released from the main thread, and waiting from the main thread will always timeout. + // This situation has only been observed when navigating out of a submenu and not changing tabs. + // and for that use case the nav bar does not change so it's safe to return here. + Logger.printDebug(() -> "Cannot block main thread waiting for nav button. Using last known navbar button status."); + return; + } + + try { + Logger.printDebug(() -> "Latch wait started"); + if (latch.await(LATCH_AWAIT_TIMEOUT_MILLISECONDS, TimeUnit.MILLISECONDS)) { + // Back button changed the navigation tab. + Logger.printDebug(() -> "Latch wait complete"); + return; + } + + // Timeout occurred, and a normal event when pressing the physical back button + // does not change navigation tabs. + releaseNavButtonLatch(); // Prevent other threads from waiting for no reason. + Logger.printDebug(() -> "Latch wait timed out"); + + } catch (InterruptedException ex) { + Logger.printException(() -> "Latch wait interrupted failure", ex); // Will never happen. + } + } + + /** + * Last YT navigation enum loaded. Not necessarily the active navigation tab. + * Always accessed from the main thread. + */ + @Nullable + private static String lastYTNavigationEnumName; + + /** + * Injection point. + */ + public static void setLastAppNavigationEnum(@Nullable Enum ytNavigationEnumName) { + if (ytNavigationEnumName != null) { + lastYTNavigationEnumName = ytNavigationEnumName.name(); + } + } + + /** + * Injection point. + */ + public static void navigationTabLoaded(final View navigationButtonGroup) { + try { + String lastEnumName = lastYTNavigationEnumName; + + for (NavigationButton buttonType : NavigationButton.values()) { + if (buttonType.ytEnumNames.contains(lastEnumName)) { + Logger.printDebug(() -> "navigationTabLoaded: " + lastEnumName); + viewToButtonMap.put(navigationButtonGroup, buttonType); + navigationTabCreatedCallback(buttonType, navigationButtonGroup); + return; + } + } + + // Log the unknown tab as exception level, only if debug is enabled. + // This is because unknown tabs do no harm, and it's only relevant to developers. + if (Settings.ENABLE_DEBUG_LOGGING.get()) { + Logger.printException(() -> "Unknown tab: " + lastEnumName + + " view: " + navigationButtonGroup.getClass()); + } + } catch (Exception ex) { + Logger.printException(() -> "navigationTabLoaded failure", ex); + } + } + + /** + * Injection point. + *

+ * Unique hook just for the 'Create' and 'You' tab. + */ + public static void navigationImageResourceTabLoaded(View view) { + // 'You' tab has no YT enum name and the enum hook is not called for it. + // Compare the last enum to figure out which tab this actually is. + if (CREATE.ytEnumNames.contains(lastYTNavigationEnumName)) { + navigationTabLoaded(view); + } else { + lastYTNavigationEnumName = NavigationButton.LIBRARY.ytEnumNames.get(0); + navigationTabLoaded(view); + } + } + + /** + * Injection point. + */ + public static void navigationTabSelected(View navButtonImageView, boolean isSelected) { + try { + if (!isSelected) { + return; + } + + NavigationButton button = viewToButtonMap.get(navButtonImageView); + + if (button == null) { // An unknown tab was selected. + // Show a toast only if debug mode is enabled. + if (Settings.ENABLE_DEBUG_LOGGING.get()) { + Logger.printException(() -> "Unknown navigation view selected: " + navButtonImageView); + } + + NavigationButton.selectedNavigationButton = null; + return; + } + + NavigationButton.selectedNavigationButton = button; + Logger.printDebug(() -> "Changed to navigation button: " + button); + + // Release any threads waiting for the selected nav button. + releaseNavButtonLatch(); + } catch (Exception ex) { + Logger.printException(() -> "navigationTabSelected failure", ex); + } + } + + /** + * Injection point. + */ + public static void onBackPressed(Activity activity) { + Logger.printDebug(() -> "Back button pressed"); + createNavButtonLatch(); + } + + /** + * @noinspection EmptyMethod + */ + private static void navigationTabCreatedCallback(NavigationButton button, View tabView) { + // Code is added during patching. + } + + public enum NavigationButton { + HOME("PIVOT_HOME", "TAB_HOME_CAIRO"), + SHORTS("TAB_SHORTS", "TAB_SHORTS_CAIRO"), + /** + * Create new video tab. + * This tab will never be in a selected state, even if the create video UI is on screen. + */ + CREATE("CREATION_TAB_LARGE", "CREATION_TAB_LARGE_CAIRO"), + SUBSCRIPTIONS("PIVOT_SUBSCRIPTIONS", "TAB_SUBSCRIPTIONS_CAIRO"), + /** + * Notifications tab. Only present when + * {@link Settings#SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON} is active. + */ + NOTIFICATIONS("TAB_ACTIVITY", "TAB_ACTIVITY_CAIRO"), + /** + * Library tab, including if the user is in incognito mode or when logged out. + */ + LIBRARY( + // Modern library tab with 'You' layout. + // The hooked YT code does not use an enum, and a dummy name is used here. + "YOU_LIBRARY_DUMMY_PLACEHOLDER_NAME", + // User is logged out. + "ACCOUNT_CIRCLE", + "ACCOUNT_CIRCLE_CAIRO", + // User is logged in with incognito mode enabled. + "INCOGNITO_CIRCLE", + "INCOGNITO_CAIRO", + // Old library tab (pre 'You' layout), only present when version spoofing. + "VIDEO_LIBRARY_WHITE", + // 'You' library tab that is sometimes momentarily loaded. + // This might be a temporary tab while the user profile photo is loading, + // but its exact purpose is not entirely clear. + "PIVOT_LIBRARY" + ); + + @Nullable + private static volatile NavigationButton selectedNavigationButton; + + /** + * This will return null only if the currently selected tab is unknown. + * This scenario will only happen if the UI has different tabs due to an A/B user test + * or YT abruptly changes the navigation layout for some other reason. + *

+ * All code calling this method should handle a null return value. + *

+ * Due to issues with how YT processes physical back button events, + * this patch uses workarounds that can cause this method to take up to 75ms + * if the device back button was recently pressed. + * + * @return The active navigation tab. + * If the user is in the upload video UI, this returns tab that is still visually + * selected on screen (whatever tab the user was on before tapping the upload button). + */ + @Nullable + public static NavigationButton getSelectedNavigationButton() { + waitForNavButtonLatchIfNeeded(); + return selectedNavigationButton; + } + + /** + * YouTube enum name for this tab. + */ + private final List ytEnumNames; + + NavigationButton(String... ytEnumNames) { + this.ytEnumNames = Arrays.asList(ytEnumNames); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibility.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibility.kt new file mode 100644 index 000000000..e9d5468d4 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibility.kt @@ -0,0 +1,43 @@ +package app.revanced.extension.youtube.shared + +import app.revanced.extension.shared.utils.Logger + +/** + * PlayerControls visibility state. + */ +enum class PlayerControlsVisibility { + PLAYER_CONTROLS_VISIBILITY_UNKNOWN, + PLAYER_CONTROLS_VISIBILITY_WILL_HIDE, + PLAYER_CONTROLS_VISIBILITY_HIDDEN, + PLAYER_CONTROLS_VISIBILITY_WILL_SHOW, + PLAYER_CONTROLS_VISIBILITY_SHOWN; + + companion object { + + private val nameToPlayerControlsVisibility = values().associateBy { it.name } + + @JvmStatic + fun setFromString(enumName: String) { + val state = nameToPlayerControlsVisibility[enumName] + if (state == null) { + Logger.printException { "Unknown PlayerControlsVisibility encountered: $enumName" } + } else if (currentPlayerControlsVisibility != state) { + Logger.printDebug { "PlayerControlsVisibility changed to: $state" } + currentPlayerControlsVisibility = state + } + } + + /** + * Depending on which hook this is called from, + * this value may not be up to date with the actual playback state. + */ + @JvmStatic + var current: PlayerControlsVisibility? + get() = currentPlayerControlsVisibility + private set(value) { + currentPlayerControlsVisibility = value + } + + private var currentPlayerControlsVisibility: PlayerControlsVisibility? = null + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibilityObserver.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibilityObserver.kt new file mode 100644 index 000000000..569292d85 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibilityObserver.kt @@ -0,0 +1,89 @@ +package app.revanced.extension.youtube.shared + +import android.app.Activity +import android.view.View +import android.view.ViewGroup +import app.revanced.extension.shared.utils.ResourceUtils.ResourceType +import app.revanced.extension.shared.utils.ResourceUtils.getIdentifier +import java.lang.ref.WeakReference + +/** + * default implementation of [PlayerControlsVisibilityObserver] + * + * @param activity activity that contains the controls_layout view + */ +class PlayerControlsVisibilityObserverImpl( + private val activity: Activity +) : PlayerControlsVisibilityObserver { + + /** + * id of the direct parent of controls_layout, R.id.controls_button_group_layout + */ + private val controlsLayoutParentId = + getIdentifier("controls_button_group_layout", ResourceType.ID, activity) + + /** + * id of R.id.player_control_play_pause_replay_button_touch_area + */ + private val controlsLayoutId = + getIdentifier( + "player_control_play_pause_replay_button_touch_area", + ResourceType.ID, + activity + ) + + /** + * reference to the controls layout view + */ + private var controlsLayoutView = WeakReference(null) + + /** + * is the [controlsLayoutView] set to a valid reference of a view? + */ + private val isAttached: Boolean + get() { + val view = controlsLayoutView.get() + return view != null && view.parent != null + } + + /** + * find and attach the controls_layout view if needed + */ + private fun maybeAttach() { + if (isAttached) return + + // find parent, then controls_layout view + // this is needed because there may be two views where id=R.id.controls_layout + // because why should google confine themselves to their own guidelines... + activity.findViewById(controlsLayoutParentId)?.let { parent -> + parent.findViewById(controlsLayoutId)?.let { + controlsLayoutView = WeakReference(it) + } + } + } + + override val playerControlsVisibility: Int + get() { + maybeAttach() + return controlsLayoutView.get()?.visibility ?: View.GONE + } + + override val arePlayerControlsVisible: Boolean + get() = playerControlsVisibility == View.VISIBLE +} + +/** + * provides the visibility status of the fullscreen player controls_layout view. + * this can be used for detecting when the player controls are shown + */ +interface PlayerControlsVisibilityObserver { + /** + * current visibility int of the controls_layout view + */ + val playerControlsVisibility: Int + + /** + * is the value of [playerControlsVisibility] equal to [View.VISIBLE]? + */ + val arePlayerControlsVisible: Boolean +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt new file mode 100644 index 000000000..9bfaffe58 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt @@ -0,0 +1,150 @@ +package app.revanced.extension.youtube.shared + +import app.revanced.extension.shared.utils.Event +import app.revanced.extension.shared.utils.Logger + +/** + * WatchWhile player type + */ +enum class PlayerType { + /** + * Either no video, or a Short is playing. + */ + NONE, + + /** + * A Short is playing. Occurs if a regular video is first opened + * and then a Short is opened (without first closing the regular video). + */ + HIDDEN, + + /** + * A regular video is minimized. + * + * When spoofing to 16.x YouTube and watching a short with a regular video in the background, + * the type can be this (and not [HIDDEN]). + */ + WATCH_WHILE_MINIMIZED, + WATCH_WHILE_MAXIMIZED, + WATCH_WHILE_FULLSCREEN, + WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN, + WATCH_WHILE_SLIDING_MINIMIZED_MAXIMIZED, + + /** + * Player is either sliding to [HIDDEN] state because a Short was opened while a regular video is on screen. + * OR + * The user has swiped a minimized player away to be closed (and no Short is being opened). + */ + WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED, + WATCH_WHILE_SLIDING_FULLSCREEN_DISMISSED, + + /** + * Home feed video playback. + */ + INLINE_MINIMAL, + VIRTUAL_REALITY_FULLSCREEN, + WATCH_WHILE_PICTURE_IN_PICTURE; + + companion object { + + private val nameToPlayerType = entries.associateBy { it.name } + + @JvmStatic + fun setFromString(enumName: String) { + val newType = nameToPlayerType[enumName] + if (newType == null) { + Logger.printException { "Unknown PlayerType encountered: $enumName" } + } else if (current != newType) { + Logger.printDebug { "PlayerType changed to: $newType" } + current = newType + } + } + + /** + * The current player type. + */ + @JvmStatic + var current + get() = currentPlayerType + private set(value) { + currentPlayerType = value + onChange(value) + } + + @Volatile // value is read/write from different threads + private var currentPlayerType = NONE + + /** + * player type change listener + */ + @JvmStatic + val onChange = Event() + } + + /** + * Check if the current player type is [NONE] or [HIDDEN]. + * Useful to check if a short is currently playing. + * + * Does not include the first moment after a short is opened when a regular video is minimized on screen, + * or while watching a short with a regular video present on a spoofed 16.x version of YouTube. + * To include those situations instead use [isNoneHiddenOrMinimized]. + */ + fun isNoneOrHidden(): Boolean { + return this == NONE || this == HIDDEN + } + + /** + * Check if the current player type is + * [NONE], [HIDDEN], [WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED]. + * + * Useful to check if a Short is being played or opened. + * + * Usually covers all use cases with no false positives, except if called from some hooks + * when spoofing to an old version this will return false even + * though a Short is being opened or is on screen (see [isNoneHiddenOrMinimized]). + * + * @return If nothing, a Short, or a regular video is sliding off screen to a dismissed or hidden state. + */ + fun isNoneHiddenOrSlidingMinimized(): Boolean { + return isNoneOrHidden() || this == WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED + } + + /** + * Check if the current player type is + * [NONE], [HIDDEN], [WATCH_WHILE_MINIMIZED], [WATCH_WHILE_SLIDING_MINIMIZED_DISMISSED]. + * + * Useful to check if a Short is being played, + * although will return false positive if a regular video is + * opened and minimized (and a Short is not playing or being opened). + * + * Typically used to detect if a Short is playing when the player cannot be in a minimized state, + * such as the user interacting with a button or element of the player. + * + * @return If nothing, a Short, a regular video is sliding off screen to a dismissed or hidden state, + * a regular video is minimized (and a new video is not being opened). + */ + fun isNoneHiddenOrMinimized(): Boolean { + return isNoneHiddenOrSlidingMinimized() || this == WATCH_WHILE_MINIMIZED + } + + /** + * Check if the current player type is + * [WATCH_WHILE_MAXIMIZED], [WATCH_WHILE_FULLSCREEN]. + * + * Useful to check if a regular video is being played. + */ + fun isMaximizedOrFullscreen(): Boolean { + return this == WATCH_WHILE_MAXIMIZED || this == WATCH_WHILE_FULLSCREEN + } + + /** + * Check if the current player type is + * [WATCH_WHILE_FULLSCREEN], [WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN]. + * + * Useful to check if a video is fullscreen. + */ + fun isFullScreenOrSlidingFullScreen(): Boolean { + return this == WATCH_WHILE_FULLSCREEN || this == WATCH_WHILE_SLIDING_MAXIMIZED_FULLSCREEN + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlaylistIdPrefix.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlaylistIdPrefix.java new file mode 100644 index 000000000..2197c3eaf --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlaylistIdPrefix.java @@ -0,0 +1,39 @@ +package app.revanced.extension.youtube.shared; + +import androidx.annotation.NonNull; + +public enum PlaylistIdPrefix { + /** + * To check all available prefixes, + * See this document. + */ + ALL_CONTENTS_WITH_TIME_ASCENDING("UL", false), + ALL_CONTENTS_WITH_TIME_DESCENDING("UU", true), + ALL_CONTENTS_WITH_POPULAR_DESCENDING("PU", true), + VIDEOS_ONLY_WITH_TIME_DESCENDING("UULF", true), + VIDEOS_ONLY_WITH_POPULAR_DESCENDING("UULP", true), + SHORTS_ONLY_WITH_TIME_DESCENDING("UUSH", true), + SHORTS_ONLY_WITH_POPULAR_DESCENDING("UUPS", true), + LIVESTREAMS_ONLY_WITH_TIME_DESCENDING("UULV", true), + LIVESTREAMS_ONLY_WITH_POPULAR_DESCENDING("UUPV", true), + ALL_MEMBERSHIPS_CONTENTS("UUMO", true), + MEMBERSHIPS_VIDEOS_ONLY("UUMF", true), + MEMBERSHIPS_SHORTS_ONLY("UUMS", true), + MEMBERSHIPS_LIVESTREAMS_ONLY("UUMV", true); + + /** + * Prefix of playlist id. + */ + @NonNull + public final String prefixId; + + /** + * Whether to use channelId. + */ + public final boolean useChannelId; + + PlaylistIdPrefix(@NonNull String prefixId, boolean useChannelId) { + this.prefixId = prefixId; + this.useChannelId = useChannelId; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java new file mode 100644 index 000000000..1a6d97edd --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/RootView.java @@ -0,0 +1,37 @@ +package app.revanced.extension.youtube.shared; + +import static app.revanced.extension.youtube.patches.components.RelatedVideoFilter.isActionBarVisible; + +@SuppressWarnings("unused") +public final class RootView { + + /** + * @return If the search bar is on screen. This includes if the player + * is on screen and the search results are behind the player (and not visible). + * Detecting the search is covered by the player can be done by checking {@link RootView#isPlayerActive()}. + */ + public static boolean isSearchBarActive() { + String searchQuery = getSearchQuery(); + return !searchQuery.isEmpty(); + } + + public static boolean isPlayerActive() { + return PlayerType.getCurrent().isMaximizedOrFullscreen() || isActionBarVisible.get(); + } + + /** + * Get current BrowseId. + * Rest of the implementation added by patch. + */ + public static String getBrowseId() { + return ""; + } + + /** + * Get current SearchQuery. + * Rest of the implementation added by patch. + */ + public static String getSearchQuery() { + return ""; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt new file mode 100644 index 000000000..b0aed2e79 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/ShortsPlayerState.kt @@ -0,0 +1,51 @@ +package app.revanced.extension.youtube.shared + +import app.revanced.extension.shared.utils.Event +import app.revanced.extension.shared.utils.Logger + +/** + * ShortsPlayerState shorts player state. + */ +enum class ShortsPlayerState { + CLOSED, + OPEN; + + companion object { + + @JvmStatic + fun set(enum: ShortsPlayerState) { + if (current != enum) { + Logger.printDebug { "ShortsPlayerState changed to: ${enum.name}" } + current = enum + } + } + + /** + * The current shorts player state. + */ + @JvmStatic + var current + get() = currentShortsPlayerState + private set(value) { + currentShortsPlayerState = value + onChange(value) + } + + @Volatile // value is read/write from different threads + private var currentShortsPlayerState = CLOSED + + /** + * shorts player state change listener + */ + @JvmStatic + val onChange = Event() + } + + /** + * Check if the shorts player is [CLOSED]. + * Useful for checking if a shorts player is closed. + */ + fun isClosed(): Boolean { + return this == CLOSED + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/VideoInformation.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/VideoInformation.java new file mode 100644 index 000000000..06217d432 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/VideoInformation.java @@ -0,0 +1,573 @@ +package app.revanced.extension.youtube.shared; + +import static app.revanced.extension.shared.utils.ResourceUtils.getString; +import static app.revanced.extension.shared.utils.Utils.getFormattedTimeStamp; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.List; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.utils.AlwaysRepeatPatch; + +/** + * Hooking class for the current playing video. + */ +@SuppressWarnings("all") +public final class VideoInformation { + private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f; + private static final int DEFAULT_YOUTUBE_VIDEO_QUALITY = -2; + private static final String DEFAULT_YOUTUBE_VIDEO_QUALITY_STRING = getString("quality_auto"); + /** + * Prefix present in all Short player parameters signature. + */ + private static final String SHORTS_PLAYER_PARAMETERS = "8AEB"; + /** + * Prefix that presents in the player parameter signature when a user manually opens a YouTube Mix and plays a video included in the YouTube Mix. + */ + private static final String YOUTUBE_MIX_PLAYER_PARAMETERS = "8AUB"; + /** + * Prefix present in all YouTube Mix (auto-generated playlist) playlist id. + */ + private static final String YOUTUBE_MIX_PLAYLIST_ID_PREFIX = "RD"; + + @NonNull + private static String channelId = ""; + @NonNull + private static String channelName = ""; + @NonNull + private static String videoId = ""; + @NonNull + private static String videoTitle = ""; + private static long videoLength = 0; + private static boolean videoIsLiveStream; + private static long videoTime = -1; + + @NonNull + private static volatile String playerResponseVideoId = ""; + private static volatile boolean playerResponseVideoIdIsShort; + private static volatile boolean videoIdIsShort; + private static volatile boolean playerResponseVideoIdIsAutoGeneratedMixPlaylist; + + /** + * The current playback speed + */ + private static float playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED; + /** + * The current video quality + */ + private static int videoQuality = DEFAULT_YOUTUBE_VIDEO_QUALITY; + /** + * The current video quality string + */ + private static String videoQualityString = DEFAULT_YOUTUBE_VIDEO_QUALITY_STRING; + /** + * The available qualities of the current video in human readable form: [1080, 720, 480] + */ + @Nullable + private static List videoQualities; + + private static boolean qualityNeedsUpdating; + + /** + * Injection point. + */ + public static void initialize() { + videoTime = -1; + videoLength = 0; + playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED; + Logger.printDebug(() -> "Initialized Player"); + } + + /** + * Injection point. + */ + public static void initializeMdx() { + Logger.printDebug(() -> "Initialized Mdx Player"); + } + + public static boolean seekTo(final long seekTime) { + return seekTo(seekTime, getVideoLength()); + } + + /** + * Seek on the current video. + * Does not function for playback of Shorts. + *

+ * Caution: If called from a videoTimeHook() callback, + * this will cause a recursive call into the same videoTimeHook() callback. + * + * @param seekTime The seekTime to seek the video to. + * @return true if the seek was successful. + */ + public static boolean seekTo(final long seekTime, final long videoLength) { + Utils.verifyOnMainThread(); + try { + final long videoTime = getVideoTime(); + final long adjustedSeekTime = getAdjustedSeekTime(seekTime, videoLength); + + Logger.printDebug(() -> "Seeking to: " + getFormattedTimeStamp(adjustedSeekTime)); + + // Try regular playback controller first, and it will not succeed if casting. + if (overrideVideoTime(adjustedSeekTime)) return true; + Logger.printDebug(() -> "seekTo did not succeeded. Trying MXD."); + // Else the video is loading or changing videos, or video is casting to a different device. + + // Try calling the seekTo method of the MDX player director (called when casting). + // The difference has to be a different second mark in order to avoid infinite skip loops + // as the Lounge API only supports seconds. + if (adjustedSeekTime / 1000 == videoTime / 1000) { + Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small " + + "(" + (adjustedSeekTime - videoTime) + "ms)"); + return false; + } + + return overrideMDXVideoTime(adjustedSeekTime); + } catch (Exception ex) { + Logger.printException(() -> "Failed to seek", ex); + return false; + } + } + + // Prevent issues such as play/pause button or autoplay not working. + private static long getAdjustedSeekTime(final long seekTime, final long videoLength) { + // If the user skips to a section that is 500 ms before the video length, + // it will get stuck in a loop. + if (videoLength - seekTime > 500) { + return seekTime; + } + + // Both the current video time and the seekTo are in the last 500ms of the video. + if (AlwaysRepeatPatch.alwaysRepeatEnabled()) { + // If always-repeat is turned on, just skips to time 0. + return 0; + } else { + // Otherwise, just skips to a time longer than the video length. + // Paradoxically, if user skips to a section much longer than the video length, does not get stuck in a loop. + return Integer.MAX_VALUE; + } + } + + /** + * Seeks a relative amount. Should always be used over {@link #seekTo(long)} + * when the desired seek time is an offset of the current time. + * + * @noinspection UnusedReturnValue + */ + public static boolean seekToRelative(long seekTime) { + Utils.verifyOnMainThread(); + try { + Logger.printDebug(() -> "Seeking relative to: " + seekTime); + + // Try regular playback controller first, and it will not succeed if casting. + if (overrideVideoTimeRelative(seekTime)) return true; + Logger.printDebug(() -> "seekToRelative did not succeeded. Trying MXD."); + + // Adjust the fine adjustment function so it's at least 1 second before/after. + // Otherwise the fine adjustment will do nothing when casting. + final long adjustedSeekTime = seekTime < 0 + ? Math.min(seekTime, -1000) + : Math.max(seekTime, 1000); + + return overrideMDXVideoTimeRelative(adjustedSeekTime); + } catch (Exception ex) { + Logger.printException(() -> "Failed to seek relative", ex); + return false; + } + } + + /** + * Injection point. + * + * @param newlyLoadedChannelId id of the current channel. + * @param newlyLoadedChannelName name of the current channel. + * @param newlyLoadedVideoId id of the current video. + * @param newlyLoadedVideoTitle title of the current video. + * @param newlyLoadedVideoLength length of the video in milliseconds. + * @param newlyLoadedLiveStreamValue whether the current video is a livestream. + */ + public static void setVideoInformation(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, + @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, + final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { + if (videoId.equals(newlyLoadedVideoId)) + return; + + channelId = newlyLoadedChannelId; + channelName = newlyLoadedChannelName; + videoId = newlyLoadedVideoId; + videoTitle = newlyLoadedVideoTitle; + videoLength = newlyLoadedVideoLength; + videoIsLiveStream = newlyLoadedLiveStreamValue; + + Logger.printDebug(() -> + "channelId='" + + newlyLoadedChannelId + + "'\nchannelName='" + + newlyLoadedChannelName + + "'\nvideoId='" + + newlyLoadedVideoId + + "'\nvideoTitle='" + + newlyLoadedVideoTitle + + "'\nvideoLength=" + + getFormattedTimeStamp(newlyLoadedVideoLength) + + "videoIsLiveStream='" + + newlyLoadedLiveStreamValue + + "'" + ); + } + + /** + * Injection point. + * + * @param newlyLoadedVideoId id of the current video + */ + public static void setVideoId(@NonNull String newlyLoadedVideoId) { + if (videoId.equals(newlyLoadedVideoId)) + return; + + videoId = newlyLoadedVideoId; + } + + /** + * Id of the last video opened. Includes Shorts. + * + * @return The id of the video, or an empty string if no videos have been opened yet. + */ + @NonNull + public static String getVideoId() { + return videoId; + } + + /** + * Channel Name of the last video opened. Includes Shorts. + * + * @return The channel name of the video. + */ + @NonNull + public static String getChannelName() { + return channelName; + } + + /** + * ChannelId of the last video opened. Includes Shorts. + * + * @return The channel id of the video. + */ + @NonNull + public static String getChannelId() { + return channelId; + } + + public static boolean getLiveStreamState() { + return videoIsLiveStream; + } + + + /** + * Differs from {@link #videoId} as this is the video id for the + * last player response received, which may not be the last video opened. + *

+ * If Shorts are loading the background, this commonly will be + * different from the Short that is currently on screen. + *

+ * For most use cases, you should instead use {@link #getVideoId()}. + * + * @return The id of the last video loaded, or an empty string if no videos have been loaded yet. + */ + @NonNull + public static String getPlayerResponseVideoId() { + return playerResponseVideoId; + } + + + /** + * @return If the last player response video id was a Short. + * Includes Shorts shelf items appearing in the feed that are not opened. + * @see #lastVideoIdIsShort() + */ + public static boolean lastPlayerResponseIsShort() { + return playerResponseVideoIdIsShort; + } + + /** + * @return If the last player response video id _that was opened_ was a Short. + */ + public static boolean lastVideoIdIsShort() { + return videoIdIsShort; + } + + /** + * @return If the last player response video id was an auto-generated YouTube Mix. + */ + public static boolean lastPlayerResponseIsAutoGeneratedMixPlaylist() { + return playerResponseVideoIdIsAutoGeneratedMixPlaylist; + } + + /** + * @return If the player parameters are for a Short. + */ + public static boolean playerParametersAreShort(@Nullable String playerParameter) { + return playerParameter != null && playerParameter.startsWith(SHORTS_PLAYER_PARAMETERS); + } + + /** + * @return Whether given id belongs to a YouTube Mix. + */ + private static boolean isYoutubeMixId(@Nullable final String playlistId) { + return playlistId != null && playlistId.startsWith(YOUTUBE_MIX_PLAYLIST_ID_PREFIX); + } + + /** + * Whether the user manually opened a YouTube Mix. + */ + public static boolean isMixPlaylistsOpenedByUser(String parameter) { + return parameter != null && (parameter.isEmpty() || parameter.startsWith(YOUTUBE_MIX_PLAYER_PARAMETERS)); + } + + /** + * Injection point. + */ + @Nullable + public static String newPlayerResponseParameter(@NonNull String videoId, @Nullable String playerParameter, + @Nullable String playlistId, boolean isShortAndOpeningOrPlaying) { + final boolean isShort = playerParametersAreShort(playerParameter); + playerResponseVideoIdIsShort = isShort; + if (!isShort || isShortAndOpeningOrPlaying) { + if (videoIdIsShort != isShort) { + videoIdIsShort = isShort; + } + } + playerResponseVideoIdIsAutoGeneratedMixPlaylist = isYoutubeMixId(playlistId) && !isMixPlaylistsOpenedByUser(playerParameter); + return playerParameter; // Return the original value since we are observing and not modifying. + } + + /** + * Injection point. Called off the main thread. + * + * @param videoId The id of the last video loaded. + */ + public static void setPlayerResponseVideoId(@NonNull String videoId, boolean isShortAndOpeningOrPlaying) { + if (!playerResponseVideoId.equals(videoId)) { + playerResponseVideoId = videoId; + } + } + + /** + * @return The current playback speed. + */ + public static float getPlaybackSpeed() { + return playbackSpeed; + } + + /** + * 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; + } + } + + /** + * @return The current video quality. + */ + public static int getVideoQuality() { + return videoQuality; + } + + /** + * @return The current video quality string. + */ + public static String getVideoQualityString() { + return videoQualityString; + } + + /** + * Injection point. + * + * @param newlyLoadedQuality The current video quality string. + */ + public static void setVideoQuality(String newlyLoadedQuality) { + if (newlyLoadedQuality == null) { + return; + } + try { + String splitVideoQuality; + if (newlyLoadedQuality.contains("p")) { + splitVideoQuality = newlyLoadedQuality.split("p")[0]; + videoQuality = Integer.parseInt(splitVideoQuality); + videoQualityString = splitVideoQuality + "p"; + } else if (newlyLoadedQuality.contains("s")) { + splitVideoQuality = newlyLoadedQuality.split("s")[0]; + videoQuality = Integer.parseInt(splitVideoQuality); + videoQualityString = splitVideoQuality + "s"; + } else { + videoQuality = DEFAULT_YOUTUBE_VIDEO_QUALITY; + videoQualityString = DEFAULT_YOUTUBE_VIDEO_QUALITY_STRING; + } + } catch (NumberFormatException ignored) { + } + } + + /** + * @return available video quality. + */ + public static int getAvailableVideoQuality(int preferredQuality) { + if (!qualityNeedsUpdating || videoQualities == null) { + return preferredQuality; + } + qualityNeedsUpdating = false; + + int qualityToUse = videoQualities.get(0); // first element is automatic mode + for (Integer quality : videoQualities) { + if (quality <= preferredQuality && qualityToUse < quality) { + qualityToUse = quality; + } + } + return qualityToUse; + } + + /** + * Injection point. + * + * @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2 + */ + public static void setVideoQualityList(Object[] qualities) { + try { + if (videoQualities == null || videoQualities.size() != qualities.length) { + videoQualities = new ArrayList<>(qualities.length); + for (Object streamQuality : qualities) { + for (Field field : streamQuality.getClass().getFields()) { + if (field.getType().isAssignableFrom(Integer.TYPE) + && field.getName().length() <= 2) { + videoQualities.add(field.getInt(streamQuality)); + } + } + } + qualityNeedsUpdating = true; + Logger.printDebug(() -> "videoQualities: " + videoQualities); + } + } catch (Exception ex) { + Logger.printException(() -> "Failed to set quality list", ex); + } + } + + /** + * Length of the current video playing. Includes Shorts. + * + * @return The length of the video in milliseconds. + * If the video is not yet loaded, or if the video is playing in the background with no video visible, + * then this returns zero. + */ + public static long getVideoLength() { + return videoLength; + } + + /** + * Playback time of the current video playing. Includes Shorts. + *

+ * Value will lag behind the actual playback time by a variable amount based on the playback speed. + *

+ * If playback speed is 2.0x, this value may be up to 2000ms behind the actual playback time. + * If playback speed is 1.0x, this value may be up to 1000ms behind the actual playback time. + * If playback speed is 0.5x, this value may be up to 500ms behind the actual playback time. + * Etc. + * + * @return The time of the video in milliseconds. -1 if not set yet. + */ + public static long getVideoTime() { + return videoTime; + } + + public static long getVideoTimeInSeconds() { + return videoTime / 1000; + } + + /** + * Injection point. + * Called on the main thread every 100ms. + * + * @param time The current playback time of the video in milliseconds. + */ + public static void setVideoTime(final long time) { + videoTime = time; + Logger.printDebug(() -> "setVideoTime: " + getFormattedTimeStamp(time)); + } + + /** + * @return If the playback is at the end of the video. + *

+ * If video is playing in the background with no video visible, + * this always returns false (even if the video is actually at the end). + *

+ * This is equivalent to checking for {@link VideoState#ENDED}, + * but can give a more up-to-date result for code calling from some hooks. + * @see VideoState + */ + public static boolean isAtEndOfVideo() { + 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); + } + + /** + * Overrides the current quality. + * Rest of the implementation added by patch. + */ + public static void overrideVideoQuality(int qualityOverride) { + Logger.printDebug(() -> "Overriding video quality to: " + qualityOverride); + } + + /** + * Overrides the current video time by seeking. + * Rest of the implementation added by patch. + */ + public static boolean overrideVideoTime(final long seekTime) { + // These instructions are ignored by patch. + Logger.printDebug(() -> "Seeking to " + seekTime); + return false; + } + + /** + * Overrides the current video time by seeking. (MDX player) + * Rest of the implementation added by patch. + */ + public static boolean overrideMDXVideoTime(final long seekTime) { + // These instructions are ignored by patch. + Logger.printDebug(() -> "Seeking to " + seekTime); + return false; + } + + /** + * Overrides the current video time by seeking relative. + * Rest of the implementation added by patch. + */ + public static boolean overrideVideoTimeRelative(final long seekTime) { + // These instructions are ignored by patch. + Logger.printDebug(() -> "Seeking to " + seekTime); + return false; + } + + /** + * Overrides the current video time by seeking relative. (MDX player) + * Rest of the implementation added by patch. + */ + public static boolean overrideMDXVideoTimeRelative(final long seekTime) { + // These instructions are ignored by patch. + Logger.printDebug(() -> "Seeking to " + seekTime); + return false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/VideoState.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/VideoState.kt new file mode 100644 index 000000000..4e1888a7c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/VideoState.kt @@ -0,0 +1,44 @@ +package app.revanced.extension.youtube.shared + +import app.revanced.extension.shared.utils.Logger + +/** + * VideoState playback state. + */ +enum class VideoState { + NEW, + PLAYING, + PAUSED, + RECOVERABLE_ERROR, + UNRECOVERABLE_ERROR, + ENDED; + + companion object { + + private val nameToVideoState = entries.associateBy { it.name } + + @JvmStatic + fun setFromString(enumName: String) { + val state = nameToVideoState[enumName] + if (state == null) { + Logger.printException { "Unknown VideoState encountered: $enumName" } + } else if (current != state) { + Logger.printDebug { "VideoState changed to: $state" } + current = state + } + } + + /** + * Depending on which hook this is called from, + * this value may not be up to date with the actual playback state. + */ + @JvmStatic + var current + get() = currentVideoState + private set(value) { + currentVideoState = value + } + + private var currentVideoState: VideoState? = null + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java new file mode 100644 index 000000000..948f2d044 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java @@ -0,0 +1,797 @@ +package app.revanced.extension.youtube.sponsorblock; + +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.youtube.utils.VideoUtils.getFormattedTimeStamp; + +import android.annotation.SuppressLint; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.text.TextUtils; +import android.util.TypedValue; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; +import app.revanced.extension.youtube.shared.VideoInformation; +import app.revanced.extension.youtube.shared.VideoState; +import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour; +import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory; +import app.revanced.extension.youtube.sponsorblock.objects.SponsorSegment; +import app.revanced.extension.youtube.sponsorblock.requests.SBRequester; +import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController; +import app.revanced.extension.youtube.whitelist.Whitelist; + +/** + * Handles showing, scheduling, and skipping of all {@link SponsorSegment} for the current video. + *

+ * Class is not thread safe. All methods must be called on the main thread unless otherwise specified. + */ +@SuppressWarnings("unused") +public class SegmentPlaybackController { + /** + * Length of time to show a skip button for a highlight segment, + * or a regular segment if {@link Settings#SB_AUTO_HIDE_SKIP_BUTTON} is enabled. + *

+ * Effectively this value is rounded up to the next second. + */ + private static final long DURATION_TO_SHOW_SKIP_BUTTON = 3800; + + /* + * Highlight segments have zero length as they are a point in time. + * Draw them on screen using a fixed width bar. + * Value is independent of device dpi. + */ + private static final int HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH = 7; + /** + * Used to prevent re-showing a previously hidden skip button when exiting an embedded segment. + * Only used when {@link Settings#SB_AUTO_HIDE_SKIP_BUTTON} is enabled. + *

+ * A collection of segments that have automatically hidden the skip button for, and all segments in this list + * contain the current video time. Segment are removed when playback exits the segment. + */ + private static final List hiddenSkipSegmentsForCurrentVideoTime = new ArrayList<>(); + @NonNull + private static String videoId = ""; + private static long videoLength = 0; + + @Nullable + private static SponsorSegment[] segments; + /** + * Highlight segment, if one exists and the skip behavior is not set to {@link CategoryBehaviour#SHOW_IN_SEEKBAR}. + */ + @Nullable + private static SponsorSegment highlightSegment; + /** + * Because loading can take time, show the skip to highlight for a few seconds after the segments load. + * This is the system time (in milliseconds) to no longer show the initial display skip to highlight. + * Value will be zero if no highlight segment exists, or if the system time to show the highlight has passed. + */ + private static long highlightSegmentInitialShowEndTime; + /** + * Currently playing (non-highlight) segment that user can manually skip. + */ + @Nullable + private static SponsorSegment segmentCurrentlyPlaying; + /** + * Currently playing manual skip segment that is scheduled to hide. + * This will always be NULL or equal to {@link #segmentCurrentlyPlaying}. + */ + @Nullable + private static SponsorSegment scheduledHideSegment; + /** + * Upcoming segment that is scheduled to either autoskip or show the manual skip button. + */ + @Nullable + private static SponsorSegment scheduledUpcomingSegment; + /** + * System time (in milliseconds) of when to hide the skip button of {@link #segmentCurrentlyPlaying}. + * Value is zero if playback is not inside a segment ({@link #segmentCurrentlyPlaying} is null), + * or if {@link Settings#SB_AUTO_HIDE_SKIP_BUTTON} is not enabled. + */ + private static long skipSegmentButtonEndTime; + + @Nullable + private static String timeWithoutSegments; + + private static int sponsorBarAbsoluteLeft; + private static int sponsorAbsoluteBarRight; + private static int sponsorBarThickness; + private static SponsorSegment lastSegmentSkipped; + private static long lastSegmentSkippedTime; + private static int toastNumberOfSegmentsSkipped; + @Nullable + private static SponsorSegment toastSegmentSkipped; + private static int highlightSegmentTimeBarScreenWidth = -1; // actual pixel width to use + + @Nullable + static SponsorSegment[] getSegments() { + return segments; + } + + private static void setSegments(@NonNull SponsorSegment[] videoSegments) { + Arrays.sort(videoSegments); + segments = videoSegments; + calculateTimeWithoutSegments(); + + if (SegmentCategory.HIGHLIGHT.behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY + || SegmentCategory.HIGHLIGHT.behaviour == CategoryBehaviour.MANUAL_SKIP) { + for (SponsorSegment segment : videoSegments) { + if (segment.category == SegmentCategory.HIGHLIGHT) { + highlightSegment = segment; + return; + } + } + } + highlightSegment = null; + } + + static void addUnsubmittedSegment(@NonNull SponsorSegment segment) { + Objects.requireNonNull(segment); + if (segments == null) { + segments = new SponsorSegment[1]; + } else { + segments = Arrays.copyOf(segments, segments.length + 1); + } + segments[segments.length - 1] = segment; + setSegments(segments); + } + + static void removeUnsubmittedSegments() { + if (segments == null || segments.length == 0) { + return; + } + List replacement = new ArrayList<>(); + for (SponsorSegment segment : segments) { + if (segment.category != SegmentCategory.UNSUBMITTED) { + replacement.add(segment); + } + } + if (replacement.size() != segments.length) { + setSegments(replacement.toArray(new SponsorSegment[0])); + } + } + + public static boolean videoHasSegments() { + return segments != null && segments.length > 0; + } + + /** + * Clears all downloaded data. + */ + public static void clearData() { + videoId = ""; + videoLength = 0; + segments = null; + highlightSegment = null; + highlightSegmentInitialShowEndTime = 0; + timeWithoutSegments = null; + segmentCurrentlyPlaying = null; + scheduledUpcomingSegment = null; + scheduledHideSegment = null; + skipSegmentButtonEndTime = 0; + toastSegmentSkipped = null; + toastNumberOfSegmentsSkipped = 0; + hiddenSkipSegmentsForCurrentVideoTime.clear(); + } + + /** + * Injection point. + * Initializes SponsorBlock when the video player starts playing a new video. + */ + public static void initialize() { + try { + Utils.verifyOnMainThread(); + SponsorBlockSettings.initialize(); + clearData(); + SponsorBlockViewController.hideAll(); + SponsorBlockUtils.clearUnsubmittedSegmentTimes(); + Logger.printDebug(() -> "Initialized SponsorBlock"); + } catch (Exception ex) { + Logger.printException(() -> "Failed to initialize SponsorBlock", ex); + } + } + + /** + * Injection point. + */ + public static void newVideoStarted(@NonNull String newlyLoadedChannelId, @NonNull String newlyLoadedChannelName, + @NonNull String newlyLoadedVideoId, @NonNull String newlyLoadedVideoTitle, + final long newlyLoadedVideoLength, boolean newlyLoadedLiveStreamValue) { + try { + if (Objects.equals(videoId, newlyLoadedVideoId)) { + return; + } + clearData(); + if (!Settings.SB_ENABLED.get()) { + return; + } + if (PlayerType.getCurrent().isNoneOrHidden()) { + Logger.printDebug(() -> "ignoring Short"); + return; + } + if (Utils.isNetworkNotConnected()) { + Logger.printDebug(() -> "Network not connected, ignoring video"); + return; + } + + videoId = newlyLoadedVideoId; + videoLength = newlyLoadedVideoLength; + Logger.printDebug(() -> "newVideoStarted: " + newlyLoadedVideoId); + + if (Whitelist.isChannelWhitelistedSponsorBlock(newlyLoadedChannelId)) { + return; + } + + Utils.runOnBackgroundThread(() -> { + try { + executeDownloadSegments(newlyLoadedVideoId); + } catch (Exception e) { + Logger.printException(() -> "Failed to download segments", e); + } + }); + } catch (Exception ex) { + Logger.printException(() -> "setCurrentVideoId failure", ex); + } + } + + /** + * Id of the last video opened. Includes Shorts. + * + * @return The id of the video, or an empty string if no videos have been opened yet. + */ + @NonNull + public static String getVideoId() { + return videoId; + } + + /** + * Length of the current video playing. Includes Shorts. + * + * @return The length of the video in milliseconds. + * If the video is not yet loaded, or if the video is playing in the background with no video visible, + * then this returns zero. + */ + public static long getVideoLength() { + return videoLength; + } + + /** + * Must be called off main thread + */ + static void executeDownloadSegments(@NonNull String newlyLoadedVideoId) { + Objects.requireNonNull(newlyLoadedVideoId); + try { + SponsorSegment[] segments = SBRequester.getSegments(newlyLoadedVideoId); + + Utils.runOnMainThread(() -> { + if (!newlyLoadedVideoId.equals(videoId)) { + // user changed videos before get segments network call could complete + Logger.printDebug(() -> "Ignoring segments for prior video: " + newlyLoadedVideoId); + return; + } + setSegments(segments); + + final long videoTime = VideoInformation.getVideoTime(); + if (highlightSegment != null) { + // If the current video time is before the highlight. + final long timeUntilHighlight = highlightSegment.start - videoTime; + if (timeUntilHighlight > 0) { + if (highlightSegment.shouldAutoSkip()) { + skipSegment(highlightSegment, false); + return; + } + highlightSegmentInitialShowEndTime = System.currentTimeMillis() + Math.min( + (long) (timeUntilHighlight / VideoInformation.getPlaybackSpeed()), + DURATION_TO_SHOW_SKIP_BUTTON); + } + } + + // check for any skips now, instead of waiting for the next update to setVideoTime() + setVideoTime(videoTime); + }); + } catch (Exception ex) { + Logger.printException(() -> "executeDownloadSegments failure", ex); + } + } + + /** + * Injection point. + * Updates SponsorBlock every 100ms. + * When changing videos, this is first called with value 0 and then the video is changed. + */ + public static void setVideoTime(long millis) { + try { + if (!Settings.SB_ENABLED.get() + || PlayerType.getCurrent().isNoneOrHidden() // Shorts playback. + || segments == null || segments.length == 0) { + return; + } + Logger.printDebug(() -> "setVideoTime: " + getFormattedTimeStamp(millis)); + + updateHiddenSegments(millis); + + final float playbackSpeed = VideoInformation.getPlaybackSpeed(); + // Amount of time to look ahead for the next segment, + // and the threshold to determine if a scheduled show/hide is at the correct video time when it's run. + // + // This value must be greater than largest time between calls to this method (1000ms), + // and must be adjusted for the video speed. + // + // To debug the stale skip logic, set this to a very large value (5000 or more) + // then try manually seeking just before playback reaches a segment skip. + final long speedAdjustedTimeThreshold = (long) (playbackSpeed * 1000); + final long startTimerLookAheadThreshold = millis + speedAdjustedTimeThreshold; + + SponsorSegment foundSegmentCurrentlyPlaying = null; + SponsorSegment foundUpcomingSegment = null; + + for (final SponsorSegment segment : segments) { + if (segment.category.behaviour == CategoryBehaviour.SHOW_IN_SEEKBAR + || segment.category.behaviour == CategoryBehaviour.IGNORE + || segment.category == SegmentCategory.HIGHLIGHT) { + continue; + } + if (segment.end <= millis) { + continue; // past this segment + } + + if (segment.start <= millis) { + // we are in the segment! + if (segment.shouldAutoSkip()) { + skipSegment(segment, false); + return; // must return, as skipping causes a recursive call back into this method + } + + // first found segment, or it's an embedded segment and fully inside the outer segment + if (foundSegmentCurrentlyPlaying == null || foundSegmentCurrentlyPlaying.containsSegment(segment)) { + // If the found segment is not currently displayed, then do not show if the segment is nearly over. + // This check prevents the skip button text from rapidly changing when multiple segments end at nearly the same time. + // Also prevents showing the skip button if user seeks into the last 800ms of the segment. + final long minMillisOfSegmentRemainingThreshold = 800; + if (segmentCurrentlyPlaying == segment + || !segment.endIsNear(millis, minMillisOfSegmentRemainingThreshold)) { + foundSegmentCurrentlyPlaying = segment; + } else { + Logger.printDebug(() -> "Ignoring segment that ends very soon: " + segment); + } + } + // Keep iterating and looking. There may be an upcoming autoskip, + // or there may be another smaller segment nested inside this segment + continue; + } + + // segment is upcoming + if (startTimerLookAheadThreshold < segment.start) { + break; // segment is not close enough to schedule, and no segments after this are of interest + } + if (segment.shouldAutoSkip()) { // upcoming autoskip + foundUpcomingSegment = segment; + break; // must stop here + } + + // upcoming manual skip + + // do not schedule upcoming segment, if it is not fully contained inside the current segment + if ((foundSegmentCurrentlyPlaying == null || foundSegmentCurrentlyPlaying.containsSegment(segment)) + // use the most inner upcoming segment + && (foundUpcomingSegment == null || foundUpcomingSegment.containsSegment(segment))) { + + // Only schedule, if the segment start time is not near the end time of the current segment. + // This check is needed to prevent scheduled hide and show from clashing with each other. + // Instead the upcoming segment will be handled when the current segment scheduled hide calls back into this method. + final long minTimeBetweenStartEndOfSegments = 1000; + if (foundSegmentCurrentlyPlaying == null + || !foundSegmentCurrentlyPlaying.endIsNear(segment.start, minTimeBetweenStartEndOfSegments)) { + foundUpcomingSegment = segment; + } else { + Logger.printDebug(() -> "Not scheduling segment (start time is near end of current segment): " + segment); + } + } + } + + if (highlightSegment != null) { + if (millis < DURATION_TO_SHOW_SKIP_BUTTON || (highlightSegmentInitialShowEndTime != 0 + && System.currentTimeMillis() < highlightSegmentInitialShowEndTime)) { + SponsorBlockViewController.showSkipHighlightButton(highlightSegment); + } else { + highlightSegmentInitialShowEndTime = 0; + SponsorBlockViewController.hideSkipHighlightButton(); + } + } + + if (segmentCurrentlyPlaying != foundSegmentCurrentlyPlaying) { + setSegmentCurrentlyPlaying(foundSegmentCurrentlyPlaying); + } else if (foundSegmentCurrentlyPlaying != null + && skipSegmentButtonEndTime != 0 && skipSegmentButtonEndTime <= System.currentTimeMillis()) { + Logger.printDebug(() -> "Auto hiding skip button for segment: " + segmentCurrentlyPlaying); + skipSegmentButtonEndTime = 0; + hiddenSkipSegmentsForCurrentVideoTime.add(foundSegmentCurrentlyPlaying); + SponsorBlockViewController.hideSkipSegmentButton(); + } + + // schedule a hide, only if the segment end is near + final SponsorSegment segmentToHide = + (foundSegmentCurrentlyPlaying != null && foundSegmentCurrentlyPlaying.endIsNear(millis, speedAdjustedTimeThreshold)) + ? foundSegmentCurrentlyPlaying + : null; + + if (scheduledHideSegment != segmentToHide) { + if (segmentToHide == null) { + Logger.printDebug(() -> "Clearing scheduled hide: " + scheduledHideSegment); + scheduledHideSegment = null; + } else { + scheduledHideSegment = segmentToHide; + Logger.printDebug(() -> "Scheduling hide segment: " + segmentToHide + " playbackSpeed: " + playbackSpeed); + final long delayUntilHide = (long) ((segmentToHide.end - millis) / playbackSpeed); + Utils.runOnMainThreadDelayed(() -> { + if (scheduledHideSegment != segmentToHide) { + Logger.printDebug(() -> "Ignoring old scheduled hide segment: " + segmentToHide); + return; + } + scheduledHideSegment = null; + if (VideoState.getCurrent() != VideoState.PLAYING) { + Logger.printDebug(() -> "Ignoring scheduled hide segment as video is paused: " + segmentToHide); + return; + } + + final long videoTime = VideoInformation.getVideoTime(); + if (!segmentToHide.endIsNear(videoTime, speedAdjustedTimeThreshold)) { + // current video time is not what's expected. User paused playback + Logger.printDebug(() -> "Ignoring outdated scheduled hide: " + segmentToHide + + " videoInformation time: " + videoTime); + return; + } + Logger.printDebug(() -> "Running scheduled hide segment: " + segmentToHide); + // Need more than just hide the skip button, as this may have been an embedded segment + // Instead call back into setVideoTime to check everything again. + // Should not use VideoInformation time as it is less accurate, + // but this scheduled handler was scheduled precisely so we can just use the segment end time + setSegmentCurrentlyPlaying(null); + setVideoTime(segmentToHide.end); + }, delayUntilHide); + } + } + + if (scheduledUpcomingSegment != foundUpcomingSegment) { + if (foundUpcomingSegment == null) { + Logger.printDebug(() -> "Clearing scheduled segment: " + scheduledUpcomingSegment); + scheduledUpcomingSegment = null; + } else { + scheduledUpcomingSegment = foundUpcomingSegment; + final SponsorSegment segmentToSkip = foundUpcomingSegment; + + Logger.printDebug(() -> "Scheduling segment: " + segmentToSkip + " playbackSpeed: " + playbackSpeed); + final long delayUntilSkip = (long) ((segmentToSkip.start - millis) / playbackSpeed); + Utils.runOnMainThreadDelayed(() -> { + if (scheduledUpcomingSegment != segmentToSkip) { + Logger.printDebug(() -> "Ignoring old scheduled segment: " + segmentToSkip); + return; + } + scheduledUpcomingSegment = null; + if (VideoState.getCurrent() != VideoState.PLAYING) { + Logger.printDebug(() -> "Ignoring scheduled hide segment as video is paused: " + segmentToSkip); + return; + } + + final long videoTime = VideoInformation.getVideoTime(); + if (!segmentToSkip.startIsNear(videoTime, speedAdjustedTimeThreshold)) { + // current video time is not what's expected. User paused playback + Logger.printDebug(() -> "Ignoring outdated scheduled segment: " + segmentToSkip + + " videoInformation time: " + videoTime); + return; + } + if (segmentToSkip.shouldAutoSkip()) { + Logger.printDebug(() -> "Running scheduled skip segment: " + segmentToSkip); + skipSegment(segmentToSkip, false); + } else { + Logger.printDebug(() -> "Running scheduled show segment: " + segmentToSkip); + setSegmentCurrentlyPlaying(segmentToSkip); + } + }, delayUntilSkip); + } + } + } catch (Exception e) { + Logger.printException(() -> "setVideoTime failure", e); + } + } + + /** + * Removes all previously hidden segments that are not longer contained in the given video time. + */ + private static void updateHiddenSegments(long currentVideoTime) { + Iterator i = hiddenSkipSegmentsForCurrentVideoTime.iterator(); + while (i.hasNext()) { + SponsorSegment hiddenSegment = i.next(); + if (!hiddenSegment.containsTime(currentVideoTime)) { + Logger.printDebug(() -> "Resetting hide skip button: " + hiddenSegment); + i.remove(); + } + } + } + + private static void setSegmentCurrentlyPlaying(@Nullable SponsorSegment segment) { + if (segment == null) { + if (segmentCurrentlyPlaying != null) + Logger.printDebug(() -> "Hiding segment: " + segmentCurrentlyPlaying); + segmentCurrentlyPlaying = null; + skipSegmentButtonEndTime = 0; + SponsorBlockViewController.hideSkipSegmentButton(); + return; + } + segmentCurrentlyPlaying = segment; + skipSegmentButtonEndTime = 0; + if (Settings.SB_AUTO_HIDE_SKIP_BUTTON.get()) { + if (hiddenSkipSegmentsForCurrentVideoTime.contains(segment)) { + // Playback exited a nested segment and the outer segment skip button was previously hidden. + Logger.printDebug(() -> "Ignoring previously auto-hidden segment: " + segment); + SponsorBlockViewController.hideSkipSegmentButton(); + return; + } + skipSegmentButtonEndTime = System.currentTimeMillis() + DURATION_TO_SHOW_SKIP_BUTTON; + } + Logger.printDebug(() -> "Showing segment: " + segment); + SponsorBlockViewController.showSkipSegmentButton(segment); + } + + private static void skipSegment(@NonNull SponsorSegment segmentToSkip, boolean userManuallySkipped) { + try { + SponsorBlockViewController.hideSkipHighlightButton(); + SponsorBlockViewController.hideSkipSegmentButton(); + + final long now = System.currentTimeMillis(); + if (lastSegmentSkipped == segmentToSkip) { + // If trying to seek to end of the video, YouTube can seek just before of the actual end. + // (especially if the video does not end on a whole second boundary). + // This causes additional segment skip attempts, even though it cannot seek any closer to the desired time. + // Check for and ignore repeated skip attempts of the same segment over a small time period. + final long minTimeBetweenSkippingSameSegment = Math.max(500, (long) (500 / VideoInformation.getPlaybackSpeed())); + if (now - lastSegmentSkippedTime < minTimeBetweenSkippingSameSegment) { + Logger.printDebug(() -> "Ignoring skip segment request (already skipped as close as possible): " + segmentToSkip); + return; + } + } + + Logger.printDebug(() -> "Skipping segment: " + segmentToSkip); + lastSegmentSkipped = segmentToSkip; + lastSegmentSkippedTime = now; + setSegmentCurrentlyPlaying(null); + scheduledHideSegment = null; + scheduledUpcomingSegment = null; + if (segmentToSkip == highlightSegment) { + highlightSegmentInitialShowEndTime = 0; + } + + // If the seek is successful, then the seek causes a recursive call back into this class. + final boolean seekSuccessful = VideoInformation.seekTo(segmentToSkip.end, getVideoLength()); + if (!seekSuccessful) { + // can happen when switching videos and is normal + Logger.printDebug(() -> "Could not skip segment (seek unsuccessful): " + segmentToSkip); + return; + } + + final boolean videoIsPaused = VideoState.getCurrent() == VideoState.PAUSED; + if (!userManuallySkipped) { + // check for any smaller embedded segments, and count those as autoskipped + final boolean showSkipToast = Settings.SB_TOAST_ON_SKIP.get(); + for (final SponsorSegment otherSegment : Objects.requireNonNull(segments)) { + if (segmentToSkip.end < otherSegment.start) { + break; // no other segments can be contained + } + if (otherSegment == segmentToSkip || + (otherSegment.category != SegmentCategory.HIGHLIGHT && segmentToSkip.containsSegment(otherSegment))) { + otherSegment.didAutoSkipped = true; + // Do not show a toast if the user is scrubbing thru a paused video. + // Cannot do this video state check in setTime or earlier in this method, as the video state may not be up to date. + // So instead, only hide toasts because all other skip logic done while paused causes no harm. + if (showSkipToast && !videoIsPaused) { + showSkippedSegmentToast(otherSegment); + } + } + } + } + + if (segmentToSkip.category == SegmentCategory.UNSUBMITTED) { + removeUnsubmittedSegments(); + SponsorBlockUtils.setNewSponsorSegmentPreviewed(); + } else if (!videoIsPaused) { + SponsorBlockUtils.sendViewRequestAsync(segmentToSkip); + } + } catch (Exception ex) { + Logger.printException(() -> "skipSegment failure", ex); + } + } + + private static void showSkippedSegmentToast(@NonNull SponsorSegment segment) { + Utils.verifyOnMainThread(); + toastNumberOfSegmentsSkipped++; + if (toastNumberOfSegmentsSkipped > 1) { + return; // toast already scheduled + } + toastSegmentSkipped = segment; + + final long delayToToastMilliseconds = 250; // also the maximum time between skips to be considered skipping multiple segments + Utils.runOnMainThreadDelayed(() -> { + try { + if (toastSegmentSkipped == null) { // video was changed just after skipping segment + Logger.printDebug(() -> "Ignoring old scheduled show toast"); + return; + } + Utils.showToastShort(toastNumberOfSegmentsSkipped == 1 + ? toastSegmentSkipped.getSkippedToastText() + : str("revanced_sb_skipped_multiple_segments")); + } catch (Exception ex) { + Logger.printException(() -> "showSkippedSegmentToast failure", ex); + } finally { + toastNumberOfSegmentsSkipped = 0; + toastSegmentSkipped = null; + } + }, delayToToastMilliseconds); + } + + /** + * @param segment can be either a highlight or a regular manual skip segment. + */ + public static void onSkipSegmentClicked(@NonNull SponsorSegment segment) { + try { + if (segment != highlightSegment && segment != segmentCurrentlyPlaying) { + Logger.printException(() -> "error: segment not available to skip"); // should never happen + SponsorBlockViewController.hideSkipSegmentButton(); + SponsorBlockViewController.hideSkipHighlightButton(); + return; + } + skipSegment(segment, true); + } catch (Exception ex) { + Logger.printException(() -> "onSkipSegmentClicked failure", ex); + } + } + + /** + * Injection point + */ + public static void setSponsorBarRect(final Object self) { + try { + Field field = self.getClass().getDeclaredField("replaceMeWithsetSponsorBarRect"); + field.setAccessible(true); + Rect rect = (Rect) Objects.requireNonNull(field.get(self)); + setSponsorBarAbsoluteLeft(rect); + setSponsorBarAbsoluteRight(rect); + } catch (Exception ex) { + Logger.printException(() -> "setSponsorBarRect failure", ex); + } + } + + private static void setSponsorBarAbsoluteLeft(Rect rect) { + final int left = rect.left; + if (sponsorBarAbsoluteLeft != left) { + sponsorBarAbsoluteLeft = left; + } + } + + private static void setSponsorBarAbsoluteRight(Rect rect) { + final int right = rect.right; + if (sponsorAbsoluteBarRight != right) { + sponsorAbsoluteBarRight = right; + } + } + + /** + * Injection point + */ + public static void setSponsorBarThickness(int thickness) { + if (sponsorBarThickness != thickness) { + sponsorBarThickness = thickness; + } + } + + /** + * Injection point. + */ + public static String appendTimeWithoutSegments(String totalTime) { + try { + if (Settings.SB_ENABLED.get() && Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get() + && !TextUtils.isEmpty(totalTime) && !TextUtils.isEmpty(timeWithoutSegments)) { + // Force LTR layout, to match the same LTR video time/length layout YouTube uses for all languages + return "\u202D" + totalTime + timeWithoutSegments; // u202D = left to right override + } + } catch (Exception ex) { + Logger.printException(() -> "appendTimeWithoutSegments failure", ex); + } + + return totalTime; + } + + @SuppressLint("DefaultLocale") + private static void calculateTimeWithoutSegments() { + if (!Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get() || videoLength <= 0 + || segments == null || segments.length == 0) { + timeWithoutSegments = null; + return; + } + + boolean foundNonhighlightSegments = false; + long timeWithoutSegmentsValue = videoLength; + + for (int i = 0, length = segments.length; i < length; i++) { + SponsorSegment segment = segments[i]; + if (segment.category == SegmentCategory.HIGHLIGHT) { + continue; + } + foundNonhighlightSegments = true; + long start = segment.start; + final long end = segment.end; + // To prevent nested segments from incorrectly counting additional time, + // check if the segment overlaps any earlier segments. + for (int j = 0; j < i; j++) { + start = Math.max(start, segments[j].end); + } + if (start < end) { + timeWithoutSegmentsValue -= (end - start); + } + } + + if (!foundNonhighlightSegments) { + timeWithoutSegments = null; + return; + } + + final long hours = timeWithoutSegmentsValue / 3600000; + final long minutes = (timeWithoutSegmentsValue / 60000) % 60; + final long seconds = (timeWithoutSegmentsValue / 1000) % 60; + if (hours > 0) { + timeWithoutSegments = String.format(Locale.ENGLISH, "\u2009(%d:%02d:%02d)", hours, minutes, seconds); + } else { + timeWithoutSegments = String.format(Locale.ENGLISH, "\u2009(%d:%02d)", minutes, seconds); + } + } + + private static int getHighlightSegmentTimeBarScreenWidth() { + if (highlightSegmentTimeBarScreenWidth == -1) { + highlightSegmentTimeBarScreenWidth = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, HIGHLIGHT_SEGMENT_DRAW_BAR_WIDTH, + Objects.requireNonNull(Utils.getContext()).getResources().getDisplayMetrics()); + } + return highlightSegmentTimeBarScreenWidth; + } + + /** + * Injection point. + */ + public static void drawSponsorTimeBars(final Canvas canvas, final float posY) { + try { + if (segments == null) return; + if (videoLength <= 0) return; + + final int thicknessDiv2 = sponsorBarThickness / 2; // rounds down + final float top = posY - (sponsorBarThickness - thicknessDiv2); + final float bottom = posY + thicknessDiv2; + final float videoMillisecondsToPixels = (1f / videoLength) * (sponsorAbsoluteBarRight - sponsorBarAbsoluteLeft); + final float leftPadding = sponsorBarAbsoluteLeft; + + for (SponsorSegment segment : segments) { + final float left = leftPadding + segment.start * videoMillisecondsToPixels; + final float right; + if (segment.category == SegmentCategory.HIGHLIGHT) { + right = left + getHighlightSegmentTimeBarScreenWidth(); + } else { + right = leftPadding + segment.end * videoMillisecondsToPixels; + } + canvas.drawRect(left, top, right, bottom, segment.category.paint); + } + } catch (Exception ex) { + Logger.printException(() -> "drawSponsorTimeBars failure", ex); + } + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java new file mode 100644 index 000000000..d4228f3ec --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java @@ -0,0 +1,252 @@ +package app.revanced.extension.youtube.sponsorblock; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.app.AlertDialog; +import android.content.Context; +import android.util.Patterns; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.UUID; + +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.settings.preference.SponsorBlockSettingsPreference; +import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour; +import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory; + +public class SponsorBlockSettings { + /** + * Minimum length a SB user id must be, as set by SB API. + */ + private static final int SB_PRIVATE_USER_ID_MINIMUM_LENGTH = 30; + + public static final Setting.ImportExportCallback SB_IMPORT_EXPORT_CALLBACK = new Setting.ImportExportCallback() { + @Override + public void settingsImported(@Nullable Context context) { + SegmentCategory.loadAllCategoriesFromSettings(); + SponsorBlockSettingsPreference.updateSegmentCategories(); + } + + @Override + public void settingsExported(@Nullable Context context) { + showExportWarningIfNeeded(context); + } + }; + + public static void importDesktopSettings(@NonNull String json) { + Utils.verifyOnMainThread(); + try { + JSONObject settingsJson = new JSONObject(json); + JSONObject barTypesObject = settingsJson.getJSONObject("barTypes"); + JSONArray categorySelectionsArray = settingsJson.getJSONArray("categorySelections"); + + for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) { + // clear existing behavior, as browser plugin exports no behavior for ignored categories + category.setBehaviour(CategoryBehaviour.IGNORE); + if (barTypesObject.has(category.keyValue)) { + JSONObject categoryObject = barTypesObject.getJSONObject(category.keyValue); + category.setColor(categoryObject.getString("color")); + } + } + + for (int i = 0; i < categorySelectionsArray.length(); i++) { + JSONObject categorySelectionObject = categorySelectionsArray.getJSONObject(i); + + String categoryKey = categorySelectionObject.getString("name"); + SegmentCategory category = SegmentCategory.byCategoryKey(categoryKey); + if (category == null) { + continue; // unsupported category, ignore + } + + final int desktopValue = categorySelectionObject.getInt("option"); + CategoryBehaviour behaviour = CategoryBehaviour.byDesktopKeyValue(desktopValue); + if (behaviour == null) { + Utils.showToastLong(categoryKey + " unknown behavior key: " + categoryKey); + } else if (category == SegmentCategory.HIGHLIGHT && behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE) { + Utils.showToastLong("Skip-once behavior not allowed for " + category.keyValue); + category.setBehaviour(CategoryBehaviour.SKIP_AUTOMATICALLY); // use closest match + } else { + category.setBehaviour(behaviour); + } + } + SegmentCategory.updateEnabledCategories(); + + if (settingsJson.has("userID")) { + // User id does not exist if user never voted or created any segments. + String userID = settingsJson.getString("userID"); + if (isValidSBUserId(userID)) { + Settings.SB_PRIVATE_USER_ID.save(userID); + } + } + Settings.SB_USER_IS_VIP.save(settingsJson.getBoolean("isVip")); + Settings.SB_TOAST_ON_SKIP.save(!settingsJson.getBoolean("dontShowNotice")); + Settings.SB_TRACK_SKIP_COUNT.save(settingsJson.getBoolean("trackViewCount")); + Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save(settingsJson.getBoolean("showTimeWithSkips")); + + String serverAddress = settingsJson.getString("serverAddress"); + if (isValidSBServerAddress(serverAddress)) { // Old versions of ReVanced exported wrong url format + Settings.SB_API_URL.save(serverAddress); + } + + final float minDuration = (float) settingsJson.getDouble("minDuration"); + if (minDuration < 0) { + throw new IllegalArgumentException("invalid minDuration: " + minDuration); + } + Settings.SB_SEGMENT_MIN_DURATION.save(minDuration); + + if (settingsJson.has("skipCount")) { // Value not exported in old versions of ReVanced + int skipCount = settingsJson.getInt("skipCount"); + if (skipCount < 0) { + throw new IllegalArgumentException("invalid skipCount: " + skipCount); + } + Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.save(skipCount); + } + + if (settingsJson.has("minutesSaved")) { + final double minutesSaved = settingsJson.getDouble("minutesSaved"); + if (minutesSaved < 0) { + throw new IllegalArgumentException("invalid minutesSaved: " + minutesSaved); + } + Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.save((long) (minutesSaved * 60 * 1000)); + } + + Utils.showToastLong(str("revanced_sb_settings_import_successful")); + } catch (Exception ex) { + Logger.printInfo(() -> "failed to import settings", ex); // use info level, as we are showing our own toast + Utils.showToastLong(str("revanced_sb_settings_import_failed", ex.getMessage())); + } + } + + @NonNull + public static String exportDesktopSettings() { + Utils.verifyOnMainThread(); + try { + Logger.printDebug(() -> "Creating SponsorBlock export settings string"); + JSONObject json = new JSONObject(); + + JSONObject barTypesObject = new JSONObject(); // categories' colors + JSONArray categorySelectionsArray = new JSONArray(); // categories' behavior + + SegmentCategory[] categories = SegmentCategory.categoriesWithoutUnsubmitted(); + for (SegmentCategory category : categories) { + JSONObject categoryObject = new JSONObject(); + String categoryKey = category.keyValue; + categoryObject.put("color", category.colorString()); + barTypesObject.put(categoryKey, categoryObject); + + if (category.behaviour != CategoryBehaviour.IGNORE) { + JSONObject behaviorObject = new JSONObject(); + behaviorObject.put("name", categoryKey); + behaviorObject.put("option", category.behaviour.desktopKeyValue); + categorySelectionsArray.put(behaviorObject); + } + } + if (SponsorBlockSettings.userHasSBPrivateId()) { + json.put("userID", Settings.SB_PRIVATE_USER_ID.get()); + } + json.put("isVip", Settings.SB_USER_IS_VIP.get()); + json.put("serverAddress", Settings.SB_API_URL.get()); + json.put("dontShowNotice", !Settings.SB_TOAST_ON_SKIP.get()); + json.put("showTimeWithSkips", Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get()); + json.put("minDuration", Settings.SB_SEGMENT_MIN_DURATION.get()); + json.put("trackViewCount", Settings.SB_TRACK_SKIP_COUNT.get()); + json.put("skipCount", Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.get()); + json.put("minutesSaved", Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.get() / (60f * 1000)); + + json.put("categorySelections", categorySelectionsArray); + json.put("barTypes", barTypesObject); + + return json.toString(2); + } catch (Exception ex) { + Logger.printInfo(() -> "failed to export settings", ex); // use info level, as we are showing our own toast + Utils.showToastLong(str("revanced_sb_settings_export_failed", ex)); + return ""; + } + } + + /** + * Export the categories using flatten json (no embedded dictionaries or arrays). + */ + private static void showExportWarningIfNeeded(@Nullable Context dialogContext) { + Utils.verifyOnMainThread(); + initialize(); + + // If user has a SponsorBlock user id then show a warning. + if (dialogContext != null && SponsorBlockSettings.userHasSBPrivateId() + && !Settings.SB_HIDE_EXPORT_WARNING.get()) { + new AlertDialog.Builder(dialogContext) + .setMessage(str("revanced_sb_settings_revanced_export_user_id_warning")) + .setNeutralButton(str("revanced_sb_settings_revanced_export_user_id_warning_dismiss"), + (dialog, which) -> Settings.SB_HIDE_EXPORT_WARNING.save(true)) + .setPositiveButton(android.R.string.ok, null) + .setCancelable(false) + .show(); + } + } + + public static boolean isValidSBUserId(@NonNull String userId) { + return !userId.isEmpty() && userId.length() >= SB_PRIVATE_USER_ID_MINIMUM_LENGTH; + } + + /** + * A non comprehensive check if a SB api server address is valid. + */ + public static boolean isValidSBServerAddress(@NonNull String serverAddress) { + if (!Patterns.WEB_URL.matcher(serverAddress).matches()) { + return false; + } + // Verify url is only the server address and does not contain a path such as: "https://sponsor.ajay.app/api/" + // Could use Patterns.compile, but this is simpler + final int lastDotIndex = serverAddress.lastIndexOf('.'); + //noinspection RedundantIfStatement + if (lastDotIndex != -1 && serverAddress.substring(lastDotIndex).contains("/")) { + return false; + } + // Optionally, could also verify the domain exists using "InetAddress.getByName(serverAddress)" + // but that should not be done on the main thread. + // Instead, assume the domain exists and the user knows what they're doing. + return true; + } + + /** + * @return if the user has ever voted, created a segment, or imported existing SB settings. + */ + public static boolean userHasSBPrivateId() { + return !Settings.SB_PRIVATE_USER_ID.get().isEmpty(); + } + + /** + * Use this only if a user id is required (creating segments, voting). + */ + @NonNull + public static String getSBPrivateUserID() { + String uuid = Settings.SB_PRIVATE_USER_ID.get(); + if (uuid.isEmpty()) { + uuid = (UUID.randomUUID().toString() + + UUID.randomUUID().toString() + + UUID.randomUUID().toString()) + .replace("-", ""); + Settings.SB_PRIVATE_USER_ID.save(uuid); + } + return uuid; + } + + private static boolean initialized; + + public static void initialize() { + if (initialized) { + return; + } + initialized = true; + + SegmentCategory.updateEnabledCategories(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java new file mode 100644 index 000000000..56dc52977 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java @@ -0,0 +1,505 @@ +package app.revanced.extension.youtube.sponsorblock; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.annotation.TargetApi; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.text.Html; +import android.widget.EditText; + +import androidx.annotation.NonNull; + +import java.lang.ref.WeakReference; +import java.text.NumberFormat; +import java.time.Duration; +import java.util.Locale; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.VideoInformation; +import app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour; +import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory; +import app.revanced.extension.youtube.sponsorblock.objects.SponsorSegment; +import app.revanced.extension.youtube.sponsorblock.objects.SponsorSegment.SegmentVote; +import app.revanced.extension.youtube.sponsorblock.requests.SBRequester; +import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController; + +/** + * Not thread safe. All fields/methods must be accessed from the main thread. + * + * @noinspection deprecation + */ +public class SponsorBlockUtils { + private static final String LOCKED_COLOR = "#FFC83D"; + + private static final String MANUAL_EDIT_TIME_TEXT_HINT = "hh:mm:ss.sss"; + private static final Pattern manualEditTimePattern + = Pattern.compile("((\\d{1,2}):)?(\\d{1,2}):(\\d{2})(\\.(\\d{1,3}))?"); + private static final NumberFormat statsNumberFormatter = NumberFormat.getNumberInstance(); + + private static long newSponsorSegmentDialogShownMillis; + private static long newSponsorSegmentStartMillis = -1; + private static long newSponsorSegmentEndMillis = -1; + private static boolean newSponsorSegmentPreviewed; + private static final DialogInterface.OnClickListener newSponsorSegmentDialogListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + switch (which) { + // start + case DialogInterface.BUTTON_NEGATIVE -> + newSponsorSegmentStartMillis = newSponsorSegmentDialogShownMillis; + // end + case DialogInterface.BUTTON_POSITIVE -> + newSponsorSegmentEndMillis = newSponsorSegmentDialogShownMillis; + } + dialog.dismiss(); + } + }; + private static SegmentCategory newUserCreatedSegmentCategory; + private static final DialogInterface.OnClickListener segmentTypeListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + SegmentCategory category = SegmentCategory.categoriesWithoutHighlights()[which]; + final boolean enableButton; + if (category.behaviour == CategoryBehaviour.IGNORE) { + Utils.showToastLong(str("revanced_sb_new_segment_disabled_category")); + enableButton = false; + } else { + newUserCreatedSegmentCategory = category; + enableButton = true; + } + + ((AlertDialog) dialog) + .getButton(DialogInterface.BUTTON_POSITIVE) + .setEnabled(enableButton); + } catch (Exception ex) { + Logger.printException(() -> "segmentTypeListener failure", ex); + } + } + }; + private static final DialogInterface.OnClickListener segmentReadyDialogButtonListener = new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + try { + SponsorBlockViewController.hideNewSegmentLayout(); + Context context = ((AlertDialog) dialog).getContext(); + dialog.dismiss(); + + SegmentCategory[] categories = SegmentCategory.categoriesWithoutHighlights(); + CharSequence[] titles = new CharSequence[categories.length]; + for (int i = 0, length = categories.length; i < length; i++) { + titles[i] = categories[i].getTitleWithColorDot(); + } + + newUserCreatedSegmentCategory = null; + new AlertDialog.Builder(context) + .setTitle(str("revanced_sb_new_segment_choose_category")) + .setSingleChoiceItems(titles, -1, segmentTypeListener) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, segmentCategorySelectedDialogListener) + .show() + .getButton(DialogInterface.BUTTON_POSITIVE) + .setEnabled(false); + } catch (Exception ex) { + Logger.printException(() -> "segmentReadyDialogButtonListener failure", ex); + } + } + }; + private static final DialogInterface.OnClickListener segmentCategorySelectedDialogListener = (dialog, which) -> { + dialog.dismiss(); + submitNewSegment(); + }; + private static final EditByHandSaveDialogListener editByHandSaveDialogListener = new EditByHandSaveDialogListener(); + private static final DialogInterface.OnClickListener editByHandDialogListener = (dialog, which) -> { + try { + Context context = ((AlertDialog) dialog).getContext(); + + final boolean isStart = DialogInterface.BUTTON_NEGATIVE == which; + + final EditText textView = new EditText(context); + textView.setHint(MANUAL_EDIT_TIME_TEXT_HINT); + if (isStart) { + if (newSponsorSegmentStartMillis >= 0) + textView.setText(formatSegmentTime(newSponsorSegmentStartMillis)); + } else { + if (newSponsorSegmentEndMillis >= 0) + textView.setText(formatSegmentTime(newSponsorSegmentEndMillis)); + } + + editByHandSaveDialogListener.settingStart = isStart; + editByHandSaveDialogListener.editTextRef = new WeakReference<>(textView); + new AlertDialog.Builder(context) + .setTitle(str(isStart ? "revanced_sb_new_segment_time_start" : "revanced_sb_new_segment_time_end")) + .setView(textView) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(str("revanced_sb_new_segment_now"), editByHandSaveDialogListener) + .setPositiveButton(android.R.string.ok, editByHandSaveDialogListener) + .show(); + + dialog.dismiss(); + } catch (Exception ex) { + Logger.printException(() -> "editByHandDialogListener failure", ex); + } + }; + private static final DialogInterface.OnClickListener segmentVoteClickListener = (dialog, which) -> { + try { + final Context context = ((AlertDialog) dialog).getContext(); + SponsorSegment[] segments = SegmentPlaybackController.getSegments(); + if (segments == null || segments.length == 0) { + // should never be reached + Logger.printException(() -> "Segment is no longer available on the client"); + return; + } + SponsorSegment segment = segments[which]; + + SegmentVote[] voteOptions = (segment.category == SegmentCategory.HIGHLIGHT) + ? SegmentVote.voteTypesWithoutCategoryChange // highlight segments cannot change category + : SegmentVote.values(); + CharSequence[] items = new CharSequence[voteOptions.length]; + + for (int i = 0; i < voteOptions.length; i++) { + SegmentVote voteOption = voteOptions[i]; + String title = voteOption.title.toString(); + if (Settings.SB_USER_IS_VIP.get() && segment.isLocked && voteOption.shouldHighlight) { + items[i] = Html.fromHtml(String.format("%s", LOCKED_COLOR, title)); + } else { + items[i] = title; + } + } + + new AlertDialog.Builder(context) + .setItems(items, (dialog1, which1) -> { + SegmentVote voteOption = voteOptions[which1]; + switch (voteOption) { + case UPVOTE, DOWNVOTE -> + SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption); + case CATEGORY_CHANGE -> onNewCategorySelect(segment, context); + } + }) + .show(); + } catch (Exception ex) { + Logger.printException(() -> "segmentVoteClickListener failure", ex); + } + }; + + private SponsorBlockUtils() { + } + + static void setNewSponsorSegmentPreviewed() { + newSponsorSegmentPreviewed = true; + } + + static void clearUnsubmittedSegmentTimes() { + newSponsorSegmentDialogShownMillis = 0; + newSponsorSegmentEndMillis = newSponsorSegmentStartMillis = -1; + newSponsorSegmentPreviewed = false; + } + + private static void submitNewSegment() { + try { + Utils.verifyOnMainThread(); + final long start = newSponsorSegmentStartMillis; + final long end = newSponsorSegmentEndMillis; + final String videoId = SegmentPlaybackController.getVideoId(); + final long videoLength = SegmentPlaybackController.getVideoLength(); + final SegmentCategory segmentCategory = newUserCreatedSegmentCategory; + if (start < 0 || end < 0 || start >= end || videoLength <= 0 || videoId.isEmpty() || segmentCategory == null) { + Logger.printException(() -> "invalid parameters"); + return; + } + clearUnsubmittedSegmentTimes(); + Utils.runOnBackgroundThread(() -> { + SBRequester.submitSegments(videoId, segmentCategory.keyValue, start, end, videoLength); + SegmentPlaybackController.executeDownloadSegments(videoId); + }); + } catch (Exception e) { + Logger.printException(() -> "Unable to submit segment", e); + } + } + + public static void onMarkLocationClicked() { + try { + Utils.verifyOnMainThread(); + newSponsorSegmentDialogShownMillis = VideoInformation.getVideoTime(); + + new AlertDialog.Builder(SponsorBlockViewController.getOverLaysViewGroupContext()) + .setTitle(str("revanced_sb_new_segment_title")) + .setMessage(str("revanced_sb_new_segment_mark_current_time_as_question", + formatSegmentTime(newSponsorSegmentDialogShownMillis))) + .setNeutralButton(android.R.string.cancel, null) + .setNegativeButton(str("revanced_sb_new_segment_mark_start"), newSponsorSegmentDialogListener) + .setPositiveButton(str("revanced_sb_new_segment_mark_end"), newSponsorSegmentDialogListener) + .show(); + } catch (Exception ex) { + Logger.printException(() -> "onMarkLocationClicked failure", ex); + } + } + + public static void onPublishClicked() { + try { + Utils.verifyOnMainThread(); + if (newSponsorSegmentStartMillis < 0 || newSponsorSegmentEndMillis < 0) { + Utils.showToastShort(str("revanced_sb_new_segment_mark_locations_first")); + } else if (newSponsorSegmentStartMillis >= newSponsorSegmentEndMillis) { + Utils.showToastShort(str("revanced_sb_new_segment_start_is_before_end")); + } else if (!newSponsorSegmentPreviewed && newSponsorSegmentStartMillis != 0) { + Utils.showToastLong(str("revanced_sb_new_segment_preview_segment_first")); + } else { + final long segmentLength = (newSponsorSegmentEndMillis - newSponsorSegmentStartMillis) / 1000; + new AlertDialog.Builder(SponsorBlockViewController.getOverLaysViewGroupContext()) + .setTitle(str("revanced_sb_new_segment_confirm_title")) + .setMessage(str("revanced_sb_new_segment_confirm_contents", + formatSegmentTime(newSponsorSegmentStartMillis), + formatSegmentTime(newSponsorSegmentEndMillis), + getTimeSavedString(segmentLength))) + .setNegativeButton(android.R.string.no, null) + .setPositiveButton(android.R.string.yes, segmentReadyDialogButtonListener) + .show(); + } + } catch (Exception ex) { + Logger.printException(() -> "onPublishClicked failure", ex); + } + } + + public static void onVotingClicked(@NonNull Context context) { + try { + Utils.verifyOnMainThread(); + SponsorSegment[] segments = SegmentPlaybackController.getSegments(); + if (segments == null || segments.length == 0) { + // Button is hidden if no segments exist. + // But if prior video had segments, and current video does not, + // then the button persists until the overlay fades out (this is intentional, as abruptly hiding the button is jarring). + Utils.showToastShort(str("revanced_sb_vote_no_segments")); + return; + } + + final int numberOfSegments = segments.length; + CharSequence[] titles = new CharSequence[numberOfSegments]; + for (int i = 0; i < numberOfSegments; i++) { + SponsorSegment segment = segments[i]; + if (segment.category == SegmentCategory.UNSUBMITTED) { + continue; + } + StringBuilder htmlBuilder = new StringBuilder(); + htmlBuilder.append(String.format(" %s
", + segment.category.color, segment.category.title)); + htmlBuilder.append(formatSegmentTime(segment.start)); + if (segment.category != SegmentCategory.HIGHLIGHT) { + htmlBuilder.append(" to ").append(formatSegmentTime(segment.end)); + } + htmlBuilder.append("
"); + if (i + 1 != numberOfSegments) // prevents trailing new line after last segment + htmlBuilder.append("
"); + titles[i] = Html.fromHtml(htmlBuilder.toString()); + } + + new AlertDialog.Builder(context) + .setItems(titles, segmentVoteClickListener) + .show(); + } catch (Exception ex) { + Logger.printException(() -> "onVotingClicked failure", ex); + } + } + + private static void onNewCategorySelect(@NonNull SponsorSegment segment, @NonNull Context context) { + try { + Utils.verifyOnMainThread(); + final SegmentCategory[] values = SegmentCategory.categoriesWithoutHighlights(); + CharSequence[] titles = new CharSequence[values.length]; + for (int i = 0; i < values.length; i++) { + titles[i] = values[i].getTitleWithColorDot(); + } + + new AlertDialog.Builder(context) + .setTitle(str("revanced_sb_new_segment_choose_category")) + .setItems(titles, (dialog, which) -> SBRequester.voteToChangeCategoryOnBackgroundThread(segment, values[which])) + .show(); + } catch (Exception ex) { + Logger.printException(() -> "onNewCategorySelect failure", ex); + } + } + + public static void onPreviewClicked() { + try { + Utils.verifyOnMainThread(); + if (newSponsorSegmentStartMillis < 0 || newSponsorSegmentEndMillis < 0) { + Utils.showToastShort(str("revanced_sb_new_segment_mark_locations_first")); + } else if (newSponsorSegmentStartMillis >= newSponsorSegmentEndMillis) { + Utils.showToastShort(str("revanced_sb_new_segment_start_is_before_end")); + } else { + SegmentPlaybackController.removeUnsubmittedSegments(); // If user hits preview more than once before playing. + SegmentPlaybackController.addUnsubmittedSegment( + new SponsorSegment(SegmentCategory.UNSUBMITTED, null, + newSponsorSegmentStartMillis, newSponsorSegmentEndMillis, false)); + VideoInformation.seekTo(newSponsorSegmentStartMillis - 2000, SegmentPlaybackController.getVideoLength()); + } + } catch (Exception ex) { + Logger.printException(() -> "onPreviewClicked failure", ex); + } + } + + + static void sendViewRequestAsync(@NonNull SponsorSegment segment) { + if (segment.recordedAsSkipped || segment.category == SegmentCategory.UNSUBMITTED) { + return; + } + segment.recordedAsSkipped = true; + final long totalTimeSkipped = Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.get() + segment.length(); + Settings.SB_LOCAL_TIME_SAVED_MILLISECONDS.save(totalTimeSkipped); + Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.save(Settings.SB_LOCAL_TIME_SAVED_NUMBER_SEGMENTS.get() + 1); + + if (Settings.SB_TRACK_SKIP_COUNT.get()) { + Utils.runOnBackgroundThread(() -> SBRequester.sendSegmentSkippedViewedRequest(segment)); + } + } + + public static void showErrorDialog(String dialogMessage) { + Utils.runOnMainThreadNowOrLater(() -> + new AlertDialog.Builder(SponsorBlockViewController.getOverLaysViewGroupContext()) + .setMessage(dialogMessage) + .setPositiveButton(android.R.string.ok, null) + .setCancelable(false) + .show() + ); + } + + public static void onEditByHandClicked() { + try { + Utils.verifyOnMainThread(); + new AlertDialog.Builder(SponsorBlockViewController.getOverLaysViewGroupContext()) + .setTitle(str("revanced_sb_new_segment_edit_by_hand_title")) + .setMessage(str("revanced_sb_new_segment_edit_by_hand_content")) + .setNeutralButton(android.R.string.cancel, null) + .setNegativeButton(str("revanced_sb_new_segment_mark_start"), editByHandDialogListener) + .setPositiveButton(str("revanced_sb_new_segment_mark_end"), editByHandDialogListener) + .show(); + } catch (Exception ex) { + Logger.printException(() -> "onEditByHandClicked failure", ex); + } + } + + public static String getNumberOfSkipsString(int viewCount) { + return statsNumberFormatter.format(viewCount); + } + + @SuppressWarnings("ConstantConditions") + private static long parseSegmentTime(@NonNull String time) { + Matcher matcher = manualEditTimePattern.matcher(time); + if (!matcher.matches()) { + return -1; + } + String hoursStr = matcher.group(2); // Hours is optional. + String minutesStr = matcher.group(3); + String secondsStr = matcher.group(4); + String millisecondsStr = matcher.group(6); // Milliseconds is optional. + + try { + final int hours = (hoursStr != null) ? Integer.parseInt(hoursStr) : 0; + final int minutes = Integer.parseInt(minutesStr); + final int seconds = Integer.parseInt(secondsStr); + final int milliseconds; + if (millisecondsStr != null) { + // Pad out with zeros if not all decimal places were used. + millisecondsStr = String.format(Locale.US, "%-3s", millisecondsStr).replace(' ', '0'); + milliseconds = Integer.parseInt(millisecondsStr); + } else { + milliseconds = 0; + } + + return (hours * 3600000L) + (minutes * 60000L) + (seconds * 1000L) + milliseconds; + } catch (NumberFormatException ex) { + Logger.printInfo(() -> "Time format exception: " + time, ex); + return -1; + } + } + + private static String formatSegmentTime(long segmentTime) { + // Use same time formatting as shown in the video player. + final long videoLength = SegmentPlaybackController.getVideoLength(); + + // Cannot use DateFormatter, as videos over 24 hours will rollover and not display correctly. + final long hours = TimeUnit.MILLISECONDS.toHours(segmentTime); + final long minutes = TimeUnit.MILLISECONDS.toMinutes(segmentTime) % 60; + final long seconds = TimeUnit.MILLISECONDS.toSeconds(segmentTime) % 60; + final long milliseconds = segmentTime % 1000; + + final String formatPattern; + Object[] formatArgs = {minutes, seconds, milliseconds}; + + if (videoLength < (10 * 60 * 1000)) { + formatPattern = "%01d:%02d.%03d"; // Less than 10 minutes. + } else if (videoLength < (60 * 60 * 1000)) { + formatPattern = "%02d:%02d.%03d"; // Less than 1 hour. + } else if (videoLength < (10 * 60 * 60 * 1000)) { + formatPattern = "%01d:%02d:%02d.%03d"; // Less than 10 hours. + formatArgs = new Object[]{hours, minutes, seconds, milliseconds}; + } else { + formatPattern = "%02d:%02d:%02d.%03d"; // Why is this on YouTube? + formatArgs = new Object[]{hours, minutes, seconds, milliseconds}; + } + + return String.format(Locale.US, formatPattern, formatArgs); + } + + @TargetApi(26) + public static String getTimeSavedString(long totalSecondsSaved) { + Duration duration = Duration.ofSeconds(totalSecondsSaved); + final long hours = duration.toHours(); + final long minutes = duration.toMinutes() % 60; + // Format all numbers so non-western numbers use a consistent appearance. + String minutesFormatted = statsNumberFormatter.format(minutes); + if (hours > 0) { + String hoursFormatted = statsNumberFormatter.format(hours); + return str("revanced_sb_stats_saved_hour_format", hoursFormatted, minutesFormatted); + } + final long seconds = duration.getSeconds() % 60; + String secondsFormatted = statsNumberFormatter.format(seconds); + if (minutes > 0) { + return str("revanced_sb_stats_saved_minute_format", minutesFormatted, secondsFormatted); + } + return str("revanced_sb_stats_saved_second_format", secondsFormatted); + } + + private static class EditByHandSaveDialogListener implements DialogInterface.OnClickListener { + boolean settingStart; + WeakReference editTextRef = new WeakReference<>(null); + + @Override + public void onClick(DialogInterface dialog, int which) { + try { + final EditText editText = editTextRef.get(); + if (editText == null) return; + + final long time; + if (which == DialogInterface.BUTTON_NEUTRAL) { + time = VideoInformation.getVideoTime(); + } else { + time = parseSegmentTime(editText.getText().toString()); + if (time < 0) { + Utils.showToastLong(str("revanced_sb_new_segment_edit_by_hand_parse_error")); + return; + } + } + + if (settingStart) + newSponsorSegmentStartMillis = Math.max(time, 0); + else + newSponsorSegmentEndMillis = time; + + if (which == DialogInterface.BUTTON_NEUTRAL) + editByHandDialogListener.onClick(dialog, settingStart ? + DialogInterface.BUTTON_NEGATIVE : + DialogInterface.BUTTON_POSITIVE); + } catch (Exception ex) { + Logger.printException(() -> "EditByHandSaveDialogListener failure", ex); + } + } + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/CategoryBehaviour.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/CategoryBehaviour.java new file mode 100644 index 000000000..5e5b4e8f1 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/CategoryBehaviour.java @@ -0,0 +1,124 @@ +package app.revanced.extension.youtube.sponsorblock.objects; + +import static app.revanced.extension.shared.utils.StringRef.sf; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +import app.revanced.extension.shared.utils.StringRef; +import app.revanced.extension.shared.utils.Utils; + +public enum CategoryBehaviour { + SKIP_AUTOMATICALLY("skip", 2, true, sf("revanced_sb_skip_automatically")), + // desktop does not have skip-once behavior. Key is unique to ReVanced + SKIP_AUTOMATICALLY_ONCE("skip-once", 3, true, sf("revanced_sb_skip_automatically_once")), + MANUAL_SKIP("manual-skip", 1, false, sf("revanced_sb_skip_showbutton")), + SHOW_IN_SEEKBAR("seekbar-only", 0, false, sf("revanced_sb_skip_seekbaronly")), + // ignored categories are not exported to json, and ignore is the default behavior when importing + IGNORE("ignore", -1, false, sf("revanced_sb_skip_ignore")); + + /** + * ReVanced specific value. + */ + @NonNull + public final String reVancedKeyValue; + /** + * Desktop specific value. + */ + public final int desktopKeyValue; + /** + * If the segment should skip automatically + */ + public final boolean skipAutomatically; + @NonNull + public final StringRef description; + + CategoryBehaviour(String reVancedKeyValue, int desktopKeyValue, boolean skipAutomatically, StringRef description) { + this.reVancedKeyValue = Objects.requireNonNull(reVancedKeyValue); + this.desktopKeyValue = desktopKeyValue; + this.skipAutomatically = skipAutomatically; + this.description = Objects.requireNonNull(description); + } + + @Nullable + public static CategoryBehaviour byReVancedKeyValue(@NonNull String keyValue) { + for (CategoryBehaviour behaviour : values()) { + if (behaviour.reVancedKeyValue.equals(keyValue)) { + return behaviour; + } + } + return null; + } + + @Nullable + public static CategoryBehaviour byDesktopKeyValue(int desktopKeyValue) { + for (CategoryBehaviour behaviour : values()) { + if (behaviour.desktopKeyValue == desktopKeyValue) { + return behaviour; + } + } + return null; + } + + private static String[] behaviorKeyValues; + private static String[] behaviorDescriptions; + + private static String[] behaviorKeyValuesWithoutSkipOnce; + private static String[] behaviorDescriptionsWithoutSkipOnce; + + private static void createNameAndKeyArrays() { + Utils.verifyOnMainThread(); + + CategoryBehaviour[] behaviours = values(); + final int behaviorLength = behaviours.length; + behaviorKeyValues = new String[behaviorLength]; + behaviorDescriptions = new String[behaviorLength]; + behaviorKeyValuesWithoutSkipOnce = new String[behaviorLength - 1]; + behaviorDescriptionsWithoutSkipOnce = new String[behaviorLength - 1]; + + int behaviorIndex = 0, behaviorHighlightIndex = 0; + while (behaviorIndex < behaviorLength) { + CategoryBehaviour behaviour = behaviours[behaviorIndex]; + String value = behaviour.reVancedKeyValue; + String description = behaviour.description.toString(); + behaviorKeyValues[behaviorIndex] = value; + behaviorDescriptions[behaviorIndex] = description; + behaviorIndex++; + if (behaviour != SKIP_AUTOMATICALLY_ONCE) { + behaviorKeyValuesWithoutSkipOnce[behaviorHighlightIndex] = value; + behaviorDescriptionsWithoutSkipOnce[behaviorHighlightIndex] = description; + behaviorHighlightIndex++; + } + } + } + + public static String[] getBehaviorKeyValues() { + if (behaviorKeyValues == null) { + createNameAndKeyArrays(); + } + return behaviorKeyValues; + } + + public static String[] getBehaviorKeyValuesWithoutSkipOnce() { + if (behaviorKeyValuesWithoutSkipOnce == null) { + createNameAndKeyArrays(); + } + return behaviorKeyValuesWithoutSkipOnce; + } + + public static String[] getBehaviorDescriptions() { + if (behaviorDescriptions == null) { + createNameAndKeyArrays(); + } + return behaviorDescriptions; + } + + public static String[] getBehaviorDescriptionsWithoutSkipOnce() { + if (behaviorDescriptionsWithoutSkipOnce == null) { + createNameAndKeyArrays(); + } + return behaviorDescriptionsWithoutSkipOnce; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java new file mode 100644 index 000000000..3d1e90f66 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java @@ -0,0 +1,351 @@ +package app.revanced.extension.youtube.sponsorblock.objects; + +import static app.revanced.extension.shared.utils.StringRef.sf; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_FILLER_COLOR; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_HIGHLIGHT_COLOR; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTERACTION_COLOR; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_INTRO_COLOR; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_MUSIC_OFFTOPIC_COLOR; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_OUTRO_COLOR; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_PREVIEW_COLOR; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SELF_PROMO_COLOR; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_SPONSOR_COLOR; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED; +import static app.revanced.extension.youtube.settings.Settings.SB_CATEGORY_UNSUBMITTED_COLOR; + +import android.graphics.Color; +import android.graphics.Paint; +import android.text.Html; +import android.text.Spanned; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import app.revanced.extension.shared.settings.StringSetting; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.StringRef; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings({"deprecation", "StaticFieldLeak"}) +public enum SegmentCategory { + SPONSOR("sponsor", sf("revanced_sb_segments_sponsor"), sf("revanced_sb_skip_button_sponsor"), sf("revanced_sb_skipped_sponsor"), + SB_CATEGORY_SPONSOR, SB_CATEGORY_SPONSOR_COLOR), + SELF_PROMO("selfpromo", sf("revanced_sb_segments_selfpromo"), sf("revanced_sb_skip_button_selfpromo"), sf("revanced_sb_skipped_selfpromo"), + SB_CATEGORY_SELF_PROMO, SB_CATEGORY_SELF_PROMO_COLOR), + INTERACTION("interaction", sf("revanced_sb_segments_interaction"), sf("revanced_sb_skip_button_interaction"), sf("revanced_sb_skipped_interaction"), + SB_CATEGORY_INTERACTION, SB_CATEGORY_INTERACTION_COLOR), + /** + * Unique category that is treated differently than the rest. + */ + HIGHLIGHT("poi_highlight", sf("revanced_sb_segments_highlight"), sf("revanced_sb_skip_button_highlight"), sf("revanced_sb_skipped_highlight"), + SB_CATEGORY_HIGHLIGHT, SB_CATEGORY_HIGHLIGHT_COLOR), + INTRO("intro", sf("revanced_sb_segments_intro"), + sf("revanced_sb_skip_button_intro_beginning"), sf("revanced_sb_skip_button_intro_middle"), sf("revanced_sb_skip_button_intro_end"), + sf("revanced_sb_skipped_intro_beginning"), sf("revanced_sb_skipped_intro_middle"), sf("revanced_sb_skipped_intro_end"), + SB_CATEGORY_INTRO, SB_CATEGORY_INTRO_COLOR), + OUTRO("outro", sf("revanced_sb_segments_outro"), sf("revanced_sb_skip_button_outro"), sf("revanced_sb_skipped_outro"), + SB_CATEGORY_OUTRO, SB_CATEGORY_OUTRO_COLOR), + PREVIEW("preview", sf("revanced_sb_segments_preview"), + sf("revanced_sb_skip_button_preview_beginning"), sf("revanced_sb_skip_button_preview_middle"), sf("revanced_sb_skip_button_preview_end"), + sf("revanced_sb_skipped_preview_beginning"), sf("revanced_sb_skipped_preview_middle"), sf("revanced_sb_skipped_preview_end"), + SB_CATEGORY_PREVIEW, SB_CATEGORY_PREVIEW_COLOR), + FILLER("filler", sf("revanced_sb_segments_filler"), sf("revanced_sb_skip_button_filler"), sf("revanced_sb_skipped_filler"), + SB_CATEGORY_FILLER, SB_CATEGORY_FILLER_COLOR), + MUSIC_OFFTOPIC("music_offtopic", sf("revanced_sb_segments_nomusic"), sf("revanced_sb_skip_button_nomusic"), sf("revanced_sb_skipped_nomusic"), + SB_CATEGORY_MUSIC_OFFTOPIC, SB_CATEGORY_MUSIC_OFFTOPIC_COLOR), + UNSUBMITTED("unsubmitted", StringRef.empty, sf("revanced_sb_skip_button_unsubmitted"), sf("revanced_sb_skipped_unsubmitted"), + SB_CATEGORY_UNSUBMITTED, SB_CATEGORY_UNSUBMITTED_COLOR), + ; + + private static final StringRef skipSponsorTextCompact = sf("revanced_sb_skip_button_compact"); + private static final StringRef skipSponsorTextCompactHighlight = sf("revanced_sb_skip_button_compact_highlight"); + + private static final SegmentCategory[] categoriesWithoutHighlights = new SegmentCategory[]{ + SPONSOR, + SELF_PROMO, + INTERACTION, + INTRO, + OUTRO, + PREVIEW, + FILLER, + MUSIC_OFFTOPIC, + }; + + private static final SegmentCategory[] categoriesWithoutUnsubmitted = new SegmentCategory[]{ + SPONSOR, + SELF_PROMO, + INTERACTION, + HIGHLIGHT, + INTRO, + OUTRO, + PREVIEW, + FILLER, + MUSIC_OFFTOPIC, + }; + private static final Map mValuesMap = new HashMap<>(2 * categoriesWithoutUnsubmitted.length); + + /** + * Categories currently enabled, formatted for an API call + */ + public static String sponsorBlockAPIFetchCategories = "[]"; + + static { + for (SegmentCategory value : categoriesWithoutUnsubmitted) + mValuesMap.put(value.keyValue, value); + } + + @NonNull + public static SegmentCategory[] categoriesWithoutUnsubmitted() { + return categoriesWithoutUnsubmitted; + } + + @NonNull + public static SegmentCategory[] categoriesWithoutHighlights() { + return categoriesWithoutHighlights; + } + + @Nullable + public static SegmentCategory byCategoryKey(@NonNull String key) { + return mValuesMap.get(key); + } + + /** + * Must be called if behavior of any category is changed + */ + public static void updateEnabledCategories() { + Utils.verifyOnMainThread(); + Logger.printDebug(() -> "updateEnabledCategories"); + SegmentCategory[] categories = categoriesWithoutUnsubmitted(); + List enabledCategories = new ArrayList<>(categories.length); + for (SegmentCategory category : categories) { + if (category.behaviour != CategoryBehaviour.IGNORE) { + enabledCategories.add(category.keyValue); + } + } + + //"[%22sponsor%22,%22outro%22,%22music_offtopic%22,%22intro%22,%22selfpromo%22,%22interaction%22,%22preview%22]"; + if (enabledCategories.isEmpty()) + sponsorBlockAPIFetchCategories = "[]"; + else + sponsorBlockAPIFetchCategories = "[%22" + TextUtils.join("%22,%22", enabledCategories) + "%22]"; + } + + public static void loadAllCategoriesFromSettings() { + for (SegmentCategory category : values()) { + category.loadFromSettings(); + } + updateEnabledCategories(); + } + + @NonNull + public final String keyValue; + @NonNull + public final StringSetting behaviorSetting; + @NonNull + private final StringSetting colorSetting; + + @NonNull + public final StringRef title; + + /** + * Skip button text, if the skip occurs in the first quarter of the video + */ + @NonNull + public final StringRef skipButtonTextBeginning; + /** + * Skip button text, if the skip occurs in the middle half of the video + */ + @NonNull + public final StringRef skipButtonTextMiddle; + /** + * Skip button text, if the skip occurs in the last quarter of the video + */ + @NonNull + public final StringRef skipButtonTextEnd; + /** + * Skipped segment toast, if the skip occurred in the first quarter of the video + */ + @NonNull + public final StringRef skippedToastBeginning; + /** + * Skipped segment toast, if the skip occurred in the middle half of the video + */ + @NonNull + public final StringRef skippedToastMiddle; + /** + * Skipped segment toast, if the skip occurred in the last quarter of the video + */ + @NonNull + public final StringRef skippedToastEnd; + + @NonNull + public final Paint paint; + + /** + * Value must be changed using {@link #setColor(String)}. + */ + public int color; + + /** + * Value must be changed using {@link #setBehaviour(CategoryBehaviour)}. + * Caller must also {@link #updateEnabledCategories()}. + */ + @NonNull + public CategoryBehaviour behaviour = CategoryBehaviour.IGNORE; + + SegmentCategory(String keyValue, StringRef title, + StringRef skipButtonText, + StringRef skippedToastText, + StringSetting behavior, StringSetting color) { + this(keyValue, title, + skipButtonText, skipButtonText, skipButtonText, + skippedToastText, skippedToastText, skippedToastText, + behavior, color); + } + + SegmentCategory(String keyValue, StringRef title, + StringRef skipButtonTextBeginning, StringRef skipButtonTextMiddle, StringRef skipButtonTextEnd, + StringRef skippedToastBeginning, StringRef skippedToastMiddle, StringRef skippedToastEnd, + StringSetting behavior, StringSetting color) { + this.keyValue = Objects.requireNonNull(keyValue); + this.title = Objects.requireNonNull(title); + this.skipButtonTextBeginning = Objects.requireNonNull(skipButtonTextBeginning); + this.skipButtonTextMiddle = Objects.requireNonNull(skipButtonTextMiddle); + this.skipButtonTextEnd = Objects.requireNonNull(skipButtonTextEnd); + this.skippedToastBeginning = Objects.requireNonNull(skippedToastBeginning); + this.skippedToastMiddle = Objects.requireNonNull(skippedToastMiddle); + this.skippedToastEnd = Objects.requireNonNull(skippedToastEnd); + this.behaviorSetting = Objects.requireNonNull(behavior); + this.colorSetting = Objects.requireNonNull(color); + this.paint = new Paint(); + loadFromSettings(); + } + + private void loadFromSettings() { + String behaviorString = behaviorSetting.get(); + CategoryBehaviour savedBehavior = CategoryBehaviour.byReVancedKeyValue(behaviorString); + if (savedBehavior == null) { + Logger.printException(() -> "Invalid behavior: " + behaviorString); + behaviorSetting.resetToDefault(); + loadFromSettings(); + return; + } + this.behaviour = savedBehavior; + + String colorString = colorSetting.get(); + try { + setColor(colorString); + } catch (Exception ex) { + Logger.printException(() -> "Invalid color: " + colorString, ex); + colorSetting.resetToDefault(); + loadFromSettings(); + } + } + + public void setBehaviour(@NonNull CategoryBehaviour behaviour) { + this.behaviour = Objects.requireNonNull(behaviour); + this.behaviorSetting.save(behaviour.reVancedKeyValue); + } + + /** + * @return HTML color format string + */ + @NonNull + public String colorString() { + return String.format("#%06X", color); + } + + public void setColor(@NonNull String colorString) throws IllegalArgumentException { + final int color = Color.parseColor(colorString) & 0xFFFFFF; + this.color = color; + paint.setColor(color); + paint.setAlpha(255); + colorSetting.save(colorString); // Save after parsing. + } + + public void resetColor() { + setColor(colorSetting.defaultValue); + } + + @NonNull + private static String getCategoryColorDotHTML(int color) { + color &= 0xFFFFFF; + return String.format("", color); + } + + @NonNull + public static Spanned getCategoryColorDot(int color) { + return Html.fromHtml(getCategoryColorDotHTML(color)); + } + + @NonNull + public Spanned getCategoryColorDot() { + return getCategoryColorDot(color); + } + + @NonNull + public Spanned getTitleWithColorDot() { + return Html.fromHtml(getCategoryColorDotHTML(color) + " " + title); + } + + /** + * @param segmentStartTime video time the segment category started + * @param videoLength length of the video + * @return the skip button text + */ + @NonNull + StringRef getSkipButtonText(long segmentStartTime, long videoLength) { + if (Settings.SB_COMPACT_SKIP_BUTTON.get()) { + return (this == SegmentCategory.HIGHLIGHT) + ? skipSponsorTextCompactHighlight + : skipSponsorTextCompact; + } + + if (videoLength == 0) { + return skipButtonTextBeginning; // video is still loading. Assume it's the beginning + } + final float position = segmentStartTime / (float) videoLength; + if (position < 0.25f) { + return skipButtonTextBeginning; + } else if (position < 0.75f) { + return skipButtonTextMiddle; + } + return skipButtonTextEnd; + } + + /** + * @param segmentStartTime video time the segment category started + * @param videoLength length of the video + * @return 'skipped segment' toast message + */ + @NonNull + StringRef getSkippedToastText(long segmentStartTime, long videoLength) { + if (videoLength == 0) { + return skippedToastBeginning; // video is still loading. Assume it's the beginning + } + final float position = segmentStartTime / (float) videoLength; + if (position < 0.25f) { + return skippedToastBeginning; + } else if (position < 0.75f) { + return skippedToastMiddle; + } + return skippedToastEnd; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java new file mode 100644 index 000000000..51208c1cc --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SponsorSegment.java @@ -0,0 +1,146 @@ +package app.revanced.extension.youtube.sponsorblock.objects; + +import static app.revanced.extension.shared.utils.StringRef.sf; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +import app.revanced.extension.shared.utils.StringRef; +import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController; + +public class SponsorSegment implements Comparable { + public enum SegmentVote { + UPVOTE(sf("revanced_sb_vote_upvote"), 1, false), + DOWNVOTE(sf("revanced_sb_vote_downvote"), 0, true), + CATEGORY_CHANGE(sf("revanced_sb_vote_category"), -1, true); // apiVoteType is not used for category change + + public static final SegmentVote[] voteTypesWithoutCategoryChange = { + UPVOTE, + DOWNVOTE, + }; + + @NonNull + public final StringRef title; + public final int apiVoteType; + public final boolean shouldHighlight; + + SegmentVote(@NonNull StringRef title, int apiVoteType, boolean shouldHighlight) { + this.title = title; + this.apiVoteType = apiVoteType; + this.shouldHighlight = shouldHighlight; + } + } + + @NonNull + public final SegmentCategory category; + /** + * NULL if segment is unsubmitted + */ + @Nullable + public final String UUID; + public final long start; + public final long end; + public final boolean isLocked; + public boolean didAutoSkipped = false; + /** + * If this segment has been counted as 'skipped' + */ + public boolean recordedAsSkipped = false; + + public SponsorSegment(@NonNull SegmentCategory category, @Nullable String UUID, long start, long end, boolean isLocked) { + this.category = category; + this.UUID = UUID; + this.start = start; + this.end = end; + this.isLocked = isLocked; + } + + public boolean shouldAutoSkip() { + return category.behaviour.skipAutomatically && !(didAutoSkipped && category.behaviour == CategoryBehaviour.SKIP_AUTOMATICALLY_ONCE); + } + + /** + * @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number + */ + public boolean startIsNear(long videoTime, long nearThreshold) { + return Math.abs(start - videoTime) <= nearThreshold; + } + + /** + * @param nearThreshold threshold to declare the time parameter is near this segment. Must be a positive number + */ + public boolean endIsNear(long videoTime, long nearThreshold) { + return Math.abs(end - videoTime) <= nearThreshold; + } + + /** + * @return if the time parameter is within this segment + */ + public boolean containsTime(long videoTime) { + return start <= videoTime && videoTime < end; + } + + /** + * @return if the segment is completely contained inside this segment + */ + public boolean containsSegment(SponsorSegment other) { + return start <= other.start && other.end <= end; + } + + /** + * @return the length of this segment, in milliseconds. Always a positive number. + */ + public long length() { + return end - start; + } + + /** + * @return 'skip segment' UI overlay button text + */ + @NonNull + public String getSkipButtonText() { + return category.getSkipButtonText(start, SegmentPlaybackController.getVideoLength()).toString(); + } + + /** + * @return 'skipped segment' toast message + */ + @NonNull + public String getSkippedToastText() { + return category.getSkippedToastText(start, SegmentPlaybackController.getVideoLength()).toString(); + } + + @Override + public int compareTo(SponsorSegment o) { + // If both segments start at the same time, then sort with the longer segment first. + // This keeps the seekbar drawing correct since it draws the segments using the sorted order. + return start == o.start ? Long.compare(o.length(), length()) : Long.compare(start, o.start); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof SponsorSegment other)) return false; + return Objects.equals(UUID, other.UUID) + && category == other.category + && start == other.start + && end == other.end; + } + + @Override + public int hashCode() { + return Objects.hashCode(UUID); + } + + @NonNull + @Override + public String toString() { + return "SponsorSegment{" + + "category=" + category + + ", start=" + start + + ", end=" + end + + '}'; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java new file mode 100644 index 000000000..6a9b9a3e6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/UserStats.java @@ -0,0 +1,53 @@ +package app.revanced.extension.youtube.sponsorblock.objects; + +import androidx.annotation.NonNull; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * SponsorBlock user stats + */ +public class UserStats { + @NonNull + public final String publicUserId; + @NonNull + public final String userName; + /** + * "User reputation". Unclear how SB determines this value. + */ + public final float reputation; + /** + * {@link #segmentCount} plus {@link #ignoredSegmentCount} + */ + public final int totalSegmentCountIncludingIgnored; + public final int segmentCount; + public final int ignoredSegmentCount; + public final int viewCount; + public final double minutesSaved; + + public UserStats(@NonNull JSONObject json) throws JSONException { + publicUserId = json.getString("userID"); + userName = json.getString("userName"); + reputation = (float) json.getDouble("reputation"); + segmentCount = json.getInt("segmentCount"); + ignoredSegmentCount = json.getInt("ignoredSegmentCount"); + totalSegmentCountIncludingIgnored = segmentCount + ignoredSegmentCount; + viewCount = json.getInt("viewCount"); + minutesSaved = json.getDouble("minutesSaved"); + } + + @NonNull + @Override + public String toString() { + return "UserStats{" + + "publicUserId='" + publicUserId + '\'' + + ", userName='" + userName + '\'' + + ", reputation=" + reputation + + ", segmentCount=" + segmentCount + + ", ignoredSegmentCount=" + ignoredSegmentCount + + ", viewCount=" + viewCount + + ", minutesSaved=" + minutesSaved + + '}'; + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java new file mode 100644 index 000000000..f2e31a014 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java @@ -0,0 +1,289 @@ +package app.revanced.extension.youtube.sponsorblock.requests; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +import app.revanced.extension.shared.requests.Requester; +import app.revanced.extension.shared.requests.Route; +import app.revanced.extension.shared.sponsorblock.requests.SBRoutes; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +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.SegmentCategory; +import app.revanced.extension.youtube.sponsorblock.objects.SponsorSegment; +import app.revanced.extension.youtube.sponsorblock.objects.SponsorSegment.SegmentVote; +import app.revanced.extension.youtube.sponsorblock.objects.UserStats; + +public class SBRequester { + private static final String TIME_TEMPLATE = "%.3f"; + + /** + * TCP timeout + */ + private static final int TIMEOUT_TCP_DEFAULT_MILLISECONDS = 7000; + + /** + * HTTP response timeout + */ + private static final int TIMEOUT_HTTP_DEFAULT_MILLISECONDS = 10000; + + /** + * Response code of a successful API call + */ + private static final int HTTP_STATUS_CODE_SUCCESS = 200; + + private SBRequester() { + } + + private static void handleConnectionError(@NonNull String toastMessage, @Nullable Exception ex) { + if (Settings.SB_TOAST_ON_CONNECTION_ERROR.get()) { + Utils.showToastShort(toastMessage); + } + if (ex != null) { + Logger.printInfo(() -> toastMessage, ex); + } + } + + @NonNull + public static SponsorSegment[] getSegments(@NonNull String videoId) { + Utils.verifyOffMainThread(); + List segments = new ArrayList<>(); + try { + HttpURLConnection connection = getConnectionFromRoute(SBRoutes.GET_SEGMENTS, videoId, SegmentCategory.sponsorBlockAPIFetchCategories); + final int responseCode = connection.getResponseCode(); + + if (responseCode == HTTP_STATUS_CODE_SUCCESS) { + JSONArray responseArray = Requester.parseJSONArray(connection); + final long minSegmentDuration = (long) (Settings.SB_SEGMENT_MIN_DURATION.get() * 1000); + for (int i = 0, length = responseArray.length(); i < length; i++) { + JSONObject obj = (JSONObject) responseArray.get(i); + JSONArray segment = obj.getJSONArray("segment"); + final long start = (long) (segment.getDouble(0) * 1000); + final long end = (long) (segment.getDouble(1) * 1000); + + String uuid = obj.getString("UUID"); + final boolean locked = obj.getInt("locked") == 1; + String categoryKey = obj.getString("category"); + SegmentCategory category = SegmentCategory.byCategoryKey(categoryKey); + if (category == null) { + Logger.printException(() -> "Received unknown category: " + categoryKey); // should never happen + } else if ((end - start) >= minSegmentDuration || category == SegmentCategory.HIGHLIGHT) { + segments.add(new SponsorSegment(category, uuid, start, end, locked)); + } + } + Logger.printDebug(() -> { + StringBuilder builder = new StringBuilder("Downloaded segments:"); + for (SponsorSegment segment : segments) { + builder.append('\n').append(segment); + } + return builder.toString(); + }); + runVipCheckInBackgroundIfNeeded(); + } else if (responseCode == 404) { + // no segments are found. a normal response + Logger.printDebug(() -> "No segments found for video: " + videoId); + } else { + handleConnectionError(str("revanced_sb_sponsorblock_connection_failure_status", responseCode), null); + connection.disconnect(); // something went wrong, might as well disconnect + } + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_sb_sponsorblock_connection_failure_timeout"), ex); + } catch (IOException ex) { + handleConnectionError(str("revanced_sb_sponsorblock_connection_failure_generic"), ex); + } catch (Exception ex) { + // Should never happen + Logger.printException(() -> "getSegments failure", ex); + } + + return segments.toArray(new SponsorSegment[0]); + } + + public static void submitSegments(@NonNull String videoId, @NonNull String category, + long startTime, long endTime, long videoLength) { + Utils.verifyOffMainThread(); + try { + String privateUserId = SponsorBlockSettings.getSBPrivateUserID(); + String start = String.format(Locale.US, TIME_TEMPLATE, startTime / 1000f); + String end = String.format(Locale.US, TIME_TEMPLATE, endTime / 1000f); + String duration = String.format(Locale.US, TIME_TEMPLATE, videoLength / 1000f); + + HttpURLConnection connection = getConnectionFromRoute(SBRoutes.SUBMIT_SEGMENTS, privateUserId, videoId, category, start, end, duration); + final int responseCode = connection.getResponseCode(); + + String userMessage = switch (responseCode) { + case HTTP_STATUS_CODE_SUCCESS -> str("revanced_sb_submit_succeeded"); + case 409 -> str("revanced_sb_submit_failed_duplicate"); + case 403 -> str("revanced_sb_submit_failed_forbidden", + Requester.parseErrorStringAndDisconnect(connection)); + case 429 -> str("revanced_sb_submit_failed_rate_limit"); + case 400 -> str("revanced_sb_submit_failed_invalid", + Requester.parseErrorStringAndDisconnect(connection)); + default -> str("revanced_sb_submit_failed_unknown_error", + responseCode, connection.getResponseMessage()); + }; + // Message might be about the users account or an error too large to show in a toast. + // Use a dialog instead. + SponsorBlockUtils.showErrorDialog(userMessage); + } catch (SocketTimeoutException ex) { + Logger.printDebug(() -> "Timeout", ex); + Utils.showToastLong(str("revanced_sb_submit_failed_timeout")); + } catch (IOException ex) { + Logger.printDebug(() -> "IOException", ex); + Utils.showToastLong(str("revanced_sb_submit_failed_unknown_error", 0, ex.getMessage())); + } catch (Exception ex) { + Logger.printException(() -> "failed to submit segments", ex); // Should never happen. + } + } + + public static void sendSegmentSkippedViewedRequest(@NonNull SponsorSegment segment) { + Utils.verifyOffMainThread(); + try { + HttpURLConnection connection = getConnectionFromRoute(SBRoutes.VIEWED_SEGMENT, segment.UUID); + final int responseCode = connection.getResponseCode(); + + if (responseCode == HTTP_STATUS_CODE_SUCCESS) { + Logger.printDebug(() -> "Successfully sent view count for segment: " + segment); + } else { + Logger.printDebug(() -> "Failed to sent view count for segment: " + segment.UUID + + " responseCode: " + responseCode); // debug level, no toast is shown + } + } catch (IOException ex) { + Logger.printInfo(() -> "Failed to send view count", ex); // do not show a toast + } catch (Exception ex) { + Logger.printException(() -> "Failed to send view count request", ex); // should never happen + } + } + + public static void voteForSegmentOnBackgroundThread(@NonNull SponsorSegment segment, @NonNull SegmentVote voteOption) { + voteOrRequestCategoryChange(segment, voteOption, null); + } + + public static void voteToChangeCategoryOnBackgroundThread(@NonNull SponsorSegment segment, @NonNull SegmentCategory categoryToVoteFor) { + voteOrRequestCategoryChange(segment, SegmentVote.CATEGORY_CHANGE, categoryToVoteFor); + } + + private static void voteOrRequestCategoryChange(@NonNull SponsorSegment segment, @NonNull SegmentVote voteOption, SegmentCategory categoryToVoteFor) { + Utils.runOnBackgroundThread(() -> { + try { + String segmentUuid = segment.UUID; + String uuid = SponsorBlockSettings.getSBPrivateUserID(); + HttpURLConnection connection = (voteOption == SegmentVote.CATEGORY_CHANGE) + ? getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_CATEGORY, uuid, segmentUuid, categoryToVoteFor.keyValue) + : getConnectionFromRoute(SBRoutes.VOTE_ON_SEGMENT_QUALITY, uuid, segmentUuid, String.valueOf(voteOption.apiVoteType)); + final int responseCode = connection.getResponseCode(); + + String userMessage; + switch (responseCode) { + case HTTP_STATUS_CODE_SUCCESS: + Logger.printDebug(() -> "Vote success for segment: " + segment); + return; + case 403: + userMessage = str("revanced_sb_vote_failed_forbidden", + Requester.parseErrorStringAndDisconnect(connection)); + break; + default: + userMessage = str("revanced_sb_vote_failed_unknown_error", + responseCode, connection.getResponseMessage()); + break; + } + + SponsorBlockUtils.showErrorDialog(userMessage); + } catch (SocketTimeoutException ex) { + Utils.showToastShort(str("revanced_sb_vote_failed_timeout")); + } catch (IOException ex) { + Utils.showToastShort(str("revanced_sb_vote_failed_unknown_error", 0, ex.getMessage())); + } catch (Exception ex) { + Logger.printException(() -> "failed to vote for segment", ex); // should never happen + } + }); + } + + /** + * @return NULL, if stats fetch failed + */ + @Nullable + public static UserStats retrieveUserStats() { + Utils.verifyOffMainThread(); + try { + UserStats stats = new UserStats(getJSONObject(SBRoutes.GET_USER_STATS, SponsorBlockSettings.getSBPrivateUserID())); + Logger.printDebug(() -> "user stats: " + stats); + return stats; + } catch (IOException ex) { + Logger.printInfo(() -> "failed to retrieve user stats", ex); // info level, do not show a toast + } catch (Exception ex) { + Logger.printException(() -> "failure retrieving user stats", ex); // should never happen + } + return null; + } + + /** + * @return NULL if the call was successful. If unsuccessful, an error message is returned. + */ + @Nullable + public static String setUsername(@NonNull String username) { + Utils.verifyOffMainThread(); + try { + HttpURLConnection connection = getConnectionFromRoute(SBRoutes.CHANGE_USERNAME, SponsorBlockSettings.getSBPrivateUserID(), username); + final int responseCode = connection.getResponseCode(); + String responseMessage = connection.getResponseMessage(); + if (responseCode == HTTP_STATUS_CODE_SUCCESS) { + return null; + } + return str("revanced_sb_stats_username_change_unknown_error", responseCode, responseMessage); + } catch (Exception ex) { // should never happen + Logger.printInfo(() -> "failed to set username", ex); // do not toast + return str("revanced_sb_stats_username_change_unknown_error", 0, ex.getMessage()); + } + } + + public static void runVipCheckInBackgroundIfNeeded() { + if (!SponsorBlockSettings.userHasSBPrivateId()) { + return; // User cannot be a VIP. User has never voted, created any segments, or has imported a SB user id. + } + long now = System.currentTimeMillis(); + if (now < (Settings.SB_LAST_VIP_CHECK.get() + TimeUnit.DAYS.toMillis(3))) { + return; + } + Utils.runOnBackgroundThread(() -> { + try { + JSONObject json = getJSONObject(SBRoutes.IS_USER_VIP, SponsorBlockSettings.getSBPrivateUserID()); + boolean vip = json.getBoolean("vip"); + Settings.SB_USER_IS_VIP.save(vip); + Settings.SB_LAST_VIP_CHECK.save(now); + } catch (IOException ex) { + Logger.printInfo(() -> "Failed to check VIP (network error)", ex); // info, so no error toast is shown + } catch (Exception ex) { + Logger.printException(() -> "Failed to check VIP", ex); // should never happen + } + }); + } + + // helpers + + private static HttpURLConnection getConnectionFromRoute(@NonNull Route route, String... params) throws IOException { + HttpURLConnection connection = Requester.getConnectionFromRoute(Settings.SB_API_URL.get(), route, params); + connection.setConnectTimeout(TIMEOUT_TCP_DEFAULT_MILLISECONDS); + connection.setReadTimeout(TIMEOUT_HTTP_DEFAULT_MILLISECONDS); + return connection; + } + + private static JSONObject getJSONObject(@NonNull Route route, String... params) throws IOException, JSONException { + return Requester.parseJSONObject(getConnectionFromRoute(route, params)); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/CreateSegmentButtonController.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/CreateSegmentButtonController.java new file mode 100644 index 000000000..44478658a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/CreateSegmentButtonController.java @@ -0,0 +1,89 @@ +package app.revanced.extension.youtube.sponsorblock.ui; + +import static app.revanced.extension.shared.utils.Utils.getChildView; + +import android.view.View; +import android.widget.ImageView; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.overlaybutton.BottomControlButton; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.VideoInformation; + +@SuppressWarnings("unused") +public class CreateSegmentButtonController { + private static WeakReference buttonReference = new WeakReference<>(null); + private static boolean isVisible; + + + /** + * injection point + */ + public static void initialize(View youtubeControlsLayout) { + try { + ImageView imageView = Objects.requireNonNull(getChildView(youtubeControlsLayout, "revanced_sb_create_segment_button")); + imageView.setVisibility(View.GONE); + imageView.setOnClickListener(v -> SponsorBlockViewController.toggleNewSegmentLayoutVisibility()); + buttonReference = new WeakReference<>(imageView); + } catch (Exception ex) { + Logger.printException(() -> "Unable to set RelativeLayout", ex); + } + } + + public static void changeVisibility(boolean visible, boolean animation) { + ImageView imageView = buttonReference.get(); + if (imageView == null || isVisible == visible) return; + isVisible = visible; + + if (visible) { + imageView.clearAnimation(); + if (!shouldBeShown()) { + return; + } + if (animation) { + imageView.startAnimation(BottomControlButton.getButtonFadeIn()); + } + imageView.setVisibility(View.VISIBLE); + return; + } + if (imageView.getVisibility() == View.VISIBLE) { + imageView.clearAnimation(); + if (animation) { + imageView.startAnimation(BottomControlButton.getButtonFadeOut()); + } + imageView.setVisibility(View.GONE); + } + } + + public static void changeVisibilityNegatedImmediate() { + ImageView imageView = buttonReference.get(); + if (imageView == null) return; + if (!shouldBeShown()) return; + + imageView.clearAnimation(); + imageView.startAnimation(BottomControlButton.getButtonFadeOutImmediate()); + imageView.setVisibility(View.GONE); + } + + private static boolean shouldBeShown() { + return Settings.SB_ENABLED.get() && Settings.SB_CREATE_NEW_SEGMENT.get() + && !VideoInformation.isAtEndOfVideo(); + } + + public static void hide() { + if (!isVisible) { + return; + } + Utils.verifyOnMainThread(); + View v = buttonReference.get(); + if (v == null) { + return; + } + v.setVisibility(View.GONE); + isVisible = false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/NewSegmentLayout.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/NewSegmentLayout.java new file mode 100644 index 000000000..f4a433537 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/NewSegmentLayout.java @@ -0,0 +1,160 @@ +package app.revanced.extension.youtube.sponsorblock.ui; + +import static app.revanced.extension.shared.utils.ResourceUtils.getIdentifier; +import static app.revanced.extension.shared.utils.ResourceUtils.getLayoutIdentifier; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.ColorStateList; +import android.graphics.drawable.RippleDrawable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.widget.FrameLayout; +import android.widget.ImageButton; + +import android.widget.ImageView; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.VideoInformation; +import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils; + +public final class NewSegmentLayout extends FrameLayout { + private static final ColorStateList rippleColorStateList = new ColorStateList( + new int[][]{new int[]{android.R.attr.state_enabled}}, + new int[]{0x33ffffff} // sets the ripple color to white + ); + private final int rippleEffectId; + + private float dX, dY; + private boolean isDragging = false; + private ImageView dragHandle; + + public NewSegmentLayout(final Context context) { + this(context, null); + } + + public NewSegmentLayout(final Context context, final AttributeSet attributeSet) { + this(context, attributeSet, 0); + } + + public NewSegmentLayout(final Context context, final AttributeSet attributeSet, final int defStyleAttr) { + this(context, attributeSet, defStyleAttr, 0); + } + + public NewSegmentLayout(final Context context, final AttributeSet attributeSet, + final int defStyleAttr, final int defStyleRes) { + super(context, attributeSet, defStyleAttr, defStyleRes); + + LayoutInflater.from(context).inflate(getLayoutIdentifier("revanced_sb_new_segment"), this, true); + + + TypedValue rippleEffect = new TypedValue(); + context.getTheme().resolveAttribute(android.R.attr.selectableItemBackground, rippleEffect, true); + rippleEffectId = rippleEffect.resourceId; + + initializeButton( + context, + "revanced_sb_new_segment_rewind", + () -> VideoInformation.seekToRelative(-Settings.SB_CREATE_NEW_SEGMENT_STEP.get()), + "Rewind button clicked" + ); + + initializeButton( + context, + "revanced_sb_new_segment_forward", + () -> VideoInformation.seekToRelative(Settings.SB_CREATE_NEW_SEGMENT_STEP.get()), + "Forward button clicked" + ); + + initializeButton( + context, + "revanced_sb_new_segment_adjust", + SponsorBlockUtils::onMarkLocationClicked, + "Adjust button clicked" + ); + + initializeButton( + context, + "revanced_sb_new_segment_compare", + SponsorBlockUtils::onPreviewClicked, + "Compare button clicked" + ); + + initializeButton( + context, + "revanced_sb_new_segment_edit", + SponsorBlockUtils::onEditByHandClicked, + "Edit button clicked" + ); + + initializeButton( + context, + "revanced_sb_new_segment_publish", + SponsorBlockUtils::onPublishClicked, + "Publish button clicked" + ); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + dragHandle = findViewById(getIdentifier("revanced_sb_new_segment_drag_handle", ResourceUtils.ResourceType.ID, getContext())); + setupDragHandle(); + } + + @SuppressLint("ClickableViewAccessibility") + private void setupDragHandle() { + dragHandle.setOnTouchListener((v, event) -> { + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + dX = getX() - event.getRawX(); + dY = getY() - event.getRawY(); + isDragging = true; + break; + case MotionEvent.ACTION_MOVE: + if (isDragging) { + setY(event.getRawY() + dY); + setX(event.getRawX() + dX); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + isDragging = false; + break; + } + return true; + }); + } + + /** + * Initializes a segment button with the given resource identifier name with the given handler and a ripple effect. + * + * @param context The context. + * @param resourceIdentifierName The resource identifier name for the button. + * @param handler The handler for the button's click event. + * @param debugMessage The debug message to print when the button is clicked. + */ + private void initializeButton(final Context context, final String resourceIdentifierName, + final ButtonOnClickHandlerFunction handler, final String debugMessage) { + ImageButton button = findViewById(getIdentifier(resourceIdentifierName, ResourceUtils.ResourceType.ID, context)); + + button.setBackgroundResource(rippleEffectId); + RippleDrawable rippleDrawable = new RippleDrawable( + rippleColorStateList, null, null + ); + button.setBackground(rippleDrawable); + + button.setOnClickListener((v) -> { + handler.apply(); + Logger.printDebug(() -> debugMessage); + }); + } + + @FunctionalInterface + private interface ButtonOnClickHandlerFunction { + void apply(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SkipSponsorButton.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SkipSponsorButton.java new file mode 100644 index 000000000..43fe673ea --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SkipSponsorButton.java @@ -0,0 +1,65 @@ +package app.revanced.extension.youtube.sponsorblock.ui; + +import static app.revanced.extension.shared.utils.ResourceUtils.getDimension; +import static app.revanced.extension.shared.utils.ResourceUtils.getIdIdentifier; +import static app.revanced.extension.shared.utils.ResourceUtils.getLayoutIdentifier; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; + +import java.util.Objects; + +import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController; +import app.revanced.extension.youtube.sponsorblock.objects.SponsorSegment; + +public class SkipSponsorButton extends FrameLayout { + private final TextView skipSponsorTextView; + private SponsorSegment segment; + + public SkipSponsorButton(Context context) { + this(context, null); + } + + public SkipSponsorButton(Context context, AttributeSet attributeSet) { + this(context, attributeSet, 0); + } + + public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr) { + this(context, attributeSet, defStyleAttr, 0); + } + + public SkipSponsorButton(Context context, AttributeSet attributeSet, int defStyleAttr, int defStyleRes) { + super(context, attributeSet, defStyleAttr, defStyleRes); + + LayoutInflater.from(context).inflate(getLayoutIdentifier("revanced_sb_skip_sponsor_button"), this, true); // layout:revanced_sb_skip_sponsor_button + setMinimumHeight(getDimension("ad_skip_ad_button_min_height")); // dimen:ad_skip_ad_button_min_height + final LinearLayout skipSponsorBtnContainer = (LinearLayout) Objects.requireNonNull((View) findViewById(getIdIdentifier("revanced_sb_skip_sponsor_button_container"))); // id:revanced_sb_skip_sponsor_button_container + skipSponsorTextView = (TextView) Objects.requireNonNull((View) findViewById(getIdIdentifier("revanced_sb_skip_sponsor_button_text"))); // id:revanced_sb_skip_sponsor_button_text; + + skipSponsorBtnContainer.setOnClickListener(v -> { + // The view controller handles hiding this button, but hide it here as well just in case something goofs. + setVisibility(View.GONE); + SegmentPlaybackController.onSkipSegmentClicked(segment); + }); + } + + /** + * @return true, if this button state was changed + */ + public boolean updateSkipButtonText(@NonNull SponsorSegment segment) { + this.segment = segment; + final String newText = segment.getSkipButtonText(); + if (newText.equals(skipSponsorTextView.getText().toString())) { + return false; + } + skipSponsorTextView.setText(newText); + return true; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java new file mode 100644 index 000000000..bccbbd3e6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java @@ -0,0 +1,240 @@ +package app.revanced.extension.youtube.sponsorblock.ui; + +import static app.revanced.extension.shared.utils.ResourceUtils.getDimension; +import static app.revanced.extension.shared.utils.ResourceUtils.getLayoutIdentifier; +import static app.revanced.extension.shared.utils.Utils.getChildView; +import static app.revanced.extension.youtube.utils.ExtendedUtils.isFullscreenHidden; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.RelativeLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.player.PlayerPatch; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; +import app.revanced.extension.youtube.sponsorblock.objects.SponsorSegment; + +@SuppressWarnings("unused") +public class SponsorBlockViewController { + private static WeakReference inlineSponsorOverlayRef = new WeakReference<>(null); + private static WeakReference youtubeOverlaysLayoutRef = new WeakReference<>(null); + private static WeakReference skipHighlightButtonRef = new WeakReference<>(null); + private static WeakReference skipSponsorButtonRef = new WeakReference<>(null); + private static WeakReference newSegmentLayoutRef = new WeakReference<>(null); + private static boolean canShowViewElements; + private static boolean newSegmentLayoutVisible; + @Nullable + private static SponsorSegment skipHighlight; + @Nullable + private static SponsorSegment skipSegment; + private static final int ctaBottomMargin; + private static final int defaultBottomMargin; + private static final int hiddenBottomMargin; + + static { + PlayerType.getOnChange().addObserver((PlayerType type) -> { + playerTypeChanged(type); + return null; + }); + + defaultBottomMargin = getDimension("brand_interaction_default_bottom_margin"); + ctaBottomMargin = getDimension("brand_interaction_cta_bottom_margin") + PlayerPatch.getQuickActionsTopMargin(); + hiddenBottomMargin = (int) Math.round((ctaBottomMargin) * 0.5); + } + + public static Context getOverLaysViewGroupContext() { + ViewGroup group = youtubeOverlaysLayoutRef.get(); + if (group == null) { + return null; + } + return group.getContext(); + } + + /** + * Injection point. + */ + public static void initialize(ViewGroup viewGroup) { + try { + Logger.printDebug(() -> "initializing"); + + // hide any old components, just in case they somehow are still hanging around + hideAll(); + + Context context = Utils.getContext(); + RelativeLayout layout = new RelativeLayout(context); + layout.setLayoutParams(new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT)); + LayoutInflater.from(context).inflate(getLayoutIdentifier("revanced_sb_inline_sponsor_overlay"), layout); + inlineSponsorOverlayRef = new WeakReference<>(layout); + + viewGroup.addView(layout); + viewGroup.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() { + @Override + public void onChildViewAdded(View parent, View child) { + // ensure SB buttons and controls are always on top, otherwise the endscreen cards can cover the skip button + RelativeLayout layout = inlineSponsorOverlayRef.get(); + if (layout != null) { + layout.bringToFront(); + } + } + + @Override + public void onChildViewRemoved(View parent, View child) { + } + }); + youtubeOverlaysLayoutRef = new WeakReference<>(viewGroup); + + skipHighlightButtonRef = new WeakReference<>( + Objects.requireNonNull(getChildView(layout, "revanced_sb_skip_highlight_button"))); + skipSponsorButtonRef = new WeakReference<>( + Objects.requireNonNull(getChildView(layout, "revanced_sb_skip_sponsor_button"))); + newSegmentLayoutRef = new WeakReference<>( + Objects.requireNonNull(getChildView(layout, "revanced_sb_new_segment_view"))); + + newSegmentLayoutVisible = false; + skipHighlight = null; + skipSegment = null; + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + public static void hideAll() { + hideSkipHighlightButton(); + hideSkipSegmentButton(); + hideNewSegmentLayout(); + } + + public static void showSkipHighlightButton(@NonNull SponsorSegment segment) { + skipHighlight = Objects.requireNonNull(segment); + NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); + // don't show highlight button if create new segment is visible + final boolean buttonVisibility = newSegmentLayout == null || newSegmentLayout.getVisibility() != View.VISIBLE; + updateSkipButton(skipHighlightButtonRef.get(), segment, buttonVisibility); + } + + public static void showSkipSegmentButton(@NonNull SponsorSegment segment) { + skipSegment = Objects.requireNonNull(segment); + updateSkipButton(skipSponsorButtonRef.get(), segment, true); + } + + public static void hideSkipHighlightButton() { + skipHighlight = null; + updateSkipButton(skipHighlightButtonRef.get(), null, false); + } + + public static void hideSkipSegmentButton() { + skipSegment = null; + updateSkipButton(skipSponsorButtonRef.get(), null, false); + } + + private static void updateSkipButton(@Nullable SkipSponsorButton button, + @Nullable SponsorSegment segment, boolean visible) { + if (button == null) { + return; + } + if (segment != null) { + button.updateSkipButtonText(segment); + } + setViewVisibility(button, visible); + } + + public static void toggleNewSegmentLayoutVisibility() { + NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); + if (newSegmentLayout == null) { // should never happen + Logger.printException(() -> "toggleNewSegmentLayoutVisibility failure"); + return; + } + newSegmentLayoutVisible = (newSegmentLayout.getVisibility() != View.VISIBLE); + if (skipHighlight != null) { + setViewVisibility(skipHighlightButtonRef.get(), !newSegmentLayoutVisible); + } + setViewVisibility(newSegmentLayout, newSegmentLayoutVisible); + } + + public static void hideNewSegmentLayout() { + newSegmentLayoutVisible = false; + setViewVisibility(newSegmentLayoutRef.get(), false); + } + + private static void setViewVisibility(@Nullable View view, boolean visible) { + if (view == null) { + return; + } + visible &= canShowViewElements; + final int desiredVisibility = visible ? View.VISIBLE : View.GONE; + if (view.getVisibility() != desiredVisibility) { + view.setVisibility(desiredVisibility); + } + } + + private static void playerTypeChanged(@NonNull PlayerType playerType) { + try { + final boolean isWatchFullScreen = playerType == PlayerType.WATCH_WHILE_FULLSCREEN; + canShowViewElements = (isWatchFullScreen || playerType == PlayerType.WATCH_WHILE_MAXIMIZED); + + NewSegmentLayout newSegmentLayout = newSegmentLayoutRef.get(); + setNewSegmentLayoutMargins(newSegmentLayout, isWatchFullScreen); + setViewVisibility(newSegmentLayoutRef.get(), newSegmentLayoutVisible); + + SkipSponsorButton skipHighlightButton = skipHighlightButtonRef.get(); + setSkipButtonMargins(skipHighlightButton, isWatchFullScreen); + setViewVisibility(skipHighlightButton, skipHighlight != null); + + SkipSponsorButton skipSponsorButton = skipSponsorButtonRef.get(); + setSkipButtonMargins(skipSponsorButton, isWatchFullScreen); + setViewVisibility(skipSponsorButton, skipSegment != null); + } catch (Exception ex) { + Logger.printException(() -> "Player type changed failure", ex); + } + } + + private static void setNewSegmentLayoutMargins(@Nullable NewSegmentLayout layout, boolean fullScreen) { + if (layout != null) { + setLayoutMargins(layout, fullScreen); + } + } + + private static void setSkipButtonMargins(@Nullable SkipSponsorButton button, boolean fullScreen) { + if (button != null) { + setLayoutMargins(button, fullScreen); + } + } + + private static void setLayoutMargins(@NonNull View view, boolean fullScreen) { + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) view.getLayoutParams(); + if (params == null) { + Logger.printException(() -> "Unable to setNewSegmentLayoutMargins (params are null)"); + return; + } + params.bottomMargin = fullScreen ? (isFullscreenHidden() ? hiddenBottomMargin : ctaBottomMargin) : defaultBottomMargin; + view.setLayoutParams(params); + } + + /** + * Injection point. + */ + public static void endOfVideoReached() { + try { + Logger.printDebug(() -> "endOfVideoReached"); + // the buttons automatically set themselves to visible when appropriate, + // but if buttons are showing when the end of the video is reached then they need + // to be forcefully hidden + if (!Settings.ALWAYS_REPEAT.get()) { + CreateSegmentButtonController.hide(); + VotingButtonController.hide(); + } + } catch (Exception ex) { + Logger.printException(() -> "endOfVideoReached failure", ex); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/VotingButtonController.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/VotingButtonController.java new file mode 100644 index 000000000..4b8ab8e2e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/VotingButtonController.java @@ -0,0 +1,92 @@ +package app.revanced.extension.youtube.sponsorblock.ui; + +import static app.revanced.extension.shared.utils.Utils.getChildView; +import static app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController.videoHasSegments; + +import android.view.View; +import android.widget.ImageView; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.overlaybutton.BottomControlButton; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.VideoInformation; +import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils; + +@SuppressWarnings("unused") +public class VotingButtonController { + private static WeakReference buttonReference = new WeakReference<>(null); + private static boolean isVisible; + + + /** + * injection point + */ + public static void initialize(View youtubeControlsLayout) { + try { + ImageView imageView = Objects.requireNonNull(getChildView(youtubeControlsLayout, "revanced_sb_voting_button")); + imageView.setVisibility(View.GONE); + imageView.setOnClickListener(v -> SponsorBlockUtils.onVotingClicked(v.getContext())); + buttonReference = new WeakReference<>(imageView); + } catch (Exception ex) { + Logger.printException(() -> "Unable to set RelativeLayout", ex); + } + } + + public static void changeVisibility(boolean visible, boolean animation) { + ImageView imageView = buttonReference.get(); + if (imageView == null || isVisible == visible) return; + isVisible = visible; + + if (visible) { + imageView.clearAnimation(); + if (!shouldBeShown()) { + return; + } + if (animation) { + imageView.startAnimation(BottomControlButton.getButtonFadeIn()); + } + imageView.setVisibility(View.VISIBLE); + return; + } + if (imageView.getVisibility() == View.VISIBLE) { + imageView.clearAnimation(); + if (animation) { + imageView.startAnimation(BottomControlButton.getButtonFadeOut()); + } + imageView.setVisibility(View.GONE); + } + } + + public static void changeVisibilityNegatedImmediate() { + ImageView imageView = buttonReference.get(); + if (imageView == null) return; + if (!shouldBeShown()) return; + + + imageView.clearAnimation(); + imageView.startAnimation(BottomControlButton.getButtonFadeOutImmediate()); + imageView.setVisibility(View.GONE); + } + + private static boolean shouldBeShown() { + return Settings.SB_ENABLED.get() && Settings.SB_VOTING_BUTTON.get() + && !VideoInformation.isAtEndOfVideo() && videoHasSegments(); + } + + public static void hide() { + if (!isVisible) { + return; + } + Utils.verifyOnMainThread(); + View v = buttonReference.get(); + if (v == null) { + return; + } + v.setVisibility(View.GONE); + isVisible = false; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt new file mode 100644 index 000000000..3d3c5d83d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt @@ -0,0 +1,161 @@ +package app.revanced.extension.youtube.swipecontrols + +import android.content.Context +import android.graphics.Color +import app.revanced.extension.youtube.settings.Settings +import app.revanced.extension.youtube.shared.LockModeState +import app.revanced.extension.youtube.shared.PlayerType +import app.revanced.extension.youtube.utils.ExtendedUtils.validateValue + +/** + * provider for configuration for volume and brightness swipe controls + * + * @param context the context to create in + */ +class SwipeControlsConfigurationProvider( + private val context: Context, +) { + // region swipe enable + + /** + * should swipe controls be enabled? (global setting) + */ + val enableSwipeControls: Boolean + get() = isFullscreenVideo && (enableVolumeControls || enableBrightnessControl) + + /** + * should swipe controls for volume be enabled? + */ + val enableVolumeControls: Boolean + get() = Settings.ENABLE_SWIPE_VOLUME.get() + + /** + * should swipe controls for volume be enabled? + */ + val enableBrightnessControl: Boolean + get() = Settings.ENABLE_SWIPE_BRIGHTNESS.get() + + /** + * is the video player currently in fullscreen mode? + */ + private val isFullscreenVideo: Boolean + get() = PlayerType.current == PlayerType.WATCH_WHILE_FULLSCREEN + + /** + * is the video player currently in lock mode? + */ + val isScreenLocked: Boolean + get() = LockModeState.current.isLocked() + + val enableSwipeControlsLockMode: Boolean + get() = Settings.SWIPE_LOCK_MODE.get() + + // endregion + + // region keys enable + + /** + * should volume key controls be overwritten? (global setting) + */ + val overwriteVolumeKeyControls: Boolean + get() = isFullscreenVideo && enableVolumeControls + + // endregion + + // region gesture adjustments + + /** + * should press-to-swipe be enabled? + */ + val shouldEnablePressToSwipe: Boolean + get() = Settings.ENABLE_SWIPE_PRESS_TO_ENGAGE.get() + + /** + * threshold for swipe detection + * this may be called rapidly in onScroll, so we have to load it once and then leave it constant + */ + val swipeMagnitudeThreshold: Int + get() = Settings.SWIPE_MAGNITUDE_THRESHOLD.get() + + /** + * swipe distances for brightness + */ + val brightnessDistance: Float + get() = validateValue( + Settings.SWIPE_BRIGHTNESS_SENSITIVITY, + 1, + 1000, + "revanced_swipe_brightness_sensitivity_invalid_toast" + ).toFloat() / 100 // 1f + + /** + * swipe distances for volume + */ + val volumeDistance: Float + get() = validateValue( + Settings.SWIPE_VOLUME_SENSITIVITY, + 1, + 1000, + "revanced_swipe_volume_sensitivity_invalid_toast" + ).toFloat() / 100 * 10 // 10f + + // endregion + + // region overlay adjustments + + /** + * should the overlay enable haptic feedback? + */ + val shouldEnableHapticFeedback: Boolean + get() = Settings.ENABLE_SWIPE_HAPTIC_FEEDBACK.get() + + /** + * how long the overlay should be shown on changes + */ + val overlayShowTimeoutMillis: Long + get() = Settings.SWIPE_OVERLAY_TIMEOUT.get() + + /** + * text size for the overlay, in sp + */ + val overlayTextSize: Int + get() = Settings.SWIPE_OVERLAY_TEXT_SIZE.get() + + /** + * get the background color for text on the overlay, as a color int + */ + val overlayTextBackgroundColor: Int + get() = Color.argb(Settings.SWIPE_OVERLAY_BACKGROUND_ALPHA.get(), 0, 0, 0) + + /** + * get the foreground color for text on the overlay, as a color int + */ + val overlayForegroundColor: Int + get() = Color.WHITE + + // endregion + + // region behaviour + + /** + * should the brightness be saved and restored when exiting or entering fullscreen + */ + val shouldSaveAndRestoreBrightness: Boolean + get() = Settings.ENABLE_SAVE_AND_RESTORE_BRIGHTNESS.get() + + /** + * should auto-brightness be enabled at the lowest value of the brightness gesture + */ + val shouldLowestValueEnableAutoBrightness: Boolean + get() = Settings.ENABLE_SWIPE_LOWEST_VALUE_AUTO_BRIGHTNESS.get() + + /** + * variable that stores the brightness gesture value in the settings + */ + var savedScreenBrightnessValue: Float + get() = Settings.SWIPE_BRIGHTNESS_VALUE.get() + set(value) = Settings.SWIPE_BRIGHTNESS_VALUE.save(value) + + // endregion + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt new file mode 100644 index 000000000..3ebebc252 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity.kt @@ -0,0 +1,236 @@ +package app.revanced.extension.youtube.swipecontrols + +import android.app.Activity +import android.os.Build +import android.os.Bundle +import android.view.KeyEvent +import android.view.MotionEvent +import android.view.ViewGroup +import app.revanced.extension.shared.utils.Logger.printDebug +import app.revanced.extension.shared.utils.Logger.printException +import app.revanced.extension.youtube.shared.PlayerType +import app.revanced.extension.youtube.swipecontrols.controller.AudioVolumeController +import app.revanced.extension.youtube.swipecontrols.controller.ScreenBrightnessController +import app.revanced.extension.youtube.swipecontrols.controller.SwipeZonesController +import app.revanced.extension.youtube.swipecontrols.controller.VolumeKeysController +import app.revanced.extension.youtube.swipecontrols.controller.gesture.ClassicSwipeController +import app.revanced.extension.youtube.swipecontrols.controller.gesture.PressToSwipeController +import app.revanced.extension.youtube.swipecontrols.controller.gesture.core.GestureController +import app.revanced.extension.youtube.swipecontrols.misc.Rectangle +import app.revanced.extension.youtube.swipecontrols.views.SwipeControlsOverlayLayout +import java.lang.ref.WeakReference + +/** + * The main controller for volume and brightness swipe controls. + * note that the superclass is overwritten to the superclass of the MainActivity at patch time + * + * @smali Lapp/revanced/integrations/youtube/swipecontrols/SwipeControlsHostActivity; + */ +class SwipeControlsHostActivity : Activity() { + /** + * current instance of [AudioVolumeController] + */ + var audio: AudioVolumeController? = null + + /** + * current instance of [ScreenBrightnessController] + */ + var screen: ScreenBrightnessController? = null + + /** + * current instance of [SwipeControlsConfigurationProvider] + */ + lateinit var config: SwipeControlsConfigurationProvider + + /** + * current instance of [SwipeControlsOverlayLayout] + */ + lateinit var overlay: SwipeControlsOverlayLayout + + /** + * current instance of [SwipeZonesController] + */ + lateinit var zones: SwipeZonesController + + /** + * main gesture controller + */ + private lateinit var gesture: GestureController + + /** + * main volume keys controller + */ + private lateinit var keys: VolumeKeysController + + /** + * current content view with id [android.R.id.content] + */ + private val contentRoot + get() = window.decorView.findViewById(android.R.id.content) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + initialize() + } + + override fun onStart() { + super.onStart() + reAttachOverlays() + } + + override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { + ensureInitialized() + return if ((ev != null) && gesture.submitTouchEvent(ev)) { + true + } else { + super.dispatchTouchEvent(ev) + } + } + + override fun dispatchKeyEvent(ev: KeyEvent?): Boolean { + ensureInitialized() + return if ((ev != null) && keys.onKeyEvent(ev)) { + true + } else { + super.dispatchKeyEvent(ev) + } + } + + /** + * dispatch a touch event to downstream views + * + * @param event the event to dispatch + * @return was the event consumed? + */ + fun dispatchDownstreamTouchEvent(event: MotionEvent) = + super.dispatchTouchEvent(event) + + /** + * ensures that swipe controllers are initialized and attached. + * on some ROMs with SDK <= 23, [onCreate] and [onStart] may not be called correctly. + * see https://github.com/revanced/revanced-patches/issues/446 + */ + private fun ensureInitialized() { + if (!this::config.isInitialized) { + printException { + "swipe controls were not initialized in onCreate, initializing on-the-fly (SDK is ${Build.VERSION.SDK_INT})" + } + initialize() + reAttachOverlays() + } + } + + /** + * initializes controllers, only call once + */ + private fun initialize() { + // create controllers + printDebug { "initializing swipe controls controllers" } + config = SwipeControlsConfigurationProvider(this) + keys = VolumeKeysController(this) + audio = createAudioController() + screen = createScreenController() + + // create overlay + SwipeControlsOverlayLayout(this, config).let { + overlay = it + contentRoot.addView(it) + } + + // create swipe zone controller + zones = SwipeZonesController(this) { + Rectangle( + contentRoot.x.toInt(), + contentRoot.y.toInt(), + contentRoot.width, + contentRoot.height, + ) + } + + // create the gesture controller + gesture = createGestureController() + + // listen for changes in the player type + PlayerType.onChange += this::onPlayerTypeChanged + + // set current instance reference + currentHost = WeakReference(this) + } + + /** + * (re) attaches swipe overlays + */ + private fun reAttachOverlays() { + printDebug { "attaching swipe controls overlay" } + contentRoot.removeView(overlay) + contentRoot.addView(overlay) + } + + // Flag that indicates whether the brightness has been saved and restored default brightness + private var isBrightnessSaved = false + + /** + * called when the player type changes + * + * @param type the new player type + */ + private fun onPlayerTypeChanged(type: PlayerType) { + when { + // If saving and restoring brightness is enabled, and the player type is WATCH_WHILE_FULLSCREEN, + // and brightness has already been saved, then restore the screen brightness + config.shouldSaveAndRestoreBrightness && type == PlayerType.WATCH_WHILE_FULLSCREEN && isBrightnessSaved -> { + screen?.restore() + isBrightnessSaved = false + } + // If saving and restoring brightness is enabled, and brightness has not been saved, + // then save the current screen state, restore default brightness, and mark brightness as saved + config.shouldSaveAndRestoreBrightness && !isBrightnessSaved -> { + screen?.save() + screen?.restoreDefaultBrightness() + isBrightnessSaved = true + } + // If saving and restoring brightness is disabled, simply keep the default brightness + else -> screen?.restoreDefaultBrightness() + } + } + + /** + * create the audio volume controller + */ + private fun createAudioController() = + if (config.enableVolumeControls) { + AudioVolumeController(this) + } else { + null + } + + /** + * create the screen brightness controller instance + */ + private fun createScreenController() = + if (config.enableBrightnessControl) { + ScreenBrightnessController(this) + } else { + null + } + + /** + * create the gesture controller based on settings + */ + private fun createGestureController() = + if (config.shouldEnablePressToSwipe) { + PressToSwipeController(this, config) + } else { + ClassicSwipeController(this, config) + } + + companion object { + /** + * the currently active swipe controls host. + * the reference may be null! + */ + @JvmStatic + var currentHost: WeakReference = WeakReference(null) + private set + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/AudioVolumeController.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/AudioVolumeController.kt new file mode 100644 index 000000000..dd4ff6463 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/AudioVolumeController.kt @@ -0,0 +1,79 @@ +package app.revanced.extension.youtube.swipecontrols.controller + +import android.content.Context +import android.media.AudioManager +import app.revanced.extension.shared.utils.Logger.printException +import app.revanced.extension.shared.utils.Utils.isSDKAbove +import app.revanced.extension.youtube.swipecontrols.misc.clamp +import kotlin.properties.Delegates + +/** + * controller to adjust the device volume level + * + * @param context the context to bind the audio service in + * @param targetStream the stream that is being controlled. Must be one of the STREAM_* constants in [AudioManager] + */ +class AudioVolumeController( + context: Context, + private val targetStream: Int = AudioManager.STREAM_MUSIC, +) { + + /** + * audio service connection + */ + private lateinit var audioManager: AudioManager + private var minimumVolumeIndex by Delegates.notNull() + private var maximumVolumeIndex by Delegates.notNull() + + init { + // bind audio service + val mgr = context.getSystemService(Context.AUDIO_SERVICE) as? AudioManager + if (mgr == null) { + printException { "failed to acquire AUDIO_SERVICE" } + } else { + audioManager = mgr + maximumVolumeIndex = audioManager.getStreamMaxVolume(targetStream) + minimumVolumeIndex = + if (isSDKAbove(28)) { + audioManager.getStreamMinVolume( + targetStream, + ) + } else { + 0 + } + } + } + + /** + * the current volume, ranging from 0.0 to [maxVolume] + */ + var volume: Int + get() { + // check if initialized correctly + if (!this::audioManager.isInitialized) return 0 + + // get current volume + return currentVolumeIndex - minimumVolumeIndex + } + set(value) { + // check if initialized correctly + if (!this::audioManager.isInitialized) return + + // set new volume + currentVolumeIndex = + (value + minimumVolumeIndex).clamp(minimumVolumeIndex, maximumVolumeIndex) + } + + /** + * the maximum possible volume + */ + val maxVolume: Int + get() = maximumVolumeIndex - minimumVolumeIndex + + /** + * the current volume index of the target stream + */ + private var currentVolumeIndex: Int + get() = audioManager.getStreamVolume(targetStream) + set(value) = audioManager.setStreamVolume(targetStream, value, 0) +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/ScreenBrightnessController.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/ScreenBrightnessController.kt new file mode 100644 index 000000000..abf0d0db8 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/ScreenBrightnessController.kt @@ -0,0 +1,67 @@ +package app.revanced.extension.youtube.swipecontrols.controller + +import android.view.WindowManager +import app.revanced.extension.youtube.swipecontrols.SwipeControlsHostActivity +import app.revanced.extension.youtube.swipecontrols.misc.clamp + +/** + * controller to adjust the screen brightness level + * + * @param host the host activity of which the brightness is adjusted, the main controller instance + */ +class ScreenBrightnessController( + val host: SwipeControlsHostActivity, +) { + + /** + * the current screen brightness in percent, ranging from 0.0 to 100.0 + */ + var screenBrightness: Double + get() = rawScreenBrightness * 100.0 + set(value) { + rawScreenBrightness = (value.toFloat() / 100f).clamp(0f, 1f) + } + + /** + * restore the screen brightness to the default device brightness + */ + fun restoreDefaultBrightness() { + rawScreenBrightness = WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE + } + + // Flag that indicates whether the brightness has been restored + private var isBrightnessRestored = false + + /** + * save the current screen brightness into settings, to be brought back using [restore] + */ + fun save() { + if (isBrightnessRestored) { + // Saves the current screen brightness value into settings + host.config.savedScreenBrightnessValue = rawScreenBrightness + // Reset the flag + isBrightnessRestored = false + } + } + + /** + * restore the screen brightness from settings saved using [save] + */ + fun restore() { + // Restores the screen brightness value from the saved settings + rawScreenBrightness = host.config.savedScreenBrightnessValue + // Mark that brightness has been restored + isBrightnessRestored = true + } + + /** + * wrapper for the raw screen brightness in [WindowManager.LayoutParams.screenBrightness] + */ + private var rawScreenBrightness: Float + get() = host.window.attributes.screenBrightness + private set(value) { + val attr = host.window.attributes + attr.screenBrightness = value + host.window.attributes = attr + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/SwipeZonesController.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/SwipeZonesController.kt new file mode 100644 index 000000000..d42e770d2 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/SwipeZonesController.kt @@ -0,0 +1,154 @@ +package app.revanced.extension.youtube.swipecontrols.controller + +import android.app.Activity +import android.util.TypedValue +import android.view.ViewGroup +import app.revanced.extension.shared.utils.ResourceUtils.ResourceType +import app.revanced.extension.shared.utils.ResourceUtils.getIdentifier +import app.revanced.extension.youtube.settings.Settings +import app.revanced.extension.youtube.swipecontrols.misc.Rectangle +import app.revanced.extension.youtube.swipecontrols.misc.applyDimension +import app.revanced.extension.youtube.utils.ExtendedUtils.validateValue +import kotlin.math.min + +/** + * Y- Axis: + * -------- 0 + * ^ + * dead | 40dp + * v + * -------- yDeadTop + * ^ + * swipe | + * v + * -------- yDeadBtm + * ^ + * dead | 80dp + * v + * -------- screenHeight + * + * X- Axis: + * 0 xBrigStart xBrigEnd xVolStart xVolEnd screenWidth + * | | | | | | + * | 20dp | 3/8 | 2/8 | 3/8 | 20dp | + * | <------> | <------> | <------> | <------> | <------> | + * | dead | brightness | dead | volume | dead | + * | <--------------------------------> | + * 1/1 + */ +@Suppress("PrivatePropertyName") +class SwipeZonesController( + private val host: Activity, + private val fallbackScreenRect: () -> Rectangle, +) { + + private val overlayRectSize = validateValue( + Settings.SWIPE_OVERLAY_RECT_SIZE, + 0, + 50, + "revanced_swipe_overlay_rect_size_invalid_toast" + ) + + /** + * 20dp, in pixels + */ + private val _20dp = 20.applyDimension(host, TypedValue.COMPLEX_UNIT_DIP) + + /** + * 40dp, in pixels + */ + private val _40dp = 40.applyDimension(host, TypedValue.COMPLEX_UNIT_DIP) + + /** + * 80dp, in pixels + */ + private val _80dp = 80.applyDimension(host, TypedValue.COMPLEX_UNIT_DIP) + + /** + * id for R.id.player_view + */ + private val playerViewId = getIdentifier("player_view", ResourceType.ID, host) + + /** + * current bounding rectangle of the player + */ + private var playerRect: Rectangle? = null + + /** + * rectangle of the area that is effectively usable for swipe controls + */ + private val effectiveSwipeRect: Rectangle + get() { + maybeAttachPlayerBoundsListener() + val p = if (playerRect != null) playerRect!! else fallbackScreenRect() + return Rectangle( + p.x + _20dp, + p.y + _40dp, + p.width - _20dp, + p.height - _20dp - _80dp, + ) + } + + /** + * the rectangle of the volume control zone + */ + val volume: Rectangle + get() { + val zoneWidth = effectiveSwipeRect.width * overlayRectSize / 100 + return Rectangle( + effectiveSwipeRect.right - zoneWidth, + effectiveSwipeRect.top, + zoneWidth, + effectiveSwipeRect.height, + ) + } + + /** + * the rectangle of the screen brightness control zone + */ + val brightness: Rectangle + get() { + val zoneWidth = effectiveSwipeRect.width * overlayRectSize / 100 + return Rectangle( + effectiveSwipeRect.left, + effectiveSwipeRect.top, + zoneWidth, + effectiveSwipeRect.height, + ) + } + + /** + * try to attach a listener to the player_view and update the player rectangle. + * once a listener is attached, this function does nothing + */ + private fun maybeAttachPlayerBoundsListener() { + if (playerRect != null) return + host.findViewById(playerViewId)?.let { + onPlayerViewLayout(it) + it.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + onPlayerViewLayout(it) + } + } + } + + /** + * update the player rectangle on player_view layout + * + * @param playerView the player view + */ + private fun onPlayerViewLayout(playerView: ViewGroup) { + playerView.getChildAt(0)?.let { playerSurface -> + // the player surface is centered in the player view + // figure out the width of the surface including the padding (same on the left and right side) + // and use that width for the player rectangle size + // this automatically excludes any engagement panel from the rect + val playerWidthWithPadding = playerSurface.width + (playerSurface.x.toInt() * 2) + playerRect = Rectangle( + playerView.x.toInt(), + playerView.y.toInt(), + min(playerView.width, playerWidthWithPadding), + playerView.height, + ) + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/VolumeKeysController.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/VolumeKeysController.kt new file mode 100644 index 000000000..90aad8886 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/VolumeKeysController.kt @@ -0,0 +1,53 @@ +package app.revanced.extension.youtube.swipecontrols.controller + +import android.view.KeyEvent +import app.revanced.extension.youtube.swipecontrols.SwipeControlsHostActivity + +/** + * controller for custom volume button behaviour + * + * @param controller main controller instance + */ +class VolumeKeysController( + private val controller: SwipeControlsHostActivity, +) { + /** + * key event handler + * + * @param event the key event + * @return consume the event? + */ + fun onKeyEvent(event: KeyEvent): Boolean { + if (!controller.config.overwriteVolumeKeyControls) { + return false + } + + return when (event.keyCode) { + KeyEvent.KEYCODE_VOLUME_DOWN -> + handleVolumeKeyEvent(event, false) + + KeyEvent.KEYCODE_VOLUME_UP -> + handleVolumeKeyEvent(event, true) + + else -> false + } + } + + /** + * handle a volume up / down key event + * + * @param event the key event + * @param volumeUp was the key pressed the volume up key? + * @return consume the event? + */ + private fun handleVolumeKeyEvent(event: KeyEvent, volumeUp: Boolean): Boolean { + if (event.action == KeyEvent.ACTION_DOWN) { + controller.audio?.apply { + volume += if (volumeUp) 1 else -1 + controller.overlay.onVolumeChanged(volume, maxVolume) + } + } + + return true + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.kt new file mode 100644 index 000000000..b3221c096 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/ClassicSwipeController.kt @@ -0,0 +1,117 @@ +package app.revanced.extension.youtube.swipecontrols.controller.gesture + +import android.view.MotionEvent +import app.revanced.extension.youtube.shared.PlayerControlsVisibilityObserver +import app.revanced.extension.youtube.shared.PlayerControlsVisibilityObserverImpl +import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider +import app.revanced.extension.youtube.swipecontrols.SwipeControlsHostActivity +import app.revanced.extension.youtube.swipecontrols.controller.gesture.core.BaseGestureController +import app.revanced.extension.youtube.swipecontrols.controller.gesture.core.SwipeDetector +import app.revanced.extension.youtube.swipecontrols.misc.contains +import app.revanced.extension.youtube.swipecontrols.misc.toPoint + +/** + * provides the classic swipe controls experience, as it was with 'XFenster' + * + * @param controller reference to the main swipe controller + */ +class ClassicSwipeController( + private val controller: SwipeControlsHostActivity, + private val config: SwipeControlsConfigurationProvider, +) : BaseGestureController(controller), + PlayerControlsVisibilityObserver by PlayerControlsVisibilityObserverImpl(controller) { + /** + * the last event captured in [onDown] + */ + private var lastOnDownEvent: MotionEvent? = null + + override val shouldForceInterceptEvents: Boolean + get() = currentSwipe == SwipeDetector.SwipeDirection.VERTICAL + + override fun isInSwipeZone(motionEvent: MotionEvent): Boolean { + val inVolumeZone = motionEvent.toPoint() in controller.zones.volume + val inBrightnessZone = motionEvent.toPoint() in controller.zones.brightness + + return inVolumeZone || inBrightnessZone + } + + override fun shouldDropMotion(motionEvent: MotionEvent): Boolean { + // ignore gestures with more than one pointer + // when such a gesture is detected, dispatch the first event of the gesture to downstream + if (motionEvent.pointerCount > 1) { + lastOnDownEvent?.let { + controller.dispatchDownstreamTouchEvent(it) + it.recycle() + } + lastOnDownEvent = null + return true + } + + // ignore gestures when player controls are visible + return arePlayerControlsVisible + } + + override fun onDown(motionEvent: MotionEvent): Boolean { + // save the event for later + lastOnDownEvent?.recycle() + lastOnDownEvent = MotionEvent.obtain(motionEvent) + + // must be inside swipe zone + return isInSwipeZone(motionEvent) + } + + override fun onSingleTapUp(motionEvent: MotionEvent): Boolean { + MotionEvent.obtain(motionEvent).let { + it.action = MotionEvent.ACTION_DOWN + controller.dispatchDownstreamTouchEvent(it) + it.recycle() + } + + return false + } + + override fun onDoubleTapEvent(motionEvent: MotionEvent): Boolean { + MotionEvent.obtain(motionEvent).let { + controller.dispatchDownstreamTouchEvent(it) + it.recycle() + } + + return super.onDoubleTapEvent(motionEvent) + } + + override fun onLongPress(motionEvent: MotionEvent) { + MotionEvent.obtain(motionEvent).let { + controller.dispatchDownstreamTouchEvent(it) + it.recycle() + } + + super.onLongPress(motionEvent) + } + + override fun onSwipe( + from: MotionEvent, + to: MotionEvent, + distanceX: Double, + distanceY: Double, + ): Boolean { + // cancel if locked + if (!config.enableSwipeControlsLockMode && config.isScreenLocked) + return false + // cancel if not vertical + if (currentSwipe != SwipeDetector.SwipeDirection.VERTICAL) + return false + return when (from.toPoint()) { + in controller.zones.volume -> { + scrollVolume(distanceY) + true + } + + in controller.zones.brightness -> { + scrollBrightness(distanceY) + true + } + + else -> false + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/PressToSwipeController.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/PressToSwipeController.kt new file mode 100644 index 000000000..d6ab99c34 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/PressToSwipeController.kt @@ -0,0 +1,90 @@ +package app.revanced.extension.youtube.swipecontrols.controller.gesture + +import android.view.MotionEvent +import app.revanced.extension.youtube.patches.swipe.SwipeControlsPatch.isEngagementOverlayVisible +import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider +import app.revanced.extension.youtube.swipecontrols.SwipeControlsHostActivity +import app.revanced.extension.youtube.swipecontrols.controller.gesture.core.BaseGestureController +import app.revanced.extension.youtube.swipecontrols.controller.gesture.core.SwipeDetector +import app.revanced.extension.youtube.swipecontrols.misc.contains +import app.revanced.extension.youtube.swipecontrols.misc.toPoint + +/** + * provides the press-to-swipe (PtS) swipe controls experience + * + * @param controller reference to the main swipe controller + */ +class PressToSwipeController( + private val controller: SwipeControlsHostActivity, + private val config: SwipeControlsConfigurationProvider, +) : BaseGestureController(controller) { + /** + * monitors if the user is currently in a swipe session. + */ + private var isInSwipeSession = false + + override val shouldForceInterceptEvents: Boolean + get() = currentSwipe == SwipeDetector.SwipeDirection.VERTICAL && isInSwipeSession + + override fun shouldDropMotion(motionEvent: MotionEvent): Boolean = false + + override fun isInSwipeZone(motionEvent: MotionEvent): Boolean { + val inVolumeZone = if (controller.config.enableVolumeControls) { + (motionEvent.toPoint() in controller.zones.volume) + } else { + false + } + val inBrightnessZone = if (controller.config.enableBrightnessControl) { + (motionEvent.toPoint() in controller.zones.brightness) + } else { + false + } + + return inVolumeZone || inBrightnessZone + } + + override fun onUp(motionEvent: MotionEvent) { + super.onUp(motionEvent) + isInSwipeSession = false + } + + override fun onLongPress(motionEvent: MotionEvent) { + // enter swipe session with feedback + isInSwipeSession = true + controller.overlay.onEnterSwipeSession() + + // send GestureDetector a ACTION_CANCEL event so it will handle further events + motionEvent.action = MotionEvent.ACTION_CANCEL + detector.onTouchEvent(motionEvent) + } + + override fun onSwipe( + from: MotionEvent, + to: MotionEvent, + distanceX: Double, + distanceY: Double, + ): Boolean { + // cancel if locked + if (!config.enableSwipeControlsLockMode && config.isScreenLocked) + return false + // cancel if not in swipe session or vertical + if (!isInSwipeSession || currentSwipe != SwipeDetector.SwipeDirection.VERTICAL) + return false + // ignore gestures when engagement overlay is visible + if (isEngagementOverlayVisible()) + return false + return when (from.toPoint()) { + in controller.zones.volume -> { + scrollVolume(distanceY) + true + } + + in controller.zones.brightness -> { + scrollBrightness(distanceY) + true + } + + else -> false + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt new file mode 100644 index 000000000..ac995bfd7 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/BaseGestureController.kt @@ -0,0 +1,156 @@ +package app.revanced.extension.youtube.swipecontrols.controller.gesture.core + +import android.view.GestureDetector +import android.view.MotionEvent +import app.revanced.extension.youtube.swipecontrols.SwipeControlsHostActivity + +/** + * the common base of all [GestureController] classes. + * handles most of the boilerplate code needed for gesture detection + * + * @param controller reference to the main swipe controller + */ +abstract class BaseGestureController( + private val controller: SwipeControlsHostActivity, +) : GestureController, + GestureDetector.SimpleOnGestureListener(), + SwipeDetector by SwipeDetectorImpl( + controller.config.swipeMagnitudeThreshold.toDouble(), + ), + VolumeAndBrightnessScroller by VolumeAndBrightnessScrollerImpl( + controller, + controller.audio, + controller.screen, + controller.overlay, + controller.config.volumeDistance, + controller.config.brightnessDistance, + ) { + + /** + * the main gesture detector that powers everything + */ + @Suppress("LeakingThis") + protected val detector = GestureDetector(controller, this) + + /** + * were downstream event cancelled already? used in [onScroll] + */ + private var didCancelDownstream = false + + override fun submitTouchEvent(motionEvent: MotionEvent): Boolean { + // ignore if swipe is disabled + if (!controller.config.enableSwipeControls) { + return false + } + + // create a copy of the event so we can modify it + // without causing any issues downstream + val me = MotionEvent.obtain(motionEvent) + + // check if we should drop this motion + val dropped = shouldDropMotion(me) + if (dropped) { + me.action = MotionEvent.ACTION_CANCEL + } + + // send the event to the detector + // if we force intercept events, the event is always consumed + val consumed = detector.onTouchEvent(me) || shouldForceInterceptEvents + + // invoke the custom onUp handler + if (me.action == MotionEvent.ACTION_UP || me.action == MotionEvent.ACTION_CANCEL) { + onUp(me) + } + + // recycle the copy + me.recycle() + + // do not consume dropped events + // or events outside of any swipe zone + return !dropped && consumed && isInSwipeZone(me) + } + + /** + * custom handler for [MotionEvent.ACTION_UP] event, because GestureDetector doesn't offer that :| + * + * @param motionEvent the motion event + */ + open fun onUp(motionEvent: MotionEvent) { + didCancelDownstream = false + resetSwipe() + resetScroller() + } + + override fun onScroll( + from: MotionEvent?, + to: MotionEvent, + distanceX: Float, + distanceY: Float, + ): Boolean { + // submit to swipe detector + submitForSwipe(from!!, to, distanceX, distanceY) + + // call swipe callback if in a swipe + return if (currentSwipe != SwipeDetector.SwipeDirection.NONE) { + val consumed = onSwipe( + from, + to, + distanceX.toDouble(), + distanceY.toDouble(), + ) + + // if the swipe was consumed, cancel downstream events once + if (consumed && !didCancelDownstream) { + didCancelDownstream = true + MotionEvent.obtain(from).let { + it.action = MotionEvent.ACTION_CANCEL + controller.dispatchDownstreamTouchEvent(it) + it.recycle() + } + } + + consumed + } else { + false + } + } + + /** + * should [submitTouchEvent] force- intercept all touch events? + */ + abstract val shouldForceInterceptEvents: Boolean + + /** + * check if provided motion event is in any active swipe zone? + * + * @param motionEvent the event to check + * @return is the event in any active swipe zone? + */ + abstract fun isInSwipeZone(motionEvent: MotionEvent): Boolean + + /** + * check if a touch event should be dropped. + * when a event is dropped, the gesture detector received a [MotionEvent.ACTION_CANCEL] event and the event is not consumed + * + * @param motionEvent the event to check + * @return should the event be dropped? + */ + abstract fun shouldDropMotion(motionEvent: MotionEvent): Boolean + + /** + * handler for swipe events, once a swipe is detected. + * the direction of the swipe can be accessed in [currentSwipe] + * + * @param from start event of the swipe + * @param to end event of the swipe + * @param distanceX the horizontal distance of the swipe + * @param distanceY the vertical distance of the swipe + * @return was the event consumed? + */ + abstract fun onSwipe( + from: MotionEvent, + to: MotionEvent, + distanceX: Double, + distanceY: Double, + ): Boolean +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/GestureController.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/GestureController.kt new file mode 100644 index 000000000..49da1f210 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/GestureController.kt @@ -0,0 +1,16 @@ +package app.revanced.extension.youtube.swipecontrols.controller.gesture.core + +import android.view.MotionEvent + +/** + * describes a class that accepts motion events and detects gestures + */ +interface GestureController { + /** + * accept a touch event and try to detect the desired gestures using it + * + * @param motionEvent the motion event that was submitted + * @return was a gesture detected? + */ + fun submitTouchEvent(motionEvent: MotionEvent): Boolean +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/SwipeDetector.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/SwipeDetector.kt new file mode 100644 index 000000000..7d6fa4501 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/SwipeDetector.kt @@ -0,0 +1,94 @@ +package app.revanced.extension.youtube.swipecontrols.controller.gesture.core + +import android.view.MotionEvent +import kotlin.math.abs +import kotlin.math.pow + +/** + * describes a class that can detect swipes and their directionality + */ +interface SwipeDetector { + /** + * the currently detected swipe + */ + val currentSwipe: SwipeDirection + + /** + * submit a onScroll event for swipe detection + * + * @param from start event + * @param to end event + * @param distanceX horizontal scroll distance + * @param distanceY vertical scroll distance + */ + fun submitForSwipe( + from: MotionEvent, + to: MotionEvent, + distanceX: Float, + distanceY: Float, + ) + + /** + * reset the swipe detection + */ + fun resetSwipe() + + /** + * direction of a swipe + */ + enum class SwipeDirection { + /** + * swipe has no direction or no swipe + */ + NONE, + + /** + * swipe along the X- Axes + */ + HORIZONTAL, + + /** + * swipe along the Y- Axes + */ + VERTICAL, + } +} + +/** + * detector that can detect swipes and their directionality + * + * @param swipeMagnitudeThreshold minimum magnitude before a swipe is detected as such + */ +class SwipeDetectorImpl( + private val swipeMagnitudeThreshold: Double, +) : SwipeDetector { + override var currentSwipe = SwipeDetector.SwipeDirection.NONE + + override fun submitForSwipe( + from: MotionEvent, + to: MotionEvent, + distanceX: Float, + distanceY: Float, + ) { + if (currentSwipe == SwipeDetector.SwipeDirection.NONE) { + // no swipe direction was detected yet, try to detect one + // if the user did not swipe far enough, we cannot detect what direction they swiped + // so we wait until a greater distance was swiped + // NOTE: sqrt() can be high- cost, so using squared magnitudes here + val deltaX = abs(to.x - from.x) + val deltaY = abs(to.y - from.y) + val swipeMagnitudeSquared = deltaX.pow(2) + deltaY.pow(2) + if (swipeMagnitudeSquared > swipeMagnitudeThreshold.pow(2)) { + currentSwipe = if (deltaY > deltaX) { + SwipeDetector.SwipeDirection.VERTICAL + } else { + SwipeDetector.SwipeDirection.HORIZONTAL + } + } + } + } + + override fun resetSwipe() { + currentSwipe = SwipeDetector.SwipeDirection.NONE + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt new file mode 100644 index 000000000..4cf7303ba --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt @@ -0,0 +1,103 @@ +package app.revanced.extension.youtube.swipecontrols.controller.gesture.core + +import android.content.Context +import android.util.TypedValue +import app.revanced.extension.youtube.swipecontrols.controller.AudioVolumeController +import app.revanced.extension.youtube.swipecontrols.controller.ScreenBrightnessController +import app.revanced.extension.youtube.swipecontrols.misc.ScrollDistanceHelper +import app.revanced.extension.youtube.swipecontrols.misc.SwipeControlsOverlay +import app.revanced.extension.youtube.swipecontrols.misc.applyDimension + +/** + * describes a class that controls volume and brightness based on scrolling events + */ +interface VolumeAndBrightnessScroller { + /** + * submit a scroll for volume adjustment + * + * @param distance the scroll distance + */ + fun scrollVolume(distance: Double) + + /** + * submit a scroll for brightness adjustment + * + * @param distance the scroll distance + */ + fun scrollBrightness(distance: Double) + + /** + * reset all scroll distances to zero + */ + fun resetScroller() +} + +/** + * handles scrolling of volume and brightness, adjusts them using the provided controllers and updates the overlay + * + * @param context context to create the scrollers in + * @param volumeController volume controller instance. if null, volume control is disabled + * @param screenController screen brightness controller instance. if null, brightness control is disabled + * @param overlayController overlay controller instance + * @param volumeDistance unit distance for volume scrolling, in dp + * @param brightnessDistance unit distance for brightness scrolling, in dp + */ +class VolumeAndBrightnessScrollerImpl( + context: Context, + private val volumeController: AudioVolumeController?, + private val screenController: ScreenBrightnessController?, + private val overlayController: SwipeControlsOverlay, + volumeDistance: Float = 10.0f, + brightnessDistance: Float = 1.0f, +) : VolumeAndBrightnessScroller { + + // region volume + private val volumeScroller = + ScrollDistanceHelper( + volumeDistance.applyDimension( + context, + TypedValue.COMPLEX_UNIT_DIP, + ), + ) { _, _, direction -> + volumeController?.run { + volume += direction + overlayController.onVolumeChanged(volume, maxVolume) + } + } + + override fun scrollVolume(distance: Double) = volumeScroller.add(distance) + //endregion + + //region brightness + private val brightnessScroller = + ScrollDistanceHelper( + brightnessDistance.applyDimension( + context, + TypedValue.COMPLEX_UNIT_DIP, + ), + ) { _, _, direction -> + screenController?.run { + val shouldAdjustBrightness = + if (host.config.shouldLowestValueEnableAutoBrightness) { + screenBrightness > 0 || direction > 0 + } else { + screenBrightness >= 0 || direction >= 0 + } + + if (shouldAdjustBrightness) { + screenBrightness += direction + } else { + restoreDefaultBrightness() + } + overlayController.onBrightnessChanged(screenBrightness) + } + } + + override fun scrollBrightness(distance: Double) = brightnessScroller.add(distance) + //endregion + + override fun resetScroller() { + volumeScroller.reset() + brightnessScroller.reset() + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/Point.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/Point.kt new file mode 100644 index 000000000..8400fedaf --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/Point.kt @@ -0,0 +1,17 @@ +package app.revanced.extension.youtube.swipecontrols.misc + +import android.view.MotionEvent + +/** + * a simple 2D point class + */ +data class Point( + val x: Int, + val y: Int, +) + +/** + * convert the motion event coordinates to a point + */ +fun MotionEvent.toPoint(): Point = + Point(x.toInt(), y.toInt()) diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/Rectangle.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/Rectangle.kt new file mode 100644 index 000000000..723834318 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/Rectangle.kt @@ -0,0 +1,22 @@ +package app.revanced.extension.youtube.swipecontrols.misc + +/** + * a simple rectangle class + */ +data class Rectangle( + val x: Int, + val y: Int, + val width: Int, + val height: Int, +) { + val left = x + val right = x + width + val top = y + val bottom = y + height +} + +/** + * is the point within this rectangle? + */ +operator fun Rectangle.contains(p: Point): Boolean = + p.x in left..right && p.y in top..bottom diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/ScrollDistanceHelper.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/ScrollDistanceHelper.kt new file mode 100644 index 000000000..09a74c229 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/ScrollDistanceHelper.kt @@ -0,0 +1,56 @@ +package app.revanced.extension.youtube.swipecontrols.misc + +import kotlin.math.abs +import kotlin.math.sign + +/** + * helper for scaling onScroll handler + * + * @param unitDistance absolute distance after which the callback is invoked + * @param callback callback function for when unit distance is reached + */ +class ScrollDistanceHelper( + private val unitDistance: Double, + private val callback: (oldDistance: Double, newDistance: Double, direction: Int) -> Unit, +) { + + /** + * total distance scrolled + */ + private var scrolledDistance: Double = 0.0 + + /** + * add a scrolled distance to the total. + * if the [unitDistance] is reached, this function will also invoke the callback + * + * @param distance the distance to add + */ + fun add(distance: Double) { + scrolledDistance += distance + + // invoke the callback if we scrolled far enough + while (abs(scrolledDistance) >= unitDistance) { + val oldDistance = scrolledDistance + subtractUnitDistance() + callback.invoke( + oldDistance, + scrolledDistance, + sign(scrolledDistance).toInt(), + ) + } + } + + /** + * reset the distance scrolled to zero + */ + fun reset() { + scrolledDistance = 0.0 + } + + /** + * subtract the [unitDistance] from the total [scrolledDistance] + */ + private fun subtractUnitDistance() { + scrolledDistance -= (unitDistance * sign(scrolledDistance)) + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.kt new file mode 100644 index 000000000..5e863a3c5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/SwipeControlsOverlay.kt @@ -0,0 +1,26 @@ +package app.revanced.extension.youtube.swipecontrols.misc + +/** + * Interface for all overlays for swipe controls + */ +interface SwipeControlsOverlay { + /** + * called when the currently set volume level was changed + * + * @param newVolume the new volume level + * @param maximumVolume the maximum volume index + */ + fun onVolumeChanged(newVolume: Int, maximumVolume: Int) + + /** + * called when the currently set screen brightness was changed + * + * @param brightness the new screen brightness, in percent (range 0.0 - 100.0) + */ + fun onBrightnessChanged(brightness: Double) + + /** + * called when a new swipe- session has started + */ + fun onEnterSwipeSession() +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/SwipeControlsUtils.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/SwipeControlsUtils.kt new file mode 100644 index 000000000..74b1e777d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/SwipeControlsUtils.kt @@ -0,0 +1,33 @@ +package app.revanced.extension.youtube.swipecontrols.misc + +import android.content.Context +import android.util.TypedValue +import kotlin.math.roundToInt + +fun Float.clamp(min: Float, max: Float): Float { + if (this < min) return min + if (this > max) return max + return this +} + +fun Int.clamp(min: Int, max: Int): Int { + if (this < min) return min + if (this > max) return max + return this +} + +fun Int.applyDimension(context: Context, unit: Int): Int { + return TypedValue.applyDimension( + unit, + this.toFloat(), + context.resources.displayMetrics, + ).roundToInt() +} + +fun Float.applyDimension(context: Context, unit: Int): Double { + return TypedValue.applyDimension( + unit, + this, + context.resources.displayMetrics, + ).toDouble() +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt new file mode 100644 index 000000000..6d2cef606 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt @@ -0,0 +1,147 @@ +package app.revanced.extension.youtube.swipecontrols.views + +import android.content.Context +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.os.Handler +import android.os.Looper +import android.util.TypedValue +import android.view.HapticFeedbackConstants +import android.view.View +import android.view.ViewGroup +import android.widget.RelativeLayout +import android.widget.TextView +import app.revanced.extension.shared.utils.ResourceUtils.ResourceType +import app.revanced.extension.shared.utils.ResourceUtils.getIdentifier +import app.revanced.extension.shared.utils.StringRef.str +import app.revanced.extension.youtube.swipecontrols.SwipeControlsConfigurationProvider +import app.revanced.extension.youtube.swipecontrols.misc.SwipeControlsOverlay +import app.revanced.extension.youtube.swipecontrols.misc.applyDimension +import kotlin.math.round + +/** + * main overlay layout for volume and brightness swipe controls + * + * @param context context to create in + */ +class SwipeControlsOverlayLayout( + context: Context, + private val config: SwipeControlsConfigurationProvider, +) : RelativeLayout(context), SwipeControlsOverlay { + /** + * DO NOT use this, for tools only + */ + constructor(context: Context) : this(context, SwipeControlsConfigurationProvider(context)) + + private val feedbackTextView: TextView + private val autoBrightnessIcon: Drawable + private val manualBrightnessIcon: Drawable + private val mutedVolumeIcon: Drawable + private val normalVolumeIcon: Drawable + + private fun getDrawable(name: String, width: Int, height: Int): Drawable { + return resources.getDrawable( + getIdentifier(name, ResourceType.DRAWABLE, context), + context.theme + ).apply { + setTint(config.overlayForegroundColor) + setBounds( + 0, + 0, + width, + height, + ) + } + } + + init { + // init views + val feedbackYTextViewPadding = 5.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP) + val feedbackXTextViewPadding = 12.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP) + val compoundIconPadding = 4.applyDimension(context, TypedValue.COMPLEX_UNIT_DIP) + feedbackTextView = TextView(context).apply { + layoutParams = LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ).apply { + addRule(CENTER_IN_PARENT, TRUE) + setPadding( + feedbackXTextViewPadding, + feedbackYTextViewPadding, + feedbackXTextViewPadding, + feedbackYTextViewPadding + ) + } + background = GradientDrawable().apply { + cornerRadius = 30f + setColor(config.overlayTextBackgroundColor) + } + setTextColor(config.overlayForegroundColor) + setTextSize(TypedValue.COMPLEX_UNIT_SP, config.overlayTextSize.toFloat()) + compoundDrawablePadding = compoundIconPadding + visibility = GONE + } + addView(feedbackTextView) + + // get icons scaled, assuming square icons + val iconHeight = round(feedbackTextView.lineHeight * .8).toInt() + autoBrightnessIcon = getDrawable("ic_sc_brightness_auto", iconHeight, iconHeight) + manualBrightnessIcon = getDrawable("ic_sc_brightness_manual", iconHeight, iconHeight) + mutedVolumeIcon = getDrawable("ic_sc_volume_mute", iconHeight, iconHeight) + normalVolumeIcon = getDrawable("ic_sc_volume_normal", iconHeight, iconHeight) + } + + private val feedbackHideHandler = Handler(Looper.getMainLooper()) + private val feedbackHideCallback = Runnable { + feedbackTextView.visibility = View.GONE + } + + /** + * show the feedback view for a given time + * + * @param message the message to show + * @param icon the icon to use + */ + private fun showFeedbackView(message: String, icon: Drawable) { + feedbackHideHandler.removeCallbacks(feedbackHideCallback) + feedbackHideHandler.postDelayed(feedbackHideCallback, config.overlayShowTimeoutMillis) + feedbackTextView.apply { + text = message + setCompoundDrawablesRelative( + icon, + null, + null, + null, + ) + visibility = VISIBLE + } + } + + override fun onVolumeChanged(newVolume: Int, maximumVolume: Int) { + showFeedbackView( + "$newVolume", + if (newVolume > 0) normalVolumeIcon else mutedVolumeIcon, + ) + } + + override fun onBrightnessChanged(brightness: Double) { + if (config.shouldLowestValueEnableAutoBrightness && brightness <= 0) { + showFeedbackView( + str("revanced_swipe_lowest_value_auto_brightness_overlay_text"), + autoBrightnessIcon, + ) + } else if (brightness >= 0) { + showFeedbackView("${round(brightness).toInt()}%", manualBrightnessIcon) + } + } + + @Suppress("DEPRECATION") + override fun onEnterSwipeSession() { + if (config.shouldEnableHapticFeedback) { + performHapticFeedback( + HapticFeedbackConstants.LONG_PRESS, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, + ) + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ExtendedUtils.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ExtendedUtils.java new file mode 100644 index 000000000..77f328b52 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ExtendedUtils.java @@ -0,0 +1,115 @@ +package app.revanced.extension.youtube.utils; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import androidx.annotation.NonNull; + +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.settings.FloatSetting; +import app.revanced.extension.shared.settings.IntegerSetting; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.utils.PackageUtils; +import app.revanced.extension.youtube.settings.Settings; + +public class ExtendedUtils extends PackageUtils { + public static final boolean IS_19_17_OR_GREATER = getAppVersionName().compareTo("19.17.00") >= 0; + public static final boolean IS_19_20_OR_GREATER = getAppVersionName().compareTo("19.20.00") >= 0; + public static final boolean IS_19_21_OR_GREATER = getAppVersionName().compareTo("19.21.00") >= 0; + public static final boolean IS_19_26_OR_GREATER = getAppVersionName().compareTo("19.26.00") >= 0; + public static final boolean IS_19_29_OR_GREATER = getAppVersionName().compareTo("19.29.00") >= 0; + public static final boolean IS_19_34_OR_GREATER = getAppVersionName().compareTo("19.34.00") >= 0; + + public static int validateValue(IntegerSetting settings, int min, int max, String message) { + int value = settings.get(); + + if (value < min || value > max) { + showToastShort(str(message)); + showToastShort(str("revanced_extended_reset_to_default_toast")); + settings.resetToDefault(); + value = settings.defaultValue; + } + + return value; + } + + public static float validateValue(FloatSetting settings, float min, float max, String message) { + float value = settings.get(); + + if (value < min || value > max) { + showToastShort(str(message)); + showToastShort(str("revanced_extended_reset_to_default_toast")); + settings.resetToDefault(); + value = settings.defaultValue; + } + + return value; + } + + public static boolean isFullscreenHidden() { + return Settings.DISABLE_ENGAGEMENT_PANEL.get() || Settings.HIDE_QUICK_ACTIONS.get(); + } + + public static boolean isSpoofingToLessThan(@NonNull String versionName) { + if (!Settings.SPOOF_APP_VERSION.get()) + return false; + + return isVersionToLessThan(Settings.SPOOF_APP_VERSION_TARGET.get(), versionName); + } + + public static void setCommentPreviewSettings() { + final boolean enabled = Settings.HIDE_PREVIEW_COMMENT.get(); + final boolean newMethod = Settings.HIDE_PREVIEW_COMMENT_TYPE.get(); + + Settings.HIDE_PREVIEW_COMMENT_OLD_METHOD.save(enabled && !newMethod); + Settings.HIDE_PREVIEW_COMMENT_NEW_METHOD.save(enabled && newMethod); + } + + private static final Setting[] additionalSettings = { + Settings.HIDE_PLAYER_FLYOUT_MENU_AMBIENT, + Settings.HIDE_PLAYER_FLYOUT_MENU_HELP, + Settings.HIDE_PLAYER_FLYOUT_MENU_LOOP, + Settings.HIDE_PLAYER_FLYOUT_MENU_PIP, + Settings.HIDE_PLAYER_FLYOUT_MENU_PREMIUM_CONTROLS, + Settings.HIDE_PLAYER_FLYOUT_MENU_STABLE_VOLUME, + Settings.HIDE_PLAYER_FLYOUT_MENU_STATS_FOR_NERDS, + Settings.HIDE_PLAYER_FLYOUT_MENU_WATCH_IN_VR, + Settings.HIDE_PLAYER_FLYOUT_MENU_YT_MUSIC, + Settings.SPOOF_APP_VERSION, + Settings.SPOOF_APP_VERSION_TARGET + }; + + public static boolean anyMatchSetting(Setting setting) { + for (Setting s : additionalSettings) { + if (setting == s) return true; + } + return false; + } + + public static void setPlayerFlyoutMenuAdditionalSettings() { + Settings.HIDE_PLAYER_FLYOUT_MENU_ADDITIONAL_SETTINGS.save(isAdditionalSettingsEnabled()); + } + + private static boolean isAdditionalSettingsEnabled() { + // In the old player flyout panels, the video quality icon and additional quality icon are the same + // Therefore, additional Settings should not be blocked in old player flyout panels + if (isSpoofingToLessThan("18.22.00")) + return false; + + boolean additionalSettingsEnabled = true; + final BooleanSetting[] additionalSettings = { + Settings.HIDE_PLAYER_FLYOUT_MENU_AMBIENT, + Settings.HIDE_PLAYER_FLYOUT_MENU_HELP, + Settings.HIDE_PLAYER_FLYOUT_MENU_LOOP, + Settings.HIDE_PLAYER_FLYOUT_MENU_PIP, + Settings.HIDE_PLAYER_FLYOUT_MENU_PREMIUM_CONTROLS, + Settings.HIDE_PLAYER_FLYOUT_MENU_STABLE_VOLUME, + Settings.HIDE_PLAYER_FLYOUT_MENU_STATS_FOR_NERDS, + Settings.HIDE_PLAYER_FLYOUT_MENU_WATCH_IN_VR, + Settings.HIDE_PLAYER_FLYOUT_MENU_YT_MUSIC, + }; + for (BooleanSetting s : additionalSettings) { + additionalSettingsEnabled &= s.get(); + } + return additionalSettingsEnabled; + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ThemeUtils.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ThemeUtils.java new file mode 100644 index 000000000..75077701e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ThemeUtils.java @@ -0,0 +1,126 @@ +package app.revanced.extension.youtube.utils; + +import static app.revanced.extension.shared.utils.ResourceUtils.getColor; +import static app.revanced.extension.shared.utils.ResourceUtils.getDrawable; +import static app.revanced.extension.shared.utils.ResourceUtils.getStyleIdentifier; +import static app.revanced.extension.shared.utils.Utils.getResources; + +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.GradientDrawable; + +import app.revanced.extension.shared.utils.BaseThemeUtils; +import app.revanced.extension.shared.utils.Logger; + +@SuppressWarnings({"unused", "SameParameterValue"}) +public class ThemeUtils extends BaseThemeUtils { + + public static int getThemeId() { + final String themeName = isDarkTheme() + ? "Theme.YouTube.Settings.Dark" + : "Theme.YouTube.Settings"; + + return getStyleIdentifier(themeName); + } + + public static Drawable getBackButtonDrawable() { + final String drawableName = isDarkTheme() + ? "yt_outline_arrow_left_white_24" + : "yt_outline_arrow_left_black_24"; + + return getDrawable(drawableName); + } + + public static Drawable getTrashButtonDrawable() { + final String drawableName = isDarkTheme() + ? "yt_outline_trash_can_white_24" + : "yt_outline_trash_can_black_24"; + + return getDrawable(drawableName); + } + + /** + * Since {@link android.widget.Toolbar} is used instead of {@link android.support.v7.widget.Toolbar}, + * We have to manually specify the toolbar background. + * + * @return toolbar background color. + */ + public static int getToolbarBackgroundColor() { + final String colorName = isDarkTheme() + ? "yt_black3" // Color names used in the light theme + : "yt_white1"; // Color names used in the dark theme + + return getColor(colorName); + } + + public static int getPressedElementColor() { + String colorHex = isDarkTheme() + ? lightenColor(getBackgroundColorHexString(), 15) + : darkenColor(getBackgroundColorHexString(), 15); + return Color.parseColor(colorHex); + } + + public static GradientDrawable getSearchViewShape() { + GradientDrawable shape = new GradientDrawable(); + + String currentHex = getBackgroundColorHexString(); + String defaultHex = isDarkTheme() ? "#1A1A1A" : "#E5E5E5"; + + String finalHex; + if (currentThemeColorIsBlackOrWhite()) { + shape.setColor(Color.parseColor(defaultHex)); // stock black/white color + finalHex = defaultHex; + } else { + // custom color theme + String adjustedColor = isDarkTheme() + ? lightenColor(currentHex, 15) + : darkenColor(currentHex, 15); + shape.setColor(Color.parseColor(adjustedColor)); + finalHex = adjustedColor; + } + Logger.printDebug(() -> "searchbar color: " + finalHex); + + shape.setCornerRadius(30 * getResources().getDisplayMetrics().density); + + return shape; + } + + private static boolean currentThemeColorIsBlackOrWhite() { + final int color = isDarkTheme() + ? getDarkColor() + : getLightColor(); + + return getBackgroundColor() == color; + } + + // Convert HEX to RGB + private static int[] hexToRgb(String hex) { + int r = Integer.valueOf(hex.substring(1, 3), 16); + int g = Integer.valueOf(hex.substring(3, 5), 16); + int b = Integer.valueOf(hex.substring(5, 7), 16); + return new int[]{r, g, b}; + } + + // Convert RGB to HEX + private static String rgbToHex(int r, int g, int b) { + return String.format("#%02x%02x%02x", r, g, b); + } + + // Darken color by percentage + private static String darkenColor(String hex, double percentage) { + int[] rgb = hexToRgb(hex); + int r = (int) (rgb[0] * (1 - percentage / 100)); + int g = (int) (rgb[1] * (1 - percentage / 100)); + int b = (int) (rgb[2] * (1 - percentage / 100)); + return rgbToHex(r, g, b); + } + + // Lighten color by percentage + private static String lightenColor(String hex, double percentage) { + int[] rgb = hexToRgb(hex); + int r = (int) (rgb[0] + (255 - rgb[0]) * (percentage / 100)); + int g = (int) (rgb[1] + (255 - rgb[1]) * (percentage / 100)); + int b = (int) (rgb[2] + (255 - rgb[2]) * (percentage / 100)); + return rgbToHex(r, g, b); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java new file mode 100644 index 000000000..9fe6373da --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java @@ -0,0 +1,280 @@ +package app.revanced.extension.youtube.utils; + +import static app.revanced.extension.shared.utils.ResourceUtils.getStringArray; +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.youtube.patches.video.PlaybackSpeedPatch.userSelectedPlaybackSpeed; + +import android.app.AlertDialog; +import android.content.Context; +import android.media.AudioManager; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; + +import app.revanced.extension.shared.settings.EnumSetting; +import app.revanced.extension.shared.utils.IntentUtils; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.patches.shorts.ShortsRepeatStatePatch.ShortsLoopBehavior; +import app.revanced.extension.youtube.patches.video.CustomPlaybackSpeedPatch; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.settings.preference.ExternalDownloaderPlaylistPreference; +import app.revanced.extension.youtube.settings.preference.ExternalDownloaderVideoLongPressPreference; +import app.revanced.extension.youtube.settings.preference.ExternalDownloaderVideoPreference; +import app.revanced.extension.youtube.shared.PlaylistIdPrefix; +import app.revanced.extension.youtube.shared.VideoInformation; + +@SuppressWarnings("unused") +public class VideoUtils extends IntentUtils { + private static final String PLAYLIST_URL = "https://www.youtube.com/playlist?list="; + private static final String VIDEO_URL = "https://youtu.be/"; + private static final String VIDEO_SCHEME_INTENT_FORMAT = "vnd.youtube://%s?start=%d"; + private static final String VIDEO_SCHEME_LINK_FORMAT = "https://youtu.be/%s?t=%d"; + private static final AtomicBoolean isExternalDownloaderLaunched = new AtomicBoolean(false); + + private static String getPlaylistUrl(String playlistId) { + return PLAYLIST_URL + playlistId; + } + + private static String getVideoUrl(String videoId) { + return getVideoUrl(videoId, false); + } + + private static String getVideoUrl(boolean withTimestamp) { + return getVideoUrl(VideoInformation.getVideoId(), withTimestamp); + } + + public static String getVideoUrl(String videoId, boolean withTimestamp) { + StringBuilder builder = new StringBuilder(VIDEO_URL); + builder.append(videoId); + final long currentVideoTimeInSeconds = VideoInformation.getVideoTimeInSeconds(); + if (withTimestamp && currentVideoTimeInSeconds > 0) { + builder.append("?t="); + builder.append(currentVideoTimeInSeconds); + } + return builder.toString(); + } + + private static String getVideoScheme(String videoId, boolean isShorts) { + return String.format( + Locale.ENGLISH, + isShorts ? VIDEO_SCHEME_INTENT_FORMAT : VIDEO_SCHEME_LINK_FORMAT, + videoId, + VideoInformation.getVideoTimeInSeconds() + ); + } + + public static void copyUrl(boolean withTimestamp) { + copyUrl(getVideoUrl(withTimestamp), withTimestamp); + } + + public static void copyUrl(String videoUrl, boolean withTimestamp) { + setClipboard(videoUrl, withTimestamp + ? str("revanced_share_copy_url_timestamp_success") + : str("revanced_share_copy_url_success") + ); + } + + public static void copyTimeStamp() { + final String timeStamp = getTimeStamp(VideoInformation.getVideoTime()); + setClipboard(timeStamp, str("revanced_share_copy_timestamp_success", timeStamp)); + } + + public static void launchVideoExternalDownloader() { + launchVideoExternalDownloader(VideoInformation.getVideoId()); + } + + public static void launchVideoExternalDownloader(@NonNull String videoId) { + try { + final String downloaderPackageName = ExternalDownloaderVideoPreference.getExternalDownloaderPackageName(); + if (ExternalDownloaderVideoPreference.checkPackageIsDisabled()) { + return; + } + + isExternalDownloaderLaunched.compareAndSet(false, true); + launchExternalDownloader(getVideoUrl(videoId), downloaderPackageName); + } catch (Exception ex) { + Logger.printException(() -> "launchExternalDownloader failure", ex); + } finally { + runOnMainThreadDelayed(() -> isExternalDownloaderLaunched.compareAndSet(true, false), 500); + } + } + + public static void launchLongPressVideoExternalDownloader() { + launchLongPressVideoExternalDownloader(VideoInformation.getVideoId()); + } + + public static void launchLongPressVideoExternalDownloader(@NonNull String videoId) { + try { + final String downloaderPackageName = ExternalDownloaderVideoLongPressPreference.getExternalDownloaderPackageName(); + if (ExternalDownloaderVideoLongPressPreference.checkPackageIsDisabled()) { + return; + } + + isExternalDownloaderLaunched.compareAndSet(false, true); + launchExternalDownloader(getVideoUrl(videoId), downloaderPackageName); + } catch (Exception ex) { + Logger.printException(() -> "launchExternalDownloader failure", ex); + } finally { + runOnMainThreadDelayed(() -> isExternalDownloaderLaunched.compareAndSet(true, false), 500); + } + } + + public static void launchPlaylistExternalDownloader(@NonNull String playlistId) { + try { + final String downloaderPackageName = ExternalDownloaderPlaylistPreference.getExternalDownloaderPackageName(); + if (ExternalDownloaderPlaylistPreference.checkPackageIsDisabled()) { + return; + } + + isExternalDownloaderLaunched.compareAndSet(false, true); + launchExternalDownloader(getPlaylistUrl(playlistId), downloaderPackageName); + } catch (Exception ex) { + Logger.printException(() -> "launchPlaylistExternalDownloader failure", ex); + } finally { + runOnMainThreadDelayed(() -> isExternalDownloaderLaunched.compareAndSet(true, false), 500); + } + } + + public static void openVideo() { + openVideo(VideoInformation.getVideoId()); + } + + public static void openVideo(@NonNull String videoId) { + openVideo(videoId, false, null); + } + + public static void openVideo(@NonNull String videoId, boolean isShorts) { + openVideo(videoId, isShorts, null); + } + + public static void openVideo(@NonNull PlaylistIdPrefix playlistIdPrefix) { + openVideo(VideoInformation.getVideoId(), false, playlistIdPrefix); + } + + public static void openVideo(@NonNull String videoId, boolean isShorts, @Nullable PlaylistIdPrefix playlistIdPrefix) { + final StringBuilder sb = new StringBuilder(getVideoScheme(videoId, isShorts)); + // Create playlist with all channel videos. + if (playlistIdPrefix != null) { + sb.append("&list="); + sb.append(playlistIdPrefix.prefixId); + if (playlistIdPrefix.useChannelId) { + final String channelId = VideoInformation.getChannelId(); + // Channel id always starts with `UC` prefix + if (!channelId.startsWith("UC")) { + showToastShort(str("revanced_overlay_button_play_all_not_available_toast")); + return; + } + sb.append(channelId.substring(2)); + } else { + sb.append(videoId); + } + } + + launchView(sb.toString(), getContext().getPackageName()); + } + + /** + * Pause the media by changing audio focus. + */ + public static void pauseMedia() { + if (context != null && context.getApplicationContext().getSystemService(Context.AUDIO_SERVICE) instanceof AudioManager audioManager) { + audioManager.requestAudioFocus(null, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); + } + } + + public static void showPlaybackSpeedDialog(@NonNull Context context) { + final String[] playbackSpeedEntries = CustomPlaybackSpeedPatch.getTrimmedListEntries(); + final String[] playbackSpeedEntryValues = CustomPlaybackSpeedPatch.getTrimmedListEntryValues(); + + final float playbackSpeed = VideoInformation.getPlaybackSpeed(); + final int index = Arrays.binarySearch(playbackSpeedEntryValues, String.valueOf(playbackSpeed)); + + new AlertDialog.Builder(context) + .setSingleChoiceItems(playbackSpeedEntries, index, (mDialog, mIndex) -> { + final float selectedPlaybackSpeed = Float.parseFloat(playbackSpeedEntryValues[mIndex] + "f"); + VideoInformation.overridePlaybackSpeed(selectedPlaybackSpeed); + userSelectedPlaybackSpeed(selectedPlaybackSpeed); + mDialog.dismiss(); + }) + .show(); + } + + private static int mClickedDialogEntryIndex; + + public static void showShortsRepeatDialog(@NonNull Context context) { + final EnumSetting setting = Settings.CHANGE_SHORTS_REPEAT_STATE; + final String settingsKey = setting.key; + + final String entryKey = settingsKey + "_entries"; + final String entryValueKey = settingsKey + "_entry_values"; + final String[] mEntries = getStringArray(entryKey); + final String[] mEntryValues = getStringArray(entryValueKey); + + final int findIndex = Arrays.binarySearch(mEntryValues, String.valueOf(setting.get())); + mClickedDialogEntryIndex = findIndex >= 0 ? findIndex : setting.defaultValue.ordinal(); + + new AlertDialog.Builder(context) + .setTitle(str(settingsKey + "_title")) + .setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, (dialog, id) -> { + mClickedDialogEntryIndex = id; + for (ShortsLoopBehavior behavior : ShortsLoopBehavior.values()) { + if (behavior.ordinal() == id) setting.save(behavior); + } + dialog.dismiss(); + }) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } + + public static void showFlyoutMenu() { + if (Settings.APPEND_TIME_STAMP_INFORMATION_TYPE.get()) { + showVideoQualityFlyoutMenu(); + } else { + showPlaybackSpeedFlyoutMenu(); + } + } + + public static String getFormattedQualityString(@Nullable String prefix) { + final String qualityString = VideoInformation.getVideoQualityString(); + + return prefix == null ? qualityString : String.format("%s\u2009•\u2009%s", prefix, qualityString); + } + + public static String getFormattedSpeedString(@Nullable String prefix) { + final float playbackSpeed = VideoInformation.getPlaybackSpeed(); + + final String playbackSpeedString = isRightToLeftTextLayout() + ? "\u2066x\u2069" + playbackSpeed + : playbackSpeed + "x"; + + return prefix == null ? playbackSpeedString : String.format("%s\u2009•\u2009%s", prefix, playbackSpeedString); + } + + /** + * Injection point. + * Disable PiP mode when an external downloader Intent is started. + */ + public static boolean getExternalDownloaderLaunchedState(boolean original) { + return !isExternalDownloaderLaunched.get() && original; + } + + /** + * Rest of the implementation added by patch. + */ + public static void showPlaybackSpeedFlyoutMenu() { + Logger.printDebug(() -> "Playback speed flyout menu opened"); + } + + /** + * Rest of the implementation added by patch. + */ + public static void showVideoQualityFlyoutMenu() { + // These instructions are ignored by patch. + Log.d("Extended: VideoUtils", "Video quality flyout menu opened"); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/whitelist/VideoChannel.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/whitelist/VideoChannel.java new file mode 100644 index 000000000..50db76592 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/whitelist/VideoChannel.java @@ -0,0 +1,21 @@ +package app.revanced.extension.youtube.whitelist; + +import java.io.Serializable; + +public final class VideoChannel implements Serializable { + private final String channelName; + private final String channelId; + + public VideoChannel(String channelName, String channelId) { + this.channelName = channelName; + this.channelId = channelId; + } + + public String getChannelName() { + return channelName; + } + + public String getChannelId() { + return channelId; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/whitelist/Whitelist.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/whitelist/Whitelist.java new file mode 100644 index 000000000..013d76461 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/whitelist/Whitelist.java @@ -0,0 +1,311 @@ +package app.revanced.extension.youtube.whitelist; + +import static app.revanced.extension.shared.utils.StringRef.str; +import static app.revanced.extension.shared.utils.Utils.isSDKAbove; +import static app.revanced.extension.shared.utils.Utils.showToastShort; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.Resources; +import android.graphics.ColorFilter; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffColorFilter; +import android.graphics.drawable.Drawable; +import android.widget.Button; + +import androidx.annotation.NonNull; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.EnumMap; +import java.util.Iterator; +import java.util.Map; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.InflaterInputStream; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.utils.PatchStatus; +import app.revanced.extension.youtube.shared.VideoInformation; +import app.revanced.extension.youtube.utils.ThemeUtils; + +@SuppressWarnings("deprecation") +public class Whitelist { + private static final String ZERO_WIDTH_SPACE_CHARACTER = "\u200B"; + private static final Map> whitelistMap = parseWhitelist(); + + private static final WhitelistType whitelistTypePlaybackSpeed = WhitelistType.PLAYBACK_SPEED; + private static final WhitelistType whitelistTypeSponsorBlock = WhitelistType.SPONSOR_BLOCK; + private static final String whitelistIncluded = str("revanced_whitelist_included"); + private static final String whitelistExcluded = str("revanced_whitelist_excluded"); + private static Drawable playbackSpeedDrawable; + private static Drawable sponsorBlockDrawable; + + static { + final Resources resource = Utils.getResources(); + + final int playbackSpeedDrawableId = ResourceUtils.getDrawableIdentifier("yt_outline_play_arrow_half_circle_black_24"); + if (playbackSpeedDrawableId != 0) { + playbackSpeedDrawable = resource.getDrawable(playbackSpeedDrawableId); + } + + final int sponsorBlockDrawableId = ResourceUtils.getDrawableIdentifier("revanced_sb_logo"); + if (sponsorBlockDrawableId != 0) { + sponsorBlockDrawable = resource.getDrawable(sponsorBlockDrawableId); + } + } + + public static boolean isChannelWhitelistedSponsorBlock(String channelId) { + return isWhitelisted(whitelistTypeSponsorBlock, channelId); + } + + public static boolean isChannelWhitelistedPlaybackSpeed(String channelId) { + return isWhitelisted(whitelistTypePlaybackSpeed, channelId); + } + + public static void showWhitelistDialog(Context context) { + final String channelId = VideoInformation.getChannelId(); + final String channelName = VideoInformation.getChannelName(); + + if (channelId.isEmpty() || channelName.isEmpty()) { + Utils.showToastShort(str("revanced_whitelist_failure_generic")); + return; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(channelName); + + StringBuilder sb = new StringBuilder("\n"); + + if (PatchStatus.RememberPlaybackSpeed()) { + appendStringBuilder(sb, whitelistTypePlaybackSpeed, channelId, false); + builder.setNeutralButton(ZERO_WIDTH_SPACE_CHARACTER, + (dialog, id) -> whitelistListener( + whitelistTypePlaybackSpeed, + channelId, + channelName + ) + ); + } + + if (PatchStatus.SponsorBlock()) { + appendStringBuilder(sb, whitelistTypeSponsorBlock, channelId, true); + builder.setPositiveButton(ZERO_WIDTH_SPACE_CHARACTER, + (dialog, id) -> whitelistListener( + whitelistTypeSponsorBlock, + channelId, + channelName + ) + ); + } + + builder.setMessage(sb.toString()); + + AlertDialog dialog = builder.show(); + + final ColorFilter cf = new PorterDuffColorFilter(ThemeUtils.getForegroundColor(), PorterDuff.Mode.SRC_ATOP); + Button sponsorBlockButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + Button playbackSpeedButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); + if (sponsorBlockButton != null && sponsorBlockDrawable != null) { + sponsorBlockDrawable.setColorFilter(cf); + sponsorBlockButton.setCompoundDrawablesWithIntrinsicBounds(null, null, sponsorBlockDrawable, null); + sponsorBlockButton.setContentDescription(str("revanced_whitelist_sponsor_block")); + } + if (playbackSpeedButton != null && playbackSpeedDrawable != null) { + playbackSpeedDrawable.setColorFilter(cf); + playbackSpeedButton.setCompoundDrawablesWithIntrinsicBounds(playbackSpeedDrawable, null, null, null); + playbackSpeedButton.setContentDescription(str("revanced_whitelist_playback_speed")); + } + } + + private static void appendStringBuilder(StringBuilder sb, WhitelistType whitelistType, + String channelId, boolean eol) { + final String status = isWhitelisted(whitelistType, channelId) + ? whitelistIncluded + : whitelistExcluded; + sb.append(whitelistType.getFriendlyName()); + sb.append(":\n"); + sb.append(status); + sb.append("\n"); + if (!eol) sb.append("\n"); + } + + private static void whitelistListener(WhitelistType whitelistType, String channelId, String channelName) { + try { + if (isWhitelisted(whitelistType, channelId)) { + removeFromWhitelist(whitelistType, channelId); + } else { + addToWhitelist(whitelistType, channelId, channelName); + } + } catch (Exception ex) { + Logger.printException(() -> "whitelistListener failure", ex); + } + } + + /** + * @noinspection unchecked + */ + private static Map> parseWhitelist() { + WhitelistType[] whitelistTypes = WhitelistType.values(); + Map> whitelistMap = new EnumMap<>(WhitelistType.class); + + for (WhitelistType whitelistType : whitelistTypes) { + SharedPreferences preferences = getPreferences(whitelistType.getPreferencesName()); + String serializedChannels = preferences.getString("channels", null); + if (serializedChannels == null) { + whitelistMap.put(whitelistType, new ArrayList<>()); + continue; + } + try { + Object channelsObject = deserialize(serializedChannels); + ArrayList deserializedChannels = (ArrayList) channelsObject; + whitelistMap.put(whitelistType, deserializedChannels); + } catch (Exception ex) { + Logger.printException(() -> "parseWhitelist failure", ex); + } + } + return whitelistMap; + } + + private static boolean isWhitelisted(WhitelistType whitelistType, String channelId) { + for (VideoChannel channel : getWhitelistedChannels(whitelistType)) { + if (channel.getChannelId().equals(channelId)) { + return true; + } + } + return false; + } + + private static void addToWhitelist(WhitelistType whitelistType, String channelId, String channelName) { + final VideoChannel channel = new VideoChannel(channelName, channelId); + ArrayList whitelisted = getWhitelistedChannels(whitelistType); + for (VideoChannel whitelistedChannel : whitelisted) { + if (whitelistedChannel.getChannelId().equals(channel.getChannelId())) + return; + } + whitelisted.add(channel); + String friendlyName = whitelistType.getFriendlyName(); + if (updateWhitelist(whitelistType, whitelisted)) { + showToastShort(str("revanced_whitelist_added", channelName, friendlyName)); + } else { + showToastShort(str("revanced_whitelist_add_failed", channelName, friendlyName)); + } + } + + public static void removeFromWhitelist(WhitelistType whitelistType, String channelId) { + ArrayList whitelisted = getWhitelistedChannels(whitelistType); + Iterator iterator = whitelisted.iterator(); + String channelName = ""; + while (iterator.hasNext()) { + VideoChannel channel = iterator.next(); + if (channel.getChannelId().equals(channelId)) { + channelName = channel.getChannelName(); + iterator.remove(); + break; + } + } + String friendlyName = whitelistType.getFriendlyName(); + if (updateWhitelist(whitelistType, whitelisted)) { + showToastShort(str("revanced_whitelist_removed", channelName, friendlyName)); + } else { + showToastShort(str("revanced_whitelist_remove_failed", channelName, friendlyName)); + } + } + + private static boolean updateWhitelist(WhitelistType whitelistType, ArrayList channels) { + SharedPreferences.Editor editor = getPreferences(whitelistType.getPreferencesName()).edit(); + + final String channelName = serialize(channels); + if (channelName != null && !channelName.isEmpty()) { + editor.putString("channels", channelName); + editor.apply(); + return true; + } + return false; + } + + public static ArrayList getWhitelistedChannels(WhitelistType whitelistType) { + return whitelistMap.get(whitelistType); + } + + private static SharedPreferences getPreferences(@NonNull String prefName) { + final Context context = Utils.getContext(); + return context.getSharedPreferences(prefName, Context.MODE_PRIVATE); + } + + private static String serialize(Serializable obj) { + try { + if (obj != null) { + ByteArrayOutputStream serialObj = new ByteArrayOutputStream(); + Deflater def = new Deflater(Deflater.BEST_COMPRESSION); + ObjectOutputStream objStream = + new ObjectOutputStream(new DeflaterOutputStream(serialObj, def)); + objStream.writeObject(obj); + objStream.close(); + return encodeBytes(serialObj.toByteArray()); + } + } catch (IOException ex) { + Logger.printException(() -> "Serialization error: " + ex.getMessage(), ex); + } + return null; + } + + private static Object deserialize(@NonNull String str) { + try { + final ByteArrayInputStream serialObj = new ByteArrayInputStream(decodeBytes(str)); + final ObjectInputStream objStream = new ObjectInputStream(new InflaterInputStream(serialObj)); + return objStream.readObject(); + } catch (ClassNotFoundException | IOException ex) { + Logger.printException(() -> "Deserialization error: " + ex.getMessage(), ex); + } + return null; + } + + private static String encodeBytes(byte[] bytes) { + if (isSDKAbove(26)) { + return Base64.getEncoder().encodeToString(bytes); + } else { + return new String(bytes, StandardCharsets.UTF_8); + } + } + + private static byte[] decodeBytes(String str) { + if (isSDKAbove(26)) { + return Base64.getDecoder().decode(str.getBytes(StandardCharsets.UTF_8)); + } else { + return str.getBytes(StandardCharsets.UTF_8); + } + } + + public enum WhitelistType { + PLAYBACK_SPEED(), + SPONSOR_BLOCK(); + + private final String friendlyName; + private final String preferencesName; + + WhitelistType() { + String name = name().toLowerCase(); + this.friendlyName = str("revanced_whitelist_" + name); + this.preferencesName = "whitelist_" + name; + } + + public String getFriendlyName() { + return friendlyName; + } + + public String getPreferencesName() { + return preferencesName; + } + } +} diff --git a/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java b/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java new file mode 100644 index 000000000..1d1468478 --- /dev/null +++ b/extensions/shared/src/main/java/com/google/android/apps/youtube/app/settings/videoquality/VideoQualitySettingsActivity.java @@ -0,0 +1,184 @@ +package com.google.android.apps.youtube.app.settings.videoquality; + +import android.app.Activity; +import android.content.Context; +import android.os.Bundle; +import android.util.TypedValue; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.SearchView; +import android.widget.SearchView.OnQueryTextListener; +import android.widget.TextView; +import android.widget.Toolbar; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.ResourceUtils; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment; +import app.revanced.extension.youtube.utils.ThemeUtils; + +@SuppressWarnings("deprecation") +public class VideoQualitySettingsActivity extends Activity { + + private static final String rvxSettingsLabel = ResourceUtils.getString("revanced_extended_settings_title"); + private static final String searchLabel = ResourceUtils.getString("revanced_extended_settings_search_title"); + private static WeakReference searchViewRef = new WeakReference<>(null); + private ReVancedPreferenceFragment fragment; + + private final OnQueryTextListener onQueryTextListener = new OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String query) { + filterPreferences(query); + return true; + } + + @Override + public boolean onQueryTextChange(String newText) { + filterPreferences(newText); + return true; + } + }; + + @Override + protected void attachBaseContext(Context base) { + super.attachBaseContext(Utils.getLocalizedContextAndSetResources(base)); + } + + @Override + protected void onCreate(Bundle bundle) { + super.onCreate(bundle); + try { + // Set fragment theme + setTheme(ThemeUtils.getThemeId()); + + // Set content + setContentView(ResourceUtils.getLayoutIdentifier("revanced_settings_with_toolbar")); + + String dataString = getIntent().getDataString(); + if (dataString == null) { + Logger.printException(() -> "DataString is null"); + return; + } else if (dataString.equals("revanced_extended_settings_intent")) { + fragment = new ReVancedPreferenceFragment(); + } else { + Logger.printException(() -> "Unknown setting: " + dataString); + return; + } + + // Set toolbar + setToolbar(); + + getFragmentManager() + .beginTransaction() + .replace(ResourceUtils.getIdIdentifier("revanced_settings_fragments"), fragment) + .commit(); + + setSearchView(); + } catch (Exception ex) { + Logger.printException(() -> "onCreate failure", ex); + } + } + + private void filterPreferences(String query) { + if (fragment == null) return; + fragment.filterPreferences(query); + } + + private void setToolbar() { + if (!(findViewById(ResourceUtils.getIdIdentifier("revanced_toolbar_parent")) instanceof ViewGroup toolBarParent)) + return; + + // Remove dummy toolbar. + for (int i = 0; i < toolBarParent.getChildCount(); i++) { + View view = toolBarParent.getChildAt(i); + if (view != null) { + toolBarParent.removeView(view); + } + } + + Toolbar toolbar = new Toolbar(toolBarParent.getContext()); + toolbar.setBackgroundColor(ThemeUtils.getToolbarBackgroundColor()); + toolbar.setNavigationIcon(ThemeUtils.getBackButtonDrawable()); + toolbar.setNavigationOnClickListener(view -> VideoQualitySettingsActivity.this.onBackPressed()); + toolbar.setTitle(rvxSettingsLabel); + int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 16, getResources().getDisplayMetrics()); + toolbar.setTitleMarginStart(margin); + toolbar.setTitleMarginEnd(margin); + TextView toolbarTextView = Utils.getChildView(toolbar, view -> view instanceof TextView); + if (toolbarTextView != null) { + toolbarTextView.setTextColor(ThemeUtils.getForegroundColor()); + } + toolBarParent.addView(toolbar, 0); + } + + private void setSearchView() { + SearchView searchView = findViewById(ResourceUtils.getIdIdentifier("search_view")); + + // region compose search hint + + // if the translation is missing the %s, then it + // will use the default search hint for that language + String finalSearchHint = String.format(searchLabel, rvxSettingsLabel); + + searchView.setQueryHint(finalSearchHint); + + // endregion + + // region set the font size + + try { + // 'android.widget.SearchView' has been deprecated quite a long time ago + // So access the SearchView's EditText via reflection + Field field = searchView.getClass().getDeclaredField("mSearchSrcTextView"); + field.setAccessible(true); + + // Set the font size + if (field.get(searchView) instanceof EditText searchEditText) { + searchEditText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + } + } catch (NoSuchFieldException | IllegalAccessException ex) { + Logger.printException(() -> "Reflection error accessing mSearchSrcTextView", ex); + } + + // endregion + + // region SearchView dimensions + + // Get the current layout parameters of the SearchView + ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) searchView.getLayoutParams(); + + // Set the margins (in pixels) + int margin = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, getResources().getDisplayMetrics()); // for example, 10dp + layoutParams.setMargins(margin, layoutParams.topMargin, margin, layoutParams.bottomMargin); + + // Apply the layout parameters to the SearchView + searchView.setLayoutParams(layoutParams); + + // endregion + + // region SearchView color + + searchView.setBackground(ThemeUtils.getSearchViewShape()); + + // endregion + + // Set the listener for query text changes + searchView.setOnQueryTextListener(onQueryTextListener); + + // Keep a weak reference to the SearchView + searchViewRef = new WeakReference<>(searchView); + } + + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + SearchView searchView = searchViewRef.get(); + if (!hasFocus && searchView != null && searchView.getQuery().length() == 0) { + searchView.clearFocus(); + } + } +} diff --git a/extensions/shared/stub/build.gradle.kts b/extensions/shared/stub/build.gradle.kts new file mode 100644 index 000000000..ea7fb8015 --- /dev/null +++ b/extensions/shared/stub/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id(libs.plugins.android.library.get().pluginId) +} + +android { + namespace = "app.revanced.extension" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } +} diff --git a/extensions/shared/stub/src/main/AndroidManifest.xml b/extensions/shared/stub/src/main/AndroidManifest.xml new file mode 100644 index 000000000..568741e54 --- /dev/null +++ b/extensions/shared/stub/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/extensions/shared/stub/src/main/java/android/support/v7/widget/RecyclerView.java b/extensions/shared/stub/src/main/java/android/support/v7/widget/RecyclerView.java new file mode 100644 index 000000000..225d1c565 --- /dev/null +++ b/extensions/shared/stub/src/main/java/android/support/v7/widget/RecyclerView.java @@ -0,0 +1,19 @@ +package android.support.v7.widget; + +import android.content.Context; +import android.view.ViewGroup; + +/** + * "CompileOnly" class + *

+ * This class will not be included and "replaced" by the real package's class. + */ +public class RecyclerView extends ViewGroup { + public RecyclerView(Context context) { + super(context); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + } +} diff --git a/extensions/shared/stub/src/main/java/android/support/v7/widget/Toolbar.java b/extensions/shared/stub/src/main/java/android/support/v7/widget/Toolbar.java new file mode 100644 index 000000000..96e027cb4 --- /dev/null +++ b/extensions/shared/stub/src/main/java/android/support/v7/widget/Toolbar.java @@ -0,0 +1,19 @@ +package android.support.v7.widget; + +import android.content.Context; +import android.view.ViewGroup; + +/** + * "CompileOnly" class + *

+ * This class will not be included and "replaced" by the real package's class. + */ +public class Toolbar extends ViewGroup { + public Toolbar(Context context) { + super(context); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + } +} diff --git a/extensions/shared/stub/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java b/extensions/shared/stub/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java new file mode 100644 index 000000000..fa927116f --- /dev/null +++ b/extensions/shared/stub/src/main/java/androidx/coordinatorlayout/widget/CoordinatorLayout.java @@ -0,0 +1,24 @@ +package androidx.coordinatorlayout.widget; + +import android.content.Context; +import android.view.ViewGroup; + +/** + * "CompileOnly" class + *

+ * This class will not be included and "replaced" by the real package's class. + */ +public class CoordinatorLayout extends ViewGroup { + public CoordinatorLayout(Context context) { + super(context); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + } + + @Override + public void setVisibility(int visibility) { + super.setVisibility(visibility); + } +} diff --git a/extensions/shared/stub/src/main/java/com/airbnb/lottie/LottieAnimationView.java b/extensions/shared/stub/src/main/java/com/airbnb/lottie/LottieAnimationView.java new file mode 100644 index 000000000..239b4321e --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/airbnb/lottie/LottieAnimationView.java @@ -0,0 +1,15 @@ +package com.airbnb.lottie; + +import android.content.Context; +import android.widget.ImageView; + +public class LottieAnimationView extends ImageView { + + public LottieAnimationView(Context context) { + super(context); + } + + @SuppressWarnings("unused") + public void setAnimation(final int rawRes) { + } +} \ No newline at end of file diff --git a/extensions/shared/stub/src/main/java/com/google/android/apps/youtube/app/application/Shell_SettingsActivity.java b/extensions/shared/stub/src/main/java/com/google/android/apps/youtube/app/application/Shell_SettingsActivity.java new file mode 100644 index 000000000..32ce1d8c9 --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/google/android/apps/youtube/app/application/Shell_SettingsActivity.java @@ -0,0 +1,4 @@ +package com.google.android.apps.youtube.app.application; + +public class Shell_SettingsActivity { +} \ No newline at end of file diff --git a/extensions/shared/stub/src/main/java/com/google/android/apps/youtube/app/settings/SettingsActivity.java b/extensions/shared/stub/src/main/java/com/google/android/apps/youtube/app/settings/SettingsActivity.java new file mode 100644 index 000000000..0bbe50212 --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/google/android/apps/youtube/app/settings/SettingsActivity.java @@ -0,0 +1,4 @@ +package com.google.android.apps.youtube.app.settings; + +public class SettingsActivity { +} \ No newline at end of file diff --git a/extensions/shared/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/FormatStreamModel.java b/extensions/shared/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/FormatStreamModel.java new file mode 100644 index 000000000..5c0df267f --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/google/android/libraries/youtube/innertube/model/media/FormatStreamModel.java @@ -0,0 +1,4 @@ +package com.google.android.libraries.youtube.innertube.model.media; + +public class FormatStreamModel { +} \ No newline at end of file diff --git a/extensions/shared/stub/src/main/java/com/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar.java b/extensions/shared/stub/src/main/java/com/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar.java new file mode 100644 index 000000000..f275effdb --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar.java @@ -0,0 +1,10 @@ +package com.google.android.libraries.youtube.rendering.ui.pivotbar; + +import android.content.Context; +import android.widget.HorizontalScrollView; + +public class PivotBar extends HorizontalScrollView { + public PivotBar(Context context) { + super(context); + } +} diff --git a/extensions/shared/stub/src/main/java/com/google/android/material/textfield/TextInputLayout.java b/extensions/shared/stub/src/main/java/com/google/android/material/textfield/TextInputLayout.java new file mode 100644 index 000000000..d1d3d63a0 --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/google/android/material/textfield/TextInputLayout.java @@ -0,0 +1,11 @@ +package com.google.android.material.textfield; + +import android.content.Context; +import android.widget.LinearLayout; + +public class TextInputLayout extends LinearLayout { + + public TextInputLayout(Context context) { + super(context); + } +} \ No newline at end of file diff --git a/extensions/shared/stub/src/main/java/com/google/protos/youtube/api/innertube/StreamingDataOuterClass$StreamingData.java b/extensions/shared/stub/src/main/java/com/google/protos/youtube/api/innertube/StreamingDataOuterClass$StreamingData.java new file mode 100644 index 000000000..cb08ae162 --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/google/protos/youtube/api/innertube/StreamingDataOuterClass$StreamingData.java @@ -0,0 +1,4 @@ +package com.google.protos.youtube.api.innertube; + +public class StreamingDataOuterClass$StreamingData { +} \ No newline at end of file diff --git a/extensions/shared/stub/src/main/java/com/reddit/domain/model/ILink.java b/extensions/shared/stub/src/main/java/com/reddit/domain/model/ILink.java new file mode 100644 index 000000000..f9cbb955c --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/reddit/domain/model/ILink.java @@ -0,0 +1,7 @@ +package com.reddit.domain.model; + +public class ILink { + public boolean getPromoted() { + throw new UnsupportedOperationException("Stub"); + } +} diff --git a/extensions/shared/stub/src/main/java/org/chromium/net/UrlRequest.java b/extensions/shared/stub/src/main/java/org/chromium/net/UrlRequest.java new file mode 100644 index 000000000..565fc2227 --- /dev/null +++ b/extensions/shared/stub/src/main/java/org/chromium/net/UrlRequest.java @@ -0,0 +1,4 @@ +package org.chromium.net; + +public abstract class UrlRequest { +} diff --git a/extensions/shared/stub/src/main/java/org/chromium/net/UrlResponseInfo.java b/extensions/shared/stub/src/main/java/org/chromium/net/UrlResponseInfo.java new file mode 100644 index 000000000..8e341247d --- /dev/null +++ b/extensions/shared/stub/src/main/java/org/chromium/net/UrlResponseInfo.java @@ -0,0 +1,12 @@ +package org.chromium.net; + +//dummy class +public abstract class UrlResponseInfo { + + public abstract String getUrl(); + + public abstract int getHttpStatusCode(); + + // Add additional existing methods, if needed. + +} diff --git a/extensions/shared/stub/src/main/java/org/chromium/net/impl/CronetUrlRequest.java b/extensions/shared/stub/src/main/java/org/chromium/net/impl/CronetUrlRequest.java new file mode 100644 index 000000000..fa0dcacd9 --- /dev/null +++ b/extensions/shared/stub/src/main/java/org/chromium/net/impl/CronetUrlRequest.java @@ -0,0 +1,11 @@ +package org.chromium.net.impl; + +import org.chromium.net.UrlRequest; + +public abstract class CronetUrlRequest extends UrlRequest { + + /** + * Method is added by patch. + */ + public abstract String getHookedUrl(); +} diff --git a/gradle.properties b/gradle.properties index ed93fd281..9437e58ab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,7 @@ -org.gradle.parallel = true org.gradle.caching = true +org.gradle.jvmargs = -Xms1024M -Xmx4096M +org.gradle.parallel = true +android.useAndroidX = true kotlin.code.style = official -version = 2.231.0 +kotlin.jvm.target.validation.mode = IGNORE +version = 3.0.0-dev.8 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ffd54bff9..169ef7199 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,17 +1,19 @@ [versions] -revanced-patcher = "19.3.1" +revanced-patcher = "21.0.0" +# Tracking https://github.com/google/smali/issues/64. +#noinspection GradleDependency smali = "3.0.5" -guava = "33.0.0-jre" gson = "2.11.0" -binary-compatibility-validator = "0.14.0" -kotlin = "2.0.20" +agp = "8.2.2" +annotation = "1.9.1" +lang3 = "3.17.0" +preference = "1.2.1" [libraries] -revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } -smali = { module = "com.android.tools.smali:smali", version.ref = "smali" } -guava = { module = "com.google.guava:guava", version.ref = "guava" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } +lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "lang3" } +preference = { module = "androidx.preference:preference", version.ref = "preference" } [plugins] -binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } -kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +android-library = { id = "com.android.library", version.ref = "agp" } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 3de45b5b9..c67622290 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,18 +9,17 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "gradle-semantic-release-plugin": "^1.10.1", - "semantic-release": "^24.2.0" + "semantic-release": "^24.1.2" } }, "node_modules/@babel/code-frame": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.0.tgz", - "integrity": "sha512-INCKxTtbXtcNbUZ3YXutwMpEleqttcswhAdee7dhuoVrD2cnuc3PqtERBtxkX5nziX9vnBL8WXmSGwv8CuPV6g==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", - "js-tokens": "^4.0.0", + "@babel/highlight": "^7.24.7", "picocolors": "^1.0.0" }, "engines": { @@ -28,15 +27,99 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", "dev": true, "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -186,9 +269,9 @@ } }, "node_modules/@octokit/plugin-throttling": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-9.3.2.tgz", - "integrity": "sha512-FqpvcTpIWFpMMwIeSoypoJXysSAQ3R+ALJhXXSG1HTP3YZOIeLmcNcimKaXxTcws+Sh6yoRl13SJ5r8sXc1Fhw==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-9.3.1.tgz", + "integrity": "sha512-Qd91H4liUBhwLB2h6jZ99bsxoQdhgPk6TdwnClPyTBSDAdviGPceViEgUwj+pcQDmB/rfAXAXK7MTochpHM3yQ==", "dev": true, "license": "MIT", "dependencies": { @@ -232,9 +315,9 @@ } }, "node_modules/@octokit/types": { - "version": "13.6.1", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.1.tgz", - "integrity": "sha512-PHZE9Z+kWXb23Ndik8MKPirBPziOc0D2/3KH1P+6jK5nGWe96kadZuE4jev2/Jq7FvIfTlT2Ltg8Fv2x1v0a5g==", + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.6.0.tgz", + "integrity": "sha512-CrooV/vKCXqwLa+osmHLIMUb87brpgUqlqkPGc6iE2wCkUvTrHiXFMhAKoDDaAAYJrtKtrFTgSQTg5nObBEaew==", "dev": true, "license": "MIT", "dependencies": { @@ -865,6 +948,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@saithodev/semantic-release-backmerge/node_modules/find-versions": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", @@ -1362,6 +1458,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@semantic-release/github/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@semantic-release/github/node_modules/indent-string": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", @@ -1459,10 +1568,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@semantic-release/npm/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@semantic-release/npm/node_modules/execa": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.0.tgz", - "integrity": "sha512-t7vvYt+oKnMbF3O+S5+HkylsPrsUatwJSe4Cv+4017R0MCySjECxnVJ2eyDXVD/Xpj5H29YzyYn6eEpugG7GJA==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.4.0.tgz", + "integrity": "sha512-yKHlle2YGxZE842MERVIplWwNH5VYmqqcPFgtnlU//K8gxuFFXu0pwd/CrfXTumFpeEiufsP7+opT/bPJa1yVw==", "dev": true, "license": "MIT", "dependencies": { @@ -2464,16 +2586,13 @@ } }, "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=0.8.0" } }, "node_modules/esprima": { @@ -3545,9 +3664,9 @@ } }, "node_modules/npm": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.0.tgz", - "integrity": "sha512-ZanDioFylI9helNhl2LNd+ErmVD+H5I53ry41ixlLyCBgkuYb+58CvbAp99hW+zr5L9W4X7CchSoeqKdngOLSw==", + "version": "10.8.3", + "resolved": "https://registry.npmjs.org/npm/-/npm-10.8.3.tgz", + "integrity": "sha512-0IQlyAYvVtQ7uOhDFYZCGK8kkut2nh8cpAdA9E6FvRSJaTgtZRZgNjlC5ZCct//L73ygrpY93CxXpRJDtNqPVg==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -3629,18 +3748,18 @@ ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/arborist": "^8.0.0", - "@npmcli/config": "^9.0.0", - "@npmcli/fs": "^4.0.0", - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/package-json": "^6.0.1", - "@npmcli/promise-spawn": "^8.0.1", - "@npmcli/redact": "^3.0.0", - "@npmcli/run-script": "^9.0.1", + "@npmcli/arborist": "^7.5.4", + "@npmcli/config": "^8.3.4", + "@npmcli/fs": "^3.1.1", + "@npmcli/map-workspaces": "^3.0.6", + "@npmcli/package-json": "^5.2.0", + "@npmcli/promise-spawn": "^7.0.2", + "@npmcli/redact": "^2.0.1", + "@npmcli/run-script": "^8.1.0", "@sigstore/tuf": "^2.3.4", - "abbrev": "^3.0.0", + "abbrev": "^2.0.0", "archy": "~1.0.0", - "cacache": "^19.0.1", + "cacache": "^18.0.4", "chalk": "^5.3.0", "ci-info": "^4.0.0", "cli-columns": "^4.0.0", @@ -3648,54 +3767,54 @@ "fs-minipass": "^3.0.3", "glob": "^10.4.5", "graceful-fs": "^4.2.11", - "hosted-git-info": "^8.0.0", - "ini": "^5.0.0", - "init-package-json": "^7.0.1", + "hosted-git-info": "^7.0.2", + "ini": "^4.1.3", + "init-package-json": "^6.0.3", "is-cidr": "^5.1.0", - "json-parse-even-better-errors": "^4.0.0", - "libnpmaccess": "^9.0.0", - "libnpmdiff": "^7.0.0", - "libnpmexec": "^9.0.0", - "libnpmfund": "^6.0.0", - "libnpmhook": "^11.0.0", - "libnpmorg": "^7.0.0", - "libnpmpack": "^8.0.0", - "libnpmpublish": "^10.0.0", - "libnpmsearch": "^8.0.0", - "libnpmteam": "^7.0.0", - "libnpmversion": "^7.0.0", - "make-fetch-happen": "^14.0.1", + "json-parse-even-better-errors": "^3.0.2", + "libnpmaccess": "^8.0.6", + "libnpmdiff": "^6.1.4", + "libnpmexec": "^8.1.4", + "libnpmfund": "^5.0.12", + "libnpmhook": "^10.0.5", + "libnpmorg": "^6.0.6", + "libnpmpack": "^7.0.4", + "libnpmpublish": "^9.0.9", + "libnpmsearch": "^7.0.6", + "libnpmteam": "^6.0.5", + "libnpmversion": "^6.0.3", + "make-fetch-happen": "^13.0.1", "minimatch": "^9.0.5", "minipass": "^7.1.1", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", "node-gyp": "^10.2.0", - "nopt": "^8.0.0", - "normalize-package-data": "^7.0.0", - "npm-audit-report": "^6.0.0", - "npm-install-checks": "^7.1.0", - "npm-package-arg": "^12.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-profile": "^11.0.1", - "npm-registry-fetch": "^18.0.1", - "npm-user-validate": "^3.0.0", + "nopt": "^7.2.1", + "normalize-package-data": "^6.0.2", + "npm-audit-report": "^5.0.0", + "npm-install-checks": "^6.3.0", + "npm-package-arg": "^11.0.3", + "npm-pick-manifest": "^9.1.0", + "npm-profile": "^10.0.0", + "npm-registry-fetch": "^17.1.0", + "npm-user-validate": "^2.0.1", "p-map": "^4.0.0", - "pacote": "^19.0.0", - "parse-conflict-json": "^4.0.0", - "proc-log": "^5.0.0", + "pacote": "^18.0.6", + "parse-conflict-json": "^3.0.1", + "proc-log": "^4.2.0", "qrcode-terminal": "^0.12.0", - "read": "^4.0.0", + "read": "^3.0.1", "semver": "^7.6.3", "spdx-expression-parse": "^4.0.0", - "ssri": "^12.0.0", + "ssri": "^10.0.6", "supports-color": "^9.4.0", "tar": "^6.2.1", "text-table": "~0.2.0", "tiny-relative-date": "^1.3.0", "treeverse": "^3.0.0", - "validate-npm-package-name": "^6.0.0", - "which": "^5.0.0", - "write-file-atomic": "^6.0.0" + "validate-npm-package-name": "^5.0.1", + "which": "^4.0.0", + "write-file-atomic": "^5.0.1" }, "bin": { "npm": "bin/npm-cli.js", @@ -3785,18 +3904,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/npm/node_modules/@isaacs/fs-minipass": { - "version": "4.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.4" - }, - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/npm/node_modules/@isaacs/string-locale-compare": { "version": "1.1.0", "dev": true, @@ -3804,7 +3911,7 @@ "license": "ISC" }, "node_modules/npm/node_modules/@npmcli/agent": { - "version": "3.0.0", + "version": "2.2.2", "dev": true, "inBundle": true, "license": "ISC", @@ -3816,48 +3923,48 @@ "socks-proxy-agent": "^8.0.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/arborist": { - "version": "8.0.0", + "version": "7.5.4", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", - "@npmcli/fs": "^4.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/metavuln-calculator": "^8.0.0", - "@npmcli/name-from-folder": "^3.0.0", - "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.1", - "@npmcli/query": "^4.0.0", - "@npmcli/redact": "^3.0.0", - "@npmcli/run-script": "^9.0.1", - "bin-links": "^5.0.0", - "cacache": "^19.0.1", + "@npmcli/fs": "^3.1.1", + "@npmcli/installed-package-contents": "^2.1.0", + "@npmcli/map-workspaces": "^3.0.2", + "@npmcli/metavuln-calculator": "^7.1.1", + "@npmcli/name-from-folder": "^2.0.0", + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.1.0", + "@npmcli/query": "^3.1.0", + "@npmcli/redact": "^2.0.0", + "@npmcli/run-script": "^8.1.0", + "bin-links": "^4.0.4", + "cacache": "^18.0.3", "common-ancestor-path": "^1.0.1", - "hosted-git-info": "^8.0.0", - "json-parse-even-better-errors": "^4.0.0", + "hosted-git-info": "^7.0.2", + "json-parse-even-better-errors": "^3.0.2", "json-stringify-nice": "^1.1.4", "lru-cache": "^10.2.2", "minimatch": "^9.0.4", - "nopt": "^8.0.0", - "npm-install-checks": "^7.1.0", - "npm-package-arg": "^12.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.1", - "pacote": "^19.0.0", - "parse-conflict-json": "^4.0.0", - "proc-log": "^5.0.0", - "proggy": "^3.0.0", + "nopt": "^7.2.1", + "npm-install-checks": "^6.2.0", + "npm-package-arg": "^11.0.2", + "npm-pick-manifest": "^9.0.1", + "npm-registry-fetch": "^17.0.1", + "pacote": "^18.0.6", + "parse-conflict-json": "^3.0.0", + "proc-log": "^4.2.0", + "proggy": "^2.0.0", "promise-all-reject-late": "^1.0.0", "promise-call-limit": "^3.0.1", - "read-package-json-fast": "^4.0.0", + "read-package-json-fast": "^3.0.2", "semver": "^7.3.7", - "ssri": "^12.0.0", + "ssri": "^10.0.6", "treeverse": "^3.0.0", "walk-up-path": "^3.0.1" }, @@ -3865,30 +3972,30 @@ "arborist": "bin/index.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/config": { - "version": "9.0.0", + "version": "8.3.4", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/package-json": "^6.0.1", + "@npmcli/map-workspaces": "^3.0.2", + "@npmcli/package-json": "^5.1.1", "ci-info": "^4.0.0", - "ini": "^5.0.0", - "nopt": "^8.0.0", - "proc-log": "^5.0.0", + "ini": "^4.1.2", + "nopt": "^7.2.1", + "proc-log": "^4.2.0", "semver": "^7.3.5", "walk-up-path": "^3.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/fs": { - "version": "4.0.0", + "version": "3.1.1", "dev": true, "inBundle": true, "license": "ISC", @@ -3896,160 +4003,160 @@ "semver": "^7.3.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/git": { - "version": "6.0.1", + "version": "5.0.8", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/promise-spawn": "^8.0.0", - "ini": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", + "ini": "^4.1.3", "lru-cache": "^10.0.1", - "npm-pick-manifest": "^10.0.0", - "proc-log": "^5.0.0", + "npm-pick-manifest": "^9.0.0", + "proc-log": "^4.0.0", "promise-inflight": "^1.0.1", "promise-retry": "^2.0.1", "semver": "^7.3.5", - "which": "^5.0.0" + "which": "^4.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/installed-package-contents": { - "version": "3.0.0", + "version": "2.1.0", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "npm-bundled": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" }, "bin": { "installed-package-contents": "bin/index.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "4.0.1", + "version": "3.0.6", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/name-from-folder": "^3.0.0", - "@npmcli/package-json": "^6.0.0", + "@npmcli/name-from-folder": "^2.0.0", "glob": "^10.2.2", - "minimatch": "^9.0.0" + "minimatch": "^9.0.0", + "read-package-json-fast": "^3.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { - "version": "8.0.0", + "version": "7.1.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "cacache": "^19.0.0", - "json-parse-even-better-errors": "^4.0.0", - "pacote": "^19.0.0", - "proc-log": "^5.0.0", + "cacache": "^18.0.0", + "json-parse-even-better-errors": "^3.0.0", + "pacote": "^18.0.0", + "proc-log": "^4.1.0", "semver": "^7.3.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/name-from-folder": { - "version": "3.0.0", + "version": "2.0.0", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/node-gyp": { - "version": "4.0.0", + "version": "3.0.0", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "6.0.1", + "version": "5.2.0", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^6.0.0", + "@npmcli/git": "^5.0.0", "glob": "^10.2.2", - "hosted-git-info": "^8.0.0", - "json-parse-even-better-errors": "^4.0.0", - "normalize-package-data": "^7.0.0", - "proc-log": "^5.0.0", + "hosted-git-info": "^7.0.0", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^6.0.0", + "proc-log": "^4.0.0", "semver": "^7.5.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/promise-spawn": { - "version": "8.0.1", + "version": "7.0.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "which": "^5.0.0" + "which": "^4.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/query": { - "version": "4.0.0", + "version": "3.1.0", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "postcss-selector-parser": "^6.1.2" + "postcss-selector-parser": "^6.0.10" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/redact": { - "version": "3.0.0", + "version": "2.0.1", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "9.0.1", + "version": "8.1.0", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/node-gyp": "^4.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/package-json": "^5.0.0", + "@npmcli/promise-spawn": "^7.0.0", "node-gyp": "^10.0.0", - "proc-log": "^5.0.0", - "which": "^5.0.0" + "proc-log": "^4.0.0", + "which": "^4.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/@pkgjs/parseargs": { @@ -4109,183 +4216,47 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/@npmcli/agent": { - "version": "2.2.2", + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "2.3.4", "dev": true, "inBundle": true, - "license": "ISC", + "license": "Apache-2.0", "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^2.2.1" }, "engines": { "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/@npmcli/fs": { - "version": "3.1.1", + "node_modules/npm/node_modules/@sigstore/verify": { + "version": "1.2.1", "dev": true, "inBundle": true, - "license": "ISC", + "license": "Apache-2.0", "dependencies": { - "semver": "^7.3.5" + "@sigstore/bundle": "^2.3.2", + "@sigstore/core": "^1.1.0", + "@sigstore/protobuf-specs": "^0.3.2" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/cacache": { - "version": "18.0.4", + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "2.0.0", "dev": true, "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" - }, + "license": "MIT", "engines": { "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/make-fetch-happen": { - "version": "13.0.1", + "node_modules/npm/node_modules/@tufjs/models": { + "version": "2.0.1", "dev": true, "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", - "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1", - "ssri": "^10.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/minipass-fetch": { - "version": "3.0.5", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/proc-log": { - "version": "4.2.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/ssri": { - "version": "10.0.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/unique-filename": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@sigstore/sign/node_modules/unique-slug": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@sigstore/tuf": { - "version": "2.3.4", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2", - "tuf-js": "^2.2.1" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@sigstore/verify": { - "version": "1.2.1", - "dev": true, - "inBundle": true, - "license": "Apache-2.0", - "dependencies": { - "@sigstore/bundle": "^2.3.2", - "@sigstore/core": "^1.1.0", - "@sigstore/protobuf-specs": "^0.3.2" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@tufjs/canonical-json": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/@tufjs/models": { - "version": "2.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", + "license": "MIT", "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^9.0.4" @@ -4295,12 +4266,12 @@ } }, "node_modules/npm/node_modules/abbrev": { - "version": "3.0.0", + "version": "2.0.0", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/agent-base": { @@ -4368,19 +4339,18 @@ "license": "MIT" }, "node_modules/npm/node_modules/bin-links": { - "version": "5.0.0", + "version": "4.0.4", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "cmd-shim": "^7.0.0", - "npm-normalize-package-bin": "^4.0.0", - "proc-log": "^5.0.0", - "read-cmd-shim": "^5.0.0", - "write-file-atomic": "^6.0.0" + "cmd-shim": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "read-cmd-shim": "^4.0.0", + "write-file-atomic": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/binary-extensions": { @@ -4405,12 +4375,12 @@ } }, "node_modules/npm/node_modules/cacache": { - "version": "19.0.1", + "version": "18.0.4", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/fs": "^4.0.0", + "@npmcli/fs": "^3.1.0", "fs-minipass": "^3.0.0", "glob": "^10.2.2", "lru-cache": "^10.0.1", @@ -4418,88 +4388,13 @@ "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", - "p-map": "^7.0.2", - "ssri": "^12.0.0", - "tar": "^7.4.3", - "unique-filename": "^4.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/chownr": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/minizlib": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/mkdirp": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "bin": { - "mkdirp": "dist/cjs/src/bin.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/p-map": { - "version": "7.0.2", - "dev": true, - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/tar": { - "version": "7.4.3", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@isaacs/fs-minipass": "^4.0.0", - "chownr": "^3.0.0", - "minipass": "^7.1.2", - "minizlib": "^3.0.1", - "mkdirp": "^3.0.1", - "yallist": "^5.0.0" + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" }, "engines": { - "node": ">=18" - } - }, - "node_modules/npm/node_modules/cacache/node_modules/yallist": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": ">=18" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/chalk": { @@ -4573,12 +4468,12 @@ } }, "node_modules/npm/node_modules/cmd-shim": { - "version": "7.0.0", + "version": "6.0.3", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/color-convert": { @@ -4785,7 +4680,7 @@ "license": "ISC" }, "node_modules/npm/node_modules/hosted-git-info": { - "version": "8.0.0", + "version": "7.0.2", "dev": true, "inBundle": true, "license": "ISC", @@ -4793,7 +4688,7 @@ "lru-cache": "^10.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/http-cache-semantics": { @@ -4842,7 +4737,7 @@ } }, "node_modules/npm/node_modules/ignore-walk": { - "version": "7.0.0", + "version": "6.0.5", "dev": true, "inBundle": true, "license": "ISC", @@ -4850,7 +4745,7 @@ "minimatch": "^9.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/imurmurhash": { @@ -4872,30 +4767,30 @@ } }, "node_modules/npm/node_modules/ini": { - "version": "5.0.0", + "version": "4.1.3", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/init-package-json": { - "version": "7.0.1", + "version": "6.0.3", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/package-json": "^6.0.0", - "npm-package-arg": "^12.0.0", - "promzard": "^2.0.0", - "read": "^4.0.0", + "@npmcli/package-json": "^5.0.0", + "npm-package-arg": "^11.0.0", + "promzard": "^1.0.0", + "read": "^3.0.1", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4", - "validate-npm-package-name": "^6.0.0" + "validate-npm-package-name": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/ip-address": { @@ -4978,12 +4873,12 @@ "license": "MIT" }, "node_modules/npm/node_modules/json-parse-even-better-errors": { - "version": "4.0.0", + "version": "3.0.2", "dev": true, "inBundle": true, "license": "MIT", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/json-stringify-nice": { @@ -5017,169 +4912,169 @@ "license": "MIT" }, "node_modules/npm/node_modules/libnpmaccess": { - "version": "9.0.0", + "version": "8.0.6", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "npm-package-arg": "^12.0.0", - "npm-registry-fetch": "^18.0.1" + "npm-package-arg": "^11.0.2", + "npm-registry-fetch": "^17.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmdiff": { - "version": "7.0.0", + "version": "6.1.4", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^8.0.0", - "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/arborist": "^7.5.4", + "@npmcli/installed-package-contents": "^2.1.0", "binary-extensions": "^2.3.0", "diff": "^5.1.0", "minimatch": "^9.0.4", - "npm-package-arg": "^12.0.0", - "pacote": "^19.0.0", + "npm-package-arg": "^11.0.2", + "pacote": "^18.0.6", "tar": "^6.2.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "9.0.0", + "version": "8.1.4", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^8.0.0", - "@npmcli/run-script": "^9.0.1", + "@npmcli/arborist": "^7.5.4", + "@npmcli/run-script": "^8.1.0", "ci-info": "^4.0.0", - "npm-package-arg": "^12.0.0", - "pacote": "^19.0.0", - "proc-log": "^5.0.0", - "read": "^4.0.0", - "read-package-json-fast": "^4.0.0", + "npm-package-arg": "^11.0.2", + "pacote": "^18.0.6", + "proc-log": "^4.2.0", + "read": "^3.0.1", + "read-package-json-fast": "^3.0.2", "semver": "^7.3.7", "walk-up-path": "^3.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmfund": { - "version": "6.0.0", + "version": "5.0.12", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^8.0.0" + "@npmcli/arborist": "^7.5.4" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmhook": { - "version": "11.0.0", + "version": "10.0.5", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" + "npm-registry-fetch": "^17.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmorg": { - "version": "7.0.0", + "version": "6.0.6", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" + "npm-registry-fetch": "^17.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmpack": { - "version": "8.0.0", + "version": "7.0.4", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/arborist": "^8.0.0", - "@npmcli/run-script": "^9.0.1", - "npm-package-arg": "^12.0.0", - "pacote": "^19.0.0" + "@npmcli/arborist": "^7.5.4", + "@npmcli/run-script": "^8.1.0", + "npm-package-arg": "^11.0.2", + "pacote": "^18.0.6" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmpublish": { - "version": "10.0.0", + "version": "9.0.9", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "ci-info": "^4.0.0", - "normalize-package-data": "^7.0.0", - "npm-package-arg": "^12.0.0", - "npm-registry-fetch": "^18.0.1", - "proc-log": "^5.0.0", + "normalize-package-data": "^6.0.1", + "npm-package-arg": "^11.0.2", + "npm-registry-fetch": "^17.0.1", + "proc-log": "^4.2.0", "semver": "^7.3.7", "sigstore": "^2.2.0", - "ssri": "^12.0.0" + "ssri": "^10.0.6" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmsearch": { - "version": "8.0.0", + "version": "7.0.6", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "npm-registry-fetch": "^18.0.1" + "npm-registry-fetch": "^17.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmteam": { - "version": "7.0.0", + "version": "6.0.5", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { "aproba": "^2.0.0", - "npm-registry-fetch": "^18.0.1" + "npm-registry-fetch": "^17.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/libnpmversion": { - "version": "7.0.0", + "version": "6.0.3", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^6.0.1", - "@npmcli/run-script": "^9.0.1", - "json-parse-even-better-errors": "^4.0.0", - "proc-log": "^5.0.0", + "@npmcli/git": "^5.0.7", + "@npmcli/run-script": "^8.1.0", + "json-parse-even-better-errors": "^3.0.2", + "proc-log": "^4.2.0", "semver": "^7.3.7" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/lru-cache": { @@ -5189,25 +5084,26 @@ "license": "ISC" }, "node_modules/npm/node_modules/make-fetch-happen": { - "version": "14.0.1", + "version": "13.0.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/agent": "^3.0.0", - "cacache": "^19.0.1", + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", + "minipass-fetch": "^3.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", - "proc-log": "^5.0.0", + "proc-log": "^4.2.0", "promise-retry": "^2.0.1", - "ssri": "^12.0.0" + "ssri": "^10.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/minimatch": { @@ -5247,35 +5143,22 @@ } }, "node_modules/npm/node_modules/minipass-fetch": { - "version": "4.0.0", + "version": "3.0.5", "dev": true, "inBundle": true, "license": "MIT", "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", - "minizlib": "^3.0.1" + "minizlib": "^2.1.2" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" }, "optionalDependencies": { "encoding": "^0.1.13" } }, - "node_modules/npm/node_modules/minipass-fetch/node_modules/minizlib": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" - }, - "engines": { - "node": ">= 18" - } - }, "node_modules/npm/node_modules/minipass-flush": { "version": "1.0.5", "dev": true, @@ -5392,12 +5275,12 @@ "license": "MIT" }, "node_modules/npm/node_modules/mute-stream": { - "version": "2.0.0", + "version": "1.0.0", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/negotiator": { @@ -5433,192 +5316,8 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/agent": { - "version": "2.2.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/fs": { - "version": "3.1.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/abbrev": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/cacache": { - "version": "18.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/make-fetch-happen": { - "version": "13.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", - "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1", - "ssri": "^10.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/minipass-fetch": { - "version": "3.0.5", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/nopt": { - "version": "7.2.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^2.0.0" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/proc-log": { - "version": "4.2.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/ssri": { - "version": "10.0.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/unique-filename": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^4.0.0" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/unique-slug": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/which": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, "node_modules/npm/node_modules/nopt": { - "version": "8.0.0", + "version": "7.2.1", "dev": true, "inBundle": true, "license": "ISC", @@ -5628,56 +5327,47 @@ "bin": { "nopt": "bin/nopt.js" }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/nopt/node_modules/abbrev": { - "version": "2.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/normalize-package-data": { - "version": "7.0.0", + "version": "6.0.2", "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { - "hosted-git-info": "^8.0.0", + "hosted-git-info": "^7.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/npm-audit-report": { - "version": "6.0.0", + "version": "5.0.0", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/npm-bundled": { - "version": "4.0.0", + "version": "3.0.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "npm-normalize-package-bin": "^4.0.0" + "npm-normalize-package-bin": "^3.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/npm-install-checks": { - "version": "7.1.0", + "version": "6.3.0", "dev": true, "inBundle": true, "license": "BSD-2-Clause", @@ -5685,112 +5375,99 @@ "semver": "^7.1.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/npm-normalize-package-bin": { - "version": "4.0.0", + "version": "3.0.1", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/npm-package-arg": { - "version": "12.0.0", + "version": "11.0.3", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "hosted-git-info": "^8.0.0", - "proc-log": "^5.0.0", + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", "semver": "^7.3.5", - "validate-npm-package-name": "^6.0.0" + "validate-npm-package-name": "^5.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/npm-packlist": { - "version": "9.0.0", + "version": "8.0.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "ignore-walk": "^7.0.0" + "ignore-walk": "^6.0.4" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/npm-pick-manifest": { - "version": "10.0.0", + "version": "9.1.0", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "npm-install-checks": "^7.1.0", - "npm-normalize-package-bin": "^4.0.0", - "npm-package-arg": "^12.0.0", + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^11.0.0", "semver": "^7.3.5" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/npm-profile": { - "version": "11.0.1", + "version": "10.0.0", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "npm-registry-fetch": "^18.0.0", - "proc-log": "^5.0.0" + "npm-registry-fetch": "^17.0.1", + "proc-log": "^4.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": ">=18.0.0" } }, "node_modules/npm/node_modules/npm-registry-fetch": { - "version": "18.0.1", + "version": "17.1.0", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/redact": "^3.0.0", + "@npmcli/redact": "^2.0.0", "jsonparse": "^1.3.1", - "make-fetch-happen": "^14.0.0", + "make-fetch-happen": "^13.0.0", "minipass": "^7.0.2", - "minipass-fetch": "^4.0.0", - "minizlib": "^3.0.1", - "npm-package-arg": "^12.0.0", - "proc-log": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/npm-registry-fetch/node_modules/minizlib": { - "version": "3.0.1", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" + "minipass-fetch": "^3.0.0", + "minizlib": "^2.1.2", + "npm-package-arg": "^11.0.0", + "proc-log": "^4.0.0" }, "engines": { - "node": ">= 18" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/npm-user-validate": { - "version": "3.0.0", + "version": "2.0.1", "dev": true, "inBundle": true, "license": "BSD-2-Clause", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/p-map": { @@ -5815,48 +5492,48 @@ "license": "BlueOak-1.0.0" }, "node_modules/npm/node_modules/pacote": { - "version": "19.0.0", + "version": "18.0.6", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "@npmcli/git": "^6.0.0", - "@npmcli/installed-package-contents": "^3.0.0", - "@npmcli/package-json": "^6.0.0", - "@npmcli/promise-spawn": "^8.0.0", - "@npmcli/run-script": "^9.0.0", - "cacache": "^19.0.0", + "@npmcli/git": "^5.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/package-json": "^5.1.0", + "@npmcli/promise-spawn": "^7.0.0", + "@npmcli/run-script": "^8.0.0", + "cacache": "^18.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", - "npm-package-arg": "^12.0.0", - "npm-packlist": "^9.0.0", - "npm-pick-manifest": "^10.0.0", - "npm-registry-fetch": "^18.0.0", - "proc-log": "^5.0.0", + "npm-package-arg": "^11.0.0", + "npm-packlist": "^8.0.0", + "npm-pick-manifest": "^9.0.0", + "npm-registry-fetch": "^17.0.0", + "proc-log": "^4.0.0", "promise-retry": "^2.0.1", "sigstore": "^2.2.0", - "ssri": "^12.0.0", + "ssri": "^10.0.0", "tar": "^6.1.11" }, "bin": { "pacote": "bin/index.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.14.0 || >=18.0.0" } }, "node_modules/npm/node_modules/parse-conflict-json": { - "version": "4.0.0", + "version": "3.0.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "json-parse-even-better-errors": "^4.0.0", + "json-parse-even-better-errors": "^3.0.0", "just-diff": "^6.0.0", "just-diff-apply": "^5.2.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/path-key": { @@ -5898,21 +5575,21 @@ } }, "node_modules/npm/node_modules/proc-log": { - "version": "5.0.0", + "version": "4.2.0", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/proggy": { - "version": "3.0.0", + "version": "2.0.0", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/promise-all-reject-late": { @@ -5953,15 +5630,15 @@ } }, "node_modules/npm/node_modules/promzard": { - "version": "2.0.0", + "version": "1.0.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "read": "^4.0.0" + "read": "^3.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/qrcode-terminal": { @@ -5973,37 +5650,37 @@ } }, "node_modules/npm/node_modules/read": { - "version": "4.0.0", + "version": "3.0.1", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "mute-stream": "^2.0.0" + "mute-stream": "^1.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/read-cmd-shim": { - "version": "5.0.0", + "version": "4.0.0", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/read-package-json-fast": { - "version": "4.0.0", + "version": "3.0.2", "dev": true, "inBundle": true, "license": "ISC", "dependencies": { - "json-parse-even-better-errors": "^4.0.0", - "npm-normalize-package-bin": "^4.0.0" + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/retry": { @@ -6015,21 +5692,6 @@ "node": ">= 4" } }, - "node_modules/npm/node_modules/rimraf": { - "version": "5.0.10", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/npm/node_modules/safer-buffer": { "version": "2.1.2", "dev": true, @@ -6186,7 +5848,7 @@ "license": "BSD-3-Clause" }, "node_modules/npm/node_modules/ssri": { - "version": "12.0.0", + "version": "10.0.6", "dev": true, "inBundle": true, "license": "ISC", @@ -6194,7 +5856,7 @@ "minipass": "^7.0.3" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/string-width": { @@ -6348,119 +6010,7 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/tuf-js/node_modules/@npmcli/agent": { - "version": "2.2.2", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/@npmcli/fs": { - "version": "3.1.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/cacache": { - "version": "18.0.4", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/make-fetch-happen": { - "version": "13.0.1", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", - "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1", - "ssri": "^10.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/minipass-fetch": { - "version": "3.0.5", - "dev": true, - "inBundle": true, - "license": "MIT", - "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/proc-log": { - "version": "4.2.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/ssri": { - "version": "10.0.6", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/tuf-js/node_modules/unique-filename": { + "node_modules/npm/node_modules/unique-filename": { "version": "3.0.0", "dev": true, "inBundle": true, @@ -6472,7 +6022,7 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/tuf-js/node_modules/unique-slug": { + "node_modules/npm/node_modules/unique-slug": { "version": "4.0.0", "dev": true, "inBundle": true, @@ -6484,30 +6034,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/unique-filename": { - "version": "4.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^5.0.0" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "node_modules/npm/node_modules/unique-slug": { - "version": "5.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - }, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, "node_modules/npm/node_modules/util-deprecate": { "version": "1.0.2", "dev": true, @@ -6535,12 +6061,12 @@ } }, "node_modules/npm/node_modules/validate-npm-package-name": { - "version": "6.0.0", + "version": "5.0.1", "dev": true, "inBundle": true, "license": "ISC", "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/walk-up-path": { @@ -6550,7 +6076,7 @@ "license": "ISC" }, "node_modules/npm/node_modules/which": { - "version": "5.0.0", + "version": "4.0.0", "dev": true, "inBundle": true, "license": "ISC", @@ -6561,7 +6087,7 @@ "node-which": "bin/which.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/which/node_modules/isexe": { @@ -6674,7 +6200,7 @@ } }, "node_modules/npm/node_modules/write-file-atomic": { - "version": "6.0.0", + "version": "5.0.1", "dev": true, "inBundle": true, "license": "ISC", @@ -6683,7 +6209,7 @@ "signal-exit": "^4.0.1" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/npm/node_modules/yallist": { @@ -6933,9 +6459,9 @@ } }, "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "dev": true, "license": "ISC" }, @@ -7234,9 +6760,9 @@ "license": "MIT" }, "node_modules/semantic-release": { - "version": "24.2.0", - "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.2.0.tgz", - "integrity": "sha512-fQfn6e/aYToRtVJYKqneFM1Rg3KP2gh3wSWtpYsLlz6uaPKlISrTzvYAFn+mYWo07F0X1Cz5ucU89AVE8X1mbg==", + "version": "24.1.2", + "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.1.2.tgz", + "integrity": "sha512-hvEJ7yI97pzJuLsDZCYzJgmRxF8kiEJvNZhf0oiZQcexw+Ycjy4wbdsn/sVMURgNCu8rwbAXJdBRyIxM4pe32g==", "dev": true, "license": "MIT", "dependencies": { @@ -7333,10 +6859,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/semantic-release/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/semantic-release/node_modules/execa": { - "version": "9.5.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.5.0.tgz", - "integrity": "sha512-t7vvYt+oKnMbF3O+S5+HkylsPrsUatwJSe4Cv+4017R0MCySjECxnVJ2eyDXVD/Xpj5H29YzyYn6eEpugG7GJA==", + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.4.0.tgz", + "integrity": "sha512-yKHlle2YGxZE842MERVIplWwNH5VYmqqcPFgtnlU//K8gxuFFXu0pwd/CrfXTumFpeEiufsP7+opT/bPJa1yVw==", "dev": true, "license": "MIT", "dependencies": { @@ -7627,16 +7166,6 @@ "dev": true, "license": "MIT" }, - "node_modules/signale/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/signale/node_modules/figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", diff --git a/package.json b/package.json index 788ff709a..105a5ca87 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,6 @@ "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "gradle-semantic-release-plugin": "^1.10.1", - "semantic-release": "^24.2.0" + "semantic-release": "^24.1.2" } } diff --git a/patches.json b/patches.json index aaf641748..da0084599 100644 --- a/patches.json +++ b/patches.json @@ -1 +1,3083 @@ -[{"name":"Alternative thumbnails","description":"Adds options to replace video thumbnails using the DeArrow API or image captures from the video.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Ambient mode control","description":"Adds options to disable Ambient mode and to bypass Ambient mode restrictions.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Amoled","description":"Applies a pure black theme to some components.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Bitrate default value","description":"Sets the audio quality to \u0027Always High\u0027 when you first install the app.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Bypass image region restrictions","description":"Adds an option to use a different host for static images, so that images blocked in some countries can be received.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Bypass image region restrictions","description":"Adds an option to use a different host for static images, so that images blocked in some countries can be received.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Certificate spoof","description":"Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Change package name","description":"Changes the package name for Reddit to the name specified in options.json.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":false,"requiresIntegrations":false,"options":[{"key":"PackageNameReddit","default":"com.reddit.frontpage","values":{"Clone":"com.reddit.frontpage.revanced","Default":"com.reddit.frontpage.rvx","Original":"com.reddit.frontpage"},"title":"Package name of Reddit","description":"The name of the package to rename the app to.","required":true}]},{"name":"Change player flyout menu toggles","description":"Adds an option to use text toggles instead of switch toggles within the additional settings menu.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Change share sheet","description":"Add option to change from in-app share sheet to system share sheet.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Change share sheet","description":"Add option to change from in-app share sheet to system share sheet.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Change start page","description":"Adds an option to set which page the app opens in instead of the homepage.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Change start page","description":"Adds an option to set which page the app opens in instead of the homepage.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Change version code","description":"Changes the version code of the app to the value specified in options.json. Except when mounting, this can prevent app stores from updating the app and allow the app to be installed over an existing installation that has a higher version code. By default, the highest version code is set.","compatiblePackages":null,"use":false,"requiresIntegrations":false,"options":[{"key":"ChangeVersionCode","default":false,"values":null,"title":"Change version code","description":"Changes the version code of the app.","required":true},{"key":"VersionCode","default":"2147483647","values":null,"title":"Version code","description":"The version code to use. (1 ~ 2147483647)","required":true}]},{"name":"Custom Shorts action buttons","description":"Changes, at compile time, the icon of the action buttons of the Shorts player.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[{"key":"IconType","default":"round","values":{"Cairo":"round","Outline":"outline","OutlineCircle":"outlinecircle","Round":"round","YouTube":"youtube","YouTubeOutline":"youtubeoutline"},"title":"Shorts icon style ","description":"The style of the icons for the action buttons in the Shorts player.","required":true}]},{"name":"Custom branding icon for YouTube","description":"Changes the YouTube app icon to the icon specified in options.json.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":false,"requiresIntegrations":false,"options":[{"key":"AppIcon","default":"Xisr Yellow","values":{"AFN Blue":"afn_blue","AFN Red":"afn_red","MMT":"mmt","MMT Blue":"mmt_blue","MMT Green":"mmt_green","MMT Orange":"mmt_orange","MMT Pink":"mmt_pink","MMT Turquoise":"mmt_turquoise","MMT Yellow":"mmt_yellow","Revancify Blue":"revancify_blue","Revancify Red":"revancify_red","Vanced Black":"vanced_black","Vanced Light":"vanced_light","Xisr Yellow":"xisr_yellow","YouTube":"youtube"},"title":"App icon","description":"The icon to apply to the app.\n\nIf a path to a folder is provided, the folder must contain the following folders:\n\n- mipmap-xxxhdpi\n- mipmap-xxhdpi\n- mipmap-xhdpi\n- mipmap-hdpi\n- mipmap-mdpi\n\nEach of these folders must contain the following files:\n\n- adaptiveproduct_youtube_background_color_108.png\n- adaptiveproduct_youtube_foreground_color_108.png\n- ic_launcher.png\n- ic_launcher_round.png","required":true},{"key":"ChangeHeader","default":true,"values":null,"title":"Change header","description":"Apply the custom branding icon to the header.","required":true},{"key":"CustomHeader","default":"","values":null,"title":"Custom header","description":"The header to apply to the app.\n\nIf a path to a folder is provided, the folder must contain one or more of the following folders, depending on the DPI of the device:\n\n- drawable-xxxhdpi\n- drawable-xxhdpi\n- drawable-xhdpi\n- drawable-hdpi\n- drawable-mdpi\n\nEach of the folders must contain all of the following files:\n\n- yt_premium_wordmark_header_dark.png\n- yt_premium_wordmark_header_light.png\n- yt_wordmark_header_dark.png\n- yt_wordmark_header_light.png\n\nThe image dimensions must be as follows:\n- drawable-xxxhdpi: 512px x 192px\n- drawable-xxhdpi: 387px x 144px\n- drawable-xhdpi: 258px x 96px\n- drawable-hdpi: 194px x 72px\n- drawable-mdpi: 129px x 48px","required":true},{"key":"ChangeSplashIcon","default":true,"values":null,"title":"Change splash icons","description":"Apply the custom branding icon to the splash screen.","required":true},{"key":"RestoreOldSplashAnimation","default":true,"values":null,"title":"Restore old splash animation","description":"Restore the old style splash animation.","required":true}]},{"name":"Custom branding icon for YouTube Music","description":"Changes the YouTube Music app icon to the icon specified in options.json.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":false,"requiresIntegrations":false,"options":[{"key":"AppIcon","default":"Xisr Yellow","values":{"AFN Blue":"afn_blue","AFN Red":"afn_red","MMT":"mmt","MMT Blue":"mmt_blue","MMT Green":"mmt_green","MMT Orange":"mmt_orange","MMT Pink":"mmt_pink","MMT Turquoise":"mmt_turquoise","MMT Yellow":"mmt_yellow","Revancify Blue":"revancify_blue","Revancify Red":"revancify_red","Vanced Black":"vanced_black","Vanced Light":"vanced_light","Xisr Yellow":"xisr_yellow","YouTube Music":"youtube_music"},"title":"App icon","description":"The icon to apply to the app.\n\nIf a path to a folder is provided, the folder must contain the following folders:\n\n- mipmap-xxxhdpi\n- mipmap-xxhdpi\n- mipmap-xhdpi\n- mipmap-hdpi\n- mipmap-mdpi\n\nEach of these folders must contain the following files:\n\n- adaptiveproduct_youtube_music_background_color_108.png\n- adaptiveproduct_youtube_music_foreground_color_108.png\n- ic_launcher_release.png","required":true},{"key":"ChangeHeader","default":true,"values":null,"title":"Change header","description":"Apply the custom branding icon to the header.","required":true},{"key":"ChangeSplashIcon","default":true,"values":null,"title":"Change splash icons","description":"Apply the custom branding icon to the splash screen.","required":true},{"key":"RestoreOldSplashIcon","default":false,"values":null,"title":"Restore old splash icon","description":"Restore the old style splash icon.\n\nIf you enable both the old style splash icon and the Cairo splash animation,\n\nOld style splash icon will appear first and then the Cairo splash animation will start.","required":true}]},{"name":"Custom branding name for Reddit","description":"Renames the Reddit app to the name specified in options.json.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":false,"requiresIntegrations":false,"options":[{"key":"AppName","default":"Reddit","values":{"Default":"RVX Reddit","Original":"Reddit"},"title":"App name","description":"The name of the app.","required":true}]},{"name":"Custom branding name for YouTube","description":"Renames the YouTube app to the name specified in options.json.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":false,"requiresIntegrations":false,"options":[{"key":"AppName","default":"RVX","values":{"ReVanced Extended":"ReVanced Extended","RVX":"RVX","YouTube RVX":"YouTube RVX","YouTube":"YouTube"},"title":"App name","description":"The name of the app.","required":true}]},{"name":"Custom branding name for YouTube Music","description":"Renames the YouTube Music app to the name specified in options.json.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":false,"requiresIntegrations":false,"options":[{"key":"AppNameNotification","default":"RVX Music","values":{"ReVanced Extended Music":"ReVanced Extended Music","RVX Music":"RVX Music","YouTube Music":"YouTube Music","YT Music":"YT Music"},"title":"App name in notification panel","description":"The name of the app as it appears in the notification panel.","required":true},{"key":"AppNameLauncher","default":"RVX Music","values":{"ReVanced Extended Music":"ReVanced Extended Music","RVX Music":"RVX Music","YouTube Music":"YouTube Music","YT Music":"YT Music"},"title":"App name in launcher","description":"The name of the app as it appears in the launcher.","required":true}]},{"name":"Custom double tap length","description":"Adds Double-tap to seek values that are specified in options.json.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[{"key":"DoubleTapLengthArrays","default":"3, 5, 10, 15, 20, 30, 60, 120, 180","values":null,"title":"Double-tap to seek values","description":"A list of custom Double-tap to seek lengths to be added, separated by commas.","required":true}]},{"name":"Custom header for YouTube Music","description":"Applies a custom header in the top left corner within the app.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":false,"requiresIntegrations":false,"options":[{"key":"CustomHeader","default":"custom_branding_icon","values":{"Custom branding icon":"custom_branding_icon"},"title":"Custom header","description":"The header to apply to the app.\n\nPatch option \u0027Custom branding icon\u0027 applies only when:\n\n1. Patch \u0027Custom branding icon for YouTube Music\u0027 is included.\n2. Patch option for \u0027Custom branding icon for YouTube Music\u0027 is selected from the preset.\n\nIf a path to a folder is provided, the folder must contain one or more of the following folders, depending on the DPI of the device:\n\n- drawable-xxxhdpi\n- drawable-xxhdpi\n- drawable-xhdpi\n- drawable-hdpi\n- drawable-mdpi\n\nEach of the folders must contain all of the following files:\n\n- action_bar_logo.png\n- logo_music.png\n- ytm_logo.png\n\nThe image \u0027action_bar_logo.png\u0027 dimensions must be as follows:\n\n- drawable-xxxhdpi: 320px x 96px\n- drawable-xxhdpi: 240px x 72px\n- drawable-xhdpi: 160px x 48px\n- drawable-hdpi: 121px x 36px\n- drawable-mdpi: 80px x 24px\n\nThe image \u0027logo_music.png\u0027 dimensions must be as follows:\n\n- drawable-xxxhdpi: 576px x 200px\n- drawable-xxhdpi: 432px x 150px\n- drawable-xhdpi: 288px x 100px\n- drawable-hdpi: 217px x 76px\n- drawable-mdpi: 144px x 50px\n\nThe image \u0027ytm_logo.png\u0027 dimensions must be as follows:\n\n- drawable-xxxhdpi: 412px x 144px\n- drawable-xxhdpi: 309px x 108px\n- drawable-xhdpi: 206px x 72px\n- drawable-hdpi: 155px x 54px\n- drawable-mdpi: 103px x 36px","required":true}]},{"name":"Description components","description":"Adds options to hide and disable description components.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Disable Cairo splash animation","description":"Adds an option to disable Cairo splash animation.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["7.06.54","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Disable QUIC protocol","description":"Adds an option to disable CronetEngine\u0027s QUIC protocol.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Disable auto audio tracks","description":"Adds an option to disable audio tracks from being automatically enabled.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Disable auto captions","description":"Adds an option to disable captions from being automatically enabled.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Disable auto captions","description":"Adds an option to disable captions from being automatically enabled.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Disable dislike redirection","description":"Adds an option to disable redirection to the next track when clicking the Dislike button.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Disable haptic feedback","description":"Adds options to disable haptic feedback when swiping in the video player.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Disable resuming Shorts on startup","description":"Adds an option to disable the Shorts player from resuming on app startup when Shorts were last being watched.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Disable screenshot popup","description":"Adds an option to disable the popup that appears when taking a screenshot.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Disable splash animation","description":"Adds an option to disable the splash animation on app startup.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Enable OPUS codec","description":"Adds an options to enable the OPUS audio codec if the player response includes.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Enable OPUS codec","description":"Adds an option to use the OPUS audio codec instead of the MP4A audio codec.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Enable debug logging","description":"Adds an option to enable debug logging.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Enable debug logging","description":"Adds an option to enable debug logging.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Enable external browser","description":"Adds an option to always open links in your browser instead of in the in-app-browser.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Enable gradient loading screen","description":"Adds an option to enable the gradient loading screen.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Enable landscape mode","description":"Adds an option to enable landscape mode when rotating the screen on phones.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Enable open links directly","description":"Adds an option to skip over redirection URLs in external links.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Flyout menu components","description":"Adds options to hide or change flyout menu components.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Force player buttons background","description":"Changes the dark background surrounding the video player controls at compile time.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[{"key":"BackgroundColor","default":"?ytOverlayBackgroundMediumLight","values":{"Default":"?ytOverlayBackgroundMediumLight","Transparent":"@android:color/transparent","Opacity10":"#1a000000","Opacity20":"#33000000","Opacity30":"#4d000000","Opacity40":"#66000000","Opacity50":"#80000000","Opacity60":"#99000000","Opacity70":"#b3000000","Opacity80":"#cc000000","Opacity90":"#e6000000","Opacity100":"#ff000000"},"title":"Background color","description":"Specify a background color for player buttons using a hex color code. The first two symbols of the hex code represent the alpha channel, which is used to change the opacity.","required":false}]},{"name":"Force snackbar theme","description":"Force snackbar background color to match selected theme.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[{"key":"CornerRadius","default":"8.0dip","values":null,"title":"Corner radius","description":"Specify a corner radius for the snackbar.","required":false},{"key":"BackgroundColor","default":"?ytChipBackground","values":{"Chip":"?ytChipBackground","Base":"?ytBaseBackground"},"title":"Background color","description":"Specify a background color for the snackbar. You can specify hex color.","required":false},{"key":"StrokeColor","default":"none","values":{"None":"none","Accent":"?attr/colorAccent","Inverted":"?attr/ytInvertedBackground"},"title":"Stroke color","description":"Specify a stroke color for the snackbar. You can specify hex color.","required":false}]},{"name":"Fullscreen components","description":"Adds options to hide or change components related to fullscreen.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"GmsCore support","description":"Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":true,"options":[{"key":"GmsCoreVendorGroupId","default":"app.revanced","values":{"ReVanced":"app.revanced"},"title":"GmsCore vendor group ID","description":"The vendor\u0027s group ID for GmsCore.","required":true},{"key":"CheckGmsCore","default":true,"values":null,"title":"Check GmsCore","description":"Check if GmsCore is installed on the device and has battery optimizations disabled when the app starts. \n\nIf GmsCore is not installed the app will not work, so disabling this is not recommended.","required":true},{"key":"DisableGmsServiceBroker","default":false,"values":null,"title":"Disable GmsService Broker","description":"Disabling GmsServiceBroker will somewhat improve crashes caused by unimplemented GmsCore services.\n\nFor YouTube, the \u0027Spoof streaming data\u0027 setting is required.","required":true},{"key":"PackageNameYouTube","default":"anddea.youtube","values":{"Clone":"bill.youtube","Default":"anddea.youtube"},"title":"Package name of YouTube","description":"The name of the package to use in GmsCore support.","required":true},{"key":"PackageNameYouTubeMusic","default":"anddea.youtube.music","values":{"Clone":"bill.youtube.music","Default":"anddea.youtube.music"},"title":"Package name of YouTube Music","description":"The name of the package to use in GmsCore support.","required":true}]},{"name":"GmsCore support","description":"Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":true,"options":[{"key":"GmsCoreVendorGroupId","default":"app.revanced","values":{"ReVanced":"app.revanced"},"title":"GmsCore vendor group ID","description":"The vendor\u0027s group ID for GmsCore.","required":true},{"key":"CheckGmsCore","default":true,"values":null,"title":"Check GmsCore","description":"Check if GmsCore is installed on the device and has battery optimizations disabled when the app starts. \n\nIf GmsCore is not installed the app will not work, so disabling this is not recommended.","required":true},{"key":"DisableGmsServiceBroker","default":false,"values":null,"title":"Disable GmsService Broker","description":"Disabling GmsServiceBroker will somewhat improve crashes caused by unimplemented GmsCore services.\n\nFor YouTube, the \u0027Spoof streaming data\u0027 setting is required.","required":true},{"key":"PackageNameYouTube","default":"anddea.youtube","values":{"Clone":"bill.youtube","Default":"anddea.youtube"},"title":"Package name of YouTube","description":"The name of the package to use in GmsCore support.","required":true},{"key":"PackageNameYouTubeMusic","default":"anddea.youtube.music","values":{"Clone":"bill.youtube.music","Default":"anddea.youtube.music"},"title":"Package name of YouTube Music","description":"The name of the package to use in GmsCore support.","required":true}]},{"name":"Hide Recently Visited shelf","description":"Adds an option to hide the Recently Visited shelf in the sidebar.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide Shorts dimming","description":"Removes, at compile time, the dimming effect at the top and bottom of Shorts videos.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":false,"requiresIntegrations":false,"options":[]},{"name":"Hide account components","description":"Adds options to hide components related to the account menu.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide action bar components","description":"Adds options to hide action bar components and replace the offline download button with an external download button.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide action buttons","description":"Adds options to hide action buttons under videos.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide ads","description":"Adds options to hide ads.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide ads","description":"Adds options to hide ads.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide ads","description":"Adds options to hide ads.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":true,"requiresIntegrations":true,"options":[]},{"name":"Hide comments components","description":"Adds options to hide components related to comments.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide feed components","description":"Adds options to hide components related to feeds.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide feed flyout menu","description":"Adds the ability to hide feed flyout menu components using a custom filter.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide layout components","description":"Adds options to hide general layout components.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide layout components","description":"Adds options to hide general layout components.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide navigation buttons","description":"Adds options to hide buttons in the navigation bar.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide overlay filter","description":"Removes, at compile time, the dark overlay that appears when player flyout menus are open.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":false,"requiresIntegrations":false,"options":[]},{"name":"Hide player buttons","description":"Adds options to hide buttons in the video player.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide player flyout menu","description":"Adds options to hide player flyout menu components.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide player overlay filter","description":"Removes, at compile time, the dark overlay that appears when single-tapping in the player.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":false,"requiresIntegrations":false,"options":[]},{"name":"Hide recommended communities shelf","description":"Adds an option to hide the recommended communities shelves in subreddits.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hide shortcuts","description":"Remove, at compile time, the app shortcuts that appears when app icon is long pressed.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":false,"requiresIntegrations":false,"options":[{"key":"Explore","default":false,"values":null,"title":"Hide Explore","description":"Hide Explore from shortcuts.","required":true},{"key":"Subscriptions","default":false,"values":null,"title":"Hide Subscriptions","description":"Hide Subscriptions from shortcuts.","required":true},{"key":"Search","default":false,"values":null,"title":"Hide Search","description":"Hide Search from shortcuts.","required":true},{"key":"Shorts","default":true,"values":null,"title":"Hide Shorts","description":"Hide Shorts from shortcuts.","required":true}]},{"name":"Hook YouTube Music actions","description":"Adds support for opening music in RVX Music using the in-app YouTube Music button.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Hook download actions","description":"Adds support to download videos with an external downloader app using the in-app download button.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Layout switch","description":"Adds an option to spoof the dpi in order to use a tablet or phone layout.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"MaterialYou","description":"Applies the MaterialYou theme for Android 12+ devices.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":false,"requiresIntegrations":false,"options":[]},{"name":"Miniplayer","description":"Adds options to change the in app minimized player, and if patching target 19.16+ adds options to use modern miniplayers.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Navigation bar components","description":"Adds options to hide or change components related to the navigation bar.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Navigation bar components","description":"Adds options to hide or change components related to the navigation bar.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Open links directly","description":"Adds an option to skip over redirection URLs in external links.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Open links externally","description":"Adds an option to always open links in your browser instead of in the in-app-browser.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Overlay buttons","description":"Adds options to display overlay buttons in the video player.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[{"key":"IconType","default":"rounded","values":{"Bold":"bold","Rounded":"rounded","Thin":"thin"},"title":"Icon type","description":"The icon type.","required":true},{"key":"BottomMargin","default":"5.0dip","values":{"Wider":"10.0dip","Default":"5.0dip"},"title":"Bottom margin","description":"The bottom margin for the overlay buttons and timestamp.","required":true},{"key":"WiderButtonsSpace","default":false,"values":null,"title":"Wider between-buttons space","description":"Prevent adjacent button presses by increasing the horizontal spacing between buttons.","required":true},{"key":"ChangeTopButtons","default":false,"values":null,"title":"Change top buttons","description":"Change the icons at the top of the player.","required":true}]},{"name":"Player components","description":"Adds options to hide or change components related to the video player.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Player components","description":"Adds options to hide or change components related to the player.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Premium icon","description":"Unlocks premium app icons.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Remove background playback restrictions","description":"Removes restrictions on background playback, including for music and kids videos.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Remove background playback restrictions","description":"Removes restrictions on background playback, including for kids videos.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Remove subreddit dialog","description":"Adds options to remove the NSFW community warning and notifications suggestion dialogs by dismissing them automatically.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Remove viewer discretion dialog","description":"Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Remove viewer discretion dialog","description":"Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Restore old style library shelf","description":"Adds an option to return the Library tab to the old style.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Return YouTube Dislike","description":"Adds an option to show the dislike count of videos using the Return YouTube Dislike API.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Return YouTube Dislike","description":"Adds an option to show the dislike count of songs using the Return YouTube Dislike API.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Return YouTube Username","description":"Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Return YouTube Username","description":"Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Sanitize sharing links","description":"Adds an option to remove tracking query parameters from URLs when sharing links.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Sanitize sharing links","description":"Adds an option to remove tracking query parameters from URLs when sharing links.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Sanitize sharing links","description":"Adds an option to remove tracking query parameters from URLs when sharing links.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Seekbar components","description":"Adds options to hide or change components related to the seekbar.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[{"key":"CairoStartColor","default":"#ffff2791","values":null,"title":"Cairo start color","description":"Set Cairo start color for the seekbar.","required":false},{"key":"CairoEndColor","default":"#ffff0033","values":null,"title":"Cairo end color","description":"Set Cairo end color for the seekbar.","required":false}]},{"name":"Settings for Reddit","description":"Applies mandatory patches to implement ReVanced Extended settings into the application.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":true,"requiresIntegrations":true,"options":[{"key":"RVXSettingsMenuName","default":"ReVanced Extended","values":null,"title":"RVX settings menu name","description":"The name of the RVX settings menu.","required":true}]},{"name":"Settings for YouTube","description":"Applies mandatory patches to implement ReVanced Extended settings into the application.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":true,"options":[{"key":"InsertPosition","default":"About","values":{"Parent settings":"@string/parent_tools_key","General":"@string/general_key","Account":"@string/account_switcher_key","Data saving":"@string/data_saving_settings_key","Autoplay":"@string/auto_play_key","Video quality preferences":"@string/video_quality_settings_key","Background":"@string/offline_key","Watch on TV":"@string/pair_with_tv_key","Manage all history":"@string/history_key","Your data in YouTube":"@string/your_data_key","Privacy":"@string/privacy_key","History \u0026 privacy":"@string/privacy_key","Try experimental new features":"@string/premium_early_access_browse_page_key","Purchases and memberships":"@string/subscription_product_setting_key","Billing \u0026 payments":"@string/billing_and_payment_key","Billing and payments":"@string/billing_and_payment_key","Notifications":"@string/notification_key","Connected apps":"@string/connected_accounts_browse_page_key","Live chat":"@string/live_chat_key","Captions":"@string/captions_key","Accessibility":"@string/accessibility_settings_key","About":"@string/about_key"},"title":"Insert position","description":"The settings menu name that the RVX settings menu should be above.","required":true},{"key":"RVXSettingsMenuName","default":"ReVanced Extended","values":null,"title":"RVX settings menu name","description":"The name of the RVX settings menu.","required":true}]},{"name":"Settings for YouTube Music","description":"Applies mandatory patches to implement ReVanced Extended settings into the application.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":true,"options":[{"key":"RVXSettingsMenuName","default":"ReVanced Extended","values":null,"title":"RVX settings menu name","description":"The name of the RVX settings menu.","required":true}]},{"name":"Shorts components","description":"Adds options to hide or change components related to YouTube Shorts.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"SponsorBlock","description":"Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as sponsored content.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[{"key":"OutlineIcon","default":true,"values":null,"title":"Outline icons","description":"Apply the outline icon.","required":true},{"key":"NewSegmentAlignment","default":"right","values":{"Right":"right","Left":"left"},"title":"New segment alignment","description":"Align new segment window.","required":true}]},{"name":"SponsorBlock","description":"Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Spoof app version","description":"Adds options to spoof the YouTube client version. This can be used to restore old UI elements and features.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Spoof app version","description":"Adds options to spoof the YouTube Music client version. This can remove the radio mode restriction in Canadian regions or disable real-time lyrics.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Spoof streaming data","description":"Adds options to spoof the streaming data to allow video playback.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Spoof watch history","description":"Adds an option to change the domain of the watch history or check its status.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Swipe controls","description":"Adds options for controlling volume and brightness with swiping, and whether to enter fullscreen when swiping down below the player.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Theme","description":"Changes the app\u0027s theme to the values specified in options.json.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[{"key":"DarkThemeBackgroundColor","default":"@android:color/black","values":{"Amoled Black":"@android:color/black","Catppuccin (Mocha)":"#FF181825","Dark Pink":"#FF290025","Dark Blue":"#FF001029","Dark Green":"#FF002905","Dark Yellow":"#FF282900","Dark Orange":"#FF291800","Dark Red":"#FF290000"},"title":"Dark theme background color","description":"Can be a hex color (#AARRGGBB) or a color resource reference.","required":true},{"key":"LightThemeBackgroundColor","default":"@android:color/white","values":{"White":"@android:color/white","Catppuccin (Latte)":"#FFE6E9EF","Light Pink":"#FFFCCFF3","Light Blue":"#FFD1E0FF","Light Green":"#FFCCFFCC","Light Yellow":"#FFFDFFCC","Light Orange":"#FFFFE6CC","Light Red":"#FFFFD6D6"},"title":"Light theme background color","description":"Can be a hex color (#AARRGGBB) or a color resource reference.","required":true}]},{"name":"Toolbar components","description":"Adds options to hide or change components located on the toolbar, such as toolbar buttons, search bar, and header.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Translations for YouTube","description":"Add translations or remove string resources.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[{"key":"CustomTranslation","default":"","values":null,"title":"Custom translation","description":"The path to the \u0027strings.xml\u0027 file.\nPlease note that applying the \u0027strings.xml\u0027 file will overwrite all existing translations.","required":true},{"key":"SelectedTranslations","default":"ar, bg-rBG, de-rDE, el-rGR, es-rES, fr-rFR, hu-rHU, it-rIT, ja-rJP, ko-rKR, pl-rPL, pt-rBR, ru-rRU, tr-rTR, uk-rUA, vi-rVN, zh-rCN, zh-rTW","values":null,"title":"Translations to add","description":"A list of translations to be added for the RVX settings, separated by commas.","required":true},{"key":"SelectedStringResources","default":"af, am, ar, ar-rXB, as, az, b+es+419, b+sr+Latn, be, bg, bn, bs, ca, cs, da, de, el, en-rAU, en-rCA, en-rGB, en-rIN, en-rXA, en-rXC, es, es-rUS, et, eu, fa, fi, fr, fr-rCA, gl, gu, hi, hr, hu, hy, id, in, is, it, iw, ja, ka, kk, km, kn, ko, ky, lo, lt, lv, mk, ml, mn, mr, ms, my, nb, ne, nl, no, or, pa, pl, pt, pt-rBR, pt-rPT, ro, ru, si, sk, sl, sq, sr, sv, sw, ta, te, th, tl, tr, uk, ur, uz, vi, zh, zh-rCN, zh-rHK, zh-rTW, zu","values":null,"title":"String resources to keep","description":"A list of string resources to be kept, separated by commas.\nString resources not in the list will be removed from the app.\n\nDefault string resource, English, is not removed.","required":true}]},{"name":"Translations for YouTube Music","description":"Add translations or remove string resources.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[{"key":"CustomTranslation","default":"","values":null,"title":"Custom translation","description":"The path to the \u0027strings.xml\u0027 file.\nPlease note that applying the \u0027strings.xml\u0027 file will overwrite all existing language translations.","required":true},{"key":"SelectedTranslations","default":"bg-rBG, bn, cs-rCZ, el-rGR, es-rES, fr-rFR, hu-rHU, id-rID, in, it-rIT, ja-rJP, ko-rKR, nl-rNL, pl-rPL, pt-rBR, ro-rRO, ru-rRU, tr-rTR, uk-rUA, vi-rVN, zh-rCN, zh-rTW","values":null,"title":"Translations to add","description":"A list of translations to be added for the RVX settings, separated by commas.","required":true},{"key":"SelectedStringResources","default":"af, am, ar, ar-rXB, as, az, b+es+419, b+sr+Latn, be, bg, bn, bs, ca, cs, da, de, el, en-rAU, en-rCA, en-rGB, en-rIN, en-rXA, en-rXC, es, es-rUS, et, eu, fa, fi, fr, fr-rCA, gl, gu, hi, hr, hu, hy, id, in, is, it, iw, ja, ka, kk, km, kn, ko, ky, lo, lt, lv, mk, ml, mn, mr, ms, my, nb, ne, nl, no, or, pa, pl, pt, pt-rBR, pt-rPT, ro, ru, si, sk, sl, sq, sr, sv, sw, ta, te, th, tl, tr, uk, ur, uz, vi, zh, zh-rCN, zh-rHK, zh-rTW, zu","values":null,"title":"String resources to keep","description":"A list of string resources to be kept, separated by commas.\nString resources not in the list will be removed from the app.\n\nDefault string resource, English, is not removed.","required":true}]},{"name":"Video playback","description":"Adds options to customize settings related to video playback, such as default video quality and playback speed.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Video playback","description":"Adds options to customize settings related to video playback, such as default video quality and playback speed.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[]},{"name":"Visual preferences icons for YouTube","description":"Adds icons to specific preferences in the settings.","compatiblePackages":[{"name":"com.google.android.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"requiresIntegrations":false,"options":[{"key":"ExtendedIcon","default":"Extension","values":{"Custom branding icon":"custom_branding_icon","Extension":"extension","Gear":"gear","ReVanced":"revanced","ReVanced Colored":"revanced_colored","RVX Letters":"rvx_letters","RVX Letters Bold":"rvx_letters_bold","YT Alt":"yt_alt"},"title":"RVX settings menu icon","description":"The icon for the RVX settings menu.","required":true}]},{"name":"Visual preferences icons for YouTube Music","description":"Adds icons to specific preferences in the settings.","compatiblePackages":[{"name":"com.google.android.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"requiresIntegrations":false,"options":[{"key":"ExtendedIcon","default":"Extension","values":{"Custom branding icon":"custom_branding_icon","Extension":"extension","Gear":"gear","ReVanced":"revanced","ReVanced Colored":"revanced_colored"},"title":"Extended icon","description":"Apply different icons for Extended preference.","required":false}]}] \ No newline at end of file +[ + { + "name": "Alternative thumbnails", + "description": "Adds options to replace video thumbnails using the DeArrow API or image captures from the video.", + "use": true, + "dependencies": [ + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Ambient mode control", + "description": "Adds options to disable Ambient mode and to bypass Ambient mode restrictions.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Amoled", + "description": "Applies a pure black theme to some components.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Bitrate default value", + "description": "Sets the audio quality to \u0027Always High\u0027 when you first install the app.", + "use": true, + "dependencies": [ + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Bypass image region restrictions", + "description": "Adds an option to use a different host for static images, so that images blocked in some countries can be received.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Bypass image region restrictions", + "description": "Adds an option to use a different host for static images, so that images blocked in some countries can be received.", + "use": true, + "dependencies": [ + "Settings for YouTube Music", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Certificate spoof", + "description": "Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate.", + "use": true, + "dependencies": [], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Change package name", + "description": "Changes the package name for Reddit to the name specified in patch options.", + "use": false, + "dependencies": [], + "compatiblePackages": { + "com.reddit.frontpage": null + }, + "options": [ + { + "key": "packageNameReddit", + "title": "Package name of Reddit", + "description": "The name of the package to rename the app to.", + "required": true, + "type": "kotlin.String", + "default": "com.reddit.frontpage", + "values": { + "Clone": "com.reddit.frontpage.revanced", + "Default": "com.reddit.frontpage.rvx", + "Original": "com.reddit.frontpage" + } + } + ] + }, + { + "name": "Change player flyout menu toggles", + "description": "Adds an option to use text toggles instead of switch toggles within the additional settings menu.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Change share sheet", + "description": "Add option to change from in-app share sheet to system share sheet.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "BytecodePatch", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Change share sheet", + "description": "Add option to change from in-app share sheet to system share sheet.", + "use": true, + "dependencies": [ + "Settings for YouTube Music", + "BytecodePatch", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Change start page", + "description": "Adds an option to set which page the app opens in instead of the homepage.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Change start page", + "description": "Adds an option to set which page the app opens in instead of the homepage.", + "use": true, + "dependencies": [ + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Change version code", + "description": "Changes the version code of the app to the value specified in patch options. Except when mounting, this can prevent app stores from updating the app and allow the app to be installed over an existing installation that has a higher version code. By default, the highest version code is set.", + "use": false, + "dependencies": [], + "compatiblePackages": null, + "options": [ + { + "key": "versionCode", + "title": "Version code", + "description": "The version code to use. (1 ~ 2147483647)", + "required": true, + "type": "kotlin.String", + "default": "2147483647", + "values": { + "Lowest": "1", + "Highest": "2147483647" + } + } + ] + }, + { + "name": "Custom Shorts action buttons", + "description": "Changes, at compile time, the icon of the action buttons of the Shorts player.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "iconType", + "title": "Shorts icon style ", + "description": "The style of the icons for the action buttons in the Shorts player.", + "required": true, + "type": "kotlin.String", + "default": "cairo", + "values": { + "Cairo": "cairo", + "Outline": "outline", + "OutlineCircle": "outlinecircle", + "Round": "round", + "YoutubeOutline": "youtubeoutline", + "YouTube": "youtube" + } + } + ] + }, + { + "name": "Custom branding icon for YouTube", + "description": "Changes the YouTube app icon to the icon specified in patch options.", + "use": false, + "dependencies": [ + "Settings for YouTube", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "appIcon", + "title": "App icon", + "description": "The icon to apply to the app.\n\nIf a path to a folder is provided, the folder must contain the following folders:\n\n- mipmap-xxxhdpi\n- mipmap-xxhdpi\n- mipmap-xhdpi\n- mipmap-hdpi\n- mipmap-mdpi\n\nEach of these folders must contain the following files:\n\n- adaptiveproduct_youtube_background_color_108.png\n- adaptiveproduct_youtube_foreground_color_108.png\n- ic_launcher.png\n- ic_launcher_round.png", + "required": true, + "type": "kotlin.String", + "default": "xisr_yellow", + "values": { + "AFN Blue": "afn_blue", + "AFN Red": "afn_red", + "MMT": "mmt", + "MMT Blue": "mmt_blue", + "MMT Green": "mmt_green", + "MMT Orange": "mmt_orange", + "MMT Pink": "mmt_pink", + "MMT Turquoise": "mmt_turquoise", + "MMT Yellow": "mmt_yellow", + "Revancify Blue": "revancify_blue", + "Revancify Red": "revancify_red", + "Vanced Black": "vanced_black", + "Vanced Light": "vanced_light", + "Xisr Yellow": "xisr_yellow", + "YouTube": "youtube", + "YouTube Black": "youtube_black" + } + }, + { + "key": "changeSplashIcon", + "title": "Change splash icons", + "description": "Apply the custom branding icon to the splash screen.", + "required": true, + "type": "kotlin.Boolean", + "default": true, + "values": null + }, + { + "key": "restoreOldSplashAnimation", + "title": "Restore old splash animation", + "description": "Restore the old style splash animation.", + "required": true, + "type": "kotlin.Boolean", + "default": true, + "values": null + } + ] + }, + { + "name": "Custom branding icon for YouTube Music", + "description": "Changes the YouTube Music app icon to the icon specified in patch options.", + "use": false, + "dependencies": [ + "Settings for YouTube Music", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [ + { + "key": "appIcon", + "title": "App icon", + "description": "The icon to apply to the app.\n\nIf a path to a folder is provided, the folder must contain the following folders:\n\n- mipmap-xxxhdpi\n- mipmap-xxhdpi\n- mipmap-xhdpi\n- mipmap-hdpi\n- mipmap-mdpi\n\nEach of these folders must contain the following files:\n\n- adaptiveproduct_youtube_music_background_color_108.png\n- adaptiveproduct_youtube_music_foreground_color_108.png\n- ic_launcher_release.png", + "required": true, + "type": "kotlin.String", + "default": "xisr_yellow", + "values": { + "AFN Blue": "afn_blue", + "AFN Red": "afn_red", + "MMT": "mmt", + "MMT Blue": "mmt_blue", + "MMT Green": "mmt_green", + "MMT Orange": "mmt_orange", + "MMT Pink": "mmt_pink", + "MMT Turquoise": "mmt_turquoise", + "MMT Yellow": "mmt_yellow", + "Revancify Blue": "revancify_blue", + "Revancify Red": "revancify_red", + "Vanced Black": "vanced_black", + "Vanced Light": "vanced_light", + "Xisr Yellow": "xisr_yellow", + "YouTube Music": "youtube_music" + } + }, + { + "key": "changeSplashIcon", + "title": "Change splash icons", + "description": "Apply the custom branding icon to the splash screen.", + "required": true, + "type": "kotlin.Boolean", + "default": true, + "values": null + }, + { + "key": "restoreOldSplashIcon", + "title": "Restore old splash icon", + "description": "Restore the old style splash icon.\n\nIf you enable both the old style splash icon and the Cairo splash animation,\n\nOld style splash icon will appear first and then the Cairo splash animation will start.", + "required": true, + "type": "kotlin.Boolean", + "default": false, + "values": null + } + ] + }, + { + "name": "Custom branding name for Reddit", + "description": "Renames the Reddit app to the name specified in patch options.", + "use": false, + "dependencies": [], + "compatiblePackages": { + "com.reddit.frontpage": null + }, + "options": [ + { + "key": "appName", + "title": "App name", + "description": "The name of the app.", + "required": true, + "type": "kotlin.String", + "default": "Reddit", + "values": { + "Default": "RVX Reddit", + "Original": "Reddit" + } + } + ] + }, + { + "name": "Custom branding name for YouTube", + "description": "Renames the YouTube app to the name specified in patch options.", + "use": false, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "appName", + "title": "App name", + "description": "The name of the app.", + "required": true, + "type": "kotlin.String", + "default": "RVX", + "values": { + "ReVanced Extended": "ReVanced Extended", + "RVX": "RVX", + "YouTube RVX": "YouTube RVX", + "YouTube": "YouTube" + } + } + ] + }, + { + "name": "Custom branding name for YouTube Music", + "description": "Renames the YouTube Music app to the name specified in patch options.", + "use": false, + "dependencies": [ + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [ + { + "key": "appNameNotification", + "title": "App name in notification panel", + "description": "The name of the app as it appears in the notification panel.", + "required": true, + "type": "kotlin.String", + "default": "RVX Music", + "values": { + "ReVanced Extended Music": "ReVanced Extended Music", + "RVX Music": "RVX Music", + "YouTube Music": "YouTube Music", + "YT Music": "YT Music" + } + }, + { + "key": "appNameLauncher", + "title": "App name in launcher", + "description": "The name of the app as it appears in the launcher.", + "required": true, + "type": "kotlin.String", + "default": "RVX Music", + "values": { + "ReVanced Extended Music": "ReVanced Extended Music", + "RVX Music": "RVX Music", + "YouTube Music": "YouTube Music", + "YT Music": "YT Music" + } + } + ] + }, + { + "name": "Custom double tap length", + "description": "Adds Double-tap to seek values that are specified in patch options.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "doubleTapLengthArrays", + "title": "Double-tap to seek values", + "description": "A list of custom Double-tap to seek lengths to be added, separated by commas.", + "required": true, + "type": "kotlin.String", + "default": "3, 5, 10, 15, 20, 30, 60, 120, 180", + "values": null + } + ] + }, + { + "name": "Custom header for YouTube", + "description": "Applies a custom header in the top left corner within the app.", + "use": false, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "customHeader", + "title": "Custom header", + "description": "The header to apply to the app.\n\nPatch option \u0027Custom branding icon\u0027 applies only when:\n\n1. Patch \u0027Custom branding icon for YouTube\u0027 is included.\n2. Patch option for \u0027Custom branding icon for YouTube\u0027 is selected from the preset.\n\nIf a path to a folder is provided, the folder must contain one or more of the following folders, depending on the DPI of the device:\n\n- drawable-xxxhdpi\n- drawable-xxhdpi\n- drawable-xhdpi\n- drawable-hdpi\n- drawable-mdpi\n\nEach of the folders must contain all of the following files:\n\n[Generic header]\n\n- yt_wordmark_header_light.png\n- yt_wordmark_header_dark.png\n\nThe image dimensions must be as follows:\n\n- drawable-xxxhdpi: 488px x 192px\n- drawable-xxhdpi: 366px x 144px\n- drawable-xhdpi: 244px x 96px\n- drawable-hdpi: 184px x 72px\n- drawable-mdpi: 122px x 48px\n\n[Premium header]\n\n- yt_premium_wordmark_header_light.png\n- yt_premium_wordmark_header_dark.png\n\nThe image dimensions must be as follows:\n- drawable-xxxhdpi: 516px x 192px\n- drawable-xxhdpi: 387px x 144px\n- drawable-xhdpi: 258px x 96px\n- drawable-hdpi: 194px x 72px\n- drawable-mdpi: 129px x 48px", + "required": true, + "type": "kotlin.String", + "default": "custom_branding_icon", + "values": { + "Custom branding icon": "custom_branding_icon" + } + } + ] + }, + { + "name": "Custom header for YouTube Music", + "description": "Applies a custom header in the top left corner within the app.", + "use": false, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [ + { + "key": "customHeader", + "title": "Custom header", + "description": "The header to apply to the app.\n\nPatch option \u0027Custom branding icon\u0027 applies only when:\n\n1. Patch \u0027Custom branding icon for YouTube Music\u0027 is included.\n2. Patch option for \u0027Custom branding icon for YouTube Music\u0027 is selected from the preset.\n\nIf a path to a folder is provided, the folder must contain one or more of the following folders, depending on the DPI of the device:\n\n- drawable-xxxhdpi\n- drawable-xxhdpi\n- drawable-xhdpi\n- drawable-hdpi\n- drawable-mdpi\n\nEach of the folders must contain all of the following files:\n\n- action_bar_logo.png\n- logo_music.png\n- ytm_logo.png\n\nThe image \u0027action_bar_logo.png\u0027 dimensions must be as follows:\n\n- drawable-xxxhdpi: 320px x 96px\n- drawable-xxhdpi: 240px x 72px\n- drawable-xhdpi: 160px x 48px\n- drawable-hdpi: 121px x 36px\n- drawable-mdpi: 80px x 24px\n\nThe image \u0027logo_music.png\u0027 dimensions must be as follows:\n\n- drawable-xxxhdpi: 576px x 200px\n- drawable-xxhdpi: 432px x 150px\n- drawable-xhdpi: 288px x 100px\n- drawable-hdpi: 217px x 76px\n- drawable-mdpi: 144px x 50px\n\nThe image \u0027ytm_logo.png\u0027 dimensions must be as follows:\n\n- drawable-xxxhdpi: 412px x 144px\n- drawable-xxhdpi: 309px x 108px\n- drawable-xhdpi: 206px x 72px\n- drawable-hdpi: 155px x 54px\n- drawable-mdpi: 103px x 36px", + "required": true, + "type": "kotlin.String", + "default": "custom_branding_icon", + "values": { + "Custom branding icon": "custom_branding_icon" + } + } + ] + }, + { + "name": "Description components", + "description": "Adds options to hide and disable description components.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "ResourcePatch", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Disable Cairo splash animation", + "description": "Adds an option to disable Cairo splash animation.", + "use": true, + "dependencies": [ + "Settings for YouTube Music", + "ResourcePatch", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "7.06.54", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Disable DRC audio", + "description": "Adds an option to disable DRC (Dynamic Range Compression) audio.", + "use": true, + "dependencies": [ + "Settings for YouTube Music", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Disable QUIC protocol", + "description": "Adds an option to disable CronetEngine\u0027s QUIC protocol.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Disable auto audio tracks", + "description": "Adds an option to disable audio tracks from being automatically enabled.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Disable auto captions", + "description": "Adds an option to disable captions from being automatically enabled.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Disable auto captions", + "description": "Adds an option to disable captions from being automatically enabled.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Disable dislike redirection", + "description": "Adds an option to disable redirection to the next track when clicking the Dislike button.", + "use": true, + "dependencies": [ + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Disable haptic feedback", + "description": "Adds options to disable haptic feedback when swiping in the video player.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Disable resuming Shorts on startup", + "description": "Adds an option to disable the Shorts player from resuming on app startup when Shorts were last being watched.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Disable screenshot popup", + "description": "Adds an option to disable the popup that appears when taking a screenshot.", + "use": true, + "dependencies": [ + "Settings for Reddit" + ], + "compatiblePackages": { + "com.reddit.frontpage": null + }, + "options": [] + }, + { + "name": "Disable splash animation", + "description": "Adds an option to disable the splash animation on app startup.", + "use": true, + "dependencies": [ + "ResourcePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Enable OPUS codec", + "description": "Adds an options to enable the OPUS audio codec if the player response includes.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Enable OPUS codec", + "description": "Adds an options to enable the OPUS audio codec if the player response includes.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Enable debug logging", + "description": "Adds an option to enable debug logging.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Enable debug logging", + "description": "Adds an option to enable debug logging.", + "use": true, + "dependencies": [ + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Enable external browser", + "description": "Adds an option to always open links in your browser instead of in the in-app-browser.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Enable gradient loading screen", + "description": "Adds an option to enable the gradient loading screen.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Enable landscape mode", + "description": "Adds an option to enable landscape mode when rotating the screen on phones.", + "use": true, + "dependencies": [ + "ResourcePatch", + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Enable open links directly", + "description": "Adds an option to skip over redirection URLs in external links.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Flyout menu components", + "description": "Adds options to hide or change flyout menu components.", + "use": true, + "dependencies": [ + "Settings for YouTube Music", + "ResourcePatch", + "BytecodePatch", + "BytecodePatch", + "ResourcePatch", + "ResourcePatch", + "BytecodePatch", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Force player buttons background", + "description": "Changes the dark background surrounding the video player controls at compile time.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "BackgroundColor", + "title": "Background color", + "description": "Specify a background color for player buttons using a hex color code. The first two symbols of the hex code represent the alpha channel, which is used to change the opacity.", + "required": false, + "type": "kotlin.String", + "default": "?ytOverlayBackgroundMediumLight", + "values": { + "Default": "?ytOverlayBackgroundMediumLight", + "Transparent": "@android:color/transparent", + "Opacity10": "#1a000000", + "Opacity20": "#33000000", + "Opacity30": "#4d000000", + "Opacity40": "#66000000", + "Opacity50": "#80000000", + "Opacity60": "#99000000", + "Opacity70": "#b3000000", + "Opacity80": "#cc000000", + "Opacity90": "#e6000000", + "Opacity100": "#ff000000" + } + } + ] + }, + { + "name": "Force snackbar theme", + "description": "Changes snackbar background color to match selected theme at compile time.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "cornerRadius", + "title": "Corner radius", + "description": "Specify a corner radius for the snackbar.", + "required": false, + "type": "kotlin.String", + "default": "8.0dip", + "values": null + }, + { + "key": "backgroundColor", + "title": "Background color", + "description": "Specify a background color for the snackbar. You can specify hex color.", + "required": false, + "type": "kotlin.String", + "default": "?ytChipBackground", + "values": { + "Chip": "?ytChipBackground", + "Base": "?ytBaseBackground" + } + }, + { + "key": "strokeColor", + "title": "Stroke color", + "description": "Specify a stroke color for the snackbar. You can specify hex color.", + "required": false, + "type": "kotlin.String", + "default": "none", + "values": { + "None": "none", + "Accent": "?attr/colorAccent", + "Inverted": "?attr/ytInvertedBackground" + } + } + ] + }, + { + "name": "Fullscreen components", + "description": "Adds options to hide or change components related to fullscreen.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "BytecodePatch", + "BytecodePatch", + "ResourcePatch", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "GmsCore support", + "description": "Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services.", + "use": true, + "dependencies": [ + "ResourcePatch", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "gmsCoreVendorGroupId", + "title": "GmsCore vendor group ID", + "description": "The vendor\u0027s group ID for GmsCore.", + "required": true, + "type": "kotlin.String", + "default": "app.revanced", + "values": { + "ReVanced": "app.revanced" + } + }, + { + "key": "checkGmsCore", + "title": "Check GmsCore", + "description": "Check if GmsCore is installed on the device and has battery optimizations disabled when the app starts. \n\nIf GmsCore is not installed the app will not work, so disabling this is not recommended.", + "required": true, + "type": "kotlin.Boolean", + "default": true, + "values": null + }, + { + "key": "packageNameYouTube", + "title": "Package name of YouTube", + "description": "The name of the package to use in GmsCore support.", + "required": true, + "type": "kotlin.String", + "default": "anddea.youtube", + "values": { + "Clone": "bill.youtube", + "Default": "anddea.youtube" + } + }, + { + "key": "packageNameYouTubeMusic", + "title": "Package name of YouTube Music", + "description": "The name of the package to use in GmsCore support.", + "required": true, + "type": "kotlin.String", + "default": "anddea.youtube.music", + "values": { + "Clone": "bill.youtube.music", + "Default": "anddea.youtube.music" + } + } + ] + }, + { + "name": "GmsCore support", + "description": "Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services.", + "use": true, + "dependencies": [ + "ResourcePatch", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [ + { + "key": "gmsCoreVendorGroupId", + "title": "GmsCore vendor group ID", + "description": "The vendor\u0027s group ID for GmsCore.", + "required": true, + "type": "kotlin.String", + "default": "app.revanced", + "values": { + "ReVanced": "app.revanced" + } + }, + { + "key": "checkGmsCore", + "title": "Check GmsCore", + "description": "Check if GmsCore is installed on the device and has battery optimizations disabled when the app starts. \n\nIf GmsCore is not installed the app will not work, so disabling this is not recommended.", + "required": true, + "type": "kotlin.Boolean", + "default": true, + "values": null + }, + { + "key": "packageNameYouTube", + "title": "Package name of YouTube", + "description": "The name of the package to use in GmsCore support.", + "required": true, + "type": "kotlin.String", + "default": "anddea.youtube", + "values": { + "Clone": "bill.youtube", + "Default": "anddea.youtube" + } + }, + { + "key": "packageNameYouTubeMusic", + "title": "Package name of YouTube Music", + "description": "The name of the package to use in GmsCore support.", + "required": true, + "type": "kotlin.String", + "default": "anddea.youtube.music", + "values": { + "Clone": "bill.youtube.music", + "Default": "anddea.youtube.music" + } + } + ] + }, + { + "name": "Hide Recently Visited shelf", + "description": "Adds an option to hide the Recently Visited shelf in the sidebar.", + "use": true, + "dependencies": [ + "Settings for Reddit" + ], + "compatiblePackages": { + "com.reddit.frontpage": null + }, + "options": [] + }, + { + "name": "Hide Shorts dimming", + "description": "Removes, at compile time, the dimming effect at the top and bottom of Shorts videos.", + "use": false, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Hide account components", + "description": "Adds options to hide components related to the account menu.", + "use": true, + "dependencies": [ + "ResourcePatch", + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Hide action bar components", + "description": "Adds options to hide action bar components and replace the offline download button with an external download button.", + "use": true, + "dependencies": [ + "Settings for YouTube Music", + "BytecodePatch", + "ResourcePatch", + "BytecodePatch", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Hide action buttons", + "description": "Adds options to hide action buttons under videos.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Hide ads", + "description": "Adds options to hide ads.", + "use": true, + "dependencies": [ + "Settings for Reddit", + "ResourcePatch", + "BytecodePatch" + ], + "compatiblePackages": { + "com.reddit.frontpage": null + }, + "options": [] + }, + { + "name": "Hide ads", + "description": "Adds options to hide ads.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "ResourcePatch", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Hide ads", + "description": "Adds options to hide ads.", + "use": true, + "dependencies": [ + "Settings for YouTube Music", + "BytecodePatch", + "BytecodePatch", + "Navigation bar components", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Hide comments components", + "description": "Adds options to hide components related to comments.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "BytecodePatch", + "BytecodePatch", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Hide feed components", + "description": "Adds options to hide components related to feeds.", + "use": true, + "dependencies": [ + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "ResourcePatch", + "Settings for YouTube", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Hide feed flyout menu", + "description": "Adds the ability to hide feed flyout menu components using a custom filter.", + "use": true, + "dependencies": [ + "ResourcePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Hide layout components", + "description": "Adds options to hide general layout components.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "BytecodePatch", + "ResourcePatch", + "BytecodePatch", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Hide layout components", + "description": "Adds options to hide general layout components.", + "use": true, + "dependencies": [ + "Settings for YouTube Music", + "BytecodePatch", + "ResourcePatch", + "BytecodePatch", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Hide navigation buttons", + "description": "Adds options to hide buttons in the navigation bar.", + "use": false, + "dependencies": [ + "Settings for Reddit" + ], + "compatiblePackages": { + "com.reddit.frontpage": null + }, + "options": [] + }, + { + "name": "Hide overlay filter", + "description": "Removes, at compile time, the dark overlay that appears when player flyout menus are open.", + "use": false, + "dependencies": [ + "Settings for YouTube Music", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Hide player buttons", + "description": "Adds options to hide buttons in the video player.", + "use": true, + "dependencies": [ + "BytecodePatch", + "BytecodePatch", + "ResourcePatch", + "Settings for YouTube", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Hide player flyout menu", + "description": "Adds options to hide player flyout menu components.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "BytecodePatch", + "BytecodePatch", + "ResourcePatch", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Hide player overlay filter", + "description": "Removes, at compile time, the dark overlay that appears when single-tapping in the player.", + "use": false, + "dependencies": [], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Hide recommended communities shelf", + "description": "Adds an option to hide the recommended communities shelves in subreddits.", + "use": true, + "dependencies": [ + "Settings for Reddit" + ], + "compatiblePackages": { + "com.reddit.frontpage": null + }, + "options": [] + }, + { + "name": "Hide shortcuts", + "description": "Remove, at compile time, the app shortcuts that appears when app icon is long pressed.", + "use": false, + "dependencies": [ + "Settings for YouTube", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "explore", + "title": "Hide Explore", + "description": "Hide Explore from shortcuts.", + "required": true, + "type": "kotlin.Boolean", + "default": false, + "values": null + }, + { + "key": "subscriptions", + "title": "Hide Subscriptions", + "description": "Hide Subscriptions from shortcuts.", + "required": true, + "type": "kotlin.Boolean", + "default": false, + "values": null + }, + { + "key": "search", + "title": "Hide Search", + "description": "Hide Search from shortcuts.", + "required": true, + "type": "kotlin.Boolean", + "default": false, + "values": null + }, + { + "key": "shorts", + "title": "Hide Shorts", + "description": "Hide Shorts from shortcuts.", + "required": true, + "type": "kotlin.Boolean", + "default": true, + "values": null + } + ] + }, + { + "name": "Hook YouTube Music actions", + "description": "Adds support for opening music in RVX Music using the in-app YouTube Music button.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Hook download actions", + "description": "Adds support to download videos with an external downloader app using the in-app download button.", + "use": true, + "dependencies": [ + "BytecodePatch", + "ResourcePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Layout switch", + "description": "Adds an option to spoof the dpi in order to use a tablet or phone layout.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "MaterialYou", + "description": "Applies the MaterialYou theme for Android 12+ devices.", + "use": false, + "dependencies": [ + "ResourcePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Miniplayer", + "description": "Adds options to change the in app minimized player, and if patching target 19.16+ adds options to use modern miniplayers.", + "use": true, + "dependencies": [ + "ResourcePatch", + "Settings for YouTube", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Navigation bar components", + "description": "Adds options to hide or change components related to the navigation bar.", + "use": true, + "dependencies": [ + "ResourcePatch", + "Settings for YouTube", + "ResourcePatch", + "BytecodePatch", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Navigation bar components", + "description": "Adds options to hide or change components related to the navigation bar.", + "use": true, + "dependencies": [ + "ResourcePatch", + "ResourcePatch", + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Open links directly", + "description": "Adds an option to skip over redirection URLs in external links.", + "use": true, + "dependencies": [ + "Settings for Reddit", + "BytecodePatch" + ], + "compatiblePackages": { + "com.reddit.frontpage": null + }, + "options": [] + }, + { + "name": "Open links externally", + "description": "Adds an option to always open links in your browser instead of in the in-app-browser.", + "use": true, + "dependencies": [ + "Settings for Reddit", + "BytecodePatch" + ], + "compatiblePackages": { + "com.reddit.frontpage": null + }, + "options": [] + }, + { + "name": "Overlay buttons", + "description": "Adds options to display overlay buttons in the video player.", + "use": true, + "dependencies": [ + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "ResourcePatch", + "ResourcePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "iconType", + "title": "Icon type", + "description": "The icon type.", + "required": true, + "type": "kotlin.String", + "default": "rounded", + "values": { + "Bold": "bold", + "Rounded": "rounded", + "Thin": "thin" + } + }, + { + "key": "bottomMargin", + "title": "Bottom margin", + "description": "The bottom margin for the overlay buttons and timestamp.", + "required": true, + "type": "kotlin.String", + "default": "2.5dip", + "values": { + "Default": "2.5dip", + "None": "0.0dip", + "Wider": "5.0dip" + } + }, + { + "key": "widerButtonsSpace", + "title": "Wider between-buttons space", + "description": "Prevent adjacent button presses by increasing the horizontal spacing between buttons.", + "required": true, + "type": "kotlin.Boolean", + "default": false, + "values": null + }, + { + "key": "changeTopButtons", + "title": "Change top buttons", + "description": "Change the icons at the top of the player.", + "required": true, + "type": "kotlin.Boolean", + "default": false, + "values": null + } + ] + }, + { + "name": "Player components", + "description": "Adds options to hide or change components related to the video player.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "ResourcePatch", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Player components", + "description": "Adds options to hide or change components related to the player.", + "use": true, + "dependencies": [ + "Settings for YouTube Music", + "ResourcePatch", + "ResourcePatch", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Premium icon", + "description": "Unlocks premium app icons.", + "use": true, + "dependencies": [], + "compatiblePackages": { + "com.reddit.frontpage": null + }, + "options": [] + }, + { + "name": "Remove background playback restrictions", + "description": "Removes restrictions on background playback, including for music and kids videos.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Remove background playback restrictions", + "description": "Removes restrictions on background playback, including for kids videos.", + "use": true, + "dependencies": [ + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Remove subreddit dialog", + "description": "Adds options to remove the NSFW community warning and notifications suggestion dialogs by dismissing them automatically.", + "use": true, + "dependencies": [ + "Settings for Reddit" + ], + "compatiblePackages": { + "com.reddit.frontpage": null + }, + "options": [] + }, + { + "name": "Remove viewer discretion dialog", + "description": "Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Remove viewer discretion dialog", + "description": "Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Restore old style library shelf", + "description": "Adds an option to return the Library tab to the old style.", + "use": true, + "dependencies": [ + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Return YouTube Dislike", + "description": "Adds an option to show the dislike count of videos using the Return YouTube Dislike API.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Return YouTube Dislike", + "description": "Adds an option to show the dislike count of songs using the Return YouTube Dislike API.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Return YouTube Username", + "description": "Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Return YouTube Username", + "description": "Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube Music", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Sanitize sharing links", + "description": "Adds an option to remove tracking query parameters from URLs when sharing links.", + "use": true, + "dependencies": [ + "Settings for Reddit" + ], + "compatiblePackages": { + "com.reddit.frontpage": null + }, + "options": [] + }, + { + "name": "Sanitize sharing links", + "description": "Adds an option to remove tracking query parameters from URLs when sharing links.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Sanitize sharing links", + "description": "Adds an option to remove tracking query parameters from URLs when sharing links.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Seekbar components", + "description": "Adds options to hide or change components related to the seekbar.", + "use": true, + "dependencies": [ + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "ResourcePatch", + "Settings for YouTube", + "BytecodePatch", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "cairoStartColor", + "title": "Cairo start color", + "description": "Set Cairo start color for the seekbar.", + "required": false, + "type": "kotlin.String", + "default": "#FFFF2791", + "values": null + }, + { + "key": "cairoEndColor", + "title": "Cairo end color", + "description": "Set Cairo end color for the seekbar.", + "required": false, + "type": "kotlin.String", + "default": "#FFFF0033", + "values": null + } + ] + }, + { + "name": "Settings for Reddit", + "description": "Applies mandatory patches to implement ReVanced Extended settings into the application.", + "use": true, + "dependencies": [ + "BytecodePatch", + "BytecodePatch" + ], + "compatiblePackages": { + "com.reddit.frontpage": null + }, + "options": [ + { + "key": "settingsLabel", + "title": "RVX settings menu name", + "description": "The name of the RVX settings menu.", + "required": true, + "type": "kotlin.String", + "default": "ReVanced Extended", + "values": null + } + ] + }, + { + "name": "Settings for YouTube", + "description": "Applies mandatory patches to implement ReVanced Extended settings into the application.", + "use": true, + "dependencies": [ + "BytecodePatch", + "BytecodePatch", + "ResourcePatch", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "insertPosition", + "title": "Insert position", + "description": "The settings menu name that the RVX settings menu should be above.", + "required": true, + "type": "kotlin.String", + "default": "@string/about_key", + "values": { + "Parent settings": "@string/parent_tools_key", + "General": "@string/general_key", + "Account": "@string/account_switcher_key", + "Data saving": "@string/data_saving_settings_key", + "Autoplay": "@string/auto_play_key", + "Video quality preferences": "@string/video_quality_settings_key", + "Background": "@string/offline_key", + "Watch on TV": "@string/pair_with_tv_key", + "Manage all history": "@string/history_key", + "Your data in YouTube": "@string/your_data_key", + "Privacy": "@string/privacy_key", + "History \u0026 privacy": "@string/privacy_key", + "Try experimental new features": "@string/premium_early_access_browse_page_key", + "Purchases and memberships": "@string/subscription_product_setting_key", + "Billing \u0026 payments": "@string/billing_and_payment_key", + "Billing and payments": "@string/billing_and_payment_key", + "Notifications": "@string/notification_key", + "Connected apps": "@string/connected_accounts_browse_page_key", + "Live chat": "@string/live_chat_key", + "Captions": "@string/captions_key", + "Accessibility": "@string/accessibility_settings_key", + "About": "@string/about_key" + } + }, + { + "key": "settingsLabel", + "title": "RVX settings label", + "description": "The name of the RVX settings menu.", + "required": true, + "type": "kotlin.String", + "default": "ReVanced Extended", + "values": null + }, + { + "key": "settingsSummaries", + "title": "RVX settings summaries", + "description": "Shows the summary / description of each RVX setting. If set to false, no descriptions will be provided.", + "required": true, + "type": "kotlin.Boolean", + "default": true, + "values": null + } + ] + }, + { + "name": "Settings for YouTube Music", + "description": "Applies mandatory patches to implement ReVanced Extended settings into the application.", + "use": true, + "dependencies": [ + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [ + { + "key": "settingsLabel", + "title": "RVX settings label", + "description": "The name of the RVX settings menu.", + "required": true, + "type": "kotlin.String", + "default": "ReVanced Extended", + "values": null + }, + { + "key": "settingsSummaries", + "title": "RVX settings summaries", + "description": "Shows the summary / description of each RVX setting. If set to false, no descriptions will be provided.", + "required": true, + "type": "kotlin.Boolean", + "default": true, + "values": null + } + ] + }, + { + "name": "Shorts components", + "description": "Adds options to hide or change components related to YouTube Shorts.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "ResourcePatch", + "BytecodePatch", + "ResourcePatch", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "SponsorBlock", + "description": "Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as sponsored content.", + "use": true, + "dependencies": [ + "ResourcePatch", + "BytecodePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "outlineIcon", + "title": "Outline icons", + "description": "Apply the outline icon.", + "required": true, + "type": "kotlin.Boolean", + "default": true, + "values": null + }, + { + "key": "NewSegmentAlignment", + "title": "New segment alignment", + "description": "Align new segment window.", + "required": true, + "type": "kotlin.String", + "default": "right", + "values": { + "Right": "right", + "Left": "left" + } + } + ] + }, + { + "name": "SponsorBlock", + "description": "Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Spoof app version", + "description": "Adds options to spoof the YouTube client version. This can be used to restore old UI elements and features.", + "use": true, + "dependencies": [ + "BytecodePatch", + "BytecodePatch", + "Settings for YouTube", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Spoof app version", + "description": "Adds options to spoof the YouTube Music client version. This can remove the radio mode restriction in Canadian regions or disable real-time lyrics.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Restore old style library shelf", + "Settings for YouTube Music", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53" + ] + }, + "options": [] + }, + { + "name": "Spoof client", + "description": "Adds options to spoof the client to allow playback.", + "use": false, + "dependencies": [ + "Settings for YouTube Music", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53" + ] + }, + "options": [] + }, + { + "name": "Spoof streaming data", + "description": "Adds options to spoof the streaming data to allow playback.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Spoof streaming data", + "description": "Adds options to spoof the streaming data to allow playback.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Spoof watch history", + "description": "Adds an option to change the domain of the watch history or check its status.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Swipe controls", + "description": "Adds options for controlling volume and brightness with swiping, and whether to enter fullscreen when swiping down below the player.", + "use": true, + "dependencies": [ + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "ResourcePatch", + "Settings for YouTube", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Theme", + "description": "Changes the app\u0027s theme to the values specified in patch options.", + "use": true, + "dependencies": [ + "ResourcePatch", + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "darkThemeBackgroundColor", + "title": "Dark theme background color", + "description": "Can be a hex color (#AARRGGBB) or a color resource reference.", + "required": false, + "type": "kotlin.String", + "default": "@android:color/black", + "values": { + "Amoled Black": "@android:color/black", + "Classic (Old YouTube)": "#FF212121", + "Catppuccin (Mocha)": "#FF181825", + "Dark Pink": "#FF290025", + "Dark Blue": "#FF001029", + "Dark Green": "#FF002905", + "Dark Yellow": "#FF282900", + "Dark Orange": "#FF291800", + "Dark Red": "#FF290000" + } + }, + { + "key": "lightThemeBackgroundColor", + "title": "Light theme background color", + "description": "Can be a hex color (#AARRGGBB) or a color resource reference.", + "required": false, + "type": "kotlin.String", + "default": "@android:color/white", + "values": { + "White": "@android:color/white", + "Catppuccin (Latte)": "#FFE6E9EF", + "Light Pink": "#FFFCCFF3", + "Light Blue": "#FFD1E0FF", + "Light Green": "#FFCCFFCC", + "Light Yellow": "#FFFDFFCC", + "Light Orange": "#FFFFE6CC", + "Light Red": "#FFFFD6D6", + "Pale Blue": "#FFD4FFF8", + "Pale Green": "#FFD1FFCC", + "Pale Yellow": "#FFFFE9AA" + } + } + ] + }, + { + "name": "Toolbar components", + "description": "Adds options to hide or change components located on the toolbar, such as toolbar buttons, search bar, and header.", + "use": true, + "dependencies": [ + "BytecodePatch", + "ResourcePatch", + "Settings for YouTube", + "BytecodePatch", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Translations for YouTube", + "description": "Add translations or remove string resources.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "customTranslations", + "title": "Custom translations", + "description": "The path to the \u0027strings.xml\u0027 file.\nPlease note that applying the \u0027strings.xml\u0027 file will overwrite all existing translations.", + "required": true, + "type": "kotlin.String", + "default": "", + "values": null + }, + { + "key": "selectedTranslations", + "title": "Translations to add", + "description": "A list of translations to be added for the RVX settings, separated by commas.", + "required": true, + "type": "kotlin.String", + "default": "ar, bg-rBG, de-rDE, el-rGR, es-rES, fr-rFR, hu-rHU, it-rIT, ja-rJP, ko-rKR, pl-rPL, pt-rBR, ru-rRU, tr-rTR, uk-rUA, vi-rVN, zh-rCN, zh-rTW", + "values": null + }, + { + "key": "selectedStringResources", + "title": "String resources to keep", + "description": "A list of string resources to be kept, separated by commas.\nString resources not in the list will be removed from the app.\n\nDefault string resource, English, is not removed.", + "required": true, + "type": "kotlin.String", + "default": "af, am, ar, ar-rXB, as, az, b+es+419, b+sr+Latn, be, bg, bn, bs, ca, cs, da, de, el, en-rAU, en-rCA, en-rGB, en-rIN, en-rXA, en-rXC, es, es-rUS, et, eu, fa, fi, fr, fr-rCA, gl, gu, hi, hr, hu, hy, id, in, is, it, iw, ja, ka, kk, km, kn, ko, ky, lo, lt, lv, mk, ml, mn, mr, ms, my, nb, ne, nl, no, or, pa, pl, pt, pt-rBR, pt-rPT, ro, ru, si, sk, sl, sq, sr, sv, sw, ta, te, th, tl, tr, uk, ur, uz, vi, zh, zh-rCN, zh-rHK, zh-rTW, zu", + "values": null + } + ] + }, + { + "name": "Translations for YouTube Music", + "description": "Add translations or remove string resources.", + "use": true, + "dependencies": [ + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [ + { + "key": "customTranslations", + "title": "Custom translations", + "description": "The path to the \u0027strings.xml\u0027 file.\nPlease note that applying the \u0027strings.xml\u0027 file will overwrite all existing translations.", + "required": true, + "type": "kotlin.String", + "default": "", + "values": null + }, + { + "key": "selectedTranslations", + "title": "Translations to add", + "description": "A list of translations to be added for the RVX settings, separated by commas.", + "required": true, + "type": "kotlin.String", + "default": "bg-rBG, bn, cs-rCZ, el-rGR, es-rES, fr-rFR, hu-rHU, id-rID, in, it-rIT, ja-rJP, ko-rKR, nl-rNL, pl-rPL, pt-rBR, ro-rRO, ru-rRU, tr-rTR, uk-rUA, vi-rVN, zh-rCN, zh-rTW", + "values": null + }, + { + "key": "selectedStringResources", + "title": "String resources to keep", + "description": "A list of string resources to be kept, separated by commas.\nString resources not in the list will be removed from the app.\n\nDefault string resource, English, is not removed.", + "required": true, + "type": "kotlin.String", + "default": "af, am, ar, ar-rXB, as, az, b+es+419, b+sr+Latn, be, bg, bn, bs, ca, cs, da, de, el, en-rAU, en-rCA, en-rGB, en-rIN, en-rXA, en-rXC, es, es-rUS, et, eu, fa, fi, fr, fr-rCA, gl, gu, hi, hr, hu, hy, id, in, is, it, iw, ja, ka, kk, km, kn, ko, ky, lo, lt, lv, mk, ml, mn, mr, ms, my, nb, ne, nl, no, or, pa, pl, pt, pt-rBR, pt-rPT, ro, ru, si, sk, sl, sq, sr, sv, sw, ta, te, th, tl, tr, uk, ur, uz, vi, zh, zh-rCN, zh-rHK, zh-rTW, zu", + "values": null + } + ] + }, + { + "name": "Video playback", + "description": "Adds options to customize settings related to video playback, such as default video quality and playback speed.", + "use": true, + "dependencies": [ + "Settings for YouTube", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "BytecodePatch", + "ResourcePatch" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [] + }, + { + "name": "Video playback", + "description": "Adds options to customize settings related to video playback, such as default video quality and playback speed.", + "use": true, + "dependencies": [ + "BytecodePatch", + "Settings for YouTube Music", + "BytecodePatch" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [] + }, + { + "name": "Visual preferences icons for YouTube", + "description": "Adds icons to specific preferences in the settings.", + "use": true, + "dependencies": [ + "Settings for YouTube" + ], + "compatiblePackages": { + "com.google.android.youtube": [ + "18.29.38", + "18.33.40", + "18.38.44", + "18.48.39", + "19.05.36", + "19.16.39", + "19.44.39" + ] + }, + "options": [ + { + "key": "settingsMenuIcon", + "title": "RVX settings menu icon", + "description": "The icon for the RVX settings menu.", + "required": true, + "type": "kotlin.String", + "default": "extension", + "values": { + "Custom branding icon": "custom_branding_icon", + "Extension": "extension", + "Gear": "gear", + "YT alt": "yt_alt", + "ReVanced": "revanced", + "ReVanced Colored": "revanced_colored", + "RVX Letters": "rvx_letters", + "RVX Letters Bold": "rvx_letters_bold" + } + }, + { + "key": "applyToAll", + "title": "Apply to all settings menu", + "description": "Whether to apply Visual preferences icons to all settings menus.\n\nIf true: icons are applied to the parent PreferenceScreen of YouTube settings, the parent PreferenceScreen of RVX settings and the RVX sub-settings (if supported).\n\nIf false: icons are applied only to the parent PreferenceScreen of YouTube settings and RVX settings.", + "required": true, + "type": "kotlin.Boolean", + "default": false, + "values": null + } + ] + }, + { + "name": "Visual preferences icons for YouTube Music", + "description": "Adds icons to specific preferences in the settings.", + "use": true, + "dependencies": [ + "Settings for YouTube Music" + ], + "compatiblePackages": { + "com.google.android.apps.youtube.music": [ + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + "7.25.53" + ] + }, + "options": [ + { + "key": "settingsMenuIcon", + "title": "RVX settings menu icon", + "description": "The icon for the RVX settings menu.", + "required": true, + "type": "kotlin.String", + "default": "extension", + "values": { + "Custom branding icon": "custom_branding_icon", + "Extension": "extension", + "Gear": "gear", + "ReVanced": "revanced", + "ReVanced Colored": "revanced_colored" + } + }, + { + "key": "applyToAll", + "title": "Apply to all settings menu", + "description": "Whether to apply Visual preferences icons to all settings menus.\n\nIf true: icons are applied to the parent PreferenceScreen of YouTube settings, the parent PreferenceScreen of RVX settings and the RVX sub-settings (if supported).\n\nIf false: icons are applied only to the parent PreferenceScreen of YouTube settings and RVX settings.", + "required": true, + "type": "kotlin.Boolean", + "default": false, + "values": null + } + ] + } +] \ No newline at end of file diff --git a/patches/api/patches.api b/patches/api/patches.api new file mode 100644 index 000000000..51efe99d0 --- /dev/null +++ b/patches/api/patches.api @@ -0,0 +1,1206 @@ +public final class app/revanced/generator/MainKt { + public static synthetic fun main ([Ljava/lang/String;)V +} + +public final class app/revanced/patches/all/misc/versioncode/ChangeVersionCodePatchKt { + public static final fun getChangeVersionCodePatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/account/components/AccountComponentsPatchKt { + public static final fun getAccountComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/actionbar/components/ActionBarComponentsPatchKt { + public static final fun getActionBarComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/ads/general/AdsPatchKt { + public static final fun getAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/flyoutmenu/components/FlyoutMenuComponentsPatchKt { + public static final fun getFlyoutMenuComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/general/amoled/AmoledPatchKt { + public static final fun getAmoledPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/general/autocaptions/AutoCaptionsPatchKt { + public static final fun getAutoCaptionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/general/components/FingerprintsKt { + public static final fun indexOfVisibilityInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I +} + +public final class app/revanced/patches/music/general/components/LayoutComponentsPatchKt { + public static final fun getLayoutComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/general/dialog/ViewerDiscretionDialogPatchKt { + public static final fun getViewerDiscretionDialogPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/general/landscapemode/LandScapeModePatchKt { + public static final fun getLandScapeModePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/general/oldstylelibraryshelf/OldStyleLibraryShelfPatchKt { + public static final fun getOldStyleLibraryShelfPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/general/redirection/DislikeRedirectionPatchKt { + public static final fun getDislikeRedirectionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatchKt { + public static final fun getSpoofAppVersionPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/general/startpage/ChangeStartPagePatchKt { + public static final fun getChangeStartPagePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/layout/branding/icon/CustomBrandingIconPatchKt { + public static final fun getCustomBrandingIconPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/layout/branding/name/CustomBrandingNamePatchKt { + public static final fun getCustomBrandingNamePatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/layout/header/ChangeHeaderPatchKt { + public static final fun getChangeHeaderPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/layout/overlayfilter/OverlayFilterPatchKt { + public static final fun getOverlayFilterPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/layout/playeroverlay/PlayerOverlayFilterPatchKt { + public static final fun getPlayerOverlayFilterPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/layout/translations/TranslationsPatchKt { + public static final fun getTranslationsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/layout/visual/VisualPreferencesIconsPatchKt { + public static final fun getIntentIcon ()Ljava/util/Map; + public static final fun getVisualPreferencesIconsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatchKt { + public static final fun getBackgroundPlaybackPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/misc/bitrate/BitrateDefaultValuePatchKt { + public static final fun getBitrateDefaultValuePatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/misc/codecs/OpusCodecPatchKt { + public static final fun getOpusCodecPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/misc/debugging/DebuggingPatchKt { + public static final fun getDebuggingPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/misc/drc/DrcAudioPatchKt { + public static final fun getDrcAudioPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/misc/share/ShareSheetPatchKt { + public static final fun getShareSheetPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/misc/splash/CairoSplashAnimationPatchKt { + public static final fun getCairoSplashAnimationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/misc/thumbnails/BypassImageRegionRestrictionsPatchKt { + public static final fun getBypassImageRegionRestrictionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/misc/tracking/SanitizeUrlQueryPatchKt { + public static final fun getSanitizeUrlQueryPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/navigation/components/NavigationBarComponentsPatchKt { + public static final fun getNavigationBarComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/player/components/FingerprintsKt { + public static final field AUDIO_VIDEO_SWITCH_TOGGLE_VISIBILITY Ljava/lang/String; +} + +public final class app/revanced/patches/music/player/components/PlayerComponentsPatchKt { + public static final fun getPlayerComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/utils/extension/SharedExtensionPatchKt { + public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/utils/fix/androidauto/AndroidAutoCertificatePatchKt { + public static final fun getAndroidAutoCertificatePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/utils/fix/client/FingerprintsKt { + public static final fun indexOfBuildInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I +} + +public final class app/revanced/patches/music/utils/fix/client/SpoofClientPatchKt { + public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/utils/fix/fileprovider/FileProviderPatchKt { + public static final fun fileProviderPatch (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatchKt { + public static final fun getSpoofStreamingDataPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/utils/flyoutmenu/FlyoutMenuHookPatchKt { + public static final fun getFlyoutMenuHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/utils/gms/GmsCoreSupportPatchKt { + public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/utils/mainactivity/MainActivityResolvePatchKt { + public static final fun getMainActivityResolvePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/utils/playertype/PlayerTypeHookPatchKt { + public static final fun getPlayerTypeHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/utils/playservice/VersionCheckPatchKt { + public static final fun getVersionCheckPatch ()Lapp/revanced/patcher/patch/ResourcePatch; + public static final fun is_6_27_or_greater ()Z + public static final fun is_6_36_or_greater ()Z + public static final fun is_6_42_or_greater ()Z + public static final fun is_7_06_or_greater ()Z + public static final fun is_7_13_or_greater ()Z + public static final fun is_7_17_or_greater ()Z + public static final fun is_7_18_or_greater ()Z + public static final fun is_7_20_or_greater ()Z + public static final fun is_7_23_or_greater ()Z + public static final fun is_7_25_or_greater ()Z +} + +public final class app/revanced/patches/music/utils/resourceid/SharedResourceIdPatchKt { + public static final fun getAccountSwitcherAccessibility ()J + public static final fun getBottomSheetRecyclerView ()J + public static final fun getButtonContainer ()J + public static final fun getButtonIconPaddingMedium ()J + public static final fun getChipCloud ()J + public static final fun getColorGrey ()J + public static final fun getDarkBackground ()J + public static final fun getDesignBottomSheetDialog ()J + public static final fun getEndButtonsContainer ()J + public static final fun getFloatingLayout ()J + public static final fun getHistoryMenuItem ()J + public static final fun getInlineTimeBarAdBreakMarkerColor ()J + public static final fun getInterstitialsContainer ()J + public static final fun getLikeDislikeContainer ()J + public static final fun getMainActivityLaunchAnimation ()J + public static final fun getMenuEntry ()J + public static final fun getMiniPlayerDefaultText ()J + public static final fun getMiniPlayerMdxPlaying ()J + public static final fun getMiniPlayerPlayPauseReplayButton ()J + public static final fun getMiniPlayerViewPager ()J + public static final fun getMusicNotifierShelf ()J + public static final fun getMusicTasteBuilderShelf ()J + public static final fun getNamesInactiveAccountThumbnailSize ()J + public static final fun getOfflineSettingsMenuItem ()J + public static final fun getPlayerOverlayChip ()J + public static final fun getPlayerViewPager ()J + public static final fun getPrivacyTosFooter ()J + public static final fun getQualityAuto ()J + public static final fun getRemixGenericButtonSize ()J + public static final fun getSlidingDialogAnimation ()J + public static final fun getTapBloomView ()J + public static final fun getText1 ()J + public static final fun getToolTipContentView ()J + public static final fun getTopBarMenuItemImageView ()J + public static final fun getTopEnd ()J + public static final fun getTopStart ()J + public static final fun getTosFooter ()J + public static final fun getTouchOutside ()J + public static final fun getTrimSilenceSwitch ()J + public static final fun getVarispeedUnavailableTitle ()J + public static final fun isTablet ()J +} + +public final class app/revanced/patches/music/utils/returnyoutubedislike/ReturnYouTubeDislikePatchKt { + public static final fun getReturnYouTubeDislikePatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/utils/returnyoutubedislike/Vote : java/lang/Enum { + public static final field DISLIKE Lapp/revanced/patches/music/utils/returnyoutubedislike/Vote; + public static final field LIKE Lapp/revanced/patches/music/utils/returnyoutubedislike/Vote; + public static final field REMOVE_LIKE Lapp/revanced/patches/music/utils/returnyoutubedislike/Vote; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getValue ()I + public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/music/utils/returnyoutubedislike/Vote; + public static fun values ()[Lapp/revanced/patches/music/utils/returnyoutubedislike/Vote; +} + +public final class app/revanced/patches/music/utils/returnyoutubeusername/ReturnYouTubeUsernamePatchKt { + public static final fun getReturnYouTubeUsernamePatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/utils/settings/SettingsPatchKt { + public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; + public static final fun isSettingsSummariesEnabled ()Ljava/lang/Boolean; + public static final fun setSettingsSummariesEnabled (Ljava/lang/Boolean;)V +} + +public final class app/revanced/patches/music/utils/sponsorblock/SponsorBlockPatchKt { + public static final fun getSponsorBlockPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/music/utils/videotype/VideoTypeHookPatchKt { + public static final fun getVideoTypeHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/video/information/VideoInformationPatchKt { + public static final fun getVideoInformationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/video/playback/VideoPlaybackPatchKt { + public static final fun getVideoPlaybackPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/ad/AdsPatchKt { + public static final fun getAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/layout/branding/name/CustomBrandingNamePatchKt { + public static final fun getCustomBrandingNamePatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/reddit/layout/branding/packagename/ChangePackageNamePatchKt { + public static final fun getChangePackageNamePatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/reddit/layout/communities/RecommendedCommunitiesPatchKt { + public static final fun getRecommendedCommunitiesPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/layout/navigation/FingerprintsKt { + public static final fun indexOfGetDimensionPixelSizeInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I + public static final fun indexOfGetItemsInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I + public static final fun indexOfSetSelectedItemTypeInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I +} + +public final class app/revanced/patches/reddit/layout/navigation/NavigationButtonsPatchKt { + public static final fun getNavigationButtonsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/layout/premiumicon/PremiumIconPatchKt { + public static final fun getPremiumIconPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/layout/recentlyvisited/FingerprintsKt { + public static final fun indexOfHeaderItemInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I +} + +public final class app/revanced/patches/reddit/layout/recentlyvisited/RecentlyVisitedShelfPatchKt { + public static final fun getRecentlyVisitedShelfPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatchKt { + public static final fun getScreenshotPopupPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/layout/subredditdialog/FingerprintsKt { + public static final fun indexOfSetBackgroundTintListInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I +} + +public final class app/revanced/patches/reddit/layout/subredditdialog/SubRedditDialogPatchKt { + public static final fun getSubRedditDialogPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/layout/toolbar/ToolBarButtonPatchKt { + public static final fun getToolBarButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/misc/openlink/FingerprintsKt { + public static final fun indexOfScreenNavigatorInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I +} + +public final class app/revanced/patches/reddit/misc/openlink/OpenLinksDirectlyPatchKt { + public static final fun getOpenLinksDirectlyPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/misc/openlink/OpenLinksExternallyPatchKt { + public static final fun getOpenLinksExternallyPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/misc/openlink/ScreenNavigatorMethodResolverPatchKt { + public static field screenNavigatorMethod Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static final fun getScreenNavigatorMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static final fun getScreenNavigatorMethodResolverPatch ()Lapp/revanced/patcher/patch/BytecodePatch; + public static final fun setScreenNavigatorMethod (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)V +} + +public final class app/revanced/patches/reddit/misc/tracking/url/FingerprintsKt { + public static final fun indexOfClearQueryInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I +} + +public final class app/revanced/patches/reddit/misc/tracking/url/SanitizeUrlQueryPatchKt { + public static final fun getSanitizeUrlQueryPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/utils/extension/SharedExtensionPatchKt { + public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/utils/settings/SettingsPatchKt { + public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; + public static final fun is_2024_18_or_greater ()Z +} + +public final class app/revanced/patches/shared/FingerprintsKt { + public static final field SPANNABLE_STRING_REFERENCE Ljava/lang/String; + public static final fun indexOfModelInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I + public static final fun indexOfReleaseInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I + public static final fun indexOfSpannableStringInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I +} + +public final class app/revanced/patches/shared/ads/BaseAdsPatchKt { + public static final fun baseAdsPatch (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/captions/BaseAutoCaptionsPatchKt { + public static final fun getBaseAutoCaptionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/customspeed/CustomPlaybackSpeedPatchKt { + public static final fun customPlaybackSpeedPatch (Ljava/lang/String;F)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/dialog/BaseViewerDiscretionDialogPatchKt { + public static final fun baseViewerDiscretionDialogPatch (Ljava/lang/String;Z)Lapp/revanced/patcher/patch/BytecodePatch; + public static synthetic fun baseViewerDiscretionDialogPatch$default (Ljava/lang/String;ZILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/drawable/DrawableColorHookPatchKt { + public static final fun getDrawableColorHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/extension/ExtensionHook { + public final fun getFingerprint ()Lapp/revanced/patcher/Fingerprint; + public final fun invoke (Lapp/revanced/patcher/patch/BytecodePatchContext;Ljava/lang/String;)V +} + +public final class app/revanced/patches/shared/extension/SharedExtensionPatchKt { + public static final fun extensionHook (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patches/shared/extension/ExtensionHook; + public static synthetic fun extensionHook$default (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patches/shared/extension/ExtensionHook; + public static final fun sharedExtensionPatch ([Lapp/revanced/patches/shared/extension/ExtensionHook;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/gms/FingerprintsKt { + public static final field GET_GMS_CORE_VENDOR_GROUP_ID_METHOD_NAME Ljava/lang/String; + public static final fun indexOfGetPackageNameInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I +} + +public final class app/revanced/patches/shared/gms/GmsCoreSupportPatchKt { + public static final fun gmsCoreSupportPatch (Ljava/lang/String;Lapp/revanced/patcher/Fingerprint;Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch; + public static synthetic fun gmsCoreSupportPatch$default (Ljava/lang/String;Lapp/revanced/patcher/Fingerprint;Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch; + public static final fun gmsCoreSupportResourcePatch (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patcher/patch/Option;Lapp/revanced/patcher/patch/Option;Lapp/revanced/patcher/patch/Option;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/ResourcePatch; + public static synthetic fun gmsCoreSupportResourcePatch$default (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patcher/patch/Option;Lapp/revanced/patcher/patch/Option;Lapp/revanced/patcher/patch/Option;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/shared/imageurl/CronetImageUrlHookPatchKt { + public static final fun cronetImageUrlHookPatch (Z)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/litho/LithoFilterPatchKt { + public static final fun getLithoFilterPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/mainactivity/BaseMainActivityResolvePatchKt { + public static final fun baseMainActivityResolvePatch (Lkotlin/Pair;)Lapp/revanced/patcher/patch/BytecodePatch; + public static final fun getMainActivityMutableClass ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass; + public static final fun getOnConfigurationChangedMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static final fun getOnCreateMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; +} + +public final class app/revanced/patches/shared/mapping/ResourceElement { + public fun (Ljava/lang/String;Ljava/lang/String;J)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()J + public final fun copy (Ljava/lang/String;Ljava/lang/String;J)Lapp/revanced/patches/shared/mapping/ResourceElement; + public static synthetic fun copy$default (Lapp/revanced/patches/shared/mapping/ResourceElement;Ljava/lang/String;Ljava/lang/String;JILjava/lang/Object;)Lapp/revanced/patches/shared/mapping/ResourceElement; + public fun equals (Ljava/lang/Object;)Z + public final fun getId ()J + public final fun getName ()Ljava/lang/String; + public final fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class app/revanced/patches/shared/mapping/ResourceMappingPatchKt { + public static final fun get (Ljava/util/List;Lapp/revanced/patches/shared/mapping/ResourceType;Ljava/lang/String;)J + public static final fun get (Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)J + public static final fun getResourceMappingPatch ()Lapp/revanced/patcher/patch/ResourcePatch; + public static final fun getResourceMappings ()Ljava/util/List; +} + +public final class app/revanced/patches/shared/mapping/ResourceType : java/lang/Enum { + public static final field ATTR Lapp/revanced/patches/shared/mapping/ResourceType; + public static final field BOOL Lapp/revanced/patches/shared/mapping/ResourceType; + public static final field COLOR Lapp/revanced/patches/shared/mapping/ResourceType; + public static final field DIMEN Lapp/revanced/patches/shared/mapping/ResourceType; + public static final field DRAWABLE Lapp/revanced/patches/shared/mapping/ResourceType; + public static final field ID Lapp/revanced/patches/shared/mapping/ResourceType; + public static final field INTEGER Lapp/revanced/patches/shared/mapping/ResourceType; + public static final field LAYOUT Lapp/revanced/patches/shared/mapping/ResourceType; + public static final field STRING Lapp/revanced/patches/shared/mapping/ResourceType; + public static final field STYLE Lapp/revanced/patches/shared/mapping/ResourceType; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getValue ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/shared/mapping/ResourceType; + public static fun values ()[Lapp/revanced/patches/shared/mapping/ResourceType; +} + +public final class app/revanced/patches/shared/opus/BaseOpusCodecsPatchKt { + public static final fun baseOpusCodecsPatch (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/returnyoutubeusername/BaseReturnYouTubeUsernamePatchKt { + public static final fun getBaseReturnYouTubeUsernamePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/settingmenu/SettingsMenuPatchKt { + public static final fun getSettingsMenuPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/spans/InclusiveSpanPatchKt { + public static final fun getInclusiveSpanPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/spoof/appversion/BaseSpoofAppVersionPatchKt { + public static final fun baseSpoofAppVersionPatch (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatchKt { + public static final field EXTENSION_CLASS_DESCRIPTOR Ljava/lang/String; + public static final fun baseSpoofStreamingDataPatch (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch; + public static synthetic fun baseSpoofStreamingDataPatch$default (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/spoof/streamingdata/FingerprintsKt { + public static final field STREAMING_DATA_INTERFACE Ljava/lang/String; +} + +public final class app/revanced/patches/shared/spoof/useragent/BaseSpoofUserAgentPatchKt { + public static final fun baseSpoofUserAgentPatch (Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/textcomponent/TextComponentPatchKt { + public static final fun getTextComponentPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/tracking/BaseSanitizeUrlQueryPatchKt { + public static final fun getBaseSanitizeUrlQueryPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public abstract interface class app/revanced/patches/shared/transformation/IMethodCall { + public abstract fun getDefinedClassName ()Ljava/lang/String; + public abstract fun getMethodName ()Ljava/lang/String; + public abstract fun getMethodParams ()[Ljava/lang/String; + public abstract fun getReturnType ()Ljava/lang/String; + public abstract fun replaceInvokeVirtualWithExtension (Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lcom/android/tools/smali/dexlib2/iface/instruction/formats/Instruction35c;I)V +} + +public final class app/revanced/patches/shared/transformation/IMethodCall$DefaultImpls { + public static fun replaceInvokeVirtualWithExtension (Lapp/revanced/patches/shared/transformation/IMethodCall;Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lcom/android/tools/smali/dexlib2/iface/instruction/formats/Instruction35c;I)V +} + +public final class app/revanced/patches/shared/transformation/TransformInstructionsPatchKt { + public static final fun transformInstructionsPatch (Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/translations/BaseTranslationsPatchKt { + public static final fun baseTranslationsPatch (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/lang/String;)V + public static final fun getAPP_LANGUAGES ()[Ljava/lang/String; +} + +public final class app/revanced/patches/shared/viewgroup/ViewGroupMarginLayoutParamsHookPatchKt { + public static final fun getViewGroupMarginLayoutParamsHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/ads/general/AdsPatchKt { + public static final fun getAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/alternative/thumbnails/AlternativeThumbnailsPatchKt { + public static final fun getAlternativeThumbnailsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/alternative/thumbnails/BypassImageRegionRestrictionsPatchKt { + public static final fun getBypassImageRegionRestrictionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/feed/components/FeedComponentsPatchKt { + public static final fun getFeedComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/feed/flyoutmenu/FeedFlyoutMenuPatchKt { + public static final fun getFeedFlyoutMenuPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/general/audiotracks/AudioTracksPatchKt { + public static final fun getAudioTracksPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/general/autocaptions/AutoCaptionsPatchKt { + public static final fun getAutoCaptionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/general/components/LayoutComponentsPatchKt { + public static final fun getLayoutComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/general/dialog/ViewerDiscretionDialogPatchKt { + public static final fun getViewerDiscretionDialogPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/general/downloads/DownloadActionsPatchKt { + public static final fun getDownloadActionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/general/layoutswitch/LayoutSwitchPatchKt { + public static final fun getLayoutSwitchPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/general/loadingscreen/GradientLoadingScreenPatchKt { + public static final fun getGradientLoadingScreenPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/general/miniplayer/MiniplayerPatchKt { + public static final fun getMiniplayerPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/general/music/YouTubeMusicActionsPatchKt { + public static final fun getYoutubeMusicActionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatchKt { + public static final fun getNavigationBarComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/general/snackbar/ForceSnackbarThemeKt { + public static final fun getForceSnackbarTheme ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/general/splashanimation/SplashAnimationPatchKt { + public static final fun getSplashAnimationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/general/spoofappversion/SpoofAppVersionPatchKt { + public static final fun getSpoofAppVersionPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/general/startpage/ChangeStartPagePatchKt { + public static final fun getChangeStartPagePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatchKt { + public static final fun getToolBarComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatchKt { + public static final fun getShortsActionButtonsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatchKt { + public static final fun getCustomBrandingIconPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/branding/name/CustomBrandingNamePatchKt { + public static final fun getCustomBrandingNamePatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/dimming/ShortsDimmingPatchKt { + public static final fun getShortsDimmingPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/doubletaplength/DoubleTapLengthPatchKt { + public static final fun getDoubleTapLengthPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/header/ChangeHeaderPatchKt { + public static final fun getChangeHeaderPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/playerbuttonbg/PlayerButtonBackgroundPatchKt { + public static final fun getPlayerButtonBackgroundPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/shortcut/ShortcutPatchKt { + public static final fun getShortcutPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/theme/MaterialYouPatchKt { + public static final fun getMaterialYouPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/theme/SharedThemePatchKt { + public static final fun getSharedThemePatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/theme/ThemePatchKt { + public static final fun getThemePatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/translations/TranslationsPatchKt { + public static final fun getTranslationsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/visual/VisualPreferencesIconsPatchKt { + public static final fun getIntentIcon ()Ljava/util/Map; + public static final fun getVisualPreferencesIconsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatchKt { + public static final fun getBackgroundPlaybackPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/codecs/OpusCodecPatchKt { + public static final fun getOpusCodecPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/debugging/DebuggingPatchKt { + public static final fun getDebuggingPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/externalbrowser/OpenLinksExternallyPatchKt { + public static final fun getOpenLinksExternallyPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/openlinksdirectly/OpenLinksDirectlyPatchKt { + public static final fun getOpenLinksDirectlyPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/quic/QUICProtocolPatchKt { + public static final fun getQuicProtocolPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/share/ShareSheetPatchKt { + public static final fun getShareSheetPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/tracking/SanitizeUrlQueryPatchKt { + public static final fun getSanitizeUrlQueryPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/watchhistory/WatchHistoryPatchKt { + public static final fun getWatchHistoryPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/player/action/ActionButtonsPatchKt { + public static final fun getActionButtonsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/player/ambientmode/AmbientModeSwitchPatchKt { + public static final fun getAmbientModeSwitchPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/player/buttons/PlayerButtonsPatchKt { + public static final fun getPlayerButtonsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/player/comments/CommentsComponentPatchKt { + public static final fun getCommentsComponentPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/player/components/PlayerComponentsPatchKt { + public static final fun getPlayerComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/player/descriptions/DescriptionComponentsPatchKt { + public static final fun getDescriptionComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/player/flyoutmenu/hide/PlayerFlyoutMenuPatchKt { + public static final fun getPlayerFlyoutMenuPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/player/flyoutmenu/toggle/ChangeTogglePatchKt { + public static final fun getChangeTogglePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatchKt { + public static final fun getFullscreenComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/player/hapticfeedback/HapticFeedbackPatchKt { + public static final fun getHapticFeedbackPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatchKt { + public static final fun getOverlayButtonsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatchKt { + public static final fun getSeekbarComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/shorts/components/FingerprintsKt { + public static final fun indexOfAddLiveHeaderElementsContainerInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I +} + +public final class app/revanced/patches/youtube/shorts/components/ShortsComponentPatchKt { + public static final fun getShortsComponentPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/shorts/startupshortsreset/ResumingShortsOnStartupPatchKt { + public static final fun getResumingShortsOnStartupPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/swipe/controls/SwipeControlsPatchKt { + public static final fun getSwipeControlsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/FingerprintsKt { + public static final field PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR Ljava/lang/String; + public static final fun indexOfSpannedCharSequenceInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I +} + +public final class app/revanced/patches/youtube/utils/bottomsheet/BottomSheetHookPatchKt { + public static final fun getBottomSheetHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/castbutton/CastButtonPatchKt { + public static final fun getCastButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/controlsoverlay/ControlsOverlayConfigPatchKt { + public static final fun getControlsOverlayConfigPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/extension/SharedExtensionPatchKt { + public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/fix/bottomui/CfBottomUIPatchKt { + public static final fun getCfBottomUIPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/fix/cairo/CairoSettingsPatchKt { + public static final fun getCairoSettingsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/fix/doublebacktoclose/DoubleBackToClosePatchKt { + public static final fun getDoubleBackToClosePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/fix/playbackspeed/FingerprintsKt { + public static final fun indexOfGetPlaybackSpeedInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I +} + +public final class app/revanced/patches/youtube/utils/fix/playbackspeed/PlaybackSpeedWhilePlayingPatchKt { + public static final fun getPlaybackSpeedWhilePlayingPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/fix/shortsplayback/ShortsPlaybackPatchKt { + public static final fun getShortsPlaybackPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/fix/splash/DarkModeSplashScreenPatchKt { + public static final fun getDarkModeSplashScreenPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatchKt { + public static final fun getSpoofStreamingDataPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/SuggestedVideoEndScreenPatchKt { + public static final fun getSuggestedVideoEndScreenPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/fix/swiperefresh/SwipeRefreshPatchKt { + public static final fun getSwipeRefreshPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/flyoutmenu/FlyoutMenuHookPatchKt { + public static final fun getFlyoutMenuHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/gms/GmsCoreSupportPatchKt { + public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/lockmodestate/LockModeStateHookPatchKt { + public static final fun getLockModeStateHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/lottie/LottieAnimationViewHookPatchKt { + public static final fun getLottieAnimationViewHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/mainactivity/MainActivityResolvePatchKt { + public static final fun getMainActivityResolvePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/navigation/NavigationBarHookPatchKt { + public static field hookNavigationButtonCreated Lkotlin/jvm/functions/Function1; + public static final fun addBottomBarContainerHook (Ljava/lang/String;)V + public static final fun getHookNavigationButtonCreated ()Lkotlin/jvm/functions/Function1; + public static final fun getNavigationBarHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; + public static final fun setHookNavigationButtonCreated (Lkotlin/jvm/functions/Function1;)V +} + +public final class app/revanced/patches/youtube/utils/pip/PiPStateHookPatchKt { + public static final fun getPipStateHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/playercontrols/PlayerControlsPatchKt { + public static field changeVisibilityMethod Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static field changeVisibilityNegatedImmediatelyMethod Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static field initializeBottomControlButtonMethod Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static field initializeTopControlButtonMethod Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static final fun getChangeVisibilityMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static final fun getChangeVisibilityNegatedImmediatelyMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static final fun getInitializeBottomControlButtonMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static final fun getInitializeTopControlButtonMethod ()Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static final fun getPlayerControlsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; + public static final fun hookBottomControlButton (Ljava/lang/String;)V + public static final fun hookTopControlButton (Ljava/lang/String;)V + public static final fun setChangeVisibilityMethod (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)V + public static final fun setChangeVisibilityNegatedImmediatelyMethod (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)V + public static final fun setInitializeBottomControlButtonMethod (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)V + public static final fun setInitializeTopControlButtonMethod (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;)V +} + +public final class app/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatchKt { + public static final fun getPlayerTypeHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/playservice/VersionCheckPatchKt { + public static final fun getVersionCheckPatch ()Lapp/revanced/patcher/patch/ResourcePatch; + public static final fun is_18_31_or_greater ()Z + public static final fun is_18_34_or_greater ()Z + public static final fun is_18_39_or_greater ()Z + public static final fun is_18_42_or_greater ()Z + public static final fun is_18_49_or_greater ()Z + public static final fun is_19_02_or_greater ()Z + public static final fun is_19_04_or_greater ()Z + public static final fun is_19_09_or_greater ()Z + public static final fun is_19_15_or_greater ()Z + public static final fun is_19_17_or_greater ()Z + public static final fun is_19_23_or_greater ()Z + public static final fun is_19_25_or_greater ()Z + public static final fun is_19_26_or_greater ()Z + public static final fun is_19_28_or_greater ()Z + public static final fun is_19_29_or_greater ()Z + public static final fun is_19_30_or_greater ()Z + public static final fun is_19_32_or_greater ()Z + public static final fun is_19_34_or_greater ()Z + public static final fun is_19_36_or_greater ()Z + public static final fun is_19_41_or_greater ()Z + public static final fun is_19_43_or_greater ()Z + public static final fun is_19_44_or_greater ()Z + public static final fun is_19_46_or_greater ()Z +} + +public final class app/revanced/patches/youtube/utils/recyclerview/RecyclerViewTreeObserverPatchKt { + public static final fun getRecyclerViewTreeObserverPatch ()Lapp/revanced/patcher/patch/BytecodePatch; + public static final fun recyclerViewTreeObserverHook (Ljava/lang/String;)V +} + +public final class app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatchKt { + public static final fun getAccountSwitcherAccessibility ()J + public static final fun getActionBarRingo ()J + public static final fun getActionBarRingoBackground ()J + public static final fun getAdAttribution ()J + public static final fun getAppRelatedEndScreenResults ()J + public static final fun getAppearance ()J + public static final fun getAutoNavPreviewStub ()J + public static final fun getAutoNavScrollCancelPadding ()J + public static final fun getAutoNavToggle ()J + public static final fun getBackgroundCategory ()J + public static final fun getBadgeLabel ()J + public static final fun getBar ()J + public static final fun getBarContainerHeight ()J + public static final fun getBottomBarContainer ()J + public static final fun getBottomSheetFooterText ()J + public static final fun getBottomSheetRecyclerView ()J + public static final fun getBottomUiContainerStub ()J + public static final fun getCaptionToggleContainer ()J + public static final fun getCastMediaRouteButton ()J + public static final fun getCfFullscreenButton ()J + public static final fun getChannelListSubMenu ()J + public static final fun getCompactLink ()J + public static final fun getCompactListItem ()J + public static final fun getComponentLongClickListener ()J + public static final fun getContentPill ()J + public static final fun getControlsLayoutStub ()J + public static final fun getDarkBackground ()J + public static final fun getDarkSplashAnimation ()J + public static final fun getDesignBottomSheet ()J + public static final fun getDonationCompanion ()J + public static final fun getDrawerContentView ()J + public static final fun getDrawerResults ()J + public static final fun getEasySeekEduContainer ()J + public static final fun getEditSettingsAction ()J + public static final fun getEmojiPickerIcon ()J + public static final fun getEndScreenElementLayoutCircle ()J + public static final fun getEndScreenElementLayoutIcon ()J + public static final fun getEndScreenElementLayoutVideo ()J + public static final fun getExpandButtonDown ()J + public static final fun getFab ()J + public static final fun getFadeDurationFast ()J + public static final fun getFilterBarHeight ()J + public static final fun getFloatyBarTopMargin ()J + public static final fun getFullScreenButton ()J + public static final fun getFullScreenEngagementOverlay ()J + public static final fun getFullScreenEngagementPanel ()J + public static final fun getHorizontalCardList ()J + public static final fun getImageOnlyTab ()J + public static final fun getInlineTimeBarColorizedBarPlayedColorDark ()J + public static final fun getInlineTimeBarPlayedNotHighlightedColor ()J + public static final fun getInsetOverlayViewLayout ()J + public static final fun getInterstitialsContainer ()J + public static final fun getMenuItemView ()J + public static final fun getMetaPanel ()J + public static final fun getMiniplayerMaxSize ()J + public static final fun getModernMiniPlayerClose ()J + public static final fun getModernMiniPlayerExpand ()J + public static final fun getModernMiniPlayerForwardButton ()J + public static final fun getModernMiniPlayerRewindButton ()J + public static final fun getMusicAppDeeplinkButtonView ()J + public static final fun getNotificationBigPictureIconWidth ()J + public static final fun getOfflineActionsVideoDeletedUndoSnackbarText ()J + public static final fun getPlayerCollapseButton ()J + public static final fun getPlayerControlNextButtonTouchArea ()J + public static final fun getPlayerControlPreviousButtonTouchArea ()J + public static final fun getPlayerVideoTitleView ()J + public static final fun getPosterArtWidthDefault ()J + public static final fun getQualityAuto ()J + public static final fun getQuickActionsElementContainer ()J + public static final fun getReelDynRemix ()J + public static final fun getReelDynShare ()J + public static final fun getReelFeedbackLike ()J + public static final fun getReelFeedbackPause ()J + public static final fun getReelFeedbackPlay ()J + public static final fun getReelForcedMuteButton ()J + public static final fun getReelPlayerFooter ()J + public static final fun getReelPlayerRightPivotV2Size ()J + public static final fun getReelRightDislikeIcon ()J + public static final fun getReelRightLikeIcon ()J + public static final fun getReelTimeBarPlayedColor ()J + public static final fun getReelVodTimeStampsContainer ()J + public static final fun getReelWatchPlayer ()J + public static final fun getRelatedChipCloudMargin ()J + public static final fun getRightComment ()J + public static final fun getScrimOverlay ()J + public static final fun getScrubbing ()J + public static final fun getSeekEasyHorizontalTouchOffsetToStartScrubbing ()J + public static final fun getSeekUndoEduOverlayStub ()J + public static final fun getSlidingDialogAnimation ()J + public static final fun getSubtitleMenuSettingsFooterInfo ()J + public static final fun getSuggestedAction ()J + public static final fun getTapBloomView ()J + public static final fun getTitleAnchor ()J + public static final fun getToolTipContentView ()J + public static final fun getTotalTime ()J + public static final fun getTouchArea ()J + public static final fun getVarispeedUnavailableTitle ()J + public static final fun getVideoQualityBottomSheet ()J + public static final fun getVideoQualityUnavailableAnnouncement ()J + public static final fun getVideoZoomSnapIndicator ()J + public static final fun getVoiceSearch ()J + public static final fun getYouTubeControlsOverlaySubtitleButton ()J + public static final fun getYouTubeLogo ()J + public static final fun getYtFillBell ()J + public static final fun getYtOutlinePictureInPictureWhite ()J + public static final fun getYtOutlineVideoCamera ()J + public static final fun getYtOutlineXWhite ()J + public static final fun getYtPremiumWordMarkHeader ()J + public static final fun getYtWordMarkHeader ()J +} + +public final class app/revanced/patches/youtube/utils/returnyoutubedislike/ReturnYouTubeDislikePatchKt { + public static final fun getReturnYouTubeDislikePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/returnyoutubedislike/Vote : java/lang/Enum { + public static final field DISLIKE Lapp/revanced/patches/youtube/utils/returnyoutubedislike/Vote; + public static final field LIKE Lapp/revanced/patches/youtube/utils/returnyoutubedislike/Vote; + public static final field REMOVE_LIKE Lapp/revanced/patches/youtube/utils/returnyoutubedislike/Vote; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getValue ()I + public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/youtube/utils/returnyoutubedislike/Vote; + public static fun values ()[Lapp/revanced/patches/youtube/utils/returnyoutubedislike/Vote; +} + +public final class app/revanced/patches/youtube/utils/returnyoutubeusername/ReturnYouTubeUsernamePatchKt { + public static final fun getReturnYouTubeUsernamePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/settings/SettingsPatchKt { + public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/utils/sponsorblock/SponsorBlockPatchKt { + public static final fun getSponsorBlockBytecodePatch ()Lapp/revanced/patcher/patch/BytecodePatch; + public static final fun getSponsorBlockPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/utils/toolbar/ToolBarHookPatchKt { + public static final fun getToolBarHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/utils/trackingurlhook/TrackingUrlHookPatchKt { + public static final fun getTrackingUrlHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/video/information/FingerprintsKt { + public static final fun indexOfPlayerResponseModelInterfaceInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I +} + +public final class app/revanced/patches/youtube/video/information/VideoInformationPatchKt { + public static final fun getVideoInformationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/video/playback/VideoPlaybackPatchKt { + public static final fun getVideoPlaybackPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public abstract class app/revanced/patches/youtube/video/playerresponse/Hook { + public synthetic fun (Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun toString ()Ljava/lang/String; +} + +public final class app/revanced/patches/youtube/video/playerresponse/Hook$PlayerParameter : app/revanced/patches/youtube/video/playerresponse/Hook { + public fun (Ljava/lang/String;)V +} + +public final class app/revanced/patches/youtube/video/playerresponse/Hook$PlayerParameterBeforeVideoId : app/revanced/patches/youtube/video/playerresponse/Hook { + public fun (Ljava/lang/String;)V +} + +public final class app/revanced/patches/youtube/video/playerresponse/Hook$VideoId : app/revanced/patches/youtube/video/playerresponse/Hook { + public fun (Ljava/lang/String;)V +} + +public final class app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatchKt { + public static final fun addPlayerResponseMethodHook (Lapp/revanced/patches/youtube/video/playerresponse/Hook;)V + public static final fun getPlayerResponseMethodHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/video/videoid/VideoIdPatchKt { + public static final fun getVideoIdPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/util/BytecodeUtilsKt { + public static final field REGISTER_TEMPLATE_REPLACEMENT Ljava/lang/String; + public static final fun addStaticFieldToExtension (Lapp/revanced/patcher/patch/BytecodePatchContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V + public static synthetic fun addStaticFieldToExtension$default (Lapp/revanced/patcher/patch/BytecodePatchContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)V + public static final fun cloneMutable (Lcom/android/tools/smali/dexlib2/iface/Method;IZLjava/lang/String;ILjava/util/List;Ljava/lang/String;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static synthetic fun cloneMutable$default (Lcom/android/tools/smali/dexlib2/iface/Method;IZLjava/lang/String;ILjava/util/List;Ljava/lang/String;ILjava/lang/Object;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;J)Z + public static final fun findInstructionIndicesReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)Ljava/util/List; + public static final fun findInstructionIndicesReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Lkotlin/jvm/functions/Function1;)Ljava/util/List; + public static final fun findInstructionIndicesReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)Ljava/util/List; + public static final fun findInstructionIndicesReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Lkotlin/jvm/functions/Function1;)Ljava/util/List; + public static final fun findMethodOrThrow (Lapp/revanced/patcher/patch/BytecodePatchContext;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static synthetic fun findMethodOrThrow$default (Lapp/revanced/patcher/patch/BytecodePatchContext;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static final fun findMethodsOrThrow (Lapp/revanced/patcher/patch/BytecodePatchContext;Ljava/lang/String;)Ljava/util/Set; + public static final fun findMutableMethodOf (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static final fun forEachLiteralValueInstruction (Lapp/revanced/patcher/patch/BytecodePatchContext;JLkotlin/jvm/functions/Function2;)V + public static final fun getFiveRegisters (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)Ljava/lang/String; + public static final fun getWalkerMethod (Lapp/revanced/patcher/patch/BytecodePatchContext;Lapp/revanced/patcher/Match;I)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static final fun getWalkerMethod (Lapp/revanced/patcher/patch/BytecodePatchContext;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; + public static final fun indexOfFirstInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;ILcom/android/tools/smali/dexlib2/Opcode;)I + public static final fun indexOfFirstInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;ILkotlin/jvm/functions/Function1;)I + public static final fun indexOfFirstInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)I + public static synthetic fun indexOfFirstInstruction$default (Lcom/android/tools/smali/dexlib2/iface/Method;ILcom/android/tools/smali/dexlib2/Opcode;ILjava/lang/Object;)I + public static synthetic fun indexOfFirstInstruction$default (Lcom/android/tools/smali/dexlib2/iface/Method;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)I + public static final fun indexOfFirstInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;ILcom/android/tools/smali/dexlib2/Opcode;)I + public static final fun indexOfFirstInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;ILkotlin/jvm/functions/Function1;)I + public static final fun indexOfFirstInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)I + public static synthetic fun indexOfFirstInstructionOrThrow$default (Lcom/android/tools/smali/dexlib2/iface/Method;ILcom/android/tools/smali/dexlib2/Opcode;ILjava/lang/Object;)I + public static synthetic fun indexOfFirstInstructionOrThrow$default (Lcom/android/tools/smali/dexlib2/iface/Method;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)I + public static final fun indexOfFirstInstructionReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lcom/android/tools/smali/dexlib2/Opcode;)I + public static final fun indexOfFirstInstructionReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)I + public static synthetic fun indexOfFirstInstructionReversed$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lcom/android/tools/smali/dexlib2/Opcode;ILjava/lang/Object;)I + public static synthetic fun indexOfFirstInstructionReversed$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)I + public static final fun indexOfFirstInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)I + public static final fun indexOfFirstInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lcom/android/tools/smali/dexlib2/Opcode;)I + public static final fun indexOfFirstInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)I + public static synthetic fun indexOfFirstInstructionReversedOrThrow$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lcom/android/tools/smali/dexlib2/Opcode;ILjava/lang/Object;)I + public static synthetic fun indexOfFirstInstructionReversedOrThrow$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)I + public static final fun indexOfFirstLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;J)I + public static final fun indexOfFirstLiteralInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;J)I + public static final fun indexOfFirstLiteralInstructionReversed (Lcom/android/tools/smali/dexlib2/iface/Method;J)I + public static final fun indexOfFirstLiteralInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;J)I + public static final fun indexOfFirstResourceId (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 indexOfFirstStringInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;)I + public static final fun indexOfFirstStringInstructionOrThrow (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 injectLiteralInstructionViewCall (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;JLjava/lang/String;)V + public static final fun literal (Lapp/revanced/patcher/FingerprintBuilder;Lkotlin/jvm/functions/Function0;)V + public static final fun or (ILcom/android/tools/smali/dexlib2/AccessFlags;)I + public static final fun or (Lcom/android/tools/smali/dexlib2/AccessFlags;I)I + public static final fun or (Lcom/android/tools/smali/dexlib2/AccessFlags;Lcom/android/tools/smali/dexlib2/AccessFlags;)I + public static final fun parametersEqual (Ljava/lang/Iterable;Ljava/lang/Iterable;)Z + public static final fun replaceLiteralInstructionCall (Lapp/revanced/patcher/patch/BytecodePatchContext;JLjava/lang/String;)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 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 updatePatchStatus (Lapp/revanced/patcher/patch/BytecodePatchContext;Ljava/lang/String;Ljava/lang/String;)V +} + +public final class app/revanced/util/ResourceGroup { + public fun (Ljava/lang/String;[Ljava/lang/String;)V + public final fun getResourceDirectoryName ()Ljava/lang/String; + public final fun getResources ()[Ljava/lang/String; +} + +public final class app/revanced/util/ResourceUtilsKt { + public static final fun addEntryValues (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V + public static synthetic fun addEntryValues$default (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILjava/lang/Object;)V + public static final fun adoptChild (Lorg/w3c/dom/Node;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V + public static final fun appendAppVersion (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;)V + public static final fun cloneNodes (Lorg/w3c/dom/Node;Lorg/w3c/dom/Node;)V + public static final fun copyAdaptiveIcon (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;)V + public static synthetic fun copyAdaptiveIcon$default (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;ILjava/lang/Object;)V + public static final fun copyFile (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;)Z + public static final fun copyResources (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;[Lapp/revanced/util/ResourceGroup;Z)V + public static synthetic fun copyResources$default (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;[Lapp/revanced/util/ResourceGroup;ZILjava/lang/Object;)V + public static final fun copyResourcesWithRename (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;Ljava/util/Map;)V + public static final fun copyXmlNode (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lkotlin/Unit; + public static final fun copyXmlNode (Ljava/lang/String;Lapp/revanced/patcher/util/Document;Lapp/revanced/patcher/util/Document;)Ljava/lang/AutoCloseable; + public static final fun doRecursively (Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V + public static final fun getResourceGroup (Ljava/util/List;[Ljava/lang/String;)Ljava/util/List; + public static final fun getStringOptionValue (Lapp/revanced/patcher/patch/Patch;Ljava/lang/String;)Lapp/revanced/patcher/patch/Option; + public static final fun insertNode (Lorg/w3c/dom/Node;Ljava/lang/String;Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V + public static final fun lowerCaseOrThrow (Lapp/revanced/patcher/patch/Option;)Ljava/lang/String; + public static final fun removeOverlayBackground (Lapp/revanced/patcher/patch/ResourcePatchContext;[Ljava/lang/String;[Ljava/lang/String;)V + public static final fun removeStringsElements (Lapp/revanced/patcher/patch/ResourcePatchContext;[Ljava/lang/String;)V + public static final fun removeStringsElements (Lapp/revanced/patcher/patch/ResourcePatchContext;[Ljava/lang/String;[Ljava/lang/String;)V + public static final fun underBarOrThrow (Lapp/revanced/patcher/patch/Option;)Ljava/lang/String; + public static final fun updatePathData (Lorg/w3c/dom/Document;Ljava/lang/String;)V + public static final fun valueOrThrow (Lapp/revanced/patcher/patch/Option;)I + public static final fun valueOrThrow (Lapp/revanced/patcher/patch/Option;)Ljava/lang/String; +} + +public final class app/revanced/util/fingerprint/LegacyFingerprintKt { + public static final fun injectLiteralInstructionBooleanCall (Lapp/revanced/patcher/patch/BytecodePatchContext;Lkotlin/Pair;JLjava/lang/String;)V + public static final fun injectLiteralInstructionViewCall (Lapp/revanced/patcher/patch/BytecodePatchContext;Lkotlin/Pair;JLjava/lang/String;)V +} + diff --git a/patches/build.gradle.kts b/patches/build.gradle.kts new file mode 100644 index 000000000..243c1678d --- /dev/null +++ b/patches/build.gradle.kts @@ -0,0 +1,55 @@ +group = "app.revanced" + +patches { + about { + name = "ReVanced Patches" + description = "Patches for ReVanced" + source = "git@github.com:revanced/revanced-patches.git" + author = "ReVanced" + contact = "contact@revanced.app" + website = "https://revanced.app" + license = "GNU General Public License v3.0" + } +} + +dependencies { + // Used by JsonGenerator. + implementation(libs.gson) +} + +tasks { + jar { + exclude("app/revanced/generator") + } + register("generatePatchesFiles") { + description = "Generate patches files" + + dependsOn(build) + + classpath = sourceSets["main"].runtimeClasspath + mainClass.set("app.revanced.generator.MainKt") + } + // Used by gradle-semantic-release-plugin. + publish { + dependsOn("generatePatchesFiles") + } +} + +kotlin { + compilerOptions { + freeCompilerArgs = listOf("-Xcontext-receivers") + } +} + +publishing { + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/anddea/revanced-patches") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/generator/JsonPatchesFileGenerator.kt b/patches/src/main/kotlin/app/revanced/generator/JsonPatchesFileGenerator.kt new file mode 100644 index 000000000..2e916af51 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/generator/JsonPatchesFileGenerator.kt @@ -0,0 +1,57 @@ +package app.revanced.generator + +import app.revanced.patcher.patch.Patch +import com.google.gson.GsonBuilder +import java.io.File + +typealias PackageName = String +typealias VersionName = String + +internal class JsonPatchesFileGenerator : PatchesFileGenerator { + override fun generate(patches: Set>) { + val patchesJson = File("../patches.json") + patches.sortedBy { it.name }.map { + JsonPatch( + it.name!!, + it.description, + it.use, + it.dependencies.map { dependency -> dependency.name ?: dependency.toString() }, + it.compatiblePackages?.associate { (packageName, versions) -> packageName to versions }, + it.options.values.map { option -> + JsonPatch.Option( + option.key, + option.title, + option.description, + option.required, + option.type.toString(), + option.default, + option.values, + ) + }, + ) + }.let { + patchesJson.writeText(GsonBuilder().serializeNulls().setPrettyPrinting().create().toJson(it)) + } + } + + @Suppress("unused") + private class JsonPatch( + val name: String? = null, + val description: String? = null, + val use: Boolean = true, + val dependencies: List, + val compatiblePackages: Map?>? = null, + val options: List

\n") + appendLine(tableHeader) + patches.sortedBy { it.name }.forEach { patch -> + val supportedVersionArray = + patch.compatiblePackages?.lastOrNull()?.second + val supportedVersion = + if (supportedVersionArray?.isNotEmpty() == true) { + val minVersion = supportedVersionArray.elementAt(0) + val maxVersion = + supportedVersionArray.elementAt(supportedVersionArray.size - 1) + if (minVersion == maxVersion) + maxVersion + else + "$minVersion ~ $maxVersion" + } else if (exception.containsKey(pkg)) + exception[pkg] + "+" + else + "ALL" + + appendLine( + "| `${patch.name}` " + + "| ${patch.description} " + + "| $supportedVersion |" + ) + } + appendLine("
\n") + } + } + + // copy the contents of the temp file to 'README.md' + StringBuilder(readMeFile.readText()) + .replace(Regex("\\{\\{\\s?table\\s?}}"), output.toString()) + .let(readMeFile::writeText) + } + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/versioncode/ChangeVersionCodePatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/versioncode/ChangeVersionCodePatch.kt new file mode 100644 index 000000000..b4c6b5e4d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/versioncode/ChangeVersionCodePatch.kt @@ -0,0 +1,58 @@ +package app.revanced.patches.all.misc.versioncode + +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.util.getNode +import app.revanced.util.valueOrThrow +import org.w3c.dom.Element + +private const val MAX_VALUE = Int.MAX_VALUE.toString() + +@Suppress("unused") +val changeVersionCodePatch = resourcePatch( + name = "Change version code", + description = "Changes the version code of the app to the value specified in patch options. " + + "Except when mounting, this can prevent app stores from updating the app and allow " + + "the app to be installed over an existing installation that has a higher version code. " + + "By default, the highest version code is set.", + use = false, +) { + val versionCodeOption = stringOption( + key = "versionCode", + default = MAX_VALUE, + values = mapOf( + "Lowest" to "1", + "Highest" to MAX_VALUE, + ), + title = "Version code", + description = "The version code to use. (1 ~ $MAX_VALUE)", + required = true, + ) + + execute { + fun throwVersionCodeException(versionCodeString: String): PatchException = + PatchException( + "Invalid versionCode: $versionCodeString, " + + "Version code should be larger than 1 and smaller than $MAX_VALUE." + ) + + val versionCodeString = versionCodeOption.valueOrThrow() + val versionCode: Int + + try { + versionCode = Integer.parseInt(versionCodeString) + } catch (e: NumberFormatException) { + throw throwVersionCodeException(versionCodeString) + } + + if (versionCode < 1) { + throw throwVersionCodeException(versionCodeString) + } + + document("AndroidManifest.xml").use { document -> + val manifestElement = document.getNode("manifest") as Element + manifestElement.setAttribute("android:versionCode", "$versionCode") + } + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/account/components/AccountComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/account/components/AccountComponentsPatch.kt new file mode 100644 index 000000000..f15dc184b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/account/components/AccountComponentsPatch.kt @@ -0,0 +1,160 @@ +package app.revanced.patches.music.account.components + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.ACCOUNT_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.patch.PatchList.HIDE_ACCOUNT_COMPONENTS +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addPreferenceWithIntent +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +@Suppress("unused") +val accountComponentsPatch = bytecodePatch( + HIDE_ACCOUNT_COMPONENTS.title, + HIDE_ACCOUNT_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + sharedResourceIdPatch, + settingsPatch, + ) + + execute { + + // region patch for hide account menu + + menuEntryFingerprint.methodOrThrow().apply { + val textIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setText" + } + val viewIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "addView" + } + + val textRegister = getInstruction(textIndex).registerD + val viewRegister = getInstruction(viewIndex).registerD + + addInstruction( + textIndex + 1, + "invoke-static {v$textRegister, v$viewRegister}, " + + "$ACCOUNT_CLASS_DESCRIPTOR->hideAccountMenu(Ljava/lang/CharSequence;Landroid/view/View;)V" + ) + } + + // endregion + + // region patch for hide handle + + // account menu + accountSwitcherAccessibilityLabelFingerprint.methodOrThrow().apply { + val textColorIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setTextColor" + } + val setVisibilityIndex = indexOfFirstInstructionOrThrow(textColorIndex) { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setVisibility" + } + val textViewInstruction = + getInstruction(setVisibilityIndex) + + replaceInstruction( + setVisibilityIndex, + "invoke-static {v${textViewInstruction.registerC}, v${textViewInstruction.registerD}}, " + + "$ACCOUNT_CLASS_DESCRIPTOR->hideHandle(Landroid/widget/TextView;I)V" + ) + } + + // account switcher + namesInactiveAccountThumbnailSizeFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.startIndex + val targetRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex, """ + invoke-static {v$targetRegister}, $ACCOUNT_CLASS_DESCRIPTOR->hideHandle(Z)Z + move-result v$targetRegister + """ + ) + } + } + + // endregion + + // region patch for hide terms container + + termsOfServiceFingerprint.methodOrThrow().apply { + val insertIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.name == "setVisibility" && + reference.definingClass.endsWith("/PrivacyTosFooter;") + } + val visibilityRegister = + getInstruction(insertIndex).registerD + + addInstruction( + insertIndex + 1, + "const/4 v$visibilityRegister, 0x0" + ) + addInstructions( + insertIndex, """ + invoke-static {}, $ACCOUNT_CLASS_DESCRIPTOR->hideTermsContainer()I + move-result v$visibilityRegister + """ + ) + + } + + // endregion + + addSwitchPreference( + CategoryType.ACCOUNT, + "revanced_hide_account_menu", + "false" + ) + addPreferenceWithIntent( + CategoryType.ACCOUNT, + "revanced_hide_account_menu_filter_strings", + "revanced_hide_account_menu" + ) + addSwitchPreference( + CategoryType.ACCOUNT, + "revanced_hide_account_menu_empty_component", + "false", + "revanced_hide_account_menu" + ) + addSwitchPreference( + CategoryType.ACCOUNT, + "revanced_hide_handle", + "true" + ) + addSwitchPreference( + CategoryType.ACCOUNT, + "revanced_hide_terms_container", + "false" + ) + + updatePatchStatus(HIDE_ACCOUNT_COMPONENTS) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/account/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/account/components/Fingerprints.kt new file mode 100644 index 000000000..8af714611 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/account/components/Fingerprints.kt @@ -0,0 +1,47 @@ +package app.revanced.patches.music.account.components + +import app.revanced.patches.music.utils.resourceid.accountSwitcherAccessibility +import app.revanced.patches.music.utils.resourceid.menuEntry +import app.revanced.patches.music.utils.resourceid.namesInactiveAccountThumbnailSize +import app.revanced.patches.music.utils.resourceid.tosFooter +import app.revanced.util.fingerprint.legacyFingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint( + name = "accountSwitcherAccessibilityLabelFingerprint", + returnType = "V", + parameters = listOf("L", "Ljava/lang/Object;"), + literals = listOf(accountSwitcherAccessibility) +) + +internal val menuEntryFingerprint = legacyFingerprint( + name = "menuEntryFingerprint", + returnType = "V", + literals = listOf(menuEntry) +) + +internal val namesInactiveAccountThumbnailSizeFingerprint = legacyFingerprint( + name = "namesInactiveAccountThumbnailSizeFingerprint", + returnType = "V", + parameters = listOf("L", "Ljava/lang/Object;"), + opcodes = listOf( + Opcode.IF_NEZ, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.GOTO, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IF_EQZ + ), + literals = listOf(namesInactiveAccountThumbnailSize) +) + +internal val termsOfServiceFingerprint = legacyFingerprint( + name = "termsOfServiceFingerprint", + returnType = "Landroid/view/View;", + literals = listOf(tosFooter) +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/ActionBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/ActionBarComponentsPatch.kt new file mode 100644 index 000000000..c596d8590 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/ActionBarComponentsPatch.kt @@ -0,0 +1,213 @@ +package app.revanced.patches.music.actionbar.components + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.ACTIONBAR_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.music.utils.patch.PatchList.HIDE_ACTION_BAR_COMPONENTS +import app.revanced.patches.music.utils.playservice.is_7_17_or_greater +import app.revanced.patches.music.utils.playservice.is_7_25_or_greater +import app.revanced.patches.music.utils.playservice.versionCheckPatch +import app.revanced.patches.music.utils.resourceid.likeDislikeContainer +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addPreferenceWithIntent +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.music.video.information.videoInformationPatch +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import kotlin.math.min + +private const val FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/ActionButtonsFilter;" + +@Suppress("unused") +val actionBarComponentsPatch = bytecodePatch( + HIDE_ACTION_BAR_COMPONENTS.title, + HIDE_ACTION_BAR_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + lithoFilterPatch, + sharedResourceIdPatch, + videoInformationPatch, + versionCheckPatch, + ) + + execute { + if (is_7_17_or_greater) { + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + } + + if (!is_7_25_or_greater) { + actionBarComponentFingerprint.matchOrThrow().let { + it.method.apply { + // hook download button + val addViewIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "addView" + } + val addViewRegister = + getInstruction(addViewIndex).registerD + + addInstruction( + addViewIndex + 1, + "invoke-static {v$addViewRegister}, $ACTIONBAR_CLASS_DESCRIPTOR->inAppDownloadButtonOnClick(Landroid/view/View;)V" + ) + + // hide action button label + val noLabelIndex = indexOfFirstInstructionOrThrow { + val reference = (this as? ReferenceInstruction)?.reference.toString() + opcode == Opcode.INVOKE_DIRECT && + reference.endsWith("(Landroid/content/Context;)V") && + !reference.contains("Lcom/google/android/libraries/youtube/common/ui/YouTubeButton;") + } - 2 + val replaceIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_DIRECT && + (this as? ReferenceInstruction)?.reference.toString() + .endsWith("Lcom/google/android/libraries/youtube/common/ui/YouTubeButton;->(Landroid/content/Context;)V") + } - 2 + val replaceInstruction = getInstruction(replaceIndex) + val replaceReference = + getInstruction(replaceIndex).reference + + addInstructionsWithLabels( + replaceIndex + 1, """ + invoke-static {}, $ACTIONBAR_CLASS_DESCRIPTOR->hideActionBarLabel()Z + move-result v${replaceInstruction.registerA} + if-nez v${replaceInstruction.registerA}, :hidden + iget-object v${replaceInstruction.registerA}, v${replaceInstruction.registerB}, $replaceReference + """, ExternalLabel("hidden", getInstruction(noLabelIndex)) + ) + removeInstruction(replaceIndex) + + // hide action button + val hasNextIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_INTERFACE && + getReference()?.name == "hasNext" + } + val freeRegister = min(implementation!!.registerCount - parameters.size - 2, 15) + + val spannedIndex = indexOfFirstInstructionOrThrow { + getReference()?.returnType == "Landroid/text/Spanned;" + } + val spannedRegister = + getInstruction(spannedIndex).registerC + val spannedReference = + getInstruction(spannedIndex).reference + + addInstructionsWithLabels( + spannedIndex + 1, """ + invoke-static {}, $ACTIONBAR_CLASS_DESCRIPTOR->hideActionButton()Z + move-result v$freeRegister + if-nez v$freeRegister, :hidden + invoke-static {v$spannedRegister}, $spannedReference + """, ExternalLabel("hidden", getInstruction(hasNextIndex)) + ) + removeInstruction(spannedIndex) + + // set action button identifier + val buttonTypeDownloadIndex = it.patternMatch!!.startIndex + 1 + val buttonTypeDownloadRegister = + getInstruction(buttonTypeDownloadIndex).registerA + + val buttonTypeIndex = it.patternMatch!!.endIndex - 1 + val buttonTypeRegister = + getInstruction(buttonTypeIndex).registerA + + addInstruction( + buttonTypeIndex + 2, + "invoke-static {v$buttonTypeRegister}, $ACTIONBAR_CLASS_DESCRIPTOR->setButtonType(Ljava/lang/Object;)V" + ) + + addInstruction( + buttonTypeDownloadIndex, + "invoke-static {v$buttonTypeDownloadRegister}, $ACTIONBAR_CLASS_DESCRIPTOR->setButtonTypeDownload(I)V" + ) + } + } + } + + likeDislikeContainerFingerprint.methodOrThrow().apply { + val insertIndex = + indexOfFirstLiteralInstructionOrThrow(likeDislikeContainer) + 2 + val insertRegister = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex + 1, + "invoke-static {v$insertRegister}, $ACTIONBAR_CLASS_DESCRIPTOR->hideLikeDislikeButton(Landroid/view/View;)V" + ) + } + + addSwitchPreference( + CategoryType.ACTION_BAR, + "revanced_hide_action_button_like_dislike", + "false" + ) + addSwitchPreference( + CategoryType.ACTION_BAR, + "revanced_hide_action_button_comment", + "false" + ) + addSwitchPreference( + CategoryType.ACTION_BAR, + "revanced_hide_action_button_add_to_playlist", + "false" + ) + addSwitchPreference( + CategoryType.ACTION_BAR, + "revanced_hide_action_button_download", + "false" + ) + addSwitchPreference( + CategoryType.ACTION_BAR, + "revanced_hide_action_button_share", + "false" + ) + addSwitchPreference( + CategoryType.ACTION_BAR, + "revanced_hide_action_button_radio", + "false" + ) + if (!is_7_25_or_greater) { + addSwitchPreference( + CategoryType.ACTION_BAR, + "revanced_hide_action_button_label", + "false" + ) + addSwitchPreference( + CategoryType.ACTION_BAR, + "revanced_external_downloader_action", + "false" + ) + addPreferenceWithIntent( + CategoryType.ACTION_BAR, + "revanced_external_downloader_package_name", + "revanced_external_downloader_action" + ) + } + + updatePatchStatus(HIDE_ACTION_BAR_COMPONENTS) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/Fingerprints.kt new file mode 100644 index 000000000..e7e9eb934 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/Fingerprints.kt @@ -0,0 +1,30 @@ +package app.revanced.patches.music.actionbar.components + +import app.revanced.patches.music.utils.resourceid.likeDislikeContainer +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val actionBarComponentFingerprint = legacyFingerprint( + name = "actionBarComponentFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "L"), + opcodes = listOf( + Opcode.AND_INT_LIT16, + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, + Opcode.IF_NEZ, + Opcode.SGET_OBJECT, + Opcode.SGET_OBJECT + ), + literals = listOf(99180L), +) + +internal val likeDislikeContainerFingerprint = legacyFingerprint( + name = "likeDislikeContainerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf(likeDislikeContainer) +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/ads/general/AdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/ads/general/AdsPatch.kt new file mode 100644 index 000000000..15fc2d864 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/ads/general/AdsPatch.kt @@ -0,0 +1,190 @@ +package app.revanced.patches.music.ads.general + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.navigation.components.navigationBarComponentsPatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.ADS_PATH +import app.revanced.patches.music.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.music.utils.patch.PatchList.HIDE_ADS +import app.revanced.patches.music.utils.resourceid.buttonContainer +import app.revanced.patches.music.utils.resourceid.floatingLayout +import app.revanced.patches.music.utils.resourceid.interstitialsContainer +import app.revanced.patches.music.utils.resourceid.privacyTosFooter +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.ads.baseAdsPatch +import app.revanced.patches.shared.ads.hookLithoFullscreenAds +import app.revanced.patches.shared.ads.hookNonLithoFullscreenAds +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +private const val ADS_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/AdsFilter;" + +private const val PREMIUM_PROMOTION_POP_UP_CLASS_DESCRIPTOR = + "$ADS_PATH/PremiumPromotionPatch;" + +private const val PREMIUM_PROMOTION_BANNER_CLASS_DESCRIPTOR = + "$ADS_PATH/PremiumRenewalPatch;" + +@Suppress("unused") +val adsPatch = bytecodePatch( + HIDE_ADS.title, + HIDE_ADS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + baseAdsPatch("$ADS_PATH/MusicAdsPatch;", "hideMusicAds"), + lithoFilterPatch, + navigationBarComponentsPatch, // for 'Hide upgrade button' setting + sharedResourceIdPatch, + ) + + execute { + + // region patch for hide fullscreen ads + + // non-litho view, used in some old clients + interstitialsContainerFingerprint + .methodOrThrow() + .hookNonLithoFullscreenAds(interstitialsContainer) + + // litho view, used in 'ShowDialogCommandOuterClass' in innertube + showDialogCommandFingerprint + .matchOrThrow() + .hookLithoFullscreenAds() + + // endregion + + // region patch for hide premium promotion popup + + floatingLayoutFingerprint.methodOrThrow().apply { + val targetIndex = indexOfFirstLiteralInstructionOrThrow(floatingLayout) + 2 + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $PREMIUM_PROMOTION_POP_UP_CLASS_DESCRIPTOR->hidePremiumPromotion(Landroid/view/View;)V" + ) + } + + // endregion + + // region patch for hide premium renewal banner + + notifierShelfFingerprint.methodOrThrow().apply { + val linearLayoutIndex = + indexOfFirstLiteralInstructionOrThrow(buttonContainer) + 3 + val linearLayoutRegister = + getInstruction(linearLayoutIndex).registerA + + addInstruction( + linearLayoutIndex + 1, + "invoke-static {v$linearLayoutRegister}, $PREMIUM_PROMOTION_BANNER_CLASS_DESCRIPTOR->hidePremiumRenewal(Landroid/widget/LinearLayout;)V" + ) + } + + // endregion + + // region patch for hide get premium + + // get premium button at the top of the account switching menu + getPremiumTextViewFingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.patternMatch!!.startIndex + val register = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex + 1, + "const/4 v$register, 0x0" + ) + } + } + + // get premium button at the bottom of the account switching menu + accountMenuFooterFingerprint.methodOrThrow().apply { + val constIndex = + indexOfFirstLiteralInstructionOrThrow(privacyTosFooter) + val walkerIndex = + indexOfFirstInstructionOrThrow(constIndex + 2, Opcode.INVOKE_VIRTUAL) + val viewIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.IGET_OBJECT) + val viewReference = + getInstruction(viewIndex).reference.toString() + + val walkerMethod = getWalkerMethod(walkerIndex) + walkerMethod.apply { + val insertIndex = indexOfFirstInstructionOrThrow { + getReference()?.toString() == viewReference + } + val nullCheckIndex = + indexOfFirstInstructionOrThrow(insertIndex - 1, Opcode.IF_NEZ) + val nullCheckRegister = + getInstruction(nullCheckIndex).registerA + + addInstruction( + nullCheckIndex, + "const/4 v$nullCheckRegister, 0x0" + ) + } + } + + addLithoFilter(ADS_FILTER_CLASS_DESCRIPTOR) + + addSwitchPreference( + CategoryType.ADS, + "revanced_hide_fullscreen_ads", + "false" + ) + addSwitchPreference( + CategoryType.ADS, + "revanced_hide_general_ads", + "true" + ) + addSwitchPreference( + CategoryType.ADS, + "revanced_hide_music_ads", + "true" + ) + addSwitchPreference( + CategoryType.ADS, + "revanced_hide_paid_promotion_label", + "true" + ) + addSwitchPreference( + CategoryType.ADS, + "revanced_hide_premium_promotion", + "true" + ) + addSwitchPreference( + CategoryType.ADS, + "revanced_hide_premium_renewal", + "true" + ) + addSwitchPreference( + CategoryType.ADS, + "revanced_hide_promotion_alert_banner", + "true" + ) + + updatePatchStatus(HIDE_ADS) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/ads/general/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/ads/general/Fingerprints.kt new file mode 100644 index 000000000..1c06ae47d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/ads/general/Fingerprints.kt @@ -0,0 +1,87 @@ +package app.revanced.patches.music.ads.general + +import app.revanced.patches.music.utils.resourceid.buttonContainer +import app.revanced.patches.music.utils.resourceid.floatingLayout +import app.revanced.patches.music.utils.resourceid.interstitialsContainer +import app.revanced.patches.music.utils.resourceid.musicNotifierShelf +import app.revanced.patches.music.utils.resourceid.privacyTosFooter +import app.revanced.patches.music.utils.resourceid.slidingDialogAnimation +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val accountMenuFooterFingerprint = legacyFingerprint( + name = "accountMenuFooterFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + opcodes = listOf( + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + Opcode.INVOKE_VIRTUAL, + Opcode.IGET_OBJECT + ), + literals = listOf(privacyTosFooter) +) + +internal val floatingLayoutFingerprint = legacyFingerprint( + name = "floatingLayoutFingerprint", + returnType = "Landroid/view/View;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(floatingLayout) +) + +internal val getPremiumTextViewFingerprint = legacyFingerprint( + name = "getPremiumTextViewFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.IGET_BOOLEAN, + Opcode.CONST_4, + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_STATIC + ), + strings = listOf("FEmusic_history") +) + +internal val interstitialsContainerFingerprint = legacyFingerprint( + name = "interstitialsContainerFingerprint", + returnType = "V", + strings = listOf("overlay_controller_param"), + literals = listOf(interstitialsContainer) +) + +internal val notifierShelfFingerprint = legacyFingerprint( + name = "notifierShelfFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf(musicNotifierShelf, buttonContainer) +) + +internal val showDialogCommandFingerprint = legacyFingerprint( + name = "showDialogCommandFingerprint", + returnType = "V", + opcodes = listOf( + Opcode.IF_EQ, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.IGET, // get dialog code + ), + literals = listOf(slidingDialogAnimation), + // 6.26 and earlier has a different first parameter. + // Since this fingerprint is somewhat weak, work around by checking for both method parameter signatures. + customFingerprint = custom@{ method, _ -> + // 6.26 and earlier parameters are: "L", "L" + // 6.27+ parameters are "[B", "L" + val parameterTypes = method.parameterTypes + + parameterTypes.size == 2 && parameterTypes[1].startsWith("L") + }, +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/flyoutmenu/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/flyoutmenu/components/Fingerprints.kt new file mode 100644 index 000000000..2130cc7f2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/flyoutmenu/components/Fingerprints.kt @@ -0,0 +1,78 @@ +package app.revanced.patches.music.flyoutmenu.components + +import app.revanced.patches.music.utils.resourceid.endButtonsContainer +import app.revanced.patches.music.utils.resourceid.touchOutside +import app.revanced.patches.music.utils.resourceid.trimSilenceSwitch +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionReversed +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val endButtonsContainerFingerprint = legacyFingerprint( + name = "endButtonsContainerFingerprint", + returnType = "V", + literals = listOf(endButtonsContainer) +) + +internal val menuItemFingerprint = legacyFingerprint( + name = "menuItemFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.CHECK_CAST, + Opcode.INVOKE_DIRECT, + Opcode.MOVE_RESULT_OBJECT + ), + strings = listOf("toggleMenuItemMutations") +) + +internal val screenWidthFingerprint = legacyFingerprint( + name = "screenWidthFingerprint", + returnType = "Z", + parameters = listOf("L"), + opcodes = listOf(Opcode.IF_LT), + literals = listOf(600L) +) + +internal val screenWidthParentFingerprint = legacyFingerprint( + name = "screenWidthParentFingerprint", + returnType = "Landroid/graphics/Bitmap;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("Landroid/app/Activity;", "I"), + customFingerprint = { method, _ -> + method.indexOfFirstInstructionReversed { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "destroyDrawingCache" + } >= 0 + } +) + +internal val sleepTimerFingerprint = legacyFingerprint( + name = "sleepTimerFingerprint", + returnType = "Z", + parameters = emptyList(), + literals = listOf(45372767L) +) + +internal val touchOutsideFingerprint = legacyFingerprint( + name = "touchOutsideFingerprint", + returnType = "Landroid/view/View;", + literals = listOf(touchOutside) +) + +internal val trimSilenceConfigFingerprint = legacyFingerprint( + name = "trimSilenceConfigFingerprint", + returnType = "Z", + literals = listOf(45619123L) +) + +internal val trimSilenceSwitchFingerprint = legacyFingerprint( + name = "trimSilenceSwitchFingerprint", + returnType = "Landroid/view/View;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(trimSilenceSwitch) +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/flyoutmenu/components/FlyoutMenuComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/flyoutmenu/components/FlyoutMenuComponentsPatch.kt new file mode 100644 index 000000000..8332e4e01 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/flyoutmenu/components/FlyoutMenuComponentsPatch.kt @@ -0,0 +1,466 @@ +package app.revanced.patches.music.flyoutmenu.components + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.music.utils.extension.Constants.FLYOUT_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.flyoutmenu.flyoutMenuHookPatch +import app.revanced.patches.music.utils.patch.PatchList.FLYOUT_MENU_COMPONENTS +import app.revanced.patches.music.utils.playservice.is_6_36_or_greater +import app.revanced.patches.music.utils.playservice.versionCheckPatch +import app.revanced.patches.music.utils.resourceid.endButtonsContainer +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.music.utils.resourceid.trimSilenceSwitch +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.music.utils.videotype.videoTypeHookPatch +import app.revanced.patches.music.video.information.videoInformationPatch +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.copyResources +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.resolvable +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private val resourceFileArray = arrayOf( + "yt_outline_play_arrow_half_circle_black_24" +).map { "$it.png" }.toTypedArray() + +private val flyoutMenuComponentsResourcePatch = resourcePatch( + description = "flyoutMenuComponentsResourcePatch" +) { + execute { + arrayOf("xxxhdpi", "xxhdpi", "xhdpi", "hdpi", "mdpi") + .map { "drawable-$it" } + .map { directory -> + ResourceGroup( + directory, *resourceFileArray + ) + } + .let { resourceGroups -> + resourceGroups.forEach { + copyResources("music/flyout", it) + } + } + } +} + +private const val FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/PlayerFlyoutMenuFilter;" + +@Suppress("unused") +val flyoutMenuComponentsPatch = bytecodePatch( + FLYOUT_MENU_COMPONENTS.title, + FLYOUT_MENU_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + flyoutMenuComponentsResourcePatch, + flyoutMenuHookPatch, + lithoFilterPatch, + sharedResourceIdPatch, + versionCheckPatch, + videoInformationPatch, + videoTypeHookPatch, + ) + + execute { + var trimSilenceIncluded = false + + // region patch for enable compact dialog + + screenWidthFingerprint.matchOrThrow(screenWidthParentFingerprint).let { + it.method.apply { + val index = it.patternMatch!!.startIndex + val register = getInstruction(index).registerA + + addInstructions( + index, """ + invoke-static {v$register}, $FLYOUT_CLASS_DESCRIPTOR->enableCompactDialog(I)I + move-result v$register + """ + ) + } + } + + // endregion + + // region patch for enable trim silence + + if (trimSilenceConfigFingerprint.resolvable()) { + trimSilenceConfigFingerprint.injectLiteralInstructionBooleanCall( + 45619123L, + "$FLYOUT_CLASS_DESCRIPTOR->enableTrimSilence(Z)Z" + ) + + trimSilenceSwitchFingerprint.methodOrThrow().apply { + val constIndex = + indexOfFirstLiteralInstructionOrThrow(trimSilenceSwitch) + val onCheckedChangedListenerIndex = + indexOfFirstInstructionOrThrow(constIndex, Opcode.INVOKE_DIRECT) + val onCheckedChangedListenerReference = + getInstruction(onCheckedChangedListenerIndex).reference + val onCheckedChangedListenerDefiningClass = + (onCheckedChangedListenerReference as MethodReference).definingClass + + findMethodOrThrow(onCheckedChangedListenerDefiningClass) { + name == "onCheckedChanged" + }.apply { + val onCheckedChangedWalkerIndex = + indexOfFirstInstructionOrThrow { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.returnType == "V" && + reference.parameterTypes.size == 1 && + reference.parameterTypes[0] == "Z" + } + + getWalkerMethod(onCheckedChangedWalkerIndex).apply { + val insertIndex = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT) + val insertRegister = + getInstruction(insertIndex).registerA + + addInstructions( + insertIndex + 1, """ + invoke-static {v$insertRegister}, $FLYOUT_CLASS_DESCRIPTOR->enableTrimSilenceSwitch(Z)Z + move-result v$insertRegister + """ + ) + } + } + } + trimSilenceIncluded = true + } + + // endregion + + // region patch for hide flyout menu components and replace menu + + menuItemFingerprint.matchOrThrow().let { + it.method.apply { + val freeIndex = indexOfFirstInstructionOrThrow(Opcode.OR_INT_LIT16) + val textViewIndex = it.patternMatch!!.startIndex + val imageViewIndex = it.patternMatch!!.endIndex + + val freeRegister = + getInstruction(freeIndex).registerA + val textViewRegister = + getInstruction(textViewIndex).registerA + val imageViewRegister = + getInstruction(imageViewIndex).registerA + + val enumIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_STATIC + && (this as? ReferenceInstruction)?.reference.toString() + .contains("(I)L") + } + 1 + val enumRegister = getInstruction(enumIndex).registerA + + addInstructionsWithLabels( + enumIndex + 1, + """ + invoke-static {v$enumRegister, v$textViewRegister, v$imageViewRegister}, $FLYOUT_CLASS_DESCRIPTOR->replaceComponents(Ljava/lang/Enum;Landroid/widget/TextView;Landroid/widget/ImageView;)V + invoke-static {v$enumRegister}, $FLYOUT_CLASS_DESCRIPTOR->hideComponents(Ljava/lang/Enum;)Z + move-result v$freeRegister + if-nez v$freeRegister, :hide + """, + ExternalLabel("hide", getInstruction(implementation!!.instructions.lastIndex)) + ) + } + } + + touchOutsideFingerprint.methodOrThrow().apply { + val setOnClickListenerIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setOnClickListener" + } + val setOnClickListenerRegister = + getInstruction(setOnClickListenerIndex).registerC + + addInstruction( + setOnClickListenerIndex + 1, + "invoke-static {v$setOnClickListenerRegister}, $FLYOUT_CLASS_DESCRIPTOR->setTouchOutSideView(Landroid/view/View;)V" + ) + } + + endButtonsContainerFingerprint.methodOrThrow().apply { + val startIndex = + indexOfFirstLiteralInstructionOrThrow(endButtonsContainer) + val targetIndex = + indexOfFirstInstructionOrThrow(startIndex, Opcode.MOVE_RESULT_OBJECT) + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $FLYOUT_CLASS_DESCRIPTOR->hideLikeDislikeContainer(Landroid/view/View;)V" + ) + } + + // endregion + + // region patch for enable sleep timer + + /** + * Forces sleep timer menu to be enabled. + * This method may be desperate in the future. + */ + if (sleepTimerFingerprint.resolvable()) { + sleepTimerFingerprint.methodOrThrow().apply { + val insertIndex = implementation!!.instructions.lastIndex + val targetRegister = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex, + "const/4 v$targetRegister, 0x1" + ) + } + } + + // endregion + + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_enable_compact_dialog", + "false" + ) + if (trimSilenceIncluded) { + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_enable_trim_silence", + "false" + ) + } + if (is_6_36_or_greater) { + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_3_column_component", + "false", + false + ) + } + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_like_dislike", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_add_to_queue", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_captions", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_delete_playlist", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_dismiss_queue", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_download", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_edit_playlist", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_go_to_album", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_go_to_artist", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_go_to_episode", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_go_to_podcast", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_help", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_pin_to_speed_dial", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_play_next", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_quality", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_remove_from_library", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_remove_from_playlist", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_report", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_save_episode_for_later", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_save_to_library", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_save_to_playlist", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_share", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_shuffle_play", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_sleep_timer", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_start_radio", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_stats_for_nerds", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_subscribe", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_unpin_from_speed_dial", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_hide_flyout_menu_view_song_credit", + "false", + false + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_replace_flyout_menu_dismiss_queue", + "false" + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_replace_flyout_menu_dismiss_queue_continue_watch", + "true", + "revanced_replace_flyout_menu_dismiss_queue" + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_replace_flyout_menu_report", + "true" + ) + addSwitchPreference( + CategoryType.FLYOUT, + "revanced_replace_flyout_menu_report_only_player", + "true", + "revanced_replace_flyout_menu_report" + ) + + updatePatchStatus(FLYOUT_MENU_COMPONENTS) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/amoled/AmoledPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/amoled/AmoledPatch.kt new file mode 100644 index 000000000..c2a79075d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/amoled/AmoledPatch.kt @@ -0,0 +1,46 @@ +package app.revanced.patches.music.general.amoled + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.music.utils.patch.PatchList.AMOLED +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.drawable.addDrawableColorHook +import app.revanced.patches.shared.drawable.drawableColorHookPatch +import org.w3c.dom.Element + +@Suppress("unused") +val amoledPatch = resourcePatch( + AMOLED.title, + AMOLED.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + drawableColorHookPatch, + settingsPatch + ) + + execute { + addDrawableColorHook("$UTILS_PATH/DrawableColorPatch;->getLithoColor(I)I") + + document("res/values/colors.xml").use { document -> + val resourcesNode = document.getElementsByTagName("resources").item(0) as Element + + for (i in 0 until resourcesNode.childNodes.length) { + val node = resourcesNode.childNodes.item(i) as? Element ?: continue + + node.textContent = when (node.getAttribute("name")) { + "yt_black0", "yt_black1", "yt_black1_opacity95", "yt_black1_opacity98", "yt_black2", "yt_black3", + "yt_black4", "yt_status_bar_background_dark", "ytm_color_grey_12", "material_grey_850" -> "@android:color/black" + + else -> continue + } + } + } + + updatePatchStatus(AMOLED) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/autocaptions/AutoCaptionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/autocaptions/AutoCaptionsPatch.kt new file mode 100644 index 000000000..b79c38e23 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/autocaptions/AutoCaptionsPatch.kt @@ -0,0 +1,34 @@ +package app.revanced.patches.music.general.autocaptions + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.patch.PatchList.DISABLE_AUTO_CAPTIONS +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.captions.baseAutoCaptionsPatch + +@Suppress("unused") +val autoCaptionsPatch = bytecodePatch( + DISABLE_AUTO_CAPTIONS.title, + DISABLE_AUTO_CAPTIONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseAutoCaptionsPatch, + settingsPatch + ) + + execute { + addSwitchPreference( + CategoryType.GENERAL, + "revanced_disable_auto_captions", + "false" + ) + + updatePatchStatus(DISABLE_AUTO_CAPTIONS) + + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/components/Fingerprints.kt new file mode 100644 index 000000000..cc192d88c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/components/Fingerprints.kt @@ -0,0 +1,177 @@ +package app.revanced.patches.music.general.components + +import app.revanced.patches.music.utils.resourceid.chipCloud +import app.revanced.patches.music.utils.resourceid.historyMenuItem +import app.revanced.patches.music.utils.resourceid.musicTasteBuilderShelf +import app.revanced.patches.music.utils.resourceid.offlineSettingsMenuItem +import app.revanced.patches.music.utils.resourceid.playerOverlayChip +import app.revanced.patches.music.utils.resourceid.toolTipContentView +import app.revanced.patches.music.utils.resourceid.topBarMenuItemImageView +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionReversed +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val chipCloudFingerprint = legacyFingerprint( + name = "chipCloudFingerprint", + returnType = "V", + opcodes = listOf( + Opcode.CONST, + Opcode.CONST_4, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT + ), + literals = listOf(chipCloud), +) + +internal val contentPillFingerprint = legacyFingerprint( + name = "contentPillFingerprint", + returnType = "V", + strings = listOf("Content pill VE is null") +) + +internal val floatingButtonFingerprint = legacyFingerprint( + name = "floatingButtonFingerprint", + returnType = "V", + parameters = listOf("L"), + opcodes = listOf(Opcode.AND_INT_LIT16) +) + +internal val floatingButtonParentFingerprint = legacyFingerprint( + name = "floatingButtonParentFingerprint", + returnType = "V", + accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf(Opcode.INVOKE_DIRECT), + literals = listOf(259982244L), +) + +internal val historyMenuItemFingerprint = legacyFingerprint( + name = "historyMenuItemFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/Menu;"), + opcodes = listOf( + Opcode.INVOKE_INTERFACE, + Opcode.RETURN_VOID + ), + literals = listOf(historyMenuItem), + customFingerprint = { _, classDef -> + classDef.methods.count() == 5 + } +) + +internal val historyMenuItemOfflineTabFingerprint = legacyFingerprint( + name = "historyMenuItemOfflineTabFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/Menu;"), + opcodes = listOf( + Opcode.INVOKE_INTERFACE, + Opcode.RETURN_VOID + ), + literals = listOf(historyMenuItem, offlineSettingsMenuItem), +) + +internal val mediaRouteButtonFingerprint = legacyFingerprint( + name = "mediaRouteButtonFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + strings = listOf("MediaRouteButton") +) + +internal val parentToolMenuFingerprint = legacyFingerprint( + name = "parentToolMenuFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + opcodes = listOf( + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL, + Opcode.IGET, + ), + strings = listOf("pref_key_parent_tools"), + customFingerprint = { method, _ -> + method.name == "onSettingsLoaded" + } +) + +internal val playerOverlayChipFingerprint = legacyFingerprint( + name = "playerOverlayChipFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(playerOverlayChip), +) + +internal val preferenceScreenFingerprint = legacyFingerprint( + name = "preferenceScreenFingerprint", + returnType = "V", + customFingerprint = { method, _ -> + method.definingClass == "Lcom/google/android/apps/youtube/music/settings/fragment/SettingsHeadersFragment;" && + method.name == "onCreatePreferences" + } +) + +internal val searchBarFingerprint = legacyFingerprint( + name = "searchBarFingerprint", + returnType = "V", + customFingerprint = { method, _ -> + indexOfVisibilityInstruction(method) >= 0 + } +) + +fun indexOfVisibilityInstruction(method: Method) = + method.indexOfFirstInstructionReversed { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setVisibility" + } + +internal val searchBarParentFingerprint = legacyFingerprint( + name = "searchBarParentFingerprint", + returnType = "Landroid/content/Intent;", + strings = listOf("web_search") +) + +internal val soundSearchFingerprint = legacyFingerprint( + name = "soundSearchFingerprint", + parameters = emptyList(), + literals = listOf(45625491L), +) + +internal val tasteBuilderConstructorFingerprint = legacyFingerprint( + name = "tasteBuilderConstructorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf(musicTasteBuilderShelf), +) + +internal val tasteBuilderSyntheticFingerprint = legacyFingerprint( + name = "tasteBuilderSyntheticFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL or AccessFlags.SYNTHETIC, + parameters = listOf("L", "Ljava/lang/Object;"), + opcodes = listOf( + Opcode.IF_NEZ, + Opcode.IGET_OBJECT + ) +) + +internal val tooltipContentViewFingerprint = legacyFingerprint( + name = "tooltipContentViewFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + literals = listOf(toolTipContentView), +) + +internal val topBarMenuItemImageViewFingerprint = legacyFingerprint( + name = "topBarMenuItemImageViewFingerprint", + returnType = "Landroid/view/View;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(topBarMenuItemImageView), +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/components/LayoutComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/components/LayoutComponentsPatch.kt new file mode 100644 index 000000000..842351672 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/components/LayoutComponentsPatch.kt @@ -0,0 +1,426 @@ +package app.revanced.patches.music.general.components + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.music.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.extension.Constants.GENERAL_PATH +import app.revanced.patches.music.utils.patch.PatchList.HIDE_LAYOUT_COMPONENTS +import app.revanced.patches.music.utils.playservice.is_6_42_or_greater +import app.revanced.patches.music.utils.playservice.versionCheckPatch +import app.revanced.patches.music.utils.resourceid.musicTasteBuilderShelf +import app.revanced.patches.music.utils.resourceid.playerOverlayChip +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.music.utils.resourceid.topBarMenuItemImageView +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addPreferenceWithIntent +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.shared.settingmenu.settingsMenuPatch +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.fingerprint.resolvable +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private const val EXTENSION_SETTINGS_MENU_DESCRIPTOR = + "$GENERAL_PATH/SettingsMenuPatch;" +private const val CUSTOM_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/CustomFilter;" +private const val LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/LayoutComponentsFilter;" + +@Suppress("unused") +val layoutComponentsPatch = bytecodePatch( + HIDE_LAYOUT_COMPONENTS.title, + HIDE_LAYOUT_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + lithoFilterPatch, + sharedResourceIdPatch, + settingsMenuPatch, + versionCheckPatch, + ) + + execute { + var notificationButtonIncluded = false + var soundSearchButtonIncluded = false + + // region patch for hide cast button + + // hide cast button + mediaRouteButtonFingerprint.mutableClassOrThrow().let { + val setVisibilityMethod = + it.methods.find { method -> method.name == "setVisibility" } + + setVisibilityMethod?.addInstructions( + 0, """ + invoke-static {p1}, $GENERAL_CLASS_DESCRIPTOR->hideCastButton(I)I + move-result p1 + """ + ) ?: throw PatchException("Failed to find setVisibility method") + } + + // hide floating cast banner + playerOverlayChipFingerprint.methodOrThrow().apply { + val targetIndex = + indexOfFirstLiteralInstructionOrThrow(playerOverlayChip) + 2 + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $GENERAL_CLASS_DESCRIPTOR->hideCastButton(Landroid/view/View;)V" + ) + } + + // endregion + + // region patch for hide category bar + + chipCloudFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.endIndex + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-static { v$targetRegister }, $GENERAL_CLASS_DESCRIPTOR->hideCategoryBar(Landroid/view/View;)V" + ) + } + } + + // endregion + + // region patch for hide floating button + + floatingButtonFingerprint.methodOrThrow(floatingButtonParentFingerprint).apply { + addInstructionsWithLabels( + 1, """ + invoke-static {}, $GENERAL_CLASS_DESCRIPTOR->hideFloatingButton()Z + move-result v0 + if-eqz v0, :show + return-void + """, ExternalLabel("show", getInstruction(1)) + ) + } + + // endregion + + // region patch for hide history button + + setOf( + historyMenuItemFingerprint, + historyMenuItemOfflineTabFingerprint + ).forEach { fingerprint -> + fingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.patternMatch!!.startIndex + val insertRegister = + getInstruction(insertIndex).registerD + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $GENERAL_CLASS_DESCRIPTOR->hideHistoryButton(Z)Z + move-result v$insertRegister + """ + ) + } + } + } + + // endregion + + // region patch for hide notification button + + if (is_6_42_or_greater) { + topBarMenuItemImageViewFingerprint.methodOrThrow().apply { + val constIndex = + indexOfFirstLiteralInstructionOrThrow(topBarMenuItemImageView) + val targetIndex = + indexOfFirstInstructionOrThrow(constIndex, Opcode.MOVE_RESULT_OBJECT) + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $GENERAL_CLASS_DESCRIPTOR->hideNotificationButton(Landroid/view/View;)V" + ) + } + notificationButtonIncluded = true + } + + // endregion + + // region patch for hide setting menus + + preferenceScreenFingerprint.methodOrThrow().apply { + addInstructions( + implementation!!.instructions.lastIndex, """ + invoke-virtual/range {p0 .. p0}, Lcom/google/android/apps/youtube/music/settings/fragment/SettingsHeadersFragment;->getPreferenceScreen()Landroidx/preference/PreferenceScreen; + move-result-object v0 + invoke-static {v0}, $EXTENSION_SETTINGS_MENU_DESCRIPTOR->hideSettingsMenu(Landroidx/preference/PreferenceScreen;)V + """ + ) + } + + // The lowest version supported by the patch does not have parent tool settings + if (parentToolMenuFingerprint.resolvable()) { + parentToolMenuFingerprint.matchOrThrow().let { + it.method.apply { + val index = it.patternMatch!!.startIndex + 1 + val register = getInstruction(index).registerD + + addInstructions( + index, """ + invoke-static {v$register}, $EXTENSION_SETTINGS_MENU_DESCRIPTOR->hideParentToolsMenu(Z)Z + move-result v$register + """ + ) + } + } + } + + // endregion + + // region patch for hide sound search button + + if (soundSearchFingerprint.resolvable()) { + soundSearchFingerprint.injectLiteralInstructionBooleanCall( + 45625491L, + "$GENERAL_CLASS_DESCRIPTOR->hideSoundSearchButton(Z)Z" + ) + soundSearchButtonIncluded = true + } + + // endregion + + // region patch for hide tap to update button + + contentPillFingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, + """ + invoke-static {}, $GENERAL_CLASS_DESCRIPTOR->hideTapToUpdateButton()Z + move-result v0 + if-eqz v0, :show + return-void + """, ExternalLabel("show", getInstruction(0)) + ) + } + + // endregion + + // region patch for hide taste builder + + tasteBuilderConstructorFingerprint.methodOrThrow().apply { + val constIndex = + indexOfFirstLiteralInstructionOrThrow(musicTasteBuilderShelf) + val targetIndex = + indexOfFirstInstructionOrThrow(constIndex, Opcode.MOVE_RESULT_OBJECT) + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $GENERAL_CLASS_DESCRIPTOR->hideTasteBuilder(Landroid/view/View;)V" + ) + } + + tasteBuilderSyntheticFingerprint.matchOrThrow(tasteBuilderConstructorFingerprint).let { + it.method.apply { + val insertIndex = it.patternMatch!!.startIndex + val insertRegister = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex, + "const/4 v$insertRegister, 0x0" + ) + } + } + + // endregion + + // region patch for hide tooltip content + + tooltipContentViewFingerprint.methodOrThrow().addInstruction( + 0, + "return-void" + ) + + // endregion + + // region patch for hide voice search button + + searchBarFingerprint.methodOrThrow(searchBarParentFingerprint).apply { + val setVisibilityIndex = indexOfVisibilityInstruction(this) + val setVisibilityInstruction = + getInstruction(setVisibilityIndex) + + replaceInstruction( + setVisibilityIndex, + "invoke-static {v${setVisibilityInstruction.registerC}, v${setVisibilityInstruction.registerD}}, " + + "$GENERAL_CLASS_DESCRIPTOR->hideVoiceSearchButton(Landroid/widget/ImageView;I)V" + ) + } + + // endregion + + addLithoFilter(CUSTOM_FILTER_CLASS_DESCRIPTOR) + addLithoFilter(LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR) + + addSwitchPreference( + CategoryType.GENERAL, + "revanced_custom_filter", + "false" + ) + addPreferenceWithIntent( + CategoryType.GENERAL, + "revanced_custom_filter_strings", + "revanced_custom_filter" + ) + addSwitchPreference( + CategoryType.GENERAL, + "revanced_hide_button_shelf", + "false" + ) + addSwitchPreference( + CategoryType.GENERAL, + "revanced_hide_carousel_shelf", + "false" + ) + addSwitchPreference( + CategoryType.GENERAL, + "revanced_hide_playlist_card_shelf", + "false" + ) + addSwitchPreference( + CategoryType.GENERAL, + "revanced_hide_samples_shelf", + "false" + ) + addSwitchPreference( + CategoryType.GENERAL, + "revanced_hide_cast_button", + "true" + ) + addSwitchPreference( + CategoryType.GENERAL, + "revanced_hide_category_bar", + "false" + ) + addSwitchPreference( + CategoryType.GENERAL, + "revanced_hide_floating_button", + "false" + ) + addSwitchPreference( + CategoryType.GENERAL, + "revanced_hide_tap_to_update_button", + "false" + ) + addSwitchPreference( + CategoryType.GENERAL, + "revanced_hide_history_button", + "false" + ) + if (notificationButtonIncluded) { + addSwitchPreference( + CategoryType.GENERAL, + "revanced_hide_notification_button", + "false" + ) + } + if (soundSearchButtonIncluded) { + addSwitchPreference( + CategoryType.GENERAL, + "revanced_hide_sound_search_button", + "false" + ) + } + addSwitchPreference( + CategoryType.GENERAL, + "revanced_hide_voice_search_button", + "false" + ) + + addSwitchPreference( + CategoryType.SETTINGS, + "revanced_hide_settings_menu_parent_tools", + "false", + false + ) + addSwitchPreference( + CategoryType.SETTINGS, + "revanced_hide_settings_menu_general", + "false", + false + ) + addSwitchPreference( + CategoryType.SETTINGS, + "revanced_hide_settings_menu_playback", + "false", + false + ) + addSwitchPreference( + CategoryType.SETTINGS, + "revanced_hide_settings_menu_data_saving", + "false", + false + ) + addSwitchPreference( + CategoryType.SETTINGS, + "revanced_hide_settings_menu_downloads_and_storage", + "false", + false + ) + addSwitchPreference( + CategoryType.SETTINGS, + "revanced_hide_settings_menu_notification", + "false", + false + ) + addSwitchPreference( + CategoryType.SETTINGS, + "revanced_hide_settings_menu_privacy_and_location", + "false", + false + ) + addSwitchPreference( + CategoryType.SETTINGS, + "revanced_hide_settings_menu_recommendations", + "false", + false + ) + addSwitchPreference( + CategoryType.SETTINGS, + "revanced_hide_settings_menu_paid_memberships", + "true", + false + ) + addSwitchPreference( + CategoryType.SETTINGS, + "revanced_hide_settings_menu_about", + "false", + false + ) + + updatePatchStatus(HIDE_LAYOUT_COMPONENTS) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/dialog/ViewerDiscretionDialogPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/dialog/ViewerDiscretionDialogPatch.kt new file mode 100644 index 000000000..83b03bfac --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/dialog/ViewerDiscretionDialogPatch.kt @@ -0,0 +1,35 @@ +package app.revanced.patches.music.general.dialog + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.patch.PatchList.REMOVE_VIEWER_DISCRETION_DIALOG +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.dialog.baseViewerDiscretionDialogPatch + +@Suppress("unused") +val viewerDiscretionDialogPatch = bytecodePatch( + REMOVE_VIEWER_DISCRETION_DIALOG.title, + REMOVE_VIEWER_DISCRETION_DIALOG.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseViewerDiscretionDialogPatch(GENERAL_CLASS_DESCRIPTOR), + settingsPatch, + ) + + execute { + addSwitchPreference( + CategoryType.GENERAL, + "revanced_remove_viewer_discretion_dialog", + "false" + ) + + updatePatchStatus(REMOVE_VIEWER_DISCRETION_DIALOG) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/landscapemode/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/landscapemode/Fingerprints.kt new file mode 100644 index 000000000..b57a8fb4e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/landscapemode/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.music.general.landscapemode + +import app.revanced.patches.music.utils.resourceid.isTablet +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val tabletIdentifierFingerprint = legacyFingerprint( + name = "tabletIdentifierFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("L"), + opcodes = listOf( + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT + ), + literals = listOf(isTablet) +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/landscapemode/LandScapeModePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/landscapemode/LandScapeModePatch.kt new file mode 100644 index 000000000..1d40e3b55 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/landscapemode/LandScapeModePatch.kt @@ -0,0 +1,53 @@ +package app.revanced.patches.music.general.landscapemode + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.patch.PatchList.ENABLE_LANDSCAPE_MODE +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.util.fingerprint.matchOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +@Suppress("unused") +val landScapeModePatch = bytecodePatch( + ENABLE_LANDSCAPE_MODE.title, + ENABLE_LANDSCAPE_MODE.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + sharedResourceIdPatch, + settingsPatch, + ) + + execute { + tabletIdentifierFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.endIndex + val targetRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + invoke-static {v$targetRegister}, $GENERAL_CLASS_DESCRIPTOR->enableLandScapeMode(Z)Z + move-result v$targetRegister + """ + ) + } + } + + addSwitchPreference( + CategoryType.GENERAL, + "revanced_enable_landscape_mode", + "false" + ) + + updatePatchStatus(ENABLE_LANDSCAPE_MODE) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/oldstylelibraryshelf/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/oldstylelibraryshelf/Fingerprints.kt new file mode 100644 index 000000000..fcb45c736 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/oldstylelibraryshelf/Fingerprints.kt @@ -0,0 +1,15 @@ +package app.revanced.patches.music.general.oldstylelibraryshelf + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val browseIdFingerprint = legacyFingerprint( + name = "browseIdFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + strings = listOf("FEmusic_offline"), + literals = listOf(45358178L), +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/oldstylelibraryshelf/OldStyleLibraryShelfPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/oldstylelibraryshelf/OldStyleLibraryShelfPatch.kt new file mode 100644 index 000000000..ad09f98cb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/oldstylelibraryshelf/OldStyleLibraryShelfPatch.kt @@ -0,0 +1,53 @@ +package app.revanced.patches.music.general.oldstylelibraryshelf + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.patch.PatchList.RESTORE_OLD_STYLE_LIBRARY_SHELF +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstStringInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +@Suppress("unused") +val oldStyleLibraryShelfPatch = bytecodePatch( + RESTORE_OLD_STYLE_LIBRARY_SHELF.title, + RESTORE_OLD_STYLE_LIBRARY_SHELF.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + + browseIdFingerprint.methodOrThrow().apply { + val stringIndex = indexOfFirstStringInstructionOrThrow("FEmusic_offline") + val targetIndex = + indexOfFirstInstructionReversedOrThrow(stringIndex, Opcode.IGET_OBJECT) + val targetRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + invoke-static {v$targetRegister}, $GENERAL_CLASS_DESCRIPTOR->restoreOldStyleLibraryShelf(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$targetRegister + """ + ) + } + + addSwitchPreference( + CategoryType.GENERAL, + "revanced_restore_old_style_library_shelf", + "false" + ) + + updatePatchStatus(RESTORE_OLD_STYLE_LIBRARY_SHELF) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/redirection/DislikeRedirectionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/redirection/DislikeRedirectionPatch.kt new file mode 100644 index 000000000..a39582645 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/redirection/DislikeRedirectionPatch.kt @@ -0,0 +1,97 @@ +package app.revanced.patches.music.general.redirection + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.patch.PatchList.DISABLE_DISLIKE_REDIRECTION +import app.revanced.patches.music.utils.pendingIntentReceiverFingerprint +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstStringInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.Reference + +@Suppress("unused") +val dislikeRedirectionPatch = bytecodePatch( + DISABLE_DISLIKE_REDIRECTION.title, + DISABLE_DISLIKE_REDIRECTION.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + lateinit var onClickReference: Reference + + pendingIntentReceiverFingerprint.methodOrThrow().apply { + val startIndex = indexOfFirstStringInstructionOrThrow("YTM Dislike") + val onClickRelayIndex = + indexOfFirstInstructionReversedOrThrow(startIndex, Opcode.INVOKE_VIRTUAL) + val onClickRelayMethod = getWalkerMethod(onClickRelayIndex) + + onClickRelayMethod.apply { + val onClickMethodIndex = + indexOfFirstInstructionReversedOrThrow(Opcode.INVOKE_DIRECT) + val onClickMethod = getWalkerMethod(onClickMethodIndex) + + onClickMethod.apply { + val onClickIndex = indexOfFirstInstructionOrThrow { + val reference = + ((this as? ReferenceInstruction)?.reference as? MethodReference) + + opcode == Opcode.INVOKE_INTERFACE && + reference?.returnType == "V" && + reference.parameterTypes.size == 1 + } + onClickReference = + getInstruction(onClickIndex).reference + + disableDislikeRedirection(onClickIndex) + } + } + } + + dislikeButtonOnClickListenerFingerprint.methodOrThrow().apply { + val onClickIndex = indexOfFirstInstructionOrThrow { + getReference()?.toString() == onClickReference.toString() + } + disableDislikeRedirection(onClickIndex) + } + + addSwitchPreference( + CategoryType.GENERAL, + "revanced_disable_dislike_redirection", + "false" + ) + + updatePatchStatus(DISABLE_DISLIKE_REDIRECTION) + + } +} + +private fun MutableMethod.disableDislikeRedirection(onClickIndex: Int) { + val targetIndex = indexOfFirstInstructionReversedOrThrow(onClickIndex, Opcode.IF_EQZ) + val insertRegister = getInstruction(targetIndex).registerA + + addInstructionsWithLabels( + targetIndex + 1, """ + invoke-static {}, $GENERAL_CLASS_DESCRIPTOR->disableDislikeRedirection()Z + move-result v$insertRegister + if-nez v$insertRegister, :disable + """, ExternalLabel("disable", getInstruction(onClickIndex + 1)) + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/redirection/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/redirection/Fingerprints.kt new file mode 100644 index 000000000..527af433a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/redirection/Fingerprints.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.music.general.redirection + +import app.revanced.util.containsLiteralInstruction +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val dislikeButtonOnClickListenerFingerprint = legacyFingerprint( + name = "dislikeButtonOnClickListenerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/View;"), + customFingerprint = { method, _ -> + method.name == "onClick" && + (method.containsLiteralInstruction(53465L) || method.containsLiteralInstruction( + 98173L + )) + } +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatch.kt new file mode 100644 index 000000000..1cfc4789a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatch.kt @@ -0,0 +1,102 @@ +package app.revanced.patches.music.general.spoofappversion + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.music.general.oldstylelibraryshelf.oldStyleLibraryShelfPatch +import app.revanced.patches.music.utils.compatibility.Constants.YOUTUBE_MUSIC_PACKAGE_NAME +import app.revanced.patches.music.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.patch.PatchList.SPOOF_APP_VERSION +import app.revanced.patches.music.utils.playservice.is_7_17_or_greater +import app.revanced.patches.music.utils.playservice.is_7_25_or_greater +import app.revanced.patches.music.utils.playservice.versionCheckPatch +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addPreferenceWithIntent +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.spoof.appversion.baseSpoofAppVersionPatch +import app.revanced.util.Utils.printWarn +import app.revanced.util.appendAppVersion +import app.revanced.util.findMethodOrThrow + +private var defaultValue = "false" + +private val spoofAppVersionBytecodePatch = bytecodePatch( + description = "spoofAppVersionBytecodePatch" +) { + dependsOn( + baseSpoofAppVersionPatch("$GENERAL_CLASS_DESCRIPTOR->getVersionOverride(Ljava/lang/String;)Ljava/lang/String;"), + versionCheckPatch, + ) + + execute { + if (is_7_25_or_greater) { + return@execute + } + if (is_7_17_or_greater) { + findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) { + name == "SpoofAppVersionDefaultString" + }.replaceInstruction( + 0, + "const-string v0, \"7.16.53\"" + ) + findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) { + name == "SpoofAppVersionDefaultBoolean" + }.replaceInstruction( + 0, + "const/4 v0, 0x1" + ) + + defaultValue = "true" + } + } +} + +@Suppress("unused") +val spoofAppVersionPatch = resourcePatch( + SPOOF_APP_VERSION.title, + SPOOF_APP_VERSION.summary, +) { + compatibleWith( + YOUTUBE_MUSIC_PACKAGE_NAME( + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + ), + ) + + dependsOn( + spoofAppVersionBytecodePatch, + oldStyleLibraryShelfPatch, + settingsPatch, + versionCheckPatch, + ) + + execute { + if (is_7_25_or_greater) { + printWarn("\"${SPOOF_APP_VERSION.title}\" is not supported in this version. Use YouTube Music 7.24.51 or earlier.") + return@execute + } + if (is_7_17_or_greater) { + appendAppVersion("7.16.53") + } + + addSwitchPreference( + CategoryType.GENERAL, + "revanced_spoof_app_version", + defaultValue + ) + addPreferenceWithIntent( + CategoryType.GENERAL, + "revanced_spoof_app_version_target", + "revanced_spoof_app_version" + ) + + updatePatchStatus(SPOOF_APP_VERSION) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/startpage/ChangeStartPagePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/startpage/ChangeStartPagePatch.kt new file mode 100644 index 000000000..ac6fb70e0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/startpage/ChangeStartPagePatch.kt @@ -0,0 +1,52 @@ +package app.revanced.patches.music.general.startpage + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.patch.PatchList.CHANGE_START_PAGE +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addPreferenceWithIntent +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.util.fingerprint.matchOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +@Suppress("unused") +val changeStartPagePatch = bytecodePatch( + CHANGE_START_PAGE.title, + CHANGE_START_PAGE.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + + coldStartUpFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.endIndex + val targetRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + invoke-static {v$targetRegister}, $GENERAL_CLASS_DESCRIPTOR->changeStartPage(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$targetRegister + return-object v$targetRegister + """ + ) + removeInstruction(targetIndex) + } + } + + addPreferenceWithIntent( + CategoryType.GENERAL, + "revanced_change_start_page" + ) + + updatePatchStatus(CHANGE_START_PAGE) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/general/startpage/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/startpage/Fingerprints.kt new file mode 100644 index 000000000..613adde88 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/startpage/Fingerprints.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.music.general.startpage + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val coldStartUpFingerprint = legacyFingerprint( + name = "coldStartUpFingerprint", + returnType = "Ljava/lang/String;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.GOTO, + Opcode.CONST_STRING, + Opcode.RETURN_OBJECT + ), + strings = listOf("FEmusic_library_sideloaded_tracks", "FEmusic_home") +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/icon/CustomBrandingIconPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/icon/CustomBrandingIconPatch.kt new file mode 100644 index 000000000..1e48b95ab --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/icon/CustomBrandingIconPatch.kt @@ -0,0 +1,271 @@ +package app.revanced.patches.music.layout.branding.icon + +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.booleanOption +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.patch.PatchList.CUSTOM_BRANDING_ICON_FOR_YOUTUBE_MUSIC +import app.revanced.patches.music.utils.playservice.is_7_23_or_greater +import app.revanced.patches.music.utils.playservice.versionCheckPatch +import app.revanced.patches.music.utils.settings.ResourceUtils.setIconType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.Utils.trimIndentMultiline +import app.revanced.util.copyAdaptiveIcon +import app.revanced.util.copyResources +import app.revanced.util.getResourceGroup +import app.revanced.util.underBarOrThrow +import app.revanced.util.valueOrThrow +import org.w3c.dom.Element +import java.io.File +import java.nio.file.Files + +private const val ADAPTIVE_ICON_BACKGROUND_FILE_NAME = + "adaptiveproduct_youtube_music_background_color_108" +private const val ADAPTIVE_ICON_FOREGROUND_FILE_NAME = + "adaptiveproduct_youtube_music_foreground_color_108" +private const val DEFAULT_ICON = "xisr_yellow" + +private val availableIcon = mapOf( + "AFN Blue" to "afn_blue", + "AFN Red" to "afn_red", + "MMT" to "mmt", + "MMT Blue" to "mmt_blue", + "MMT Green" to "mmt_green", + "MMT Orange" to "mmt_orange", + "MMT Pink" to "mmt_pink", + "MMT Turquoise" to "mmt_turquoise", + "MMT Yellow" to "mmt_yellow", + "Revancify Blue" to "revancify_blue", + "Revancify Red" to "revancify_red", + "Vanced Black" to "vanced_black", + "Vanced Light" to "vanced_light", + "Xisr Yellow" to DEFAULT_ICON, + "YouTube Music" to "youtube_music" +) + +private val sizeArray = arrayOf( + "xxxhdpi", + "xxhdpi", + "xhdpi", + "hdpi", + "mdpi" +) + +private val largeSizeArray = arrayOf( + "xlarge-hdpi", + "xlarge-mdpi", + "large-xhdpi", + "large-hdpi", + "large-mdpi", + "xxhdpi", + "xhdpi", + "hdpi", + "mdpi", +) + +private val largeDrawableDirectories = largeSizeArray.map { "drawable-$it" } + +private val mipmapDirectories = sizeArray.map { "mipmap-$it" } + +private val launcherIconResourceFileNames = arrayOf( + ADAPTIVE_ICON_BACKGROUND_FILE_NAME, + ADAPTIVE_ICON_FOREGROUND_FILE_NAME, + "ic_launcher_release" +).map { "$it.png" }.toTypedArray() + +private val splashIconResourceFileNames = arrayOf( + // This file only exists in [drawable-hdpi] + // Since {@code ResourceUtils#copyResources} checks for null values before copying, + // Just adds it to the array. + "action_bar_logo_release", + "record" +).map { "$it.png" }.toTypedArray() + +private val launcherIconResourceGroups = + mipmapDirectories.getResourceGroup(launcherIconResourceFileNames) + +private val splashIconResourceGroups = + largeDrawableDirectories.getResourceGroup(splashIconResourceFileNames) + +@Suppress("unused") +val customBrandingIconPatch = resourcePatch( + CUSTOM_BRANDING_ICON_FOR_YOUTUBE_MUSIC.title, + CUSTOM_BRANDING_ICON_FOR_YOUTUBE_MUSIC.summary, + false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + versionCheckPatch, + ) + + val appIconOption = stringOption( + key = "appIcon", + default = DEFAULT_ICON, + values = availableIcon, + title = "App icon", + description = """ + The icon to apply to the app. + + If a path to a folder is provided, the folder must contain the following folders: + + ${mipmapDirectories.joinToString("\n") { "- $it" }} + + Each of these folders must contain the following files: + + ${launcherIconResourceFileNames.joinToString("\n") { "- $it" }} + """.trimIndentMultiline(), + required = true, + ) + + val changeSplashIconOption by booleanOption( + key = "changeSplashIcon", + default = true, + title = "Change splash icons", + description = "Apply the custom branding icon to the splash screen.", + required = true + ) + + val restoreOldSplashIconOption by booleanOption( + key = "restoreOldSplashIcon", + default = false, + title = "Restore old splash icon", + description = """ + Restore the old style splash icon. + + If you enable both the old style splash icon and the Cairo splash animation, + + Old style splash icon will appear first and then the Cairo splash animation will start. + """.trimIndentMultiline(), + required = true, + ) + + execute { + // Check patch options first. + var appIcon = appIconOption.underBarOrThrow() + + val appIconResourcePath = "music/branding/$appIcon" + val youtubeMusicIconResourcePath = "music/branding/youtube_music" + + val resourceDirectory = get("res") + + // Check if a custom path is used in the patch options. + if (!availableIcon.containsValue(appIcon)) { + appIcon = appIconOption.valueOrThrow() + launcherIconResourceGroups.let { resourceGroups -> + try { + val path = File(appIcon) + + resourceGroups.forEach { group -> + val fromDirectory = path.resolve(group.resourceDirectoryName) + val toDirectory = resourceDirectory.resolve(group.resourceDirectoryName) + + group.resources.forEach { iconFileName -> + Files.write( + toDirectory.resolve(iconFileName).toPath(), + fromDirectory.resolve(iconFileName).readBytes() + ) + } + } + } catch (_: Exception) { + // Exception is thrown if an invalid path is used in the patch option. + throw PatchException("Invalid app icon path: $appIcon") + } + } + } else { + + // Change launcher icon. + launcherIconResourceGroups.let { resourceGroups -> + resourceGroups.forEach { + copyResources("$appIconResourcePath/launcher", it) + } + } + + // Change monochrome icon. + arrayOf( + ResourceGroup( + "drawable", + "ic_app_icons_themed_youtube_music.xml" + ) + ).forEach { resourceGroup -> + copyResources("$appIconResourcePath/monochrome", resourceGroup) + } + + // Change splash icon. + if (restoreOldSplashIconOption == true) { + var oldSplashIconNotExists: Boolean + + document("res/drawable/splash_screen.xml").use { document -> + document.apply { + val node = getElementsByTagName("layer-list").item(0) + oldSplashIconNotExists = (node as Element) + .getElementsByTagName("item") + .length == 1 + + if (oldSplashIconNotExists) { + createElement("item").also { itemNode -> + itemNode.appendChild( + createElement("bitmap").also { bitmapNode -> + bitmapNode.setAttribute("android:gravity", "center") + bitmapNode.setAttribute("android:src", "@drawable/record") + } + ) + node.appendChild(itemNode) + } + } + } + } + if (oldSplashIconNotExists) { + splashIconResourceGroups.let { resourceGroups -> + resourceGroups.forEach { + copyResources( + "$youtubeMusicIconResourcePath/splash", + it, + createDirectoryIfNotExist = true + ) + } + } + } + } + + // Change splash icon. + if (changeSplashIconOption == true) { + // Some resources have been removed in the latest YouTube Music. + // For compatibility, use try...catch. + try { + splashIconResourceGroups.let { resourceGroups -> + resourceGroups.forEach { + copyResources("$appIconResourcePath/splash", it) + } + } + } catch (_: Exception) { + } + } + + setIconType(appIcon) + } + + updatePatchStatus(CUSTOM_BRANDING_ICON_FOR_YOUTUBE_MUSIC) + + // region fix app icon + + if (!is_7_23_or_greater) { + return@execute + } + if (appIcon == "youtube_music") { + return@execute + } + + copyAdaptiveIcon( + ADAPTIVE_ICON_BACKGROUND_FILE_NAME, + ADAPTIVE_ICON_FOREGROUND_FILE_NAME, + mipmapDirectories + ) + + // endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/name/CustomBrandingNamePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/name/CustomBrandingNamePatch.kt new file mode 100644 index 000000000..5b777d05a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/name/CustomBrandingNamePatch.kt @@ -0,0 +1,82 @@ +package app.revanced.patches.music.layout.branding.name + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.patch.PatchList.CUSTOM_BRANDING_NAME_FOR_YOUTUBE_MUSIC +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.util.removeStringsElements +import app.revanced.util.valueOrThrow + +private const val APP_NAME_NOTIFICATION = "ReVanced Extended Music" +private const val APP_NAME_LAUNCHER = "RVX Music" + +@Suppress("unused") +val customBrandingNamePatch = resourcePatch( + CUSTOM_BRANDING_NAME_FOR_YOUTUBE_MUSIC.title, + CUSTOM_BRANDING_NAME_FOR_YOUTUBE_MUSIC.summary, + false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + val appNameNotificationOption = stringOption( + key = "appNameNotification", + default = APP_NAME_LAUNCHER, + values = mapOf( + "ReVanced Extended Music" to APP_NAME_NOTIFICATION, + "RVX Music" to APP_NAME_LAUNCHER, + "YouTube Music" to "YouTube Music", + "YT Music" to "YT Music", + ), + title = "App name in notification panel", + description = "The name of the app as it appears in the notification panel.", + required = true + ) + + val appNameLauncherOption = stringOption( + key = "appNameLauncher", + default = APP_NAME_LAUNCHER, + values = mapOf( + "ReVanced Extended Music" to APP_NAME_NOTIFICATION, + "RVX Music" to APP_NAME_LAUNCHER, + "YouTube Music" to "YouTube Music", + "YT Music" to "YT Music", + ), + title = "App name in launcher", + description = "The name of the app as it appears in the launcher.", + required = true + ) + + execute { + // Check patch options first. + val notificationName = appNameNotificationOption + .valueOrThrow() + val launcherName = appNameLauncherOption + .valueOrThrow() + + removeStringsElements( + arrayOf("app_launcher_name", "app_name") + ) + + document("res/values/strings.xml").use { document -> + mapOf( + "app_name" to notificationName, + "app_launcher_name" to launcherName + ).forEach { (k, v) -> + val stringElement = document.createElement("string") + + stringElement.setAttribute("name", k) + stringElement.textContent = v + + document.getElementsByTagName("resources").item(0) + .appendChild(stringElement) + } + } + + updatePatchStatus(CUSTOM_BRANDING_NAME_FOR_YOUTUBE_MUSIC) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/header/ChangeHeaderPatch.kt new file mode 100644 index 000000000..db08f642f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/header/ChangeHeaderPatch.kt @@ -0,0 +1,180 @@ +package app.revanced.patches.music.layout.header + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.patch.PatchList.CUSTOM_HEADER_FOR_YOUTUBE_MUSIC +import app.revanced.patches.music.utils.settings.ResourceUtils.getIconType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.Utils.printWarn +import app.revanced.util.Utils.trimIndentMultiline +import app.revanced.util.copyFile +import app.revanced.util.copyResources +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.resolvable +import app.revanced.util.underBarOrThrow +import app.revanced.util.valueOrThrow + +private const val DEFAULT_HEADER_KEY = "Custom branding icon" +private const val DEFAULT_HEADER_VALUE = "custom_branding_icon" + +private val actionBarLogoResourceDirectoryNames = mapOf( + "xxxhdpi" to "320px x 96px", + "xxhdpi" to "240px x 72px", + "xhdpi" to "160px x 48px", + "hdpi" to "121px x 36px", + "mdpi" to "80px x 24px", +).map { (dpi, dim) -> + "drawable-$dpi" to dim +}.toMap() + +private val logoMusicResourceDirectoryNames = mapOf( + "xxxhdpi" to "576px x 200px", + "xxhdpi" to "432px x 150px", + "xhdpi" to "288px x 100px", + "hdpi" to "217px x 76px", + "mdpi" to "144px x 50px", +).map { (dpi, dim) -> + "drawable-$dpi" to dim +}.toMap() + +private val ytmMusicLogoResourceDirectoryNames = mapOf( + "xxxhdpi" to "412px x 144px", + "xxhdpi" to "309px x 108px", + "xhdpi" to "206px x 72px", + "hdpi" to "155px x 54px", + "mdpi" to "103px x 36px", +).map { (dpi, dim) -> + "drawable-$dpi" to dim +}.toMap() + +private val headerIconResourceFileNames = arrayOf( + "action_bar_logo", + "logo_music", + "ytm_logo" +).map { "$it.png" }.toTypedArray() + +private val headerIconResourceGroups = + actionBarLogoResourceDirectoryNames.keys.map { directory -> + ResourceGroup( + directory, *headerIconResourceFileNames + ) + } + +private val getDescription = { + var descriptionBody = """ + The header to apply to the app. + + Patch option '$DEFAULT_HEADER_KEY' applies only when: + + 1. Patch 'Custom branding icon for YouTube Music' is included. + 2. Patch option for 'Custom branding icon for YouTube Music' is selected from the preset. + + If a path to a folder is provided, the folder must contain one or more of the following folders, depending on the DPI of the device: + + ${actionBarLogoResourceDirectoryNames.keys.joinToString("\n") { "- $it" }} + + Each of the folders must contain all of the following files: + + ${headerIconResourceFileNames.joinToString("\n") { "- $it" }} + """ + + mapOf( + "action_bar_logo.png" to actionBarLogoResourceDirectoryNames, + "logo_music.png" to logoMusicResourceDirectoryNames, + "ytm_logo.png" to ytmMusicLogoResourceDirectoryNames + ).forEach { (images, directoryNames) -> + descriptionBody += """ + The image '$images' dimensions must be as follows: + + ${directoryNames.map { (dpi, dim) -> "- $dpi: $dim" }.joinToString("\n")} + """ + } + + descriptionBody.trimIndentMultiline() +} + +private val changeHeaderBytecodePatch = bytecodePatch( + description = "changeHeaderBytecodePatch" +) { + execute { + /** + * New Header has been added from YouTube Music v7.04.51. + * + * The new header's file names are 'action_bar_logo_ringo2.png' and 'ytm_logo_ringo2.png'. + * The only difference between the existing header and the new header is the dimensions of the image. + * + * The affected patch is [changeHeaderPatch]. + * + * TODO: Add a new header image file to [changeHeaderPatch] later. + */ + if (!headerSwitchConfigFingerprint.resolvable()) { + return@execute + } + headerSwitchConfigFingerprint.injectLiteralInstructionBooleanCall( + 45617851L, + "0x0" + ) + } +} + +@Suppress("unused") +val changeHeaderPatch = resourcePatch( + CUSTOM_HEADER_FOR_YOUTUBE_MUSIC.title, + CUSTOM_HEADER_FOR_YOUTUBE_MUSIC.summary, + use = false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + changeHeaderBytecodePatch, + settingsPatch, + ) + + val customHeaderOption = stringOption( + key = "customHeader", + default = DEFAULT_HEADER_VALUE, + values = mapOf( + DEFAULT_HEADER_KEY to DEFAULT_HEADER_VALUE + ), + title = "Custom header", + description = getDescription(), + required = true, + ) + + execute { + // Check patch options first. + var customHeader = customHeaderOption + .underBarOrThrow() + + val isPath = customHeader != DEFAULT_HEADER_VALUE + val customBrandingIconType = getIconType() + val customBrandingIconIncluded = customBrandingIconType != "default" + customHeader = customHeaderOption.valueOrThrow() + + val warnings = "Invalid header path: $customHeader. Does not apply patches." + + if (isPath) { + copyFile( + headerIconResourceGroups, + customHeader, + warnings + ) + } else if (customBrandingIconIncluded) { + headerIconResourceGroups.let { resourceGroups -> + resourceGroups.forEach { + copyResources("music/branding/$customBrandingIconType/header", it) + } + } + } else { + printWarn(warnings) + } + + updatePatchStatus(CUSTOM_HEADER_FOR_YOUTUBE_MUSIC) + + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/header/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/header/Fingerprints.kt new file mode 100644 index 000000000..866303d2f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/header/Fingerprints.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.music.layout.header + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val headerSwitchConfigFingerprint = legacyFingerprint( + name = "headerSwitchConfigFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(45617851L) +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/overlayfilter/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/overlayfilter/Fingerprints.kt new file mode 100644 index 000000000..8b180c1b4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/overlayfilter/Fingerprints.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.music.layout.overlayfilter + +import app.revanced.patches.music.utils.resourceid.designBottomSheetDialog +import app.revanced.util.fingerprint.legacyFingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val designBottomSheetDialogFingerprint = legacyFingerprint( + name = "designBottomSheetDialogFingerprint", + returnType = "V", + parameters = emptyList(), + opcodes = listOf( + Opcode.IF_NEZ, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT + ), + literals = listOf(designBottomSheetDialog) +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/overlayfilter/OverlayFilterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/overlayfilter/OverlayFilterPatch.kt new file mode 100644 index 000000000..3479d1a09 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/overlayfilter/OverlayFilterPatch.kt @@ -0,0 +1,67 @@ +package app.revanced.patches.music.layout.overlayfilter + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.patch.PatchList.HIDE_OVERLAY_FILTER +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.util.fingerprint.matchOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private val overlayFilterBytecodePatch = bytecodePatch( + description = "overlayFilterBytecodePatch" +) { + dependsOn(sharedResourceIdPatch) + + execute { + designBottomSheetDialogFingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.patternMatch!!.endIndex - 1 + val freeRegister = getInstruction(insertIndex + 1).registerA + + addInstructions( + insertIndex, """ + invoke-virtual {p0}, $definingClass->getWindow()Landroid/view/Window; + move-result-object v$freeRegister + invoke-static {v$freeRegister}, $GENERAL_CLASS_DESCRIPTOR->disableDimBehind(Landroid/view/Window;)V + """ + ) + } + } + + } +} + +@Suppress("unused") +val overlayFilterPatch = resourcePatch( + HIDE_OVERLAY_FILTER.title, + HIDE_OVERLAY_FILTER.summary, + use = false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + overlayFilterBytecodePatch, + ) + + execute { + val styleFile = get("res/values/styles.xml") + + styleFile.writeText( + styleFile.readText() + .replace( + "ytOverlayBackgroundMedium\">@color/yt_black_pure_opacity60", + "ytOverlayBackgroundMedium\">@android:color/transparent" + ) + ) + + updatePatchStatus(HIDE_OVERLAY_FILTER) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/playeroverlay/PlayerOverlayFilterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/playeroverlay/PlayerOverlayFilterPatch.kt new file mode 100644 index 000000000..9981349da --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/playeroverlay/PlayerOverlayFilterPatch.kt @@ -0,0 +1,27 @@ +package app.revanced.patches.music.layout.playeroverlay + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.patch.PatchList.HIDE_PLAYER_OVERLAY_FILTER +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.util.removeOverlayBackground + +@Suppress("unused") +val playerOverlayFilterPatch = resourcePatch( + HIDE_PLAYER_OVERLAY_FILTER.title, + HIDE_PLAYER_OVERLAY_FILTER.summary, + use = false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + execute { + removeOverlayBackground( + arrayOf("music_controls_overlay.xml"), + arrayOf("player_control_screen") + ) + + updatePatchStatus(HIDE_PLAYER_OVERLAY_FILTER) + + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/translations/TranslationsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/translations/TranslationsPatch.kt new file mode 100644 index 000000000..b900ecdca --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/translations/TranslationsPatch.kt @@ -0,0 +1,71 @@ +package app.revanced.patches.music.layout.translations + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.patch.PatchList.TRANSLATIONS_FOR_YOUTUBE_MUSIC +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.translations.APP_LANGUAGES +import app.revanced.patches.shared.translations.baseTranslationsPatch + +// Array of supported translations, each represented by its language code. +private val SUPPORTED_TRANSLATIONS = setOf( + "bg-rBG", "bn", "cs-rCZ", "el-rGR", "es-rES", "fr-rFR", "hu-rHU", "id-rID", "in", "it-rIT", + "ja-rJP", "ko-rKR", "nl-rNL", "pl-rPL", "pt-rBR", "ro-rRO", "ru-rRU", "tr-rTR", "uk-rUA", + "vi-rVN", "zh-rCN", "zh-rTW" +) + +@Suppress("unused") +val translationsPatch = resourcePatch( + TRANSLATIONS_FOR_YOUTUBE_MUSIC.title, + TRANSLATIONS_FOR_YOUTUBE_MUSIC.summary, +) { + val customTranslations by stringOption( + key = "customTranslations", + default = "", + title = "Custom translations", + description = """ + The path to the 'strings.xml' file. + Please note that applying the 'strings.xml' file will overwrite all existing translations. + """.trimIndent(), + required = true, + ) + + val selectedTranslations by stringOption( + key = "selectedTranslations", + default = SUPPORTED_TRANSLATIONS.joinToString(", "), + title = "Translations to add", + description = "A list of translations to be added for the RVX settings, separated by commas.", + required = true, + ) + + val selectedStringResources by stringOption( + key = "selectedStringResources", + default = APP_LANGUAGES.joinToString(", "), + title = "String resources to keep", + description = """ + A list of string resources to be kept, separated by commas. + String resources not in the list will be removed from the app. + + Default string resource, English, is not removed. + """.trimIndent(), + required = true, + ) + + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + ) + + execute { + baseTranslationsPatch( + customTranslations, selectedTranslations, selectedStringResources, + SUPPORTED_TRANSLATIONS, "music" + ) + + updatePatchStatus(TRANSLATIONS_FOR_YOUTUBE_MUSIC) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/visual/VisualPreferencesIconsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/visual/VisualPreferencesIconsPatch.kt new file mode 100644 index 000000000..b0bbe65e4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/visual/VisualPreferencesIconsPatch.kt @@ -0,0 +1,308 @@ +package app.revanced.patches.music.layout.visual + +import app.revanced.patcher.patch.booleanOption +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.music.layout.branding.icon.customBrandingIconPatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.patch.PatchList.VISUAL_PREFERENCES_ICONS_FOR_YOUTUBE_MUSIC +import app.revanced.patches.music.utils.settings.ResourceUtils.SETTINGS_HEADER_PATH +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.util.* +import app.revanced.util.Utils.trimIndentMultiline +import org.w3c.dom.Element + +private const val DEFAULT_ICON = "extension" +private const val EMPTY_ICON = "empty_icon" + +@Suppress("unused") +val visualPreferencesIconsPatch = resourcePatch( + VISUAL_PREFERENCES_ICONS_FOR_YOUTUBE_MUSIC.title, + VISUAL_PREFERENCES_ICONS_FOR_YOUTUBE_MUSIC.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + val settingsMenuIconOption = stringOption( + key = "settingsMenuIcon", + default = DEFAULT_ICON, + values = mapOf( + "Custom branding icon" to "custom_branding_icon", + "Extension" to DEFAULT_ICON, + "Gear" to "gear", + "ReVanced" to "revanced", + "ReVanced Colored" to "revanced_colored", + ), + title = "RVX settings menu icon", + description = "The icon for the RVX settings menu.", + required = true, + ) + + val applyToAll by booleanOption( + key = "applyToAll", + default = false, + title = "Apply to all settings menu", + description = """ + Whether to apply Visual preferences icons to all settings menus. + + If true: icons are applied to the parent PreferenceScreen of YouTube settings, the parent PreferenceScreen of RVX settings and the RVX sub-settings (if supported). + + If false: icons are applied only to the parent PreferenceScreen of YouTube settings and RVX settings. + """.trimIndentMultiline(), + required = true + ) + + execute { + // Check patch options first. + val selectedIconType = settingsMenuIconOption + .underBarOrThrow() + + val appIconOption = customBrandingIconPatch + .getStringOptionValue("appIcon") + + val customBrandingIconType = appIconOption + .underBarOrThrow() + + if (applyToAll == true) { + preferenceKey.putAll(rvxPreferenceKey) + } + + // region copy shared resources. + + copyResourcesWithRename("music/visual/icons", preferenceKey) + + arrayOf( + ResourceGroup( + "drawable-xxhdpi", + "$EMPTY_ICON.png" + ), + ).forEach { resourceGroup -> + copyResources("music/visual/icons", resourceGroup) + } + + // endregion. + + // region copy RVX settings menu icon. + + val fallbackIconPath = "music/visual/icons/extension" + val iconPath = when (selectedIconType) { + "custom_branding_icon" -> "music/branding/$customBrandingIconType/settings" + else -> "music/visual/icons/$selectedIconType" + } + val resourceGroup = ResourceGroup( + "drawable", + "revanced_extended_settings_key_icon.xml" + ) + + try { + copyResources(iconPath, resourceGroup) + } catch (_: Exception) { + // Ignore if resource copy fails + + // Add a fallback extended icon + // It's needed if someone provides custom path to icon(s) folder + // but custom branding icons for Extended setting are predefined, + // so it won't copy custom branding icon + // and will raise an error without fallback icon + copyResources(fallbackIconPath, resourceGroup) + } + + // endregion. + + updatePatchStatus(VISUAL_PREFERENCES_ICONS_FOR_YOUTUBE_MUSIC) + + } + + finalize { + // region set visual preferences icon. + + document(SETTINGS_HEADER_PATH).use { document -> + document.doRecursively loop@{ node -> + if (node !is Element) return@loop + + node.getAttributeNode("android:key") + ?.textContent + ?.removePrefix("@string/") + ?.let { title -> + val drawableName = when (title) { + in preferenceKey.keys -> { + val pathData = preferenceKey[title] + + // If pathData is another title then use it as an icon title + if (preferenceKey.containsKey(pathData)) { + pathData + } else { + title + } + } + + // Add custom RVX settings menu icon + in intentKey -> intentIcon[title] + in emptyTitles -> EMPTY_ICON + else -> null + } + if (drawableName == EMPTY_ICON && + applyToAll == false + ) return@loop + + drawableName?.let { + node.setAttribute("android:icon", "@drawable/$it") + } + } + } + } + + // endregion. + } +} + +private var preferenceKey = mutableMapOf( + // YouTube settings + "pref_key_parent_tools" to "M 677.462 535.154 Q 645.289 535.154 622.645 512.509 Q 600 489.865 600 457.692 Q 600 425.52 622.768 402.875 Q 645.535 380.231 677.077 380.231 Q 709.634 380.231 731.894 402.999 Q 754.154 425.766 754.154 457.308 Q 754.154 489.865 731.894 512.509 Q 709.634 535.154 677.462 535.154 Z M 498.769 744.616 L 498.769 710.539 Q 498.769 691.462 507.333 677.169 Q 515.897 662.877 532.308 656 Q 566.333 640.077 602.359 632.231 Q 638.385 624.385 677.462 624.385 Q 714.98 624.385 751.067 632.115 Q 787.154 639.846 822.615 656 Q 838.192 662.923 846.789 677.192 Q 855.385 691.462 855.385 710.539 L 855.385 744.616 L 498.769 744.616 Z M 384.615 455.154 Q 335.115 455.154 302.173 422.212 Q 269.231 389.269 269.231 339.385 Q 269.231 289.5 302.173 256.942 Q 335.115 224.384 384.615 224.384 Q 434.116 224.384 467.058 256.942 Q 500 289.5 500 339.385 Q 500 389.269 467.058 422.212 Q 434.116 455.154 384.615 455.154 Z M 384.615 339.769 Z M 104.615 744.616 L 104.615 686.769 Q 104.615 660.817 118.923 639.062 Q 133.231 617.308 159.205 606.923 Q 216.538 580.769 271.851 567.308 Q 327.164 553.846 384.315 553.846 Q 407.385 553.846 427 554.923 Q 446.616 556 467.923 559.692 Q 461.327 565.904 454.731 573.269 Q 448.135 580.635 441.539 586.846 Q 428.539 586 414.308 585.308 Q 400.077 584.615 384.615 584.615 Q 331.042 584.615 278.79 595.346 Q 226.539 606.077 173.538 634 Q 158.769 641.769 147.077 655.616 Q 135.385 669.462 135.385 686.769 L 135.385 713.846 L 397.231 713.846 L 397.231 744.616 L 104.615 744.616 Z M 397.231 713.846 Z M 384.615 424.385 Q 420.539 424.385 444.885 400.038 Q 469.231 375.692 469.231 339.769 Q 469.231 303.846 444.885 279.5 Q 420.539 255.154 384.615 255.154 Q 348.692 255.154 324.346 279.5 Q 300 303.846 300 339.769 Q 300 375.692 324.346 400.038 Q 348.692 424.385 384.615 424.385 Z", + "settings_header_general" to "M 702.308 766.923 Q 652.877 766.923 618.746 732.793 Q 584.615 698.662 584.615 649.231 Q 584.615 599.8 618.746 565.669 Q 652.877 531.538 702.308 531.538 Q 751.739 531.538 785.869 565.669 Q 820 599.8 820 649.231 Q 820 698.662 785.869 732.793 Q 751.739 766.923 702.308 766.923 Z M 702.121 736.154 Q 738.769 736.154 764 711.11 Q 789.231 686.065 789.231 649.417 Q 789.231 612.769 764.187 587.539 Q 739.142 562.308 702.494 562.308 Q 665.846 562.308 640.615 587.352 Q 615.385 612.396 615.385 649.044 Q 615.385 685.692 640.429 710.923 Q 665.473 736.154 702.121 736.154 Z M 181.538 664.616 L 181.538 633.846 L 489.231 633.846 L 489.231 664.616 L 181.538 664.616 Z M 257.692 428.462 Q 208.261 428.462 174.131 394.331 Q 140 360.2 140 310.769 Q 140 261.338 174.131 227.207 Q 208.261 193.077 257.692 193.077 Q 307.123 193.077 341.254 227.207 Q 375.385 261.338 375.385 310.769 Q 375.385 360.2 341.254 394.331 Q 307.123 428.462 257.692 428.462 Z M 257.506 397.692 Q 294.154 397.692 319.385 372.648 Q 344.615 347.604 344.615 310.956 Q 344.615 274.308 319.571 249.077 Q 294.527 223.846 257.879 223.846 Q 221.231 223.846 196 248.89 Q 170.769 273.935 170.769 310.583 Q 170.769 347.231 195.813 372.461 Q 220.858 397.692 257.506 397.692 Z M 470.769 326.154 L 470.769 295.384 L 778.462 295.384 L 778.462 326.154 L 470.769 326.154 Z M 702.308 649.231 Z M 257.692 310.769 Z", + "settings_header_playback" to "M 404.615 615.077 L 404.615 344.923 L 615.077 480 L 404.615 615.077 Z M 483.154 880 Q 360.615 880 264.077 816.962 Q 167.538 753.923 110.769 637.616 L 110.769 799.769 L 79.999 799.769 L 79.999 584.615 L 293.385 584.615 L 293.385 615.385 L 134.769 615.385 Q 183.308 724.231 276 786.731 Q 368.692 849.231 483.154 849.231 Q 604.923 849.231 701.154 776.423 Q 797.385 703.615 832.308 586.308 L 862.846 592.385 Q 826 721.462 721.346 800.731 Q 616.692 880.001 483.154 880.001 Z M 82 440 Q 89.769 377.385 110.154 328.423 Q 130.538 279.461 169.692 227.461 L 192.692 248.923 Q 159.231 293.154 140.923 336.615 Q 122.615 380.077 112.769 440 L 81.999 440 Z M 248.692 193.692 L 227.461 170.461 Q 276.077 132.615 330.154 111.077 Q 384.231 89.538 441.231 83.538 L 441.231 114.308 Q 392.308 119.308 343.308 139.038 Q 294.308 158.769 248.692 193.692 Z M 709.769 193.692 Q 671.692 161.538 619.769 140.038 Q 567.846 118.538 520 114.308 L 520 83.538 Q 578 88.769 631.192 111.077 Q 684.385 133.384 731.769 171.231 L 709.769 193.692 Z M 845.692 440 Q 839.154 385.154 818.462 336.615 Q 797.769 288.077 763.308 248.461 L 785.308 226 Q 824.385 273.077 847.539 327.115 Q 870.693 381.154 876.462 440 L 845.692 440 Z", + "settings_header_data_saving" to "M 120 840 L 840 120 L 840 840 L 120 840 Z M 374.462 809.231 L 809.231 809.231 L 809.231 194 L 374.462 628.769 L 374.462 809.231 Z", + "settings_header_downloads_and_storage" to "M 480 626.231 L 341.615 487.846 L 363.846 466.384 L 464.615 566.384 L 464.615 200 L 495.385 200 L 495.385 566.384 L 596.154 466.384 L 618.385 487.846 L 480 626.231 Z M 255.384 760 Q 232.327 760 216.163 743.837 Q 200 727.673 200 704.616 L 200 597 L 230.769 597 L 230.769 704.616 Q 230.769 713.846 238.461 721.539 Q 246.154 729.231 255.384 729.231 L 704.616 729.231 Q 713.846 729.231 721.539 721.539 Q 729.231 713.846 729.231 704.616 L 729.231 597 L 760 597 L 760 704.616 Q 760 727.673 743.837 743.837 Q 727.673 760 704.616 760 L 255.384 760 Z", + "settings_header_notifications" to "M 200 750.769 L 200 720 L 264.615 720 L 264.615 392.154 Q 264.615 313.673 313.731 252.875 Q 362.846 192.077 440 178.538 L 440 160 Q 440 143.333 451.64 131.666 Q 463.28 120 479.91 120 Q 496.539 120 508.269 131.666 Q 520 143.333 520 160 L 520 178.923 Q 597.154 192.077 646.269 252.875 Q 695.385 313.673 695.385 392.154 L 695.385 720 L 760 720 L 760 750.769 L 200 750.769 Z M 480 463.385 Z M 479.864 855.385 Q 453.154 855.385 434.269 836.404 Q 415.385 817.423 415.385 790.769 L 544.615 790.769 Q 544.615 817.615 525.595 836.5 Q 506.574 855.385 479.864 855.385 Z M 295.385 720 L 664.615 720 L 664.615 392.154 Q 664.615 315.615 610.577 261.577 Q 556.538 207.539 480 207.539 Q 403.462 207.539 349.423 261.577 Q 295.385 315.615 295.385 392.154 L 295.385 720 Z", + "settings_header_privacy_and_location" to "M 684.729 684.615 Q 710.29 684.615 728.184 666.249 Q 746.077 647.882 746.077 622.905 Q 746.077 597.928 727.959 579.81 Q 709.842 561.692 684.864 561.692 Q 659.887 561.692 641.52 579.493 Q 623.154 597.294 623.154 622.724 Q 623.154 648.154 641.481 666.385 Q 659.808 684.615 684.729 684.615 Z M 684.115 806.539 Q 716.846 806.539 743.462 792.923 Q 770.077 779.308 787.539 754.077 Q 763.077 740.077 737.615 733.077 Q 712.153 726.077 684.654 726.077 Q 657.155 726.077 631.154 733.077 Q 605.154 740.077 581.692 754.077 Q 599.154 779.308 625.269 792.923 Q 651.385 806.539 684.115 806.539 Z M 480 838.462 Q 360.461 803.385 280.231 693.5 Q 200 583.615 200 441.077 L 200 227.461 L 480 122.846 L 760 227.461 L 760 473.615 Q 752.923 471.231 744.231 468.423 Q 735.539 465.615 729.231 464.385 L 729.231 247.923 L 480 156.538 L 230.769 247.923 L 230.769 441.077 Q 230.769 514 253.731 576.077 Q 276.692 638.154 314.308 686.654 Q 351.923 735.154 399.077 767.538 Q 446.231 799.923 495.385 812.385 L 497.692 811.615 Q 500.615 814.385 504.923 819.385 Q 509.231 824.385 511.846 827 Q 503.615 831.231 495.538 833.731 Q 487.462 836.231 480 838.462 Z M 685.947 840 Q 621.893 840 576.908 794.885 Q 531.923 749.769 531.923 686.077 Q 531.923 621.242 576.898 576.082 Q 621.873 530.923 686.447 530.923 Q 750 530.923 795.116 576.082 Q 840.231 621.242 840.231 686.077 Q 840.231 749.769 795.116 794.885 Q 750 840 685.947 840 Z M 480 484.077 Z", + "settings_header_recommendations" to "M 154.549 522.836 C 141.802 522.76 130.526 518.188 121.587 509.249 C 112.648 500.31 108.076 489.034 108 476.288 L 108 275.82 C 108.018 272.336 108.684 268.793 109.932 265.561 C 111.233 262.309 113.607 258.829 116.76 255.527 L 265.165 107.122 L 266.577 108.636 L 277.46 120.297 C 279.338 122.448 281.014 124.528 282.445 126.484 C 283.964 128.61 284.859 130.909 284.935 132.787 L 284.935 138.502 L 256.798 261.445 L 415.652 261.445 C 429.902 261.515 442.178 266.323 451.606 275.751 C 461.035 285.18 465.843 297.458 465.913 311.705 L 465.913 319.564 C 465.909 323.12 465.613 326.217 465.055 328.622 C 464.496 330.97 463.687 333.457 462.714 335.834 L 390.388 502.885 C 387.809 508.903 383.763 513.903 378.598 517.459 C 373.355 520.95 366.752 522.8 359.348 522.836 Z M 362.98 494.087 L 437.164 321.379 L 437.164 307.993 C 437.232 302.584 435.671 298.552 432.187 295.183 C 428.817 291.698 424.775 290.126 419.364 290.194 L 221.183 290.194 L 248.351 164.585 L 136.749 276.953 L 136.749 476.288 C 136.683 481.703 138.242 485.735 141.725 489.098 C 145.09 492.582 149.134 494.153 154.549 494.087 Z M 693.423 851.364 L 682.539 839.702 C 680.643 837.526 679.027 835.383 677.78 833.395 C 676.49 831.279 675.74 829.041 675.684 827.212 L 675.684 821.502 L 703.214 698.555 L 544.349 698.555 C 530.097 698.485 517.82 693.676 508.393 684.249 C 498.966 674.822 494.157 662.544 494.087 648.294 L 494.087 640.436 C 494.091 636.88 494.387 633.783 494.945 631.377 C 495.503 629.029 496.313 626.546 497.287 624.168 L 569.608 456.505 C 572.092 450.522 576.136 445.614 581.374 442.232 C 586.659 438.938 593.264 437.196 600.652 437.164 L 805.451 437.164 C 818.198 437.24 829.474 441.812 838.413 450.751 C 847.352 459.69 851.924 470.966 852 483.712 L 852 684.18 C 851.982 687.665 851.316 691.206 850.067 694.439 C 848.767 697.69 846.393 701.171 843.24 704.473 L 694.835 852.878 Z M 597.018 465.913 L 522.836 638.002 L 522.836 652.007 C 522.768 657.416 524.329 661.448 527.813 664.817 C 531.183 668.302 535.225 669.874 540.636 669.806 L 739.451 669.806 L 711.693 794.779 L 823.251 683.042 L 823.251 483.711 C 823.319 478.302 821.758 474.27 818.273 470.901 C 814.904 467.416 810.862 465.844 805.451 465.913 Z", + "settings_header_paid_memberships" to "M 395.77 531.08 L 427.92 427.31 L 344.46 367.46 L 447.17 367.46 L 480 260 L 511.83 367.46 L 615.54 367.46 L 532.85 427.31 L 564 531.08 L 480 467 L 395.77 531.08 Z M 281.69 858.46 L 281.69 596.77 Q 240.54 557.46 220.27 506.08 Q 200 454.69 200 400 Q 200 282.46 281.23 201.23 Q 362.46 120 480 120 Q 597.54 120 678.77 201.23 Q 760 282.46 760 400 Q 760 454.69 739.73 506.08 Q 719.46 557.46 678.31 596.77 L 678.31 858.46 L 480 798.67 L 281.69 858.46 Z M 479.91 649.23 Q 584.38 649.23 656.81 576.9 Q 729.23 504.57 729.23 400.09 Q 729.23 295.62 656.9 223.19 Q 584.57 150.77 480.09 150.77 Q 375.62 150.77 303.19 223.1 Q 230.77 295.43 230.77 399.91 Q 230.77 504.38 303.1 576.81 Q 375.43 649.23 479.91 649.23 Z M 312.46 817.54 L 480 766.38 L 647.54 817.54 L 647.54 622.69 Q 611.38 651.69 568.08 665.85 Q 524.77 680 480 680 Q 435.23 680 391.92 665.85 Q 348.62 651.69 312.46 622.69 L 312.46 817.54 Z M 480 720 Z", + "settings_header_about_youtube_music" to "M 466.077 660 L 496.846 660 L 496.846 440 L 466.077 440 L 466.077 660 Z M 479.982 386 Q 489 386 495.231 380.069 Q 501.462 374.138 501.462 364.769 Q 501.462 355.319 495.248 349.198 Q 489.035 343.077 480.018 343.077 Q 470.231 343.077 464.385 349.198 Q 458.538 355.319 458.538 364.769 Q 458.538 374.138 464.752 380.069 Q 470.965 386 479.982 386 Z M 480.4 840 Q 405.224 840 340.106 811.661 Q 274.987 783.321 225.859 734.239 Q 176.732 685.157 148.366 620.026 Q 120 554.894 120 479.634 Q 120 405.143 148.339 339.565 Q 176.679 273.987 225.761 225.359 Q 274.843 176.732 339.974 148.366 Q 405.106 120 480.366 120 Q 554.857 120 620.435 148.339 Q 686.013 176.679 734.641 225.261 Q 783.268 273.843 811.634 339.518 Q 840 405.194 840 479.6 Q 840 554.776 811.661 619.894 Q 783.321 685.013 734.739 733.956 Q 686.157 782.9 620.482 811.45 Q 554.806 840 480.4 840 Z M 480.5 809.231 Q 617.385 809.231 713.308 713.192 Q 809.231 617.154 809.231 479.5 Q 809.231 342.615 713.495 246.692 Q 617.76 150.769 480 150.769 Q 342.846 150.769 246.808 246.505 Q 150.769 342.24 150.769 480 Q 150.769 617.154 246.808 713.192 Q 342.846 809.231 480.5 809.231 Z M 480 480 Z", + + // RVX settings + "revanced_preference_screen_account" to "M 480 455.154 Q 430.5 455.154 397.558 422.212 Q 364.615 389.269 364.615 339.385 Q 364.615 289.5 397.558 256.942 Q 430.5 224.384 480 224.384 Q 529.5 224.384 562.443 256.942 Q 595.385 289.5 595.385 339.385 Q 595.385 389.269 562.443 422.212 Q 529.5 455.154 480 455.154 Z M 200 744.616 L 200 686.769 Q 200 660.308 215.154 639.462 Q 230.307 618.615 254.923 606.923 Q 314.231 580.769 369.938 567.308 Q 425.645 553.846 479.976 553.846 Q 534.308 553.846 589.923 567.423 Q 645.539 581 704.425 607.274 Q 729.872 618.771 744.936 639.54 Q 760 660.308 760 686.769 L 760 744.616 L 200 744.616 Z M 230.769 713.846 L 729.231 713.846 L 729.231 686.769 Q 729.231 671.539 718.962 657.423 Q 708.692 643.308 690.846 634 Q 636.846 607.615 585.241 596.115 Q 533.636 584.615 480 584.615 Q 426.364 584.615 374.144 596.115 Q 321.923 607.615 268.923 634 Q 251.077 643.308 240.923 657.423 Q 230.769 671.539 230.769 686.769 L 230.769 713.846 Z M 480 424.385 Q 515.923 424.385 540.269 400.038 Q 564.615 375.692 564.615 339.769 Q 564.615 303.846 540.269 279.5 Q 515.923 255.154 480 255.154 Q 444.077 255.154 419.731 279.5 Q 395.385 303.846 395.385 339.769 Q 395.385 375.692 419.731 400.038 Q 444.077 424.385 480 424.385 Z M 480 339.769 Z M 480 713.846 Z", + "revanced_preference_screen_action_bar" to "M 258.308 662.923 L 701.692 662.923 L 701.692 589.461 L 258.308 589.461 L 258.308 662.923 Z M 175.384 760 Q 152.327 760 136.163 743.837 Q 120 727.673 120 704.616 L 120 255.384 Q 120 232.327 136.163 216.163 Q 152.327 200 175.384 200 L 784.616 200 Q 807.673 200 823.837 216.163 Q 840 232.327 840 255.384 L 840 704.616 Q 840 727.673 823.837 743.837 Q 807.673 760 784.616 760 L 175.384 760 Z M 175.384 729.231 L 784.616 729.231 Q 793.846 729.231 801.539 721.539 Q 809.231 713.846 809.231 704.616 L 809.231 255.384 Q 809.231 246.154 801.539 238.461 Q 793.846 230.769 784.616 230.769 L 175.384 230.769 Q 166.154 230.769 158.461 238.461 Q 150.769 246.154 150.769 255.384 L 150.769 704.616 Q 150.769 713.846 158.461 721.539 Q 166.154 729.231 175.384 729.231 Z M 150.769 729.231 L 150.769 230.769 L 150.769 729.231 Z", + "revanced_preference_screen_ads" to "M 721.539 495.385 L 721.539 464.615 L 846.154 464.615 L 846.154 495.385 L 721.539 495.385 Z M 766.154 758.462 L 665.923 683.846 L 685 659.692 L 785.231 734.308 L 766.154 758.462 Z M 683.385 297 L 664.308 272.846 L 763.077 198.461 L 782.154 222.615 L 683.385 297 Z M 224.615 718.462 L 224.615 566.154 L 169.231 566.154 Q 146.788 566.154 130.317 549.683 Q 113.846 533.212 113.846 510.769 L 113.846 449.231 Q 113.846 426.788 130.317 410.317 Q 146.788 393.846 169.231 393.846 L 327.692 393.846 L 486.154 300 L 486.154 660 L 327.692 566.154 L 255.385 566.154 L 255.385 718.462 L 224.615 718.462 Z M 455.385 605.539 L 455.385 354.461 L 336 424.615 L 169.231 424.615 Q 160 424.615 152.308 432.308 Q 144.615 440 144.615 449.231 L 144.615 510.769 Q 144.615 520 152.308 527.692 Q 160 535.385 169.231 535.385 L 336 535.385 L 455.385 605.539 Z M 556.923 595.539 L 556.923 364.461 Q 577 383.077 589.269 413.346 Q 601.539 443.615 601.539 480 Q 601.539 516.385 589.269 546.654 Q 577 576.923 556.923 595.539 Z M 300 480 Z", + "revanced_preference_screen_flyout" to "M 392.385 741.231 L 392.385 710.461 L 800 710.461 L 800 741.231 L 392.385 741.231 Z M 392.385 495.385 L 392.385 464.615 L 800 464.615 L 800 495.385 L 392.385 495.385 Z M 392.385 249.308 L 392.385 218.538 L 800 218.538 L 800 249.308 L 392.385 249.308 Z M 208.299 772.846 Q 188.209 772.846 174.22 759.063 Q 160.231 745.279 160.231 725.577 Q 160.231 705.875 174.13 691.976 Q 188.029 678.077 207.731 678.077 Q 227.433 678.077 241.216 692.451 Q 255 706.825 255 726.462 Q 255 745.273 241.282 759.06 Q 227.563 772.846 208.299 772.846 Z M 208.299 527 Q 188.209 527 174.22 512.92 Q 160.231 498.839 160.231 480 Q 160.231 461.161 174.377 447.08 Q 188.523 433 208.387 433 Q 227.427 433 241.213 447.08 Q 255 461.161 255 480 Q 255 498.839 241.282 512.92 Q 227.563 527 208.299 527 Z M 207.231 280.923 Q 188.391 280.923 174.311 266.843 Q 160.231 252.763 160.231 233.923 Q 160.231 215.084 174.311 201.003 Q 188.391 186.923 207.615 186.923 Q 226.839 186.923 240.92 201.003 Q 255 215.084 255 233.923 Q 255 252.763 240.968 266.843 Q 226.936 280.923 207.231 280.923 Z", + "revanced_preference_screen_general" to "settings_header_general", + "revanced_preference_screen_navigation" to "M 160 640 L 160 320 L 800 320 L 800 466.154 L 769.231 466.154 L 769.231 350.769 L 190.769 350.769 L 190.769 609.231 L 601.538 609.231 L 601.538 640 L 160 640 Z M 190.769 609.231 L 190.769 350.769 L 190.769 609.231 Z M 871.692 758.308 L 718.462 605.308 L 718.462 740 L 687.692 740 L 687.692 552.308 L 875.385 552.308 L 875.385 583.077 L 740.462 583.077 L 892.923 737.077 L 871.692 758.308 Z", + "revanced_preference_screen_player" to "M 401.461 618.462 L 618.462 480 L 401.461 341.538 L 401.461 618.462 Z M 480.134 840 Q 405.692 840 340.34 811.661 Q 274.987 783.321 225.859 734.239 Q 176.732 685.157 148.366 619.866 Q 120 554.575 120 480.134 Q 120 405.461 148.339 339.724 Q 176.679 273.987 225.761 225.359 Q 274.843 176.732 340.134 148.366 Q 405.425 120 479.866 120 Q 554.539 120 620.276 148.339 Q 686.013 176.679 734.641 225.261 Q 783.268 273.843 811.634 339.518 Q 840 405.194 840 479.866 Q 840 554.308 811.661 619.66 Q 783.321 685.013 734.739 734.141 Q 686.157 783.268 620.482 811.634 Q 554.806 840 480.134 840 Z M 480 809.231 Q 617.385 809.231 713.308 713.192 Q 809.231 617.154 809.231 480 Q 809.231 342.615 713.308 246.692 Q 617.385 150.769 480 150.769 Q 342.846 150.769 246.808 246.692 Q 150.769 342.615 150.769 480 Q 150.769 617.154 246.808 713.192 Q 342.846 809.231 480 809.231 Z M 480 480 Z", + "revanced_preference_screen_settings" to "M 413.38 840 L 397.23 725.54 Q 375.15 718.54 348.77 703.85 Q 322.38 689.15 304.08 672.31 L 198.38 720.15 L 131.54 601.54 L 225.69 531.77 Q 223.69 519.69 222.42 506.27 Q 221.15 492.85 221.15 480.77 Q 221.15 469.46 222.42 456.04 Q 223.69 442.62 225.69 428.23 L 131.54 357.69 L 198.38 241.38 L 303.31 287.69 Q 323.92 270.85 348.77 256.54 Q 373.62 242.23 396.46 235.46 L 413.38 120 L 546.62 120 L 562.77 235.23 Q 587.92 244.54 610.58 257.19 Q 633.23 269.85 653.62 287.69 L 762.38 241.38 L 828.46 357.69 L 731.23 429.31 Q 734.77 443.15 735.65 455.81 Q 736.54 468.46 736.54 480 Q 736.54 490.77 735.27 503.31 Q 734 515.85 731.23 531.23 L 827.69 601.54 L 760.85 720.15 L 653.62 671.54 Q 632.23 689.92 609.42 703.96 Q 586.62 718 562.77 724.77 L 546.62 840 L 413.38 840 Z M 438.31 809.23 L 520.92 809.23 L 535.69 698 Q 566.38 690 592.04 675.31 Q 617.69 660.62 644 635.85 L 746.92 680.31 L 786.92 610.62 L 696 543.15 Q 700 524.62 702.12 509.65 Q 704.23 494.69 704.23 480 Q 704.23 463.77 702.23 449.58 Q 700.23 435.38 696 418.38 L 788.46 349.38 L 748.46 279.69 L 643.23 324.15 Q 624.08 302.77 593.54 284.12 Q 563 265.46 534.92 262 L 521.69 150.77 L 438.31 150.77 L 425.85 261.23 Q 393.38 267.46 366.96 282.54 Q 340.54 297.62 315.23 323.38 L 211.54 279.69 L 171.54 349.38 L 262.46 416.08 Q 257.69 430.77 255.58 446.88 Q 253.46 463 253.46 480.77 Q 253.46 497 255.58 512.35 Q 257.69 527.69 261.69 543.15 L 171.54 610.62 L 211.54 680.31 L 314.46 636.62 Q 338.46 661.38 365.27 676.08 Q 392.08 690.77 425.08 698.77 L 438.31 809.23 Z M 477.69 575.38 Q 517.85 575.38 545.46 547.77 Q 573.08 520.15 573.08 480 Q 573.08 439.85 545.46 412.23 Q 517.85 384.62 477.69 384.62 Q 438.31 384.62 410.31 412.23 Q 382.31 439.85 382.31 480 Q 382.31 520.15 410.31 547.77 Q 438.31 575.38 477.69 575.38 Z M 480 480 Z", + "revanced_preference_screen_video" to "M 443.231 546.231 L 657.077 409.231 L 443.231 272.231 L 443.231 546.231 Z M 296.923 698.462 Q 273.865 698.462 257.702 682.298 Q 241.538 666.135 241.538 643.077 L 241.538 175.384 Q 241.538 152.327 257.702 136.163 Q 273.865 120 296.923 120 L 764.616 120 Q 787.673 120 803.837 136.163 Q 820 152.327 820 175.384 L 820 643.077 Q 820 666.135 803.837 682.298 Q 787.673 698.462 764.616 698.462 L 296.923 698.462 Z M 296.923 667.693 L 764.616 667.693 Q 773.846 667.693 781.539 660 Q 789.231 652.308 789.231 643.077 L 789.231 175.384 Q 789.231 166.154 781.539 158.461 Q 773.846 150.769 764.616 150.769 L 296.923 150.769 Q 287.692 150.769 280 158.461 Q 272.308 166.154 272.308 175.384 L 272.308 643.077 Q 272.308 652.308 280 660 Q 287.692 667.693 296.923 667.693 Z M 195.384 800 Q 172.327 800 156.163 783.837 Q 140 767.674 140 744.616 L 140 246.154 L 170.769 246.154 L 170.769 744.616 Q 170.769 753.847 178.461 761.539 Q 186.154 769.231 195.384 769.231 L 693.847 769.231 L 693.847 800 L 195.384 800 Z M 272.308 150.769 L 272.308 667.693 L 272.308 150.769 Z", + "revanced_preference_screen_return_youtube_username" to "M 480 840 Q 405.46 840 339.72 811.66 Q 273.99 783.32 225.36 734.74 Q 176.73 686.16 148.37 620.48 Q 120 554.81 120 480.13 Q 120 405.46 148.34 339.72 Q 176.68 273.99 225.26 225.36 Q 273.84 176.73 339.52 148.37 Q 405.19 120 479.87 120 Q 554.54 120 620.28 148.35 Q 686.01 176.7 734.64 225.3 Q 783.27 273.9 811.63 339.6 Q 840 405.3 840 480 L 840 516.85 Q 840 565.92 805.78 599.81 Q 771.55 633.69 721.69 633.69 Q 685.4 633.69 655.47 612.73 Q 625.54 591.77 613.92 557.46 Q 591.77 593 556.48 613.35 Q 521.2 633.69 480 633.69 Q 416.24 633.69 371.16 588.92 Q 326.08 544.15 326.08 479.6 Q 326.08 415.05 371.16 370.45 Q 416.24 325.85 480 325.85 Q 543.76 325.85 588.84 370.45 Q 633.92 415.06 633.92 480.09 L 633.92 516.85 Q 633.92 552.84 659.88 577.88 Q 685.85 602.92 721.58 602.92 Q 757.31 602.92 783.27 577.88 Q 809.23 552.84 809.23 516.85 L 809.23 480 Q 809.23 342.13 713.57 246.45 Q 617.91 150.77 480.07 150.77 Q 342.24 150.77 246.5 246.43 Q 150.77 342.09 150.77 479.93 Q 150.77 617.76 246.45 713.5 Q 342.13 809.23 480 809.23 L 686.31 809.23 L 686.31 840 L 480 840 Z M 480.1 602.92 Q 531.46 602.92 567.31 567.07 Q 603.15 531.22 603.15 480 Q 603.15 427.92 567.2 392.27 Q 531.25 356.62 479.9 356.62 Q 428.54 356.62 392.69 392.35 Q 356.85 428.08 356.85 480.27 Q 356.85 530.96 392.8 566.94 Q 428.75 602.92 480.1 602.92 Z", + "revanced_preference_screen_ryd" to "M 262.654 192.307 L 666 192.307 L 666 628.923 L 415.692 880 L 403.602 871.186 Q 398.384 866.308 395.384 859.231 Q 392.384 852.154 392.384 843.769 L 392.384 839.923 L 433.538 628.923 L 136.846 628.923 Q 115.461 628.923 98.461 611.923 Q 81.461 594.923 81.461 573.538 L 81.461 523.176 Q 81.461 517.615 81.115 511.269 Q 80.769 504.923 83 499.461 L 195.154 237.153 Q 202.494 218.211 222.441 205.259 Q 242.388 192.307 262.654 192.307 Z M 635.231 223.077 L 256.692 223.077 Q 248.231 223.077 239.385 227.692 Q 230.538 232.307 225.923 243.077 L 112.231 512.077 L 112.231 573.538 Q 112.231 583.538 119.154 590.846 Q 126.077 598.154 136.846 598.154 L 470.615 598.154 L 424.538 829.461 L 635.231 615.461 L 635.231 223.077 Z M 635.231 615.461 L 635.231 223.077 L 635.231 615.461 Z M 666 628.923 L 666 598.154 L 809 598.154 L 809 223.077 L 666 223.077 L 666 192.307 L 839.769 192.307 L 839.769 628.923 L 666 628.923 Z", + "revanced_preference_screen_sb" to "M 480 838.231 Q 359.231 801.693 279.615 690.346 Q 200 579 200 440.846 L 200 227.461 L 480 122.846 L 760 227.461 L 760 440.846 Q 760 579 680.385 690.346 Q 600.769 801.693 480 838.231 Z M 480 805.462 Q 588.846 770.539 659.039 668.5 Q 729.231 566.462 729.231 440.846 L 729.231 248.692 L 480 155.308 L 230.769 248.692 L 230.769 440.846 Q 230.769 566.462 300.961 668.5 Q 371.154 770.539 480 805.462 Z M 480 480.769 Z", + "revanced_preference_screen_misc" to "M 658.231 466.308 L 495.231 303.308 L 658.231 140.307 L 821.231 303.308 L 658.231 466.308 Z M 184.615 416.615 L 184.615 184.846 L 415.615 184.846 L 415.615 416.615 L 184.615 416.615 Z M 543.385 775.385 L 543.385 544.385 L 775.154 544.385 L 775.154 775.385 L 543.385 775.385 Z M 184.615 775.385 L 184.615 544.385 L 415.615 544.385 L 415.615 775.385 L 184.615 775.385 Z M 215.384 385.846 L 384.846 385.846 L 384.846 215.615 L 215.384 215.615 L 215.384 385.846 Z M 660.462 425.308 L 780.231 305.538 L 660.462 185 L 539.923 305.538 L 660.462 425.308 Z M 574.154 744.616 L 744.385 744.616 L 744.385 575.154 L 574.154 575.154 L 574.154 744.616 Z M 215.384 744.616 L 384.846 744.616 L 384.846 575.154 L 215.384 575.154 L 215.384 744.616 Z M 384.846 385.846 Z M 539.923 305.538 Z M 384.846 575.154 Z M 574.154 575.154 Z", +) + +private val rvxPreferenceKey = mapOf( + // Internal RVX settings + // Action bar + "revanced_hide_action_button_like_dislike" to "settings_header_recommendations", + "revanced_hide_action_button_comment" to "M 349.504 315.587 L 610.497 315.587 L 610.497 368.835 L 349.504 368.835 L 349.504 315.587 Z M 755.994 135.465 L 755.994 824.535 L 584.287 652.829 L 204.007 652.829 L 204.007 135.465 L 755.994 135.465 Z M 236.631 169.09 L 236.631 621.204 L 236.631 620.204 L 597.654 620.204 L 597.947 620.497 L 607.403 629.952 L 721.662 744.211 L 723.369 745.918 L 725.076 747.625 L 723.369 745.918 L 723.369 748.332 L 723.369 167.09 L 723.369 168.09 L 237.631 168.09 M 349.504 454.083 L 506.624 454.083 L 506.624 507.332 L 349.504 507.332 L 349.504 454.083 Z", + "revanced_hide_action_button_add_to_playlist" to "M 835.42 506.992 L 689.452 506.992 L 689.452 652.961 L 648.468 652.961 L 648.468 506.992 L 502.501 506.992 L 502.501 466.008 L 648.468 466.008 L 648.468 320.039 L 689.452 320.039 L 689.452 466.008 L 835.42 466.008 L 835.42 506.992 Z M 124.579 598.977 L 410.517 598.977 L 410.517 629.469 L 124.579 629.469 L 124.579 598.977 Z M 124.579 453.008 L 410.517 453.008 L 410.517 483.5 L 124.579 483.5 L 124.579 453.008 Z M 556.485 337.531 L 124.579 337.531 L 124.579 307.039 L 556.485 307.039 L 556.485 337.531 Z", + "revanced_hide_action_button_download" to "settings_header_downloads_and_storage", + "revanced_hide_action_button_share" to "M 582.272 263.479 L 773.48 478.672 L 774.07 479.336 L 774.66 480 L 774.07 480.664 L 773.48 481.328 L 582.272 696.521 L 580.524 698.488 L 578.776 700.455 L 578.776 549.565 L 546.994 549.565 C 413.517 549.519 306.714 583.081 218.867 653.513 L 215.61 656.123 L 212.353 658.734 L 214.074 654.931 L 215.795 651.128 C 278.284 513.232 389.516 433.944 551.428 410.119 L 578.776 405.986 L 578.776 264.807 M 548.994 181.22 L 548.994 380.383 L 547.283 380.633 C 416.228 399.594 324.882 452.742 261.305 525.152 C 197.703 597.506 164.544 683.009 145.423 775.231 C 238.974 645.857 358.678 579.473 546.994 579.347 L 548.994 579.347 L 548.994 778.78 L 814.576 480 L 548.994 181.22 Z", + "revanced_hide_action_button_radio" to "M 422.104 386.98 L 583.72 479.994 L 422.104 573.399 Z M 346.939 350.115 L 325.211 328.692 L 325.212 328.691 C 286.868 367.592 263.417 419.864 263.417 480 C 263.417 540.137 286.859 592.393 325.198 631.293 L 325.197 631.293 L 346.944 609.545 L 346.945 609.546 C 314.561 576.347 294.014 531.493 294.014 480 C 294.014 428.508 314.559 383.655 346.939 350.115 Z M 634.801 328.707 L 634.802 328.707 L 613.055 350.455 L 613.054 350.454 C 645.442 383.655 665.986 428.508 665.986 480 C 665.986 531.493 645.436 576.361 613.045 609.901 L 634.793 631.649 L 634.792 631.65 C 673.135 592.398 696.583 540.137 696.583 480 C 696.583 419.864 673.14 367.607 634.801 328.707 Z M 243.391 246.873 L 221.977 225.152 L 221.978 225.152 C 156.934 290.764 117.029 379.607 117.029 480 C 117.029 580.394 156.94 669.249 221.992 734.863 L 243.736 713.119 L 243.736 713.12 C 184.646 653.203 147.627 571.747 147.627 480 C 147.627 388.254 184.649 306.786 243.391 246.873 Z M 738.009 225.137 L 716.264 246.882 L 716.264 246.881 C 775.353 306.797 812.374 388.254 812.374 480 C 812.374 571.747 775.356 653.2 716.623 713.113 L 715.93 713.82 L 716.623 713.113 L 738.368 734.857 C 803.062 669.243 842.97 580.394 842.97 480 C 842.97 379.607 803.059 290.752 738.009 225.137 Z", + + // Ads + "revanced_hide_fullscreen_ads" to "M 160 800 L 160 626.231 L 190.769 626.231 L 190.769 769.231 L 333.769 769.231 L 333.769 800 L 160 800 Z M 627 800 L 627 769.231 L 770 769.231 L 770 626.231 L 800.769 626.231 L 800.769 800 L 627 800 Z M 160 333.769 L 160 160 L 333.769 160 L 333.769 190.769 L 190.769 190.769 L 190.769 333.769 L 160 333.769 Z M 770 333.769 L 770 190.769 L 627 190.769 L 627 160 L 800.769 160 L 800.769 333.769 L 770 333.769 Z", + "revanced_hide_general_ads" to "settings_header_general", + "revanced_hide_music_ads" to "M 415.341 563.645 L 415.341 396.354 L 573.171 480 Z M 479.656 301.499 C 479.656 301.499 477.602 301.508 476.535 301.526 C 475.472 301.543 474.414 301.566 473.365 301.609 C 471.265 301.679 469.115 301.788 467.036 301.928 C 462.876 302.214 458.683 302.642 454.633 303.196 C 446.53 304.311 438.482 305.992 430.859 308.122 C 415.597 312.413 401.026 318.776 387.962 326.606 C 361.88 342.363 340.123 364.946 325.164 390.733 C 295.581 442.402 291.436 509.98 325.133 569.213 C 359.578 628.011 420.197 658.5 479.742 658.5 C 539.285 658.5 599.865 628.085 634.31 569.28 C 649.78 542.311 658.243 511.102 658.243 480 C 658.243 431.069 637.942 385.862 605.968 353.786 C 573.891 321.811 528.671 301.715 479.747 301.499 M 479.741 276.799 C 558.491 276.799 621.3 318.581 655.718 378.4 C 690.316 438.118 695.446 513.631 655.711 581.614 C 616.704 650.02 548.755 683.201 479.742 683.201 C 410.728 683.201 342.765 649.975 303.754 581.583 C 285.734 550.54 276.541 515.901 276.541 480 C 276.541 423.527 299.01 373.362 336.057 336.315 C 373.104 299.268 423.269 276.799 479.741 276.799 Z M 479.7 162.698 C 479.7 162.698 477.838 162.702 476.886 162.71 C 475.936 162.718 474.984 162.73 474.038 162.746 C 472.15 162.777 470.251 162.825 468.372 162.892 C 464.619 163.018 460.846 163.211 457.138 163.461 C 449.722 163.968 442.308 164.728 435.085 165.717 C 420.637 167.703 406.421 170.67 392.825 174.472 C 365.611 182.106 339.875 193.348 316.582 207.329 C 270.048 235.374 231.602 275.308 204.954 321.342 C 151.998 413.493 144.517 533.126 204.939 638.628 C 266.097 743.704 373.453 797.301 479.742 797.301 C 586.029 797.301 693.363 743.748 754.522 638.667 C 782.177 590.593 797.043 535.468 797.043 480 C 797.043 392.742 761.231 312.805 704.109 255.635 C 646.94 198.513 567.001 162.915 479.744 162.698 M 479.741 137.998 C 611.917 137.998 718.227 208.65 775.925 309.001 C 833.983 409.147 842.365 536.758 775.918 651.011 C 710.197 765.683 595.501 822.001 479.742 822.001 C 363.983 822.001 249.28 765.673 183.558 650.999 C 153.354 598.855 137.74 540.266 137.74 480 C 137.74 385.198 175.745 300.333 237.91 238.168 C 300.075 176.003 384.94 137.998 479.741 137.998 Z", + + // Flyout menu + "revanced_hide_flyout_menu_like_dislike" to "settings_header_recommendations", + "revanced_hide_flyout_menu_add_to_queue" to "M 546.4 312.25 L 151 312.25 L 151 284.8 L 546.4 284.8 L 546.4 312.25 Z M 809 374.15 L 675.2 374.15 L 675.2 600.37 L 674.93 600.917 L 674.881 602.426 L 674.87 602.672 C 674.423 623.314 666.047 640.647 651.935 654.022 C 638.147 667.701 620.56 675.2 599.575 675.2 C 578.132 675.2 560.236 667.17 546.108 653.042 C 531.98 638.914 523.95 621.018 523.95 599.575 C 523.95 578.132 531.98 560.236 546.108 546.108 C 560.236 531.98 578.132 523.95 599.575 523.95 C 617.26 523.95 631.904 529.247 644.895 539.369 L 657.75 549.185 L 657.75 289.8 L 809 289.8 L 809 374.15 Z M 412.6 579.85 L 151 579.85 L 151 552.4 L 412.6 552.4 L 412.6 579.85 Z M 412.6 446.05 L 151 446.05 L 151 418.6 L 412.6 418.6 L 412.6 446.05 Z", + "revanced_hide_flyout_menu_captions" to "M 215.384 760 Q 192.327 760 176.163 743.837 Q 160 727.673 160 704.616 L 160 255.384 Q 160 232.327 176.163 216.163 Q 192.327 200 215.384 200 L 744.616 200 Q 767.673 200 783.837 216.163 Q 800 232.327 800 255.384 L 800 704.616 Q 800 727.673 783.837 743.837 Q 767.673 760 744.616 760 L 215.384 760 Z M 215.384 729.231 L 744.616 729.231 Q 753.846 729.231 761.539 721.539 Q 769.231 713.846 769.231 704.616 L 769.231 255.384 Q 769.231 246.154 761.539 238.461 Q 753.846 230.769 744.616 230.769 L 215.384 230.769 Q 206.154 230.769 198.461 238.461 Q 190.769 246.154 190.769 255.384 L 190.769 704.616 Q 190.769 713.846 198.461 721.539 Q 206.154 729.231 215.384 729.231 Z M 306.154 587.462 L 405.846 587.462 Q 421.654 587.462 432.981 576.135 Q 444.308 564.808 444.308 549 L 444.308 532.385 L 413.538 532.385 L 413.538 544.385 Q 413.538 549 409.692 552.846 Q 405.846 556.692 401.231 556.692 L 310.769 556.692 Q 306.154 556.692 302.308 552.846 Q 298.461 549 298.461 544.385 L 298.461 415.615 Q 298.461 411 302.308 407.154 Q 306.154 403.308 310.769 403.308 L 401.231 403.308 Q 405.846 403.308 409.692 407.154 Q 413.538 411 413.538 415.615 L 413.538 429.154 L 444.308 429.154 L 444.308 411 Q 444.308 395.192 432.981 383.865 Q 421.654 372.538 405.846 372.538 L 306.154 372.538 Q 290.346 372.538 279.019 383.865 Q 267.692 395.192 267.692 411 L 267.692 549 Q 267.692 564.808 279.019 576.135 Q 290.346 587.462 306.154 587.462 Z M 555.154 587.462 L 654.077 587.462 Q 669.923 587.462 681.231 575.76 Q 692.539 564.058 692.539 549 L 692.539 532.385 L 661.769 532.385 L 661.769 544.385 Q 661.769 549 657.923 552.846 Q 654.077 556.692 649.462 556.692 L 559.769 556.692 Q 555.154 556.692 551.308 552.846 Q 547.462 549 547.462 544.385 L 547.462 415.615 Q 547.462 411 551.308 407.154 Q 555.154 403.308 559.769 403.308 L 649.462 403.308 Q 654.077 403.308 657.923 407.154 Q 661.769 411 661.769 415.615 L 661.769 429.154 L 692.539 429.154 L 692.539 411 Q 692.539 395.942 681.231 384.24 Q 669.923 372.538 654.077 372.538 L 555.154 372.538 Q 539.308 372.538 528 384.24 Q 516.692 395.942 516.692 411 L 516.692 549 Q 516.692 564.058 528 575.76 Q 539.308 587.462 555.154 587.462 Z M 190.769 729.231 L 190.769 230.769 L 190.769 729.231 Z", + "revanced_hide_flyout_menu_delete_playlist" to "M 295.62 800 Q 273.37 800 256.8 783.43 Q 240.23 766.87 240.23 744.62 L 240.23 226.15 L 215.38 226.15 Q 208.85 226.15 204.42 221.67 Q 200 217.18 200 210.55 Q 200 203.92 204.42 199.65 Q 208.85 195.38 215.38 195.38 L 354.15 195.38 Q 354.15 184.69 362.02 176.96 Q 369.88 169.23 380.31 169.23 L 579.69 169.23 Q 590.12 169.23 597.98 177.1 Q 605.85 184.96 605.85 195.38 L 744.62 195.38 Q 751.15 195.38 755.58 199.87 Q 760 204.35 760 210.98 Q 760 217.62 755.58 221.88 Q 751.15 226.15 744.62 226.15 L 719.77 226.15 L 719.77 744.62 Q 719.77 766.87 703.2 783.43 Q 686.63 800 664.38 800 L 295.62 800 Z M 689 226.15 L 271 226.15 L 271 744.62 Q 271 755.38 278.31 762.31 Q 285.62 769.23 295.62 769.23 L 664.38 769.23 Q 674.38 769.23 681.69 762.31 Q 689 755.38 689 744.62 L 689 226.15 Z M 411.06 686.31 Q 417.69 686.31 421.96 681.88 Q 426.23 677.46 426.23 670.92 L 426.23 323.46 Q 426.23 317.67 421.75 312.87 Q 417.26 308.08 410.63 308.08 Q 404 308.08 399.73 312.87 Q 395.46 317.67 395.46 323.46 L 395.46 670.92 Q 395.46 677.46 399.95 681.88 Q 404.43 686.31 411.06 686.31 Z M 549.37 686.31 Q 556 686.31 560.27 681.88 Q 564.54 677.46 564.54 670.92 L 564.54 323.46 Q 564.54 317.67 560.05 312.87 Q 555.57 308.08 548.94 308.08 Q 542.31 308.08 538.04 312.87 Q 533.77 317.67 533.77 323.46 L 533.77 670.92 Q 533.77 677.46 538.25 681.88 Q 542.74 686.31 549.37 686.31 Z M 271 226.15 L 271 769.23 L 271 226.15 Z", + "revanced_hide_flyout_menu_dismiss_queue" to "M 824.486 638.927 L 793.746 669.666 L 707.078 582.998 L 620.41 669.666 L 589.67 638.927 L 676.339 552.259 L 589.67 465.59 L 620.41 434.851 L 707.078 521.519 L 793.746 434.851 L 824.486 465.59 L 737.817 552.259 L 824.486 638.927 Z M 514.846 624.951 L 104.536 624.951 L 104.536 593.105 L 514.846 593.105 L 514.846 624.951 Z M 514.846 473.566 L 104.536 473.566 L 104.536 441.719 L 514.846 441.719 L 514.846 473.566 Z M 855.463 322.18 L 104.536 322.18 L 104.536 290.334 L 855.463 290.334 L 855.463 322.18 Z", + "revanced_hide_flyout_menu_download" to "settings_header_downloads_and_storage", + "revanced_hide_flyout_menu_edit_playlist" to "M 544.005 339.586 L 619.928 415.509 L 620.635 416.216 L 621.342 416.923 L 622.049 417.63 L 622.756 418.337 L 622.049 419.044 L 621.342 419.751 L 620.635 420.458 L 619.928 421.165 L 293.524 747.568 L 293.231 747.861 L 292.938 748.154 L 292.645 748.447 L 292.352 748.74 L 210.774 748.74 L 210.774 667.161 L 211.067 666.868 L 211.36 666.575 L 211.653 666.282 L 211.946 665.989 L 538.349 339.586 M 541.177 302.322 L 186.328 657.17 L 186.328 773.186 L 302.343 773.186 L 657.192 418.337 L 541.177 302.322 Z M 659.187 224.404 L 736.083 301.3 L 736.79 302.007 L 737.497 302.714 L 738.204 303.421 L 738.911 304.128 L 738.204 304.835 L 737.497 305.542 L 736.79 306.249 L 736.083 306.956 L 699.095 343.944 L 698.388 344.651 L 697.681 345.358 L 696.974 346.065 L 696.267 346.772 L 695.56 346.065 L 694.853 345.358 L 694.146 344.651 L 693.439 343.944 L 616.543 267.048 L 615.836 266.341 L 615.129 265.634 L 614.422 264.927 L 613.715 264.22 L 614.422 263.513 L 615.129 262.806 L 615.836 262.099 L 616.543 261.392 L 653.531 224.404 M 656.359 186.815 L 579.279 263.895 L 696.592 381.208 L 773.672 304.128 L 656.359 186.815 Z", + "revanced_hide_flyout_menu_go_to_album" to "M 480 612.46 Q 536.15 612.46 575.77 574.19 Q 615.38 535.92 615.38 480 Q 615.38 423.62 575.88 384.12 Q 536.38 344.62 480 344.62 Q 424.08 344.62 385.81 384.23 Q 347.54 423.85 347.54 480 Q 347.54 535.92 385.81 574.19 Q 424.08 612.46 480 612.46 Z M 480 520 Q 463 520 451.5 508.5 Q 440 497 440 480 Q 440 463 451.5 451.5 Q 463 440 480 440 Q 497 440 508.5 451.5 Q 520 463 520 480 Q 520 497 508.5 508.5 Q 497 520 480 520 Z M 480.13 840 Q 405.69 840 340.34 811.66 Q 274.99 783.32 225.86 734.24 Q 176.73 685.16 148.37 619.87 Q 120 554.58 120 480.13 Q 120 405.46 148.34 339.72 Q 176.68 273.99 225.76 225.36 Q 274.84 176.73 340.13 148.37 Q 405.42 120 479.87 120 Q 554.54 120 620.28 148.34 Q 686.01 176.68 734.64 225.26 Q 783.27 273.84 811.63 339.52 Q 840 405.19 840 479.87 Q 840 554.31 811.66 619.66 Q 783.32 685.01 734.74 734.14 Q 686.16 783.27 620.48 811.63 Q 554.81 840 480.13 840 Z M 480 809.23 Q 617.38 809.23 713.31 713.19 Q 809.23 617.15 809.23 480 Q 809.23 342.62 713.31 246.69 Q 617.38 150.77 480 150.77 Q 342.85 150.77 246.81 246.69 Q 150.77 342.62 150.77 480 Q 150.77 617.15 246.81 713.19 Q 342.85 809.23 480 809.23 Z M 480 480 Z", + "revanced_hide_flyout_menu_go_to_artist" to "M 696.27 744.62 Q 660.38 744.62 637.15 721.43 Q 613.92 698.24 613.92 662.35 Q 613.92 626.46 637.12 603.23 Q 660.32 580 696.62 580 Q 712.85 580 726.08 584.77 Q 739.31 589.54 748 599.08 L 748 421.54 Q 748 409.77 756.34 401.81 Q 764.67 393.85 775.69 393.85 L 833.85 393.85 Q 844.27 393.85 852.13 401.42 Q 860 408.99 860 420.19 Q 860 430.69 852.13 438.42 Q 844.27 446.15 833.85 446.15 L 778.54 446.15 L 778.54 661.92 Q 778.54 698.22 755.35 721.42 Q 732.16 744.62 696.27 744.62 Z M 149.23 744.62 Q 137.46 744.62 129.5 736.65 Q 121.54 728.69 121.54 716.92 L 121.54 686.77 Q 121.54 663.23 135.46 641.42 Q 149.38 619.62 176.46 606.92 Q 240.69 578.54 294.05 566.19 Q 347.41 553.85 401.54 553.85 Q 433.46 553.85 461.58 557.58 Q 489.69 561.31 517.77 568.77 Q 525.31 571.46 527.23 576.46 Q 529.15 581.45 528.31 586.42 Q 527.46 591.38 523.46 594.54 Q 519.46 597.69 513.38 596 Q 489 590.31 459.97 587.46 Q 430.93 584.62 401.54 584.62 Q 348.15 584.62 297.92 595.62 Q 247.69 606.62 190.46 634 Q 171.08 642.84 161.69 657.77 Q 152.31 672.7 152.31 686.77 L 152.31 713.85 L 508.92 713.85 Q 516.62 713.85 520.46 718.55 Q 524.31 723.26 524.31 728.91 Q 524.31 734.56 520.21 739.59 Q 516.12 744.62 507.92 744.62 L 149.23 744.62 Z M 401.54 455.15 Q 352.04 455.15 319.1 422.21 Q 286.15 389.27 286.15 339.38 Q 286.15 289.5 319.1 256.94 Q 352.04 224.38 401.54 224.38 Q 451.04 224.38 483.98 256.94 Q 516.92 289.5 516.92 339.38 Q 516.92 389.27 483.98 422.21 Q 451.04 455.15 401.54 455.15 Z M 401.54 424.38 Q 437.46 424.38 461.81 400.04 Q 486.15 375.69 486.15 339.77 Q 486.15 303.85 461.81 279.5 Q 437.46 255.15 401.54 255.15 Q 365.62 255.15 341.27 279.5 Q 316.92 303.85 316.92 339.77 Q 316.92 375.69 341.27 400.04 Q 365.62 424.38 401.54 424.38 Z M 401.54 339.77 Z M 401.54 713.85 Z", + "revanced_hide_flyout_menu_go_to_episode" to "settings_header_about_youtube_music", + "revanced_hide_flyout_menu_go_to_podcast" to "M 464.62 824.62 L 464.62 516.31 Q 452.62 511.15 446.31 501.15 Q 440 491.15 440 480 Q 440 463.92 451.96 451.96 Q 463.92 440 480 440 Q 496.08 440 508.04 451.96 Q 520 463.92 520 480 Q 520 491.15 513.69 500.77 Q 507.38 510.38 495.38 516.31 L 495.38 824.62 Q 495.38 831.46 491.12 835.73 Q 486.85 840 480 840 Q 473.15 840 468.88 835.73 Q 464.62 831.46 464.62 824.62 Z M 243.69 730.31 Q 238.54 735.46 231.19 734.69 Q 223.85 733.92 219.69 728.54 Q 173.23 679.15 146.62 616.27 Q 120 553.38 120 480 Q 120 405.46 148.42 339.77 Q 176.85 274.08 225.46 225.46 Q 274.08 176.85 339.77 148.42 Q 405.46 120 480 120 Q 554.54 120 620.23 148.42 Q 685.92 176.85 734.54 225.46 Q 783.15 274.08 811.58 339.77 Q 840 405.46 840 480 Q 840 552.62 813.38 615.88 Q 786.77 679.15 740.31 728.54 Q 735.38 733.92 728.04 734.81 Q 720.69 735.69 716.31 730.54 Q 711.15 725.38 712.54 718.15 Q 713.92 710.92 719.08 705.54 Q 761.38 661.31 785.31 603.92 Q 809.23 546.54 809.23 480 Q 809.23 342.62 713.31 246.69 Q 617.38 150.77 480 150.77 Q 342.62 150.77 246.69 246.69 Q 150.77 342.62 150.77 480 Q 150.77 546.54 174.31 603.92 Q 197.85 661.31 240.15 705.54 Q 245.31 711.69 247.08 718.42 Q 248.85 725.15 243.69 730.31 Z M 356.69 617.31 Q 351.54 622.46 345.08 621.69 Q 338.62 620.92 333.69 616.31 Q 308.46 588.92 294.23 554.65 Q 280 520.38 280 480 Q 280 396.92 338.46 338.46 Q 396.92 280 480 280 Q 563.08 280 621.54 338.46 Q 680 396.92 680 480 Q 680 519.62 665.77 554.27 Q 651.54 588.92 626.31 616.31 Q 621.38 621.69 615.04 622.19 Q 608.69 622.69 603.54 617.54 Q 599.15 612.38 599.15 605.92 Q 599.15 599.46 604.08 594.85 Q 625.15 572.62 637.19 543.08 Q 649.23 513.54 649.23 480 Q 649.23 409.62 599.81 360.19 Q 550.38 310.77 480 310.77 Q 409.62 310.77 360.19 360.19 Q 310.77 409.62 310.77 480 Q 310.77 513.54 322.81 543.08 Q 334.85 572.62 355.92 594.85 Q 360.85 600.23 361.35 606.19 Q 361.85 612.15 356.69 617.31 Z", + "revanced_hide_flyout_menu_help" to "M 484.043 686.077 Q 494.615 686.077 502.154 678.111 Q 509.692 670.144 509.692 659.572 Q 509.692 649.769 502.111 641.846 Q 494.529 633.923 483.957 633.923 Q 473.385 633.923 465.461 641.89 Q 457.538 649.856 457.538 659.659 Q 457.538 670.231 465.505 678.154 Q 473.471 686.077 484.043 686.077 Z M 463.615 557 L 495.692 557 Q 496.462 534.077 504.5 516.423 Q 512.539 498.769 538.077 476.154 Q 566.769 449.385 579 427.461 Q 591.231 405.538 591.231 379.134 Q 591.231 333.308 561.04 304.384 Q 530.848 275.461 485.231 275.461 Q 445.461 275.461 414.5 296.5 Q 383.538 317.538 368.077 348.231 L 398.769 360.538 Q 410.538 334.846 430.615 320.5 Q 450.692 306.154 482.231 306.154 Q 521.615 306.154 541.846 327.731 Q 562.077 349.308 562.077 379.077 Q 562.077 400.308 550.615 417.885 Q 539.154 435.461 518 454.154 Q 489.538 480.154 476.577 505.269 Q 463.615 530.385 463.615 557 Z M 480.134 840 Q 405.692 840 340.34 811.661 Q 274.987 783.321 225.859 734.239 Q 176.732 685.157 148.366 619.866 Q 120 554.575 120 480.134 Q 120 405.461 148.339 339.724 Q 176.679 273.987 225.761 225.359 Q 274.843 176.732 340.134 148.366 Q 405.425 120 479.866 120 Q 554.539 120 620.276 148.339 Q 686.013 176.679 734.641 225.261 Q 783.268 273.843 811.634 339.518 Q 840 405.194 840 479.866 Q 840 554.308 811.661 619.66 Q 783.321 685.013 734.739 734.141 Q 686.157 783.268 620.482 811.634 Q 554.806 840 480.134 840 Z M 480 809.231 Q 617.385 809.231 713.308 713.192 Q 809.231 617.154 809.231 480 Q 809.231 342.615 713.308 246.692 Q 617.385 150.769 480 150.769 Q 342.846 150.769 246.808 246.692 Q 150.769 342.615 150.769 480 Q 150.769 617.154 246.808 713.192 Q 342.846 809.231 480 809.231 Z M 480 480 Z", + "revanced_hide_flyout_menu_play_next" to "M 832.907 300.993 L 127.094 300.993 L 127.094 271.402 L 832.907 271.402 L 832.907 300.993 Z M 590.772 417.657 L 827.843 553.127 L 590.772 688.598 L 590.772 417.657 Z M 512.591 585.718 L 127.094 585.718 L 127.094 556.127 L 512.591 556.127 L 512.591 585.718 Z M 512.591 443.355 L 127.094 443.355 L 127.094 413.765 L 512.591 413.765 L 512.591 443.355 Z", + "revanced_hide_flyout_menu_quality" to "M 413.384 840 L 397.231 725.539 Q 375.154 718.539 348.769 703.846 Q 322.385 689.154 304.077 672.308 L 198.384 720.154 L 131.538 601.538 L 225.692 531.769 Q 223.692 519.692 222.423 506.269 Q 221.154 492.846 221.154 480.769 Q 221.154 469.462 222.423 456.038 Q 223.692 442.615 225.692 428.231 L 131.538 357.692 L 198.384 241.384 L 303.308 287.692 Q 323.923 270.846 348.769 256.538 Q 373.615 242.231 396.461 235.461 L 413.384 120 L 546.616 120 L 562.769 235.231 Q 587.923 244.538 610.577 257.192 Q 633.231 269.846 653.615 287.692 L 762.385 241.384 L 828.462 357.692 L 731.231 429.308 Q 734.769 443.154 735.654 455.808 Q 736.539 468.462 736.539 480 Q 736.539 490.769 735.269 503.308 Q 734 515.846 731.231 531.231 L 827.693 601.538 L 760.846 720.154 L 653.615 671.539 Q 632.231 689.923 609.423 703.962 Q 586.616 718 562.769 724.769 L 546.616 840 L 413.384 840 Z M 438.308 809.231 L 520.923 809.231 L 535.692 698 Q 566.385 690 592.039 675.308 Q 617.692 660.615 644 635.846 L 746.923 680.308 L 786.923 610.615 L 696 543.154 Q 700 524.615 702.115 509.654 Q 704.231 494.692 704.231 480 Q 704.231 463.769 702.231 449.577 Q 700.231 435.385 696 418.385 L 788.462 349.385 L 748.462 279.692 L 643.231 324.154 Q 624.077 302.769 593.539 284.115 Q 563 265.461 534.923 262 L 521.692 150.769 L 438.308 150.769 L 425.846 261.231 Q 393.385 267.461 366.961 282.538 Q 340.538 297.615 315.231 323.385 L 211.538 279.692 L 171.538 349.385 L 262.461 416.077 Q 257.692 430.769 255.577 446.885 Q 253.461 463 253.461 480.769 Q 253.461 497 255.577 512.346 Q 257.692 527.692 261.692 543.154 L 171.538 610.615 L 211.538 680.308 L 314.461 636.615 Q 338.461 661.385 365.269 676.077 Q 392.077 690.769 425.077 698.769 L 438.308 809.231 Z M 477.692 575.385 Q 517.846 575.385 545.462 547.769 Q 573.077 520.154 573.077 480 Q 573.077 439.846 545.462 412.231 Q 517.846 384.615 477.692 384.615 Q 438.308 384.615 410.308 412.231 Q 382.307 439.846 382.307 480 Q 382.307 520.154 410.308 547.769 Q 438.308 575.385 477.692 575.385 Z M 480 480 Z", + "revanced_hide_flyout_menu_remove_from_library" to "M 700.333 780.777 L 700.333 811.999 L 148 811.999 L 148 259.666 L 179.222 259.666 L 179.222 780.777 L 700.333 780.777 Z M 811.999 148 L 811.999 700.332 L 259.667 700.332 L 259.667 148 L 811.999 148 Z M 420.816 386.424 L 364.091 443.15 L 479.627 558.686 L 691.941 346.371 L 635.216 289.646 L 479.627 445.235 L 420.816 386.424 Z", + "revanced_hide_flyout_menu_remove_from_playlist" to "revanced_hide_flyout_menu_delete_playlist", + "revanced_hide_flyout_menu_report" to "M 240 820 L 240 200 L 519.923 200 L 537.385 282.923 L 760 282.923 L 760 589.077 L 563.231 589.077 L 545.887 506.385 L 270.769 506.385 L 270.769 820 L 240 820 Z M 500 394.154 Z M 590.385 558.308 L 729.231 558.308 L 729.231 313.692 L 510.231 313.692 L 492.769 230.769 L 270.769 230.769 L 270.769 475.615 L 572.923 475.615 L 590.385 558.308 Z", + "revanced_hide_flyout_menu_save_episode_for_later" to "M 240 780 L 240 212.692 Q 240 190.231 256.163 173.769 Q 272.327 157.307 295.384 157.307 L 664.616 157.307 Q 687.673 157.307 703.837 173.769 Q 720 190.231 720 212.692 L 720 780 L 480 676.923 L 240 780 Z M 270.769 732.077 L 480 642.923 L 689.231 732.077 L 689.231 212.692 Q 689.231 203.461 681.539 195.769 Q 673.846 188.077 664.616 188.077 L 295.384 188.077 Q 286.154 188.077 278.461 195.769 Q 270.769 203.461 270.769 212.692 L 270.769 732.077 Z M 270.769 188.077 L 689.231 188.077 L 270.769 188.077 Z", + "revanced_hide_flyout_menu_save_to_library" to "M 530.96 550.46 Q 537.62 550.46 541.88 546.06 Q 546.15 541.65 546.15 535.08 L 546.15 424.62 L 656.62 424.62 Q 663.19 424.62 667.6 420.16 Q 672 415.7 672 409.04 Q 672 402.38 667.6 398.12 Q 663.19 393.85 656.62 393.85 L 546.15 393.85 L 546.15 283.38 Q 546.15 276.81 541.7 272.4 Q 537.24 268 530.58 268 Q 523.92 268 519.65 272.4 Q 515.38 276.81 515.38 283.38 L 515.38 393.85 L 404.92 393.85 Q 398.35 393.85 393.94 398.3 Q 389.54 402.76 389.54 409.42 Q 389.54 416.08 393.94 420.35 Q 398.35 424.62 404.92 424.62 L 515.38 424.62 L 515.38 535.08 Q 515.38 541.65 519.84 546.06 Q 524.3 550.46 530.96 550.46 Z M 296.92 698.46 Q 273.87 698.46 257.7 682.3 Q 241.54 666.13 241.54 643.08 L 241.54 175.38 Q 241.54 152.33 257.7 136.16 Q 273.87 120 296.92 120 L 764.62 120 Q 787.67 120 803.84 136.16 Q 820 152.33 820 175.38 L 820 643.08 Q 820 666.13 803.84 682.3 Q 787.67 698.46 764.62 698.46 L 296.92 698.46 Z M 296.92 667.69 L 764.62 667.69 Q 773.85 667.69 781.54 660 Q 789.23 652.31 789.23 643.08 L 789.23 175.38 Q 789.23 166.15 781.54 158.46 Q 773.85 150.77 764.62 150.77 L 296.92 150.77 Q 287.69 150.77 280 158.46 Q 272.31 166.15 272.31 175.38 L 272.31 643.08 Q 272.31 652.31 280 660 Q 287.69 667.69 296.92 667.69 Z M 195.38 800 Q 172.33 800 156.16 783.84 Q 140 767.67 140 744.62 L 140 261.54 Q 140 254.96 144.46 250.56 Q 148.92 246.15 155.57 246.15 Q 162.23 246.15 166.5 250.56 Q 170.77 254.96 170.77 261.54 L 170.77 744.62 Q 170.77 753.85 178.46 761.54 Q 186.15 769.23 195.38 769.23 L 678.46 769.23 Q 685.04 769.23 689.44 773.69 Q 693.85 778.15 693.85 784.8 Q 693.85 791.46 689.44 795.73 Q 685.04 800 678.46 800 L 195.38 800 Z M 272.31 150.77 L 272.31 667.69 L 272.31 150.77 Z", + "revanced_hide_flyout_menu_save_to_playlist" to "revanced_hide_action_button_add_to_playlist", + "revanced_hide_flyout_menu_share" to "revanced_hide_action_button_share", + "revanced_hide_flyout_menu_shuffle_play" to "M 676.173 536.538 L 794.375 654.74 L 676.188 772.926 L 657.858 754.593 L 745.143 667.639 L 703.218 667.639 C 613.531 667.774 531.017 630.041 472.47 563.469 L 492.084 546.694 C 545.31 606.758 621.505 641.702 703.219 641.84 L 745.122 641.84 L 657.868 554.586 L 676.173 536.538 Z M 165.625 317.861 L 165.625 292.055 C 253.245 292.763 333.593 329.038 391.796 394.351 L 372.437 411.389 C 318.941 352.29 245.254 318.827 165.625 317.861 Z M 455.727 464.734 L 432.512 507.346 C 378.685 606.624 277.522 666.763 165.625 667.628 L 165.625 641.82 C 267.306 640.541 360.595 585.195 410.116 494.949 L 433.332 452.337 C 487.158 353.059 590.144 291.833 703.219 292.043 L 745.161 292.043 L 657.862 205.402 L 676.188 187.075 L 794.375 305.261 L 676.188 423.448 L 657.853 405.113 L 745.122 317.843 L 703.217 317.843 C 600.376 318.053 505.249 374.487 455.727 464.734 Z", + "revanced_hide_flyout_menu_sleep_timer" to "M 619.909 211.686 C 708.837 266.332 766.91 365.444 766.91 472.772 C 766.91 556.761 732.454 633.532 677.278 688.708 C 622.102 743.884 545.331 778.34 461.341 778.34 C 372.969 778.34 290.15 740.045 233.175 675.8 L 231.617 674.041 L 230.058 672.281 L 228.499 670.521 L 230.849 670.619 L 233.199 670.717 L 235.548 670.815 C 243.618 671.152 251.623 671.485 259.629 671.485 C 361.049 671.485 452.416 630.597 518.935 563.985 C 585.547 497.466 626.435 406.098 626.435 304.678 C 626.435 289.304 625.524 274.19 623.683 259.196 C 622.766 251.699 621.623 244.301 620.246 236.914 C 619.556 233.221 618.815 229.564 618.01 225.901 C 617.609 224.069 617.196 222.253 616.765 220.429 C 616.549 219.516 616.332 218.609 616.11 217.701 C 615.887 216.792 615.428 214.956 615.428 214.956 M 727.436 272.364 C 685.26 216.468 630.095 177.263 564.323 156.001 C 586.053 200.723 598.816 248.823 598.816 304.678 C 598.816 398.079 560.616 483.21 499.435 544.484 C 438.162 605.665 353.03 643.866 259.629 643.866 C 225.798 643.866 199.617 640.804 168.464 631.341 C 224.81 735.577 332.06 805.959 461.341 805.959 C 553.517 805.959 636.479 768.853 696.905 708.335 C 757.423 647.911 794.529 564.948 794.529 472.772 C 794.529 397.301 769.69 328.209 727.436 272.364 Z M 458.341 303.664 L 302.152 442.153 L 458.341 442.153 L 458.341 469.772 L 262.629 469.772 L 262.629 440.167 L 418.818 301.678 L 262.629 301.678 L 262.629 274.059 L 458.341 274.059 L 458.341 303.664 Z", + "revanced_hide_flyout_menu_start_radio" to "revanced_hide_action_button_radio", + "revanced_hide_flyout_menu_stats_for_nerds" to "M 503.538 860 L 357.769 190.154 L 250 697.308 L 100 697.308 L 100 667.538 L 224.846 667.538 L 336.846 138.769 L 378.231 138.769 L 523.231 808.769 L 619.461 381.923 L 664.385 381.923 L 735.385 667.538 L 860 667.538 L 860 697.308 L 711.692 697.308 L 642.154 424 L 542.462 860 L 503.538 860 Z", + "revanced_hide_flyout_menu_subscribe" to "settings_header_notifications", + "revanced_hide_flyout_menu_view_song_credit" to "M 64.62 703.08 Q 54.9 703.08 47.45 695.63 Q 40 688.17 40 678.46 L 40 673.15 Q 40 638.43 78.04 615.37 Q 116.08 592.31 176.53 592.31 Q 184.85 592.31 195.31 593.19 Q 205.77 594.08 216.77 595.73 Q 211.08 610.77 207.85 625.51 Q 204.62 640.25 204.62 655 L 204.62 703.08 L 64.62 703.08 Z M 308.01 703.08 Q 295.71 703.08 287.86 695.13 Q 280 687.17 280 675.38 L 280 658.08 Q 280 633.9 294.04 613.87 Q 308.08 593.85 335.46 579.23 Q 362.85 564.62 399.27 557.31 Q 435.69 550 479.69 550 Q 524.54 550 560.96 557.31 Q 597.38 564.62 624.77 579.23 Q 652.15 593.85 666.08 613.87 Q 680 633.9 680 658.08 L 680 675.38 Q 680 687.17 672.05 695.13 Q 664.1 703.08 652.31 703.08 L 308.01 703.08 Z M 755.38 703.08 L 755.38 655.11 Q 755.38 638.99 752.65 624.11 Q 749.92 609.23 743.46 595.8 Q 756 594.08 766.02 593.19 Q 776.04 592.31 784.62 592.31 Q 845.19 592.31 882.6 614.92 Q 920 637.54 920 673.15 L 920 678.46 Q 920 688.17 912.55 695.63 Q 905.1 703.08 895.38 703.08 L 755.38 703.08 Z M 310 672.31 L 650 672.31 L 650 660.92 Q 652.31 624.69 606.04 602.73 Q 559.77 580.77 480 580.77 Q 401 580.77 354.35 602.73 Q 307.69 624.69 310 661.15 L 310 672.31 Z M 175.52 559.23 Q 154.08 559.23 138.96 543.91 Q 123.85 528.59 123.85 506.92 Q 123.85 485.62 139.17 470.5 Q 154.49 455.38 176.15 455.38 Q 197.46 455.38 212.96 470.5 Q 228.46 485.62 228.46 507.59 Q 228.46 528.23 213.38 543.73 Q 198.31 559.23 175.52 559.23 Z M 784.18 559.23 Q 763.31 559.23 747.81 543.67 Q 732.31 528.11 732.31 507.15 Q 732.31 485.62 747.87 470.5 Q 763.43 455.38 784.76 455.38 Q 806.69 455.38 821.81 470.5 Q 836.92 485.62 836.92 507.36 Q 836.92 528.71 821.9 543.97 Q 806.88 559.23 784.18 559.23 Z M 480.27 520 Q 443.85 520 418.08 494.42 Q 392.31 468.85 392.31 432.31 Q 392.31 395.04 417.88 369.83 Q 443.46 344.61 480 344.61 Q 517.27 344.61 542.48 369.75 Q 567.69 394.88 567.69 432.04 Q 567.69 468.46 542.56 494.23 Q 517.43 520 480.27 520 Z M 480.74 489.23 Q 504.46 489.23 520.69 472.65 Q 536.92 456.07 536.92 431.57 Q 536.92 407.85 520.64 391.61 Q 504.36 375.38 480.35 375.38 Q 456.54 375.38 439.81 391.67 Q 423.08 407.95 423.08 431.96 Q 423.08 455.77 439.66 472.5 Q 456.24 489.23 480.74 489.23 Z M 480 672.31 Z M 480 432.31 Z", + "revanced_replace_flyout_menu_dismiss_queue" to "M 800.154 630.324 L 774.159 656.318 L 692.902 575.061 L 611.644 656.318 L 585.651 630.324 L 666.908 549.067 L 585.651 467.809 L 611.644 441.815 L 692.902 523.073 L 774.159 441.815 L 800.154 467.809 L 718.896 549.067 L 800.154 630.324 Z M 512.484 617.034 L 128.164 617.034 L 128.164 587.55 L 512.484 587.55 L 512.484 617.034 Z M 512.484 475.1 L 128.164 475.1 L 128.164 445.616 L 512.484 445.616 L 512.484 475.1 Z M 831.837 333.165 L 128.164 333.165 L 128.164 303.681 L 831.837 303.681 L 831.837 333.165 Z", + "revanced_replace_flyout_menu_report" to "revanced_hide_flyout_menu_report", + + // General + "revanced_disable_auto_captions" to "revanced_hide_flyout_menu_captions", + "revanced_hide_samples_shelf" to "revanced_hide_navigation_samples_button", + "revanced_hide_cast_button" to "M 480.23 480 Z M 840.23 255.38 L 840.23 704.62 Q 840.23 726.87 823.96 743.43 Q 807.69 760 784.85 760 L 594.62 760 Q 588.08 760 583.65 755.52 Q 579.23 751.03 579.23 744.4 Q 579.23 737.77 583.65 733.5 Q 588.08 729.23 594.62 729.23 L 784.85 729.23 Q 795.62 729.23 802.54 722.31 Q 809.46 715.38 809.46 704.62 L 809.46 255.38 Q 809.46 244.62 802.54 237.69 Q 795.62 230.77 784.85 230.77 L 175.62 230.77 Q 165.62 230.77 158.31 237.69 Q 151 244.62 151 255.38 L 151 285.38 Q 151 291.92 146.51 296.35 Q 142.03 300.77 135.4 300.77 Q 128.77 300.77 124.5 296.35 Q 120.23 291.92 120.23 285.38 L 120.23 255.38 Q 120.23 233.13 136.8 216.57 Q 153.37 200 175.62 200 L 784.85 200 Q 807.69 200 823.96 216.57 Q 840.23 233.13 840.23 255.38 Z M 307.62 760 Q 301.11 760 296.92 755.36 Q 292.74 750.73 292.08 743.77 Q 286.15 681.69 241.73 637.42 Q 197.31 593.15 135.46 586.46 Q 129 586.23 124.62 581.44 Q 120.23 576.65 120.23 570.52 Q 120.23 564 124.94 559.62 Q 129.66 555.23 136.23 555.46 Q 211.54 562.15 264.62 615.58 Q 317.69 669 323.08 744.31 Q 323.54 750.71 319.04 755.36 Q 314.54 760 307.62 760 Z M 458.76 760 Q 452.08 760 447.69 753.92 Q 443.31 747.85 443.08 740.46 Q 435.15 616.46 348.42 529.15 Q 261.69 441.85 137.92 434.69 Q 131.21 434.97 125.72 430.34 Q 120.23 425.72 120.23 419.55 Q 120.23 413 126.88 408.73 Q 133.54 404.46 141.38 404.69 Q 277.04 412.23 372.13 508.92 Q 467.23 605.62 474.62 741.62 Q 474.08 749.84 469.73 754.92 Q 465.39 760 458.76 760 Z M 146.12 760 Q 135.08 760 127.42 751.78 Q 119.77 743.55 119.77 732.89 Q 119.77 722.23 127.63 714.19 Q 135.49 706.15 146.88 706.15 Q 157.54 706.15 165.58 714.38 Q 173.62 722.6 173.62 733.26 Q 173.62 743.92 165.39 751.96 Q 157.17 760 146.12 760 Z", + "revanced_hide_category_bar" to "M 342.08 364.15 L 456.77 176.38 Q 461 170.15 467.19 167.04 Q 473.38 163.92 480.23 163.92 Q 487.08 163.92 493.65 167.04 Q 500.23 170.15 504.46 176.38 L 619.15 364.15 Q 623.38 371 623.27 378.81 Q 623.15 386.62 619.54 392.85 Q 615.92 399.08 610.02 402.69 Q 604.11 406.31 595.46 406.31 L 365 406.31 Q 356.61 406.31 350.32 402.47 Q 344.03 398.63 341.31 392.85 Q 338.08 386.86 337.96 379.12 Q 337.85 371.38 342.08 364.15 Z M 702.54 849.23 Q 640.46 849.23 598.54 806.92 Q 556.62 764.62 556.62 702.54 Q 556.62 640.46 598.54 598.54 Q 640.46 556.62 702.54 556.62 Q 764.62 556.62 806.92 598.54 Q 849.23 640.46 849.23 702.54 Q 849.23 764.62 806.92 806.92 Q 764.62 849.23 702.54 849.23 Z M 150.77 798.82 L 150.77 603.13 Q 150.77 592.1 158.74 583.86 Q 166.71 575.62 178.49 575.62 L 374.18 575.62 Q 385.96 575.62 393.83 583.96 Q 401.69 592.3 401.69 603.33 L 401.69 799.03 Q 401.69 810.81 393.72 818.67 Q 385.75 826.54 373.97 826.54 L 178.28 826.54 Q 166.5 826.54 158.63 818.57 Q 150.77 810.6 150.77 798.82 Z M 702.99 818.46 Q 750.92 818.46 784.69 784.63 Q 818.46 750.8 818.46 702.48 Q 818.46 654.15 784.63 620.77 Q 750.8 587.38 702.48 587.38 Q 654.15 587.38 620.77 620.91 Q 587.38 654.43 587.38 702.99 Q 587.38 750.92 620.91 784.69 Q 654.43 818.46 702.99 818.46 Z M 181.54 795.77 L 370.92 795.77 L 370.92 606.38 L 181.54 606.38 L 181.54 795.77 Z M 369.77 375.54 L 591.46 375.54 L 480.23 198.38 L 369.77 375.54 Z M 480.23 375.54 Z M 370.92 606.38 Z M 702.92 702.92 Z", + "revanced_hide_floating_button" to "M 480.13 840 Q 405.69 840 340.34 811.66 Q 274.99 783.32 225.86 734.24 Q 176.73 685.16 148.37 619.87 Q 120 554.58 120 480.13 Q 120 405.46 148.34 339.72 Q 176.68 273.99 225.76 225.36 Q 274.84 176.73 340.13 148.37 Q 405.42 120 479.87 120 Q 554.54 120 620.28 148.34 Q 686.01 176.68 734.64 225.26 Q 783.27 273.84 811.63 339.52 Q 840 405.19 840 479.87 Q 840 554.31 811.66 619.66 Q 783.32 685.01 734.74 734.14 Q 686.16 783.27 620.48 811.63 Q 554.81 840 480.13 840 Z M 480 809.23 Q 617.38 809.23 713.31 713.19 Q 809.23 617.15 809.23 480 Q 809.23 342.62 713.31 246.69 Q 617.38 150.77 480 150.77 Q 342.85 150.77 246.81 246.69 Q 150.77 342.62 150.77 480 Q 150.77 617.15 246.81 713.19 Q 342.85 809.23 480 809.23 Z M 480 480 Z", + "revanced_hide_tap_to_update_button" to "M 482.23 800 Q 415.69 800 357.38 774.58 Q 299.08 749.15 255.35 705.92 Q 211.62 662.69 186.19 604 Q 160.77 545.31 160.77 478.77 Q 160.77 413 186.19 354.92 Q 211.62 296.85 255.35 253.35 Q 299.08 209.85 357.38 184.92 Q 415.69 160 482.23 160 Q 549.92 160 611.42 188.46 Q 672.92 216.92 719.54 266.38 L 719.54 165 L 750.31 165 L 750.31 316.85 L 599 316.85 L 599 286.08 L 695.54 286.08 Q 653.08 242 598.19 216.38 Q 543.31 190.77 482.23 190.77 Q 361.08 190.77 276.31 273.96 Q 191.54 357.15 191.54 477.31 Q 191.54 598.92 276.08 684.08 Q 360.62 769.23 482.23 769.23 Q 596.46 769.23 677.85 691.62 Q 759.23 614 768.46 501.31 L 799.23 501.31 Q 791.54 628.23 700.23 714.12 Q 608.92 800 482.23 800 Z M 612.69 631.46 L 465.62 485.62 L 465.62 278.54 L 496.38 278.54 L 496.38 472.92 L 634.92 609.23 L 612.69 631.46 Z", + "revanced_hide_history_button" to "M 477 800 Q 350.308 800 259 714.116 Q 167.692 628.231 160 501.308 L 190.769 501.308 Q 200 614 281.385 691.615 Q 362.769 769.231 477 769.231 Q 598.615 769.231 683.154 684.077 Q 767.692 598.923 767.692 477.308 Q 767.692 357.154 682.923 273.961 Q 598.154 190.769 477 190.769 Q 415.923 190.769 361.038 216.385 Q 306.154 242 263.692 286.077 L 360.231 286.077 L 360.231 316.846 L 208.923 316.846 L 208.923 165 L 239.692 165 L 239.692 266.385 Q 286.307 216.923 347.808 188.461 Q 409.308 160 477 160 Q 543.539 160 601.846 184.923 Q 660.154 209.846 703.885 253.346 Q 747.616 296.846 773.039 354.923 Q 798.462 413 798.462 478.769 Q 798.462 545.308 773.039 604 Q 747.616 662.692 703.885 705.923 Q 660.154 749.154 601.846 774.577 Q 543.539 800 477 800 Z M 612.692 631.462 L 465.615 485.615 L 465.615 278.538 L 496.385 278.538 L 496.385 472.923 L 634.923 609.231 L 612.692 631.462 Z", + "revanced_hide_notification_button" to "settings_header_notifications", + "revanced_hide_sound_search_button" to "M 388.417 669.23 L 388.417 290.77 C 388.417 286.41 389.91 282.757 392.897 279.81 C 395.883 276.857 399.587 275.38 404.007 275.38 C 407.92 275.38 411.43 276.857 414.537 279.81 C 417.637 282.757 419.187 286.41 419.187 290.77 L 419.187 669.23 C 419.187 673.59 417.57 677.243 414.337 680.19 C 411.097 683.143 407.39 684.62 403.217 684.62 C 399.043 684.62 395.533 683.143 392.687 680.19 C 389.84 677.243 388.417 673.59 388.417 669.23 Z M 692.57 613.462 L 692.571 346.435 C 692.761 342.451 694.675 338.035 697.293 335.18 C 700.149 332.606 704.477 330.684 708.559 330.684 C 712.337 330.684 716.644 332.734 719.509 335.261 C 722.241 338.138 724.076 342.542 724.274 346.538 L 724.272 613.58 C 724.054 617.618 721.993 622.078 719.149 624.944 C 716.174 627.458 711.91 629.316 708.009 629.316 C 704.041 629.316 699.653 627.171 696.91 624.584 C 694.409 621.718 692.74 617.399 692.57 613.462 Z M 235.727 516.92 L 235.727 443.08 C 235.727 438.72 237.22 435.067 240.207 432.12 C 243.2 429.167 246.907 427.69 251.327 427.69 C 255.747 427.69 259.38 429.167 262.227 432.12 C 265.073 435.067 266.497 438.72 266.497 443.08 L 266.497 516.92 C 266.497 521.28 265 524.933 262.007 527.88 C 259.02 530.833 255.317 532.31 250.897 532.31 C 246.477 532.31 242.843 530.833 239.997 527.88 C 237.15 524.933 235.727 521.28 235.727 516.92 Z M 540.347 824.62 L 540.347 135.38 C 540.347 131.027 541.84 127.373 544.827 124.42 C 547.82 121.473 551.527 120 555.947 120 C 560.367 120 564 121.473 566.847 124.42 C 569.687 127.373 571.107 131.027 571.107 135.38 L 571.107 824.62 C 571.107 828.973 569.613 832.627 566.627 835.58 C 563.633 838.527 559.927 840 555.507 840 C 551.087 840 547.453 838.527 544.607 835.58 C 541.767 832.627 540.347 828.973 540.347 824.62 Z", + "revanced_hide_voice_search_button" to "M 480 509.31 Q 448.99 509.31 428.99 488.07 Q 409 466.83 409 435.62 L 409 190.77 Q 409 161.41 429.43 140.7 Q 449.87 120 479.94 120 Q 510.01 120 530.51 140.7 Q 551 161.41 551 190.77 L 551 435.62 Q 551 466.83 531.01 488.07 Q 511.01 509.31 480 509.31 Z M 480 315.15 Z M 464.62 804.62 L 464.62 673.23 Q 376.31 666.85 311.96 604.96 Q 247.62 543.08 241.23 452 Q 240 445.38 244.49 440.5 Q 248.98 435.62 255.61 435.62 Q 262.23 435.62 266.6 440.78 Q 270.97 445.94 271.23 452.77 Q 278.62 534.54 338.88 588.73 Q 399.14 642.92 479.65 642.92 Q 561.77 642.92 621.58 588.35 Q 681.38 533.77 688.77 452.77 Q 689.02 445.94 693.52 440.78 Q 698.02 435.62 704.74 435.62 Q 711.46 435.62 715.73 440.5 Q 720 445.38 718.77 452 Q 712.38 541.31 648.54 603.69 Q 584.69 666.08 495.38 673.23 L 495.38 804.62 Q 495.38 811.19 490.93 815.6 Q 486.47 820 479.81 820 Q 473.15 820 468.88 815.6 Q 464.62 811.19 464.62 804.62 Z M 480 478.54 Q 497.23 478.54 508.73 466.19 Q 520.23 453.85 520.23 435.3 L 520.23 190.77 Q 520.23 173.77 508.66 162.27 Q 497.1 150.77 480 150.77 Q 462.9 150.77 451.34 162.27 Q 439.77 173.77 439.77 190.77 L 439.77 435.62 Q 439.77 453.85 451.27 466.19 Q 462.77 478.54 480 478.54 Z", + + // Navigation bar + "revanced_hide_navigation_home_button" to "M 230.769 769.231 L 392.308 769.231 L 392.308 529.231 L 567.692 529.231 L 567.692 769.231 L 729.231 769.231 L 729.231 395.385 L 480 206.538 L 230.769 395.128 L 230.769 769.231 Z M 200 800 L 200 380 L 480 168.461 L 760 380 L 760 800 L 536.923 800 L 536.923 560 L 423.077 560 L 423.077 800 L 200 800 Z M 480 487.769 Z", + "revanced_hide_navigation_samples_button" to "M 183.618 784.954 L 183.618 175.047 L 673.235 480.001 L 183.618 784.954 Z M 217.502 236.037 L 217.502 723.964 L 608.861 480.001 L 217.502 236.037 Z M 420.802 217.401 L 420.802 257.385 L 778.278 480.001 L 420.802 702.616 L 420.802 742.6 L 842.657 480.001 L 420.802 217.401 Z", + "revanced_hide_navigation_explore_button" to "M 480 520 Q 463 520 451.5 508.5 Q 440 497 440 480 Q 440 463 451.5 451.5 Q 463 440 480 440 Q 497 440 508.5 451.5 Q 520 463 520 480 Q 520 497 508.5 508.5 Q 497 520 480 520 Z M 480.13 840 Q 405.69 840 340.34 811.66 Q 274.99 783.32 225.86 734.24 Q 176.73 685.16 148.37 619.87 Q 120 554.58 120 480.13 Q 120 405.46 148.34 339.72 Q 176.68 273.99 225.76 225.36 Q 274.84 176.73 340.13 148.37 Q 405.42 120 479.87 120 Q 554.54 120 620.28 148.34 Q 686.01 176.68 734.64 225.26 Q 783.27 273.84 811.63 339.52 Q 840 405.19 840 479.87 Q 840 554.31 811.66 619.66 Q 783.32 685.01 734.74 734.14 Q 686.16 783.27 620.48 811.63 Q 554.81 840 480.13 840 Z M 480 809.23 Q 617.38 809.23 713.31 713.19 Q 809.23 617.15 809.23 480 Q 809.23 342.62 713.31 246.69 Q 617.38 150.77 480 150.77 Q 342.85 150.77 246.81 246.69 Q 150.77 342.62 150.77 480 Q 150.77 617.15 246.81 713.19 Q 342.85 809.23 480 809.23 Z M 480 480 Z M 333.77 647 L 528.62 539.77 Q 532.08 538.54 534.81 535.81 Q 537.54 533.08 539.54 528.85 L 646.77 334 Q 652.54 324 644.19 316.12 Q 635.85 308.23 626 313.23 L 431.15 420.46 Q 426.92 422.46 424.19 425.19 Q 421.46 427.92 420.23 431.38 L 313 626.23 Q 307.23 637 315.12 644.88 Q 323 652.77 333.77 647 Z", + "revanced_hide_navigation_library_button" to "M 443.231 546.231 L 657.077 409.231 L 443.231 272.231 L 443.231 546.231 Z M 296.923 698.462 Q 273.865 698.462 257.702 682.298 Q 241.538 666.135 241.538 643.077 L 241.538 175.384 Q 241.538 152.327 257.702 136.163 Q 273.865 120 296.923 120 L 764.616 120 Q 787.673 120 803.837 136.163 Q 820 152.327 820 175.384 L 820 643.077 Q 820 666.135 803.837 682.298 Q 787.673 698.462 764.616 698.462 L 296.923 698.462 Z M 296.923 667.693 L 764.616 667.693 Q 773.846 667.693 781.539 660 Q 789.231 652.308 789.231 643.077 L 789.231 175.384 Q 789.231 166.154 781.539 158.461 Q 773.846 150.769 764.616 150.769 L 296.923 150.769 Q 287.692 150.769 280 158.461 Q 272.308 166.154 272.308 175.384 L 272.308 643.077 Q 272.308 652.308 280 660 Q 287.692 667.693 296.923 667.693 Z M 195.384 800 Q 172.327 800 156.163 783.837 Q 140 767.674 140 744.616 L 140 246.154 L 170.769 246.154 L 170.769 744.616 Q 170.769 753.847 178.461 761.539 Q 186.154 769.231 195.384 769.231 L 693.847 769.231 L 693.847 800 L 195.384 800 Z M 272.308 150.769 L 272.308 667.693 L 272.308 150.769 Z", + "revanced_hide_navigation_upgrade_button" to "M 480.13 840 C 430.503 840 383.907 830.553 340.34 811.66 C 296.773 792.767 258.613 766.96 225.86 734.24 C 193.107 701.52 167.277 663.397 148.37 619.87 C 129.457 576.343 120 529.763 120 480.13 C 120 430.35 129.447 383.547 148.34 339.72 C 167.233 295.9 193.04 257.78 225.76 225.36 C 258.48 192.94 296.603 167.277 340.13 148.37 C 383.657 129.457 430.237 120 479.87 120 C 529.65 120 576.453 129.447 620.28 148.34 C 664.1 167.233 702.22 192.873 734.64 225.26 C 767.06 257.647 792.723 295.733 811.63 339.52 C 830.543 383.3 840 430.083 840 479.87 C 840 529.497 830.553 576.093 811.66 619.66 C 792.767 663.227 767.127 701.387 734.74 734.14 C 702.353 766.893 664.267 792.723 620.48 811.63 C 576.7 830.543 529.917 840 480.13 840 Z M 480 809.23 C 571.587 809.23 649.357 777.217 713.31 713.19 C 777.257 649.163 809.23 571.433 809.23 480 C 809.23 388.413 777.257 310.643 713.31 246.69 C 649.357 182.743 571.587 150.77 480 150.77 C 388.567 150.77 310.837 182.743 246.81 246.69 C 182.783 310.643 150.77 388.413 150.77 480 C 150.77 571.433 182.783 649.163 246.81 713.19 C 310.837 777.217 388.567 809.23 480 809.23 Z M 480 480 Z M 424.094 607.092 C 426.644 606.802 429.081 605.814 431.994 603.759 L 603.18 493.856 C 608.821 490.61 610.708 486.835 610.708 480.093 C 610.708 473.363 609.083 469.661 603.358 466.262 L 432.308 356.465 C 429.074 354.183 426.322 353.027 423.415 352.875 C 420.557 352.694 418.272 353.255 415.125 355.23 C 411.887 356.868 409.377 358.833 407.887 361.176 C 406.528 363.56 405.941 366.351 406.133 369.962 L 406.143 589.671 C 405.951 593.279 406.426 596.242 407.743 598.645 C 409.158 600.953 411.291 602.824 414.605 604.477 C 417.669 606.381 420.529 607.317 423.43 607.134 L 424.094 607.092 Z M 480 706.26 C 543.296 706.26 596.269 684.61 640.342 640.268 C 684.628 596.14 706.26 543.188 706.26 480 C 706.26 416.704 684.635 363.73 640.35 319.658 C 596.276 275.37 543.296 253.74 480 253.74 C 416.812 253.74 363.863 275.364 319.739 319.654 C 275.397 363.725 253.74 416.704 253.74 480 C 253.74 543.186 275.395 596.135 319.737 640.261 C 363.862 684.606 416.814 706.26 480 706.26 Z M 480.091 735.78 C 444.985 735.78 411.595 728.964 380.727 715.627 C 349.935 702.238 322.628 683.764 299.427 660.641 C 276.277 637.463 257.794 610.178 244.39 579.415 C 231.038 548.574 224.22 515.203 224.22 480.091 C 224.22 444.88 231.033 411.348 244.369 380.302 C 257.763 349.326 276.244 322.037 299.376 299.062 C 322.548 276.153 349.827 257.79 380.586 244.39 C 411.425 231.036 444.797 224.22 479.909 224.22 C 515.12 224.22 548.651 231.033 579.698 244.369 C 610.669 257.758 637.951 276.119 660.923 299.01 C 683.839 321.958 702.212 349.222 715.612 380.167 C 728.963 411.185 735.78 444.692 735.78 479.909 C 735.78 515.015 728.976 548.408 715.628 579.27 C 702.249 610.064 683.9 637.365 661.005 660.558 C 638.063 683.722 610.783 702.209 579.833 715.613 C 548.816 728.962 515.309 735.78 480.091 735.78 Z", + + // Player + "revanced_enable_mini_player_next_button" to "M 697.222 722.968 L 697.221 236.93 C 696.994 231.868 698.381 228.676 701.97 225.477 C 705.157 221.957 708.323 220.936 713.62 220.936 C 718.889 220.936 722.074 222.072 725.034 225.545 C 728.438 228.728 729.6 231.893 729.4 237.032 L 729.401 723.07 C 729.628 728.132 728.241 731.324 724.652 734.523 C 721.465 738.043 718.299 739.064 713.002 739.064 C 707.733 739.064 704.547 737.928 701.588 734.453 C 698.183 731.272 697.022 728.107 697.222 722.968 Z M 262.765 671.477 L 547.06 479.638 L 262.765 286.615 L 262.765 671.477 Z M 230.588 669.466 L 230.588 289.723 C 230.36 278.965 233.427 271.537 240.608 265.334 C 247.589 258.658 254.82 256.018 264.343 256.018 C 267.999 256.018 271.174 256.427 274.584 257.401 C 277.809 258.237 280.86 259.881 284.273 262.489 L 561.394 450.918 C 566.825 454.511 570.287 458.262 572.688 463.127 C 575.28 467.896 576.3 472.869 576.3 479.448 C 576.3 486.019 575.227 491.153 572.621 496.071 C 570.222 501.038 566.789 504.757 561.369 508.34 L 284.028 696.927 C 280.628 699.526 277.775 701.009 274.535 701.851 C 271.128 702.828 268.001 703.222 264.343 703.222 C 254.822 703.222 247.505 700.505 240.523 693.827 C 233.341 687.628 230.363 680.228 230.588 669.466 Z", + "revanced_enable_mini_player_previous_button" to "M 262.778 722.968 L 262.779 236.93 C 263.006 231.868 261.619 228.676 258.03 225.477 C 254.843 221.957 251.677 220.936 246.38 220.936 C 241.111 220.936 237.926 222.072 234.966 225.545 C 231.562 228.728 230.4 231.893 230.6 237.032 L 230.599 723.07 C 230.372 728.132 231.759 731.324 235.348 734.523 C 238.535 738.043 241.701 739.064 246.998 739.064 C 252.267 739.064 255.453 737.928 258.412 734.453 C 261.817 731.272 262.978 728.107 262.778 722.968 Z M 729.412 669.466 L 729.412 289.723 C 729.64 278.965 726.573 271.537 719.392 265.334 C 712.411 258.658 705.18 256.018 695.657 256.018 C 692.001 256.018 688.826 256.427 685.416 257.401 C 682.191 258.237 679.14 259.881 675.727 262.489 L 398.606 450.918 C 393.175 454.511 389.713 458.262 387.312 463.127 C 384.72 467.896 383.7 472.869 383.7 479.448 C 383.7 486.019 384.773 491.153 387.379 496.071 C 389.778 501.038 393.211 504.757 398.631 508.34 L 675.972 696.927 C 679.372 699.526 682.225 701.009 685.465 701.851 C 688.872 702.828 691.999 703.222 695.657 703.222 C 705.178 703.222 712.495 700.505 719.477 693.827 C 726.659 687.628 729.637 680.228 729.412 669.466 Z M 697.235 671.477 L 412.94 479.638 L 697.235 286.615 L 697.235 671.477 Z", + "revanced_enable_swipe_to_dismiss_mini_player" to "M 196.923 590.231 L 80.923 474.231 L 98.923 456.231 L 179.846 536.923 Q 170.307 499.538 165.154 462.538 Q 160 425.538 160 387.384 Q 160 314.231 183.346 246.461 Q 206.692 178.692 249.846 120 L 268.846 139 Q 229.154 194 207.654 256.961 Q 186.154 319.923 186.154 387.384 Q 186.154 426.769 191.769 465.538 Q 197.385 504.307 208.461 542.461 L 294.923 456.231 L 312.923 474.231 L 196.923 590.231 Z M 646 792.462 Q 629.077 798.923 610.154 798.308 Q 591.231 797.693 573.538 789.231 L 318.077 671.385 L 325.077 653.846 Q 328.538 643.308 336.769 637.077 Q 345 630.846 356.538 629.615 L 484.769 616.231 L 367.769 297.769 Q 365.077 291.154 368.269 285.654 Q 371.461 280.154 378.077 278.231 Q 383.923 275.538 389.538 278.346 Q 395.154 281.154 397.846 287.769 L 528 644.077 L 364.616 659.154 L 586.462 762.154 Q 597.769 767.692 610.846 767.692 Q 623.923 767.692 636 763.923 L 774.231 712.846 Q 817.077 697.308 836.5 656.731 Q 855.923 616.154 840.385 573.308 L 782.231 414.308 Q 779.538 407.692 781.962 402.462 Q 784.385 397.231 791 394.538 Q 797.615 391.846 803.231 394.269 Q 808.846 396.692 811.539 403.308 L 868.693 562.308 Q 889.385 617.615 864.962 669.808 Q 840.539 722 785.231 741.923 L 646 792.462 Z M 579.154 535.769 L 521.923 380.385 Q 519.231 373.769 522.423 368.654 Q 525.615 363.538 532.231 360.846 Q 538.077 358.154 543.577 361.346 Q 549.077 364.538 551.769 370.385 L 608.231 525 L 579.154 535.769 Z M 687.923 495.077 L 645.461 378.462 Q 642.769 371.846 645.577 366.615 Q 648.385 361.385 655 358.692 Q 661.615 356.769 667.115 359.192 Q 672.615 361.615 674.539 368.231 L 718 485.846 L 687.923 495.077 Z M 677.769 612.923 Z", + "revanced_enable_zen_mode" to "M 267.15 772.31 Q 246.54 772.31 233.38 760.69 Q 220.23 749.08 220.23 729.23 Q 220.23 715.08 227.69 703.96 Q 235.15 692.85 248.31 688.15 L 423.38 618.62 L 423.38 446.77 Q 350.31 533.62 294.58 572.69 Q 238.85 611.77 162.31 622.62 Q 156.46 623.85 151.31 619.08 Q 146.15 614.31 146.15 608.23 Q 146.15 600.85 151.19 596.46 Q 156.23 592.08 162.85 590.85 Q 218.54 583.85 267.92 553.85 Q 317.31 523.85 359.62 473.62 L 416.08 410.92 Q 423 401.23 434.19 395.15 Q 445.38 389.08 458.31 389.08 L 501.69 389.08 Q 514.62 389.08 525.92 395.15 Q 537.23 401.23 544.92 410.92 L 600.62 473.62 Q 644.15 523.38 692.92 553.62 Q 741.69 583.85 797.15 590.85 Q 803.77 592.08 808.81 596.46 Q 813.85 600.85 813.85 608.23 Q 813.85 614.31 808.69 619.08 Q 803.54 623.85 797.69 622.62 Q 721.15 611.77 665.42 572.69 Q 609.69 533.62 536.62 446.77 L 536.62 618.62 L 711.69 688.15 Q 724.85 692.85 732.31 703.96 Q 739.77 715.08 739.77 729.23 Q 739.77 749.08 726.62 760.69 Q 713.46 772.31 692.85 772.31 L 398.31 772.31 L 398.31 770.54 Q 398.31 746.85 411.23 733.54 Q 424.15 720.23 447.85 720.23 L 583.15 720.23 Q 591.92 720.23 597.54 714.62 Q 603.15 709 603.15 700.23 Q 603.15 691.46 597.54 685.85 Q 591.92 680.23 583.15 680.23 L 447.85 680.23 Q 407.69 680.23 383.23 704.92 Q 358.77 729.62 358.77 770.54 L 358.77 772.31 L 267.15 772.31 Z M 480 324.46 Q 453.08 324.46 434.27 305.65 Q 415.46 286.85 415.46 259.92 Q 415.46 233 434.27 214.19 Q 453.08 195.38 480 195.38 Q 506.92 195.38 525.73 214.19 Q 544.54 233 544.54 259.92 Q 544.54 286.85 525.73 305.65 Q 506.92 324.46 480 324.46 Z", + "revanced_hide_audio_video_switch_toggle" to "M 280 680 Q 196.67 680 138.33 621.72 Q 80 563.44 80 480.18 Q 80 396.92 138.33 338.46 Q 196.67 280 280 280 L 680 280 Q 763.33 280 821.67 338.28 Q 880 396.56 880 479.82 Q 880 563.08 821.67 621.54 Q 763.33 680 680 680 L 280 680 Z M 280 649.23 L 680 649.23 Q 750.56 649.23 799.89 599.93 Q 849.23 550.63 849.23 480.12 Q 849.23 409.62 799.89 360.19 Q 750.56 310.77 680 310.77 L 280 310.77 Q 209.44 310.77 160.11 360.07 Q 110.77 409.37 110.77 479.88 Q 110.77 550.38 160.11 599.81 Q 209.44 649.23 280 649.23 Z M 279.71 571 Q 317.92 571 344.35 544.73 Q 370.77 518.45 370.77 480.15 Q 370.77 441.85 344.5 415.42 Q 318.22 389 280.01 389 Q 241.8 389 214.9 415.27 Q 188 441.55 188 479.85 Q 188 518.15 214.75 544.58 Q 241.5 571 279.71 571 Z M 480 480 Z", + "revanced_hide_comment_channel_guidelines" to "M 480.35 759.77 Q 363.92 759.77 282.08 678.5 Q 200.23 597.22 200.23 480.12 Q 200.23 362.83 281.95 281.3 Q 363.68 199.77 480.49 199.77 Q 597.31 199.77 678.77 281.18 Q 760.23 362.58 760.23 479.88 Q 760.23 596.98 678.89 678.37 Q 597.55 759.77 480.35 759.77 Z M 481.46 729 Q 584.38 729 656.69 656.44 Q 729 583.88 729 480.23 Q 729 460.91 725.77 442.07 Q 722.54 423.22 716.08 404.46 Q 691.08 408.69 674.38 410.58 Q 657.69 412.46 643.19 412.46 Q 576.15 412.46 514.27 379.62 Q 452.38 346.77 407 289.62 Q 382.54 352.46 337.12 399.5 Q 291.69 446.54 230.54 468.08 Q 229.77 577.54 303.29 653.27 Q 376.8 729 481.46 729 Z M 237.77 430.46 Q 282.15 409.77 327.81 363.88 Q 373.46 318 380.54 251.15 Q 323.62 274.92 286.15 322.35 Q 248.69 369.77 237.77 430.46 Z M 392.69 545.38 Q 376.6 545.38 365.73 534.51 Q 354.85 523.63 354.85 507.54 Q 354.85 491.46 365.73 479.96 Q 376.6 468.46 392.69 468.46 Q 408.77 468.46 420.27 479.96 Q 431.77 491.46 431.77 507.54 Q 431.77 523.63 420.27 534.51 Q 408.77 545.38 392.69 545.38 Z M 647.85 381.69 Q 663.92 381.69 678.27 379.92 Q 692.62 378.15 706.69 374.62 Q 676.77 307.31 614.08 268.92 Q 551.38 230.54 480 230.54 Q 462 230.54 447.31 232.19 Q 432.62 233.85 416.23 238.15 Q 451 312.08 518.81 346.88 Q 586.62 381.69 647.85 381.69 Z M 567.84 545.62 Q 551.76 545.62 540.76 534.67 Q 529.77 523.72 529.77 507.54 Q 529.77 491.36 540.71 479.8 Q 551.66 468.23 567.84 468.23 Q 583.92 468.23 595.42 479.8 Q 606.92 491.36 606.92 507.54 Q 606.92 523.72 595.42 534.67 Q 583.92 545.62 567.84 545.62 Z M 80 236.62 L 80 135.38 Q 80 112.33 96.16 96.16 Q 112.33 80 135.38 80 L 236.62 80 L 236.62 110.77 L 135.38 110.77 Q 126.15 110.77 118.46 118.46 Q 110.77 126.15 110.77 135.38 L 110.77 236.62 L 80 236.62 Z M 236.62 880 L 135.38 880 Q 112.33 880 96.16 863.84 Q 80 847.67 80 824.62 L 80 723.38 L 110.77 723.38 L 110.77 824.62 Q 110.77 833.85 118.46 841.54 Q 126.15 849.23 135.38 849.23 L 236.62 849.23 L 236.62 880 Z M 723.38 880 L 723.38 849.23 L 824.62 849.23 Q 833.85 849.23 841.54 841.54 Q 849.23 833.85 849.23 824.62 L 849.23 723.38 L 880 723.38 L 880 824.62 Q 880 847.67 863.84 863.84 Q 847.67 880 824.62 880 L 723.38 880 Z M 849.23 236.62 L 849.23 135.38 Q 849.23 126.15 841.54 118.46 Q 833.85 110.77 824.62 110.77 L 723.38 110.77 L 723.38 80 L 824.62 80 Q 847.67 80 863.84 96.16 Q 880 112.33 880 135.38 L 880 236.62 L 849.23 236.62 Z M 416.23 238.15 Z M 380.54 251.15 Z", + "revanced_hide_comment_timestamp_and_emoji_buttons" to "M 615.41 418.54 Q 632.35 418.54 644.63 406.07 Q 656.92 393.6 656.92 376.67 Q 656.92 359.73 644.45 347.44 Q 631.98 335.15 615.05 335.15 Q 598.12 335.15 585.83 347.62 Q 573.54 360.09 573.54 377.03 Q 573.54 393.96 586.01 406.25 Q 598.48 418.54 615.41 418.54 Z M 344.95 418.54 Q 361.88 418.54 374.17 406.07 Q 386.46 393.6 386.46 376.67 Q 386.46 359.73 373.99 347.44 Q 361.52 335.15 344.59 335.15 Q 327.65 335.15 315.37 347.62 Q 303.08 360.09 303.08 377.03 Q 303.08 393.96 315.55 406.25 Q 328.02 418.54 344.95 418.54 Z M 480.13 840 Q 405.46 840 339.72 811.66 Q 273.99 783.32 225.36 734.74 Q 176.73 686.16 148.37 620.48 Q 120 554.81 120 480.13 Q 120 405.46 148.34 339.72 Q 176.68 273.99 225.26 225.36 Q 273.84 176.73 339.52 148.37 Q 405.19 120 479.87 120 Q 554.54 120 620.28 148.34 Q 686.01 176.68 734.64 225.26 Q 783.27 273.84 811.63 339.52 Q 840 405.19 840 479.87 Q 840 554.54 811.66 620.28 Q 783.32 686.01 734.74 734.64 Q 686.16 783.27 620.48 811.63 Q 554.81 840 480.13 840 Z M 480 480 Z M 479.93 809.23 Q 617.76 809.23 713.5 713.57 Q 809.23 617.91 809.23 480.07 Q 809.23 342.24 713.57 246.5 Q 617.91 150.77 480.07 150.77 Q 342.24 150.77 246.5 246.43 Q 150.77 342.09 150.77 479.93 Q 150.77 617.76 246.43 713.5 Q 342.09 809.23 479.93 809.23 Z M 479.32 675.15 Q 528.71 675.15 570.49 653.12 Q 612.28 631.08 639.26 591.52 Q 645.38 582.08 639.79 572.62 Q 634.19 563.15 623.77 563.15 L 336.15 563.15 Q 325.31 563.15 319.96 572.62 Q 314.62 582.08 320.74 591.52 Q 347.63 631.08 389.21 653.12 Q 430.79 675.15 479.32 675.15 Z", + "revanced_hide_double_tap_overlay_filter" to "M 568.38 840 Q 549.58 840 532.21 832.77 Q 514.85 825.54 501.15 811.85 L 330.31 641.54 L 350.15 620.69 Q 358.54 612.31 369.64 610.23 Q 380.74 608.15 392.62 611.62 L 480 631.54 L 480 336.15 Q 480 329.58 484.38 325.17 Q 488.77 320.77 495.5 320.77 Q 502.23 320.77 506.5 325.17 Q 510.77 329.58 510.77 336.15 L 510.77 672.31 L 370.08 636.54 L 522.62 790.38 Q 531.92 799.92 543.5 804.58 Q 555.08 809.23 568.38 809.23 L 716.92 809.23 Q 760.31 809.23 790.92 778.61 Q 821.54 747.98 821.54 704.62 L 821.54 573.08 Q 821.54 566.5 825.92 562.1 Q 830.31 557.69 837.04 557.69 Q 843.77 557.69 848.04 562.1 Q 852.31 566.5 852.31 573.08 L 852.31 704.62 Q 852.31 761.46 813.04 800.73 Q 773.77 840 716.92 840 L 568.38 840 Z M 593.92 578.46 L 593.92 290 Q 593.92 283.42 598.31 279.02 Q 602.69 274.62 609.42 274.62 Q 616.15 274.62 620.42 279.02 Q 624.69 283.42 624.69 290 L 624.69 578.46 L 593.92 578.46 Z M 707.62 578.46 L 707.62 477.69 Q 707.62 471.12 712 466.71 Q 716.38 462.31 723.12 462.31 Q 729.85 462.31 734.12 466.71 Q 738.38 471.12 738.38 477.69 L 738.38 578.46 L 707.62 578.46 Z M 716.92 809.23 L 522.62 809.23 L 716.92 809.23 Z M 175.38 720 Q 152.33 720 136.16 703.84 Q 120 687.67 120 664.62 L 120 215.38 Q 120 192.33 136.16 176.16 Q 152.33 160 175.38 160 L 744.62 160 Q 767.67 160 783.84 176.16 Q 800 192.33 800 215.38 L 800 362.31 L 769.23 362.31 L 769.23 215.38 Q 769.23 206.15 761.54 198.46 Q 753.85 190.77 744.62 190.77 L 175.38 190.77 Q 166.15 190.77 158.46 198.46 Q 150.77 206.15 150.77 215.38 L 150.77 664.62 Q 150.77 673.85 158.46 681.54 Q 166.15 689.23 175.38 689.23 L 261.46 689.23 L 292 720 L 175.38 720 Z", + "revanced_hide_fullscreen_share_button" to "revanced_hide_action_button_share", + "revanced_remember_repeat_state" to "M 219.85 723.08 L 303.31 806.54 Q 307.69 811.06 307.96 817.22 Q 308.23 823.38 303.09 828.6 Q 297.82 833.81 292.03 833.87 Q 286.23 833.92 281.08 828.77 L 179.46 727.15 Q 175.23 722.92 173.23 718.07 Q 171.23 713.22 171.23 707.46 Q 171.23 701.69 173.23 697.08 Q 175.23 692.46 179.46 688.23 L 281.08 586.62 Q 285.63 582.23 291.89 581.96 Q 298.15 581.69 303.31 586.96 Q 308.35 592.22 308.41 597.96 Q 308.46 603.69 303.31 608.85 L 219.85 692.31 L 676.92 692.31 Q 687.69 692.31 694.62 685.38 Q 701.54 678.46 701.54 667.69 L 701.54 547.69 Q 701.54 541.12 706 536.71 Q 710.45 532.31 717.11 532.31 Q 723.77 532.31 728.04 536.71 Q 732.31 541.12 732.31 547.69 L 732.31 667.69 Q 732.31 690.13 715.84 706.61 Q 699.37 723.08 676.92 723.08 L 219.85 723.08 Z M 740.15 267.69 L 283.08 267.69 Q 272.31 267.69 265.38 274.62 Q 258.46 281.54 258.46 292.31 L 258.46 412.31 Q 258.46 418.88 254 423.29 Q 249.55 427.69 242.89 427.69 Q 236.23 427.69 231.96 423.29 Q 227.69 418.88 227.69 412.31 L 227.69 292.31 Q 227.69 269.87 244.16 253.39 Q 260.63 236.92 283.08 236.92 L 740.15 236.92 L 656.69 153.46 Q 652.31 148.94 652.04 142.78 Q 651.77 136.62 656.91 131.4 Q 662.18 126.19 667.97 126.13 Q 673.77 126.08 678.92 131.23 L 780.54 232.85 Q 784.77 237.08 787.15 241.93 Q 789.54 246.78 789.54 252.54 Q 789.54 258.31 787.15 262.92 Q 784.77 267.54 780.54 271.77 L 678.92 373.38 Q 674.37 377.77 668.11 378.04 Q 661.85 378.31 656.69 373.04 Q 651.65 367.78 651.59 362.04 Q 651.54 356.31 656.69 351.15 L 740.15 267.69 Z", + "revanced_remember_shuffle_state" to "revanced_hide_flyout_menu_shuffle_play", + + // Settings menu + "revanced_hide_settings_menu_parent_tools" to "pref_key_parent_tools", + "revanced_hide_settings_menu_general" to "settings_header_general", + "revanced_hide_settings_menu_playback" to "settings_header_playback", + "revanced_hide_settings_menu_data_saving" to "settings_header_data_saving", + "revanced_hide_settings_menu_downloads_and_storage" to "settings_header_downloads_and_storage", + "revanced_hide_settings_menu_notification" to "settings_header_notifications", + "revanced_hide_settings_menu_privacy_and_location" to "settings_header_privacy_and_location", + "revanced_hide_settings_menu_recommendations" to "settings_header_recommendations", + "revanced_hide_settings_menu_paid_memberships" to "settings_header_paid_memberships", + "revanced_hide_settings_menu_about" to "settings_header_about_youtube_music", + + // Video + "revanced_remember_playback_speed_last_selected" to "M 425.461 614.616 Q 443.077 632.616 475.038 630.346 Q 507 628.077 520.846 607.077 L 727.616 312.692 L 433.385 519.385 Q 411.846 534 409.846 565.308 Q 407.846 596.615 425.461 614.616 Z M 478.769 200.231 Q 533.462 200.231 582.385 214.884 Q 631.308 229.538 679.846 262.231 L 654 282.308 Q 613.615 256.154 568.269 243.577 Q 522.923 231 478.961 231 Q 342.121 231 246.445 327.639 Q 150.769 424.278 150.769 561.744 Q 150.769 605.154 162.5 648.462 Q 174.231 691.769 196.667 729.231 L 760.846 729.231 Q 783.615 692.462 795.462 647.923 Q 807.308 603.385 807.308 558.923 Q 807.308 520 795.962 473.423 Q 784.615 426.846 758 388.923 L 778.539 363.077 Q 811.923 416.769 824.385 462.884 Q 836.846 509 838.077 556.769 Q 838.539 609.077 826.846 654.385 Q 815.154 699.692 790.462 743.923 Q 784.616 753.846 777.654 756.923 Q 770.692 760 759.154 760 L 198.154 760 Q 189.511 760 181.14 754.962 Q 172.769 749.923 167.846 740.846 Q 148 705.154 134 661.423 Q 120 617.692 120 561.692 Q 120 487.923 147.978 422.218 Q 175.956 356.513 224.247 307.295 Q 272.538 258.077 338.295 229.154 Q 404.052 200.231 478.769 200.231 Z M 473.615 487.385 Z", + "revanced_remember_video_quality_last_selected" to "M 592.69 651.08 L 623.46 651.08 L 623.46 589.15 L 655.23 589.15 Q 671.76 589.15 683.57 577.41 Q 695.38 565.66 695.38 549.23 L 695.38 411 Q 695.38 394.47 683.57 382.66 Q 671.76 370.85 655.23 370.85 L 563.08 370.85 Q 546.77 370.85 533.38 382.66 Q 520 394.47 520 411 L 520 549.23 Q 520 565.66 533.38 577.41 Q 546.77 589.15 563.08 589.15 L 592.69 589.15 L 592.69 651.08 Z M 264.62 589.15 L 295.38 589.15 L 295.38 504.77 L 409.23 504.77 L 409.23 589.15 L 440 589.15 L 440 370.85 L 409.23 370.85 L 409.23 474 L 295.38 474 L 295.38 370.85 L 264.62 370.85 L 264.62 589.15 Z M 563.08 558.38 Q 558.46 558.38 554.62 554.54 Q 550.77 550.69 550.77 546.08 L 550.77 413.92 Q 550.77 409.31 554.62 405.46 Q 558.46 401.62 563.08 401.62 L 652.31 401.62 Q 656.92 401.62 660.77 405.46 Q 664.62 409.31 664.62 413.92 L 664.62 546.08 Q 664.62 550.69 660.77 554.54 Q 656.92 558.38 652.31 558.38 L 563.08 558.38 Z M 175.38 760 Q 152.33 760 136.16 743.84 Q 120 727.67 120 704.62 L 120 255.38 Q 120 232.33 136.16 216.16 Q 152.33 200 175.38 200 L 784.62 200 Q 807.67 200 823.84 216.16 Q 840 232.33 840 255.38 L 840 704.62 Q 840 727.67 823.84 743.84 Q 807.67 760 784.62 760 L 175.38 760 Z M 175.38 729.23 L 784.62 729.23 Q 793.85 729.23 801.54 721.54 Q 809.23 713.85 809.23 704.62 L 809.23 255.38 Q 809.23 246.15 801.54 238.46 Q 793.85 230.77 784.62 230.77 L 175.38 230.77 Q 166.15 230.77 158.46 238.46 Q 150.77 246.15 150.77 255.38 L 150.77 704.62 Q 150.77 713.85 158.46 721.54 Q 166.15 729.23 175.38 729.23 Z M 150.77 729.23 L 150.77 230.77 L 150.77 729.23 Z", + + // Misc + "revanced_change_share_sheet" to "revanced_hide_action_button_share", + "revanced_enable_opus_codec" to "M 498.445 225.191 C 409.356 225.838 332.555 239.944 268.01 267.467 C 188.575 301.394 135.618 351.697 109.18 418.362 C 101.242 438.803 96.979 458.062 96.352 476.146 C 88.714 607.252 172.458 681.097 224.169 713.924 C 244.921 727.904 263.372 734.832 263.372 734.832 L 287.071 658.597 L 294.079 639.858 C 325.275 645.791 359.951 649.928 398.303 652.167 C 515.733 659.061 614.148 645.566 693.583 611.631 C 773.017 577.737 825.537 527.842 851.135 461.903 C 876.732 396.008 864.533 342.108 814.505 300.154 L 814.473 300.178 C 775.008 267.112 714.252 244.985 632.37 233.678 L 572.196 426.243 C 570.897 433.312 568.657 441.327 565.19 450.572 C 559.043 466.019 551.647 478.747 542.959 488.769 C 534.258 498.807 524.523 506.634 513.737 512.347 C 502.941 518.017 491.197 521.948 478.518 524.105 C 465.825 526.254 452.715 526.918 439.159 526.125 C 425.585 525.33 414.037 523.471 404.537 520.539 C 395.028 517.61 386.282 512.762 378.304 505.965 C 372.411 499.296 368.985 490.546 368.048 479.692 C 367.107 468.851 369.725 455.441 375.884 439.461 C 381.399 425.573 388.375 413.463 396.83 403.174 C 405.276 392.881 415.157 384.897 426.462 379.237 C 437.255 373.565 448.857 369.761 461.278 367.847 C 473.665 365.944 487.441 365.446 502.556 366.332 C 515.082 367.074 526.741 368.933 537.562 371.95 C 542.691 373.364 547.191 375.238 551.102 377.488 L 597.276 229.622 C 585.876 228.54 574.214 227.586 562.088 226.888 C 540.223 225.599 519.004 225.042 498.445 225.191 Z M 518.855 249.594 C 532.487 249.804 546.427 250.324 560.678 251.16 L 560.698 251.16 C 562.213 251.25 563.612 251.406 565.109 251.5 L 535.4 346.648 C 525.29 344.382 514.888 342.713 503.978 342.062 L 503.954 342.062 C 487.461 341.097 472.038 341.603 457.634 343.816 L 457.626 343.816 C 442.789 346.102 428.615 350.728 415.512 357.582 C 401.195 364.78 388.585 375.037 378.204 387.688 C 367.985 400.129 359.768 414.509 353.446 430.424 L 353.407 430.541 L 353.357 430.663 C 346.385 448.758 342.547 465.348 343.979 481.818 C 345.259 496.642 350.489 511.07 360.254 522.123 L 361.391 523.411 L 362.7 524.521 C 372.993 533.291 384.783 539.883 397.476 543.79 C 409.48 547.489 422.794 549.525 437.751 550.399 C 452.997 551.29 467.958 550.539 482.525 548.071 L 482.54 548.071 C 497.555 545.518 511.798 540.792 524.906 533.909 L 524.946 533.885 L 524.987 533.872 C 538.522 526.703 550.686 516.838 561.168 504.753 C 572.054 492.194 580.69 477.027 587.621 459.617 L 587.714 459.387 L 587.799 459.155 C 591.346 449.699 593.758 441.116 595.389 433.03 L 649.024 261.425 C 717.399 273.095 767.93 292.827 799.018 318.869 L 799.418 319.208 C 820.988 337.418 832.841 356.322 837.627 377.455 C 842.446 398.732 840.147 423.42 828.635 453.052 L 828.635 453.061 C 805.611 512.368 759.271 557.188 684.154 589.241 L 684.147 589.249 C 609.196 621.269 514.533 634.631 399.718 627.888 L 399.71 627.888 C 362.232 625.704 328.564 621.677 298.567 615.969 L 278.621 612.173 L 264.224 650.675 L 248.83 700.171 C 244.984 697.977 241.9 696.607 237.614 693.718 L 237.338 693.538 L 237.059 693.363 C 189.221 662.996 113.458 597.771 120.463 477.563 L 120.487 477.28 L 120.495 476.99 C 121.009 462.165 124.543 445.616 131.66 427.278 C 155.541 367.135 202.398 321.901 277.438 289.85 C 333.686 265.863 400.767 252.328 478.881 249.884 C 491.901 249.475 505.222 249.379 518.855 249.594 Z", + "revanced_enable_debug_logging" to "M 243.08 447.46 L 243.08 413.51 Q 243.08 352.38 270.69 302.62 Q 298.31 252.85 344.62 220.23 L 281.15 156.77 L 311 126.15 L 382.81 197.85 Q 404.16 187.38 429.23 182.04 Q 454.31 176.69 479.84 176.69 Q 505.38 176.69 530.65 182.04 Q 555.92 187.38 577.31 197.85 L 649 126.15 L 678.85 156.77 L 615.38 220.23 Q 661.69 252.85 689.31 302.67 Q 716.92 352.5 716.92 413.55 L 716.92 447.46 L 243.08 447.46 Z M 581.54 379.77 Q 595.92 379.77 605.65 369.65 Q 615.38 359.54 615.38 345.92 Q 615.38 331.54 605.65 321.81 Q 595.92 312.08 581.54 312.08 Q 567.15 312.08 557.42 321.81 Q 547.69 331.54 547.69 345.92 Q 547.69 359.54 557.42 369.65 Q 567.15 379.77 581.54 379.77 Z M 378.46 379.77 Q 392.85 379.77 402.58 369.65 Q 412.31 359.54 412.31 345.92 Q 412.31 331.54 402.58 321.81 Q 392.85 312.08 378.46 312.08 Q 364.08 312.08 354.35 321.81 Q 344.62 331.54 344.62 345.92 Q 344.62 359.54 354.35 369.65 Q 364.08 379.77 378.46 379.77 Z M 480 853.85 Q 381 853.85 312.04 784.88 Q 243.08 715.92 243.08 616.92 L 243.08 481.31 L 716.92 481.31 L 716.92 617.1 Q 716.92 716.23 647.96 785.04 Q 579 853.85 480 853.85 Z", + "gms_core_settings" to "M 432.54 840 C 427.307 840 422.563 838.283 418.31 834.85 C 414.05 831.41 411.51 827.077 410.69 821.85 L 397.23 725.54 C 382.51 720.873 366.357 713.643 348.77 703.85 C 331.177 694.05 316.28 683.537 304.08 672.31 L 216.54 712.31 C 211.307 714.257 206.077 714.5 200.85 713.04 C 195.617 711.58 191.617 708.31 188.85 703.23 L 140.08 618.46 C 137.307 613.387 136.447 608.36 137.5 603.38 C 138.553 598.407 141.54 594.203 146.46 590.77 L 225.69 531.77 C 224.357 523.717 223.267 515.217 222.42 506.27 C 221.573 497.323 221.15 488.823 221.15 480.77 C 221.15 473.23 221.573 464.987 222.42 456.04 C 223.267 447.093 224.357 437.823 225.69 428.23 L 146.46 369.23 C 141.54 365.797 138.68 361.463 137.88 356.23 C 137.087 350.997 138.077 345.843 140.85 340.77 L 188.85 258.31 C 191.617 253.743 195.617 250.6 200.85 248.88 C 206.077 247.167 211.307 247.283 216.54 249.23 L 303.31 287.69 C 317.05 276.463 332.203 266.08 348.77 256.54 C 365.337 247 381.233 239.973 396.46 235.46 L 410.69 138.15 C 411.51 132.923 414.05 128.59 418.31 125.15 C 422.563 121.717 427.307 120 432.54 120 L 527.46 120 C 532.693 120 537.437 121.717 541.69 125.15 C 545.95 128.59 548.49 132.923 549.31 138.15 L 562.77 235.23 C 579.537 241.437 595.473 248.757 610.58 257.19 C 625.68 265.63 640.027 275.797 653.62 287.69 L 744.23 249.23 C 749.463 247.283 754.657 247.167 759.81 248.88 C 764.963 250.6 768.923 253.743 771.69 258.31 L 819.92 341.54 C 822.693 346.613 823.553 351.807 822.5 357.12 C 821.447 362.427 818.46 366.463 813.54 369.23 L 731.23 429.31 C 733.59 438.537 735.063 447.37 735.65 455.81 C 736.243 464.243 736.54 472.307 736.54 480 C 736.54 487.18 736.117 494.95 735.27 503.31 C 734.423 511.67 733.077 520.977 731.23 531.23 L 811.23 590.77 C 816.157 593.537 819.273 597.573 820.58 602.88 C 821.887 608.193 821.153 613.387 818.38 618.46 L 771.15 702.46 C 767.87 707.54 763.487 710.81 758 712.27 C 752.513 713.73 747.41 713.487 742.69 711.54 L 653.62 671.54 C 639.36 683.793 624.627 694.6 609.42 703.96 C 594.22 713.32 578.67 720.257 562.77 724.77 L 549.31 821.85 C 548.49 827.077 545.95 831.41 541.69 834.85 C 537.437 838.283 532.693 840 527.46 840 L 432.54 840 Z M 438.31 809.23 L 520.92 809.23 L 535.69 698 C 556.15 692.667 574.933 685.103 592.04 675.31 C 609.14 665.517 626.46 652.363 644 635.85 L 746.92 680.31 L 786.92 610.62 L 696 543.15 C 698.667 530.797 700.707 519.63 702.12 509.65 C 703.527 499.677 704.23 489.793 704.23 480 C 704.23 469.18 703.563 459.04 702.23 449.58 C 700.897 440.113 698.82 429.713 696 418.38 L 788.46 349.38 L 748.46 279.69 L 643.23 324.15 C 630.463 309.897 613.9 296.553 593.54 284.12 C 573.18 271.68 553.64 264.307 534.92 262 L 521.69 150.77 L 438.31 150.77 L 425.85 261.23 C 404.203 265.383 384.573 272.487 366.96 282.54 C 349.347 292.593 332.103 306.207 315.23 323.38 L 211.54 279.69 L 171.54 349.38 L 262.46 416.08 C 259.28 425.873 256.987 436.14 255.58 446.88 C 254.167 457.627 253.46 468.923 253.46 480.77 C 253.46 491.59 254.167 502.117 255.58 512.35 C 256.987 522.577 259.023 532.843 261.69 543.15 L 171.54 610.62 L 211.54 680.31 L 314.46 636.62 C 330.46 653.127 347.397 666.28 365.27 676.08 C 383.143 685.873 403.08 693.437 425.08 698.77 L 438.31 809.23 Z M 430.15 587.46 L 529.85 587.46 C 540.384 587.46 549.427 583.683 556.98 576.13 C 564.534 568.583 568.31 559.54 568.31 549 L 568.31 544.76 C 568.31 540.353 566.887 536.766 564.04 534 C 561.2 531.233 557.777 529.85 553.77 529.85 L 551.31 529.85 C 547.23 529.85 543.914 531.27 541.36 534.11 C 538.814 536.95 537.54 540.373 537.54 544.38 C 537.54 547.46 536.257 550.283 533.69 552.85 C 531.13 555.41 528.31 556.69 525.23 556.69 L 434.77 556.69 C 431.69 556.69 428.87 555.41 426.31 552.85 C 423.744 550.283 422.46 547.46 422.46 544.38 L 422.46 415.62 C 422.46 412.54 423.744 409.716 426.31 407.15 C 428.87 404.59 431.69 403.31 434.77 403.31 L 525.23 403.31 C 528.31 403.31 531.13 404.59 533.69 407.15 C 536.257 409.716 537.54 412.54 537.54 415.62 C 537.54 419.72 538.814 423.166 541.36 425.96 C 543.914 428.753 547.23 430.15 551.31 430.15 L 553.77 430.15 C 557.777 430.15 561.2 428.766 564.04 426 C 566.887 423.233 568.31 419.646 568.31 415.24 L 568.31 411 C 568.31 400.46 564.534 391.416 556.98 383.87 C 549.427 376.316 540.384 372.54 529.85 372.54 L 430.15 372.54 C 419.617 372.54 410.574 376.316 403.02 383.87 C 395.467 391.416 391.69 400.46 391.69 411 L 391.69 549 C 391.69 559.54 395.467 568.583 403.02 576.13 C 410.574 583.683 419.617 587.46 430.15 587.46 Z", + "revanced_sanitize_sharing_links" to "M 264.874 586.16 C 219.754 586.16 181.294 570.263 149.494 538.47 C 117.694 506.677 101.794 468.227 101.794 423.12 C 101.794 378.013 117.694 339.55 149.494 307.73 C 181.294 275.91 219.754 260 264.874 260 L 395.644 260 C 399.998 260 403.651 261.497 406.604 264.49 C 409.551 267.477 411.024 271.18 411.024 275.6 C 411.024 280.02 409.551 283.653 406.604 286.5 C 403.651 289.347 399.998 290.77 395.644 290.77 L 264.784 290.77 C 228.004 290.77 196.771 303.59 171.084 329.23 C 145.404 354.87 132.564 386.073 132.564 422.84 C 132.564 459.613 145.404 490.9 171.084 516.7 C 196.771 542.493 228.004 555.39 264.784 555.39 L 395.644 555.39 C 399.998 555.39 403.651 556.883 406.604 559.87 C 409.551 562.863 411.024 566.57 411.024 570.99 C 411.024 575.41 409.551 579.043 406.604 581.89 C 403.651 584.737 399.998 586.16 395.644 586.16 L 264.874 586.16 Z M 774.144 538.511 C 771.086 540.683 766.86 540.783 764.544 540.57 C 760.366 540.185 758.078 538.357 754.98 535.666 C 751.882 532.975 751.63 531.654 750.649 528.904 C 750.182 527.596 748.871 524.358 751.415 519.354 C 751.415 519.354 791.024 460.087 791.024 423.32 C 791.024 386.547 778.184 355.26 752.504 329.46 C 726.817 303.667 695.584 290.77 658.804 290.77 L 527.944 290.77 C 523.591 290.77 519.937 289.277 516.984 286.29 C 514.037 283.297 512.564 279.59 512.564 275.17 C 512.564 270.75 514.037 267.117 516.984 264.27 C 519.937 261.423 523.591 260 527.944 260 L 658.714 260 C 703.834 260 742.294 275.897 774.094 307.69 C 805.894 339.483 821.794 377.933 821.794 423.04 C 821.794 468.147 775.114 537.606 774.094 538.43 C 773.074 539.254 774.144 538.511 774.144 538.511 Z M 339.874 438.46 C 335.514 438.46 331.861 436.967 328.914 433.98 C 325.961 430.987 324.484 427.28 324.484 422.86 C 324.484 418.44 325.961 414.807 328.914 411.96 C 331.861 409.12 335.514 407.7 339.874 407.7 L 584.484 407.7 C 588.344 407.7 591.874 409.193 595.074 412.18 C 598.274 415.173 599.874 418.88 599.874 423.3 C 599.874 427.72 598.274 431.353 595.074 434.2 C 591.874 437.04 588.344 438.46 584.484 438.46 L 339.874 438.46 Z M 646.735 588.639 L 693.414 588.639 L 693.414 455.293 C 693.625 448.262 691.73 443.428 686.819 438.764 C 682.285 433.815 677.325 431.938 670.075 431.938 C 662.807 431.938 658.004 433.624 653.477 438.498 C 648.582 443.064 646.523 448.007 646.735 455.196 L 646.735 588.639 Z M 535.979 661.988 L 804.17 661.988 L 804.17 621.073 C 804.243 619.885 804.252 619.077 804.117 618.501 C 803.933 617.98 803.629 617.584 802.916 617.122 L 802.882 617.102 L 802.939 617.136 L 802.837 617.054 L 802.682 616.929 L 802.549 616.765 C 802.173 616.141 801.725 615.567 801.043 615.343 C 800.452 615.148 799.673 615.201 798.63 615.302 L 541.758 615.309 C 540.691 615.221 539.923 615.135 539.324 615.274 C 538.608 615.44 538.111 616 537.717 616.642 L 537.652 616.721 L 537.57 616.815 L 537.447 616.924 C 536.071 617.969 535.788 618.595 535.971 620.804 L 535.979 661.988 Z M 508.72 774.053 L 508.78 774.118 L 508.859 774.245 C 509.474 775.283 509.976 775.881 510.571 776.247 C 511.209 776.521 511.95 776.583 513.159 776.417 L 513.22 776.409 L 513.152 776.418 L 565.057 776.41 L 565.057 732.567 C 565.284 729.457 566.793 725.542 568.823 723.197 L 568.859 723.155 L 568.892 723.117 L 568.98 723.039 C 571.239 721.103 575.188 719.199 578.494 719.199 C 581.796 719.199 585.587 720.982 587.831 722.966 L 587.879 723.011 L 587.933 723.06 L 587.966 723.091 L 588.065 723.203 C 589.949 725.489 591.494 729.213 591.72 732.322 L 591.726 776.41 L 656.742 776.41 L 656.742 732.568 C 656.974 729.451 658.491 725.532 660.502 723.204 L 660.527 723.174 L 660.572 723.122 L 660.672 723.035 C 662.928 721.096 666.87 719.199 670.179 719.199 C 673.482 719.199 677.271 720.98 679.514 722.964 L 679.567 723.014 L 679.616 723.059 L 679.65 723.09 L 679.746 723.198 C 681.629 725.486 683.175 729.217 683.401 732.322 L 683.407 776.41 L 748.423 776.41 L 748.423 732.568 C 748.656 729.451 750.165 725.553 752.17 723.221 L 752.225 723.161 L 752.262 723.12 L 752.344 723.048 C 754.601 721.106 758.552 719.199 761.86 719.199 C 765.214 719.199 768.948 721.002 771.135 722.905 L 771.253 723.013 L 771.299 723.055 L 771.418 723.196 C 773.249 725.43 774.834 729.143 775.085 732.292 L 775.093 776.41 L 826.747 776.41 L 826.743 776.41 C 828.006 776.549 828.813 776.524 829.478 776.288 C 830.101 775.964 830.588 775.454 831.148 774.44 L 831.197 774.357 L 831.239 774.29 L 831.34 774.176 C 832.184 773.282 832.647 772.626 832.853 771.97 C 832.961 771.295 832.846 770.602 832.392 769.542 L 832.395 769.548 L 807.926 684.253 L 532.224 684.253 L 507.807 769.382 L 507.811 769.372 C 507.368 770.472 507.205 771.203 507.272 771.877 C 507.439 772.532 507.826 773.138 508.662 773.991 L 508.72 774.053 Z M 823.872 803.08 L 516.178 803.08 C 505.681 802.767 495.733 797.822 488.81 789.629 C 482.33 780.953 480.227 769.972 482.969 759.447 L 509.309 668.961 L 509.309 623.173 C 509.536 614.027 513.263 605.083 519.403 598.606 C 525.882 592.467 534.786 588.865 543.927 588.639 L 620.065 588.639 L 620.065 455.228 C 620.295 441.882 625.518 429.339 634.704 419.822 C 644.207 410.638 656.692 405.268 670.075 405.268 C 683.46 405.268 696.029 410.726 705.528 419.906 C 714.719 429.427 719.854 441.944 720.084 455.286 L 720.084 588.639 L 796.305 588.639 C 805.448 588.867 814.4 592.6 820.877 598.737 C 827.014 605.213 830.615 614.113 830.84 623.256 L 830.84 668.97 L 857.238 760.388 C 859.706 770.645 857.386 781.446 851.039 789.956 C 844.225 798.038 834.36 802.775 823.872 803.08 Z", + "revanced_extended_settings_import_export" to "M 300.38 743.08 L 258.69 701.38 Q 208.69 649.85 185.27 594.88 Q 161.85 539.92 161.85 484.77 Q 161.85 397.69 206.77 324.96 Q 251.69 252.23 327.31 211.77 Q 333.69 208.85 340.19 209.5 Q 346.69 210.15 349.38 216.54 Q 352.08 222.15 349.5 228.15 Q 346.92 234.15 341.31 237.08 Q 272.46 272.85 232.54 339.27 Q 192.62 405.69 192.62 484.77 Q 192.62 536.77 211.65 584.42 Q 230.69 632.08 272.69 672.38 L 322.69 720.92 L 322.69 606.23 Q 322.69 599.38 326.96 595.12 Q 331.23 590.85 338.08 590.85 Q 344.15 590.85 348.81 595.12 Q 353.46 599.38 353.46 606.23 L 353.46 746.15 Q 353.46 758.38 345.35 766.12 Q 337.23 773.85 325.77 773.85 L 185.85 773.85 Q 179 773.85 174.73 769.58 Q 170.46 765.31 170.46 758.46 Q 170.46 751.62 174.73 747.35 Q 179 743.08 185.85 743.08 L 300.38 743.08 Z M 638.08 239.08 L 638.08 353.77 Q 638.08 360.62 633.42 364.88 Q 628.77 369.15 622.69 369.15 Q 615.85 369.15 611.58 364.88 Q 607.31 360.62 607.31 353.77 L 607.31 213.85 Q 607.31 201.62 615.04 193.88 Q 622.77 186.15 635 186.15 L 774.15 186.15 Q 781 186.15 785.27 190.42 Q 789.54 194.69 789.54 201.54 Q 789.54 208.38 785.27 212.65 Q 781 216.92 774.15 216.92 L 659.38 216.92 L 701.31 258.62 Q 743.92 300.54 766.08 346.69 Q 788.23 392.85 793.31 439.08 L 763.31 439.08 Q 757.46 396.54 739.08 358.42 Q 720.69 320.31 688.08 287.62 L 638.08 239.08 Z M 700.77 832.31 Q 695.54 832.31 691.54 828.81 Q 687.54 825.31 686.54 819.31 L 686.38 803.54 Q 660.23 798.31 640.38 786.42 Q 620.54 774.54 606.77 758.23 L 592.92 766.85 Q 587.92 769.62 582.42 768.62 Q 576.92 767.62 574.69 763.39 L 569.85 756.77 Q 566.08 751.77 567.08 746.38 Q 568.08 741 572.31 737.77 L 586.23 727.31 Q 576.62 701.23 576.62 678.5 Q 576.62 655.77 586.23 629.69 L 572.31 619.23 Q 568.08 616 567.08 610.62 Q 566.08 605.23 569.85 600.23 L 574.69 592.62 Q 576.92 588.38 582.42 587.77 Q 587.92 587.15 592.92 589.92 L 606.77 598.54 Q 620.54 582.69 640.38 570.69 Q 660.23 558.69 686.38 553.46 L 686.54 537.46 Q 687.54 531.46 691.54 527.96 Q 695.54 524.46 700.77 524.46 L 705.31 524.46 Q 710.54 524.46 714.54 527.96 Q 718.54 531.46 719.54 537.46 L 719.69 553.46 Q 745.85 558.69 765.69 570.69 Q 785.54 582.69 799.31 597.77 L 813.92 589.92 Q 818.92 587.15 823.65 587.77 Q 828.38 588.38 831.38 592.62 L 836.23 600.23 Q 840 605.23 839 610.23 Q 838 615.23 833 619.23 L 819.85 629.69 Q 829.46 655.77 829.46 678.12 Q 829.46 700.46 819.85 727.31 L 833.77 737.77 Q 838 741 839 746.38 Q 840 751.77 836.23 756.77 L 831.38 763.39 Q 828.38 767.62 823.27 768.62 Q 818.15 769.62 813.15 766.85 L 799.31 758.23 Q 785.54 774.54 765.69 786.42 Q 745.85 798.31 719.69 803.54 L 719.54 819.31 Q 718.54 825.31 714.54 828.81 Q 710.54 832.31 705.31 832.31 L 700.77 832.31 Z M 702.92 774.23 Q 743.08 774.23 770.92 746.38 Q 798.77 718.54 798.77 678.38 Q 798.77 638.23 770.92 610 Q 743.08 581.77 702.92 581.77 Q 662 581.77 634.15 610 Q 606.31 638.23 606.31 678.38 Q 606.31 718.54 634.15 746.38 Q 662 774.23 702.92 774.23 Z", +) + +private val intentKey = setOf( + "revanced_extended_settings", +) + +val intentIcon = intentKey.associateWith { "${it}_key_icon" } + +private val emptyTitles = setOf( + "revanced_enable_debug_buffer_logging", + "revanced_hide_fullscreen_ads_type", + "revanced_remember_playback_speed_last_selected_toast", + "revanced_remember_video_quality_last_selected_toast", + "revanced_replace_flyout_menu_dismiss_queue_continue_watch", + "revanced_replace_flyout_menu_report_only_player", + "revanced_enable_zen_mode_podcast", + "revanced_gms_show_dialog", +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt new file mode 100644 index 000000000..4e0e2b1bd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt @@ -0,0 +1,107 @@ +package app.revanced.patches.music.misc.backgroundplayback + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.patch.PatchList.REMOVE_BACKGROUND_PLAYBACK_RESTRICTIONS +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.resolvable +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstStringInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +@Suppress("unused") +val backgroundPlaybackPatch = bytecodePatch( + REMOVE_BACKGROUND_PLAYBACK_RESTRICTIONS.title, + REMOVE_BACKGROUND_PLAYBACK_RESTRICTIONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + // region patch for background play + + backgroundPlaybackManagerFingerprint.methodOrThrow().addInstructions( + 0, """ + const/4 v0, 0x1 + return v0 + """ + ) + + // endregion + + // region patch for exclusive audio playback + + // don't play music video + musicBrowserServiceFingerprint.matchOrThrow().let { + it.method.apply { + val stringIndex = it.stringMatches!!.first().index + val targetIndex = indexOfFirstInstructionOrThrow(stringIndex) { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.returnType == "Z" && + reference.parameterTypes.size == 0 + } + + getWalkerMethod(targetIndex).addInstructions( + 0, """ + const/4 v0, 0x1 + return v0 + """ + ) + } + } + + // don't play podcast videos + // enable by default from YouTube Music 7.05.52+ + + if (podCastConfigFingerprint.resolvable() && + dataSavingSettingsFragmentFingerprint.resolvable() + ) { + podCastConfigFingerprint.methodOrThrow().apply { + val insertIndex = implementation!!.instructions.size - 1 + val targetRegister = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex, + "const/4 v$targetRegister, 0x1" + ) + } + + dataSavingSettingsFragmentFingerprint.methodOrThrow().apply { + val insertIndex = + indexOfFirstStringInstructionOrThrow("pref_key_dont_play_nma_video") + 4 + val targetRegister = getInstruction(insertIndex).registerD + + addInstruction( + insertIndex, + "const/4 v$targetRegister, 0x1" + ) + } + } + + // endregion + + // region patch for minimized playback + + kidsBackgroundPlaybackPolicyControllerFingerprint.methodOrThrow().addInstruction( + 0, "return-void" + ) + + // endregion + + updatePatchStatus(REMOVE_BACKGROUND_PLAYBACK_RESTRICTIONS) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/Fingerprints.kt new file mode 100644 index 000000000..35524cdb8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/Fingerprints.kt @@ -0,0 +1,65 @@ +package app.revanced.patches.music.misc.backgroundplayback + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val backgroundPlaybackManagerFingerprint = legacyFingerprint( + name = "backgroundPlaybackManagerFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("L"), + literals = listOf(64657230L), +) + +internal val dataSavingSettingsFragmentFingerprint = legacyFingerprint( + name = "dataSavingSettingsFragmentFingerprint", + returnType = "V", + parameters = listOf("Landroid/os/Bundle;", "Ljava/lang/String;"), + strings = listOf("pref_key_dont_play_nma_video"), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/DataSavingSettingsFragment;") && + method.name == "onCreatePreferences" + } +) + +internal val kidsBackgroundPlaybackPolicyControllerFingerprint = legacyFingerprint( + name = "kidsBackgroundPlaybackPolicyControllerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("I", "L", "Z"), + opcodes = listOf( + Opcode.IGET, + Opcode.IF_NE, + Opcode.IGET_OBJECT, + Opcode.IF_NE, + Opcode.IGET_BOOLEAN, + Opcode.IF_EQ, + Opcode.GOTO, + Opcode.RETURN_VOID, + Opcode.SGET_OBJECT, + Opcode.CONST_4, + Opcode.IF_NE, + Opcode.IPUT_BOOLEAN + ) +) + +internal val musicBrowserServiceFingerprint = legacyFingerprint( + name = "musicBrowserServiceFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Ljava/lang/String;", "Landroid/os/Bundle;"), + strings = listOf("android.service.media.extra.RECENT"), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/MusicBrowserService;") + }, +) + +internal val podCastConfigFingerprint = legacyFingerprint( + name = "podCastConfigFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(45388403L), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/bitrate/BitrateDefaultValuePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/bitrate/BitrateDefaultValuePatch.kt new file mode 100644 index 000000000..c0311d746 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/bitrate/BitrateDefaultValuePatch.kt @@ -0,0 +1,41 @@ +package app.revanced.patches.music.misc.bitrate + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.patch.PatchList.BITRATE_DEFAULT_VALUE +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.settingsPatch + +@Suppress("unused") +val bitrateDefaultValuePatch = resourcePatch( + BITRATE_DEFAULT_VALUE.title, + BITRATE_DEFAULT_VALUE.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + document("res/xml/data_saving_settings.xml").use { document -> + document.getElementsByTagName("com.google.android.apps.youtube.music.ui.preference.PreferenceCategoryCompat") + .item(0).childNodes.apply { + arrayOf("BitrateAudioMobile", "BitrateAudioWiFi").forEach { + for (i in 1 until length) { + val view = item(i) + if ( + view.hasAttributes() && + view.attributes.getNamedItem("android:key").nodeValue.endsWith(it) + ) { + view.attributes.getNamedItem("android:defaultValue").nodeValue = + "Always High" + break + } + } + } + } + } + + updatePatchStatus(BITRATE_DEFAULT_VALUE) + + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/codecs/OpusCodecPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/codecs/OpusCodecPatch.kt new file mode 100644 index 000000000..ee0a01215 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/codecs/OpusCodecPatch.kt @@ -0,0 +1,37 @@ +package app.revanced.patches.music.misc.codecs + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.MISC_PATH +import app.revanced.patches.music.utils.patch.PatchList.ENABLE_OPUS_CODEC +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.opus.baseOpusCodecsPatch + +@Suppress("unused") +val opusCodecPatch = resourcePatch( + ENABLE_OPUS_CODEC.title, + ENABLE_OPUS_CODEC.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseOpusCodecsPatch( + "$MISC_PATH/OpusCodecPatch;->enableOpusCodec()Z" + ), + settingsPatch + ) + + execute { + addSwitchPreference( + CategoryType.MISC, + "revanced_enable_opus_codec", + "false" + ) + + updatePatchStatus(ENABLE_OPUS_CODEC) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/DebuggingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/DebuggingPatch.kt new file mode 100644 index 000000000..9c51bae09 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/debugging/DebuggingPatch.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.music.misc.debugging + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.patch.PatchList.ENABLE_DEBUG_LOGGING +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch + +@Suppress("unused") +val debuggingPatch = resourcePatch( + ENABLE_DEBUG_LOGGING.title, + ENABLE_DEBUG_LOGGING.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + addSwitchPreference( + CategoryType.MISC, + "revanced_enable_debug_logging", + "false" + ) + addSwitchPreference( + CategoryType.MISC, + "revanced_enable_debug_buffer_logging", + "false", + "revanced_enable_debug_logging" + ) + + updatePatchStatus(ENABLE_DEBUG_LOGGING) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/drc/DrcAudioPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/drc/DrcAudioPatch.kt new file mode 100644 index 000000000..af9a47dd0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/drc/DrcAudioPatch.kt @@ -0,0 +1,66 @@ +package app.revanced.patches.music.misc.drc + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.MISC_PATH +import app.revanced.patches.music.utils.patch.PatchList.DISABLE_DRC_AUDIO +import app.revanced.patches.music.utils.playservice.is_7_13_or_greater +import app.revanced.patches.music.utils.playservice.versionCheckPatch +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.formatStreamModelConstructorFingerprint +import app.revanced.util.fingerprint.matchOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$MISC_PATH/DrcAudioPatch;" + +@Suppress("unused") +val DrcAudioPatch = bytecodePatch( + DISABLE_DRC_AUDIO.title, + DISABLE_DRC_AUDIO.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + versionCheckPatch, + ) + + execute { + val fingerprint = if (is_7_13_or_greater) { + compressionRatioFingerprint + } else { + compressionRatioLegacyFingerprint + } + + fingerprint.matchOrThrow(formatStreamModelConstructorFingerprint).let { + it.method.apply { + val insertIndex = it.patternMatch!!.endIndex + val insertRegister = + getInstruction(insertIndex - 1).registerA + + addInstructions( + insertIndex, + """ + invoke-static {v$insertRegister}, $EXTENSION_CLASS_DESCRIPTOR->disableDrcAudio(F)F + move-result v$insertRegister + """ + ) + } + } + + addSwitchPreference( + CategoryType.MISC, + "revanced_disable_drc_audio", + "false" + ) + + updatePatchStatus(DISABLE_DRC_AUDIO) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/drc/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/drc/Fingerprints.kt new file mode 100644 index 000000000..15842faf9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/drc/Fingerprints.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.music.misc.drc + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +/** + * YouTube Music 7.13.52 ~ + */ +internal val compressionRatioFingerprint = legacyFingerprint( + name = "compressionRatioFingerprint", + returnType = "Lj${'$'}/util/Optional;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.IF_EQZ, + Opcode.IGET, + Opcode.NEG_FLOAT, + ) +) + +/** + * ~ YouTube Music 7.12.52 + */ +internal val compressionRatioLegacyFingerprint = legacyFingerprint( + name = "compressionRatioLegacyFingerprint", + returnType = "F", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.IGET, + Opcode.RETURN, + ) +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/share/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/share/Fingerprints.kt new file mode 100644 index 000000000..6924eadba --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/share/Fingerprints.kt @@ -0,0 +1,14 @@ +package app.revanced.patches.music.misc.share + +import app.revanced.patches.music.utils.resourceid.bottomSheetRecyclerView +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val bottomSheetRecyclerViewFingerprint = legacyFingerprint( + name = "bottomSheetRecyclerViewFingerprint", + returnType = "Lj${'$'}/util/Optional;", + accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(bottomSheetRecyclerView), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/share/ShareSheetPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/share/ShareSheetPatch.kt new file mode 100644 index 000000000..85904ec56 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/share/ShareSheetPatch.kt @@ -0,0 +1,66 @@ +package app.revanced.patches.music.misc.share + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.music.utils.extension.Constants.MISC_PATH +import app.revanced.patches.music.utils.patch.PatchList.CHANGE_SHARE_SHEET +import app.revanced.patches.music.utils.resourceid.bottomSheetRecyclerView +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$MISC_PATH/ShareSheetPatch;" + +private const val FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/ShareSheetMenuFilter;" + +@Suppress("unused") +val shareSheetPatch = bytecodePatch( + CHANGE_SHARE_SHEET.title, + CHANGE_SHARE_SHEET.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + lithoFilterPatch, + sharedResourceIdPatch + ) + + execute { + bottomSheetRecyclerViewFingerprint.methodOrThrow().apply { + val constIndex = indexOfFirstLiteralInstructionOrThrow(bottomSheetRecyclerView) + val targetIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.CHECK_CAST) + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $EXTENSION_CLASS_DESCRIPTOR->onShareSheetMenuCreate(Landroid/support/v7/widget/RecyclerView;)V" + ) + } + + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + + addSwitchPreference( + CategoryType.MISC, + "revanced_change_share_sheet", + "false" + ) + + updatePatchStatus(CHANGE_SHARE_SHEET) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt new file mode 100644 index 000000000..ed45770dc --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt @@ -0,0 +1,103 @@ +package app.revanced.patches.music.misc.splash + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.music.utils.compatibility.Constants.YOUTUBE_MUSIC_PACKAGE_NAME +import app.revanced.patches.music.utils.extension.Constants.MISC_PATH +import app.revanced.patches.music.utils.patch.PatchList.DISABLE_CAIRO_SPLASH_ANIMATION +import app.revanced.patches.music.utils.playservice.is_7_06_or_greater +import app.revanced.patches.music.utils.playservice.is_7_20_or_greater +import app.revanced.patches.music.utils.playservice.versionCheckPatch +import app.revanced.patches.music.utils.resourceid.mainActivityLaunchAnimation +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.util.Utils.printWarn +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val EXTENSION_METHOD_DESCRIPTOR = + "$MISC_PATH/CairoSplashAnimationPatch;->disableCairoSplashAnimation(Z)Z" + +@Suppress("unused") +val cairoSplashAnimationPatch = bytecodePatch( + DISABLE_CAIRO_SPLASH_ANIMATION.title, + DISABLE_CAIRO_SPLASH_ANIMATION.summary, +) { + compatibleWith( + YOUTUBE_MUSIC_PACKAGE_NAME( + "7.06.54", + "7.16.53", + "7.25.53", + ), + ) + + dependsOn( + settingsPatch, + sharedResourceIdPatch, + versionCheckPatch, + ) + + execute { + if (!is_7_06_or_greater) { + printWarn("\"${DISABLE_CAIRO_SPLASH_ANIMATION.title}\" is not supported in this version. Use YouTube Music 7.06.54 or later.") + return@execute + } else if (!is_7_20_or_greater) { + cairoSplashAnimationConfigFingerprint.injectLiteralInstructionBooleanCall( + 45635386L, + EXTENSION_METHOD_DESCRIPTOR + ) + } else { + cairoSplashAnimationConfigFingerprint.methodOrThrow().apply { + val literalIndex = indexOfFirstLiteralInstructionOrThrow( + mainActivityLaunchAnimation + ) + val insertIndex = indexOfFirstInstructionReversedOrThrow(literalIndex) { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setContentView" + } + 1 + val viewStubFindViewByIdIndex = indexOfFirstInstructionOrThrow(literalIndex) { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.name == "findViewById" && + reference.definingClass != "Landroid/view/View;" + } + val freeRegister = + getInstruction(viewStubFindViewByIdIndex).registerD + val jumpIndex = indexOfFirstInstructionReversedOrThrow( + viewStubFindViewByIdIndex, + Opcode.IGET_OBJECT + ) + + addInstructionsWithLabels( + insertIndex, """ + const/4 v$freeRegister, 0x1 + invoke-static {v$freeRegister}, $EXTENSION_METHOD_DESCRIPTOR + move-result v$freeRegister + if-eqz v$freeRegister, :skip + """, ExternalLabel("skip", getInstruction(jumpIndex)) + ) + } + } + + addSwitchPreference( + CategoryType.MISC, + "revanced_disable_cairo_splash_animation", + "false" + ) + + updatePatchStatus(DISABLE_CAIRO_SPLASH_ANIMATION) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/Fingerprints.kt new file mode 100644 index 000000000..05fbdf843 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/Fingerprints.kt @@ -0,0 +1,26 @@ +package app.revanced.patches.music.misc.splash + +import app.revanced.patches.music.utils.playservice.is_7_20_or_greater +import app.revanced.patches.music.utils.resourceid.mainActivityLaunchAnimation +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.indexOfFirstLiteralInstruction + +/** + * This fingerprint is compatible with YouTube Music v7.06.53+ + */ +internal val cairoSplashAnimationConfigFingerprint = legacyFingerprint( + name = "cairoSplashAnimationConfigFingerprint", + returnType = "V", + customFingerprint = handler@{ method, _ -> + if (method.definingClass != "Lcom/google/android/apps/youtube/music/activities/MusicActivity;") + return@handler false + if (method.name != "onCreate") + return@handler false + + if (is_7_20_or_greater) { + method.indexOfFirstLiteralInstruction(mainActivityLaunchAnimation) >= 0 + } else { + method.indexOfFirstLiteralInstruction(45635386) >= 0 + } + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/thumbnails/BypassImageRegionRestrictionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/thumbnails/BypassImageRegionRestrictionsPatch.kt new file mode 100644 index 000000000..7390e1ef3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/thumbnails/BypassImageRegionRestrictionsPatch.kt @@ -0,0 +1,37 @@ +package app.revanced.patches.music.misc.thumbnails + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.patch.PatchList.BYPASS_IMAGE_REGION_RESTRICTIONS +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.imageurl.addImageUrlHook +import app.revanced.patches.shared.imageurl.cronetImageUrlHookPatch + +@Suppress("unused") +val bypassImageRegionRestrictionsPatch = bytecodePatch( + BYPASS_IMAGE_REGION_RESTRICTIONS.title, + BYPASS_IMAGE_REGION_RESTRICTIONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + cronetImageUrlHookPatch(false) + ) + + execute { + addImageUrlHook() + + addSwitchPreference( + CategoryType.MISC, + "revanced_bypass_image_region_restrictions", + "false" + ) + + updatePatchStatus(BYPASS_IMAGE_REGION_RESTRICTIONS) + + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/tracking/SanitizeUrlQueryPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/tracking/SanitizeUrlQueryPatch.kt new file mode 100644 index 000000000..1a8421382 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/tracking/SanitizeUrlQueryPatch.kt @@ -0,0 +1,34 @@ +package app.revanced.patches.music.misc.tracking + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.patch.PatchList.SANITIZE_SHARING_LINKS +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.tracking.baseSanitizeUrlQueryPatch + +@Suppress("unused") +val sanitizeUrlQueryPatch = bytecodePatch( + SANITIZE_SHARING_LINKS.title, + SANITIZE_SHARING_LINKS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseSanitizeUrlQueryPatch, + settingsPatch, + ) + + execute { + addSwitchPreference( + CategoryType.MISC, + "revanced_sanitize_sharing_links", + "true" + ) + + updatePatchStatus(SANITIZE_SHARING_LINKS) + + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/Fingerprints.kt new file mode 100644 index 000000000..239029a93 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/Fingerprints.kt @@ -0,0 +1,34 @@ +package app.revanced.patches.music.navigation.components + +import app.revanced.patches.music.utils.resourceid.colorGrey +import app.revanced.patches.music.utils.resourceid.text1 +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val tabLayoutFingerprint = legacyFingerprint( + name = "tabLayoutFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + strings = listOf("FEmusic_radio_builder"), + literals = listOf(colorGrey) +) + +internal val tabLayoutTextFingerprint = legacyFingerprint( + name = "tabLayoutTextFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.IGET, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IF_NEZ, + Opcode.SGET_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT + ), + literals = listOf(text1) +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt new file mode 100644 index 000000000..55adfbadf --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt @@ -0,0 +1,174 @@ +package app.revanced.patches.music.navigation.components + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.NAVIGATION_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.patch.PatchList.NAVIGATION_BAR_COMPONENTS +import app.revanced.patches.music.utils.resourceid.colorGrey +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.music.utils.resourceid.text1 +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val FLAG = "android:layout_weight" +private const val RESOURCE_FILE_PATH = "res/layout/image_with_text_tab.xml" + +private val navigationBarComponentsResourcePatch = resourcePatch( + description = "navigationBarComponentsResourcePatch" +) { + execute { + document(RESOURCE_FILE_PATH).use { document -> + with(document.getElementsByTagName("ImageView").item(0)) { + if (attributes.getNamedItem(FLAG) != null) + return@with + + document.createAttribute(FLAG) + .apply { value = "0.5" } + .let(attributes::setNamedItem) + } + } + } +} + +@Suppress("unused") +val navigationBarComponentsPatch = bytecodePatch( + NAVIGATION_BAR_COMPONENTS.title, + NAVIGATION_BAR_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + navigationBarComponentsResourcePatch, + sharedResourceIdPatch, + settingsPatch, + ) + + execute { + /** + * Enable black navigation bar + */ + tabLayoutFingerprint.methodOrThrow().apply { + val constIndex = indexOfFirstLiteralInstructionOrThrow(colorGrey) + val insertIndex = indexOfFirstInstructionOrThrow(constIndex) { + opcode == Opcode.INVOKE_VIRTUAL + && getReference()?.name == "setBackgroundColor" + } + val insertRegister = getInstruction(insertIndex).registerD + + addInstructions( + insertIndex, """ + invoke-static {}, $NAVIGATION_CLASS_DESCRIPTOR->enableBlackNavigationBar()I + move-result v$insertRegister + """ + ) + } + + /** + * Hide navigation labels + */ + tabLayoutTextFingerprint.methodOrThrow().apply { + val constIndex = + indexOfFirstLiteralInstructionOrThrow(text1) + val targetIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.CHECK_CAST) + val targetParameter = getInstruction(targetIndex).reference + val targetRegister = getInstruction(targetIndex).registerA + + if (!targetParameter.toString().endsWith("Landroid/widget/TextView;")) + throw PatchException("Method signature parameter did not match: $targetParameter") + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $NAVIGATION_CLASS_DESCRIPTOR->hideNavigationLabel(Landroid/widget/TextView;)V" + ) + } + + /** + * Hide navigation bar & buttons + */ + tabLayoutTextFingerprint.matchOrThrow().let { + it.method.apply { + val enumIndex = it.patternMatch!!.startIndex + 3 + val enumRegister = getInstruction(enumIndex).registerA + val insertEnumIndex = indexOfFirstInstructionOrThrow(Opcode.AND_INT_LIT8) - 2 + + val pivotTabIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "getVisibility" + } + val pivotTabRegister = + getInstruction(pivotTabIndex).registerC + + addInstruction( + pivotTabIndex, + "invoke-static {v$pivotTabRegister}, $NAVIGATION_CLASS_DESCRIPTOR->hideNavigationButton(Landroid/view/View;)V" + ) + + addInstruction( + insertEnumIndex, + "sput-object v$enumRegister, $NAVIGATION_CLASS_DESCRIPTOR->lastPivotTab:Ljava/lang/Enum;" + ) + } + } + + addSwitchPreference( + CategoryType.NAVIGATION, + "revanced_enable_black_navigation_bar", + "true" + ) + addSwitchPreference( + CategoryType.NAVIGATION, + "revanced_hide_navigation_home_button", + "false" + ) + addSwitchPreference( + CategoryType.NAVIGATION, + "revanced_hide_navigation_samples_button", + "false" + ) + addSwitchPreference( + CategoryType.NAVIGATION, + "revanced_hide_navigation_explore_button", + "false" + ) + addSwitchPreference( + CategoryType.NAVIGATION, + "revanced_hide_navigation_library_button", + "false" + ) + addSwitchPreference( + CategoryType.NAVIGATION, + "revanced_hide_navigation_upgrade_button", + "true" + ) + addSwitchPreference( + CategoryType.NAVIGATION, + "revanced_hide_navigation_bar", + "false" + ) + addSwitchPreference( + CategoryType.NAVIGATION, + "revanced_hide_navigation_label", + "false" + ) + + updatePatchStatus(NAVIGATION_BAR_COMPONENTS) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt new file mode 100644 index 000000000..ea0046e1b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/player/components/Fingerprints.kt @@ -0,0 +1,356 @@ +package app.revanced.patches.music.player.components + +import app.revanced.patches.music.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.playservice.is_7_18_or_greater +import app.revanced.patches.music.utils.resourceid.colorGrey +import app.revanced.patches.music.utils.resourceid.darkBackground +import app.revanced.patches.music.utils.resourceid.miniPlayerDefaultText +import app.revanced.patches.music.utils.resourceid.miniPlayerMdxPlaying +import app.revanced.patches.music.utils.resourceid.miniPlayerPlayPauseReplayButton +import app.revanced.patches.music.utils.resourceid.miniPlayerViewPager +import app.revanced.patches.music.utils.resourceid.playerViewPager +import app.revanced.patches.music.utils.resourceid.remixGenericButtonSize +import app.revanced.patches.music.utils.resourceid.tapBloomView +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +const val AUDIO_VIDEO_SWITCH_TOGGLE_VISIBILITY = + "Lcom/google/android/apps/youtube/music/player/AudioVideoSwitcherToggleView;->setVisibility(I)V" + +internal val audioVideoSwitchToggleFingerprint = legacyFingerprint( + name = "audioVideoSwitchToggleFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.toString() == AUDIO_VIDEO_SWITCH_TOGGLE_VISIBILITY + } >= 0 + } +) + +internal val engagementPanelHeightFingerprint = legacyFingerprint( + name = "engagementPanelHeightFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + // In YouTube Music 7.21.50+, there are two methods with similar structure, so this Opcode pattern must be used. + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + ), + parameters = emptyList(), + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "booleanValue" + } >= 0 + } +) + +internal val engagementPanelHeightParentFingerprint = legacyFingerprint( + name = "engagementPanelHeightParentFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + opcodes = listOf(Opcode.NEW_ARRAY), + parameters = emptyList(), + customFingerprint = custom@{ method, _ -> + if (method.definingClass.startsWith("Lcom/")) { + return@custom false + } + if (method.returnType == "Ljava/lang/Object;") { + return@custom false + } + method.indexOfFirstInstruction { + opcode == Opcode.CHECK_CAST && + (this as? ReferenceInstruction)?.reference?.toString() == "Lcom/google/android/libraries/youtube/engagementpanel/size/EngagementPanelSizeBehavior;" + } >= 0 + } +) + +internal val handleSearchRenderedFingerprint = legacyFingerprint( + name = "handleSearchRenderedFingerprint", + returnType = "V", + parameters = listOf("L"), + customFingerprint = { method, _ -> method.name == "handleSearchRendered" } +) + +internal val handleSignInEventFingerprint = legacyFingerprint( + name = "handleSignInEventFingerprint", + returnType = "V", + parameters = listOf("L"), + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ), + customFingerprint = { method, _ -> method.name == "handleSignInEvent" } +) + +internal val interactionLoggingEnumFingerprint = legacyFingerprint( + name = "interactionLoggingEnumFingerprint", + returnType = "V", + strings = listOf("INTERACTION_LOGGING_GESTURE_TYPE_SWIPE") +) + +internal val minimizedPlayerFingerprint = legacyFingerprint( + name = "minimizedPlayerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "L"), + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_NEZ, + Opcode.IF_EQZ + ), + strings = listOf("w_st") +) + +internal val miniPlayerConstructorFingerprint = legacyFingerprint( + name = "miniPlayerConstructorFingerprint", + returnType = "V", + strings = listOf("sharedToggleMenuItemMutations"), + literals = listOf(colorGrey, miniPlayerPlayPauseReplayButton) +) + +internal val miniPlayerDefaultTextFingerprint = legacyFingerprint( + name = "miniPlayerDefaultTextFingerprint", + returnType = "V", + parameters = listOf("Ljava/lang/Object;"), + opcodes = listOf( + Opcode.SGET_OBJECT, + Opcode.IF_NE + ), + literals = listOf(miniPlayerDefaultText) +) + +internal val miniPlayerDefaultViewVisibilityFingerprint = legacyFingerprint( + name = "miniPlayerDefaultViewVisibilityFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/View;", "F"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.SUB_FLOAT_2ADDR, + Opcode.SGET_OBJECT, + Opcode.INVOKE_VIRTUAL + ), + customFingerprint = { method, classDef -> + method.name == "a" && + classDef.methods.count() == 3 + } +) + +internal val miniPlayerParentFingerprint = legacyFingerprint( + name = "miniPlayerParentFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(miniPlayerMdxPlaying) +) + +internal val mppWatchWhileLayoutFingerprint = legacyFingerprint( + name = "mppWatchWhileLayoutFingerprint", + returnType = "V", + opcodes = listOf(Opcode.NEW_ARRAY), + literals = listOf(miniPlayerPlayPauseReplayButton), + customFingerprint = custom@{ method, _ -> + if (!method.definingClass.endsWith("/MppWatchWhileLayout;")) { + return@custom false + } + if (method.name != "onFinishInflate") { + return@custom false + } + if (!is_7_18_or_greater) { + return@custom true + } + + indexOfCallableInstruction(method) >= 0 + } +) + +internal fun indexOfCallableInstruction(method: Method) = + method.indexOfFirstInstruction { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.returnType == "V" && + reference.parameterTypes.size == 1 && + reference.parameterTypes.firstOrNull() == "Ljava/util/concurrent/Callable;" + } + +internal val musicActivityWidgetFingerprint = legacyFingerprint( + name = "musicActivityWidgetFingerprint", + literals = listOf(79500L), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/MusicActivity;") + } +) + +internal val musicPlaybackControlsFingerprint = legacyFingerprint( + name = "musicPlaybackControlsFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Z"), + opcodes = listOf( + Opcode.IPUT_BOOLEAN, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/MusicPlaybackControls;") + } +) + +internal val nextButtonVisibilityFingerprint = legacyFingerprint( + name = "nextButtonVisibilityFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.CONST_16, + Opcode.IF_EQZ + ) +) + +internal val oldEngagementPanelFingerprint = legacyFingerprint( + name = "oldEngagementPanelFingerprint", + returnType = "Z", + parameters = emptyList(), + literals = listOf(45427672L), +) + +/** + * Deprecated in YouTube Music v6.34.51+ + */ +internal val oldPlayerBackgroundFingerprint = legacyFingerprint( + name = "oldPlayerBackgroundFingerprint", + returnType = "Z", + parameters = emptyList(), + literals = listOf(45415319L), +) + +/** + * Deprecated in YouTube Music v6.31.55+ + */ +internal val oldPlayerLayoutFingerprint = legacyFingerprint( + name = "oldPlayerLayoutFingerprint", + returnType = "Z", + parameters = emptyList(), + literals = listOf(45399578L), +) + +internal val playerPatchConstructorFingerprint = legacyFingerprint( + name = "playerPatchConstructorFingerprint", + returnType = "V", + customFingerprint = { method, _ -> + method.definingClass == PLAYER_CLASS_DESCRIPTOR && + method.name == "" + } +) + +internal val playerViewPagerConstructorFingerprint = legacyFingerprint( + name = "playerViewPagerConstructorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf(miniPlayerViewPager, playerViewPager), +) + +internal val quickSeekOverlayFingerprint = legacyFingerprint( + name = "quickSeekOverlayFingerprint", + returnType = "V", + parameters = emptyList(), + literals = listOf(darkBackground, tapBloomView), +) + +internal val remixGenericButtonFingerprint = legacyFingerprint( + name = "remixGenericButtonFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + opcodes = listOf( + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.FLOAT_TO_INT + ), + literals = listOf(remixGenericButtonSize), +) + +internal val repeatTrackFingerprint = legacyFingerprint( + name = "repeatTrackFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "L"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.SGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_NEZ + ), + strings = listOf("w_st") +) + +internal val shuffleOnClickFingerprint = legacyFingerprint( + name = "shuffleOnClickFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/View;"), + literals = listOf(45468L), + customFingerprint = { method, _ -> + method.name == "onClick" && + indexOfAccessibilityInstruction(method) >= 0 + } +) + +internal fun indexOfAccessibilityInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "announceForAccessibility" + } + +internal val swipeToCloseFingerprint = legacyFingerprint( + name = "swipeToCloseFingerprint", + returnType = "Z", + parameters = emptyList(), + literals = listOf(45398432L), +) + +internal val switchToggleColorFingerprint = legacyFingerprint( + name = "switchToggleColorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + parameters = listOf("L", "J"), + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + Opcode.IGET + ) +) + +internal val zenModeFingerprint = legacyFingerprint( + name = "zenModeFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "J"), + opcodes = listOf( + Opcode.MOVE_RESULT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.GOTO, + Opcode.NOP, + Opcode.SGET_OBJECT + ) +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/player/components/PlayerComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/player/components/PlayerComponentsPatch.kt new file mode 100644 index 000000000..1528f747d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/player/components/PlayerComponentsPatch.kt @@ -0,0 +1,1086 @@ +package app.revanced.patches.music.player.components + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.ResourcePatchContext +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.music.utils.extension.Constants.EXTENSION_PATH +import app.revanced.patches.music.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.mainactivity.mainActivityResolvePatch +import app.revanced.patches.music.utils.patch.PatchList.PLAYER_COMPONENTS +import app.revanced.patches.music.utils.pendingIntentReceiverFingerprint +import app.revanced.patches.music.utils.playservice.is_6_27_or_greater +import app.revanced.patches.music.utils.playservice.is_6_42_or_greater +import app.revanced.patches.music.utils.playservice.is_7_18_or_greater +import app.revanced.patches.music.utils.playservice.is_7_25_or_greater +import app.revanced.patches.music.utils.playservice.versionCheckPatch +import app.revanced.patches.music.utils.resourceid.colorGrey +import app.revanced.patches.music.utils.resourceid.darkBackground +import app.revanced.patches.music.utils.resourceid.miniPlayerPlayPauseReplayButton +import app.revanced.patches.music.utils.resourceid.miniPlayerViewPager +import app.revanced.patches.music.utils.resourceid.playerViewPager +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.music.utils.resourceid.tapBloomView +import app.revanced.patches.music.utils.resourceid.topEnd +import app.revanced.patches.music.utils.resourceid.topStart +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.music.utils.videotype.videoTypeHookPatch +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.shared.mainactivity.getMainActivityMethod +import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT +import app.revanced.util.addStaticFieldToExtension +import app.revanced.util.adoptChild +import app.revanced.util.cloneMutable +import app.revanced.util.doRecursively +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.injectLiteralInstructionViewCall +import app.revanced.util.fingerprint.matchOrNull +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.fingerprint.resolvable +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import app.revanced.util.indexOfFirstStringInstructionOrThrow +import app.revanced.util.insertNode +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.ImmutableField +import org.w3c.dom.Element + +private const val IMAGE_VIEW_TAG_NAME = + "com.google.android.libraries.youtube.common.ui.TouchImageView" +private const val NEXT_BUTTON_VIEW_ID = + "mini_player_next_button" +private const val PREVIOUS_BUTTON_VIEW_ID = + "mini_player_previous_button" + +private val playerComponentsResourcePatch = resourcePatch( + description = "playerComponentsResourcePatch" +) { + dependsOn(versionCheckPatch) + + execute { + val publicFile = get("res/values/public.xml") + + // Since YT Music v6.42.51,the resources for the next button have been removed, we need to add them manually. + if (is_6_42_or_greater) { + publicFile.writeText( + publicFile.readText() + .replace( + "\"TOP_START\"", + "\"$NEXT_BUTTON_VIEW_ID\"" + ) + ) + insertNode(false) + } + publicFile.writeText( + publicFile.readText() + .replace( + "\"TOP_END\"", + "\"$PREVIOUS_BUTTON_VIEW_ID\"" + ) + ) + insertNode(true) + } +} + +private fun ResourcePatchContext.insertNode(isPreviousButton: Boolean) { + var shouldAddPreviousButton = true + + document("res/layout/watch_while_layout.xml").use { document -> + document.doRecursively loop@{ node -> + if (node !is Element) return@loop + + node.getAttributeNode("android:id")?.let { attribute -> + if (isPreviousButton) { + if (attribute.textContent == "@id/mini_player_play_pause_replay_button" && + shouldAddPreviousButton + ) { + node.insertNode(IMAGE_VIEW_TAG_NAME, node) { + setPreviousButtonNodeAttribute() + } + shouldAddPreviousButton = false + } + } else { + if (attribute.textContent == "@id/mini_player") { + node.adoptChild(IMAGE_VIEW_TAG_NAME) { + setNextButtonNodeAttribute() + } + } + } + } + } + } +} + +private fun Element.setNextButtonNodeAttribute() { + mapOf( + "android:id" to "@id/$NEXT_BUTTON_VIEW_ID", + "android:padding" to "@dimen/item_medium_spacing", + "android:layout_width" to "@dimen/remix_generic_button_size", + "android:layout_height" to "@dimen/remix_generic_button_size", + "android:src" to "@drawable/music_player_next", + "android:scaleType" to "fitCenter", + "android:contentDescription" to "@string/accessibility_next", + "style" to "@style/MusicPlayerButton" + ).forEach { (k, v) -> + setAttribute(k, v) + } +} + +private fun Element.setPreviousButtonNodeAttribute() { + mapOf( + "android:id" to "@id/$PREVIOUS_BUTTON_VIEW_ID", + "android:padding" to "@dimen/item_medium_spacing", + "android:layout_width" to "@dimen/remix_generic_button_size", + "android:layout_height" to "@dimen/remix_generic_button_size", + "android:src" to "@drawable/music_player_prev", + "android:scaleType" to "fitCenter", + "android:contentDescription" to "@string/accessibility_previous", + "style" to "@style/MusicPlayerButton" + ).forEach { (k, v) -> + setAttribute(k, v) + } +} + +private const val FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/PlayerComponentsFilter;" + +private const val EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR = + "$EXTENSION_PATH/utils/VideoUtils;" + +@Suppress("unused") +val playerComponentsPatch = bytecodePatch( + PLAYER_COMPONENTS.title, + PLAYER_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + playerComponentsResourcePatch, + sharedResourceIdPatch, + lithoFilterPatch, + mainActivityResolvePatch, + videoTypeHookPatch, + ) + + execute { + // region patch for disable gesture in player + + val playerViewPagerConstructorMethod = + playerViewPagerConstructorFingerprint.methodOrThrow() + val mainActivityOnStartMethod = + getMainActivityMethod("onStart") + + mapOf( + miniPlayerViewPager to "disableMiniPlayerGesture", + playerViewPager to "disablePlayerGesture" + ).forEach { (literal, methodName) -> + val viewPagerReference = with(playerViewPagerConstructorMethod) { + val constIndex = indexOfFirstLiteralInstructionOrThrow(literal) + val targetIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.IPUT_OBJECT) + + getInstruction(targetIndex).reference.toString() + } + mainActivityOnStartMethod.apply { + val insertIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.IGET_OBJECT && + getReference()?.toString() == viewPagerReference + } + val insertRegister = getInstruction(insertIndex).registerA + val jumpIndex = + indexOfFirstInstructionOrThrow(insertIndex, Opcode.INVOKE_VIRTUAL) + 1 + + addInstructionsWithLabels( + insertIndex, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->$methodName()Z + move-result v$insertRegister + if-nez v$insertRegister, :disable + """, ExternalLabel("disable", getInstruction(jumpIndex)) + ) + } + } + + addSwitchPreference( + CategoryType.PLAYER, + "revanced_disable_mini_player_gesture", + "false" + ) + addSwitchPreference( + CategoryType.PLAYER, + "revanced_disable_player_gesture", + "false" + ) + + // endregion + + // region patch for enable color match player and enable black player background + + val ( + colorMathPlayerMethodParameter, + colorMathPlayerInvokeVirtualReference, + colorMathPlayerIGetReference + ) = switchToggleColorFingerprint.matchOrThrow(miniPlayerConstructorFingerprint).let { + with(it.method) { + val relativeIndex = it.patternMatch!!.endIndex + 1 + val invokeVirtualIndex = + indexOfFirstInstructionOrThrow(relativeIndex, Opcode.INVOKE_VIRTUAL) + val iGetIndex = indexOfFirstInstructionOrThrow(relativeIndex, Opcode.IGET) + + // black player background + val invokeDirectIndex = indexOfFirstInstructionOrThrow(Opcode.INVOKE_DIRECT) + val targetMethod = getWalkerMethod(invokeDirectIndex) + val insertIndex = targetMethod.indexOfFirstInstructionOrThrow(Opcode.IF_NE) + + targetMethod.addInstructions( + insertIndex, """ + invoke-static {p1}, $PLAYER_CLASS_DESCRIPTOR->enableBlackPlayerBackground(I)I + move-result p1 + invoke-static {p2}, $PLAYER_CLASS_DESCRIPTOR->enableBlackPlayerBackground(I)I + move-result p2 + """ + ) + Triple( + parameters, + getInstruction(invokeVirtualIndex).reference, + getInstruction(iGetIndex).reference + ) + } + } + + val colorMathPlayerIPutReference = with(miniPlayerConstructorFingerprint.methodOrThrow()) { + val colorGreyIndex = indexOfFirstLiteralInstructionOrThrow(colorGrey) + val iPutIndex = indexOfFirstInstructionOrThrow(colorGreyIndex, Opcode.IPUT) + getInstruction(iPutIndex).reference + } + + miniPlayerConstructorFingerprint.mutableClassOrThrow().methods.filter { + it.accessFlags == AccessFlags.PUBLIC or AccessFlags.FINAL && + it.parameters == colorMathPlayerMethodParameter && + it.returnType == "V" + }.forEach { method -> + method.apply { + val freeRegister = implementation!!.registerCount - parameters.size - 3 + + val invokeDirectIndex = + indexOfFirstInstructionReversedOrThrow(Opcode.INVOKE_DIRECT) + val invokeDirectReference = + getInstruction(invokeDirectIndex).reference + + addInstructionsWithLabels( + invokeDirectIndex + 1, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableColorMatchPlayer()Z + move-result v$freeRegister + if-eqz v$freeRegister, :off + invoke-virtual {p1}, $colorMathPlayerInvokeVirtualReference + move-result-object v$freeRegister + check-cast v$freeRegister, ${(colorMathPlayerIGetReference as FieldReference).definingClass} + iget v$freeRegister, v$freeRegister, $colorMathPlayerIGetReference + iput v$freeRegister, p0, $colorMathPlayerIPutReference + :off + invoke-direct {p0}, $invokeDirectReference + """ + ) + removeInstruction(invokeDirectIndex) + } + } + + addSwitchPreference( + CategoryType.PLAYER, + "revanced_enable_black_player_background", + "false" + ) + addSwitchPreference( + CategoryType.PLAYER, + "revanced_enable_color_match_player", + "true" + ) + + // endregion + + // region patch for enable force minimized player + + minimizedPlayerFingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.patternMatch!!.endIndex + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $PLAYER_CLASS_DESCRIPTOR->enableForceMinimizedPlayer(Z)Z + move-result v$insertRegister + """ + ) + } + } + + addSwitchPreference( + CategoryType.PLAYER, + "revanced_enable_force_minimized_player", + "true" + ) + + // endregion + + // region patch for enable next previous button + + val nextButtonFieldName = "nextButton" + val previousButtonFieldName = "previousButton" + val nextButtonClassFieldName = "nextButtonClass" + val previousButtonClassFieldName = "previousButtonClass" + val nextButtonButtonMethodName = "setNextButton" + val previousButtonMethodName = "setPreviousButton" + val nextButtonOnClickListenerMethodName = "setNextButtonOnClickListener" + val previousButtonOnClickListenerMethodName = "setPreviousButtonOnClickListener" + val nextButtonIntentString = "YTM Next" + val previousButtonIntentString = "YTM Previous" + + fun MutableMethod.setStaticFieldValue( + fieldName: String, + viewId: Long + ) { + val miniPlayerPlayPauseReplayButtonIndex = + indexOfFirstLiteralInstructionOrThrow(miniPlayerPlayPauseReplayButton) + val constRegister = + getInstruction(miniPlayerPlayPauseReplayButtonIndex).registerA + val findViewByIdIndex = + indexOfFirstInstructionOrThrow( + miniPlayerPlayPauseReplayButtonIndex, + Opcode.INVOKE_VIRTUAL + ) + val findViewByIdRegister = + getInstruction(findViewByIdIndex).registerC + + addInstructions( + miniPlayerPlayPauseReplayButtonIndex, """ + const v$constRegister, $viewId + invoke-virtual {v$findViewByIdRegister, v$constRegister}, $definingClass->findViewById(I)Landroid/view/View; + move-result-object v$constRegister + sput-object v$constRegister, $PLAYER_CLASS_DESCRIPTOR->$fieldName:Landroid/view/View; + """ + ) + } + + fun MutableMethod.setViewArray() { + val miniPlayerPlayPauseReplayButtonIndex = + indexOfFirstLiteralInstructionOrThrow(miniPlayerPlayPauseReplayButton) + val invokeStaticIndex = + indexOfFirstInstructionOrThrow( + miniPlayerPlayPauseReplayButtonIndex, + Opcode.INVOKE_STATIC + ) + val viewArrayRegister = + getInstruction(invokeStaticIndex).registerC + + addInstructions( + invokeStaticIndex, """ + invoke-static {v$viewArrayRegister}, $PLAYER_CLASS_DESCRIPTOR->getViewArray([Landroid/view/View;)[Landroid/view/View; + move-result-object v$viewArrayRegister + """ + ) + } + + fun MutableMethod.setOnClickListener( + intentString: String, + methodName: String, + fieldName: String + ) { + val startIndex = indexOfFirstStringInstructionOrThrow(intentString) + val onClickIndex = + indexOfFirstInstructionReversedOrThrow(startIndex, Opcode.INVOKE_VIRTUAL) + val onClickReference = getInstruction(onClickIndex).reference + val onClickReferenceDefiningClass = (onClickReference as MethodReference).definingClass + + findMethodOrThrow(onClickReferenceDefiningClass) + .apply { + addInstruction( + implementation!!.instructions.lastIndex, + "sput-object p0, $PLAYER_CLASS_DESCRIPTOR->$fieldName:$onClickReferenceDefiningClass" + ) + } + + playerPatchConstructorFingerprint.mutableClassOrThrow().let { mutableClass -> + mutableClass.methods.find { method -> method.name == methodName } + ?.apply { + mutableClass.staticFields.add( + ImmutableField( + definingClass, + fieldName, + onClickReferenceDefiningClass, + AccessFlags.PUBLIC or AccessFlags.STATIC, + null, + annotations, + null + ).toMutable() + ) + addInstructionsWithLabels( + 0, """ + sget-object v0, $PLAYER_CLASS_DESCRIPTOR->$fieldName:$onClickReferenceDefiningClass + if-eqz v0, :ignore + invoke-virtual {v0}, $onClickReference + :ignore + return-void + """ + ) + } + } + } + + val miniPlayerConstructorMutableMethod = + miniPlayerConstructorFingerprint.methodOrThrow() + + val mppWatchWhileLayoutMutableMethod = + mppWatchWhileLayoutFingerprint.methodOrThrow() + + val pendingIntentReceiverMutableMethod = + pendingIntentReceiverFingerprint.methodOrThrow() + + if (!is_6_42_or_greater) { + nextButtonVisibilityFingerprint.matchOrThrow(miniPlayerParentFingerprint).let { + it.method.apply { + val targetIndex = it.patternMatch!!.startIndex + 1 + val targetRegister = + getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + invoke-static {v$targetRegister}, $PLAYER_CLASS_DESCRIPTOR->enableMiniPlayerNextButton(Z)Z + move-result v$targetRegister + """ + ) + } + } + } else { + miniPlayerConstructorMutableMethod.setInstanceFieldValue( + nextButtonButtonMethodName, + topStart + ) + mppWatchWhileLayoutMutableMethod.setStaticFieldValue(nextButtonFieldName, topStart) + pendingIntentReceiverMutableMethod.setOnClickListener( + nextButtonIntentString, + nextButtonOnClickListenerMethodName, + nextButtonClassFieldName + ) + } + + miniPlayerConstructorMutableMethod.setInstanceFieldValue( + previousButtonMethodName, + topEnd + ) + mppWatchWhileLayoutMutableMethod.setStaticFieldValue(previousButtonFieldName, topEnd) + pendingIntentReceiverMutableMethod.setOnClickListener( + previousButtonIntentString, + previousButtonOnClickListenerMethodName, + previousButtonClassFieldName + ) + + mppWatchWhileLayoutMutableMethod.setViewArray() + + addSwitchPreference( + CategoryType.PLAYER, + "revanced_enable_mini_player_next_button", + "true" + ) + addSwitchPreference( + CategoryType.PLAYER, + "revanced_enable_mini_player_previous_button", + "true" + ) + + // endregion + + // region patch for enable swipe to dismiss mini player + + if (!is_6_42_or_greater) { + swipeToCloseFingerprint.methodOrThrow().apply { + val insertIndex = implementation!!.instructions.lastIndex + val targetRegister = + getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$targetRegister}, $PLAYER_CLASS_DESCRIPTOR->enableSwipeToDismissMiniPlayer(Z)Z + move-result v$targetRegister + """ + ) + } + } else { + + // region dismiss mini player by swiping down + + val swipeToDismissSGetObjectReference = + with(interactionLoggingEnumFingerprint.methodOrThrow()) { + val stringIndex = + indexOfFirstStringInstructionOrThrow("INTERACTION_LOGGING_GESTURE_TYPE_SWIPE") + val sPutObjectIndex = + indexOfFirstInstructionOrThrow(stringIndex, Opcode.SPUT_OBJECT) + + getInstruction(sPutObjectIndex).reference + } + + val musicActivityWidgetMethod = + musicActivityWidgetFingerprint.methodOrThrow() + + val swipeToDismissWidgetIndex = + musicActivityWidgetMethod.indexOfFirstLiteralInstructionOrThrow(79500L) + + fun getSwipeToDismissReference( + opcode: Opcode, + reversed: Boolean + ) = with(musicActivityWidgetMethod) { + val targetIndex = if (reversed) + indexOfFirstInstructionReversedOrThrow(swipeToDismissWidgetIndex, opcode) + else + indexOfFirstInstructionOrThrow(swipeToDismissWidgetIndex, opcode) + + getInstruction(targetIndex).reference + } + + val swipeToDismissIGetObjectReference = + getSwipeToDismissReference(Opcode.IGET_OBJECT, true) + val swipeToDismissInvokeInterfacePrimaryReference = + getSwipeToDismissReference(Opcode.INVOKE_INTERFACE, true) + val swipeToDismissCheckCastReference = + getSwipeToDismissReference(Opcode.CHECK_CAST, true) + val swipeToDismissNewInstanceReference = + getSwipeToDismissReference(Opcode.NEW_INSTANCE, true) + val swipeToDismissInvokeStaticReference = + getSwipeToDismissReference(Opcode.INVOKE_STATIC, false) + val swipeToDismissInvokeDirectReference = + getSwipeToDismissReference(Opcode.INVOKE_DIRECT, false) + val swipeToDismissInvokeInterfaceSecondaryReference = + getSwipeToDismissReference(Opcode.INVOKE_INTERFACE, false) + + handleSignInEventFingerprint.matchOrThrow(handleSearchRenderedFingerprint).let { + val dismissBehaviorMethod = + it.getWalkerMethod(it.patternMatch!!.startIndex) + + dismissBehaviorMethod.apply { + val insertIndex = indexOfFirstInstructionOrThrow { + getReference()?.type == "Ljava/util/concurrent/atomic/AtomicBoolean;" + } + val primaryRegister = + getInstruction(insertIndex).registerB + val secondaryRegister = primaryRegister + 1 + val tertiaryRegister = secondaryRegister + 1 + + val freeRegister = implementation!!.registerCount - parameters.size - 2 + + addInstructionsWithLabels( + insertIndex, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableSwipeToDismissMiniPlayer()Z + move-result v$freeRegister + if-nez v$freeRegister, :dismiss + iget-object v$primaryRegister, v$primaryRegister, $swipeToDismissIGetObjectReference + invoke-interface {v$primaryRegister}, $swipeToDismissInvokeInterfacePrimaryReference + move-result-object v$primaryRegister + check-cast v$primaryRegister, $swipeToDismissCheckCastReference + sget-object v$secondaryRegister, $swipeToDismissSGetObjectReference + new-instance v$tertiaryRegister, $swipeToDismissNewInstanceReference + const p0, 0x878b + invoke-static {p0}, $swipeToDismissInvokeStaticReference + move-result-object p0 + invoke-direct {v$tertiaryRegister, p0}, $swipeToDismissInvokeDirectReference + const/4 p0, 0x0 + invoke-interface {v$primaryRegister, v$secondaryRegister, v$tertiaryRegister, p0}, $swipeToDismissInvokeInterfaceSecondaryReference + return-void + """, ExternalLabel("dismiss", getInstruction(insertIndex)) + ) + } + } + + // endregion + + // region hides default text display when the app is cold started + + miniPlayerDefaultTextFingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.patternMatch!!.endIndex + val insertRegister = + getInstruction(insertIndex).registerB + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $PLAYER_CLASS_DESCRIPTOR->enableSwipeToDismissMiniPlayer(Ljava/lang/Object;)Ljava/lang/Object; + move-result-object v$insertRegister + """ + ) + } + } + + // endregion + + // region hides default text display after dismissing the mini player + + miniPlayerDefaultViewVisibilityFingerprint.mutableClassOrThrow().let { + it.methods.find { method -> + method.parameters == listOf("Landroid/view/View;", "I") + }?.apply { + val bottomSheetBehaviorIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.definingClass == "Lcom/google/android/material/bottomsheet/BottomSheetBehavior;" && + reference.parameterTypes.first() == "Z" + } + val freeRegister = + getInstruction(bottomSheetBehaviorIndex).registerD + + addInstructionsWithLabels( + bottomSheetBehaviorIndex - 2, + """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableSwipeToDismissMiniPlayer()Z + move-result v$freeRegister + if-nez v$freeRegister, :dismiss + """, + ExternalLabel("dismiss", getInstruction(bottomSheetBehaviorIndex + 1)) + ) + } ?: throw PatchException("Could not find targetMethod") + } + + // endregion + + } + + addSwitchPreference( + CategoryType.PLAYER, + "revanced_enable_swipe_to_dismiss_mini_player", + "true" + ) + + // endregion + + // region patch for enable zen mode + + // this method is used for old player background (deprecated since YT Music v6.34.51) + zenModeFingerprint.matchOrNull(miniPlayerConstructorFingerprint)?.let { + it.method.apply { + val startIndex = it.patternMatch!!.startIndex + val targetRegister = + getInstruction(startIndex).registerA + + val insertIndex = it.patternMatch!!.endIndex + 1 + + addInstructions( + insertIndex, """ + invoke-static {v$targetRegister}, $PLAYER_CLASS_DESCRIPTOR->enableZenMode(I)I + move-result v$targetRegister + """ + ) + } + } // no exception + + switchToggleColorFingerprint.methodOrThrow(miniPlayerConstructorFingerprint).apply { + val invokeDirectIndex = indexOfFirstInstructionOrThrow(Opcode.INVOKE_DIRECT) + val walkerMethod = getWalkerMethod(invokeDirectIndex) + + walkerMethod.addInstructions( + 0, """ + invoke-static {p1}, $PLAYER_CLASS_DESCRIPTOR->enableZenMode(I)I + move-result p1 + invoke-static {p2}, $PLAYER_CLASS_DESCRIPTOR->enableZenMode(I)I + move-result p2 + """ + ) + } + + addSwitchPreference( + CategoryType.PLAYER, + "revanced_enable_zen_mode", + "false" + ) + addSwitchPreference( + CategoryType.PLAYER, + "revanced_enable_zen_mode_podcast", + "false", + "revanced_enable_zen_mode" + ) + + // endregion + + // region patch for hide audio video switch toggle + + audioVideoSwitchToggleFingerprint.methodOrThrow().apply { + implementation!!.instructions + .withIndex() + .filter { (_, instruction) -> + val reference = (instruction as? ReferenceInstruction)?.reference + instruction.opcode == Opcode.INVOKE_VIRTUAL && + reference is MethodReference && + reference.toString() == AUDIO_VIDEO_SWITCH_TOGGLE_VISIBILITY + } + .map { (index, _) -> index } + .reversed() + .forEach { index -> + val instruction = getInstruction(index) + + replaceInstruction( + index, + "invoke-static {v${instruction.registerC}, v${instruction.registerD}}," + + "$PLAYER_CLASS_DESCRIPTOR->hideAudioVideoSwitchToggle(Landroid/view/View;I)V" + ) + } + } + + addSwitchPreference( + CategoryType.PLAYER, + "revanced_hide_audio_video_switch_toggle", + "false" + ) + + // endregion + + // region patch for hide channel guideline, timestamps & emoji picker buttons + + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + + addSwitchPreference( + CategoryType.PLAYER, + "revanced_hide_comment_channel_guidelines", + "true" + ) + addSwitchPreference( + CategoryType.PLAYER, + "revanced_hide_comment_timestamp_and_emoji_buttons", + "false" + ) + + // region patch for hide double-tap overlay filter + + val smaliInstruction = """ + invoke-static {v$REGISTER_TEMPLATE_REPLACEMENT}, $PLAYER_CLASS_DESCRIPTOR->hideDoubleTapOverlayFilter(Landroid/view/View;)V + """ + + arrayOf( + darkBackground, + tapBloomView + ).forEach { literal -> + quickSeekOverlayFingerprint.injectLiteralInstructionViewCall( + literal, + smaliInstruction + ) + } + + addSwitchPreference( + CategoryType.PLAYER, + "revanced_hide_double_tap_overlay_filter", + "false" + ) + + // endregion + + // region patch for hide fullscreen share button + + remixGenericButtonFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.endIndex + val targetRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + invoke-static {v$targetRegister}, $PLAYER_CLASS_DESCRIPTOR->hideFullscreenShareButton(I)I + move-result v$targetRegister + """ + ) + } + } + + addSwitchPreference( + CategoryType.PLAYER, + "revanced_hide_fullscreen_share_button", + "false" + ) + + // endregion + + // region patch for remember repeat state + + val (repeatTrackMethod, repeatTrackIndex) = repeatTrackFingerprint.matchOrThrow().let { + with(it.method) { + val targetIndex = it.patternMatch!!.endIndex + val targetRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex, """ + invoke-static {v$targetRegister}, $PLAYER_CLASS_DESCRIPTOR->rememberRepeatState(Z)Z + move-result v$targetRegister + """ + ) + Pair(this, targetIndex) + } + } + + addSwitchPreference( + CategoryType.PLAYER, + "revanced_remember_repeat_state", + "true" + ) + + // endregion + + // region patch for remember shuffle state + + shuffleOnClickFingerprint.methodOrThrow().apply { + val accessibilityIndex = indexOfAccessibilityInstruction(this) + + // region set shuffle enum + + val enumIndex = indexOfFirstInstructionReversedOrThrow(accessibilityIndex) { + opcode == Opcode.INVOKE_DIRECT && + getReference()?.returnType == "Ljava/lang/String;" + } + val enumRegister = getInstruction(enumIndex).registerD + val enumClass = + (getInstruction(enumIndex).reference as MethodReference).parameterTypes.first() + + addInstruction( + enumIndex, + "invoke-static {v$enumRegister}, $PLAYER_CLASS_DESCRIPTOR->setShuffleState(Ljava/lang/Enum;)V" + ) + + // endregion + + // region set static field + + val shuffleClassIndex = + indexOfFirstInstructionReversedOrThrow(accessibilityIndex, Opcode.CHECK_CAST) + val shuffleClass = + getInstruction(shuffleClassIndex).reference.toString() + val shuffleMutableClass = classBy { classDef -> + classDef.type == shuffleClass + }?.mutableClass + ?: throw PatchException("shuffle class not found") + + val smaliInstructions = + """ + if-eqz v0, :ignore + sget-object v1, $enumClass->b:$enumClass + invoke-virtual {v0, v1}, $shuffleClass->shuffleTracks($enumClass)V + :ignore + return-void + """ + + addStaticFieldToExtension( + EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR, + "shuffleTracks", + "shuffleClass", + shuffleClass, + smaliInstructions + ) + + // endregion + + // region make all methods accessible + + val shuffleMethod = shuffleMutableClass.methods.find { method -> + method.parameterTypes.firstOrNull() == enumClass && + method.parameterTypes.size == 1 && + method.returnType == "V" + } ?: throw PatchException("shuffle method not found") + + shuffleMutableClass.methods.add( + shuffleMethod.cloneMutable( + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + name = "shuffleTracks" + ) + ) + + // endregion + + } + + musicPlaybackControlsFingerprint.methodOrThrow().addInstruction( + 0, + "invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->shuffleTracks()V" + ) + + if (is_7_25_or_greater) { + repeatTrackMethod.addInstruction( + repeatTrackIndex, + "invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->shuffleTracksWithDelay()V" + ) + } + + addSwitchPreference( + CategoryType.PLAYER, + "revanced_remember_shuffle_state", + "true" + ) + + // endregion + + // region patch for restore old comments popup panels + + var restoreOldCommentsPopupPanel = false + + if (is_6_27_or_greater && !is_7_18_or_greater) { + oldEngagementPanelFingerprint.injectLiteralInstructionBooleanCall( + 45427672L, + "$PLAYER_CLASS_DESCRIPTOR->restoreOldCommentsPopUpPanels(Z)Z" + ) + restoreOldCommentsPopupPanel = true + } else if (is_7_18_or_greater) { + + // region disable player from being pushed to the top when opening a comment + + mppWatchWhileLayoutFingerprint.methodOrThrow().apply { + val callableIndex = indexOfCallableInstruction(this) + val insertIndex = + indexOfFirstInstructionReversedOrThrow(callableIndex, Opcode.NEW_INSTANCE) + val insertRegister = getInstruction(insertIndex).registerA + + addInstructionsWithLabels( + insertIndex, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->restoreOldCommentsPopUpPanels()Z + move-result v$insertRegister + if-eqz v$insertRegister, :restore + """, ExternalLabel("restore", getInstruction(callableIndex + 1)) + ) + } + + // endregion + + // region region limit the height of the engagement panel + + engagementPanelHeightFingerprint.matchOrThrow(engagementPanelHeightParentFingerprint) + .let { + it.method.apply { + val targetIndex = it.patternMatch!!.endIndex + val targetRegister = + getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + invoke-static {v$targetRegister}, $PLAYER_CLASS_DESCRIPTOR->restoreOldCommentsPopUpPanels(Z)Z + move-result v$targetRegister + """ + ) + } + } + + miniPlayerDefaultViewVisibilityFingerprint.mutableClassOrThrow().let { + it.methods.find { method -> + method.parameters == listOf("Landroid/view/View;", "I") + }?.apply { + val targetIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + opcode == Opcode.INVOKE_INTERFACE && + reference?.returnType == "Z" && + reference.parameterTypes.size == 0 + } + 1 + val targetRegister = + getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + invoke-static {v$targetRegister}, $PLAYER_CLASS_DESCRIPTOR->restoreOldCommentsPopUpPanels(Z)Z + move-result v$targetRegister + """ + ) + } ?: throw PatchException("Could not find targetMethod") + } + + // endregion + + restoreOldCommentsPopupPanel = true + } + + if (restoreOldCommentsPopupPanel) { + addSwitchPreference( + CategoryType.PLAYER, + "revanced_restore_old_comments_popup_panels", + "false" + ) + } + + // endregion + + // region patch for restore old player background + + if (oldPlayerBackgroundFingerprint.resolvable()) { + oldPlayerBackgroundFingerprint.injectLiteralInstructionBooleanCall( + 45415319L, + "$PLAYER_CLASS_DESCRIPTOR->restoreOldPlayerBackground(Z)Z" + ) + addSwitchPreference( + CategoryType.PLAYER, + "revanced_restore_old_player_background", + "false" + ) + } + + // endregion + + // region patch for restore old player layout + + if (oldPlayerLayoutFingerprint.resolvable()) { + oldPlayerLayoutFingerprint.injectLiteralInstructionBooleanCall( + 45399578L, + "$PLAYER_CLASS_DESCRIPTOR->restoreOldPlayerLayout(Z)Z" + ) + addSwitchPreference( + CategoryType.PLAYER, + "revanced_restore_old_player_layout", + "false" + ) + } + + // endregion + + updatePatchStatus(PLAYER_COMPONENTS) + + } +} + +private fun MutableMethod.setInstanceFieldValue( + methodName: String, + viewId: Long +) { + val miniPlayerPlayPauseReplayButtonIndex = + indexOfFirstLiteralInstructionOrThrow(miniPlayerPlayPauseReplayButton) + val miniPlayerPlayPauseReplayButtonRegister = + getInstruction(miniPlayerPlayPauseReplayButtonIndex).registerA + val findViewByIdIndex = + indexOfFirstInstructionOrThrow( + miniPlayerPlayPauseReplayButtonIndex, + Opcode.INVOKE_VIRTUAL + ) + val parentViewRegister = + getInstruction(findViewByIdIndex).registerC + + addInstructions( + miniPlayerPlayPauseReplayButtonIndex, """ + const v$miniPlayerPlayPauseReplayButtonRegister, $viewId + invoke-virtual {v$parentViewRegister, v$miniPlayerPlayPauseReplayButtonRegister}, Landroid/view/View;->findViewById(I)Landroid/view/View; + move-result-object v$miniPlayerPlayPauseReplayButtonRegister + invoke-static {v$miniPlayerPlayPauseReplayButtonRegister}, $PLAYER_CLASS_DESCRIPTOR->$methodName(Landroid/view/View;)V + """ + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/Fingerprints.kt new file mode 100644 index 000000000..2bbf58903 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/Fingerprints.kt @@ -0,0 +1,43 @@ +package app.revanced.patches.music.utils + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val pendingIntentReceiverFingerprint = legacyFingerprint( + name = "pendingIntentReceiverFingerprint", + returnType = "V", + strings = listOf("YTM Dislike", "YTM Next", "YTM Previous"), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/PendingIntentReceiver;") + } +) + +internal val playbackSpeedBottomSheetFingerprint = legacyFingerprint( + name = "playbackSpeedBottomSheetFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + strings = listOf("PLAYBACK_RATE_MENU_BOTTOM_SHEET_FRAGMENT") +) + +internal val playbackSpeedFingerprint = legacyFingerprint( + name = "playbackSpeedFingerprint", + returnType = "V", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.CHECK_CAST, + Opcode.CONST_HIGH16, + Opcode.INVOKE_VIRTUAL + ) +) + +internal val playbackSpeedParentFingerprint = legacyFingerprint( + name = "playbackSpeedParentFingerprint", + returnType = "V", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + parameters = emptyList(), + strings = listOf("BT metadata: %s, %s, %s") +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt new file mode 100644 index 000000000..b964b90bb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.music.utils.compatibility + +import app.revanced.patcher.patch.PackageName +import app.revanced.patcher.patch.VersionName + +internal object Constants { + internal const val YOUTUBE_MUSIC_PACKAGE_NAME = "com.google.android.apps.youtube.music" + + val COMPATIBLE_PACKAGE: Pair?> = Pair( + YOUTUBE_MUSIC_PACKAGE_NAME, + setOf( + "6.20.51", // This is the latest version that supports Android 5.0 + "6.29.59", // This is the latest version that supports the 'Restore old player layout' setting. + "6.42.55", // This is the latest version that supports Android 7.0 + "6.51.53", // This is the latest version of YouTube Music 6.xx.xx + "7.16.53", // This is the latest version that supports the 'Spoof app version' patch. + "7.25.53", // This is the latest version supported by the RVX patch. + ) + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/Constants.kt new file mode 100644 index 000000000..554e0e0dc --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/Constants.kt @@ -0,0 +1,29 @@ +package app.revanced.patches.music.utils.extension + +@Suppress("MemberVisibilityCanBePrivate") +internal object Constants { + const val EXTENSION_PATH = "Lapp/revanced/extension/music" + const val SHARED_PATH = "$EXTENSION_PATH/shared" + const val PATCHES_PATH = "$EXTENSION_PATH/patches" + + const val ACCOUNT_PATH = "$PATCHES_PATH/account" + const val ACTIONBAR_PATH = "$PATCHES_PATH/actionbar" + const val ADS_PATH = "$PATCHES_PATH/ads" + const val COMPONENTS_PATH = "$PATCHES_PATH/components" + const val FLYOUT_PATH = "$PATCHES_PATH/flyout" + const val GENERAL_PATH = "$PATCHES_PATH/general" + const val MISC_PATH = "$PATCHES_PATH/misc" + const val NAVIGATION_PATH = "$PATCHES_PATH/navigation" + const val PLAYER_PATH = "$PATCHES_PATH/player" + const val VIDEO_PATH = "$PATCHES_PATH/video" + const val UTILS_PATH = "$PATCHES_PATH/utils" + + const val ACCOUNT_CLASS_DESCRIPTOR = "$ACCOUNT_PATH/AccountPatch;" + const val ACTIONBAR_CLASS_DESCRIPTOR = "$ACTIONBAR_PATH/ActionBarPatch;" + const val FLYOUT_CLASS_DESCRIPTOR = "$FLYOUT_PATH/FlyoutPatch;" + const val GENERAL_CLASS_DESCRIPTOR = "$GENERAL_PATH/GeneralPatch;" + const val NAVIGATION_CLASS_DESCRIPTOR = "$NAVIGATION_PATH/NavigationPatch;" + const val PLAYER_CLASS_DESCRIPTOR = "$PLAYER_PATH/PlayerPatch;" + + const val PATCH_STATUS_CLASS_DESCRIPTOR = "$UTILS_PATH/PatchStatus;" +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/SharedExtensionPatch.kt new file mode 100644 index 000000000..2151b9af5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/SharedExtensionPatch.kt @@ -0,0 +1,6 @@ +package app.revanced.patches.music.utils.extension + +import app.revanced.patches.music.utils.extension.hooks.applicationInitHook +import app.revanced.patches.shared.extension.sharedExtensionPatch + +val sharedExtensionPatch = sharedExtensionPatch(applicationInitHook) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/hooks/ApplicationInitHook.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/hooks/ApplicationInitHook.kt new file mode 100644 index 000000000..c30a7e1b5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/hooks/ApplicationInitHook.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.music.utils.extension.hooks + +import app.revanced.patches.shared.extension.extensionHook +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val applicationInitHook = extensionHook { + returns("V") + parameters() + strings("activity") + custom { method, _ -> + method.name == "onCreate" && + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL + && getReference()?.name == "getRunningAppProcesses" + } >= 0 + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/androidauto/AndroidAutoCertificatePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/androidauto/AndroidAutoCertificatePatch.kt new file mode 100644 index 000000000..9f178f0f2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/androidauto/AndroidAutoCertificatePatch.kt @@ -0,0 +1,29 @@ +package app.revanced.patches.music.utils.fix.androidauto + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.patch.PatchList.CERTIFICATE_SPOOF +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.util.fingerprint.methodOrThrow + +@Suppress("unused") +val androidAutoCertificatePatch = bytecodePatch( + CERTIFICATE_SPOOF.title, + CERTIFICATE_SPOOF.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + execute { + certificateCheckFingerprint.methodOrThrow().addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + + updatePatchStatus(CERTIFICATE_SPOOF) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/androidauto/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/androidauto/Fingerprints.kt new file mode 100644 index 000000000..28746adfc --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/androidauto/Fingerprints.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.music.utils.fix.androidauto + +import app.revanced.util.fingerprint.legacyFingerprint + +internal val certificateCheckFingerprint = legacyFingerprint( + name = "certificateCheckFingerprint", + returnType = "Z", + parameters = listOf("L"), + strings = listOf("X509") +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/Fingerprints.kt new file mode 100644 index 000000000..fcbbdf9ac --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/Fingerprints.kt @@ -0,0 +1,66 @@ +package app.revanced.patches.music.utils.fix.client + +import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val createPlayerRequestBodyFingerprint = legacyFingerprint( + name = "createPlayerRequestBodyFingerprint", + returnType = "V", + parameters = listOf("L"), + opcodes = listOf( + Opcode.CHECK_CAST, + Opcode.IGET, + Opcode.AND_INT_LIT16, + ), + strings = listOf("ms"), +) + +internal val createPlayerRequestBodyWithVersionReleaseFingerprint = legacyFingerprint( + name = "createPlayerRequestBodyWithVersionReleaseFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + strings = listOf("Google Inc."), + customFingerprint = { method, _ -> + indexOfBuildInstruction(method) >= 0 + }, +) + +fun indexOfBuildInstruction(method: Method) = + method.indexOfFirstInstruction { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.name == "build" && + reference.parameterTypes.isEmpty() && + reference.returnType.startsWith("L") + } + +internal val setPlayerRequestClientTypeFingerprint = legacyFingerprint( + name = "setPlayerRequestClientTypeFingerprint", + opcodes = listOf( + Opcode.IGET, + Opcode.IPUT, // Sets ClientInfo.clientId. + ), + strings = listOf("10.29"), +) + +/** + * This is the fingerprint used in the 'client-spoof' patch around 2022. + * (Integrated into [baseSpoofUserAgentPatch] now.) + * + * This method is modified by [baseSpoofUserAgentPatch], so the fingerprint does not check the [Opcode]. + */ +internal val userAgentHeaderBuilderFingerprint = legacyFingerprint( + name = "userAgentHeaderBuilderFingerprint", + returnType = "Ljava/lang/String;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("Landroid/content/Context;"), + strings = listOf("(Linux; U; Android "), +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt new file mode 100644 index 000000000..af3dc5463 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt @@ -0,0 +1,290 @@ +package app.revanced.patches.music.utils.fix.client + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patches.music.utils.compatibility.Constants +import app.revanced.patches.music.utils.extension.Constants.MISC_PATH +import app.revanced.patches.music.utils.patch.PatchList.SPOOF_CLIENT +import app.revanced.patches.music.utils.playbackSpeedBottomSheetFingerprint +import app.revanced.patches.music.utils.playservice.is_7_25_or_greater +import app.revanced.patches.music.utils.playservice.versionCheckPatch +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addPreferenceWithIntent +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint +import app.revanced.patches.shared.indexOfModelInstruction +import app.revanced.util.Utils.printWarn +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$MISC_PATH/SpoofClientPatch;" +private const val CLIENT_INFO_CLASS_DESCRIPTOR = + "Lcom/google/protos/youtube/api/innertube/InnertubeContext\$ClientInfo;" + +@Suppress("unused") +val spoofClientPatch = bytecodePatch( + SPOOF_CLIENT.title, + SPOOF_CLIENT.summary, + false, +) { + compatibleWith( + Constants.YOUTUBE_MUSIC_PACKAGE_NAME( + "6.20.51", + "6.29.59", + "6.42.55", + "6.51.53", + "7.16.53", + ), + ) + + dependsOn( + settingsPatch, + versionCheckPatch, + ) + + execute { + if (is_7_25_or_greater) { + printWarn("\"${SPOOF_CLIENT.title}\" is not supported in this version. Use YouTube Music 7.24.51 or earlier.") + return@execute + } + + // region Get field references to be used below. + + val (clientInfoField, clientInfoClientTypeField, clientInfoClientVersionField) = + setPlayerRequestClientTypeFingerprint.matchOrThrow().let { result -> + with(result.method) { + // Field in the player request object that holds the client info object. + val clientInfoField = instructions.find { instruction -> + // requestMessage.clientInfo = clientInfoBuilder.build(); + instruction.opcode == Opcode.IPUT_OBJECT && + instruction.getReference()?.type == CLIENT_INFO_CLASS_DESCRIPTOR + }?.getReference() + ?: throw PatchException("Could not find clientInfoField") + + // Client info object's client type field. + val clientInfoClientTypeField = + getInstruction(result.patternMatch!!.endIndex) + .getReference() + ?: throw PatchException("Could not find clientInfoClientTypeField") + + val clientInfoVersionIndex = result.stringMatches!!.first().index + val clientInfoVersionRegister = + getInstruction(clientInfoVersionIndex).registerA + val clientInfoClientVersionFieldIndex = + indexOfFirstInstructionOrThrow(clientInfoVersionIndex) { + opcode == Opcode.IPUT_OBJECT && + (this as TwoRegisterInstruction).registerA == clientInfoVersionRegister + } + + // Client info object's client version field. + val clientInfoClientVersionField = + getInstruction(clientInfoClientVersionFieldIndex) + .getReference() + ?: throw PatchException("Could not find clientInfoClientVersionField") + + Triple(clientInfoField, clientInfoClientTypeField, clientInfoClientVersionField) + } + } + + val clientInfoClientModelField = + with(createPlayerRequestBodyWithModelFingerprint.methodOrThrow()) { + // The next IPUT_OBJECT instruction after getting the client model is setting the client model field. + val clientInfoClientModelIndex = + indexOfFirstInstructionOrThrow(indexOfModelInstruction(this)) { + val reference = getReference() + opcode == Opcode.IPUT_OBJECT && + reference?.definingClass == CLIENT_INFO_CLASS_DESCRIPTOR && + reference.type == "Ljava/lang/String;" + } + getInstruction(clientInfoClientModelIndex).reference + } + + val clientInfoOsVersionField = + with(createPlayerRequestBodyWithVersionReleaseFingerprint.methodOrThrow()) { + val buildIndex = indexOfBuildInstruction(this) + val clientInfoOsVersionIndex = indexOfFirstInstructionOrThrow(buildIndex - 5) { + val reference = getReference() + opcode == Opcode.IPUT_OBJECT && + reference?.definingClass == CLIENT_INFO_CLASS_DESCRIPTOR && + reference.type == "Ljava/lang/String;" + } + getInstruction(clientInfoOsVersionIndex).reference + } + + // endregion + + // region Spoof client type for /player requests. + + createPlayerRequestBodyFingerprint.matchOrThrow().let { + it.method.apply { + val setClientInfoMethodName = "setClientInfo" + val checkCastIndex = it.patternMatch!!.startIndex + + val checkCastInstruction = getInstruction(checkCastIndex) + val requestMessageInstanceRegister = checkCastInstruction.registerA + val clientInfoContainerClassName = + checkCastInstruction.getReference()!!.type + + addInstruction( + checkCastIndex + 1, + "invoke-static { v$requestMessageInstanceRegister }," + + " $definingClass->$setClientInfoMethodName($clientInfoContainerClassName)V", + ) + + // Change client info to use the spoofed values. + // Do this in a helper method, to remove the need of picking out multiple free registers from the hooked code. + it.classDef.methods.add( + ImmutableMethod( + definingClass, + setClientInfoMethodName, + listOf( + ImmutableMethodParameter( + clientInfoContainerClassName, + annotations, + "clientInfoContainer" + ) + ), + "V", + AccessFlags.PRIVATE or AccessFlags.STATIC, + annotations, + null, + MutableMethodImplementation(3), + ).toMutable().apply { + addInstructions( + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isClientSpoofingEnabled()Z + move-result v0 + if-eqz v0, :disabled + + iget-object v0, p0, $clientInfoField + + # Set client type to the spoofed value. + iget v1, v0, $clientInfoClientTypeField + invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getClientTypeId(I)I + move-result v1 + iput v1, v0, $clientInfoClientTypeField + + # Set client model to the spoofed value. + iget-object v1, v0, $clientInfoClientModelField + invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getClientModel(Ljava/lang/String;)Ljava/lang/String; + move-result-object v1 + iput-object v1, v0, $clientInfoClientModelField + + # Set client version to the spoofed value. + iget-object v1, v0, $clientInfoClientVersionField + invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getClientVersion(Ljava/lang/String;)Ljava/lang/String; + move-result-object v1 + iput-object v1, v0, $clientInfoClientVersionField + + # Set client os version to the spoofed value. + iget-object v1, v0, $clientInfoOsVersionField + invoke-static { v1 }, $EXTENSION_CLASS_DESCRIPTOR->getOsVersion(Ljava/lang/String;)Ljava/lang/String; + move-result-object v1 + iput-object v1, v0, $clientInfoOsVersionField + + :disabled + return-void + """, + ) + }, + ) + } + } + + // endregion + + // region Spoof user-agent + + userAgentHeaderBuilderFingerprint.methodOrThrow().apply { + val insertIndex = implementation!!.instructions.lastIndex + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static { v$insertRegister }, $EXTENSION_CLASS_DESCRIPTOR->getUserAgent(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$insertRegister + """ + ) + } + + // endregion + + // region fix for playback speed menu is not available in Podcasts + + playbackSpeedBottomSheetFingerprint.mutableClassOrThrow().let { + val onItemClickMethod = + it.methods.find { method -> method.name == "onItemClick" } + ?: throw PatchException("Failed to find onItemClick method") + + onItemClickMethod.apply { + val createPlaybackSpeedMenuItemIndex = indexOfFirstInstructionReversedOrThrow { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.returnType == "V" && + reference.parameterTypes.firstOrNull()?.startsWith("[L") == true + } + val createPlaybackSpeedMenuItemMethod = + getWalkerMethod(createPlaybackSpeedMenuItemIndex) + createPlaybackSpeedMenuItemMethod.apply { + val shouldCreateMenuIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.returnType == "Z" && + reference.parameterTypes.isEmpty() + } + 2 + val shouldCreateMenuRegister = + getInstruction(shouldCreateMenuIndex - 1).registerA + + addInstructions( + shouldCreateMenuIndex, + """ + invoke-static { v$shouldCreateMenuRegister }, $EXTENSION_CLASS_DESCRIPTOR->forceCreatePlaybackSpeedMenu(Z)Z + move-result v$shouldCreateMenuRegister + """, + ) + } + } + } + + // endregion + + addSwitchPreference( + CategoryType.MISC, + "revanced_spoof_client", + "false" + ) + addPreferenceWithIntent( + CategoryType.MISC, + "revanced_spoof_client_type", + "revanced_spoof_client", + ) + + updatePatchStatus(SPOOF_CLIENT) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/fileprovider/FileProviderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/fileprovider/FileProviderPatch.kt new file mode 100644 index 000000000..1def915a1 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/fileprovider/FileProviderPatch.kt @@ -0,0 +1,45 @@ +package app.revanced.patches.music.utils.fix.fileprovider + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.util.fingerprint.methodOrThrow + +fun fileProviderPatch( + youtubePackageName: String, + musicPackageName: String +) = bytecodePatch( + description = "fileProviderPatch" +) { + execute { + + /** + * For some reason, if the app gets "android.support.FILE_PROVIDER_PATHS", + * the package name of YouTube is used, not the package name of the YT Music. + * + * There is no issue in the stock YT Music, but this is an issue in the GmsCore Build. + * https://github.com/inotia00/ReVanced_Extended/issues/1830 + * + * To solve this issue, replace the package name of YouTube with YT Music's package name. + */ + fileProviderResolverFingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, """ + const-string v0, "com.google.android.youtube.fileprovider" + invoke-static {p1, v0}, Ljava/util/Objects;->equals(Ljava/lang/Object;Ljava/lang/Object;)Z + move-result v0 + if-nez v0, :fix + const-string v0, "$youtubePackageName.fileprovider" + invoke-static {p1, v0}, Ljava/util/Objects;->equals(Ljava/lang/Object;Ljava/lang/Object;)Z + move-result v0 + if-nez v0, :fix + goto :ignore + :fix + const-string p1, "$musicPackageName.fileprovider" + """, ExternalLabel("ignore", getInstruction(0)) + ) + } + + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/fileprovider/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/fileprovider/Fingerprints.kt new file mode 100644 index 000000000..44e5d72ff --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/fileprovider/Fingerprints.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.music.utils.fix.fileprovider + +import app.revanced.util.fingerprint.legacyFingerprint + +internal val fileProviderResolverFingerprint = legacyFingerprint( + name = "fileProviderResolverFingerprint", + returnType = "L", + strings = listOf( + "android.support.FILE_PROVIDER_PATHS", + "Name must not be empty" + ) +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt new file mode 100644 index 000000000..a37eeeeb8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/streamingdata/SpoofStreamingDataPatch.kt @@ -0,0 +1,45 @@ +package app.revanced.patches.music.utils.fix.streamingdata + +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.compatibility.Constants.YOUTUBE_MUSIC_PACKAGE_NAME +import app.revanced.patches.music.utils.patch.PatchList.SPOOF_STREAMING_DATA +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addPreferenceWithIntent +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.spoof.streamingdata.baseSpoofStreamingDataPatch +import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch + +@Suppress("unused") +val spoofStreamingDataPatch = baseSpoofStreamingDataPatch( + { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseSpoofUserAgentPatch(YOUTUBE_MUSIC_PACKAGE_NAME), + settingsPatch, + ) + }, + { + addSwitchPreference( + CategoryType.MISC, + "revanced_spoof_streaming_data", + "true" + ) + addPreferenceWithIntent( + CategoryType.MISC, + "revanced_spoof_streaming_data_type", + "revanced_spoof_streaming_data" + ) + addSwitchPreference( + CategoryType.MISC, + "revanced_spoof_streaming_data_stats_for_nerds", + "true", + "revanced_spoof_streaming_data" + ) + + updatePatchStatus(SPOOF_STREAMING_DATA) + + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/flyoutmenu/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/flyoutmenu/Fingerprints.kt new file mode 100644 index 000000000..17366b163 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/flyoutmenu/Fingerprints.kt @@ -0,0 +1,14 @@ +package app.revanced.patches.music.utils.flyoutmenu + +import app.revanced.patches.music.utils.resourceid.varispeedUnavailableTitle +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val playbackRateBottomSheetClassFingerprint = legacyFingerprint( + name = "playbackRateBottomSheetClassFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(varispeedUnavailableTitle) +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/flyoutmenu/FlyoutMenuHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/flyoutmenu/FlyoutMenuHookPatch.kt new file mode 100644 index 000000000..c2d7b0c8f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/flyoutmenu/FlyoutMenuHookPatch.kt @@ -0,0 +1,38 @@ +package app.revanced.patches.music.utils.flyoutmenu + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.extension.Constants.EXTENSION_PATH +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.util.addStaticFieldToExtension +import app.revanced.util.fingerprint.methodOrThrow + +private const val EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR = + "$EXTENSION_PATH/utils/VideoUtils;" + +val flyoutMenuHookPatch = bytecodePatch( + description = "flyoutMenuHookPatch", +) { + dependsOn(sharedResourceIdPatch) + + execute { + + playbackRateBottomSheetClassFingerprint.methodOrThrow().apply { + val smaliInstructions = + """ + if-eqz v0, :ignore + invoke-virtual {v0}, $definingClass->$name()V + :ignore + return-void + """ + + addStaticFieldToExtension( + EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR, + "showPlaybackSpeedFlyoutMenu", + "playbackRateBottomSheetClass", + definingClass, + smaliInstructions + ) + } + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/gms/GmsCoreSupportPatch.kt new file mode 100644 index 000000000..cb7dcd7fd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/gms/GmsCoreSupportPatch.kt @@ -0,0 +1,68 @@ +package app.revanced.patches.music.utils.gms + +import app.revanced.patcher.patch.Option +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.compatibility.Constants.YOUTUBE_MUSIC_PACKAGE_NAME +import app.revanced.patches.music.utils.extension.sharedExtensionPatch +import app.revanced.patches.music.utils.fix.fileprovider.fileProviderPatch +import app.revanced.patches.music.utils.mainactivity.mainActivityFingerprint +import app.revanced.patches.music.utils.patch.PatchList.GMSCORE_SUPPORT +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.addGmsCorePreference +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePackageName +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.gms.gmsCoreSupportPatch +import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch +import app.revanced.util.valueOrThrow + +@Suppress("unused") +val gmsCoreSupportPatch = gmsCoreSupportPatch( + fromPackageName = YOUTUBE_MUSIC_PACKAGE_NAME, + mainActivityOnCreateFingerprint = mainActivityFingerprint.second, + extensionPatch = sharedExtensionPatch, + gmsCoreSupportResourcePatchFactory = ::gmsCoreSupportResourcePatch, +) { + compatibleWith(COMPATIBLE_PACKAGE) +} + +private fun gmsCoreSupportResourcePatch( + gmsCoreVendorGroupIdOption: Option, + packageNameYouTubeOption: Option, + packageNameYouTubeMusicOption: Option, +) = app.revanced.patches.shared.gms.gmsCoreSupportResourcePatch( + fromPackageName = YOUTUBE_MUSIC_PACKAGE_NAME, + spoofedPackageSignature = "afb0fed5eeaebdd86f56a97742f4b6b33ef59875", + gmsCoreVendorGroupIdOption = gmsCoreVendorGroupIdOption, + packageNameYouTubeOption = packageNameYouTubeOption, + packageNameYouTubeMusicOption = packageNameYouTubeMusicOption, + executeBlock = { + updatePackageName(packageNameYouTubeMusicOption.valueOrThrow()) + + addGmsCorePreference( + CategoryType.MISC.value, + "gms_core_settings", + gmsCoreVendorGroupIdOption.valueOrThrow() + ".android.gms", + "org.microg.gms.ui.SettingsActivity" + ) + + addSwitchPreference( + CategoryType.MISC, + "revanced_gms_show_dialog", + "true" + ) + + updatePatchStatus(GMSCORE_SUPPORT) + + }, +) { + dependsOn( + baseSpoofUserAgentPatch(YOUTUBE_MUSIC_PACKAGE_NAME), + settingsPatch, + fileProviderPatch( + packageNameYouTubeOption.valueOrThrow(), + packageNameYouTubeMusicOption.valueOrThrow() + ), + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/mainactivity/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/mainactivity/Fingerprints.kt new file mode 100644 index 000000000..e80b3f804 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/mainactivity/Fingerprints.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.music.utils.mainactivity + +import app.revanced.util.fingerprint.legacyFingerprint + +internal val mainActivityFingerprint = legacyFingerprint( + name = "mainActivityFingerprint", + returnType = "V", + parameters = listOf("Landroid/os/Bundle;"), + strings = listOf( + "android.intent.action.MAIN", + "FEmusic_home" + ), + customFingerprint = { method, classDef -> + method.name == "onCreate" && classDef.endsWith("Activity;") + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/mainactivity/MainActivityResolvePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/mainactivity/MainActivityResolvePatch.kt new file mode 100644 index 000000000..e1cf54b3e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/mainactivity/MainActivityResolvePatch.kt @@ -0,0 +1,5 @@ +package app.revanced.patches.music.utils.mainactivity + +import app.revanced.patches.shared.mainactivity.baseMainActivityResolvePatch + +val mainActivityResolvePatch = baseMainActivityResolvePatch(mainActivityFingerprint) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt new file mode 100644 index 000000000..c814a7d97 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt @@ -0,0 +1,168 @@ +package app.revanced.patches.music.utils.patch + +internal enum class PatchList( + val title: String, + val summary: String, + var included: Boolean? = false +) { + AMOLED( + "Amoled", + "Applies a pure black theme to some components." + ), + BITRATE_DEFAULT_VALUE( + "Bitrate default value", + "Sets the audio quality to 'Always High' when you first install the app." + ), + BYPASS_IMAGE_REGION_RESTRICTIONS( + "Bypass image region restrictions", + "Adds an option to use a different host for static images, so that images blocked in some countries can be received." + ), + CERTIFICATE_SPOOF( + "Certificate spoof", + "Enables YouTube Music to work with Android Auto by spoofing the YouTube Music certificate." + ), + CHANGE_SHARE_SHEET( + "Change share sheet", + "Add option to change from in-app share sheet to system share sheet." + ), + CHANGE_START_PAGE( + "Change start page", + "Adds an option to set which page the app opens in instead of the homepage." + ), + CUSTOM_BRANDING_ICON_FOR_YOUTUBE_MUSIC( + "Custom branding icon for YouTube Music", + "Changes the YouTube Music app icon to the icon specified in patch options." + ), + CUSTOM_BRANDING_NAME_FOR_YOUTUBE_MUSIC( + "Custom branding name for YouTube Music", + "Renames the YouTube Music app to the name specified in patch options." + ), + CUSTOM_HEADER_FOR_YOUTUBE_MUSIC( + "Custom header for YouTube Music", + "Applies a custom header in the top left corner within the app." + ), + DISABLE_CAIRO_SPLASH_ANIMATION( + "Disable Cairo splash animation", + "Adds an option to disable Cairo splash animation." + ), + DISABLE_DRC_AUDIO( + "Disable DRC audio", + "Adds an option to disable DRC (Dynamic Range Compression) audio." + ), + DISABLE_AUTO_CAPTIONS( + "Disable auto captions", + "Adds an option to disable captions from being automatically enabled." + ), + DISABLE_DISLIKE_REDIRECTION( + "Disable dislike redirection", + "Adds an option to disable redirection to the next track when clicking the Dislike button." + ), + ENABLE_OPUS_CODEC( + "Enable OPUS codec", + "Adds an options to enable the OPUS audio codec if the player response includes." + ), + ENABLE_DEBUG_LOGGING( + "Enable debug logging", + "Adds an option to enable debug logging." + ), + ENABLE_LANDSCAPE_MODE( + "Enable landscape mode", + "Adds an option to enable landscape mode when rotating the screen on phones." + ), + FLYOUT_MENU_COMPONENTS( + "Flyout menu components", + "Adds options to hide or change flyout menu components." + ), + GMSCORE_SUPPORT( + "GmsCore support", + "Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services." + ), + HIDE_ACCOUNT_COMPONENTS( + "Hide account components", + "Adds options to hide components related to the account menu." + ), + HIDE_ACTION_BAR_COMPONENTS( + "Hide action bar components", + "Adds options to hide action bar components and replace the offline download button with an external download button." + ), + HIDE_ADS( + "Hide ads", + "Adds options to hide ads." + ), + HIDE_LAYOUT_COMPONENTS( + "Hide layout components", + "Adds options to hide general layout components." + ), + HIDE_OVERLAY_FILTER( + "Hide overlay filter", + "Removes, at compile time, the dark overlay that appears when player flyout menus are open." + ), + HIDE_PLAYER_OVERLAY_FILTER( + "Hide player overlay filter", + "Removes, at compile time, the dark overlay that appears when single-tapping in the player." + ), + NAVIGATION_BAR_COMPONENTS( + "Navigation bar components", + "Adds options to hide or change components related to the navigation bar." + ), + PLAYER_COMPONENTS( + "Player components", + "Adds options to hide or change components related to the player." + ), + REMOVE_BACKGROUND_PLAYBACK_RESTRICTIONS( + "Remove background playback restrictions", + "Removes restrictions on background playback, including for kids videos." + ), + REMOVE_VIEWER_DISCRETION_DIALOG( + "Remove viewer discretion dialog", + "Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction." + ), + RESTORE_OLD_STYLE_LIBRARY_SHELF( + "Restore old style library shelf", + "Adds an option to return the Library tab to the old style." + ), + RETURN_YOUTUBE_DISLIKE( + "Return YouTube Dislike", + "Adds an option to show the dislike count of songs using the Return YouTube Dislike API." + ), + RETURN_YOUTUBE_USERNAME( + "Return YouTube Username", + "Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3." + ), + SANITIZE_SHARING_LINKS( + "Sanitize sharing links", + "Adds an option to remove tracking query parameters from URLs when sharing links." + ), + SETTINGS_FOR_YOUTUBE_MUSIC( + "Settings for YouTube Music", + "Applies mandatory patches to implement ReVanced Extended settings into the application." + ), + SPONSORBLOCK( + "SponsorBlock", + "Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as non-music sections." + ), + SPOOF_APP_VERSION( + "Spoof app version", + "Adds options to spoof the YouTube Music client version. This can remove the radio mode restriction in Canadian regions or disable real-time lyrics." + ), + SPOOF_CLIENT( + "Spoof client", + "Adds options to spoof the client to allow playback." + ), + SPOOF_STREAMING_DATA( + "Spoof streaming data", + "Adds options to spoof the streaming data to allow playback." + ), + TRANSLATIONS_FOR_YOUTUBE_MUSIC( + "Translations for YouTube Music", + "Add translations or remove string resources." + ), + VIDEO_PLAYBACK( + "Video playback", + "Adds options to customize settings related to video playback, such as default video quality and playback speed." + ), + VISUAL_PREFERENCES_ICONS_FOR_YOUTUBE_MUSIC( + "Visual preferences icons for YouTube Music", + "Adds icons to specific preferences in the settings." + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/playertype/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/playertype/Fingerprints.kt new file mode 100644 index 000000000..8c8847ebb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/playertype/Fingerprints.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.music.utils.playertype + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val playerTypeFingerprint = legacyFingerprint( + name = "playerTypeFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.IGET_BOOLEAN, + Opcode.IF_NEZ, + Opcode.IPUT_OBJECT, + Opcode.RETURN_VOID + ), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/MppWatchWhileLayout;") + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/playertype/PlayerTypeHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/playertype/PlayerTypeHookPatch.kt new file mode 100644 index 000000000..f630ee42e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/playertype/PlayerTypeHookPatch.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.music.utils.playertype + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.extension.Constants.UTILS_PATH +import app.revanced.util.fingerprint.methodOrThrow + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$UTILS_PATH/PlayerTypeHookPatch;" + +@Suppress("unused") +val playerTypeHookPatch = bytecodePatch( + description = "playerTypeHookPatch" +) { + + execute { + + playerTypeFingerprint.methodOrThrow().addInstruction( + 0, + "invoke-static {p1}, $EXTENSION_CLASS_DESCRIPTOR->setPlayerType(Ljava/lang/Enum;)V" + ) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/playservice/VersionCheckPatch.kt new file mode 100644 index 000000000..372251b2b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/playservice/VersionCheckPatch.kt @@ -0,0 +1,54 @@ +@file:Suppress("ktlint:standard:property-naming") + +package app.revanced.patches.music.utils.playservice + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.util.findElementByAttributeValueOrThrow + +var is_6_27_or_greater = false + private set +var is_6_36_or_greater = false + private set +var is_6_42_or_greater = false + private set +var is_7_06_or_greater = false + private set +var is_7_13_or_greater = false + private set +var is_7_17_or_greater = false + private set +var is_7_18_or_greater = false + private set +var is_7_20_or_greater = false + private set +var is_7_23_or_greater = false + private set +var is_7_25_or_greater = false + private set + +val versionCheckPatch = resourcePatch( + description = "versionCheckPatch", +) { + execute { + // The app version is missing from the decompiled manifest, + // so instead use the Google Play services version and compare against specific releases. + val playStoreServicesVersion = document("res/values/integers.xml").use { document -> + document.documentElement.childNodes.findElementByAttributeValueOrThrow( + "name", + "google_play_services_version", + ).textContent.toInt() + } + + // All bug fix releases always seem to use the same play store version as the minor version. + is_6_27_or_greater = 234412000 <= playStoreServicesVersion + is_6_36_or_greater = 240399000 <= playStoreServicesVersion + is_6_42_or_greater = 240999000 <= playStoreServicesVersion + is_7_06_or_greater = 242499000 <= playStoreServicesVersion + is_7_13_or_greater = 243199000 <= playStoreServicesVersion + is_7_17_or_greater = 243530000 <= playStoreServicesVersion + is_7_18_or_greater = 243699000 <= playStoreServicesVersion + is_7_20_or_greater = 243899000 <= playStoreServicesVersion + is_7_23_or_greater = 244199000 <= playStoreServicesVersion + is_7_25_or_greater = 244399000 <= playStoreServicesVersion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt new file mode 100644 index 000000000..a3358901f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/resourceid/SharedResourceIdPatch.kt @@ -0,0 +1,269 @@ +package app.revanced.patches.music.utils.resourceid + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.shared.mapping.ResourceType.BOOL +import app.revanced.patches.shared.mapping.ResourceType.COLOR +import app.revanced.patches.shared.mapping.ResourceType.DIMEN +import app.revanced.patches.shared.mapping.ResourceType.ID +import app.revanced.patches.shared.mapping.ResourceType.LAYOUT +import app.revanced.patches.shared.mapping.ResourceType.STRING +import app.revanced.patches.shared.mapping.ResourceType.STYLE +import app.revanced.patches.shared.mapping.get +import app.revanced.patches.shared.mapping.resourceMappingPatch +import app.revanced.patches.shared.mapping.resourceMappings + +var accountSwitcherAccessibility = -1L + private set +var bottomSheetRecyclerView = -1L + private set +var buttonContainer = -1L + private set +var buttonIconPaddingMedium = -1L + private set +var chipCloud = -1L + private set +var colorGrey = -1L + private set +var darkBackground = -1L + private set +var designBottomSheetDialog = -1L + private set +var endButtonsContainer = -1L + private set +var floatingLayout = -1L + private set +var historyMenuItem = -1L + private set +var inlineTimeBarAdBreakMarkerColor = -1L + private set +var interstitialsContainer = -1L + private set +var isTablet = -1L + private set +var likeDislikeContainer = -1L + private set +var mainActivityLaunchAnimation = -1L + private set +var menuEntry = -1L + private set +var miniPlayerDefaultText = -1L + private set +var miniPlayerMdxPlaying = -1L + private set +var miniPlayerPlayPauseReplayButton = -1L + private set +var miniPlayerViewPager = -1L + private set +var musicNotifierShelf = -1L + private set +var musicTasteBuilderShelf = -1L + private set +var namesInactiveAccountThumbnailSize = -1L + private set +var offlineSettingsMenuItem = -1L + private set +var playerOverlayChip = -1L + private set +var playerViewPager = -1L + private set +var privacyTosFooter = -1L + private set +var qualityAuto = -1L + private set +var remixGenericButtonSize = -1L + private set +var slidingDialogAnimation = -1L + private set +var tapBloomView = -1L + private set +var text1 = -1L + private set +var toolTipContentView = -1L + private set +var topEnd = -1L + private set +var topStart = -1L + private set +var topBarMenuItemImageView = -1L + private set +var tosFooter = -1L + private set +var touchOutside = -1L + private set +var trimSilenceSwitch = -1L + private set +var varispeedUnavailableTitle = -1L + private set + +internal val sharedResourceIdPatch = resourcePatch( + description = "sharedResourceIdPatch" +) { + dependsOn(resourceMappingPatch) + + execute { + accountSwitcherAccessibility = resourceMappings[ + STRING, + "account_switcher_accessibility_label", + ] + bottomSheetRecyclerView = resourceMappings[ + LAYOUT, + "bottom_sheet_recycler_view" + ] + buttonContainer = resourceMappings[ + ID, + "button_container" + ] + buttonIconPaddingMedium = resourceMappings[ + DIMEN, + "button_icon_padding_medium" + ] + chipCloud = resourceMappings[ + LAYOUT, + "chip_cloud" + ] + colorGrey = resourceMappings[ + COLOR, + "ytm_color_grey_12" + ] + darkBackground = resourceMappings[ + ID, + "dark_background" + ] + designBottomSheetDialog = resourceMappings[ + LAYOUT, + "design_bottom_sheet_dialog" + ] + endButtonsContainer = resourceMappings[ + ID, + "end_buttons_container" + ] + floatingLayout = resourceMappings[ + ID, + "floating_layout" + ] + historyMenuItem = resourceMappings[ + ID, + "history_menu_item" + ] + inlineTimeBarAdBreakMarkerColor = resourceMappings[ + COLOR, + "inline_time_bar_ad_break_marker_color" + ] + interstitialsContainer = resourceMappings[ + ID, + "interstitials_container" + ] + isTablet = resourceMappings[ + BOOL, + "is_tablet" + ] + likeDislikeContainer = resourceMappings[ + ID, + "like_dislike_container" + ] + mainActivityLaunchAnimation = resourceMappings[ + LAYOUT, + "main_activity_launch_animation" + ] + menuEntry = resourceMappings[ + LAYOUT, + "menu_entry" + ] + miniPlayerDefaultText = resourceMappings[ + STRING, + "mini_player_default_text" + ] + miniPlayerMdxPlaying = resourceMappings[ + STRING, + "mini_player_mdx_playing" + ] + miniPlayerPlayPauseReplayButton = resourceMappings[ + ID, + "mini_player_play_pause_replay_button" + ] + miniPlayerViewPager = resourceMappings[ + ID, + "mini_player_view_pager" + ] + musicNotifierShelf = resourceMappings[ + LAYOUT, + "music_notifier_shelf" + ] + musicTasteBuilderShelf = resourceMappings[ + LAYOUT, + "music_tastebuilder_shelf" + ] + namesInactiveAccountThumbnailSize = resourceMappings[ + DIMEN, + "names_inactive_account_thumbnail_size" + ] + offlineSettingsMenuItem = resourceMappings[ + ID, + "offline_settings_menu_item" + ] + playerOverlayChip = resourceMappings[ + ID, + "player_overlay_chip" + ] + playerViewPager = resourceMappings[ + ID, + "player_view_pager" + ] + privacyTosFooter = resourceMappings[ + ID, + "privacy_tos_footer" + ] + qualityAuto = resourceMappings[ + STRING, + "quality_auto" + ] + remixGenericButtonSize = resourceMappings[ + DIMEN, + "remix_generic_button_size" + ] + slidingDialogAnimation = resourceMappings[ + STYLE, + "SlidingDialogAnimation" + ] + tapBloomView = resourceMappings[ + ID, + "tap_bloom_view" + ] + text1 = resourceMappings[ + ID, + "text1" + ] + toolTipContentView = resourceMappings[ + LAYOUT, + "tooltip_content_view" + ] + topEnd = resourceMappings[ + ID, + "TOP_END" + ] + topStart = resourceMappings[ + ID, + "TOP_START" + ] + topBarMenuItemImageView = resourceMappings[ + ID, + "top_bar_menu_item_image_view" + ] + tosFooter = resourceMappings[ + ID, + "tos_footer" + ] + touchOutside = resourceMappings[ + ID, + "touch_outside" + ] + trimSilenceSwitch = resourceMappings[ + ID, + "trim_silence_switch" + ] + varispeedUnavailableTitle = resourceMappings[ + STRING, + "varispeed_unavailable_title" + ] + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubedislike/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubedislike/Fingerprints.kt new file mode 100644 index 000000000..d1c803c70 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubedislike/Fingerprints.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.music.utils.returnyoutubedislike + +import app.revanced.patches.music.utils.resourceid.buttonIconPaddingMedium +import app.revanced.util.fingerprint.legacyFingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val textComponentFingerprint = legacyFingerprint( + name = "textComponentFingerprint", + returnType = "V", + opcodes = listOf(Opcode.CONST_HIGH16), + literals = listOf(buttonIconPaddingMedium), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubedislike/ReturnYouTubeDislikePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubedislike/ReturnYouTubeDislikePatch.kt new file mode 100644 index 000000000..eb4a694e2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubedislike/ReturnYouTubeDislikePatch.kt @@ -0,0 +1,170 @@ +package app.revanced.patches.music.utils.returnyoutubedislike + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.music.utils.patch.PatchList.RETURN_YOUTUBE_DISLIKE +import app.revanced.patches.music.utils.playservice.is_7_17_or_greater +import app.revanced.patches.music.utils.playservice.is_7_25_or_greater +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.PREFERENCE_CATEGORY_TAG_NAME +import app.revanced.patches.music.utils.settings.ResourceUtils.SETTINGS_HEADER_PATH +import app.revanced.patches.music.utils.settings.ResourceUtils.addPreferenceCategoryUnderPreferenceScreen +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.music.video.information.videoIdHook +import app.revanced.patches.music.video.information.videoInformationPatch +import app.revanced.patches.shared.dislikeFingerprint +import app.revanced.patches.shared.likeFingerprint +import app.revanced.patches.shared.removeLikeFingerprint +import app.revanced.patches.shared.textcomponent.hookSpannableString +import app.revanced.patches.shared.textcomponent.textComponentPatch +import app.revanced.util.adoptChild +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import org.w3c.dom.Element + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$UTILS_PATH/ReturnYouTubeDislikePatch;" + +private val returnYouTubeDislikeBytecodePatch = bytecodePatch( + description = "returnYouTubeDislikeBytecodePatch" +) { + dependsOn( + settingsPatch, + sharedResourceIdPatch, + videoInformationPatch, + textComponentPatch, + ) + + execute { + + mapOf( + likeFingerprint to Vote.LIKE, + dislikeFingerprint to Vote.DISLIKE, + removeLikeFingerprint to Vote.REMOVE_LIKE, + ).forEach { (fingerprint, vote) -> + fingerprint.methodOrThrow().addInstructions( + 0, + """ + const/4 v0, ${vote.value} + invoke-static {v0}, $EXTENSION_CLASS_DESCRIPTOR->sendVote(I)V + """, + ) + } + + if (!is_7_25_or_greater) { + textComponentFingerprint.methodOrThrow().apply { + val insertIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_STATIC + && (this as ReferenceInstruction).reference.toString() + .endsWith("Ljava/lang/CharSequence;") + } + 2 + val insertRegister = + getInstruction(insertIndex - 1).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $EXTENSION_CLASS_DESCRIPTOR->onSpannedCreated(Landroid/text/Spanned;)Landroid/text/Spanned; + move-result-object v$insertRegister + """ + ) + } + } + + if (is_7_17_or_greater) { + hookSpannableString(EXTENSION_CLASS_DESCRIPTOR, "onLithoTextLoaded") + } + + videoIdHook("$EXTENSION_CLASS_DESCRIPTOR->newVideoLoaded(Ljava/lang/String;)V") + + } +} + +enum class Vote(val value: Int) { + LIKE(1), + DISLIKE(-1), + REMOVE_LIKE(0), +} + +private const val ABOUT_CATEGORY_KEY = "revanced_ryd_about" +private const val RYD_ATTRIBUTION_KEY = "revanced_ryd_attribution" + +@Suppress("unused") +val returnYouTubeDislikePatch = resourcePatch( + RETURN_YOUTUBE_DISLIKE.title, + RETURN_YOUTUBE_DISLIKE.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + returnYouTubeDislikeBytecodePatch, + settingsPatch, + ) + + execute { + addSwitchPreference( + CategoryType.RETURN_YOUTUBE_DISLIKE, + "revanced_ryd_enabled", + "true" + ) + addSwitchPreference( + CategoryType.RETURN_YOUTUBE_DISLIKE, + "revanced_ryd_dislike_percentage", + "false", + "revanced_ryd_enabled" + ) + addSwitchPreference( + CategoryType.RETURN_YOUTUBE_DISLIKE, + "revanced_ryd_compact_layout", + "false", + "revanced_ryd_enabled" + ) + addSwitchPreference( + CategoryType.RETURN_YOUTUBE_DISLIKE, + "revanced_ryd_estimated_like", + "false", + "revanced_ryd_enabled" + ) + addSwitchPreference( + CategoryType.RETURN_YOUTUBE_DISLIKE, + "revanced_ryd_toast_on_connection_error", + "false", + "revanced_ryd_enabled" + ) + + addPreferenceCategoryUnderPreferenceScreen( + CategoryType.RETURN_YOUTUBE_DISLIKE.value, + ABOUT_CATEGORY_KEY + ) + + document(SETTINGS_HEADER_PATH).use { document -> + val tags = document.getElementsByTagName(PREFERENCE_CATEGORY_TAG_NAME) + List(tags.length) { tags.item(it) as Element } + .filter { it.getAttribute("android:key").contains(ABOUT_CATEGORY_KEY) } + .forEach { + it.adoptChild("Preference") { + setAttribute("android:title", "@string/$RYD_ATTRIBUTION_KEY" + "_title") + setAttribute("android:summary", "@string/$RYD_ATTRIBUTION_KEY" + "_summary") + setAttribute("android:key", RYD_ATTRIBUTION_KEY) + this.adoptChild("intent") { + setAttribute("android:action", "android.intent.action.VIEW") + setAttribute("android:data", "https://returnyoutubedislike.com") + } + } + } + } + + updatePatchStatus(RETURN_YOUTUBE_DISLIKE) + + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubeusername/ReturnYouTubeUsernamePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubeusername/ReturnYouTubeUsernamePatch.kt new file mode 100644 index 000000000..0adbde7d3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubeusername/ReturnYouTubeUsernamePatch.kt @@ -0,0 +1,54 @@ +package app.revanced.patches.music.utils.returnyoutubeusername + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.patch.PatchList.RETURN_YOUTUBE_USERNAME +import app.revanced.patches.music.utils.playservice.is_6_42_or_greater +import app.revanced.patches.music.utils.playservice.versionCheckPatch +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addPreferenceWithIntent +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.shared.returnyoutubeusername.baseReturnYouTubeUsernamePatch + +@Suppress("unused") +val returnYouTubeUsernamePatch = resourcePatch( + RETURN_YOUTUBE_USERNAME.title, + RETURN_YOUTUBE_USERNAME.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseReturnYouTubeUsernamePatch, + settingsPatch, + versionCheckPatch, + ) + + execute { + addSwitchPreference( + CategoryType.RETURN_YOUTUBE_USERNAME, + "revanced_return_youtube_username_enabled", + "false" + ) + addPreferenceWithIntent( + CategoryType.RETURN_YOUTUBE_USERNAME, + "revanced_return_youtube_username_display_format", + "revanced_return_youtube_username_enabled" + ) + addPreferenceWithIntent( + CategoryType.RETURN_YOUTUBE_USERNAME, + "revanced_return_youtube_username_youtube_data_api_v3_developer_key", + "revanced_return_youtube_username_enabled" + ) + if (is_6_42_or_greater) { + addPreferenceWithIntent( + CategoryType.RETURN_YOUTUBE_USERNAME, + "revanced_return_youtube_username_youtube_data_api_v3_about" + ) + } + + updatePatchStatus(RETURN_YOUTUBE_USERNAME) + + } +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/music/utils/settings/CategoryType.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/settings/CategoryType.kt similarity index 86% rename from src/main/kotlin/app/revanced/patches/music/utils/settings/CategoryType.kt rename to patches/src/main/kotlin/app/revanced/patches/music/utils/settings/CategoryType.kt index 3301aa5a2..80d07a410 100644 --- a/src/main/kotlin/app/revanced/patches/music/utils/settings/CategoryType.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/settings/CategoryType.kt @@ -1,6 +1,6 @@ package app.revanced.patches.music.utils.settings -enum class CategoryType(val value: String, var added: Boolean) { +internal enum class CategoryType(val value: String, var added: Boolean) { GENERAL("general", false), ACCOUNT("account", false), ACTION_BAR("action_bar", false), diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/settings/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/settings/Fingerprints.kt new file mode 100644 index 000000000..543e8ab35 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/settings/Fingerprints.kt @@ -0,0 +1,47 @@ +package app.revanced.patches.music.utils.settings + +import app.revanced.util.fingerprint.legacyFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val googleApiActivityFingerprint = legacyFingerprint( + name = "googleApiActivityFingerprint", + returnType = "V", + parameters = listOf("Landroid/os/Bundle;"), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/GoogleApiActivity;") && + method.name == "onCreate" + } +) + +internal val preferenceFingerprint = legacyFingerprint( + name = "preferenceFingerprint", + accessFlags = AccessFlags.PROTECTED.value, + returnType = "V", + parameters = listOf("Z"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + ), + customFingerprint = { method, _ -> + method.definingClass == "Landroidx/preference/Preference;" + } +) + +internal val settingsHeadersFragmentFingerprint = legacyFingerprint( + name = "settingsHeadersFragmentFingerprint", + returnType = "V", + parameters = listOf("L"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + ), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/SettingsHeadersFragment;") && + method.name == "onCreate" + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/settings/ResourceUtils.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/settings/ResourceUtils.kt new file mode 100644 index 000000000..1af89771a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/settings/ResourceUtils.kt @@ -0,0 +1,262 @@ +package app.revanced.patches.music.utils.settings + +import app.revanced.patcher.patch.ResourcePatchContext +import app.revanced.patches.music.utils.compatibility.Constants.YOUTUBE_MUSIC_PACKAGE_NAME +import app.revanced.patches.music.utils.patch.PatchList +import app.revanced.util.adoptChild +import app.revanced.util.cloneNodes +import app.revanced.util.doRecursively +import app.revanced.util.insertNode +import org.w3c.dom.Element + +internal object ResourceUtils { + private lateinit var context: ResourcePatchContext + + fun setContext(context: ResourcePatchContext) { + this.context = context + } + + private const val RVX_SETTINGS_KEY = "revanced_extended_settings" + + const val SETTINGS_HEADER_PATH = "res/xml/settings_headers.xml" + + const val PREFERENCE_SCREEN_TAG_NAME = + "PreferenceScreen" + + const val PREFERENCE_CATEGORY_TAG_NAME = + "com.google.android.apps.youtube.music.ui.preference.PreferenceCategoryCompat" + + const val SWITCH_PREFERENCE_TAG_NAME = + "com.google.android.apps.youtube.music.ui.preference.SwitchCompatPreference" + + const val ACTIVITY_HOOK_TARGET_CLASS = + "com.google.android.gms.common.api.GoogleApiActivity" + + var musicPackageName = YOUTUBE_MUSIC_PACKAGE_NAME + + private var iconType = "default" + fun getIconType() = iconType + + fun setIconType(iconName: String) { + iconType = iconName + } + + private fun isIncludedCategory(category: String): Boolean { + CategoryType.entries.forEach { preference -> + if (category == preference.value) + return preference.added + } + return false + } + + private fun replacePackageName() = context.apply { + val xmlFile = get(SETTINGS_HEADER_PATH) + xmlFile.writeText( + xmlFile.readText() + .replace( + "\"com.google.android.apps.youtube.music\"", + "\"" + musicPackageName + "\"" + ) + ) + } + + + private fun setPreferenceCategory(newCategory: String) { + CategoryType.entries.forEach { preference -> + if (newCategory == preference.value) + preference.added = true + } + } + + fun updatePackageName(newPackage: String) { + musicPackageName = newPackage + replacePackageName() + } + + fun updatePatchStatus(patch: PatchList) { + patch.included = true + } + + fun addPreferenceCategory(category: String) { + context.document(SETTINGS_HEADER_PATH).use { document -> + val tags = document.getElementsByTagName(PREFERENCE_SCREEN_TAG_NAME) + List(tags.length) { tags.item(it) as Element } + .filter { it.getAttribute("android:key").contains(RVX_SETTINGS_KEY) } + .forEach { + if (!isIncludedCategory(category)) { + it.adoptChild(PREFERENCE_SCREEN_TAG_NAME) { + setAttribute( + "android:title", + "@string/revanced_preference_screen_$category" + "_title" + ) + setAttribute("android:key", "revanced_preference_screen_$category") + } + setPreferenceCategory(category) + } + } + } + } + + fun addPreferenceCategoryUnderPreferenceScreen( + preferenceScreenKey: String, + category: String + ) { + context.document(SETTINGS_HEADER_PATH).use { document -> + val tags = document.getElementsByTagName(PREFERENCE_SCREEN_TAG_NAME) + List(tags.length) { tags.item(it) as Element } + .filter { it.getAttribute("android:key").contains(preferenceScreenKey) } + .forEach { + it.adoptChild(PREFERENCE_CATEGORY_TAG_NAME) { + setAttribute("android:title", "@string/$category") + setAttribute("android:key", category) + } + } + } + } + + fun sortPreferenceCategory( + category: String + ) { + context.document(SETTINGS_HEADER_PATH).use { document -> + document.doRecursively node@{ + if (it !is Element) return@node + + it.getAttributeNode("android:key")?.let { attribute -> + if (attribute.textContent == "revanced_preference_screen_$category") { + it.cloneNodes(it.parentNode) + } + } + } + } + replacePackageName() + } + + fun addGmsCorePreference( + category: String, + key: String, + packageName: String, + targetClassName: String + ) { + context.document(SETTINGS_HEADER_PATH).use { document -> + val tags = document.getElementsByTagName(PREFERENCE_SCREEN_TAG_NAME) + List(tags.length) { tags.item(it) as Element } + .filter { + it.getAttribute("android:key").contains("revanced_preference_screen_$category") + } + .forEach { + it.adoptChild("Preference") { + setAttribute("android:key", key) + setAttribute("android:title", "@string/$key" + "_title") + setAttribute("android:summary", "@string/$key" + "_summary") + this.adoptChild("intent") { + setAttribute("android:targetPackage", packageName) + setAttribute("android:data", key) + setAttribute( + "android:targetClass", + targetClassName + ) + } + } + } + } + } + + fun addSwitchPreference( + category: String, + key: String, + defaultValue: String, + dependencyKey: String, + setSummary: Boolean + ) { + context.document(SETTINGS_HEADER_PATH).use { document -> + val tags = document.getElementsByTagName(PREFERENCE_SCREEN_TAG_NAME) + List(tags.length) { tags.item(it) as Element } + .filter { + it.getAttribute("android:key").contains("revanced_preference_screen_$category") + } + .forEach { + it.adoptChild(SWITCH_PREFERENCE_TAG_NAME) { + setAttribute("android:title", "@string/$key" + "_title") + if (setSummary) { + setAttribute("android:summary", "@string/$key" + "_summary") + } + setAttribute("android:key", key) + setAttribute("android:defaultValue", defaultValue) + if (dependencyKey != "") { + setAttribute("android:dependency", dependencyKey) + } + } + } + } + } + + fun addPreferenceWithIntent( + category: String, + key: String, + dependencyKey: String + ) { + context.document(SETTINGS_HEADER_PATH).use { document -> + val tags = document.getElementsByTagName(PREFERENCE_SCREEN_TAG_NAME) + List(tags.length) { tags.item(it) as Element } + .filter { + it.getAttribute("android:key").contains("revanced_preference_screen_$category") + } + .forEach { + it.adoptChild("Preference") { + setAttribute("android:title", "@string/$key" + "_title") + if (isSettingsSummariesEnabled == true) { + setAttribute("android:summary", "@string/$key" + "_summary") + } + setAttribute("android:key", key) + if (dependencyKey != "") { + setAttribute("android:dependency", dependencyKey) + } + this.adoptChild("intent") { + setAttribute("android:targetPackage", musicPackageName) + setAttribute("android:data", key) + setAttribute( + "android:targetClass", + ACTIVITY_HOOK_TARGET_CLASS + ) + } + } + } + } + } + + fun addRVXSettingsPreference() { + context.document(SETTINGS_HEADER_PATH).use { document -> + document.doRecursively node@{ + if (it !is Element) return@node + + it.getAttributeNode("android:key")?.let { attribute -> + if (attribute.textContent == "settings_header_about_youtube_music" && it.getAttributeNode( + "app:allowDividerBelow" + ).textContent == "false" + ) { + it.insertNode(PREFERENCE_SCREEN_TAG_NAME, it) { + setAttribute( + "android:title", + "@string/revanced_extended_settings_title" + ) + setAttribute("android:key", "revanced_extended_settings") + setAttribute("app:allowDividerAbove", "false") + } + it.getAttributeNode("app:allowDividerBelow").textContent = "true" + return@node + } + } + } + + document.doRecursively node@{ + if (it !is Element) return@node + + it.getAttributeNode("app:allowDividerBelow")?.let { attribute -> + if (attribute.textContent == "true") { + attribute.textContent = "false" + } + } + } + } + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/settings/SettingsPatch.kt new file mode 100644 index 000000000..fcb517477 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/settings/SettingsPatch.kt @@ -0,0 +1,321 @@ +package app.revanced.patches.music.utils.settings + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.booleanOption +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.EXTENSION_PATH +import app.revanced.patches.music.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.music.utils.extension.sharedExtensionPatch +import app.revanced.patches.music.utils.mainactivity.mainActivityResolvePatch +import app.revanced.patches.music.utils.patch.PatchList.SETTINGS_FOR_YOUTUBE_MUSIC +import app.revanced.patches.music.utils.playservice.versionCheckPatch +import app.revanced.patches.shared.extension.Constants.EXTENSION_UTILS_CLASS_DESCRIPTOR +import app.revanced.patches.shared.mainactivity.injectConstructorMethodCall +import app.revanced.patches.shared.mainactivity.injectOnCreateMethodCall +import app.revanced.patches.shared.sharedSettingFingerprint +import app.revanced.util.copyXmlNode +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.removeStringsElements +import app.revanced.util.valueOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import org.w3c.dom.Element + +private const val EXTENSION_ACTIVITY_CLASS_DESCRIPTOR = + "$EXTENSION_PATH/settings/ActivityHook;" +private const val EXTENSION_FRAGMENT_CLASS_DESCRIPTOR = + "$EXTENSION_PATH/settings/preference/ReVancedPreferenceFragment;" +private const val EXTENSION_INITIALIZATION_CLASS_DESCRIPTOR = + "$UTILS_PATH/InitializationPatch;" + +private val settingsBytecodePatch = bytecodePatch( + description = "settingsBytecodePatch" +) { + dependsOn( + sharedExtensionPatch, + mainActivityResolvePatch, + versionCheckPatch, + ) + + execute { + + // region patch for set SharedPrefCategory + + sharedSettingFingerprint.methodOrThrow().apply { + val stringIndex = indexOfFirstInstructionOrThrow(Opcode.CONST_STRING) + val stringRegister = getInstruction(stringIndex).registerA + + replaceInstruction( + stringIndex, + "const-string v$stringRegister, \"youtube\"" + ) + } + + // endregion + + // region patch for hook activity + + settingsHeadersFragmentFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.endIndex + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $EXTENSION_ACTIVITY_CLASS_DESCRIPTOR->setActivity(Ljava/lang/Object;)V" + ) + } + } + + // endregion + + // region patch for hook preference change listener + + preferenceFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.endIndex + val keyRegister = getInstruction(targetIndex).registerD + val valueRegister = getInstruction(targetIndex).registerE + + addInstruction( + targetIndex, + "invoke-static {v$keyRegister, v$valueRegister}, $EXTENSION_FRAGMENT_CLASS_DESCRIPTOR->onPreferenceChanged(Ljava/lang/String;Z)V" + ) + } + } + + // endregion + + // region patch for hook dummy Activity for intent + + googleApiActivityFingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 1, + """ + invoke-static {p0}, $EXTENSION_ACTIVITY_CLASS_DESCRIPTOR->initialize(Landroid/app/Activity;)Z + move-result v0 + if-eqz v0, :show + return-void + """, + ExternalLabel("show", getInstruction(1)), + ) + } + + // endregion + + injectOnCreateMethodCall( + EXTENSION_INITIALIZATION_CLASS_DESCRIPTOR, + "onCreate" + ) + injectConstructorMethodCall( + EXTENSION_UTILS_CLASS_DESCRIPTOR, + "setActivity" + ) + + } +} + +private const val DEFAULT_LABEL = "ReVanced Extended" +private lateinit var customName: String + +var isSettingsSummariesEnabled: Boolean? = true + +val settingsPatch = resourcePatch( + SETTINGS_FOR_YOUTUBE_MUSIC.title, + SETTINGS_FOR_YOUTUBE_MUSIC.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsBytecodePatch, + ) + + val settingsLabel = stringOption( + key = "settingsLabel", + default = DEFAULT_LABEL, + title = "RVX settings label", + description = "The name of the RVX settings menu.", + required = true, + ) + + val settingsSummaries by booleanOption( + key = "settingsSummaries", + default = true, + title = "RVX settings summaries", + description = "Shows the summary / description of each RVX setting. If set to false, no descriptions will be provided.", + required = true, + ) + + execute { + /** + * check patch options + */ + customName = settingsLabel + .valueOrThrow() + + isSettingsSummariesEnabled = settingsSummaries + + /** + * copy arrays, colors and strings + */ + arrayOf( + "arrays.xml", + "colors.xml", + "strings.xml" + ).forEach { xmlFile -> + copyXmlNode("music/settings/host", "values/$xmlFile", "resources") + } + + /** + * hide divider + */ + val styleFile = get("res/values/styles.xml") + + styleFile.writeText( + styleFile.readText() + .replace( + "allowDividerAbove\">true", + "allowDividerAbove\">false" + ).replace( + "allowDividerBelow\">true", + "allowDividerBelow\">false" + ) + ) + + /** + * Change colors + */ + document("res/values/colors.xml").use { document -> + val resourcesNode = document.getElementsByTagName("resources").item(0) as Element + val children = resourcesNode.childNodes + for (i in 0 until children.length) { + val node = children.item(i) as? Element ?: continue + + node.textContent = + when (node.getAttribute("name")) { + "material_deep_teal_500", + -> "@android:color/white" + + else -> continue + } + } + } + + ResourceUtils.setContext(this) + ResourceUtils.addRVXSettingsPreference() + + ResourceUtils.updatePatchStatus(SETTINGS_FOR_YOUTUBE_MUSIC) + } + + finalize { + /** + * change RVX settings menu name + * since it must be invoked after the Translations patch, it must be the last in the order. + */ + if (customName != DEFAULT_LABEL) { + removeStringsElements( + arrayOf("revanced_extended_settings_title") + ) + document("res/values/strings.xml").use { document -> + mapOf( + "revanced_extended_settings_title" to customName + ).forEach { (k, v) -> + val stringElement = document.createElement("string") + + stringElement.setAttribute("name", k) + stringElement.textContent = v + + document.getElementsByTagName("resources").item(0) + .appendChild(stringElement) + } + } + } + + /** + * add open default app settings + */ + addPreferenceWithIntent( + CategoryType.MISC, + "revanced_default_app_settings" + ) + + /** + * add import export settings + */ + addPreferenceWithIntent( + CategoryType.MISC, + "revanced_extended_settings_import_export" + ) + + /** + * sort preference + */ + CategoryType.entries.sorted().forEach { + ResourceUtils.sortPreferenceCategory(it.value) + } + } +} + +internal fun addSwitchPreference( + category: CategoryType, + key: String, + defaultValue: String +) = addSwitchPreference(category, key, defaultValue, "") + +internal fun addSwitchPreference( + category: CategoryType, + key: String, + defaultValue: String, + setSummary: Boolean +) = addSwitchPreference(category, key, defaultValue, "", setSummary) + +internal fun addSwitchPreference( + category: CategoryType, + key: String, + defaultValue: String, + dependencyKey: String +) = addSwitchPreference(category, key, defaultValue, dependencyKey, true) + +internal fun addSwitchPreference( + category: CategoryType, + key: String, + defaultValue: String, + dependencyKey: String, + setSummary: Boolean +) { + val categoryValue = category.value + ResourceUtils.addPreferenceCategory(categoryValue) + + // Check the exported value of isSettingsSummariesEnabled + if (isSettingsSummariesEnabled == true) { + ResourceUtils.addSwitchPreference(categoryValue, key, defaultValue, dependencyKey, setSummary) + } else { + ResourceUtils.addSwitchPreference(categoryValue, key, defaultValue, dependencyKey, false) + } +} + +internal fun addPreferenceWithIntent( + category: CategoryType, + key: String +) = addPreferenceWithIntent(category, key, "") + +internal fun addPreferenceWithIntent( + category: CategoryType, + key: String, + dependencyKey: String +) { + val categoryValue = category.value + ResourceUtils.addPreferenceCategory(categoryValue) + ResourceUtils.addPreferenceWithIntent(categoryValue, key, dependencyKey) +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/Fingerprints.kt new file mode 100644 index 000000000..fd4281524 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/Fingerprints.kt @@ -0,0 +1,63 @@ +package app.revanced.patches.music.utils.sponsorblock + +import app.revanced.patches.music.utils.resourceid.inlineTimeBarAdBreakMarkerColor +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionReversed +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val musicPlaybackControlsTimeBarDrawFingerprint = legacyFingerprint( + name = "musicPlaybackControlsTimeBarDrawFingerprint", + returnType = "V", + customFingerprint = { method, _ -> + method.definingClass.endsWith("/MusicPlaybackControlsTimeBar;") && + method.name == "draw" + } +) + +internal val musicPlaybackControlsTimeBarOnMeasureFingerprint = legacyFingerprint( + name = "musicPlaybackControlsTimeBarOnMeasureFingerprint", + returnType = "V", + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/MusicPlaybackControlsTimeBar;") && + method.name == "onMeasure" + } +) + +internal val rectangleFieldInvalidatorFingerprint = legacyFingerprint( + name = "rectangleFieldInvalidatorFingerprint", + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_WIDE, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_WIDE, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_WIDE + ), + customFingerprint = { method, _ -> + indexOfInvalidateInstruction(method) >= 0 + } +) + +internal fun indexOfInvalidateInstruction(method: Method) = + method.indexOfFirstInstructionReversed { + getReference()?.name == "invalidate" + } + +internal val seekBarConstructorFingerprint = legacyFingerprint( + name = "seekBarConstructorFingerprint", + returnType = "V", + literals = listOf(inlineTimeBarAdBreakMarkerColor), +) + +internal val seekbarOnDrawFingerprint = legacyFingerprint( + name = "seekbarOnDrawFingerprint", + customFingerprint = { method, _ -> method.name == "onDraw" } +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/SponsorBlockPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/SponsorBlockPatch.kt new file mode 100644 index 000000000..269378cfc --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/SponsorBlockPatch.kt @@ -0,0 +1,389 @@ +package app.revanced.patches.music.utils.sponsorblock + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.EXTENSION_PATH +import app.revanced.patches.music.utils.patch.PatchList.SPONSORBLOCK +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.ACTIVITY_HOOK_TARGET_CLASS +import app.revanced.patches.music.utils.settings.ResourceUtils.PREFERENCE_CATEGORY_TAG_NAME +import app.revanced.patches.music.utils.settings.ResourceUtils.PREFERENCE_SCREEN_TAG_NAME +import app.revanced.patches.music.utils.settings.ResourceUtils.SETTINGS_HEADER_PATH +import app.revanced.patches.music.utils.settings.ResourceUtils.SWITCH_PREFERENCE_TAG_NAME +import app.revanced.patches.music.utils.settings.ResourceUtils.addPreferenceCategory +import app.revanced.patches.music.utils.settings.ResourceUtils.musicPackageName +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.music.video.information.videoIdHook +import app.revanced.patches.music.video.information.videoInformationPatch +import app.revanced.patches.music.video.information.videoTimeHook +import app.revanced.util.adoptChild +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import org.w3c.dom.Element + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$EXTENSION_PATH/sponsorblock/SegmentPlaybackController;" + +private val sponsorBlockBytecodePatch = bytecodePatch( + description = "sponsorBlockBytecodePatch" +) { + dependsOn( + sharedResourceIdPatch, + videoInformationPatch + ) + + execute { + + /** + * Hook the video time methods & Initialize the player controller + */ + videoTimeHook(EXTENSION_CLASS_DESCRIPTOR, "setVideoTime") + + /** + * Responsible for seekbar in fullscreen + */ + var rectangleFieldName = + with(rectangleFieldInvalidatorFingerprint.methodOrThrow(seekBarConstructorFingerprint)) { + val invalidateIndex = indexOfInvalidateInstruction(this) + val rectangleIndex = + indexOfFirstInstructionReversedOrThrow(invalidateIndex + 1) { + getReference()?.type == "Landroid/graphics/Rect;" + } + val rectangleReference = + getInstruction(rectangleIndex).reference + + (rectangleReference as FieldReference).name + } + + seekbarOnDrawFingerprint.methodOrThrow(seekBarConstructorFingerprint).apply { + // Initialize seekbar method + addInstructions( + 0, """ + move-object/from16 v0, p0 + const-string v1, "$rectangleFieldName" + invoke-static {v0, v1}, $EXTENSION_CLASS_DESCRIPTOR->setSponsorBarRect(Ljava/lang/Object;Ljava/lang/String;)V + """ + ) + + // Set seekbar thickness + val roundIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "round" + } + 1 + val roundRegister = getInstruction(roundIndex).registerA + addInstruction( + roundIndex + 1, + "invoke-static {v$roundRegister}, " + + "$EXTENSION_CLASS_DESCRIPTOR->setSponsorBarThickness(I)V" + ) + + // Draw segment + val drawCircleIndex = indexOfFirstInstructionReversedOrThrow { + getReference()?.name == "drawCircle" + } + val drawCircleInstruction = getInstruction(drawCircleIndex) + addInstruction( + drawCircleIndex, + "invoke-static {v${drawCircleInstruction.registerC}, v${drawCircleInstruction.registerE}}, " + + "$EXTENSION_CLASS_DESCRIPTOR->drawSponsorTimeBars(Landroid/graphics/Canvas;F)V" + ) + } + + + /** + * Responsible for seekbar in player + */ + rectangleFieldName = + musicPlaybackControlsTimeBarOnMeasureFingerprint.matchOrThrow().let { + with(it.method) { + val rectangleIndex = + indexOfFirstInstructionReversedOrThrow(it.patternMatch!!.endIndex) { + opcode == Opcode.IGET_OBJECT && + getReference()?.type == "Landroid/graphics/Rect;" + } + val rectangleReference = + getInstruction(rectangleIndex).reference + (rectangleReference as FieldReference).name + } + } + + musicPlaybackControlsTimeBarDrawFingerprint.methodOrThrow().apply { + // Initialize seekbar method + addInstructions( + 1, """ + move-object/from16 v0, p0 + const-string v1, "$rectangleFieldName" + invoke-static {v0, v1}, $EXTENSION_CLASS_DESCRIPTOR->setSponsorBarRect(Ljava/lang/Object;Ljava/lang/String;)V + """ + ) + + // Draw segment + val drawCircleIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "drawCircle" + } + val drawCircleInstruction = getInstruction(drawCircleIndex) + addInstruction( + drawCircleIndex, + "invoke-static {v${drawCircleInstruction.registerC}, v${drawCircleInstruction.registerE}}, " + + "$EXTENSION_CLASS_DESCRIPTOR->drawSponsorTimeBars(Landroid/graphics/Canvas;F)V" + ) + } + + /** + * Set current video id + */ + videoIdHook("$EXTENSION_CLASS_DESCRIPTOR->setVideoId(Ljava/lang/String;)V") + } +} + +private const val SEGMENTS_CATEGORY_KEY = "sb_diff_segments" +private const val ABOUT_CATEGORY_KEY = "sb_about" + +private val SPONSOR_BLOCK_CATEGORY = CategoryType.SPONSOR_BLOCK.value + +@Suppress("unused") +val sponsorBlockPatch = resourcePatch( + SPONSORBLOCK.title, + SPONSORBLOCK.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + sponsorBlockBytecodePatch, + settingsPatch, + ) + + execute { + fun addSwitchPreference( + category: String, + key: String, + defaultValue: String, + dependencyKey: String + ) { + document(SETTINGS_HEADER_PATH).use { document -> + val tags = document.getElementsByTagName(PREFERENCE_SCREEN_TAG_NAME) + List(tags.length) { tags.item(it) as Element } + .filter { + it.getAttribute("android:key") + .contains("revanced_preference_screen_$category") + } + .forEach { + it.adoptChild(SWITCH_PREFERENCE_TAG_NAME) { + setAttribute("android:title", "@string/revanced_$key") + setAttribute("android:summary", "@string/revanced_$key" + "_sum") + setAttribute("android:key", key) + setAttribute("android:defaultValue", defaultValue) + if (dependencyKey != "") { + setAttribute("android:dependency", dependencyKey) + } + } + } + } + } + + fun addSwitchPreference( + category: String, + key: String, + defaultValue: String + ) = addSwitchPreference(category, key, defaultValue, "") + + fun addPreferenceWithIntent( + category: String, + key: String, + dependencyKey: String + ) { + document(SETTINGS_HEADER_PATH).use { document -> + val tags = document.getElementsByTagName(PREFERENCE_SCREEN_TAG_NAME) + List(tags.length) { tags.item(it) as Element } + .filter { + it.getAttribute("android:key") + .contains("revanced_preference_screen_$category") + } + .forEach { + it.adoptChild("Preference") { + setAttribute("android:title", "@string/revanced_$key") + setAttribute("android:summary", "@string/revanced_$key" + "_sum") + setAttribute("android:key", key) + setAttribute("android:dependency", dependencyKey) + this.adoptChild("intent") { + setAttribute("android:targetPackage", musicPackageName) + setAttribute("android:data", key) + setAttribute( + "android:targetClass", + ACTIVITY_HOOK_TARGET_CLASS + ) + } + } + } + } + } + + fun addPreferenceCategoryUnderPreferenceScreen( + preferenceScreenKey: String, + category: String + ) { + document(SETTINGS_HEADER_PATH).use { document -> + val tags = document.getElementsByTagName(PREFERENCE_SCREEN_TAG_NAME) + List(tags.length) { tags.item(it) as Element } + .filter { it.getAttribute("android:key").contains(preferenceScreenKey) } + .forEach { + it.adoptChild(PREFERENCE_CATEGORY_TAG_NAME) { + setAttribute("android:title", "@string/revanced_$category") + setAttribute("android:key", category) + } + } + } + } + + fun addSegmentsPreference( + key: String, + dependencyKey: String + ) { + document(SETTINGS_HEADER_PATH).use { document -> + val tags = document.getElementsByTagName(PREFERENCE_CATEGORY_TAG_NAME) + List(tags.length) { tags.item(it) as Element } + .filter { it.getAttribute("android:key") == SEGMENTS_CATEGORY_KEY } + .forEach { + it.adoptChild("Preference") { + setAttribute("android:title", "@string/revanced_$key") + setAttribute("android:summary", "@string/revanced_$key" + "_sum") + setAttribute("android:key", key) + setAttribute("android:dependency", dependencyKey) + this.adoptChild("intent") { + setAttribute("android:targetPackage", musicPackageName) + setAttribute("android:data", key) + setAttribute( + "android:targetClass", + ACTIVITY_HOOK_TARGET_CLASS + ) + } + } + } + } + } + + fun addAboutPreference( + key: String, + data: String + ) { + document(SETTINGS_HEADER_PATH).use { document -> + val tags = document.getElementsByTagName(PREFERENCE_CATEGORY_TAG_NAME) + List(tags.length) { tags.item(it) as Element } + .filter { it.getAttribute("android:key") == ABOUT_CATEGORY_KEY } + .forEach { + it.adoptChild("Preference") { + setAttribute("android:title", "@string/revanced_$key") + setAttribute("android:summary", "@string/revanced_$key" + "_sum") + setAttribute("android:key", key) + this.adoptChild("intent") { + setAttribute("android:action", "android.intent.action.VIEW") + setAttribute("android:data", data) + } + } + } + } + } + + addPreferenceCategory(SPONSOR_BLOCK_CATEGORY) + + addSwitchPreference( + SPONSOR_BLOCK_CATEGORY, + "sb_enabled", + "true" + ) + addSwitchPreference( + SPONSOR_BLOCK_CATEGORY, + "sb_toast_on_skip", + "true", + "sb_enabled" + ) + addSwitchPreference( + SPONSOR_BLOCK_CATEGORY, + "sb_toast_on_connection_error", + "false", + "sb_enabled" + ) + addPreferenceWithIntent( + SPONSOR_BLOCK_CATEGORY, + "sb_api_url", + "sb_enabled" + ) + + addPreferenceCategoryUnderPreferenceScreen( + SPONSOR_BLOCK_CATEGORY, + SEGMENTS_CATEGORY_KEY + ) + + addSegmentsPreference( + "sb_segments_sponsor", + "sb_enabled" + ) + addSegmentsPreference( + "sb_segments_selfpromo", + "sb_enabled" + ) + addSegmentsPreference( + "sb_segments_interaction", + "sb_enabled" + ) + addSegmentsPreference( + "sb_segments_intro", + "sb_enabled" + ) + addSegmentsPreference( + "sb_segments_outro", + "sb_enabled" + ) + addSegmentsPreference( + "sb_segments_preview", + "sb_enabled" + ) + addSegmentsPreference( + "sb_segments_filler", + "sb_enabled" + ) + addSegmentsPreference( + "sb_segments_nomusic", + "sb_enabled" + ) + + addPreferenceCategoryUnderPreferenceScreen( + CategoryType.SPONSOR_BLOCK.value, + ABOUT_CATEGORY_KEY + ) + + addAboutPreference( + "sb_about_api", + "https://sponsor.ajay.app" + ) + + get(SETTINGS_HEADER_PATH).apply { + writeText( + readText() + .replace( + "\"sb_segments_nomusic", + "\"sb_segments_music_offtopic" + ) + ) + } + + updatePatchStatus(SPONSORBLOCK) + + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/videotype/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/videotype/Fingerprints.kt new file mode 100644 index 000000000..0edb35019 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/videotype/Fingerprints.kt @@ -0,0 +1,30 @@ +package app.revanced.patches.music.utils.videotype + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val videoTypeFingerprint = legacyFingerprint( + name = "videoTypeFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("L"), + opcodes = listOf( + Opcode.IGET, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IF_NEZ, + Opcode.SGET_OBJECT, + Opcode.GOTO, + Opcode.SGET_OBJECT + ) +) + +internal val videoTypeParentFingerprint = legacyFingerprint( + name = "videoTypeParentFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("L", "L"), + strings = listOf("RQ") +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/videotype/VideoTypeHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/videotype/VideoTypeHookPatch.kt new file mode 100644 index 000000000..587048a3d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/videotype/VideoTypeHookPatch.kt @@ -0,0 +1,38 @@ +package app.revanced.patches.music.utils.videotype + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.extension.Constants.UTILS_PATH +import app.revanced.util.fingerprint.matchOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$UTILS_PATH/VideoTypeHookPatch;" + +@Suppress("unused") +val videoTypeHookPatch = bytecodePatch( + description = "videoTypeHookPatch" +) { + + execute { + + videoTypeFingerprint.matchOrThrow(videoTypeParentFingerprint).let { + it.method.apply { + val insertIndex = it.patternMatch!!.startIndex + 3 + val referenceIndex = insertIndex + 1 + val referenceInstruction = + getInstruction(referenceIndex).reference + + addInstructionsWithLabels( + insertIndex, """ + if-nez p0, :dismiss + sget-object p0, $referenceInstruction + :dismiss + invoke-static {p0}, $EXTENSION_CLASS_DESCRIPTOR->setVideoType(Ljava/lang/Enum;)V + """ + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/information/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/information/Fingerprints.kt new file mode 100644 index 000000000..d6a110041 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/video/information/Fingerprints.kt @@ -0,0 +1,62 @@ +package app.revanced.patches.music.video.information + +import app.revanced.patches.music.utils.resourceid.qualityAuto +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val playerControllerSetTimeReferenceFingerprint = legacyFingerprint( + name = "playerControllerSetTimeReferenceFingerprint", + returnType = "V", + opcodes = listOf( + Opcode.INVOKE_DIRECT_RANGE, + Opcode.IGET_OBJECT + ), + strings = listOf("Media progress reported outside media playback: ") +) + +internal val videoEndFingerprint = legacyFingerprint( + name = "videoEndFingerprint", + strings = listOf("Attempting to seek during an ad") +) + +internal val videoIdFingerprint = legacyFingerprint( + name = "videoIdFingerprint", + returnType = "V", + parameters = listOf("L", "Ljava/lang/String;"), + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + opcodes = listOf( + Opcode.INVOKE_INTERFACE_RANGE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_INTERFACE_RANGE, + Opcode.MOVE_RESULT_OBJECT, + ), + strings = listOf("Null initialPlayabilityStatus") +) + +internal val videoQualityListFingerprint = legacyFingerprint( + name = "videoQualityListFingerprint", + returnType = "V", + parameters = listOf("L"), + opcodes = listOf( + Opcode.INVOKE_INTERFACE, + Opcode.RETURN_VOID + ), + literals = listOf(qualityAuto) +) + +internal val videoQualityTextFingerprint = legacyFingerprint( + name = "videoQualityTextFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("[L", "I", "Z"), + opcodes = listOf( + Opcode.IF_EQZ, + Opcode.IF_LTZ, + Opcode.ARRAY_LENGTH, + Opcode.IF_GE, + Opcode.AGET_OBJECT, + Opcode.IGET_OBJECT + ) +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt new file mode 100644 index 000000000..e4901ea36 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt @@ -0,0 +1,392 @@ +package app.revanced.patches.music.video.information + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableClass +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patcher.util.smali.toInstructions +import app.revanced.patches.music.utils.extension.Constants.SHARED_PATH +import app.revanced.patches.music.utils.playbackSpeedFingerprint +import app.revanced.patches.music.utils.playbackSpeedParentFingerprint +import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.shared.mdxPlayerDirectorSetVideoStageFingerprint +import app.revanced.patches.shared.videoLengthFingerprint +import app.revanced.util.addStaticFieldToExtension +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$SHARED_PATH/VideoInformation;" + +private const val REGISTER_PLAYER_RESPONSE_MODEL = 4 + +private const val REGISTER_VIDEO_ID = 0 +private const val REGISTER_VIDEO_LENGTH = 1 + +@Suppress("unused") +private const val REGISTER_VIDEO_LENGTH_DUMMY = 2 + +private lateinit var PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR: String +private lateinit var videoIdMethodCall: String +private lateinit var videoLengthMethodCall: String + +private lateinit var videoInformationMethod: MutableMethod + +/** + * Used in [videoEndFingerprint] and [mdxPlayerDirectorSetVideoStageFingerprint]. + * Since both classes are inherited from the same class, + * [videoEndFingerprint] and [mdxPlayerDirectorSetVideoStageFingerprint] always have the same [seekSourceEnumType] and [seekSourceMethodName]. + */ +private var seekSourceEnumType = "" +private var seekSourceMethodName = "" + +private lateinit var playerConstructorMethod: MutableMethod +private var playerConstructorInsertIndex = -1 + +private lateinit var mdxConstructorMethod: MutableMethod +private var mdxConstructorInsertIndex = -1 + +private lateinit var videoTimeConstructorMethod: MutableMethod +private var videoTimeConstructorInsertIndex = 2 + +val videoInformationPatch = bytecodePatch( + description = "videoInformationPatch", +) { + dependsOn(sharedResourceIdPatch) + + execute { + fun addSeekInterfaceMethods( + targetClass: MutableClass, + targetMethod: MutableMethod, + seekMethodName: String, + methodName: String, + fieldName: String + ) { + targetMethod.apply { + targetClass.methods.add( + ImmutableMethod( + definingClass, + "seekTo", + listOf(ImmutableMethodParameter("J", annotations, "time")), + "Z", + AccessFlags.PUBLIC or AccessFlags.FINAL, + annotations, + null, + ImmutableMethodImplementation( + 4, """ + # first enum (field a) is SEEK_SOURCE_UNKNOWN + sget-object v0, $seekSourceEnumType->a:$seekSourceEnumType + invoke-virtual {p0, p1, p2, v0}, $definingClass->$seekMethodName(J$seekSourceEnumType)Z + move-result p1 + return p1 + """.toInstructions(), + null, + null + ) + ).toMutable() + ) + + val smaliInstructions = + """ + if-eqz v0, :ignore + invoke-virtual {v0, p0, p1}, $definingClass->seekTo(J)Z + move-result v0 + return v0 + :ignore + const/4 v0, 0x0 + return v0 + """ + + addStaticFieldToExtension( + EXTENSION_CLASS_DESCRIPTOR, + methodName, + fieldName, + definingClass, + smaliInstructions + ) + } + } + + fun Pair.getPlayerResponseInstruction(returnType: String): String { + methodOrThrow().apply { + val targetReference = getInstruction( + indexOfFirstInstructionOrThrow { + val reference = getReference() + (opcode == Opcode.INVOKE_INTERFACE_RANGE || opcode == Opcode.INVOKE_INTERFACE) && + reference?.definingClass == PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR && + reference.returnType == returnType + } + ).reference + + return "invoke-interface {v$REGISTER_PLAYER_RESPONSE_MODEL}, $targetReference" + } + } + + videoEndFingerprint.methodOrThrow().apply { + findMethodOrThrow(definingClass).let { + playerConstructorMethod = it + playerConstructorInsertIndex = it.indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_DIRECT && getReference()?.name == "" + } + 1 + } + + // hook the player controller for use through extension + onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "initialize") + + seekSourceEnumType = parameterTypes[1].toString() + seekSourceMethodName = name + + // Create extension interface methods. + addSeekInterfaceMethods( + videoEndFingerprint.mutableClassOrThrow(), + this, + seekSourceMethodName, + "overrideVideoTime", + "videoInformationClass" + ) + } + + mdxPlayerDirectorSetVideoStageFingerprint.methodOrThrow().apply { + findMethodOrThrow(definingClass).let { + mdxConstructorMethod = it + mdxConstructorInsertIndex = it.indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_DIRECT && getReference()?.name == "" + } + 1 + } + + // hook the MDX director for use through extension + onCreateHookMdx(EXTENSION_CLASS_DESCRIPTOR, "initializeMdx") + + // Create extension interface methods. + addSeekInterfaceMethods( + mdxPlayerDirectorSetVideoStageFingerprint.mutableClassOrThrow(), + this, + seekSourceMethodName, + "overrideMDXVideoTime", + "videoInformationMDXClass" + ) + } + + /** + * Set current video information + */ + videoIdFingerprint.matchOrThrow().let { + it.method.apply { + val playerResponseModelIndex = it.patternMatch!!.startIndex + + PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR = + getInstruction(playerResponseModelIndex) + .getReference() + ?.definingClass + ?: throw PatchException("Could not find Player Response Model class") + + videoIdMethodCall = + videoIdFingerprint.getPlayerResponseInstruction("Ljava/lang/String;") + videoLengthMethodCall = + videoLengthFingerprint.getPlayerResponseInstruction("J") + + videoInformationMethod = getVideoInformationMethod() + it.classDef.methods.add(videoInformationMethod) + + addInstruction( + playerResponseModelIndex + 2, + "invoke-direct/range {p0 .. p1}, $definingClass->setVideoInformation($PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR)V" + ) + } + } + + /** + * Set the video time method + */ + playerControllerSetTimeReferenceFingerprint.matchOrThrow().let { + videoTimeConstructorMethod = + it.getWalkerMethod(it.patternMatch!!.startIndex) + } + + /** + * Set current video time + */ + videoTimeHook(EXTENSION_CLASS_DESCRIPTOR, "setVideoTime") + + /** + * Set current video length + */ + videoLengthHook("$EXTENSION_CLASS_DESCRIPTOR->setVideoLength(J)V") + + /** + * Set current video id + */ + videoIdHook("$EXTENSION_CLASS_DESCRIPTOR->setVideoId(Ljava/lang/String;)V") + + /** + * Hook current playback speed + */ + playbackSpeedFingerprint.matchOrThrow(playbackSpeedParentFingerprint).let { + it.getWalkerMethod(it.patternMatch!!.endIndex).apply { + addInstruction( + implementation!!.instructions.lastIndex, + "invoke-static {p1}, $EXTENSION_CLASS_DESCRIPTOR->setPlaybackSpeed(F)V" + ) + } + } + + /** + * Hook current video quality + */ + videoQualityListFingerprint.matchOrThrow().let { + it.method.apply { + val videoQualityMethodName = + findMethodOrThrow(definingClass) { parameterTypes.first() == "I" }.name + // set video quality array + val listIndex = it.patternMatch!!.startIndex + val listRegister = getInstruction(listIndex).registerD + + addInstruction( + listIndex, + "invoke-static {v$listRegister}, $EXTENSION_CLASS_DESCRIPTOR->setVideoQualityList([Ljava/lang/Object;)V" + ) + + val smaliInstructions = + """ + if-eqz v0, :ignore + invoke-virtual {v0, p0}, $definingClass->$videoQualityMethodName(I)V + :ignore + return-void + """ + + addStaticFieldToExtension( + EXTENSION_CLASS_DESCRIPTOR, + "overrideVideoQuality", + "videoQualityClass", + definingClass, + smaliInstructions + ) + + } + } + + // set current video quality + videoQualityTextFingerprint.matchOrThrow().let { + it.method.apply { + val textIndex = it.patternMatch!!.endIndex + val textRegister = getInstruction(textIndex).registerA + + addInstruction( + textIndex + 1, + "invoke-static {v$textRegister}, $EXTENSION_CLASS_DESCRIPTOR->setVideoQuality(Ljava/lang/String;)V" + ) + } + } + } +} + +private fun MutableMethod.getVideoInformationMethod(): MutableMethod = + ImmutableMethod( + definingClass, + "setVideoInformation", + listOf( + ImmutableMethodParameter( + PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR, + annotations, + null + ) + ), + "V", + AccessFlags.PRIVATE or AccessFlags.FINAL, + annotations, + null, + ImmutableMethodImplementation( + REGISTER_PLAYER_RESPONSE_MODEL + 1, """ + $videoIdMethodCall + move-result-object v$REGISTER_VIDEO_ID + $videoLengthMethodCall + move-result-wide v$REGISTER_VIDEO_LENGTH + return-void + """.toInstructions(), + null, + null + ) + ).toMutable() + +private fun MutableMethod.insert(insertIndex: Int, register: String, descriptor: String) = + addInstruction(insertIndex, "invoke-static { $register }, $descriptor") + +private fun MutableMethod.insertTimeHook(insertIndex: Int, descriptor: String) = + insert(insertIndex, "p1, p2", descriptor) + +/** + * Hook the player controller. Called when a video is opened or the current video is changed. + * + * Note: This hook is called very early and is called before the video id, video time, video length, + * and many other data fields are set. + * + * @param targetMethodClass The descriptor for the class to invoke when the player controller is created. + * @param targetMethodName The name of the static method to invoke when the player controller is created. + */ +internal fun onCreateHook(targetMethodClass: String, targetMethodName: String) = + playerConstructorMethod.addInstruction( + playerConstructorInsertIndex++, + "invoke-static { }, $targetMethodClass->$targetMethodName()V" + ) + +/** + * Hook the MDX player director. Called when playing videos while casting to a big screen device. + * + * @param targetMethodClass The descriptor for the class to invoke when the player controller is created. + * @param targetMethodName The name of the static method to invoke when the player controller is created. + */ +internal fun onCreateHookMdx(targetMethodClass: String, targetMethodName: String) = + mdxConstructorMethod.addInstruction( + mdxConstructorInsertIndex++, + "invoke-static { }, $targetMethodClass->$targetMethodName()V" + ) + +internal fun videoIdHook( + descriptor: String +) = videoInformationMethod.apply { + addInstruction( + implementation!!.instructions.lastIndex, + "invoke-static {v$REGISTER_VIDEO_ID}, $descriptor" + ) +} + +internal fun videoLengthHook( + descriptor: String +) = videoInformationMethod.apply { + addInstruction( + implementation!!.instructions.lastIndex, + "invoke-static {v$REGISTER_VIDEO_LENGTH, v$REGISTER_VIDEO_LENGTH_DUMMY}, $descriptor" + ) +} + +/** + * Hook the video time. + * The hook is usually called once per second. + * + * @param targetMethodClass The descriptor for the static method to invoke when the player controller is created. + * @param targetMethodName The name of the static method to invoke when the player controller is created. + */ +internal fun videoTimeHook(targetMethodClass: String, targetMethodName: String) = + videoTimeConstructorMethod.insertTimeHook( + videoTimeConstructorInsertIndex++, + "$targetMethodClass->$targetMethodName(J)V" + ) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/playback/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/playback/Fingerprints.kt new file mode 100644 index 000000000..e074cd7ee --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/video/playback/Fingerprints.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.music.video.playback + +import app.revanced.util.fingerprint.legacyFingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val userQualityChangeFingerprint = legacyFingerprint( + name = "userQualityChangeFingerprint", + returnType = "V", + opcodes = listOf( + Opcode.CONST_STRING, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IF_EQZ, + Opcode.CHECK_CAST + ), + strings = listOf("VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT") +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/playback/VideoPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/playback/VideoPlaybackPatch.kt new file mode 100644 index 000000000..61c8d9a25 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/video/playback/VideoPlaybackPatch.kt @@ -0,0 +1,140 @@ +package app.revanced.patches.music.video.playback + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.VIDEO_PATH +import app.revanced.patches.music.utils.patch.PatchList.VIDEO_PLAYBACK +import app.revanced.patches.music.utils.playbackSpeedBottomSheetFingerprint +import app.revanced.patches.music.utils.playbackSpeedFingerprint +import app.revanced.patches.music.utils.playbackSpeedParentFingerprint +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addPreferenceWithIntent +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.music.video.information.videoIdHook +import app.revanced.patches.music.video.information.videoInformationPatch +import app.revanced.patches.shared.customspeed.customPlaybackSpeedPatch +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +private const val EXTENSION_PLAYBACK_SPEED_CLASS_DESCRIPTOR = + "$VIDEO_PATH/PlaybackSpeedPatch;" +private const val EXTENSION_VIDEO_QUALITY_CLASS_DESCRIPTOR = + "$VIDEO_PATH/VideoQualityPatch;" + +@Suppress("unused") +val videoPlaybackPatch = bytecodePatch( + VIDEO_PLAYBACK.title, + VIDEO_PLAYBACK.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + customPlaybackSpeedPatch( + "$VIDEO_PATH/CustomPlaybackSpeedPatch;", + 5.0f + ), + settingsPatch, + videoInformationPatch, + ) + + execute { + // region patch for default playback speed + + playbackSpeedBottomSheetFingerprint.mutableClassOrThrow().let { + val onItemClickMethod = + it.methods.find { method -> method.name == "onItemClick" } + ?: throw PatchException("Failed to find onItemClick method") + + onItemClickMethod.apply { + val targetIndex = indexOfFirstInstructionOrThrow(Opcode.IGET) + val targetRegister = + getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $EXTENSION_PLAYBACK_SPEED_CLASS_DESCRIPTOR->userSelectedPlaybackSpeed(F)V" + ) + } + } + + playbackSpeedFingerprint.matchOrThrow(playbackSpeedParentFingerprint).let { + it.method.apply { + val startIndex = it.patternMatch!!.startIndex + val speedRegister = + getInstruction(startIndex + 1).registerA + + addInstructions( + startIndex + 2, """ + invoke-static {v$speedRegister}, $EXTENSION_PLAYBACK_SPEED_CLASS_DESCRIPTOR->getPlaybackSpeed(F)F + move-result v$speedRegister + """ + ) + } + } + + // endregion + + // region patch for default video quality + + userQualityChangeFingerprint.matchOrThrow().let { + it.method.apply { + val endIndex = it.patternMatch!!.endIndex + val qualityChangedClass = + getInstruction(endIndex).reference.toString() + + findMethodOrThrow(qualityChangedClass) { + name == "onItemClick" + }.addInstruction( + 0, + "invoke-static {}, $EXTENSION_VIDEO_QUALITY_CLASS_DESCRIPTOR->userSelectedVideoQuality()V" + ) + } + } + + videoIdHook("$EXTENSION_VIDEO_QUALITY_CLASS_DESCRIPTOR->newVideoStarted(Ljava/lang/String;)V") + + // endregion + + addPreferenceWithIntent( + CategoryType.VIDEO, + "revanced_custom_playback_speeds" + ) + addSwitchPreference( + CategoryType.VIDEO, + "revanced_remember_playback_speed_last_selected", + "true" + ) + addSwitchPreference( + CategoryType.VIDEO, + "revanced_remember_playback_speed_last_selected_toast", + "true", + "revanced_remember_playback_speed_last_selected" + ) + addSwitchPreference( + CategoryType.VIDEO, + "revanced_remember_video_quality_last_selected", + "true" + ) + addSwitchPreference( + CategoryType.VIDEO, + "revanced_remember_video_quality_last_selected_toast", + "true", + "revanced_remember_video_quality_last_selected" + ) + + updatePatchStatus(VIDEO_PLAYBACK) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/ad/AdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/AdsPatch.kt new file mode 100644 index 000000000..df6539d9a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/AdsPatch.kt @@ -0,0 +1,135 @@ +package app.revanced.patches.reddit.ad + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH +import app.revanced.patches.reddit.utils.patch.PatchList.HIDE_ADS +import app.revanced.patches.reddit.utils.settings.settingsPatch +import app.revanced.patches.reddit.utils.settings.updatePatchStatus +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstStringInstruction +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +private const val RESOURCE_FILE_PATH = "res/layout/merge_listheader_link_detail.xml" + +private val bannerAdsPatch = resourcePatch( + description = "bannerAdsPatch", +) { + execute { + document(RESOURCE_FILE_PATH).use { document -> + document.getElementsByTagName("merge").item(0).childNodes.apply { + val attributes = arrayOf("height", "width") + + for (i in 1 until length) { + val view = item(i) + if ( + view.hasAttributes() && + view.attributes.getNamedItem("android:id").nodeValue.endsWith("ad_view_stub") + ) { + attributes.forEach { attribute -> + view.attributes.getNamedItem("android:layout_$attribute").nodeValue = + "0.0dip" + } + + break + } + } + } + } + } +} + +private const val EXTENSION_METHOD_DESCRIPTOR = + "$PATCHES_PATH/GeneralAdsPatch;->hideCommentAds()Z" + +private val commentAdsPatch = bytecodePatch( + description = "commentAdsPatch", +) { + execute { + commentAdsFingerprint.matchOrThrow().let { + val walkerMethod = it.getWalkerMethod(it.patternMatch!!.startIndex) + walkerMethod.apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $EXTENSION_METHOD_DESCRIPTOR + move-result v0 + if-eqz v0, :show + new-instance v0, Ljava/lang/Object; + invoke-direct {v0}, Ljava/lang/Object;->()V + return-object v0 + """, ExternalLabel("show", getInstruction(0)) + ) + } + } + } +} + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$PATCHES_PATH/GeneralAdsPatch;" + +@Suppress("unused") +val adsPatch = bytecodePatch( + HIDE_ADS.title, + HIDE_ADS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + bannerAdsPatch, + commentAdsPatch, + ) + + execute { + // region Filter promoted ads (does not work in popular or latest feed) + adPostFingerprint.methodOrThrow().apply { + val targetIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "children" + } + val targetRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex, """ + invoke-static {v$targetRegister}, $EXTENSION_CLASS_DESCRIPTOR->hideOldPostAds(Ljava/util/List;)Ljava/util/List; + move-result-object v$targetRegister + """ + ) + } + + // The new feeds work by inserting posts into lists. + // AdElementConverter is conveniently responsible for inserting all feed ads. + // By removing the appending instruction no ad posts gets appended to the feed. + val newAdPostMethod = newAdPostFingerprint.second.methodOrNull + ?: newAdPostLegacyFingerprint.methodOrThrow() + + newAdPostMethod.apply { + val startIndex = + 0.coerceAtLeast(indexOfFirstStringInstruction("android_feed_freeform_render_variant")) + val targetIndex = indexOfAddArrayListInstruction(this, startIndex) + val targetInstruction = getInstruction(targetIndex) + + replaceInstruction( + targetIndex, + "invoke-static {v${targetInstruction.registerC}, v${targetInstruction.registerD}}, " + + "$EXTENSION_CLASS_DESCRIPTOR->hideNewPostAds(Ljava/util/ArrayList;Ljava/lang/Object;)V" + ) + } + + updatePatchStatus( + "enableGeneralAds", + HIDE_ADS + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/ad/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/Fingerprints.kt new file mode 100644 index 000000000..ad7bd5971 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/Fingerprints.kt @@ -0,0 +1,78 @@ +package app.revanced.patches.reddit.ad + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val commentAdsFingerprint = legacyFingerprint( + name = "commentAdsFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.RETURN_OBJECT + ), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/PostDetailPresenter\$loadAd\$1;") && + method.name == "invokeSuspend" + }, +) + +internal val adPostFingerprint = legacyFingerprint( + name = "adPostFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + opcodes = listOf( + Opcode.INVOKE_DIRECT, + Opcode.IPUT_OBJECT + ), + // "children" are present throughout multiple versions + strings = listOf( + "children", + "uxExperiences" + ), + customFingerprint = { _, classDef -> + classDef.type.endsWith("/Listing;") + }, +) + +internal val newAdPostFingerprint = legacyFingerprint( + name = "newAdPostFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + strings = listOf( + "feedElement", + "android_feed_freeform_render_variant", + ), + customFingerprint = { method, _ -> + indexOfAddArrayListInstruction(method) >= 0 + }, +) + +internal val newAdPostLegacyFingerprint = legacyFingerprint( + name = "newAdPostLegacyFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + opcodes = listOf(Opcode.INVOKE_VIRTUAL), + strings = listOf( + "chain", + "feedElement" + ), + customFingerprint = { method, classDef -> + classDef.sourceFile == "AdElementConverter.kt" && + indexOfAddArrayListInstruction(method) >= 0 + }, +) + +internal fun indexOfAddArrayListInstruction(method: Method, index: Int = 0) = + method.indexOfFirstInstruction(index) { + getReference()?.toString() == "Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z" + } + diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/branding/name/CustomBrandingNamePatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/branding/name/CustomBrandingNamePatch.kt new file mode 100644 index 000000000..fee03bb0e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/branding/name/CustomBrandingNamePatch.kt @@ -0,0 +1,74 @@ +package app.revanced.patches.reddit.layout.branding.name + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.reddit.utils.patch.PatchList.CUSTOM_BRANDING_NAME_FOR_REDDIT +import app.revanced.patches.reddit.utils.settings.updatePatchStatus +import app.revanced.util.Utils.printInfo +import app.revanced.util.valueOrThrow +import java.io.FileWriter +import java.nio.file.Files + +private const val ORIGINAL_APP_NAME = "Reddit" +private const val APP_NAME = "RVX Reddit" + +@Suppress("unused") +val customBrandingNamePatch = resourcePatch( + CUSTOM_BRANDING_NAME_FOR_REDDIT.title, + CUSTOM_BRANDING_NAME_FOR_REDDIT.summary, + false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + val appNameOption = stringOption( + key = "appName", + default = ORIGINAL_APP_NAME, + values = mapOf( + "Default" to APP_NAME, + "Original" to ORIGINAL_APP_NAME, + ), + title = "App name", + description = "The name of the app.", + required = true + ) + + execute { + val appName = appNameOption + .valueOrThrow() + + if (appName == ORIGINAL_APP_NAME) { + printInfo("App name will remain unchanged as it matches the original.") + return@execute + } + + val resDirectory = get("res") + + val valuesV24Directory = resDirectory.resolve("values-v24") + if (!valuesV24Directory.isDirectory) + Files.createDirectories(valuesV24Directory.toPath()) + + val stringsXml = valuesV24Directory.resolve("strings.xml") + + if (!stringsXml.exists()) { + FileWriter(stringsXml).use { + it.write("") + } + } + + document("res/values-v24/strings.xml").use { document -> + mapOf( + "app_name" to appName + ).forEach { (k, v) -> + val stringElement = document.createElement("string") + + stringElement.setAttribute("name", k) + stringElement.textContent = v + + document.getElementsByTagName("resources").item(0).appendChild(stringElement) + } + } + + updatePatchStatus(CUSTOM_BRANDING_NAME_FOR_REDDIT) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/branding/packagename/ChangePackageNamePatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/branding/packagename/ChangePackageNamePatch.kt new file mode 100644 index 000000000..a795e5b56 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/branding/packagename/ChangePackageNamePatch.kt @@ -0,0 +1,111 @@ +package app.revanced.patches.reddit.layout.branding.packagename + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.reddit.utils.patch.PatchList.CHANGE_PACKAGE_NAME +import app.revanced.patches.reddit.utils.settings.updatePatchStatus +import app.revanced.util.Utils.printInfo +import app.revanced.util.valueOrThrow +import org.w3c.dom.Element + +private const val PACKAGE_NAME_REDDIT = "com.reddit.frontpage" +private const val CLONE_PACKAGE_NAME_REDDIT = "$PACKAGE_NAME_REDDIT.revanced" +private const val DEFAULT_PACKAGE_NAME_REDDIT = "$PACKAGE_NAME_REDDIT.rvx" + +private var redditPackageName = PACKAGE_NAME_REDDIT + +@Suppress("unused") +val changePackageNamePatch = resourcePatch( + CHANGE_PACKAGE_NAME.title, + CHANGE_PACKAGE_NAME.summary, + false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + val packageNameRedditOption = stringOption( + key = "packageNameReddit", + default = PACKAGE_NAME_REDDIT, + values = mapOf( + "Clone" to CLONE_PACKAGE_NAME_REDDIT, + "Default" to DEFAULT_PACKAGE_NAME_REDDIT, + "Original" to PACKAGE_NAME_REDDIT, + ), + title = "Package name of Reddit", + description = "The name of the package to rename the app to.", + required = true + ) + + execute { + fun replacePackageName() { + // replace strings + document("res/values/strings.xml").use { document -> + val resourcesNode = document.getElementsByTagName("resources").item(0) as Element + + val children = resourcesNode.childNodes + for (i in 0 until children.length) { + val node = children.item(i) as? Element ?: continue + + node.textContent = when (node.getAttribute("name")) { + "provider_authority_appdata", "provider_authority_file", + "provider_authority_userdata", "provider_workmanager_init" + -> node.textContent.replace(PACKAGE_NAME_REDDIT, redditPackageName) + + else -> continue + } + } + } + + // replace manifest permission and provider + get("AndroidManifest.xml").apply { + writeText( + readText() + .replace( + "android:authorities=\"$PACKAGE_NAME_REDDIT", + "android:authorities=\"$redditPackageName" + ) + ) + } + } + + redditPackageName = packageNameRedditOption + .valueOrThrow() + + if (redditPackageName == PACKAGE_NAME_REDDIT) { + printInfo("Package name will remain unchanged as it matches the original.") + return@execute + } + + // Ensure device runs Android. + try { + // RVX Manager + // ==== + // For some reason, in Android AAPT2, a compilation error occurs when changing the [strings.xml] of the Reddit + // This only affects RVX Manager, and has not yet found a valid workaround + Class.forName("android.os.Environment") + } catch (_: ClassNotFoundException) { + // CLI + replacePackageName() + } + + updatePatchStatus(CHANGE_PACKAGE_NAME) + } + + finalize { + if (redditPackageName != PACKAGE_NAME_REDDIT) { + get("AndroidManifest.xml").apply { + writeText( + readText() + .replace( + "package=\"$PACKAGE_NAME_REDDIT", + "package=\"$redditPackageName" + ) + .replace( + "$PACKAGE_NAME_REDDIT.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION", + "$redditPackageName.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" + ) + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/communities/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/communities/Fingerprints.kt new file mode 100644 index 000000000..8bc9c4084 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/communities/Fingerprints.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.reddit.layout.communities + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val communityRecommendationSectionFingerprint = legacyFingerprint( + name = "communityRecommendationSectionFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + strings = listOf("feedContext"), +) + +internal val communityRecommendationSectionParentFingerprint = legacyFingerprint( + name = "communityRecommendationSectionParentFingerprint", + returnType = "Ljava/lang/String;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + strings = listOf("community_recomendation_section_"), + customFingerprint = { method, _ -> + method.definingClass.startsWith("Lcom/reddit/onboardingfeedscomponents/communityrecommendation/impl/") && + method.name == "key" + } +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/communities/RecommendedCommunitiesPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/communities/RecommendedCommunitiesPatch.kt new file mode 100644 index 000000000..d4241956d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/communities/RecommendedCommunitiesPatch.kt @@ -0,0 +1,46 @@ +package app.revanced.patches.reddit.layout.communities + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH +import app.revanced.patches.reddit.utils.patch.PatchList.HIDE_RECOMMENDED_COMMUNITIES_SHELF +import app.revanced.patches.reddit.utils.settings.settingsPatch +import app.revanced.patches.reddit.utils.settings.updatePatchStatus +import app.revanced.util.fingerprint.methodOrThrow + +private const val EXTENSION_METHOD_DESCRIPTOR = + "$PATCHES_PATH/RecommendedCommunitiesPatch;->hideRecommendedCommunitiesShelf()Z" + +@Suppress("unused") +val recommendedCommunitiesPatch = bytecodePatch( + HIDE_RECOMMENDED_COMMUNITIES_SHELF.title, + HIDE_RECOMMENDED_COMMUNITIES_SHELF.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + communityRecommendationSectionFingerprint.methodOrThrow( + communityRecommendationSectionParentFingerprint + ).apply { + addInstructionsWithLabels( + 0, + """ + invoke-static {}, $EXTENSION_METHOD_DESCRIPTOR + move-result v0 + if-eqz v0, :off + return-void + """, ExternalLabel("off", getInstruction(0)) + ) + } + + updatePatchStatus( + "enableRecommendedCommunitiesShelf", + HIDE_RECOMMENDED_COMMUNITIES_SHELF + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/Fingerprints.kt new file mode 100644 index 000000000..cd80daa09 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/Fingerprints.kt @@ -0,0 +1,63 @@ +package app.revanced.patches.reddit.layout.navigation + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val bottomNavScreenFingerprint = legacyFingerprint( + name = "bottomNavScreenFingerprint", + returnType = "Landroid/view/View;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + customFingerprint = { method, _ -> + method.definingClass == "Lcom/reddit/launch/bottomnav/BottomNavScreen;" && + indexOfGetDimensionPixelSizeInstruction(method) >= 0 + } +) + +fun indexOfGetDimensionPixelSizeInstruction(methodDef: Method) = + methodDef.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.toString() == "Landroid/content/res/Resources;->getDimensionPixelSize(I)I" + } + +internal val bottomNavScreenHandlerFingerprint = legacyFingerprint( + name = "bottomNavScreenHandlerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "L", "Z", "Landroid/view/ViewGroup;", "L"), + customFingerprint = { method, _ -> + indexOfGetItemsInstruction(method) >= 0 && + indexOfSetSelectedItemTypeInstruction(method) >= 0 + } +) + +fun indexOfGetItemsInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "getItems" + } + +fun indexOfSetSelectedItemTypeInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setSelectedItemType" + } + +internal val bottomNavScreenOnGlobalLayoutFingerprint = legacyFingerprint( + name = "bottomNavScreenOnGlobalLayoutFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ), + customFingerprint = { methodDef, _ -> + methodDef.name == "onGlobalLayout" + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/NavigationButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/NavigationButtonsPatch.kt new file mode 100644 index 000000000..df3c587f5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/NavigationButtonsPatch.kt @@ -0,0 +1,87 @@ +package app.revanced.patches.reddit.layout.navigation + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH +import app.revanced.patches.reddit.utils.patch.PatchList.HIDE_NAVIGATION_BUTTONS +import app.revanced.patches.reddit.utils.settings.is_2024_18_or_greater +import app.revanced.patches.reddit.utils.settings.settingsPatch +import app.revanced.patches.reddit.utils.settings.updatePatchStatus +import app.revanced.util.Utils.printWarn +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.resolvable +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$PATCHES_PATH/NavigationButtonsPatch;" + +@Suppress("unused") +val navigationButtonsPatch = bytecodePatch( + HIDE_NAVIGATION_BUTTONS.title, + HIDE_NAVIGATION_BUTTONS.summary, + false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + + if (is_2024_18_or_greater) { + printWarn("\"Hide navigation buttons\" patch is not supported in this version. Use Reddit 2024.17.0 or earlier.") + return@execute + } + + if (bottomNavScreenFingerprint.resolvable()) { + val bottomNavScreenMutableClass = with(bottomNavScreenFingerprint.methodOrThrow()) { + val startIndex = indexOfGetDimensionPixelSizeInstruction(this) + val targetIndex = indexOfFirstInstructionOrThrow(startIndex, Opcode.NEW_INSTANCE) + val targetReference = + getInstruction(targetIndex).reference.toString() + + classBy { it.type == targetReference } + ?.mutableClass + ?: throw ClassNotFoundException("Failed to find class $targetReference") + } + + bottomNavScreenOnGlobalLayoutFingerprint.second.matchOrNull(bottomNavScreenMutableClass) + ?.let { + it.method.apply { + val startIndex = it.patternMatch!!.startIndex + val targetRegister = + getInstruction(startIndex).registerC + + addInstruction( + startIndex + 1, + "invoke-static {v$targetRegister}, $EXTENSION_CLASS_DESCRIPTOR->hideNavigationButtons(Landroid/view/ViewGroup;)V" + ) + } + } + } else { + // Legacy method. + bottomNavScreenHandlerFingerprint.methodOrThrow().apply { + val targetIndex = indexOfGetItemsInstruction(this) + 1 + val targetRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + invoke-static {v$targetRegister}, $EXTENSION_CLASS_DESCRIPTOR->hideNavigationButtons(Ljava/util/List;)Ljava/util/List; + move-result-object v$targetRegister + """ + ) + } + } + + updatePatchStatus( + "enableNavigationButtons", + HIDE_NAVIGATION_BUTTONS + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/premiumicon/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/premiumicon/Fingerprints.kt new file mode 100644 index 000000000..a6d308a07 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/premiumicon/Fingerprints.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.reddit.layout.premiumicon + +import app.revanced.util.fingerprint.legacyFingerprint + +internal val premiumIconFingerprint = legacyFingerprint( + name = "premiumIconFingerprint", + returnType = "Z", + customFingerprint = { method, _ -> + method.definingClass == "Lcom/reddit/domain/model/MyAccount;" && + method.name == "isPremiumSubscriber" + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/premiumicon/PremiumIconPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/premiumicon/PremiumIconPatch.kt new file mode 100644 index 000000000..d7d2eee1a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/premiumicon/PremiumIconPatch.kt @@ -0,0 +1,27 @@ +package app.revanced.patches.reddit.layout.premiumicon + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.reddit.utils.patch.PatchList.PREMIUM_ICON +import app.revanced.patches.reddit.utils.settings.updatePatchStatus +import app.revanced.util.fingerprint.methodOrThrow + +@Suppress("unused") +val premiumIconPatch = bytecodePatch( + PREMIUM_ICON.title, + PREMIUM_ICON.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + execute { + premiumIconFingerprint.methodOrThrow().addInstructions( + 0, """ + const/4 v0, 0x1 + return v0 + """ + ) + + updatePatchStatus(PREMIUM_ICON) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/recentlyvisited/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/recentlyvisited/Fingerprints.kt new file mode 100644 index 000000000..a1c24dd0f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/recentlyvisited/Fingerprints.kt @@ -0,0 +1,37 @@ +package app.revanced.patches.reddit.layout.recentlyvisited + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +internal val communityDrawerPresenterConstructorFingerprint = legacyFingerprint( + name = "communityDrawerPresenterConstructorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + strings = listOf("matureFeedFeatures", "communityDrawerSettings"), + customFingerprint = { method, _ -> + indexOfHeaderItemInstruction(method) >= 0 + } +) + +fun indexOfHeaderItemInstruction(method: Method) = + method.indexOfFirstInstruction { + getReference()?.name == "RECENTLY_VISITED" + } + +internal val communityDrawerPresenterFingerprint = legacyFingerprint( + name = "communityDrawerPresenterFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.XOR_INT_2ADDR, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + ) +) diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/recentlyvisited/RecentlyVisitedShelfPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/recentlyvisited/RecentlyVisitedShelfPatch.kt new file mode 100644 index 000000000..f4b221a14 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/recentlyvisited/RecentlyVisitedShelfPatch.kt @@ -0,0 +1,78 @@ +package app.revanced.patches.reddit.layout.recentlyvisited + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH +import app.revanced.patches.reddit.utils.patch.PatchList.HIDE_RECENTLY_VISITED_SHELF +import app.revanced.patches.reddit.utils.settings.settingsPatch +import app.revanced.patches.reddit.utils.settings.updatePatchStatus +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction + +private const val EXTENSION_METHOD_DESCRIPTOR = + "$PATCHES_PATH/RecentlyVisitedShelfPatch;" + + "->" + + "hideRecentlyVisitedShelf(Ljava/util/List;)Ljava/util/List;" + +@Suppress("unused") +val recentlyVisitedShelfPatch = bytecodePatch( + HIDE_RECENTLY_VISITED_SHELF.title, + HIDE_RECENTLY_VISITED_SHELF.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + + val recentlyVisitedReference = + with(communityDrawerPresenterConstructorFingerprint.methodOrThrow()) { + val recentlyVisitedFieldIndex = indexOfHeaderItemInstruction(this) + val recentlyVisitedObjectIndex = + indexOfFirstInstructionOrThrow(recentlyVisitedFieldIndex, Opcode.IPUT_OBJECT) + + getInstruction(recentlyVisitedObjectIndex).reference.toString() + } + + communityDrawerPresenterFingerprint.methodOrThrow( + communityDrawerPresenterConstructorFingerprint + ).apply { + val recentlyVisitedObjectIndex = + indexOfFirstInstructionOrThrow { + (this as? ReferenceInstruction)?.reference?.toString() == recentlyVisitedReference + } + + arrayOf( + indexOfFirstInstructionOrThrow( + recentlyVisitedObjectIndex, + Opcode.INVOKE_STATIC + ), + indexOfFirstInstructionReversedOrThrow( + recentlyVisitedObjectIndex, + Opcode.INVOKE_STATIC + ) + ).forEach { staticIndex -> + val insertRegister = + getInstruction(staticIndex + 1).registerA + + addInstructions( + staticIndex + 2, """ + invoke-static {v$insertRegister}, $EXTENSION_METHOD_DESCRIPTOR + move-result-object v$insertRegister + """ + ) + } + } + + updatePatchStatus( + "enableRecentlyVisitedShelf", + HIDE_RECENTLY_VISITED_SHELF + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/Fingerprints.kt new file mode 100644 index 000000000..13767b47b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/Fingerprints.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.reddit.layout.screenshotpopup + +import app.revanced.util.fingerprint.legacyFingerprint + +internal val screenshotTakenBannerFingerprint = legacyFingerprint( + name = "screenshotTakenBannerFingerprint", + returnType = "V", + parameters = listOf("Landroidx/compose/runtime/", "I"), + customFingerprint = { method, classDef -> + classDef.type.endsWith("\$ScreenshotTakenBannerKt\$lambda-1\$1;") && + method.name == "invoke" + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatch.kt new file mode 100644 index 000000000..48f009c59 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatch.kt @@ -0,0 +1,43 @@ +package app.revanced.patches.reddit.layout.screenshotpopup + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH +import app.revanced.patches.reddit.utils.patch.PatchList.DISABLE_SCREENSHOT_POPUP +import app.revanced.patches.reddit.utils.settings.settingsPatch +import app.revanced.patches.reddit.utils.settings.updatePatchStatus +import app.revanced.util.fingerprint.methodOrThrow + +private const val EXTENSION_METHOD_DESCRIPTOR = + "$PATCHES_PATH/ScreenshotPopupPatch;->disableScreenshotPopup()Z" + +@Suppress("unused") +val screenshotPopupPatch = bytecodePatch( + DISABLE_SCREENSHOT_POPUP.title, + DISABLE_SCREENSHOT_POPUP.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + screenshotTakenBannerFingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $EXTENSION_METHOD_DESCRIPTOR + move-result v0 + if-eqz v0, :dismiss + return-void + """, ExternalLabel("dismiss", getInstruction(0)) + ) + } + + updatePatchStatus( + "enableScreenshotPopup", + DISABLE_SCREENSHOT_POPUP + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/subredditdialog/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/subredditdialog/Fingerprints.kt new file mode 100644 index 000000000..bebd879c6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/subredditdialog/Fingerprints.kt @@ -0,0 +1,42 @@ +package app.revanced.patches.reddit.layout.subredditdialog + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val frequentUpdatesSheetScreenFingerprint = legacyFingerprint( + name = "frequentUpdatesSheetScreenFingerprint", + returnType = "Landroid/view/View;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + opcodes = listOf( + Opcode.CONST, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + Opcode.IF_EQZ + ), + customFingerprint = { _, classDef -> + classDef.type == "Lcom/reddit/screens/pager/FrequentUpdatesSheetScreen;" + } +) + +internal val redditAlertDialogsFingerprint = legacyFingerprint( + name = "redditAlertDialogsFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + customFingerprint = { method, _ -> + method.definingClass.startsWith("Lcom/reddit/screen/dialog/") && + indexOfSetBackgroundTintListInstruction(method) >= 0 + } +) + +fun indexOfSetBackgroundTintListInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setBackgroundTintList" + } \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/subredditdialog/SubRedditDialogPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/subredditdialog/SubRedditDialogPatch.kt new file mode 100644 index 000000000..fa57ecabe --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/subredditdialog/SubRedditDialogPatch.kt @@ -0,0 +1,66 @@ +package app.revanced.patches.reddit.layout.subredditdialog + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH +import app.revanced.patches.reddit.utils.patch.PatchList.REMOVE_SUBREDDIT_DIALOG +import app.revanced.patches.reddit.utils.settings.settingsPatch +import app.revanced.patches.reddit.utils.settings.updatePatchStatus +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$PATCHES_PATH/RemoveSubRedditDialogPatch;" + +@Suppress("unused") +val subRedditDialogPatch = bytecodePatch( + REMOVE_SUBREDDIT_DIALOG.title, + REMOVE_SUBREDDIT_DIALOG.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + frequentUpdatesSheetScreenFingerprint.matchOrThrow().let { + it.method.apply { + val cancelButtonViewIndex = it.patternMatch!!.startIndex + 2 + val cancelButtonViewRegister = + getInstruction(cancelButtonViewIndex).registerA + + addInstruction( + cancelButtonViewIndex + 1, + "invoke-static {v$cancelButtonViewRegister}, $EXTENSION_CLASS_DESCRIPTOR->dismissDialog(Landroid/view/View;)V" + ) + } + } + + redditAlertDialogsFingerprint.methodOrThrow().apply { + val backgroundTintIndex = indexOfSetBackgroundTintListInstruction(this) + val insertIndex = + indexOfFirstInstructionOrThrow(backgroundTintIndex) { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setTextAppearance" + } + val insertRegister = getInstruction(insertIndex).registerC + + addInstruction( + insertIndex, + "invoke-static {v$insertRegister}, $EXTENSION_CLASS_DESCRIPTOR->confirmDialog(Landroid/widget/TextView;)V" + ) + } + + updatePatchStatus( + "enableSubRedditDialog", + REMOVE_SUBREDDIT_DIALOG + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/toolbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/toolbar/Fingerprints.kt new file mode 100644 index 000000000..a6ae79283 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/toolbar/Fingerprints.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.reddit.layout.toolbar + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val homePagerScreenFingerprint = legacyFingerprint( + name = "homePagerScreenFingerprint", + returnType = "Landroid/view/View;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/LayoutInflater;", "Landroid/view/ViewGroup;"), + strings = listOf("recapNavEntryPointDelegate"), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/HomePagerScreen;") + } +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/toolbar/ToolBarButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/toolbar/ToolBarButtonPatch.kt new file mode 100644 index 000000000..0ddab6e57 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/toolbar/ToolBarButtonPatch.kt @@ -0,0 +1,45 @@ +package app.revanced.patches.reddit.layout.toolbar + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH +import app.revanced.patches.reddit.utils.patch.PatchList.HIDE_TOOLBAR_BUTTON +import app.revanced.patches.reddit.utils.settings.settingsPatch +import app.revanced.patches.reddit.utils.settings.updatePatchStatus +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private const val EXTENSION_METHOD_DESCRIPTOR = + "$PATCHES_PATH/ToolBarButtonPatch;->hideToolBarButton(Landroid/view/View;)V" + +@Suppress("unused") +@Deprecated("This patch is deprecated until Reddit adds a button like r/place or Reddit recap button to the toolbar.") +val toolBarButtonPatch = bytecodePatch { + // compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + homePagerScreenFingerprint.matchOrThrow().let { + it.method.apply { + val stringIndex = it.stringMatches!!.first().index + val insertIndex = indexOfFirstInstructionOrThrow(stringIndex, Opcode.CHECK_CAST) + val insertRegister = + getInstruction(insertIndex).registerA + + addInstruction( + insertIndex, + "invoke-static {v$insertRegister}, $EXTENSION_METHOD_DESCRIPTOR" + ) + } + } + + updatePatchStatus( + "enableToolBarButton", + HIDE_TOOLBAR_BUTTON + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/Fingerprints.kt new file mode 100644 index 000000000..12fe68511 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/Fingerprints.kt @@ -0,0 +1,38 @@ +package app.revanced.patches.reddit.misc.openlink + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction + +internal val customReportsFingerprint = legacyFingerprint( + name = "customReportsFingerprint", + returnType = "V", + strings = listOf("https://www.crisistextline.org/", "screenNavigator"), + customFingerprint = { method, _ -> + indexOfScreenNavigatorInstruction(method) >= 0 + } +) + +fun indexOfScreenNavigatorInstruction(method: Method) = + method.indexOfFirstInstruction { + (this as? ReferenceInstruction)?.reference?.toString() + ?.contains("Landroid/app/Activity;Landroid/net/Uri;") == true + } + +internal val screenNavigatorFingerprint = legacyFingerprint( + name = "screenNavigatorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + opcodes = listOf( + Opcode.CONST_STRING, + Opcode.INVOKE_STATIC, + Opcode.CONST_STRING, + Opcode.INVOKE_STATIC + ), + strings = listOf("activity", "uri"), + customFingerprint = { _, classDef -> classDef.sourceFile == "RedditScreenNavigator.kt" } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/OpenLinksDirectlyPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/OpenLinksDirectlyPatch.kt new file mode 100644 index 000000000..d346ac29c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/OpenLinksDirectlyPatch.kt @@ -0,0 +1,41 @@ +package app.revanced.patches.reddit.misc.openlink + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH +import app.revanced.patches.reddit.utils.patch.PatchList.OPEN_LINKS_DIRECTLY +import app.revanced.patches.reddit.utils.settings.settingsPatch +import app.revanced.patches.reddit.utils.settings.updatePatchStatus + +private const val EXTENSION_METHOD_DESCRIPTOR = + "$PATCHES_PATH/OpenLinksDirectlyPatch;" + + "->" + + "parseRedirectUri(Landroid/net/Uri;)Landroid/net/Uri;" + +@Suppress("unused") +val openLinksDirectlyPatch = bytecodePatch( + OPEN_LINKS_DIRECTLY.title, + OPEN_LINKS_DIRECTLY.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + screenNavigatorMethodResolverPatch + ) + + execute { + screenNavigatorMethod.addInstructions( + 0, """ + invoke-static {p2}, $EXTENSION_METHOD_DESCRIPTOR + move-result-object p2 + """ + ) + + updatePatchStatus( + "enableOpenLinksDirectly", + OPEN_LINKS_DIRECTLY + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/OpenLinksExternallyPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/OpenLinksExternallyPatch.kt new file mode 100644 index 000000000..0d74acc0a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/OpenLinksExternallyPatch.kt @@ -0,0 +1,50 @@ +package app.revanced.patches.reddit.misc.openlink + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH +import app.revanced.patches.reddit.utils.patch.PatchList.OPEN_LINKS_EXTERNALLY +import app.revanced.patches.reddit.utils.settings.settingsPatch +import app.revanced.patches.reddit.utils.settings.updatePatchStatus +import app.revanced.util.indexOfFirstStringInstructionOrThrow + +private const val EXTENSION_METHOD_DESCRIPTOR = + "$PATCHES_PATH/OpenLinksExternallyPatch;" + + "->" + + "openLinksExternally(Landroid/app/Activity;Landroid/net/Uri;)Z" + +@Suppress("unused") +val openLinksExternallyPatch = bytecodePatch( + OPEN_LINKS_EXTERNALLY.title, + OPEN_LINKS_EXTERNALLY.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + screenNavigatorMethodResolverPatch + ) + + execute { + screenNavigatorMethod.apply { + val insertIndex = indexOfFirstStringInstructionOrThrow("uri") + 2 + + addInstructionsWithLabels( + insertIndex, """ + invoke-static {p1, p2}, $EXTENSION_METHOD_DESCRIPTOR + move-result v0 + if-eqz v0, :dismiss + return-void + """, ExternalLabel("dismiss", getInstruction(insertIndex)) + ) + } + + updatePatchStatus( + "enableOpenLinksExternally", + OPEN_LINKS_EXTERNALLY + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/ScreenNavigatorMethodResolverPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/ScreenNavigatorMethodResolverPatch.kt new file mode 100644 index 000000000..7a0c3f190 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/ScreenNavigatorMethodResolverPatch.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.reddit.misc.openlink + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getWalkerMethod + +lateinit var screenNavigatorMethod: MutableMethod + +val screenNavigatorMethodResolverPatch = bytecodePatch( + description = "screenNavigatorMethodResolverPatch" +) { + execute { + screenNavigatorMethod = + // ~ Reddit 2024.25.3 + screenNavigatorFingerprint.second.methodOrNull + // Reddit 2024.26.1 ~ + ?: with(customReportsFingerprint.methodOrThrow()) { + getWalkerMethod(indexOfScreenNavigatorInstruction(this)) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/misc/tracking/url/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/tracking/url/Fingerprints.kt new file mode 100644 index 000000000..41ecdb04f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/tracking/url/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.reddit.misc.tracking.url + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val shareLinkFormatterFingerprint = legacyFingerprint( + name = "shareLinkFormatterFingerprint", + returnType = "Ljava/lang/String;", + parameters = listOf("Ljava/lang/String;", "Ljava/util/Map;"), + customFingerprint = { method, _ -> + indexOfClearQueryInstruction(method) >= 0 + } +) + +fun indexOfClearQueryInstruction(method: Method) = + method.indexOfFirstInstruction { + getReference()?.toString() == "Landroid/net/Uri${'$'}Builder;->clearQuery()Landroid/net/Uri${'$'}Builder;" + } diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/misc/tracking/url/SanitizeUrlQueryPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/tracking/url/SanitizeUrlQueryPatch.kt new file mode 100644 index 000000000..22a14728f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/tracking/url/SanitizeUrlQueryPatch.kt @@ -0,0 +1,44 @@ +package app.revanced.patches.reddit.misc.tracking.url + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.reddit.utils.extension.Constants.PATCHES_PATH +import app.revanced.patches.reddit.utils.patch.PatchList.SANITIZE_SHARING_LINKS +import app.revanced.patches.reddit.utils.settings.settingsPatch +import app.revanced.patches.reddit.utils.settings.updatePatchStatus +import app.revanced.util.fingerprint.methodOrThrow + +private const val SANITIZE_METHOD_DESCRIPTOR = + "$PATCHES_PATH/SanitizeUrlQueryPatch;->stripQueryParameters()Z" + +@Suppress("unused") +val sanitizeUrlQueryPatch = bytecodePatch( + SANITIZE_SHARING_LINKS.title, + SANITIZE_SHARING_LINKS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + shareLinkFormatterFingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, + """ + invoke-static {}, $SANITIZE_METHOD_DESCRIPTOR + move-result v0 + if-eqz v0, :off + return-object p0 + """, ExternalLabel("off", getInstruction(0)) + ) + } + + updatePatchStatus( + "enableSanitizeUrlQuery", + SANITIZE_SHARING_LINKS + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/utils/compatibility/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/compatibility/Constants.kt new file mode 100644 index 000000000..7ef039926 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/compatibility/Constants.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.reddit.utils.compatibility + +import app.revanced.patcher.patch.PackageName +import app.revanced.patcher.patch.VersionName + +internal object Constants { + internal const val REDDIT_PACKAGE_NAME = "com.reddit.frontpage" + + val COMPATIBLE_PACKAGE: Pair?> = Pair( + REDDIT_PACKAGE_NAME, + null + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/utils/extension/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/extension/Constants.kt new file mode 100644 index 000000000..0ae266b52 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/extension/Constants.kt @@ -0,0 +1,7 @@ +package app.revanced.patches.reddit.utils.extension + +@Suppress("MemberVisibilityCanBePrivate") +internal object Constants { + const val EXTENSION_PATH = "Lapp/revanced/extension/reddit" + const val PATCHES_PATH = "$EXTENSION_PATH/patches" +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/utils/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/extension/SharedExtensionPatch.kt new file mode 100644 index 000000000..92d2851d9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/extension/SharedExtensionPatch.kt @@ -0,0 +1,6 @@ +package app.revanced.patches.reddit.utils.extension + +import app.revanced.patches.reddit.utils.extension.hooks.applicationInitHook +import app.revanced.patches.shared.extension.sharedExtensionPatch + +val sharedExtensionPatch = sharedExtensionPatch(applicationInitHook) diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/utils/extension/hooks/ApplicationInitHook.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/extension/hooks/ApplicationInitHook.kt new file mode 100644 index 000000000..dd8f64431 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/extension/hooks/ApplicationInitHook.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.reddit.utils.extension.hooks + +import app.revanced.patches.shared.extension.extensionHook + +internal val applicationInitHook = extensionHook { + custom { method, _ -> + method.definingClass.endsWith("/FrontpageApplication;") && + method.name == "onCreate" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/utils/patch/PatchList.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/patch/PatchList.kt new file mode 100644 index 000000000..434e5321f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/patch/PatchList.kt @@ -0,0 +1,64 @@ +package app.revanced.patches.reddit.utils.patch + +internal enum class PatchList( + val title: String, + val summary: String, + var included: Boolean? = false +) { + CHANGE_PACKAGE_NAME( + "Change package name", + "Changes the package name for Reddit to the name specified in patch options." + ), + CUSTOM_BRANDING_NAME_FOR_REDDIT( + "Custom branding name for Reddit", + "Renames the Reddit app to the name specified in patch options." + ), + DISABLE_SCREENSHOT_POPUP( + "Disable screenshot popup", + "Adds an option to disable the popup that appears when taking a screenshot." + ), + HIDE_RECENTLY_VISITED_SHELF( + "Hide Recently Visited shelf", + "Adds an option to hide the Recently Visited shelf in the sidebar." + ), + HIDE_ADS( + "Hide ads", + "Adds options to hide ads." + ), + HIDE_NAVIGATION_BUTTONS( + "Hide navigation buttons", + "Adds options to hide buttons in the navigation bar." + ), + HIDE_RECOMMENDED_COMMUNITIES_SHELF( + "Hide recommended communities shelf", + "Adds an option to hide the recommended communities shelves in subreddits." + ), + HIDE_TOOLBAR_BUTTON( + "Hide toolbar button", + "Adds an option to hide the r/place or Reddit recap button in the toolbar." + ), + OPEN_LINKS_DIRECTLY( + "Open links directly", + "Adds an option to skip over redirection URLs in external links." + ), + OPEN_LINKS_EXTERNALLY( + "Open links externally", + "Adds an option to always open links in your browser instead of in the in-app-browser." + ), + PREMIUM_ICON( + "Premium icon", + "Unlocks premium app icons." + ), + REMOVE_SUBREDDIT_DIALOG( + "Remove subreddit dialog", + "Adds options to remove the NSFW community warning and notifications suggestion dialogs by dismissing them automatically." + ), + SANITIZE_SHARING_LINKS( + "Sanitize sharing links", + "Adds an option to remove tracking query parameters from URLs when sharing links." + ), + SETTINGS_FOR_REDDIT( + "Settings for Reddit", + "Applies mandatory patches to implement ReVanced Extended settings into the application." + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/utils/settings/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/settings/Fingerprints.kt new file mode 100644 index 000000000..8390f47b4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/settings/Fingerprints.kt @@ -0,0 +1,49 @@ +package app.revanced.patches.reddit.utils.settings + +import app.revanced.patches.reddit.utils.extension.Constants.EXTENSION_PATH +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val acknowledgementsLabelBuilderFingerprint = legacyFingerprint( + name = "acknowledgementsLabelBuilderFingerprint", + returnType = "Z", + parameters = listOf("Landroidx/preference/Preference;"), + strings = listOf("onboardingAnalytics"), + customFingerprint = { method, _ -> + method.definingClass.startsWith("Lcom/reddit/screen/settings/preferences/") + } +) + +internal val ossLicensesMenuActivityOnCreateFingerprint = legacyFingerprint( + name = "ossLicensesMenuActivityOnCreateFingerprint", + returnType = "V", + opcodes = listOf( + Opcode.IGET_BOOLEAN, + Opcode.IF_EQZ, + Opcode.INVOKE_STATIC + ), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/OssLicensesMenuActivity;") && + method.name == "onCreate" + } +) + +internal val redditInternalFeaturesFingerprint = legacyFingerprint( + name = "redditInternalFeaturesFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + strings = listOf("RELEASE"), + customFingerprint = { methodDef, _ -> + !methodDef.definingClass.startsWith("Lcom/") + } +) + +internal val settingsStatusLoadFingerprint = legacyFingerprint( + name = "settingsStatusLoadFingerprint", + customFingerprint = { method, _ -> + method.definingClass.endsWith("$EXTENSION_PATH/settings/SettingsStatus;") && + method.name == "load" + } +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/utils/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/settings/SettingsPatch.kt new file mode 100644 index 000000000..65ca353e8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/settings/SettingsPatch.kt @@ -0,0 +1,186 @@ +package app.revanced.patches.reddit.utils.settings + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.reddit.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.reddit.utils.extension.Constants.EXTENSION_PATH +import app.revanced.patches.reddit.utils.extension.sharedExtensionPatch +import app.revanced.patches.reddit.utils.patch.PatchList +import app.revanced.patches.reddit.utils.patch.PatchList.SETTINGS_FOR_REDDIT +import app.revanced.patches.shared.sharedSettingFingerprint +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstStringInstructionOrThrow +import app.revanced.util.valueOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import kotlin.io.path.exists + +private const val EXTENSION_METHOD_DESCRIPTOR = + "$EXTENSION_PATH/settings/ActivityHook;->initialize(Landroid/app/Activity;)V" + +private lateinit var acknowledgementsLabelBuilderMethod: MutableMethod +private lateinit var settingsStatusLoadMethod: MutableMethod + +var is_2024_18_or_greater = false + private set + +private val settingsBytecodePatch = bytecodePatch( + description = "settingsBytecodePatch" +) { + + execute { + + /** + * Set version info + */ + redditInternalFeaturesFingerprint.methodOrThrow().apply { + val versionIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.CONST_STRING + && (this as? BuilderInstruction21c)?.reference.toString().startsWith("202") + } + + val versionNumber = + getInstruction(versionIndex).reference.toString() + .replace(".", "").toInt() + + is_2024_18_or_greater = 2024180 <= versionNumber + } + + /** + * Set SharedPrefCategory + */ + sharedSettingFingerprint.methodOrThrow().apply { + val stringIndex = indexOfFirstInstructionOrThrow(Opcode.CONST_STRING) + val stringRegister = getInstruction(stringIndex).registerA + + replaceInstruction( + stringIndex, + "const-string v$stringRegister, \"reddit_revanced\"" + ) + } + + /** + * Replace settings label + */ + acknowledgementsLabelBuilderMethod = + acknowledgementsLabelBuilderFingerprint.methodOrThrow() + + /** + * Initialize settings activity + */ + ossLicensesMenuActivityOnCreateFingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.patternMatch!!.startIndex + 1 + + addInstructions( + insertIndex, """ + invoke-static {p0}, $EXTENSION_METHOD_DESCRIPTOR + return-void + """ + ) + } + } + + settingsStatusLoadMethod = settingsStatusLoadFingerprint.methodOrThrow() + } +} + +internal fun updateSettingsLabel(label: String) = + acknowledgementsLabelBuilderMethod.apply { + val stringIndex = indexOfFirstStringInstructionOrThrow("onboardingAnalytics") + val insertIndex = indexOfFirstInstructionReversedOrThrow(stringIndex) { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "getString" + } + 2 + val insertRegister = + getInstruction(insertIndex - 1).registerA + + addInstruction( + insertIndex, + "const-string v$insertRegister, \"$label\"" + ) + } + +internal fun updatePatchStatus(description: String) = + settingsStatusLoadMethod.addInstruction( + 0, + "invoke-static {}, $EXTENSION_PATH/settings/SettingsStatus;->$description()V" + ) + +internal fun updatePatchStatus(patch: PatchList) { + patch.included = true +} + +internal fun updatePatchStatus( + description: String, + patch: PatchList +) { + updatePatchStatus(description) + updatePatchStatus(patch) +} + +private const val DEFAULT_LABEL = "ReVanced Extended" + +val settingsPatch = resourcePatch( + SETTINGS_FOR_REDDIT.title, + SETTINGS_FOR_REDDIT.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + sharedExtensionPatch, + settingsBytecodePatch + ) + + val settingsLabelOption = stringOption( + key = "settingsLabel", + default = DEFAULT_LABEL, + title = "RVX settings menu name", + description = "The name of the RVX settings menu.", + required = true + ) + + execute { + /** + * Replace settings icon and label + */ + val settingsLabel = settingsLabelOption + .valueOrThrow() + + arrayOf( + "preferences.xml", + "preferences_logged_in.xml", + "preferences_logged_in_old.xml", + ).forEach { targetXML -> + val resDirectory = get("res") + val targetXml = resDirectory.resolve("xml").resolve(targetXML).toPath() + + if (targetXml.exists()) { + val preference = get("res/xml/$targetXML") + + preference.writeText( + preference.readText() + .replace( + "\"@drawable/icon_text_post\" android:title=\"@string/label_acknowledgements\"", + "\"@drawable/icon_beta_planet\" android:title=\"$settingsLabel\"" + ) + ) + } + } + + updateSettingsLabel(settingsLabel) + updatePatchStatus(SETTINGS_FOR_REDDIT) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/Fingerprints.kt new file mode 100644 index 000000000..699e4fb0e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/Fingerprints.kt @@ -0,0 +1,128 @@ +package app.revanced.patches.shared + +import app.revanced.patches.shared.extension.Constants.EXTENSION_SETTING_CLASS_DESCRIPTOR +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val createPlayerRequestBodyWithModelFingerprint = legacyFingerprint( + name = "createPlayerRequestBodyWithModelFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf(Opcode.OR_INT_LIT16), + customFingerprint = { method, _ -> + indexOfModelInstruction(method) >= 0 && + indexOfReleaseInstruction(method) >= 0 + } +) + +fun indexOfModelInstruction(method: Method) = + method.indexOfFieldReference("Landroid/os/Build;->MODEL:Ljava/lang/String;") + +fun indexOfReleaseInstruction(method: Method) = + method.indexOfFieldReference("Landroid/os/Build${'$'}VERSION;->RELEASE:Ljava/lang/String;") + +private fun Method.indexOfFieldReference(string: String) = indexOfFirstInstruction { + val reference = getReference() ?: return@indexOfFirstInstruction false + + reference.toString() == string +} + +/** + * On YouTube, this class is 'Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;' + * On YouTube Music, class names are obfuscated. + */ +internal val formatStreamModelConstructorFingerprint = legacyFingerprint( + name = "formatStreamModelConstructorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + opcodes = listOf( + Opcode.IGET_WIDE, + Opcode.IPUT_WIDE, + ), + literals = listOf(45374643L), +) + +internal val mdxPlayerDirectorSetVideoStageFingerprint = legacyFingerprint( + name = "mdxPlayerDirectorSetVideoStageFingerprint", + strings = listOf("MdxDirector setVideoStage ad should be null when videoStage is not an Ad state ") +) + +internal val sharedSettingFingerprint = legacyFingerprint( + name = "sharedSettingFingerprint", + returnType = "V", + customFingerprint = { method, _ -> + method.definingClass == EXTENSION_SETTING_CLASS_DESCRIPTOR && + method.name == "" + } +) + +internal val spannableStringBuilderFingerprint = legacyFingerprint( + name = "spannableStringBuilderFingerprint", + returnType = "Ljava/lang/CharSequence;", + strings = listOf("Failed to set PB Style Run Extension in TextComponentSpec. Extension id: %s"), + customFingerprint = { method, _ -> + indexOfSpannableStringInstruction(method) >= 0 + } +) + +const val SPANNABLE_STRING_REFERENCE = + "Landroid/text/SpannableString;->valueOf(Ljava/lang/CharSequence;)Landroid/text/SpannableString;" + +fun indexOfSpannableStringInstruction(method: Method) = method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_STATIC && + getReference()?.toString() == SPANNABLE_STRING_REFERENCE +} + +internal val startVideoInformerFingerprint = legacyFingerprint( + name = "startVideoInformerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + opcodes = listOf( + Opcode.INVOKE_INTERFACE, + Opcode.RETURN_VOID + ), + strings = listOf("pc"), + customFingerprint = { method, _ -> + method.implementation + ?.instructions + ?.withIndex() + ?.filter { (_, instruction) -> + instruction.opcode == Opcode.CONST_STRING + } + ?.map { (index, _) -> index } + ?.size == 1 + } +) + +internal val videoLengthFingerprint = legacyFingerprint( + name = "videoLengthFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + strings = listOf("Gaplessly transitioning away from an Ad before it ends.") +) + +internal val dislikeFingerprint = legacyFingerprint( + name = "dislikeFingerprint", + returnType = "V", + strings = listOf("like/dislike") +) + +internal val likeFingerprint = legacyFingerprint( + name = "likeFingerprint", + returnType = "V", + strings = listOf("like/like") +) + +internal val removeLikeFingerprint = legacyFingerprint( + name = "removeLikeFingerprint", + returnType = "V", + strings = listOf("like/removelike") +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/ads/BaseAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/ads/BaseAdsPatch.kt new file mode 100644 index 000000000..66863f474 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/ads/BaseAdsPatch.kt @@ -0,0 +1,137 @@ +package app.revanced.patches.shared.ads + +import app.revanced.patcher.Match +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.extension.Constants.PATCHES_PATH +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$PATCHES_PATH/FullscreenAdsPatch;" + +fun baseAdsPatch( + classDescriptor: String, + methodDescriptor: String, +) = bytecodePatch( + description = "baseAdsPatch" +) { + execute { + + setOf( + sslGuardFingerprint, + videoAdsFingerprint, + ).forEach { fingerprint -> + fingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $classDescriptor->$methodDescriptor()Z + move-result v0 + if-nez v0, :show_ads + return-void + """, ExternalLabel("show_ads", getInstruction(0)) + ) + } + } + + musicAdsFingerprint.methodOrThrow().apply { + val targetIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.returnType == "V" && + reference.parameterTypes.size == 1 && + reference.parameterTypes.first() == "Z" + } + + getWalkerMethod(targetIndex) + .addInstructions( + 0, """ + invoke-static {p1}, $classDescriptor->$methodDescriptor(Z)Z + move-result p1 + """ + ) + } + + advertisingIdFingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.stringMatches!!.first().index + val insertRegister = getInstruction(insertIndex).registerA + addInstructionsWithLabels( + insertIndex, """ + invoke-static {}, $classDescriptor->$methodDescriptor()Z + move-result v$insertRegister + if-nez v$insertRegister, :enable_id + return-void + """, ExternalLabel("enable_id", getInstruction(insertIndex)) + ) + } + } + + } +} + +internal fun MutableMethod.hookNonLithoFullscreenAds(literal: Long) { + val targetIndex = indexOfFirstLiteralInstructionOrThrow(literal) + 2 + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $EXTENSION_CLASS_DESCRIPTOR->hideFullscreenAds(Landroid/view/View;)V" + ) +} + +internal fun Match.hookLithoFullscreenAds() { + method.apply { + val dialogCodeIndex = patternMatch!!.endIndex + val dialogCodeField = + getInstruction(dialogCodeIndex).reference as FieldReference + if (dialogCodeField.type != "I") + throw PatchException("Invalid dialogCodeField: $dialogCodeField") + + var prependInstructions = """ + move-object/from16 v0, p1 + move-object/from16 v1, p2 + """ + + if (parameterTypes.firstOrNull() != "[B") { + val toByteArrayReference = getInstruction( + indexOfFirstInstructionOrThrow { + getReference()?.name == "toByteArray" + } + ).reference + + prependInstructions += """ + invoke-virtual {v0}, $toByteArrayReference + move-result-object v0 + """ + } + + // Disable fullscreen ads + addInstructionsWithLabels( + 0, prependInstructions + """ + check-cast v1, ${dialogCodeField.definingClass} + iget v1, v1, $dialogCodeField + invoke-static {v0, v1}, $EXTENSION_CLASS_DESCRIPTOR->disableFullscreenAds([BI)Z + move-result v1 + if-eqz v1, :show + return-void + """, ExternalLabel("show", getInstruction(0)) + ) + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/ads/Fingerprints.kt new file mode 100644 index 000000000..6853546b4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/ads/Fingerprints.kt @@ -0,0 +1,48 @@ +package app.revanced.patches.shared.ads + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.util.MethodUtil + +internal val advertisingIdFingerprint = legacyFingerprint( + name = "advertisingIdFingerprint", + returnType = "V", + strings = listOf("a."), + customFingerprint = { method, classDef -> + MethodUtil.isConstructor(method) && + classDef.fields.find { it.type == "Ljava/util/Random;" } != null + } +) + +internal val sslGuardFingerprint = legacyFingerprint( + name = "sslGuardFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + strings = listOf("Cannot initialize SslGuardSocketFactory will null"), +) + +internal val musicAdsFingerprint = legacyFingerprint( + name = "musicAdsFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.CONST_WIDE_16, + Opcode.IPUT_WIDE, + Opcode.CONST_WIDE_16, + Opcode.IPUT_WIDE, + Opcode.IPUT_WIDE, + Opcode.IPUT_WIDE, + Opcode.IPUT_WIDE, + Opcode.CONST_4, + ), + literals = listOf(4L) +) + +internal val videoAdsFingerprint = legacyFingerprint( + name = "videoAdsFingerprint", + returnType = "V", + strings = listOf("markFillRequested", "requestEnterSlot") +) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/captions/BaseAutoCaptionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/captions/BaseAutoCaptionsPatch.kt new file mode 100644 index 000000000..26c19b4cf --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/captions/BaseAutoCaptionsPatch.kt @@ -0,0 +1,44 @@ +package app.revanced.patches.shared.captions + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.extension.Constants.PATCHES_PATH +import app.revanced.patches.shared.startVideoInformerFingerprint +import app.revanced.util.fingerprint.methodOrThrow + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$PATCHES_PATH/AutoCaptionsPatch;" + +val baseAutoCaptionsPatch = bytecodePatch( + description = "baseAutoCaptionsPatch" +) { + execute { + subtitleTrackFingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->disableAutoCaptions()Z + move-result v0 + if-eqz v0, :disabled + const/4 v0, 0x1 + return v0 + """, ExternalLabel("disabled", getInstruction(0)) + ) + } + + mapOf( + startVideoInformerFingerprint to 0, + storyboardRendererDecoderRecommendedLevelFingerprint to 1 + ).forEach { (fingerprint, enabled) -> + fingerprint.methodOrThrow().addInstructions( + 0, """ + const/4 v0, 0x$enabled + invoke-static {v0}, $EXTENSION_CLASS_DESCRIPTOR->setCaptionsButtonStatus(Z)V + """ + ) + } + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/captions/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/captions/Fingerprints.kt new file mode 100644 index 000000000..0f2bb490e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/captions/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.shared.captions + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val storyboardRendererDecoderRecommendedLevelFingerprint = legacyFingerprint( + name = "storyboardRendererDecoderRecommendedLevelFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + strings = listOf("#-1#") +) + +internal val subtitleTrackFingerprint = legacyFingerprint( + name = "subtitleTrackFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + strings = listOf("DISABLE_CAPTIONS_OPTION") +) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/customspeed/CustomPlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/customspeed/CustomPlaybackSpeedPatch.kt new file mode 100644 index 000000000..c23316358 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/customspeed/CustomPlaybackSpeedPatch.kt @@ -0,0 +1,90 @@ +package app.revanced.patches.shared.customspeed + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +fun customPlaybackSpeedPatch( + descriptor: String, + maxSpeed: Float +) = bytecodePatch( + description = "customPlaybackSpeedPatch" +) { + execute { + arrayGeneratorFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.startIndex + val targetRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + invoke-static {v$targetRegister}, $descriptor->getLength(I)I + move-result v$targetRegister + """ + ) + + val sizeIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "size" + } + 1 + val sizeRegister = getInstruction(sizeIndex).registerA + + addInstructions( + sizeIndex + 1, """ + invoke-static {v$sizeRegister}, $descriptor->getSize(I)I + move-result v$sizeRegister + """ + ) + + val arrayIndex = indexOfFirstInstructionOrThrow { + getReference()?.type == "[F" + } + val arrayRegister = getInstruction(arrayIndex).registerA + + addInstructions( + arrayIndex + 1, """ + invoke-static {v$arrayRegister}, $descriptor->getArray([F)[F + move-result-object v$arrayRegister + """ + ) + } + } + + setOf( + limiterFallBackFingerprint.methodOrThrow(), + limiterFingerprint.methodOrThrow(limiterFallBackFingerprint) + ).forEach { method -> + method.apply { + val limitMinIndex = + indexOfFirstLiteralInstructionOrThrow(0.25f.toRawBits().toLong()) + val limitMaxIndex = + indexOfFirstInstructionOrThrow(limitMinIndex + 1, Opcode.CONST_HIGH16) + + val limitMinRegister = + getInstruction(limitMinIndex).registerA + val limitMaxRegister = + getInstruction(limitMaxIndex).registerA + + replaceInstruction( + limitMinIndex, + "const/high16 v$limitMinRegister, 0x0" + ) + replaceInstruction( + limitMaxIndex, + "const/high16 v$limitMaxRegister, ${maxSpeed.toRawBits()}" + ) + } + } + + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/customspeed/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/customspeed/Fingerprints.kt new file mode 100644 index 000000000..0632c4218 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/customspeed/Fingerprints.kt @@ -0,0 +1,41 @@ +package app.revanced.patches.shared.customspeed + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val arrayGeneratorFingerprint = legacyFingerprint( + name = "arrayGeneratorFingerprint", + returnType = "[L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + opcodes = listOf( + Opcode.CONST_4, + Opcode.NEW_ARRAY + ), + strings = listOf("0.0#") +) + +internal val limiterFallBackFingerprint = legacyFingerprint( + name = "limiterFallBackFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.CONST_HIGH16, + Opcode.CONST_HIGH16, + Opcode.INVOKE_STATIC + ), + strings = listOf("Playback rate: %f") +) + +internal val limiterFingerprint = legacyFingerprint( + name = "limiterFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("F"), + opcodes = listOf( + Opcode.CONST_HIGH16, + Opcode.CONST_HIGH16, + Opcode.INVOKE_STATIC, + ) +) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/dialog/BaseViewerDiscretionDialogPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/dialog/BaseViewerDiscretionDialogPatch.kt new file mode 100644 index 000000000..c68befc60 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/dialog/BaseViewerDiscretionDialogPatch.kt @@ -0,0 +1,46 @@ +package app.revanced.patches.shared.dialog + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +fun baseViewerDiscretionDialogPatch( + classDescriptor: String, + isAgeVerified: Boolean = false +) = bytecodePatch( + description = "baseViewerDiscretionDialogPatch" +) { + execute { + createDialogFingerprint + .methodOrThrow() + .invoke(classDescriptor, "confirmDialog") + + if (isAgeVerified) { + ageVerifiedFingerprint.matchOrThrow().let { + it.getWalkerMethod(it.patternMatch!!.endIndex - 1) + .invoke(classDescriptor, "confirmDialogAgeVerified") + } + } + } +} + +private fun MutableMethod.invoke(classDescriptor: String, methodName: String) { + val showDialogIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "show" + } + val dialogRegister = getInstruction(showDialogIndex).registerC + + addInstruction( + showDialogIndex + 1, + "invoke-static { v$dialogRegister }, $classDescriptor->$methodName(Landroid/app/AlertDialog;)V" + ) +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/dialog/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/dialog/Fingerprints.kt new file mode 100644 index 000000000..d20d5bf2f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/dialog/Fingerprints.kt @@ -0,0 +1,46 @@ +package app.revanced.patches.shared.dialog + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val ageVerifiedFingerprint = legacyFingerprint( + name = "ageVerifiedFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "Ljava/util/Map;"), + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ), + strings = listOf( + "com.google.android.libraries.youtube.rendering.elements.sender_view", + "com.google.android.libraries.youtube.innertube.endpoint.tag", + "com.google.android.libraries.youtube.innertube.bundle", + "com.google.android.libraries.youtube.logging.interaction_logger" + ) +) + +internal val createDialogFingerprint = legacyFingerprint( + name = "createDialogFingerprint", + returnType = "V", + accessFlags = AccessFlags.PROTECTED.value, + parameters = listOf("L", "L", "Ljava/lang/String;"), + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IPUT_OBJECT, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL // dialog.show() + ) +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/drawable/DrawableColorHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/drawable/DrawableColorHookPatch.kt new file mode 100644 index 000000000..89d8c816a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/drawable/DrawableColorHookPatch.kt @@ -0,0 +1,43 @@ +package app.revanced.patches.shared.drawable + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private lateinit var insertMethod: MutableMethod +private var insertIndex: Int = 0 +private var insertRegister: Int = 0 +private var offset = 0 + +val drawableColorHookPatch = bytecodePatch( + description = "drawableColorHookPatch" +) { + execute { + drawableColorFingerprint.methodOrThrow().apply { + insertMethod = this + insertIndex = indexOfFirstInstructionReversedOrThrow { + getReference()?.name == "setColor" + } + insertRegister = getInstruction(insertIndex).registerD + } + } +} + +internal fun addDrawableColorHook( + methodDescriptor: String +) { + insertMethod.addInstructions( + insertIndex + offset, """ + invoke-static {v$insertRegister}, $methodDescriptor + move-result v$insertRegister + """ + ) + offset += 2 +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/drawable/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/drawable/Fingerprints.kt new file mode 100644 index 000000000..536622b13 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/drawable/Fingerprints.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.shared.drawable + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val drawableColorFingerprint = legacyFingerprint( + name = "drawableColorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.IF_NEZ, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, // Paint.setColor: inject point + Opcode.RETURN_VOID + ), + customFingerprint = { method, classDef -> + method.name == "onBoundsChange" && + classDef.superclass == "Landroid/graphics/drawable/Drawable;" + } +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/extension/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/shared/extension/Constants.kt new file mode 100644 index 000000000..0d8eb94e4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/extension/Constants.kt @@ -0,0 +1,14 @@ +package app.revanced.patches.shared.extension + +@Suppress("MemberVisibilityCanBePrivate") +internal object Constants { + const val EXTENSION_PATH = "Lapp/revanced/extension/shared" + const val PATCHES_PATH = "$EXTENSION_PATH/patches" + const val COMPONENTS_PATH = "$PATCHES_PATH/components" + const val SPANS_PATH = "$PATCHES_PATH/spans" + const val SPOOF_PATH = "$PATCHES_PATH/spoof" + + const val EXTENSION_UTILS_PATH = "$EXTENSION_PATH/utils" + const val EXTENSION_SETTING_CLASS_DESCRIPTOR = "$EXTENSION_PATH/settings/Setting;" + const val EXTENSION_UTILS_CLASS_DESCRIPTOR = "$EXTENSION_UTILS_PATH/Utils;" +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/extension/SharedExtensionPatch.kt new file mode 100644 index 000000000..40a0caf1b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/extension/SharedExtensionPatch.kt @@ -0,0 +1,57 @@ +package app.revanced.patches.shared.extension + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.FingerprintBuilder +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.fingerprint +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.extension.Constants.EXTENSION_UTILS_CLASS_DESCRIPTOR +import com.android.tools.smali.dexlib2.iface.Method + +fun sharedExtensionPatch( + vararg hooks: ExtensionHook, +) = bytecodePatch( + description = "sharedExtensionPatch" +) { + extendWith("extensions/shared.rve") + + execute { + if (classes.none { EXTENSION_UTILS_CLASS_DESCRIPTOR == it.type }) { + throw PatchException( + "Shared extension has not been merged yet. This patch can not succeed without merging it.", + ) + } + hooks.forEach { hook -> hook(EXTENSION_UTILS_CLASS_DESCRIPTOR) } + } +} + +@Suppress("CONTEXT_RECEIVERS_DEPRECATED") +class ExtensionHook internal constructor( + val fingerprint: Fingerprint, + private val insertIndexResolver: ((Method) -> Int), + private val contextRegisterResolver: (Method) -> String, +) { + context(BytecodePatchContext) + operator fun invoke(extensionClassDescriptor: String) { + val insertIndex = insertIndexResolver(fingerprint.method) + val contextRegister = contextRegisterResolver(fingerprint.method) + + fingerprint.method.addInstruction( + insertIndex, + "invoke-static/range { $contextRegister .. $contextRegister }, " + + "$extensionClassDescriptor->setContext(Landroid/content/Context;)V", + ) + } +} + +fun extensionHook( + insertIndexResolver: ((Method) -> Int) = { 0 }, + contextRegisterResolver: (Method) -> String = { "p0" }, + fingerprintBuilderBlock: FingerprintBuilder.() -> Unit, +) = ExtensionHook( + fingerprint(block = fingerprintBuilderBlock), + insertIndexResolver, + contextRegisterResolver +) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/gms/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/gms/Fingerprints.kt new file mode 100644 index 000000000..8734a722f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/gms/Fingerprints.kt @@ -0,0 +1,108 @@ +package app.revanced.patches.shared.gms + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.StringReference +import com.android.tools.smali.dexlib2.util.MethodUtil + +const val GET_GMS_CORE_VENDOR_GROUP_ID_METHOD_NAME = "getGmsCoreVendorGroupId" + +internal val gmsCoreSupportFingerprint = legacyFingerprint( + name = "gmsCoreSupportFingerprint", + customFingerprint = { _, classDef -> + classDef.endsWith("GmsCoreSupport;") + } +) + +internal val castContextFetchFingerprint = legacyFingerprint( + name = "castContextFetchFingerprint", + strings = listOf("Error fetching CastContext.") +) + +internal val castDynamiteModuleFingerprint = legacyFingerprint( + name = "castDynamiteModuleFingerprint", + strings = listOf("com.google.android.gms.cast.framework.internal.CastDynamiteModuleImpl") +) +internal val castDynamiteModuleV2Fingerprint = legacyFingerprint( + name = "castDynamiteModuleV2Fingerprint", + strings = listOf("Failed to load module via V2: ") +) + +internal val googlePlayUtilityFingerprint = legacyFingerprint( + name = "castContextFetchFingerprint", + returnType = "I", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("L", "I"), + strings = listOf( + "This should never happen.", + "MetadataValueReader" + ) +) + +internal val serviceCheckFingerprint = legacyFingerprint( + name = "serviceCheckFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("L", "I"), + strings = listOf("Google Play Services not available") +) + +internal val primesApiFingerprint = legacyFingerprint( + name = "primesApiFingerprint", + returnType = "V", + strings = listOf("PrimesApiImpl.java"), + customFingerprint = { method, _ -> + MethodUtil.isConstructor(method) + } +) + +internal val primesBackgroundInitializationFingerprint = legacyFingerprint( + name = "primesBackgroundInitializationFingerprint", + opcodes = listOf(Opcode.NEW_INSTANCE), + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + opcode == Opcode.CONST_STRING && + getReference() + ?.string.toString() + .startsWith("Primes init triggered from background in package:") + } >= 0 + } +) + +internal val primesLifecycleEventFingerprint = legacyFingerprint( + name = "primesLifecycleEventFingerprint", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + returnType = "V", + parameters = emptyList(), + opcodes = listOf(Opcode.NEW_INSTANCE), + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + opcode == Opcode.CONST_STRING && + getReference() + ?.string.toString() + .startsWith("Primes did not observe lifecycle events in the expected order.") + } >= 0 + } +) + +internal val certificateFingerprint = legacyFingerprint( + name = "certificateFingerprint", + returnType = "Ljava/lang/String;", + accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL, + parameters = emptyList(), + strings = listOf("X.509", "user", "S"), + customFingerprint = { method, _ -> + indexOfGetPackageNameInstruction(method) >= 0 + } +) + +fun indexOfGetPackageNameInstruction(method: Method) = + method.indexOfFirstInstruction { + getReference()?.toString() == "Landroid/content/Context;->getPackageName()Ljava/lang/String;" + } \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/gms/GmsCoreSupportPatch.kt new file mode 100644 index 000000000..131112245 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/gms/GmsCoreSupportPatch.kt @@ -0,0 +1,623 @@ +package app.revanced.patches.shared.gms + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.BytecodePatchBuilder +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.Option +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.ResourcePatchBuilder +import app.revanced.patcher.patch.ResourcePatchContext +import app.revanced.patcher.patch.booleanOption +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.shared.extension.Constants.PATCHES_PATH +import app.revanced.patches.shared.gms.Constants.ACTIONS +import app.revanced.patches.shared.gms.Constants.AUTHORITIES +import app.revanced.patches.shared.gms.Constants.PERMISSIONS +import app.revanced.util.Utils.trimIndentMultiline +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.returnEarly +import app.revanced.util.valueOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction21c +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.StringReference +import com.android.tools.smali.dexlib2.immutable.reference.ImmutableStringReference +import com.android.tools.smali.dexlib2.util.MethodUtil +import org.w3c.dom.Element +import org.w3c.dom.Node + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$PATCHES_PATH/GmsCoreSupport;" + +private const val PACKAGE_NAME_REGEX_PATTERN = "^[a-z]\\w*(\\.[a-z]\\w*)+\$" + +private const val CLONE_PACKAGE_NAME_YOUTUBE = "bill.youtube" +private const val DEFAULT_PACKAGE_NAME_YOUTUBE = "anddea.youtube" +internal const val ORIGINAL_PACKAGE_NAME_YOUTUBE = "com.google.android.youtube" + +private const val CLONE_PACKAGE_NAME_YOUTUBE_MUSIC = "bill.youtube.music" +private const val DEFAULT_PACKAGE_NAME_YOUTUBE_MUSIC = "anddea.youtube.music" +internal const val ORIGINAL_PACKAGE_NAME_YOUTUBE_MUSIC = + "com.google.android.apps.youtube.music" + +/** + * A patch that allows patched Google apps to run without root and under a different package name + * by using GmsCore instead of Google Play Services. + * + * @param fromPackageName The package name of the original app. + * @param mainActivityOnCreateFingerprint The fingerprint of the main activity onCreate method. + * @param extensionPatch The patch responsible for the extension. + * @param gmsCoreSupportResourcePatchFactory The factory for the corresponding resource patch + * that is used to patch the resources. + * @param executeBlock The additional execution block of the patch. + * @param block The additional block to build the patch. + */ +fun gmsCoreSupportPatch( + fromPackageName: String, + mainActivityOnCreateFingerprint: Fingerprint, + extensionPatch: Patch<*>, + gmsCoreSupportResourcePatchFactory: (gmsCoreVendorGroupIdOption: Option, packageNameYouTubeOption: Option, packageNameYouTubeMusicOption: Option) -> Patch<*>, + executeBlock: BytecodePatchContext.() -> Unit = {}, + block: BytecodePatchBuilder.() -> Unit = {}, +) = bytecodePatch( + name = "GmsCore support", + description = "Allows patched Google apps to run without root and under a different package name " + + "by using GmsCore instead of Google Play Services.", +) { + val gmsCoreVendorGroupIdOption = stringOption( + key = "gmsCoreVendorGroupId", + default = "app.revanced", + values = + mapOf( + "ReVanced" to "app.revanced", + ), + title = "GmsCore vendor group ID", + description = "The vendor's group ID for GmsCore.", + required = true, + ) { it!!.matches(Regex(PACKAGE_NAME_REGEX_PATTERN)) } + + val checkGmsCore by booleanOption( + key = "checkGmsCore", + default = true, + title = "Check GmsCore", + description = """ + Check if GmsCore is installed on the device and has battery optimizations disabled when the app starts. + + If GmsCore is not installed the app will not work, so disabling this is not recommended. + """.trimIndentMultiline(), + required = true, + ) + + val packageNameYouTubeOption = stringOption( + key = "packageNameYouTube", + default = DEFAULT_PACKAGE_NAME_YOUTUBE, + values = mapOf( + "Clone" to CLONE_PACKAGE_NAME_YOUTUBE, + "Default" to DEFAULT_PACKAGE_NAME_YOUTUBE + ), + title = "Package name of YouTube", + description = "The name of the package to use in GmsCore support.", + required = true + ) { it!!.matches(Regex(PACKAGE_NAME_REGEX_PATTERN)) && it != ORIGINAL_PACKAGE_NAME_YOUTUBE } + + val packageNameYouTubeMusicOption = stringOption( + key = "packageNameYouTubeMusic", + default = DEFAULT_PACKAGE_NAME_YOUTUBE_MUSIC, + values = mapOf( + "Clone" to CLONE_PACKAGE_NAME_YOUTUBE_MUSIC, + "Default" to DEFAULT_PACKAGE_NAME_YOUTUBE_MUSIC + ), + title = "Package name of YouTube Music", + description = "The name of the package to use in GmsCore support.", + required = true + ) { it!!.matches(Regex(PACKAGE_NAME_REGEX_PATTERN)) && it != ORIGINAL_PACKAGE_NAME_YOUTUBE_MUSIC } + + dependsOn( + gmsCoreSupportResourcePatchFactory( + gmsCoreVendorGroupIdOption, + packageNameYouTubeOption, + packageNameYouTubeMusicOption + ), + extensionPatch, + ) + + val gmsCoreVendorGroupId by gmsCoreVendorGroupIdOption + + execute { + fun transformStringReferences(transform: (str: String) -> String?) = classes.forEach { + val mutableClass by lazy { + proxy(it).mutableClass + } + + it.methods.forEach classLoop@{ method -> + val implementation = method.implementation ?: return@classLoop + + val mutableMethod by lazy { + mutableClass.methods.first { target -> + MethodUtil.methodSignaturesMatch( + target, + method + ) + } + } + + implementation.instructions.forEachIndexed insnLoop@{ index, instruction -> + val string = + ((instruction as? Instruction21c)?.reference as? StringReference)?.string + ?: return@insnLoop + + // Apply transformation. + val transformedString = transform(string) ?: return@insnLoop + + mutableMethod.replaceInstruction( + index, + BuilderInstruction21c( + Opcode.CONST_STRING, + instruction.registerA, + ImmutableStringReference(transformedString), + ), + ) + } + } + } + + // region Collection of transformations that are applied to all strings. + + fun commonTransform(referencedString: String): String? = + when (referencedString) { + "com.google", + "com.google.android.gms", + in PERMISSIONS, + in ACTIONS, + in AUTHORITIES, + -> referencedString.replace("com.google", gmsCoreVendorGroupId!!) + + // No vendor prefix for whatever reason... + "subscribedfeeds" -> "$gmsCoreVendorGroupId.subscribedfeeds" + else -> null + } + + fun contentUrisTransform(str: String): String? { + // only when content:// uri + if (str.startsWith("content://")) { + // check if matches any authority + for (authority in AUTHORITIES) { + val uriPrefix = "content://$authority" + if (str.startsWith(uriPrefix)) { + return str.replace( + uriPrefix, + "content://${authority.replace("com.google", gmsCoreVendorGroupId!!)}", + ) + } + } + + // gms also has a 'subscribedfeeds' authority, check for that one too + val subFeedsUriPrefix = "content://subscribedfeeds" + if (str.startsWith(subFeedsUriPrefix)) { + return str.replace( + subFeedsUriPrefix, + "content://$gmsCoreVendorGroupId.subscribedfeeds" + ) + } + } + + return null + } + + fun packageNameTransform( + fromPackageName: String, + toPackageName: String + ): (String) -> String? = { string -> + when (string) { + "$fromPackageName.SuggestionsProvider", + "$fromPackageName.fileprovider", + -> string.replace(fromPackageName, toPackageName) + + else -> null + } + } + + fun transformPrimeMethod() { + setOf( + primesBackgroundInitializationFingerprint, + primesLifecycleEventFingerprint + ).forEach { fingerprint -> + fingerprint.methodOrThrow().apply { + val exceptionIndex = indexOfFirstInstructionReversedOrThrow { + opcode == Opcode.NEW_INSTANCE && + (this as? ReferenceInstruction)?.reference?.toString() == "Ljava/lang/IllegalStateException;" + } + val index = + indexOfFirstInstructionReversedOrThrow(exceptionIndex, Opcode.IF_EQZ) + val register = getInstruction(index).registerA + addInstruction( + index, + "const/4 v$register, 0x1" + ) + } + } + primesApiFingerprint.mutableClassOrThrow().methods.filter { method -> + method.name != "" && + method.returnType == "V" + }.forEach { method -> + method.apply { + val index = if (MethodUtil.isConstructor(method)) + indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_DIRECT && + getReference()?.name == "" + } + 1 + else 0 + addInstruction( + index, + "return-void" + ) + } + } + } + + // endregion + + val packageName = + getPackageName(fromPackageName, packageNameYouTubeOption, packageNameYouTubeMusicOption) + + // Transform all strings using all provided transforms, first match wins. + val transformations = arrayOf( + ::commonTransform, + ::contentUrisTransform, + packageNameTransform(fromPackageName, packageName), + ) + transformStringReferences transform@{ string -> + transformations.forEach { transform -> + transform(string)?.let { transformedString -> return@transform transformedString } + } + + return@transform null + } + + // Return these methods early to prevent the app from crashing. + setOf( + castContextFetchFingerprint, + castDynamiteModuleFingerprint, + castDynamiteModuleV2Fingerprint, + googlePlayUtilityFingerprint, + serviceCheckFingerprint, + ).forEach { it.methodOrThrow().returnEarly() } + + // Specific method that needs to be patched. + transformPrimeMethod() + + // Verify GmsCore is installed and whitelisted for power optimizations and background usage. + mainActivityOnCreateFingerprint.method.apply { + // Temporary fix for patches with an extension patch that hook the onCreate method as well. + val setContextIndex = indexOfFirstInstruction { + val reference = + getReference() ?: return@indexOfFirstInstruction false + + reference.toString() == "Lapp/revanced/extension/shared/Utils;->setContext(Landroid/content/Context;)V" + } + + // Add after setContext call, because this patch needs the context. + if (checkGmsCore == true) { + addInstructions( + if (setContextIndex < 0) 0 else setContextIndex + 1, + "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->" + + "checkGmsCore(Landroid/app/Activity;)V", + ) + } + } + + // Change the vendor of GmsCore in the extension. + gmsCoreSupportFingerprint.mutableClassOrThrow().methods + .single { it.name == GET_GMS_CORE_VENDOR_GROUP_ID_METHOD_NAME } + .replaceInstruction(0, "const-string v0, \"$gmsCoreVendorGroupId\"") + + certificateFingerprint.second.classDefOrNull?.methods?.forEach { mutableMethod -> + mutableMethod.apply { + val getPackageNameIndex = indexOfGetPackageNameInstruction(this) + + if (getPackageNameIndex > -1) { + val targetRegister = + (getInstruction(getPackageNameIndex) as FiveRegisterInstruction).registerC + + replaceInstruction( + getPackageNameIndex, + "invoke-static {v$targetRegister}, $EXTENSION_CLASS_DESCRIPTOR->spoofPackageName(Landroid/content/Context;)Ljava/lang/String;", + ) + } + } + } // Since it has only been confirmed to work on YouTube and YouTube Music, does not raise an exception even if the fingerprint cannot be solved. + + executeBlock() + } + + block() +} + +/** + * A collection of permissions, intents and content provider authorities + * that are present in GmsCore which need to be transformed. + */ +private object Constants { + /** + * All permissions. + */ + val PERMISSIONS = setOf( + // C2DM / GCM + "com.google.android.c2dm.permission.RECEIVE", + "com.google.android.c2dm.permission.SEND", + "com.google.android.gtalkservice.permission.GTALK_SERVICE", + "com.google.android.providers.gsf.permission.READ_GSERVICES", + + // GAuth + "com.google.android.googleapps.permission.GOOGLE_AUTH", + "com.google.android.googleapps.permission.GOOGLE_AUTH.cp", + "com.google.android.googleapps.permission.GOOGLE_AUTH.local", + "com.google.android.googleapps.permission.GOOGLE_AUTH.mail", + "com.google.android.googleapps.permission.GOOGLE_AUTH.writely", + + // Ad + "com.google.android.gms.permission.AD_ID_NOTIFICATION", + "com.google.android.gms.permission.AD_ID", + ) + + /** + * All intent actions. + */ + val ACTIONS = setOf( + // location + "com.google.android.gms.location.places.ui.PICK_PLACE", + "com.google.android.gms.location.places.GeoDataApi", + "com.google.android.gms.location.places.PlacesApi", + "com.google.android.gms.location.places.PlaceDetectionApi", + "com.google.android.gms.wearable.MESSAGE_RECEIVED", + "com.google.android.gms.checkin.BIND_TO_SERVICE", + + // C2DM / GCM + "com.google.android.c2dm.intent.REGISTER", + "com.google.android.c2dm.intent.REGISTRATION", + "com.google.android.c2dm.intent.UNREGISTER", + "com.google.android.c2dm.intent.RECEIVE", + "com.google.iid.TOKEN_REQUEST", + "com.google.android.gcm.intent.SEND", + + // car + "com.google.android.gms.car.service.START", + + // people + "com.google.android.gms.people.service.START", + + // wearable + "com.google.android.gms.wearable.BIND", + + // auth + "com.google.android.gsf.login", + "com.google.android.gsf.action.GET_GLS", + "com.google.android.gms.common.account.CHOOSE_ACCOUNT", + "com.google.android.gms.auth.login.LOGIN", + "com.google.android.gms.auth.api.credentials.PICKER", + "com.google.android.gms.auth.api.credentials.service.START", + "com.google.android.gms.auth.service.START", + "com.google.firebase.auth.api.gms.service.START", + "com.google.android.gms.auth.be.appcert.AppCertService", + "com.google.android.gms.credential.manager.service.firstparty.START", + "com.google.android.gms.auth.GOOGLE_SIGN_IN", + "com.google.android.gms.signin.service.START", + "com.google.android.gms.auth.api.signin.service.START", + "com.google.android.gms.auth.api.identity.service.signin.START", + "com.google.android.gms.accountsettings.action.VIEW_SETTINGS", + + // fido + "com.google.android.gms.fido.fido2.privileged.START", + + // gass + "com.google.android.gms.gass.START", + + // games + "com.google.android.gms.games.service.START", + "com.google.android.gms.games.PLAY_GAMES_UPGRADE", + "com.google.android.gms.games.internal.connect.service.START", + + // help + "com.google.android.gms.googlehelp.service.GoogleHelpService.START", + "com.google.android.gms.googlehelp.HELP", + "com.google.android.gms.feedback.internal.IFeedbackService", + + // cast + "com.google.android.gms.cast.firstparty.START", + "com.google.android.gms.cast.service.BIND_CAST_DEVICE_CONTROLLER_SERVICE", + + // fonts + "com.google.android.gms.fonts", + + // phenotype + "com.google.android.gms.phenotype.service.START", + + // location + "com.google.android.gms.location.reporting.service.START", + + // misc + "com.google.android.gms.gmscompliance.service.START", + "com.google.android.gms.oss.licenses.service.START", + "com.google.android.gms.tapandpay.service.BIND", + "com.google.android.gms.measurement.START", + "com.google.android.gms.languageprofile.service.START", + "com.google.android.gms.clearcut.service.START", + "com.google.android.gms.icing.LIGHTWEIGHT_INDEX_SERVICE", + "com.google.android.gms.icing.INDEX_SERVICE", + "com.google.android.gms.mdm.services.START", + + // potoken + "com.google.android.gms.potokens.service.START", + + // droidguard, safetynet + "com.google.android.gms.droidguard.service.START", + "com.google.android.gms.safetynet.service.START", + ) + + /** + * All content provider authorities. + */ + val AUTHORITIES = setOf( + // gsf + "com.google.android.gsf.gservices", + "com.google.settings", + + // auth + "com.google.android.gms.auth.accounts", + + // fonts + "com.google.android.gms.fonts", + + // phenotype + "com.google.android.gms.phenotype", + ) +} + +private fun getPackageName( + originalPackageName: String, + packageNameYouTubeOption: Option, + packageNameYouTubeMusicOption: Option +): String { + if (originalPackageName == ORIGINAL_PACKAGE_NAME_YOUTUBE) { + return packageNameYouTubeOption.valueOrThrow() + } else if (originalPackageName == ORIGINAL_PACKAGE_NAME_YOUTUBE_MUSIC) { + return packageNameYouTubeMusicOption.valueOrThrow() + } + throw PatchException("Unknown package name: $originalPackageName") +} + +/** + * Abstract resource patch that allows Google apps to run without root and under a different package name + * by using GmsCore instead of Google Play Services. + * + * @param fromPackageName The package name of the original app. + * @param spoofedPackageSignature The signature of the package to spoof to. + * @param gmsCoreVendorGroupIdOption The option to get the vendor group ID of GmsCore. + * @param executeBlock The additional execution block of the patch. + * @param block The additional block to build the patch. + */ +fun gmsCoreSupportResourcePatch( + fromPackageName: String, + spoofedPackageSignature: String, + gmsCoreVendorGroupIdOption: Option, + packageNameYouTubeOption: Option, + packageNameYouTubeMusicOption: Option, + executeBlock: ResourcePatchContext.() -> Unit = {}, + block: ResourcePatchBuilder.() -> Unit = {}, +) = resourcePatch { + val gmsCoreVendorGroupId by gmsCoreVendorGroupIdOption + + execute { + /** + * Add metadata to manifest to support spoofing the package name and signature of GmsCore. + */ + fun addSpoofingMetadata() { + fun Node.adoptChild( + tagName: String, + block: Element.() -> Unit, + ) { + val child = ownerDocument.createElement(tagName) + child.block() + appendChild(child) + } + + document("AndroidManifest.xml").use { document -> + val applicationNode = + document + .getElementsByTagName("application") + .item(0) + + // Spoof package name and signature. + applicationNode.adoptChild("meta-data") { + setAttribute( + "android:name", + "$gmsCoreVendorGroupId.android.gms.SPOOFED_PACKAGE_NAME" + ) + setAttribute("android:value", fromPackageName) + } + + applicationNode.adoptChild("meta-data") { + setAttribute( + "android:name", + "$gmsCoreVendorGroupId.android.gms.SPOOFED_PACKAGE_SIGNATURE" + ) + setAttribute("android:value", spoofedPackageSignature) + } + + // GmsCore presence detection in extension. + applicationNode.adoptChild("meta-data") { + // TODO: The name of this metadata should be dynamic. + setAttribute("android:name", "app.revanced.MICROG_PACKAGE_NAME") + setAttribute("android:value", "$gmsCoreVendorGroupId.android.gms") + } + } + } + + /** + * Patch the manifest to support GmsCore. + */ + fun patchManifest() { + val packageName = getPackageName( + fromPackageName, + packageNameYouTubeOption, + packageNameYouTubeMusicOption + ) + + val transformations = mapOf( + "package=\"$fromPackageName" to "package=\"$packageName", + "android:authorities=\"$fromPackageName" to "android:authorities=\"$packageName", + "$fromPackageName.permission.C2D_MESSAGE" to "$packageName.permission.C2D_MESSAGE", + "$fromPackageName.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION" to "$packageName.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION", + "com.google.android.c2dm" to "$gmsCoreVendorGroupId.android.c2dm", + "com.google.android.libraries.photos.api.mars" to "$gmsCoreVendorGroupId.android.apps.photos.api.mars", + ) + + // 'QUERY_ALL_PACKAGES' permission is required, + // To check whether apps such as GmsCore, YouTube or YouTube Music are installed on the device. + document("AndroidManifest.xml").use { document -> + document.getElementsByTagName("manifest").item(0).also { + it.appendChild( + it.ownerDocument.createElement("uses-permission").also { element -> + element.setAttribute( + "android:name", + "android.permission.QUERY_ALL_PACKAGES" + ) + }) + } + } + + val manifest = get("AndroidManifest.xml") + manifest.writeText( + transformations.entries.fold(manifest.readText()) { acc, (from, to) -> + acc.replace( + from, + to, + ) + }, + ) + } + + patchManifest() + addSpoofingMetadata() + + executeBlock() + } + + block() +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/imageurl/CronetImageUrlHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/imageurl/CronetImageUrlHookPatch.kt new file mode 100644 index 000000000..ea8c41153 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/imageurl/CronetImageUrlHookPatch.kt @@ -0,0 +1,124 @@ +package app.revanced.patches.shared.imageurl + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patches.shared.extension.Constants.PATCHES_PATH +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod + +private const val EXTENSION_SHARED_CLASS_DESCRIPTOR = + "$PATCHES_PATH/BypassImageRegionRestrictionsPatch;" + +private lateinit var loadImageUrlMethod: MutableMethod +private var loadImageUrlIndex = 0 + +private lateinit var loadImageSuccessCallbackMethod: MutableMethod +private var loadImageSuccessCallbackIndex = 0 + +private lateinit var loadImageErrorCallbackMethod: MutableMethod +private var loadImageErrorCallbackIndex = 0 + +fun cronetImageUrlHookPatch( + resolveCronetRequest: Boolean, +) = bytecodePatch( + description = "cronetImageUrlHookPatch", +) { + execute { + loadImageUrlMethod = messageDigestImageUrlFingerprint + .matchOrThrow(messageDigestImageUrlParentFingerprint).method + + if (!resolveCronetRequest) return@execute + + loadImageSuccessCallbackMethod = onSucceededFingerprint + .matchOrThrow(onResponseStartedFingerprint).method + + loadImageErrorCallbackMethod = onFailureFingerprint + .matchOrThrow(onResponseStartedFingerprint).method + + // The URL is required for the failure callback hook, but the URL field is obfuscated. + // Add a helper get method that returns the URL field. + requestFingerprint.methodOrThrow().apply { + // The url is the only string field that is set inside the constructor. + val urlFieldInstruction = instructions.first { + val reference = it.getReference() + it.opcode == Opcode.IPUT_OBJECT && reference?.type == "Ljava/lang/String;" + } as ReferenceInstruction + + val urlFieldName = (urlFieldInstruction.reference as FieldReference).name + val definingClass = CRONET_URL_REQUEST_CLASS_DESCRIPTOR + val addedMethodName = "getHookedUrl" + requestFingerprint.mutableClassOrThrow().methods.add( + ImmutableMethod( + definingClass, + addedMethodName, + emptyList(), + "Ljava/lang/String;", + AccessFlags.PUBLIC.value, + null, + null, + MutableMethodImplementation(2), + ).toMutable().apply { + addInstructions( + """ + iget-object v0, p0, $definingClass->$urlFieldName:Ljava/lang/String; + return-object v0 + """, + ) + } + ) + } + } +} + +/** + * @param highPriority If the hook should be called before all other hooks. + */ +internal fun addImageUrlHook( + targetMethodClass: String = EXTENSION_SHARED_CLASS_DESCRIPTOR, + highPriority: Boolean = true +) { + loadImageUrlMethod.addInstructions( + if (highPriority) 0 else loadImageUrlIndex, + """ + invoke-static { p1 }, $targetMethodClass->overrideImageURL(Ljava/lang/String;)Ljava/lang/String; + move-result-object p1 + """, + ) + loadImageUrlIndex += 2 +} + +/** + * If a connection completed, which includes normal 200 responses but also includes + * status 404 and other error like http responses. + */ +internal fun addImageUrlSuccessCallbackHook(targetMethodClass: String) { + loadImageSuccessCallbackMethod.addInstruction( + loadImageSuccessCallbackIndex++, + "invoke-static { p1, p2 }, $targetMethodClass->handleCronetSuccess(" + + "Lorg/chromium/net/UrlRequest;Lorg/chromium/net/UrlResponseInfo;)V", + ) +} + +/** + * If a connection outright failed to complete any connection. + */ +internal fun addImageUrlErrorCallbackHook(targetMethodClass: String) { + loadImageErrorCallbackMethod.addInstruction( + loadImageErrorCallbackIndex++, + "invoke-static { p1, p2, p3 }, $targetMethodClass->handleCronetFailure(" + + "Lorg/chromium/net/UrlRequest;Lorg/chromium/net/UrlResponseInfo;Ljava/io/IOException;)V", + ) +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/imageurl/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/imageurl/Fingerprints.kt new file mode 100644 index 000000000..083f50d41 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/imageurl/Fingerprints.kt @@ -0,0 +1,71 @@ +package app.revanced.patches.shared.imageurl + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val onFailureFingerprint = legacyFingerprint( + name = "onFailureFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "V", + parameters = listOf( + "Lorg/chromium/net/UrlRequest;", + "Lorg/chromium/net/UrlResponseInfo;", + "Lorg/chromium/net/CronetException;" + ), + customFingerprint = { method, _ -> + method.name == "onFailed" + } +) + +// Acts as a parent fingerprint. +internal val onResponseStartedFingerprint = legacyFingerprint( + name = "onResponseStartedFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Lorg/chromium/net/UrlRequest;", "Lorg/chromium/net/UrlResponseInfo;"), + strings = listOf( + "Content-Length", + "Content-Type", + "identity", + "application/x-protobuf" + ), + customFingerprint = { method, _ -> + method.name == "onResponseStarted" + } +) + +internal val onSucceededFingerprint = legacyFingerprint( + name = "onSucceededFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Lorg/chromium/net/UrlRequest;", "Lorg/chromium/net/UrlResponseInfo;"), + customFingerprint = { method, _ -> + method.name == "onSucceeded" + } +) + +internal const val CRONET_URL_REQUEST_CLASS_DESCRIPTOR = "Lorg/chromium/net/impl/CronetUrlRequest;" + +internal val requestFingerprint = legacyFingerprint( + name = "requestFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + customFingerprint = { _, classDef -> + classDef.type == CRONET_URL_REQUEST_CLASS_DESCRIPTOR + } +) + +internal val messageDigestImageUrlFingerprint = legacyFingerprint( + name = "messageDigestImageUrlFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + parameters = listOf("Ljava/lang/String;", "L") +) + +internal val messageDigestImageUrlParentFingerprint = legacyFingerprint( + name = "messageDigestImageUrlParentFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "Ljava/lang/String;", + parameters = emptyList(), + strings = listOf("@#&=*+-_.,:!?()/~'%;\$"), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/litho/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/litho/Fingerprints.kt new file mode 100644 index 000000000..1adf981c2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/litho/Fingerprints.kt @@ -0,0 +1,74 @@ +package app.revanced.patches.shared.litho + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val bufferUpbFeatureFlagFingerprint = legacyFingerprint( + name = "bufferUpbFeatureFlagFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("L"), + literals = listOf(45419603L), +) + +internal val byteBufferFingerprint = legacyFingerprint( + name = "byteBufferFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("I", "Ljava/nio/ByteBuffer;"), + opcodes = listOf( + null, + Opcode.IF_EQZ, + Opcode.IPUT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.SUB_INT_2ADDR, + Opcode.IPUT, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IPUT, + Opcode.RETURN_VOID, + Opcode.CONST_4, + Opcode.IPUT, + Opcode.IPUT, + Opcode.GOTO + ), + // Check method count and field count to support both YouTube and YouTube Music + customFingerprint = { _, classDef -> + classDef.methods.count() > 6 + && classDef.fields.count() > 4 + }, +) + +internal val emptyComponentsFingerprint = legacyFingerprint( + name = "emptyComponentsFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + opcodes = listOf( + Opcode.INVOKE_INTERFACE, + Opcode.INVOKE_STATIC_RANGE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IGET_OBJECT + ), + strings = listOf("Error while converting %s"), +) + +/** + * Since YouTube v19.18.41 and YT Music 7.01.53, pathBuilder is being handled by a different Method. + */ +internal val pathBuilderFingerprint = legacyFingerprint( + name = "pathBuilderFingerprint", + returnType = "L", + strings = listOf("Number of bits must be positive"), +) + +internal val pathUpbFeatureFlagFingerprint = legacyFingerprint( + name = "pathUpbFeatureFlagFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(45631264L), +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/litho/LithoFilterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/litho/LithoFilterPatch.kt new file mode 100644 index 000000000..af61831fd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/litho/LithoFilterPatch.kt @@ -0,0 +1,235 @@ +package app.revanced.patches.shared.litho + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.extension.Constants.COMPONENTS_PATH +import app.revanced.util.findMethodsOrThrow +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstStringInstructionOrThrow +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.util.MethodUtil + +private const val EXTENSION_LITHO_FILER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/LithoFilterPatch;" + +private const val EXTENSION_FILER_ARRAY_DESCRIPTOR = + "[$COMPONENTS_PATH/Filter;" + +private lateinit var filterArrayMethod: MutableMethod +private var filterCount = 0 + +internal lateinit var addLithoFilter: (String) -> Unit + private set + +val lithoFilterPatch = bytecodePatch( + description = "lithoFilterPatch", +) { + execute { + + // region Pass the buffer into extension. + + byteBufferFingerprint.methodOrThrow().addInstruction( + 0, + "invoke-static { p2 }, $EXTENSION_LITHO_FILER_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V" + ) + + // endregion + + var (emptyComponentMethod, emptyComponentLabel) = + emptyComponentsFingerprint.matchOrThrow().let { + with(it.method) { + val emptyComponentMethodIndex = it.patternMatch!!.startIndex + 1 + val emptyComponentMethodReference = + getInstruction(emptyComponentMethodIndex).reference + val emptyComponentFieldReference = + getInstruction(emptyComponentMethodIndex + 2).reference + + val label = """ + move-object/from16 v0, p1 + invoke-static {v0}, $emptyComponentMethodReference + move-result-object v0 + iget-object v0, v0, $emptyComponentFieldReference + return-object v0 + """ + + Pair(this, label) + } + } + + fun checkMethodSignatureMatch(pathBuilder: MutableMethod) = emptyComponentMethod.apply { + if (!MethodUtil.methodSignaturesMatch(pathBuilder, this)) { + implementation!!.instructions + .withIndex() + .filter { (_, instruction) -> + val reference = (instruction as? ReferenceInstruction)?.reference + reference is MethodReference && + MethodUtil.methodSignaturesMatch(pathBuilder, reference) + } + .map { (index, _) -> index } + .reversed() + .forEach { index -> + val insertInstruction = getInstruction(index + 1) + if (insertInstruction is OneRegisterInstruction) { + val insertRegister = + insertInstruction.registerA + val insertIndex = index + 2 + + addInstructionsWithLabels( + insertIndex, """ + if-nez v$insertRegister, :ignore + """ + emptyComponentLabel, + ExternalLabel("ignore", getInstruction(insertIndex)) + ) + } + } + + emptyComponentLabel = """ + const/4 v0, 0x0 + return-object v0 + """ + } + } + + pathBuilderFingerprint.methodOrThrow().apply { + checkMethodSignatureMatch(this) + + val stringBuilderIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.IPUT_OBJECT && + getReference()?.type == "Ljava/lang/StringBuilder;" + } + val stringBuilderRegister = + getInstruction(stringBuilderIndex).registerA + + val emptyStringIndex = indexOfFirstStringInstructionOrThrow("") + val identifierRegister = getInstruction( + indexOfFirstInstructionReversedOrThrow(emptyStringIndex) { + opcode == Opcode.IPUT_OBJECT + && getReference()?.type == "Ljava/lang/String;" + } + ).registerA + val objectRegister = getInstruction( + indexOfFirstInstructionOrThrow(emptyStringIndex) { + opcode == Opcode.INVOKE_VIRTUAL + } + ).registerC + + val insertIndex = stringBuilderIndex + 1 + + addInstructionsWithLabels( + insertIndex, """ + invoke-static {v$stringBuilderRegister, v$identifierRegister, v$objectRegister}, $EXTENSION_LITHO_FILER_CLASS_DESCRIPTOR->filter(Ljava/lang/StringBuilder;Ljava/lang/String;Ljava/lang/Object;)Z + move-result v$stringBuilderRegister + if-eqz v$stringBuilderRegister, :filter + """ + emptyComponentLabel, + ExternalLabel("filter", getInstruction(insertIndex)) + ) + } + + // region A/B test of new Litho native code. + + // Turn off native code that handles litho component names. If this feature is on then nearly + // all litho components have a null name and identifier/path filtering is completely broken. + + if (bufferUpbFeatureFlagFingerprint.second.methodOrNull != null && + pathUpbFeatureFlagFingerprint.second.methodOrNull != null + ) { + mapOf( + bufferUpbFeatureFlagFingerprint to 45419603L, + pathUpbFeatureFlagFingerprint to 45631264L, + ).forEach { (fingerprint, literalValue) -> + fingerprint.injectLiteralInstructionBooleanCall( + literalValue, + "0x0" + ) + } + } + + // endregion + + // Create a new method to get the filter array to avoid register conflicts. + // This fixes an issue with extension compiled with Android Gradle Plugin 8.3.0+. + // https://github.com/ReVanced/revanced-patches/issues/2818 + val lithoFilterMethods = findMethodsOrThrow(EXTENSION_LITHO_FILER_CLASS_DESCRIPTOR) + + lithoFilterMethods + .first { it.name == "" } + .apply { + val setArrayIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.SPUT_OBJECT && + getReference()?.type == EXTENSION_FILER_ARRAY_DESCRIPTOR + } + val setArrayRegister = + getInstruction(setArrayIndex).registerA + val addedMethodName = "getFilterArray" + + addInstructions( + setArrayIndex, """ + invoke-static {}, $EXTENSION_LITHO_FILER_CLASS_DESCRIPTOR->$addedMethodName()$EXTENSION_FILER_ARRAY_DESCRIPTOR + move-result-object v$setArrayRegister + """ + ) + + filterArrayMethod = ImmutableMethod( + definingClass, + addedMethodName, + emptyList(), + EXTENSION_FILER_ARRAY_DESCRIPTOR, + AccessFlags.PRIVATE or AccessFlags.STATIC, + null, + null, + MutableMethodImplementation(3), + ).toMutable().apply { + addInstruction( + 0, + "return-object v2" + ) + } + + lithoFilterMethods.add(filterArrayMethod) + } + + addLithoFilter = { classDescriptor -> + filterArrayMethod.addInstructions( + 0, + """ + new-instance v0, $classDescriptor + invoke-direct {v0}, $classDescriptor->()V + const/16 v1, ${filterCount++} + aput-object v0, v2, v1 + """ + ) + } + } + + finalize { + filterArrayMethod.addInstructions( + 0, """ + const/16 v0, $filterCount + new-array v2, v0, $EXTENSION_FILER_ARRAY_DESCRIPTOR + """ + ) + } +} + + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/mainactivity/BaseMainActivityResolvePatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/mainactivity/BaseMainActivityResolvePatch.kt new file mode 100644 index 000000000..8122949da --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/mainactivity/BaseMainActivityResolvePatch.kt @@ -0,0 +1,84 @@ +package app.revanced.patches.shared.mainactivity + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableClass +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import kotlin.properties.Delegates + +lateinit var mainActivityMutableClass: MutableClass + private set +lateinit var onConfigurationChangedMethod: MutableMethod + private set +lateinit var onCreateMethod: MutableMethod + private set + +private lateinit var constructorMethod: MutableMethod +private lateinit var onBackPressedMethod: MutableMethod + +private var constructorMethodIndex by Delegates.notNull() +private var onBackPressedMethodIndex by Delegates.notNull() + +fun baseMainActivityResolvePatch( + mainActivityOnCreateFingerprint: Pair, +) = bytecodePatch( + description = "baseMainActivityResolvePatch" +) { + execute { + onCreateMethod = mainActivityOnCreateFingerprint.methodOrThrow() + mainActivityMutableClass = mainActivityOnCreateFingerprint.mutableClassOrThrow() + + // set constructor method + constructorMethod = getMainActivityMethod("") + constructorMethodIndex = constructorMethod.implementation!!.instructions.lastIndex + + // set onBackPressed method + onBackPressedMethod = getMainActivityMethod("onBackPressed") + onBackPressedMethodIndex = + onBackPressedMethod.indexOfFirstInstructionOrThrow(Opcode.RETURN_VOID) + + // set onConfigurationChanged method + onConfigurationChangedMethod = getMainActivityMethod("onConfigurationChanged") + } +} + +internal fun injectConstructorMethodCall(classDescriptor: String, methodDescriptor: String) = + constructorMethod.injectMethodCall( + classDescriptor, + methodDescriptor, + constructorMethodIndex + ) + +internal fun injectOnBackPressedMethodCall(classDescriptor: String, methodDescriptor: String) = + onBackPressedMethod.injectMethodCall( + classDescriptor, + methodDescriptor, + onBackPressedMethodIndex + ) + +internal fun injectOnCreateMethodCall(classDescriptor: String, methodDescriptor: String) = + onCreateMethod.injectMethodCall(classDescriptor, methodDescriptor) + +internal fun getMainActivityMethod(methodDescriptor: String) = + mainActivityMutableClass.methods.find { method -> method.name == methodDescriptor } + ?: throw PatchException("Could not find $methodDescriptor") + +private fun MutableMethod.injectMethodCall( + classDescriptor: String, + methodDescriptor: String +) = injectMethodCall(classDescriptor, methodDescriptor, 0) + +private fun MutableMethod.injectMethodCall( + classDescriptor: String, + methodDescriptor: String, + insertIndex: Int +) = addInstruction( + insertIndex, + "invoke-static/range {p0 .. p0}, $classDescriptor->$methodDescriptor(Landroid/app/Activity;)V" +) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/mapping/ResourceMappingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/mapping/ResourceMappingPatch.kt new file mode 100644 index 000000000..a19a19599 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/mapping/ResourceMappingPatch.kt @@ -0,0 +1,81 @@ +package app.revanced.patches.shared.mapping + +import app.revanced.patcher.patch.resourcePatch +import org.w3c.dom.Element +import java.util.Collections +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +// TODO: Probably renaming the patch/this is a good idea. +lateinit var resourceMappings: List + private set + +val resourceMappingPatch = resourcePatch( + description = "resourceMappingPatch" +) { + val threadCount = Runtime.getRuntime().availableProcessors() + val threadPoolExecutor = Executors.newFixedThreadPool(threadCount) + + val resourceMappings = Collections.synchronizedList(mutableListOf()) + + execute { + // Save the file in memory to concurrently read from it. + val resourceXmlFile = get("res/values/public.xml").readBytes() + + for (threadIndex in 0 until threadCount) { + threadPoolExecutor.execute thread@{ + document(resourceXmlFile.inputStream()).use { document -> + + val resources = document.documentElement.childNodes + val resourcesLength = resources.length + val jobSize = resourcesLength / threadCount + + val batchStart = jobSize * threadIndex + val batchEnd = jobSize * (threadIndex + 1) + element@ for (i in batchStart until batchEnd) { + // Prevent out of bounds. + if (i >= resourcesLength) return@thread + + val node = resources.item(i) + if (node !is Element) continue + + val nameAttribute = node.getAttribute("name") + val typeAttribute = node.getAttribute("type") + + if (node.nodeName != "public" || nameAttribute.startsWith("APKTOOL")) continue + + val id = node.getAttribute("id").substring(2).toLong(16) + + resourceMappings.add(ResourceElement(typeAttribute, nameAttribute, id)) + } + } + } + } + + threadPoolExecutor.also { it.shutdown() }.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS) + + app.revanced.patches.shared.mapping.resourceMappings = resourceMappings + } +} + +operator fun List.get(type: String, name: String) = resourceMappings.firstOrNull { + it.type == type && it.name == name +}?.id ?: -1L + +operator fun List.get(resourceType: ResourceType, name: String) = + get(resourceType.value, name) + +data class ResourceElement(val type: String, val name: String, val id: Long) + +enum class ResourceType(val value: String) { + ATTR("attr"), + BOOL("bool"), + COLOR("color"), + DIMEN("dimen"), + DRAWABLE("drawable"), + ID("id"), + INTEGER("integer"), + LAYOUT("layout"), + STRING("string"), + STYLE("style") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/opus/BaseOpusCodecsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/opus/BaseOpusCodecsPatch.kt new file mode 100644 index 000000000..3918ebfbd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/opus/BaseOpusCodecsPatch.kt @@ -0,0 +1,49 @@ +package app.revanced.patches.shared.opus + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +fun baseOpusCodecsPatch( + descriptor: String, +) = bytecodePatch( + description = "baseOpusCodecsPatch" +) { + execute { + val opusCodecReference = with(codecReferenceFingerprint.methodOrThrow()) { + val codecIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_STATIC && + getReference()?.returnType == "Ljava/util/Set;" + } + getInstruction(codecIndex).reference + } + + codecSelectorFingerprint.matchOrThrow().let { + it.method.apply { + val freeRegister = implementation!!.registerCount - parameters.size - 2 + val targetIndex = it.patternMatch!!.endIndex + val targetRegister = getInstruction(targetIndex).registerA + + addInstructionsWithLabels( + targetIndex + 1, """ + invoke-static {}, $descriptor + move-result v$freeRegister + if-eqz v$freeRegister, :mp4a + invoke-static {}, $opusCodecReference + move-result-object v$targetRegister + """, ExternalLabel("mp4a", getInstruction(targetIndex + 1)) + ) + } + } + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/opus/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/opus/Fingerprints.kt new file mode 100644 index 000000000..45d6be36c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/opus/Fingerprints.kt @@ -0,0 +1,29 @@ +package app.revanced.patches.shared.opus + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val codecReferenceFingerprint = legacyFingerprint( + name = "codecReferenceFingerprint", + returnType = "J", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf(Opcode.INVOKE_SUPER), + strings = listOf("itag") +) + +internal val codecSelectorFingerprint = legacyFingerprint( + name = "codecSelectorFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + opcodes = listOf( + Opcode.NEW_INSTANCE, + Opcode.NEW_INSTANCE, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT + ), + strings = listOf("Audio track id %s not in audio streams") +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/returnyoutubeusername/BaseReturnYouTubeUsernamePatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/returnyoutubeusername/BaseReturnYouTubeUsernamePatch.kt new file mode 100644 index 000000000..83e6b0334 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/returnyoutubeusername/BaseReturnYouTubeUsernamePatch.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.shared.returnyoutubeusername + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.extension.Constants.PATCHES_PATH +import app.revanced.patches.shared.textcomponent.hookSpannableString +import app.revanced.patches.shared.textcomponent.hookTextComponent +import app.revanced.patches.shared.textcomponent.textComponentPatch + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$PATCHES_PATH/ReturnYouTubeUsernamePatch;" + +val baseReturnYouTubeUsernamePatch = bytecodePatch( + description = "baseReturnYouTubeUsernamePatch" +) { + dependsOn(textComponentPatch) + + execute { + hookSpannableString(EXTENSION_CLASS_DESCRIPTOR, "preFetchLithoText") + hookTextComponent(EXTENSION_CLASS_DESCRIPTOR) + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/settingmenu/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/settingmenu/Fingerprints.kt new file mode 100644 index 000000000..7c3b0f97e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/settingmenu/Fingerprints.kt @@ -0,0 +1,28 @@ +package app.revanced.patches.shared.settingmenu + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val findPreferenceFingerprint = legacyFingerprint( + name = "findPreferenceFingerprint", + returnType = "Landroidx/preference/Preference;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Ljava/lang/CharSequence;"), + strings = listOf("Key cannot be null"), + customFingerprint = { method, _ -> + method.definingClass == "Landroidx/preference/PreferenceGroup;" + } +) + +internal val removePreferenceFingerprint = legacyFingerprint( + name = "removePreferenceFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroidx/preference/Preference;"), + opcodes = listOf(Opcode.INVOKE_VIRTUAL), + customFingerprint = { method, classDef -> + classDef.type == "Landroidx/preference/PreferenceGroup;" && + method.implementation?.instructions?.elementAt(0)?.opcode == Opcode.INVOKE_DIRECT + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/settingmenu/SettingsMenuPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/settingmenu/SettingsMenuPatch.kt new file mode 100644 index 000000000..a9c51fc4e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/settingmenu/SettingsMenuPatch.kt @@ -0,0 +1,33 @@ +package app.revanced.patches.shared.settingmenu + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.extension.Constants.PATCHES_PATH +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.methodCall + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$PATCHES_PATH/BaseSettingsMenuPatch;" + +val settingsMenuPatch = bytecodePatch( + description = "settingsMenuPatch", +) { + execute { + val findPreferenceMethodCall = findPreferenceFingerprint.methodCall() + val removePreferenceMethodCall = removePreferenceFingerprint.methodCall() + + findMethodOrThrow(EXTENSION_CLASS_DESCRIPTOR) { + name == "removePreference" + }.addInstructionsWithLabels( + 0, """ + invoke-virtual {p0, p1}, $findPreferenceMethodCall + move-result-object v0 + if-eqz v0, :ignore + invoke-virtual {p0, v0}, $removePreferenceMethodCall + :ignore + return-void + """ + ) + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spans/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spans/Fingerprints.kt new file mode 100644 index 000000000..a54bda583 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/spans/Fingerprints.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.shared.spans + +import app.revanced.util.fingerprint.legacyFingerprint + +internal val customCharacterStyleFingerprint = legacyFingerprint( + name = "customCharacterStyleFingerprint", + returnType = "Landroid/graphics/Path;", + parameters = listOf("Landroid/text/Layout;"), +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spans/InclusiveSpanPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spans/InclusiveSpanPatch.kt new file mode 100644 index 000000000..e60ae1f05 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/spans/InclusiveSpanPatch.kt @@ -0,0 +1,168 @@ +package app.revanced.patches.shared.spans + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patches.shared.extension.Constants.SPANS_PATH +import app.revanced.patches.shared.indexOfSpannableStringInstruction +import app.revanced.patches.shared.spannableStringBuilderFingerprint +import app.revanced.patches.shared.textcomponent.hookSpannableString +import app.revanced.patches.shared.textcomponent.textComponentPatch +import app.revanced.util.findMethodOrThrow +import app.revanced.util.findMethodsOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.getFiveRegisters +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod + +private const val EXTENSION_SPANS_CLASS_DESCRIPTOR = + "$SPANS_PATH/InclusiveSpanPatch;" + +private const val EXTENSION_FILER_ARRAY_DESCRIPTOR = + "[$SPANS_PATH/Filter;" + +private lateinit var filterArrayMethod: MutableMethod +private var filterCount = 0 + +internal lateinit var addSpanFilter: (String) -> Unit + private set + +val inclusiveSpanPatch = bytecodePatch( + description = "inclusiveSpanPatch" +) { + dependsOn(textComponentPatch) + + execute { + hookSpannableString( + EXTENSION_SPANS_CLASS_DESCRIPTOR, + "setConversionContext" + ) + + spannableStringBuilderFingerprint.methodOrThrow().apply { + val spannedIndex = indexOfSpannableStringInstruction(this) + val setInclusiveSpanIndex = indexOfFirstInstructionOrThrow(spannedIndex) { + val reference = getReference() + opcode == Opcode.INVOKE_STATIC && + reference?.returnType == "V" && + reference.parameterTypes.size > 3 && + reference.parameterTypes.firstOrNull() == "Landroid/text/SpannableString;" + } + val setInclusiveSpanMethod = getWalkerMethod(setInclusiveSpanIndex) + + setInclusiveSpanMethod.apply { + val insertIndex = indexOfFirstInstructionReversedOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference().toString() == "Landroid/text/SpannableString;->setSpan(Ljava/lang/Object;III)V" + } + replaceInstruction( + insertIndex, + "invoke-static { ${getFiveRegisters(insertIndex)} }, " + + EXTENSION_SPANS_CLASS_DESCRIPTOR + + "->" + + "setSpan(Landroid/text/SpannableString;Ljava/lang/Object;III)V" + ) + } + + val customCharacterStyle = + customCharacterStyleFingerprint.mutableClassOrThrow().type + + findMethodOrThrow(EXTENSION_SPANS_CLASS_DESCRIPTOR) { + name == "getSpanType" && + returnType != "Ljava/lang/String;" + }.apply { + val index = indexOfFirstInstructionOrThrow { + opcode == Opcode.INSTANCE_OF && + (this as? ReferenceInstruction)?.reference?.toString() == "Landroid/text/style/CharacterStyle;" + } + val instruction = getInstruction(index) + replaceInstruction( + index, + "instance-of v${instruction.registerA}, v${instruction.registerB}, $customCharacterStyle" + ) + } + + + // Create a new method to get the filter array to avoid register conflicts. + // This fixes an issue with extension compiled with Android Gradle Plugin 8.3.0+. + // https://github.com/ReVanced/revanced-patches/issues/2818 + val spansFilterMethods = findMethodsOrThrow(EXTENSION_SPANS_CLASS_DESCRIPTOR) + + spansFilterMethods + .first { it.name == "" } + .apply { + val setArrayIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.SPUT_OBJECT && + getReference()?.type == EXTENSION_FILER_ARRAY_DESCRIPTOR + } + val setArrayRegister = + getInstruction(setArrayIndex).registerA + val addedMethodName = "getFilterArray" + + addInstructions( + setArrayIndex, """ + invoke-static {}, $EXTENSION_SPANS_CLASS_DESCRIPTOR->$addedMethodName()$EXTENSION_FILER_ARRAY_DESCRIPTOR + move-result-object v$setArrayRegister + """ + ) + + filterArrayMethod = ImmutableMethod( + definingClass, + addedMethodName, + emptyList(), + EXTENSION_FILER_ARRAY_DESCRIPTOR, + AccessFlags.PRIVATE or AccessFlags.STATIC, + null, + null, + MutableMethodImplementation(3), + ).toMutable().apply { + addInstruction( + 0, + "return-object v2" + ) + } + + spansFilterMethods.add(filterArrayMethod) + } + + addSpanFilter = { classDescriptor -> + filterArrayMethod.addInstructions( + 0, """ + new-instance v0, $classDescriptor + invoke-direct {v0}, $classDescriptor->()V + const/16 v1, ${filterCount++} + aput-object v0, v2, v1 + """ + ) + } + } + + } + + finalize { + filterArrayMethod.addInstructions( + 0, """ + const/16 v0, $filterCount + new-array v2, v0, $EXTENSION_FILER_ARRAY_DESCRIPTOR + """ + ) + } +} + + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/appversion/BaseSpoofAppVersionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/appversion/BaseSpoofAppVersionPatch.kt new file mode 100644 index 000000000..8d4025c41 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/appversion/BaseSpoofAppVersionPatch.kt @@ -0,0 +1,33 @@ +package app.revanced.patches.shared.spoof.appversion + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint +import app.revanced.patches.shared.indexOfReleaseInstruction +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +fun baseSpoofAppVersionPatch( + descriptor: String, +) = bytecodePatch( + description = "baseSpoofAppVersionPatch" +) { + execute { + createPlayerRequestBodyWithModelFingerprint.methodOrThrow().apply { + val versionIndex = indexOfReleaseInstruction(this) + 1 + val insertIndex = + indexOfFirstInstructionReversedOrThrow(versionIndex, Opcode.IPUT_OBJECT) + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $descriptor + move-result-object v$insertRegister + """ + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt new file mode 100644 index 000000000..0ddda9abf --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/BaseSpoofStreamingDataPatch.kt @@ -0,0 +1,375 @@ +package app.revanced.patches.shared.spoof.streamingdata + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.BytecodePatchBuilder +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patches.shared.extension.Constants.PATCHES_PATH +import app.revanced.patches.shared.extension.Constants.SPOOF_PATH +import app.revanced.patches.shared.formatStreamModelConstructorFingerprint +import app.revanced.util.findInstructionIndicesReversedOrThrow +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.definingClassOrThrow +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter + +const val EXTENSION_CLASS_DESCRIPTOR = + "$SPOOF_PATH/SpoofStreamingDataPatch;" + +fun baseSpoofStreamingDataPatch( + block: BytecodePatchBuilder.() -> Unit = {}, + executeBlock: BytecodePatchContext.() -> Unit = {}, +) = bytecodePatch( + name = "Spoof streaming data", + description = "Adds options to spoof the streaming data to allow playback." +) { + block() + + execute { + // region Block /initplayback requests to fall back to /get_watch requests. + + buildInitPlaybackRequestFingerprint.matchOrThrow().let { + it.method.apply { + val moveUriStringIndex = it.patternMatch!!.startIndex + val targetRegister = + getInstruction(moveUriStringIndex).registerA + + addInstructions( + moveUriStringIndex + 1, + """ + invoke-static { v$targetRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockInitPlaybackRequest(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$targetRegister + """, + ) + } + } + + // endregion + + // region Block /get_watch requests to fall back to /player requests. + + buildPlayerRequestURIFingerprint.methodOrThrow().apply { + val invokeToStringIndex = indexOfToStringInstruction(this) + val uriRegister = + getInstruction(invokeToStringIndex).registerC + + addInstructions( + invokeToStringIndex, + """ + invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri; + move-result-object v$uriRegister + """, + ) + } + + // endregion + + // region Get replacement streams at player requests. + + buildRequestFingerprint.methodOrThrow().apply { + val newRequestBuilderIndex = indexOfNewUrlRequestBuilderInstruction(this) + val urlRegister = + getInstruction(newRequestBuilderIndex).registerD + + val entrySetIndex = indexOfEntrySetInstruction(this) + val mapRegister = if (entrySetIndex < 0) + urlRegister + 1 + else + getInstruction(entrySetIndex).registerC + + var smaliInstructions = + "invoke-static { v$urlRegister, v$mapRegister }, " + + "$EXTENSION_CLASS_DESCRIPTOR->" + + "fetchStreams(Ljava/lang/String;Ljava/util/Map;)V" + + if (entrySetIndex < 0) smaliInstructions = """ + move-object/from16 v$mapRegister, p1 + + """ + smaliInstructions + + // Copy request headers for streaming data fetch. + addInstructions(newRequestBuilderIndex + 2, smaliInstructions) + } + + // endregion + + // region Replace the streaming data. + + val approxDurationMsReference = formatStreamModelConstructorFingerprint.matchOrThrow().let { + with (it.method) { + getInstruction(it.patternMatch!!.startIndex).reference + } + } + + val streamingDataFormatsReference = with(videoStreamingDataConstructorFingerprint.methodOrThrow(videoStreamingDataToStringFingerprint)) { + val getFormatsFieldIndex = indexOfGetFormatsFieldInstruction(this) + val longMaxValueIndex = indexOfLongMaxValueInstruction(this, getFormatsFieldIndex) + val longMaxValueRegister = getInstruction(longMaxValueIndex).registerA + val videoIdIndex = + indexOfFirstInstructionOrThrow(longMaxValueIndex) { + val reference = getReference() + opcode == Opcode.IGET_OBJECT && + reference?.type == "Ljava/lang/String;" && + reference.definingClass == definingClass + } + + val definingClassRegister = + getInstruction(videoIdIndex).registerB + val videoIdReference = + getInstruction(videoIdIndex).reference + + addInstructions( + longMaxValueIndex + 1, """ + # Get video id. + iget-object v$longMaxValueRegister, v$definingClassRegister, $videoIdReference + + # Override approxDurationMs. + invoke-static { v$longMaxValueRegister }, $EXTENSION_CLASS_DESCRIPTOR->getApproxDurationMs(Ljava/lang/String;)J + move-result-wide v$longMaxValueRegister + """ + ) + removeInstruction(longMaxValueIndex) + + getInstruction(getFormatsFieldIndex).reference + } + + createStreamingDataFingerprint.matchOrThrow(createStreamingDataParentFingerprint) + .let { result -> + result.method.apply { + val setStreamDataMethodName = "patch_setStreamingData" + val calcApproxDurationMsMethodName = "patch_calcApproxDurationMs" + val resultClassDef = result.classDef + val resultMethodType = resultClassDef.type + val setStreamingDataIndex = result.patternMatch!!.startIndex + val setStreamingDataField = + getInstruction(setStreamingDataIndex).getReference() + .toString() + + val playerProtoClass = + getInstruction(setStreamingDataIndex + 1).getReference()!!.definingClass + val protobufClass = + protobufClassParseByteBufferFingerprint.definingClassOrThrow() + + val getStreamingDataField = instructions.find { instruction -> + instruction.opcode == Opcode.IGET_OBJECT && + instruction.getReference()?.definingClass == playerProtoClass + }?.getReference() + ?: throw PatchException("Could not find getStreamingDataField") + + val videoDetailsIndex = result.patternMatch!!.endIndex + val videoDetailsRegister = + getInstruction(videoDetailsIndex).registerA + val videoDetailsClass = + getInstruction(videoDetailsIndex).getReference()!!.type + + addInstruction( + videoDetailsIndex + 1, + "invoke-direct { p0, v$videoDetailsRegister }, " + + "$resultMethodType->$setStreamDataMethodName($videoDetailsClass)V", + ) + + result.classDef.methods.add( + ImmutableMethod( + resultMethodType, + setStreamDataMethodName, + listOf( + ImmutableMethodParameter( + videoDetailsClass, + annotations, + "videoDetails" + ) + ), + "V", + AccessFlags.PRIVATE.value or AccessFlags.FINAL.value, + annotations, + null, + MutableMethodImplementation(9), + ).toMutable().apply { + addInstructionsWithLabels( + 0, + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z + move-result v0 + if-eqz v0, :disabled + + # Get video id. + iget-object v2, p1, $videoDetailsClass->c:Ljava/lang/String; + if-eqz v2, :disabled + + # Get streaming data. + invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer; + move-result-object v3 + + if-eqz v3, :disabled + + # Parse streaming data. + sget-object v4, $playerProtoClass->a:$playerProtoClass + invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass + move-result-object v5 + check-cast v5, $playerProtoClass + + iget-object v6, v5, $getStreamingDataField + if-eqz v6, :disabled + + # Caculate approxDurationMs. + invoke-direct { p0, v2 }, $resultMethodType->$calcApproxDurationMsMethodName(Ljava/lang/String;)V + + # Set spoofed streaming data. + iput-object v6, p0, $setStreamingDataField + + :disabled + return-void + """, + ) + }, + ) + + resultClassDef.methods.add( + ImmutableMethod( + resultMethodType, + calcApproxDurationMsMethodName, + listOf( + ImmutableMethodParameter( + "Ljava/lang/String;", + annotations, + "videoId" + ) + ), + "V", + AccessFlags.PRIVATE.value or AccessFlags.FINAL.value, + annotations, + null, + MutableMethodImplementation(12), + ).toMutable().apply { + addInstructionsWithLabels( + 0, + """ + # Get video format list. + iget-object v0, p0, $setStreamingDataField + iget-object v0, v0, $streamingDataFormatsReference + invoke-interface {v0}, Ljava/util/List;->iterator()Ljava/util/Iterator; + move-result-object v0 + + # Initialize approxDurationMs field. + const-wide v1, 0x7fffffffffffffffL + + :loop + # Loop over all video formats to get the approxDurationMs + invoke-interface {v0}, Ljava/util/Iterator;->hasNext()Z + move-result v3 + const-wide/16 v4, 0x0 + + if-eqz v3, :exit + invoke-interface {v0}, Ljava/util/Iterator;->next()Ljava/lang/Object; + move-result-object v3 + check-cast v3, ${(approxDurationMsReference as FieldReference).definingClass} + + # Get approxDurationMs from format + iget-wide v6, v3, $approxDurationMsReference + + # Compare with zero to make sure approxDurationMs is not negative + cmp-long v8, v6, v4 + if-lez v8, :loop + + # Only use the min value of approxDurationMs + invoke-static {v1, v2, v6, v7}, Ljava/lang/Math;->min(JJ)J + move-result-wide v1 + goto :loop + + :exit + # Save approxDurationMs to integrations + invoke-static { p1, v1, v2 }, $EXTENSION_CLASS_DESCRIPTOR->setApproxDurationMs(Ljava/lang/String;J)V + + return-void + """, + ) + }, + ) + } + } + + // endregion + + // region Remove /videoplayback request body to fix playback. + // This is needed when using iOS client as streaming data source. + + buildMediaDataSourceFingerprint.methodOrThrow().apply { + val targetIndex = instructions.lastIndex + + addInstructions( + targetIndex, + """ + # Field a: Stream uri. + # Field c: Http method. + # Field d: Post data. + move-object/from16 v0, p0 + iget-object v1, v0, $definingClass->a:Landroid/net/Uri; + iget v2, v0, $definingClass->c:I + iget-object v3, v0, $definingClass->d:[B + invoke-static { v1, v2, v3 }, $EXTENSION_CLASS_DESCRIPTOR->removeVideoPlaybackPostBody(Landroid/net/Uri;I[B)[B + move-result-object v1 + iput-object v1, v0, $definingClass->d:[B + """, + ) + } + + // endregion + + // region Append spoof info. + + nerdsStatsVideoFormatBuilderFingerprint.methodOrThrow().apply { + findInstructionIndicesReversedOrThrow(Opcode.RETURN_OBJECT).forEach { index -> + val register = getInstruction(index).registerA + + addInstructions( + index, """ + invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->appendSpoofedClient(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$register + """ + ) + } + } + + // endregion + + // region Fix iOS livestream current time. + + hlsCurrentTimeFingerprint.injectLiteralInstructionBooleanCall( + HLS_CURRENT_TIME_FEATURE_FLAG, + "$EXTENSION_CLASS_DESCRIPTOR->fixHLSCurrentTime(Z)Z" + ) + + // endregion + + findMethodOrThrow("$PATCHES_PATH/PatchStatus;") { + name == "SpoofStreamingData" + }.replaceInstruction( + 0, + "const/4 v0, 0x1" + ) + + executeBlock() + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt new file mode 100644 index 000000000..36ae9360b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/streamingdata/Fingerprints.kt @@ -0,0 +1,199 @@ +package app.revanced.patches.shared.spoof.streamingdata + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +// In YouTube 17.34.36, this class is obfuscated. +const val STREAMING_DATA_INTERFACE = + "Lcom/google/protos/youtube/api/innertube/StreamingDataOuterClass${'$'}StreamingData;" + +internal val buildInitPlaybackRequestFingerprint = legacyFingerprint( + name = "buildInitPlaybackRequestFingerprint", + returnType = "Lorg/chromium/net/UrlRequest\$Builder;", + opcodes = listOf( + Opcode.MOVE_RESULT_OBJECT, + Opcode.IGET_OBJECT, // Moves the request URI string to a register to build the request with. + ), + strings = listOf( + "Content-Type", + "Range", + ), +) + +internal val buildPlayerRequestURIFingerprint = legacyFingerprint( + name = "buildPlayerRequestURIFingerprint", + returnType = "Ljava/lang/String;", + strings = listOf( + "key", + "asig", + ), + customFingerprint = { method, _ -> + indexOfToStringInstruction(method) >= 0 + }, +) + +internal fun indexOfToStringInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference().toString() == "Landroid/net/Uri;->toString()Ljava/lang/String;" + } + +internal val buildMediaDataSourceFingerprint = legacyFingerprint( + name = "buildMediaDataSourceFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + returnType = "V", + parameters = listOf( + "Landroid/net/Uri;", + "J", + "I", + "[B", + "Ljava/util/Map;", + "J", + "J", + "Ljava/lang/String;", + "I", + "Ljava/lang/Object;" + ) +) + +internal val buildRequestFingerprint = legacyFingerprint( + name = "buildRequestFingerprint", + customFingerprint = { method, _ -> + method.implementation != null && + indexOfRequestFinishedListenerInstruction(method) >= 0 && + !method.definingClass.startsWith("Lorg/") && + indexOfNewUrlRequestBuilderInstruction(method) >= 0 && + // Earlier targets + (indexOfEntrySetInstruction(method) >= 0 || + // Later targets + method.parameters[1].type == "Ljava/util/Map;") + } +) + +internal fun indexOfRequestFinishedListenerInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setRequestFinishedListener" + } + +internal fun indexOfNewUrlRequestBuilderInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference().toString() == "Lorg/chromium/net/CronetEngine;->newUrlRequestBuilder(Ljava/lang/String;Lorg/chromium/net/UrlRequest${'$'}Callback;Ljava/util/concurrent/Executor;)Lorg/chromium/net/UrlRequest${'$'}Builder;" + } + +internal fun indexOfEntrySetInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_INTERFACE && + getReference().toString() == "Ljava/util/Map;->entrySet()Ljava/util/Set;" + } + +internal val createStreamingDataFingerprint = legacyFingerprint( + name = "createStreamingDataFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + returnType = "V", + parameters = listOf("L"), + opcodes = listOf( + Opcode.IPUT_OBJECT, + Opcode.IGET_OBJECT, + Opcode.IF_NEZ, + Opcode.SGET_OBJECT, + Opcode.IPUT_OBJECT + ), +) + +internal val createStreamingDataParentFingerprint = legacyFingerprint( + name = "createStreamingDataParentFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "L", + parameters = emptyList(), + strings = listOf("Invalid playback type; streaming data is not playable"), +) + +internal val nerdsStatsVideoFormatBuilderFingerprint = legacyFingerprint( + name = "nerdsStatsVideoFormatBuilderFingerprint", + returnType = "Ljava/lang/String;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("L"), + strings = listOf("codecs=\""), +) + +internal val protobufClassParseByteBufferFingerprint = legacyFingerprint( + name = "protobufClassParseByteBufferFingerprint", + accessFlags = AccessFlags.PROTECTED or AccessFlags.STATIC, + parameters = listOf("L", "Ljava/nio/ByteBuffer;"), + returnType = "L", + opcodes = listOf( + Opcode.SGET_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.RETURN_OBJECT, + ), + customFingerprint = { method, _ -> method.name == "parseFrom" }, +) + +internal val videoStreamingDataConstructorFingerprint = legacyFingerprint( + name = "videoStreamingDataConstructorFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + returnType = "V", + customFingerprint = { method, _ -> + indexOfGetFormatsFieldInstruction(method) >= 0 && + indexOfLongMaxValueInstruction(method) >= 0 && + indexOfFormatStreamModelInitInstruction(method) >= 0 + }, +) + +internal fun indexOfGetFormatsFieldInstruction(method: Method) = + method.indexOfFirstInstruction { + val reference = getReference() + opcode == Opcode.IGET_OBJECT && + reference?.definingClass == STREAMING_DATA_INTERFACE && + // Field e: 'formats'. + // Field name is always 'e', regardless of the client version. + reference.name == "e" && + reference.type.startsWith("L") + } + +internal fun indexOfLongMaxValueInstruction(method: Method, index: Int = 0) = + method.indexOfFirstInstruction(index) { + (this as? WideLiteralInstruction)?.wideLiteral == Long.MAX_VALUE + } + +internal fun indexOfFormatStreamModelInitInstruction(method: Method) = + method.indexOfFirstInstruction { + val reference = getReference() + opcode == Opcode.INVOKE_DIRECT && + reference?.name == "" && + reference.parameterTypes.size > 1 + } + +/** + * On YouTube, this class is 'Lcom/google/android/libraries/youtube/innertube/model/media/VideoStreamingData;' + * On YouTube Music, class names are obfuscated. + */ +internal val videoStreamingDataToStringFingerprint = legacyFingerprint( + name = "videoStreamingDataToStringFingerprint", + returnType = "Ljava/lang/String;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + strings = listOf("VideoStreamingData(itags="), + customFingerprint = { method, _ -> + method.name == "toString" + }, +) + +internal const val HLS_CURRENT_TIME_FEATURE_FLAG = 45355374L + +internal val hlsCurrentTimeFingerprint = legacyFingerprint( + name = "hlsCurrentTimeFingerprint", + parameters = listOf("Z", "L"), + literals = listOf(HLS_CURRENT_TIME_FEATURE_FLAG), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/spoof/useragent/BaseSpoofUserAgentPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/useragent/BaseSpoofUserAgentPatch.kt new file mode 100644 index 000000000..105472d90 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/useragent/BaseSpoofUserAgentPatch.kt @@ -0,0 +1,84 @@ +package app.revanced.patches.shared.spoof.useragent + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patches.shared.transformation.IMethodCall +import app.revanced.patches.shared.transformation.filterMapInstruction35c +import app.revanced.patches.shared.transformation.transformInstructionsPatch +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.StringReference + +private const val USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE = + "Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;" + +fun baseSpoofUserAgentPatch( + packageName: String, +) = transformInstructionsPatch( + filterMap = { classDef, _, instruction, instructionIndex -> + filterMapInstruction35c( + "Lapp/revanced/extension", + classDef, + instruction, + instructionIndex, + ) + }, + transform = transform@{ mutableMethod, entry -> + val (_, _, instructionIndex) = entry + + // Replace the result of context.getPackageName(), if it is used in a user agent string. + mutableMethod.apply { + // After context.getPackageName() the result is moved to a register. + val targetRegister = ( + getInstruction(instructionIndex + 1) + as? OneRegisterInstruction ?: return@transform + ).registerA + + // IndexOutOfBoundsException is technically possible here, + // but no such occurrences are present in the app. + val referee = + getInstruction(instructionIndex + 2).getReference()?.toString() + + // Only replace string builder usage. + if (referee != USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE) { + return@transform + } + + // Do not change the package name in methods that use resources, or for methods that use GmsCore. + // Changing these package names will result in playback limitations, + // particularly Android VR background audio only playback. + val resourceOrGmsStringInstructionIndex = indexOfFirstInstruction { + val reference = getReference() + opcode == Opcode.CONST_STRING && + (reference?.string == "android.resource://" || reference?.string == "gcore_") + } + if (resourceOrGmsStringInstructionIndex >= 0) { + return@transform + } + + // Overwrite the result of context.getPackageName() with the original package name. + replaceInstruction( + instructionIndex + 1, + "const-string v$targetRegister, \"$packageName\"", + ) + } + }, +) + +@Suppress("unused") +private enum class MethodCall( + override val definedClassName: String, + override val methodName: String, + override val methodParams: Array, + override val returnType: String, +) : IMethodCall { + GetPackageName( + "Landroid/content/Context;", + "getPackageName", + emptyArray(), + "Ljava/lang/String;", + ), +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/textcomponent/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/textcomponent/Fingerprints.kt new file mode 100644 index 000000000..cd5f03466 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/textcomponent/Fingerprints.kt @@ -0,0 +1,26 @@ +package app.revanced.patches.shared.textcomponent + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val textComponentConstructorFingerprint = legacyFingerprint( + name = "textComponentConstructorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PRIVATE or AccessFlags.CONSTRUCTOR, + strings = listOf("TextComponent") +) + +internal val textComponentContextFingerprint = legacyFingerprint( + name = "textComponentContextFingerprint", + returnType = "L", + accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.IGET_BOOLEAN + ) +) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/textcomponent/TextComponentPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/textcomponent/TextComponentPatch.kt new file mode 100644 index 000000000..6b04a1689 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/textcomponent/TextComponentPatch.kt @@ -0,0 +1,125 @@ +package app.revanced.patches.shared.textcomponent + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.shared.SPANNABLE_STRING_REFERENCE +import app.revanced.patches.shared.indexOfSpannableStringInstruction +import app.revanced.patches.shared.spannableStringBuilderFingerprint +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private lateinit var spannedMethod: MutableMethod +private var spannedIndex = 0 +private var spannedRegister = 0 +private var spannedContextRegister = 0 + +private lateinit var textComponentMethod: MutableMethod +private var textComponentIndex = 0 +private var textComponentRegister = 0 +private var textComponentContextRegister = 0 + +val textComponentPatch = bytecodePatch( + description = "textComponentPatch" +) { + execute { + spannableStringBuilderFingerprint.methodOrThrow().apply { + spannedMethod = this + spannedIndex = indexOfSpannableStringInstruction(this) + spannedRegister = getInstruction(spannedIndex).registerC + spannedContextRegister = + getInstruction(0).registerA + + replaceInstruction( + spannedIndex, + "nop" + ) + addInstruction( + ++spannedIndex, + "invoke-static {v$spannedRegister}, $SPANNABLE_STRING_REFERENCE" + ) + } + + textComponentContextFingerprint.methodOrThrow(textComponentConstructorFingerprint).apply { + textComponentMethod = this + val conversionContextFieldIndex = indexOfFirstInstructionOrThrow { + getReference()?.type == "Ljava/util/Map;" + } - 1 + val conversionContextFieldReference = + getInstruction(conversionContextFieldIndex).reference + + // ~ YouTube 19.32.xx + val legacyCharSequenceIndex = indexOfFirstInstruction { + getReference()?.type == "Ljava/util/BitSet;" + } - 1 + val charSequenceIndex = indexOfFirstInstruction { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.returnType == "V" && + reference.parameterTypes.firstOrNull() == "Ljava/lang/CharSequence;" + } + + val insertIndex: Int + + if (legacyCharSequenceIndex > -2) { + textComponentRegister = + getInstruction(legacyCharSequenceIndex).registerA + insertIndex = legacyCharSequenceIndex - 1 + } else if (charSequenceIndex > -1) { + textComponentRegister = + getInstruction(charSequenceIndex).registerD + insertIndex = charSequenceIndex + } else { + throw PatchException("Could not find insert index") + } + + textComponentContextRegister = getInstruction( + indexOfFirstInstructionOrThrow(insertIndex, Opcode.IGET_OBJECT) + ).registerA + + addInstructions( + insertIndex, """ + move-object/from16 v$textComponentContextRegister, p0 + iget-object v$textComponentContextRegister, v$textComponentContextRegister, $conversionContextFieldReference + """ + ) + textComponentIndex = insertIndex + 2 + } + } +} + +internal fun hookSpannableString( + classDescriptor: String, + methodName: String +) = spannedMethod.addInstructions( + spannedIndex, """ + invoke-static {v$spannedContextRegister, v$spannedRegister}, $classDescriptor->$methodName(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence; + move-result-object v$spannedRegister + """ +) + +internal fun hookTextComponent( + classDescriptor: String, + methodName: String = "onLithoTextLoaded" +) = textComponentMethod.apply { + addInstructions( + textComponentIndex, """ + invoke-static {v$textComponentContextRegister, v$textComponentRegister}, $classDescriptor->$methodName(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence; + move-result-object v$textComponentRegister + """ + ) + textComponentIndex += 2 +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/tracking/BaseSanitizeUrlQueryPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/tracking/BaseSanitizeUrlQueryPatch.kt new file mode 100644 index 000000000..cb3cd55b1 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/tracking/BaseSanitizeUrlQueryPatch.kt @@ -0,0 +1,63 @@ +package app.revanced.patches.shared.tracking + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.extension.Constants.PATCHES_PATH +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$PATCHES_PATH/SanitizeUrlQueryPatch;" + +val baseSanitizeUrlQueryPatch = bytecodePatch( + description = "baseSanitizeUrlQueryPatch" +) { + execute { + copyTextEndpointFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.startIndex + val targetRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 2, """ + invoke-static {v$targetRegister}, $EXTENSION_CLASS_DESCRIPTOR->stripQueryParameters(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$targetRegister + """ + ) + } + } + + setOf( + shareLinkFormatterFingerprint, + systemShareLinkFormatterFingerprint + ).forEach { fingerprint -> + fingerprint.methodOrThrow().apply { + for ((index, instruction) in implementation!!.instructions.withIndex()) { + if (instruction.opcode != Opcode.INVOKE_VIRTUAL) + continue + + if ((instruction as ReferenceInstruction).reference.toString() != "Landroid/content/Intent;->putExtra(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;") + continue + + if (getInstruction(index + 1).opcode != Opcode.GOTO) + continue + + val invokeInstruction = instruction as FiveRegisterInstruction + + replaceInstruction( + index, + "invoke-static {v${invokeInstruction.registerC}, v${invokeInstruction.registerD}, v${invokeInstruction.registerE}}, " + + "$EXTENSION_CLASS_DESCRIPTOR->stripQueryParameters(Landroid/content/Intent;Ljava/lang/String;Ljava/lang/String;)V" + ) + } + } + } + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/tracking/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/tracking/Fingerprints.kt new file mode 100644 index 000000000..471464fbc --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/tracking/Fingerprints.kt @@ -0,0 +1,64 @@ +package app.revanced.patches.shared.tracking + +import app.revanced.util.fingerprint.legacyFingerprint +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +/** + * Copy URL from sharing panel + */ +internal val copyTextEndpointFingerprint = legacyFingerprint( + name = "copyTextEndpointFingerprint", + returnType = "V", + parameters = listOf("L", "Ljava/util/Map;"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.CONST_STRING, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.RETURN_VOID + ), + strings = listOf("text/plain") +) + +/** + * Sharing panel + */ +internal val shareLinkFormatterFingerprint = legacyFingerprint( + name = "shareLinkFormatterFingerprint", + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.CHECK_CAST, + Opcode.GOTO, + null, + Opcode.INVOKE_VIRTUAL + ), + customFingerprint = custom@{ method, _ -> + method.implementation + ?.instructions + ?.withIndex() + ?.filter { (_, instruction) -> + val reference = (instruction as? ReferenceInstruction)?.reference + instruction.opcode == Opcode.SGET_OBJECT && + reference is FieldReference && + reference.name == "androidAppEndpoint" + } + ?.map { (index, _) -> index } + ?.size == 2 + } +) + +/** + * Sharing panel of System + */ +internal val systemShareLinkFormatterFingerprint = legacyFingerprint( + name = "systemShareLinkFormatterFingerprint", + returnType = "V", + parameters = listOf("L", "Ljava/util/Map;"), + strings = listOf("YTShare_Logging_Share_Intent_Endpoint_Byte_Array") +) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/transformation/MethodCall.kt b/patches/src/main/kotlin/app/revanced/patches/shared/transformation/MethodCall.kt new file mode 100644 index 000000000..37213570e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/transformation/MethodCall.kt @@ -0,0 +1,95 @@ +package app.revanced.patches.shared.transformation + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.instruction.Instruction +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +typealias Instruction35cInfo = Triple + +interface IMethodCall { + val definedClassName: String + val methodName: String + val methodParams: Array + val returnType: String + + /** + * Replaces an invoke-virtual instruction with an invoke-static instruction, + * which calls a static replacement method in the respective extension class. + * The method definition in the extension class is expected to be the same, + * except that the method should be static and take as a first parameter + * an instance of the class, in which the original method was defined in. + * + * Example: + * + * original method: Window#setFlags(int, int) + * + * replacement method: Extension#setFlags(Window, int, int) + */ + fun replaceInvokeVirtualWithExtension( + definingClassDescriptor: String, + method: MutableMethod, + instruction: Instruction35c, + instructionIndex: Int, + ) { + val registers = arrayOf( + instruction.registerC, + instruction.registerD, + instruction.registerE, + instruction.registerF, + instruction.registerG, + ) + val argsNum = methodParams.size + 1 // + 1 for instance of definedClassName + if (argsNum > registers.size) { + // should never happen, but just to be sure (also for the future) a safety check + throw RuntimeException( + "Not enough registers for $definedClassName#$methodName: " + + "Required $argsNum registers, but only got ${registers.size}.", + ) + } + + val args = registers.take(argsNum).joinToString(separator = ", ") { reg -> "v$reg" } + val replacementMethod = + "$methodName(${definedClassName}${methodParams.joinToString(separator = "")})$returnType" + + method.replaceInstruction( + instructionIndex, + "invoke-static { $args }, $definingClassDescriptor->$replacementMethod", + ) + } +} + +inline fun fromMethodReference( + methodReference: MethodReference, +) + where E : Enum, E : IMethodCall = enumValues().firstOrNull { search -> + search.definedClassName == methodReference.definingClass && + search.methodName == methodReference.name && + methodReference.parameterTypes.toTypedArray().contentEquals(search.methodParams) && + search.returnType == methodReference.returnType +} + +inline fun filterMapInstruction35c( + extensionClassDescriptorPrefix: String, + classDef: ClassDef, + instruction: Instruction, + instructionIndex: Int, +): Instruction35cInfo? where E : Enum, E : IMethodCall { + if (classDef.startsWith(extensionClassDescriptorPrefix)) { + // avoid infinite recursion + return null + } + + if (instruction.opcode != Opcode.INVOKE_VIRTUAL) { + return null + } + + val invokeInstruction = instruction as Instruction35c + val methodRef = invokeInstruction.reference as MethodReference + val methodCall = fromMethodReference(methodRef) ?: return null + + return Instruction35cInfo(methodCall, invokeInstruction, instructionIndex) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/transformation/TransformInstructionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/transformation/TransformInstructionsPatch.kt new file mode 100644 index 000000000..46a78a714 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/transformation/TransformInstructionsPatch.kt @@ -0,0 +1,53 @@ +package app.revanced.patches.shared.transformation + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.util.findMutableMethodOf +import com.android.tools.smali.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.Instruction + +fun transformInstructionsPatch( + filterMap: (ClassDef, Method, Instruction, Int) -> T?, + transform: (MutableMethod, T) -> Unit, +) = bytecodePatch( + description = "transformInstructionsPatch" +) { + // Returns the patch indices as a Sequence, which will execute lazily. + fun findPatchIndices(classDef: ClassDef, method: Method): Sequence? = + method.implementation?.instructions?.asSequence()?.withIndex() + ?.mapNotNull { (index, instruction) -> + filterMap(classDef, method, instruction, index) + } + + execute { + // Find all methods to patch + buildMap { + classes.forEach { classDef -> + val methods = buildList { + classDef.methods.forEach { method -> + // Since the Sequence executes lazily, + // using any() results in only calling + // filterMap until the first index has been found. + if (findPatchIndices(classDef, method)?.any() == true) add(method) + } + } + + if (methods.isNotEmpty()) { + put(classDef, methods) + } + } + }.forEach { (classDef, methods) -> + // And finally transform the methods... + val mutableClass = proxy(classDef).mutableClass + + methods.map(mutableClass::findMutableMethodOf).forEach methods@{ mutableMethod -> + val patchIndices = + findPatchIndices(mutableClass, mutableMethod)?.toCollection(ArrayDeque()) + ?: return@methods + + while (!patchIndices.isEmpty()) transform(mutableMethod, patchIndices.removeLast()) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/translations/BaseTranslationsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/translations/BaseTranslationsPatch.kt new file mode 100644 index 000000000..8554a4d62 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/translations/BaseTranslationsPatch.kt @@ -0,0 +1,171 @@ +package app.revanced.patches.shared.translations + +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.ResourcePatchContext +import app.revanced.util.inputStreamFromBundledResource +import org.w3c.dom.Node +import java.io.File +import java.nio.file.Files +import java.nio.file.StandardCopyOption +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.OutputKeys +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult + +// Array of all possible app languages. +val APP_LANGUAGES = arrayOf( + "af", "am", "ar", "ar-rXB", "as", "az", + "b+es+419", "b+sr+Latn", "be", "bg", "bn", "bs", + "ca", "cs", + "da", "de", + "el", "en-rAU", "en-rCA", "en-rGB", "en-rIN", "en-rXA", "en-rXC", "es", "es-rUS", "et", "eu", + "fa", "fi", "fr", "fr-rCA", + "gl", "gu", + "hi", "hr", "hu", "hy", + "id", "in", "is", "it", "iw", + "ja", + "ka", "kk", "km", "kn", "ko", "ky", + "lo", "lt", "lv", + "mk", "ml", "mn", "mr", "ms", "my", + "nb", "ne", "nl", "no", + "or", + "pa", "pl", "pt", "pt-rBR", "pt-rPT", + "ro", "ru", + "si", "sk", "sl", "sq", "sr", "sv", "sw", + "ta", "te", "th", "tl", "tr", + "uk", "ur", "uz", + "vi", + "zh", "zh-rCN", "zh-rHK", "zh-rTW", "zu", +) + +fun ResourcePatchContext.baseTranslationsPatch( + customTranslations: String?, + selectedTranslations: String?, + selectedStringResources: String?, + translationsArray: Set, + sourceDirectory: String, +) { + val resourceDirectory = get("res") + + // Check if the custom translation path is valid. + customTranslations?.takeIf { it.isNotEmpty() }?.let { customLang -> + try { + val customLangFile = File(customLang) + if (!customLangFile.exists() || !customLangFile.isFile || customLangFile.name != "strings.xml") { + throw PatchException("Invalid custom language file: $customLang") + } + val valuesDirectory = resourceDirectory.resolve("values") + val destinationFile = valuesDirectory.resolve("strings.xml") + + updateStringsXml(customLangFile, destinationFile) + } catch (e: Exception) { + // Exception is thrown if an invalid path is used in the patch option. + throw PatchException("Invalid custom translations path: $customLang") + } + } ?: run { + // Process selected translations if no custom translation is set. + val selectedTranslationsArray = + selectedTranslations?.split(",")?.map { it.trim() }?.toTypedArray() + ?: throw PatchException("Invalid selected languages.") + val filteredLanguages = + translationsArray.filter { it in selectedTranslationsArray }.toTypedArray() + copyStringsXml(sourceDirectory, filteredLanguages) + } + + // Process selected string resources. + val selectedStringResourcesArray = + selectedStringResources?.split(",")?.map { it.trim() }?.toTypedArray() + ?: throw PatchException("Invalid selected string resources.") + val filteredStringResources = + APP_LANGUAGES.filter { it in selectedStringResourcesArray }.toTypedArray() + + // Remove unselected app languages. + APP_LANGUAGES.filter { it !in filteredStringResources }.forEach { language -> + resourceDirectory.resolve("values-$language").takeIf { it.exists() && it.isDirectory } + ?.deleteRecursively() + } +} + +/** + * Extension function to ResourceContext to copy XML translation files. + * + * @param sourceDirectory The source directory containing the translation files. + * @param languageArray The array of language codes to process. + */ +private fun ResourcePatchContext.copyStringsXml( + sourceDirectory: String, + languageArray: Array +) { + val resourceDirectory = get("res") + languageArray.forEach { language -> + inputStreamFromBundledResource( + "$sourceDirectory/translations", + "$language/strings.xml" + )?.let { inputStream -> + val directory = "values-$language-v21" + val valuesV21Directory = resourceDirectory.resolve(directory) + if (!valuesV21Directory.isDirectory) Files.createDirectories(valuesV21Directory.toPath()) + + Files.copy( + inputStream, + resourceDirectory.resolve("$directory/strings.xml").toPath(), + StandardCopyOption.REPLACE_EXISTING + ) + } + } +} + +/** + * Updates the contents of the destination strings.xml file by merging it with the source strings.xml file. + * + * This function reads both source and destination XML files, compares each element by their + * unique "name" attribute, and if a match is found, it replaces the content in the destination file with + * the content from the source file. + * + * @param sourceFile The source strings.xml file containing new string values. + * @param destinationFile The destination strings.xml file to be updated with values from the source file. + */ +private fun updateStringsXml(sourceFile: File, destinationFile: File) { + val documentBuilderFactory = DocumentBuilderFactory.newInstance() + val documentBuilder = documentBuilderFactory.newDocumentBuilder() + + // Parse the source and destination XML files into Document objects + val sourceDoc = documentBuilder.parse(sourceFile) + val destinationDoc = documentBuilder.parse(destinationFile) + + val sourceStrings = sourceDoc.getElementsByTagName("string") + val destinationStrings = destinationDoc.getElementsByTagName("string") + + // Create a map to store the elements from the source document by their "name" attribute + val sourceMap = mutableMapOf() + + // Populate the map with nodes from the source document + for (i in 0 until sourceStrings.length) { + val node = sourceStrings.item(i) + val name = node.attributes.getNamedItem("name").nodeValue + sourceMap[name] = node + } + + // Update the destination document with values from the source document + for (i in 0 until destinationStrings.length) { + val node = destinationStrings.item(i) + val name = node.attributes.getNamedItem("name").nodeValue + if (sourceMap.containsKey(name)) { + node.textContent = sourceMap[name]?.textContent + } + } + + /** + * Prepare the transformer for writing the updated document back to the file. + * The transformer is configured to indent the output XML for better readability. + */ + val transformerFactory = TransformerFactory.newInstance() + val transformer = transformerFactory.newTransformer() + transformer.setOutputProperty(OutputKeys.INDENT, "yes") + transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2") + + val domSource = DOMSource(destinationDoc) + val streamResult = StreamResult(destinationFile) + transformer.transform(domSource, streamResult) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/viewgroup/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/viewgroup/Fingerprints.kt new file mode 100644 index 000000000..b9b0e7fdb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/viewgroup/Fingerprints.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.shared.viewgroup + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val viewGroupMarginFingerprint = legacyFingerprint( + name = "viewGroupMarginFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("Landroid/view/View;", "I", "I"), +) + +internal val viewGroupMarginParentFingerprint = legacyFingerprint( + name = "viewGroupMarginParentFingerprint", + returnType = "Landroid/view/ViewGroup${'$'}LayoutParams;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("Ljava/lang/Class;", "Landroid/view/ViewGroup${'$'}LayoutParams;"), + strings = listOf("SafeLayoutParams"), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/viewgroup/ViewGroupMarginLayoutParamsHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/viewgroup/ViewGroupMarginLayoutParamsHookPatch.kt new file mode 100644 index 000000000..370cf1460 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/viewgroup/ViewGroupMarginLayoutParamsHookPatch.kt @@ -0,0 +1,29 @@ +package app.revanced.patches.shared.viewgroup + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.extension.Constants.EXTENSION_UTILS_CLASS_DESCRIPTOR +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.methodOrThrow + +val viewGroupMarginLayoutParamsHookPatch = bytecodePatch( + description = "viewGroupMarginLayoutParamsHookPatch" +) { + execute { + val setViewGroupMarginCall = with( + viewGroupMarginFingerprint.methodOrThrow(viewGroupMarginParentFingerprint) + ) { + "$definingClass->$name(Landroid/view/View;II)V" + } + + findMethodOrThrow(EXTENSION_UTILS_CLASS_DESCRIPTOR) { + name == "hideViewGroupByMarginLayoutParams" + }.addInstructions( + 0, """ + const/4 v0, 0x0 + invoke-static {p0, v0, v0}, $setViewGroupMarginCall + """ + ) + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ads/general/AdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ads/general/AdsPatch.kt new file mode 100644 index 000000000..5419d11e8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ads/general/AdsPatch.kt @@ -0,0 +1,154 @@ +package app.revanced.patches.youtube.ads.general + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.ads.baseAdsPatch +import app.revanced.patches.shared.ads.hookLithoFullscreenAds +import app.revanced.patches.shared.ads.hookNonLithoFullscreenAds +import app.revanced.patches.shared.extension.Constants.PATCHES_PATH +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.ADS_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.youtube.utils.fix.doublebacktoclose.doubleBackToClosePatch +import app.revanced.patches.youtube.utils.fix.swiperefresh.swipeRefreshPatch +import app.revanced.patches.youtube.utils.patch.PatchList.HIDE_ADS +import app.revanced.patches.youtube.utils.resourceid.adAttribution +import app.revanced.patches.youtube.utils.resourceid.interstitialsContainer +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.findMethodOrThrow +import app.revanced.util.findMutableMethodOf +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.injectHideViewCall +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction31i +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c + +private const val ADS_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/AdsFilter;" + +@Suppress("unused") +val adsPatch = bytecodePatch( + HIDE_ADS.title, + HIDE_ADS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + baseAdsPatch(ADS_CLASS_DESCRIPTOR, "hideVideoAds"), + doubleBackToClosePatch, + lithoFilterPatch, + sharedResourceIdPatch, + swipeRefreshPatch, + ) + + execute { + addLithoFilter(ADS_FILTER_CLASS_DESCRIPTOR) + + // region patch for hide fullscreen ads + + // non-litho view, used in some old clients. + interstitialsContainerFingerprint + .methodOrThrow() + .hookNonLithoFullscreenAds(interstitialsContainer) + + // litho view, used in 'ShowDialogCommandOuterClass' in innertube + showDialogCommandFingerprint + .matchOrThrow() + .hookLithoFullscreenAds() + + // endregion + + // region patch for hide general ads + + classes.forEach { classDef -> + classDef.methods.forEach { method -> + method.implementation.apply { + this?.instructions?.forEachIndexed { index, instruction -> + if (instruction.opcode != Opcode.CONST) + return@forEachIndexed + // Instruction to store the id adAttribution into a register + if ((instruction as Instruction31i).wideLiteral != adAttribution) + return@forEachIndexed + + val insertIndex = index + 1 + + // Call to get the view with the id adAttribution + (instructions.elementAt(insertIndex)).apply { + if (opcode != Opcode.INVOKE_VIRTUAL) + return@forEachIndexed + + // Hide the view + val viewRegister = (this as Instruction35c).registerC + proxy(classDef) + .mutableClass + .findMutableMethodOf(method) + .injectHideViewCall( + insertIndex, + viewRegister, + ADS_CLASS_DESCRIPTOR, + "hideAdAttributionView" + ) + } + } + } + } + } + + // endregion + + // region patch for hide get premium + + compactYpcOfferModuleViewFingerprint.matchOrThrow().let { + it.method.apply { + val startIndex = it.patternMatch!!.startIndex + val measuredWidthRegister = + getInstruction(startIndex).registerA + val measuredHeightInstruction = + getInstruction(startIndex + 1) + val measuredHeightRegister = measuredHeightInstruction.registerA + val tempRegister = measuredHeightInstruction.registerB + + addInstructionsWithLabels( + startIndex + 2, """ + invoke-static {}, $ADS_CLASS_DESCRIPTOR->hideGetPremium()Z + move-result v$tempRegister + if-eqz v$tempRegister, :show + const/4 v$measuredWidthRegister, 0x0 + const/4 v$measuredHeightRegister, 0x0 + """, ExternalLabel("show", getInstruction(startIndex + 2)) + ) + } + } + + // endregion + + findMethodOrThrow("$PATCHES_PATH/PatchStatus;") { + name == "HideFullscreenAdsDefaultBoolean" + }.replaceInstruction( + 0, + "const/4 v0, 0x1" + ) + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: ADS" + ), + HIDE_ADS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ads/general/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ads/general/Fingerprints.kt new file mode 100644 index 000000000..c51509b41 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ads/general/Fingerprints.kt @@ -0,0 +1,53 @@ +package app.revanced.patches.youtube.ads.general + +import app.revanced.patches.youtube.utils.resourceid.interstitialsContainer +import app.revanced.patches.youtube.utils.resourceid.slidingDialogAnimation +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val compactYpcOfferModuleViewFingerprint = legacyFingerprint( + name = "compactYpcOfferModuleViewFingerprint", + returnType = "V", + accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL, + parameters = listOf("I", "I"), + opcodes = listOf( + Opcode.ADD_INT_2ADDR, + Opcode.ADD_INT_2ADDR, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/CompactYpcOfferModuleView;") && + method.name == "onMeasure" + } +) + +internal val interstitialsContainerFingerprint = legacyFingerprint( + name = "interstitialsContainerFingerprint", + returnType = "V", + strings = listOf("overlay_controller_param"), + literals = listOf(interstitialsContainer) +) + +internal val showDialogCommandFingerprint = legacyFingerprint( + name = "showDialogCommandFingerprint", + returnType = "V", + opcodes = listOf( + Opcode.IF_EQ, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.IGET, // get dialog code + ), + literals = listOf(slidingDialogAnimation), + // 18.43 and earlier has a different first parameter. + // Since this fingerprint is somewhat weak, work around by checking for both method parameter signatures. + customFingerprint = { method, _ -> + // 18.43 and earlier parameters are: "L", "L" + // 18.44+ parameters are "[B", "L" + val parameterTypes = method.parameterTypes + + parameterTypes.size == 2 && parameterTypes[1].startsWith("L") + }, +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/alternative/thumbnails/AlternativeThumbnailsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/alternative/thumbnails/AlternativeThumbnailsPatch.kt new file mode 100644 index 000000000..15d5f8d23 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/alternative/thumbnails/AlternativeThumbnailsPatch.kt @@ -0,0 +1,48 @@ +package app.revanced.patches.youtube.alternative.thumbnails + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.imageurl.addImageUrlErrorCallbackHook +import app.revanced.patches.shared.imageurl.addImageUrlHook +import app.revanced.patches.shared.imageurl.addImageUrlSuccessCallbackHook +import app.revanced.patches.shared.imageurl.cronetImageUrlHookPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.navigation.navigationBarHookPatch +import app.revanced.patches.youtube.utils.patch.PatchList.ALTERNATIVE_THUMBNAILS +import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch + +@Suppress("unused") +val alternativeThumbnailsPatch = bytecodePatch( + ALTERNATIVE_THUMBNAILS.title, + ALTERNATIVE_THUMBNAILS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + cronetImageUrlHookPatch(true), + navigationBarHookPatch, + playerTypeHookPatch, + settingsPatch, + ) + execute { + + addImageUrlHook(ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR) + addImageUrlSuccessCallbackHook(ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR) + addImageUrlErrorCallbackHook(ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR) + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: ALTERNATIVE_THUMBNAILS", + "SETTINGS: ALTERNATIVE_THUMBNAILS" + ), + ALTERNATIVE_THUMBNAILS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/alternative/thumbnails/BypassImageRegionRestrictionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/alternative/thumbnails/BypassImageRegionRestrictionsPatch.kt new file mode 100644 index 000000000..ac627b170 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/alternative/thumbnails/BypassImageRegionRestrictionsPatch.kt @@ -0,0 +1,39 @@ +package app.revanced.patches.youtube.alternative.thumbnails + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.imageurl.addImageUrlHook +import app.revanced.patches.shared.imageurl.cronetImageUrlHookPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.BYPASS_IMAGE_REGION_RESTRICTIONS +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch + +@Suppress("unused") +val bypassImageRegionRestrictionsPatch = bytecodePatch( + BYPASS_IMAGE_REGION_RESTRICTIONS.title, + BYPASS_IMAGE_REGION_RESTRICTIONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + cronetImageUrlHookPatch(true), + settingsPatch, + ) + execute { + + addImageUrlHook() + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: ALTERNATIVE_THUMBNAILS", + "SETTINGS: BYPASS_IMAGE_REGION_RESTRICTIONS" + ), + BYPASS_IMAGE_REGION_RESTRICTIONS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt new file mode 100644 index 000000000..fa99e6c4b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt @@ -0,0 +1,402 @@ +package app.revanced.patches.youtube.feed.components + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.mainactivity.onCreateMethod +import app.revanced.patches.youtube.utils.bottomsheet.bottomSheetHookPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.engagementPanelBuilderFingerprint +import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.youtube.utils.extension.Constants.FEED_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.FEED_PATH +import app.revanced.patches.youtube.utils.mainactivity.mainActivityResolvePatch +import app.revanced.patches.youtube.utils.navigation.navigationBarHookPatch +import app.revanced.patches.youtube.utils.patch.PatchList.HIDE_FEED_COMPONENTS +import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.utils.resourceid.bar +import app.revanced.patches.youtube.utils.resourceid.captionToggleContainer +import app.revanced.patches.youtube.utils.resourceid.channelListSubMenu +import app.revanced.patches.youtube.utils.resourceid.contentPill +import app.revanced.patches.youtube.utils.resourceid.horizontalCardList +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.scrollTopParentFingerprint +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT +import app.revanced.util.fingerprint.injectLiteralInstructionViewCall +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.StringReference +import com.android.tools.smali.dexlib2.util.MethodUtil + +private const val CAROUSEL_SHELF_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/CarouselShelfFilter;" +private const val FEED_COMPONENTS_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/FeedComponentsFilter;" +private const val FEED_VIDEO_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/FeedVideoFilter;" +private const val FEED_VIDEO_VIEWS_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/FeedVideoViewsFilter;" +private const val KEYWORD_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/KeywordContentFilter;" +private const val RELATED_VIDEO_CLASS_DESCRIPTOR = + "$FEED_PATH/RelatedVideoPatch;" + +@Suppress("unused") +val feedComponentsPatch = bytecodePatch( + HIDE_FEED_COMPONENTS.title, + HIDE_FEED_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + mainActivityResolvePatch, + navigationBarHookPatch, + playerTypeHookPatch, + sharedResourceIdPatch, + settingsPatch, + bottomSheetHookPatch, + ) + execute { + + // region patch for hide carousel shelf, subscriptions channel section, latest videos button + + listOf( + // carousel shelf, only used to tablet layout. + Triple( + breakingNewsFingerprint, + "hideBreakingNewsShelf", + horizontalCardList + ), + // subscriptions channel section. + Triple( + channelListSubMenuFingerprint, + "hideSubscriptionsChannelSection", + channelListSubMenu + ), + // latest videos button + Triple( + contentPillFingerprint, + "hideLatestVideosButton", + contentPill + ), + Triple( + latestVideosButtonFingerprint, + "hideLatestVideosButton", + bar + ), + ).forEach { (fingerprint, methodName, literal) -> + val smaliInstruction = """ + invoke-static {v$REGISTER_TEMPLATE_REPLACEMENT}, $FEED_CLASS_DESCRIPTOR->$methodName(Landroid/view/View;)V + """ + fingerprint.injectLiteralInstructionViewCall(literal, smaliInstruction) + } + + // endregion + + // region patch for hide caption button + + captionsButtonFingerprint.methodOrThrow().apply { + val constIndex = indexOfFirstLiteralInstructionOrThrow(captionToggleContainer) + val insertIndex = indexOfFirstInstructionReversedOrThrow(constIndex, Opcode.IF_EQZ) + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $FEED_CLASS_DESCRIPTOR->hideCaptionsButton(Landroid/view/View;)Landroid/view/View; + move-result-object v$insertRegister + """ + ) + } + + captionsButtonSyntheticFingerprint.methodOrThrow().apply { + val constIndex = indexOfFirstLiteralInstructionOrThrow(captionToggleContainer) + val targetIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.MOVE_RESULT_OBJECT) + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $FEED_CLASS_DESCRIPTOR->hideCaptionsButtonContainer(Landroid/view/View;)V" + ) + } + + // endregion + + // region patch for hide floating button + + onCreateMethod.apply { + val stringIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.CONST_STRING && + getReference()?.string == "fab" + } + val stringRegister = getInstruction(stringIndex).registerA + val insertIndex = indexOfFirstInstructionOrThrow(stringIndex) { + opcode == Opcode.INVOKE_DIRECT && + getReference()?.name == "" + } + val jumpIndex = indexOfFirstInstructionOrThrow(insertIndex, Opcode.CONST_STRING) + + addInstructionsWithLabels( + insertIndex, """ + invoke-static {v$stringRegister}, $FEED_CLASS_DESCRIPTOR->hideFloatingButton(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$stringRegister + if-eqz v$stringRegister, :hide + """, ExternalLabel("hide", getInstruction(jumpIndex)) + ) + } + + // endregion + + // region patch for hide relative video + + fun Method.indexOfEngagementPanelBuilderInstruction(targetMethod: MutableMethod) = + indexOfFirstInstruction { + opcode == Opcode.INVOKE_DIRECT && + MethodUtil.methodSignaturesMatch( + targetMethod, + getReference()!! + ) + } + + engagementPanelBuilderFingerprint.matchOrThrow().let { + it.classDef.methods.filter { method -> + method.indexOfEngagementPanelBuilderInstruction(it.method) >= 0 + }.forEach { method -> + method.apply { + val index = indexOfEngagementPanelBuilderInstruction(it.method) + val register = getInstruction(index + 1).registerA + + addInstruction( + index + 2, + "invoke-static {v$register}, " + + "$RELATED_VIDEO_CLASS_DESCRIPTOR->showEngagementPanel(Ljava/lang/Object;)V" + ) + } + } + } + + engagementPanelUpdateFingerprint.methodOrThrow(engagementPanelBuilderFingerprint) + .addInstruction( + 0, + "invoke-static {}, $RELATED_VIDEO_CLASS_DESCRIPTOR->hideEngagementPanel()V" + ) + + linearLayoutManagerItemCountsFingerprint.matchOrThrow().let { + val methodWalker = + it.getWalkerMethod(it.patternMatch!!.endIndex) + methodWalker.apply { + val index = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT) + val register = getInstruction(index).registerA + + addInstructions( + index + 1, """ + invoke-static {v$register}, $RELATED_VIDEO_CLASS_DESCRIPTOR->overrideItemCounts(I)I + move-result v$register + """ + ) + } + } + + // endregion + + // region patch for hide subscriptions channel section for tablet + + arrayOf( + channelListSubMenuTabletFingerprint, + channelListSubMenuTabletSyntheticFingerprint + ).forEach { fingerprint -> + fingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $FEED_CLASS_DESCRIPTOR->hideSubscriptionsChannelSection()Z + move-result v0 + if-eqz v0, :show + return-void + """, ExternalLabel("show", getInstruction(0)) + ) + } + } + + // endregion + + // region patch for hide category bar + + fun Pair.patch( + insertIndexOffset: Int = 0, + hookRegisterOffset: Int = 0, + instructions: (Int) -> String + ) = + matchOrThrow().let { + it.method.apply { + val endIndex = it.patternMatch!!.endIndex + + val insertIndex = endIndex + insertIndexOffset + val register = + getInstruction(endIndex + hookRegisterOffset).registerA + + addInstructions(insertIndex, instructions(register)) + } + } + + filterBarHeightFingerprint.patch { register -> + """ + invoke-static { v$register }, $FEED_CLASS_DESCRIPTOR->hideCategoryBarInFeed(I)I + move-result v$register + """ + } + + relatedChipCloudFingerprint.patch(1) { register -> + "invoke-static { v$register }, " + + "$FEED_CLASS_DESCRIPTOR->hideCategoryBarInRelatedVideos(Landroid/view/View;)V" + } + + searchResultsChipBarFingerprint.patch(-1, -2) { register -> + """ + invoke-static { v$register }, $FEED_CLASS_DESCRIPTOR->hideCategoryBarInSearch(I)I + move-result v$register + """ + } + + // endregion + + // region patch for hide mix playlists + + elementParserFingerprint.matchOrThrow(elementParserParentFingerprint).let { + it.method.apply { + val freeRegister = implementation!!.registerCount - parameters.size - 2 + val insertIndex = indexOfFirstInstructionOrThrow { + val reference = ((this as? ReferenceInstruction)?.reference as? MethodReference) + + reference?.parameterTypes?.size == 1 && + reference.parameterTypes.first() == "[B" && + reference.returnType.startsWith("L") + } + + val objectIndex = indexOfFirstInstructionOrThrow(Opcode.MOVE_OBJECT) + val objectRegister = getInstruction(objectIndex).registerA + + val jumpIndex = it.patternMatch!!.startIndex + + addInstructionsWithLabels( + insertIndex, """ + invoke-static {v$objectRegister, v$freeRegister}, $FEED_COMPONENTS_FILTER_CLASS_DESCRIPTOR->filterMixPlaylists(Ljava/lang/Object;[B)Z + move-result v$freeRegister + if-nez v$freeRegister, :filter + """, ExternalLabel("filter", getInstruction(jumpIndex)) + ) + + addInstruction( + 0, + "move-object/from16 v$freeRegister, p3" + ) + } + } + + // endregion + + // region patch for hide show more button + + showMoreButtonFingerprint.mutableClassOrThrow().let { + val getViewMethod = + it.methods.find { method -> + method.parameters.isEmpty() && + method.returnType == "Landroid/view/View;" + } + + getViewMethod?.apply { + val targetIndex = implementation!!.instructions.size - 1 + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex, + "invoke-static {v$targetRegister}, $FEED_CLASS_DESCRIPTOR->hideShowMoreButton(Landroid/view/View;)V" + ) + } ?: throw PatchException("Failed to find getView method") + } + + // endregion + + // region patch for hide channel tab + + val channelTabBuilderMethod = + channelTabBuilderFingerprint.methodOrThrow(scrollTopParentFingerprint) + + channelTabRendererFingerprint.matchOrThrow().let { + it.method.apply { + val iteratorIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "hasNext" + } + val iteratorRegister = + getInstruction(iteratorIndex).registerC + + val targetIndex = indexOfFirstInstructionOrThrow { + val reference = ((this as? ReferenceInstruction)?.reference as? MethodReference) + + opcode == Opcode.INVOKE_INTERFACE && + reference?.returnType == channelTabBuilderMethod.returnType && + reference.parameterTypes == channelTabBuilderMethod.parameterTypes + } + + val objectIndex = + indexOfFirstInstructionReversedOrThrow(targetIndex, Opcode.IGET_OBJECT) + val objectInstruction = getInstruction(objectIndex) + val objectReference = getInstruction(objectIndex).reference + + addInstructionsWithLabels( + objectIndex + 1, """ + invoke-static {v${objectInstruction.registerA}}, $FEED_CLASS_DESCRIPTOR->hideChannelTab(Ljava/lang/String;)Z + move-result v${objectInstruction.registerA} + if-eqz v${objectInstruction.registerA}, :ignore + invoke-interface {v$iteratorRegister}, Ljava/util/Iterator;->remove()V + goto :next_iterator + :ignore + iget-object v${objectInstruction.registerA}, v${objectInstruction.registerB}, $objectReference + """, ExternalLabel("next_iterator", getInstruction(iteratorIndex)) + ) + } + } + + // endregion + + addLithoFilter(CAROUSEL_SHELF_FILTER_CLASS_DESCRIPTOR) + addLithoFilter(FEED_COMPONENTS_FILTER_CLASS_DESCRIPTOR) + addLithoFilter(FEED_VIDEO_FILTER_CLASS_DESCRIPTOR) + addLithoFilter(FEED_VIDEO_VIEWS_FILTER_CLASS_DESCRIPTOR) + addLithoFilter(KEYWORD_FILTER_CLASS_DESCRIPTOR) + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: FEED", + "SETTINGS: HIDE_FEED_COMPONENTS" + ), + HIDE_FEED_COMPONENTS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/Fingerprints.kt new file mode 100644 index 000000000..f1e655d25 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/Fingerprints.kt @@ -0,0 +1,192 @@ +package app.revanced.patches.youtube.feed.components + +import app.revanced.patches.youtube.utils.resourceid.bar +import app.revanced.patches.youtube.utils.resourceid.barContainerHeight +import app.revanced.patches.youtube.utils.resourceid.captionToggleContainer +import app.revanced.patches.youtube.utils.resourceid.channelListSubMenu +import app.revanced.patches.youtube.utils.resourceid.contentPill +import app.revanced.patches.youtube.utils.resourceid.drawerResults +import app.revanced.patches.youtube.utils.resourceid.expandButtonDown +import app.revanced.patches.youtube.utils.resourceid.filterBarHeight +import app.revanced.patches.youtube.utils.resourceid.horizontalCardList +import app.revanced.patches.youtube.utils.resourceid.relatedChipCloudMargin +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val breakingNewsFingerprint = legacyFingerprint( + name = "breakingNewsFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf(horizontalCardList), +) + +internal val captionsButtonFingerprint = legacyFingerprint( + name = "captionsButtonFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(captionToggleContainer), +) + +internal val captionsButtonSyntheticFingerprint = legacyFingerprint( + name = "captionsButtonSyntheticFingerprint", + returnType = "Landroid/view/View;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL or AccessFlags.BRIDGE or AccessFlags.SYNTHETIC, + parameters = listOf("Landroid/content/Context;"), + literals = listOf(captionToggleContainer), +) + +internal val channelListSubMenuFingerprint = legacyFingerprint( + name = "channelListSubMenuFingerprint", + literals = listOf(channelListSubMenu), +) + +internal val channelListSubMenuTabletFingerprint = legacyFingerprint( + name = "channelListSubMenuTabletFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(drawerResults), +) + +internal val channelListSubMenuTabletSyntheticFingerprint = legacyFingerprint( + name = "channelListSubMenuTabletSyntheticFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL or AccessFlags.SYNTHETIC, + strings = listOf("is_horizontal_drawer_context") +) + +internal val channelTabBuilderFingerprint = legacyFingerprint( + name = "channelTabBuilderFingerprint", + returnType = "Landroid/view/View;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Ljava/lang/CharSequence;", "Ljava/lang/CharSequence;", "Z", "L") +) + +internal val channelTabRendererFingerprint = legacyFingerprint( + name = "channelTabRendererFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "Ljava/util/List;", "I"), + strings = listOf("TabRenderer.content contains SectionListRenderer but the tab does not have a section list controller.") +) + +internal val contentPillFingerprint = legacyFingerprint( + name = "contentPillFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "Z"), + literals = listOf(contentPill), +) + +internal val elementParserFingerprint = legacyFingerprint( + name = "elementParserFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "L", "[B", "L", "L"), + opcodes = listOf( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IGET_OBJECT, + Opcode.RETURN_OBJECT + ) +) + +internal val elementParserParentFingerprint = legacyFingerprint( + name = "elementParserParentFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + strings = listOf("Element tree missing id in debug mode.") +) + +internal val engagementPanelUpdateFingerprint = legacyFingerprint( + name = "engagementPanelUpdateFingerprint", + returnType = "V", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + parameters = listOf("L", "Z"), + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference().toString() == "Ljava/util/ArrayDeque;->pop()Ljava/lang/Object;" + } >= 0 + } +) + +internal val filterBarHeightFingerprint = legacyFingerprint( + name = "filterBarHeightFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + opcodes = listOf( + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IPUT + ), + literals = listOf(filterBarHeight), +) + +internal val latestVideosButtonFingerprint = legacyFingerprint( + name = "latestVideosButtonFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "Z"), + literals = listOf(bar), +) + +internal val linearLayoutManagerItemCountsFingerprint = legacyFingerprint( + name = "linearLayoutManagerItemCountsFingerprint", + returnType = "I", + accessFlags = AccessFlags.FINAL.value, + parameters = listOf("L", "L", "L", "Z"), + opcodes = listOf( + Opcode.IF_NEZ, + Opcode.IF_LEZ, + Opcode.INVOKE_VIRTUAL, + ), + customFingerprint = { method, _ -> + method.definingClass == "Landroid/support/v7/widget/LinearLayoutManager;" + } +) + +internal val relatedChipCloudFingerprint = legacyFingerprint( + name = "relatedChipCloudFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + opcodes = listOf( + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT + ), + literals = listOf(relatedChipCloudMargin), +) + +internal val searchResultsChipBarFingerprint = legacyFingerprint( + name = "searchResultsChipBarFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + opcodes = listOf( + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT + ), + literals = listOf(barContainerHeight), +) + +internal val showMoreButtonFingerprint = legacyFingerprint( + name = "showMoreButtonFingerprint", + opcodes = listOf( + Opcode.CONST, + Opcode.CONST_4, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT + ), + literals = listOf(expandButtonDown), +) + + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/feed/flyoutmenu/FeedFlyoutMenuPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/flyoutmenu/FeedFlyoutMenuPatch.kt new file mode 100644 index 000000000..4da47d69b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/flyoutmenu/FeedFlyoutMenuPatch.kt @@ -0,0 +1,86 @@ +package app.revanced.patches.youtube.feed.flyoutmenu + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.bottomSheetMenuItemBuilderFingerprint +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.FEED_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.indexOfSpannedCharSequenceInstruction +import app.revanced.patches.youtube.utils.patch.PatchList.HIDE_FEED_FLYOUT_MENU +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +@Suppress("unused") +val feedFlyoutMenuPatch = bytecodePatch( + HIDE_FEED_FLYOUT_MENU.title, + HIDE_FEED_FLYOUT_MENU.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + sharedResourceIdPatch, + settingsPatch, + ) + execute { + + // region patch for phone + + bottomSheetMenuItemBuilderFingerprint.methodOrThrow().apply { + val insertIndex = indexOfSpannedCharSequenceInstruction(this) + 2 + val insertRegister = getInstruction(insertIndex - 1).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $FEED_CLASS_DESCRIPTOR->hideFlyoutMenu(Ljava/lang/CharSequence;)Ljava/lang/CharSequence; + move-result-object v$insertRegister + """ + ) + } + + // endregion + + // region patch for tablet + + contextualMenuItemBuilderFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.startIndex + 1 + val targetInstruction = getInstruction(targetIndex) + + val targetReferenceName = + (targetInstruction.reference as MethodReference).name + if (targetReferenceName != "setText") + throw PatchException("Method name did not match: $targetReferenceName") + + addInstruction( + targetIndex + 1, + "invoke-static {v${targetInstruction.registerC}, v${targetInstruction.registerD}}, " + + "$FEED_CLASS_DESCRIPTOR->hideFlyoutMenu(Landroid/widget/TextView;Ljava/lang/CharSequence;)V" + ) + } + } + + // endregion + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: FEED", + "SETTINGS: HIDE_FEED_FLYOUT_MENU" + ), + HIDE_FEED_FLYOUT_MENU + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/feed/flyoutmenu/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/flyoutmenu/Fingerprints.kt new file mode 100644 index 000000000..13b95fe72 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/flyoutmenu/Fingerprints.kt @@ -0,0 +1,23 @@ +package app.revanced.patches.youtube.feed.flyoutmenu + +import app.revanced.patches.youtube.utils.resourceid.posterArtWidthDefault +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val contextualMenuItemBuilderFingerprint = legacyFingerprint( + name = "contextualMenuItemBuilderFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL or AccessFlags.SYNTHETIC, + parameters = listOf("L", "L"), + opcodes = listOf( + Opcode.CHECK_CAST, + Opcode.INVOKE_VIRTUAL, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.ADD_INT_2ADDR + ), + literals = listOf(posterArtWidthDefault), +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/audiotracks/AudioTracksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/audiotracks/AudioTracksPatch.kt new file mode 100644 index 000000000..e4fb18962 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/audiotracks/AudioTracksPatch.kt @@ -0,0 +1,76 @@ +package app.revanced.patches.youtube.general.audiotracks + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.patch.PatchList.DISABLE_AUTO_AUDIO_TRACKS +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +@Suppress("unused") +val audioTracksPatch = bytecodePatch( + DISABLE_AUTO_AUDIO_TRACKS.title, + DISABLE_AUTO_AUDIO_TRACKS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + execute { + + + streamingModelBuilderFingerprint.methodOrThrow().apply { + val formatStreamModelIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.CHECK_CAST + && (this as ReferenceInstruction).reference.toString() == "Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;" + } + val arrayListIndex = indexOfFirstInstructionOrThrow(formatStreamModelIndex) { + opcode == Opcode.INVOKE_INTERFACE && + getReference()?.toString() == "Ljava/util/List;->add(Ljava/lang/Object;)Z" + } + val insertIndex = indexOfFirstInstructionOrThrow(arrayListIndex) { + opcode == Opcode.INVOKE_INTERFACE && + getReference()?.toString() == "Ljava/util/List;->isEmpty()Z" + } + 2 + + val formatStreamModelRegister = + getInstruction(formatStreamModelIndex).registerA + val arrayListRegister = + getInstruction(arrayListIndex).registerC + + addInstructions( + insertIndex, """ + invoke-static {v$arrayListRegister}, $GENERAL_CLASS_DESCRIPTOR->getFormatStreamModelArray(Ljava/util/ArrayList;)Ljava/util/ArrayList; + move-result-object v$arrayListRegister + """ + ) + + addInstructions( + formatStreamModelIndex + 1, + "invoke-static {v$formatStreamModelRegister}, $GENERAL_CLASS_DESCRIPTOR->setFormatStreamModelArray(Ljava/lang/Object;)V" + ) + } + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "SETTINGS: DISABLE_AUTO_AUDIO_TRACKS" + ), + DISABLE_AUTO_AUDIO_TRACKS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/audiotracks/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/audiotracks/Fingerprints.kt new file mode 100644 index 000000000..1c9c13403 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/audiotracks/Fingerprints.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.youtube.general.audiotracks + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val streamingModelBuilderFingerprint = legacyFingerprint( + name = "streamingModelBuilderFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + strings = listOf("vprng") +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/autocaptions/AutoCaptionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/autocaptions/AutoCaptionsPatch.kt new file mode 100644 index 000000000..2beaf7ae2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/autocaptions/AutoCaptionsPatch.kt @@ -0,0 +1,37 @@ +package app.revanced.patches.youtube.general.autocaptions + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.captions.baseAutoCaptionsPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.DISABLE_AUTO_CAPTIONS +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch + +@Suppress("unused") +val autoCaptionsPatch = bytecodePatch( + DISABLE_AUTO_CAPTIONS.title, + DISABLE_AUTO_CAPTIONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseAutoCaptionsPatch, + settingsPatch, + ) + + execute { + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "SETTINGS: DISABLE_AUTO_CAPTIONS" + ), + DISABLE_AUTO_CAPTIONS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/Fingerprints.kt new file mode 100644 index 000000000..e37d96d1d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/Fingerprints.kt @@ -0,0 +1,136 @@ +package app.revanced.patches.youtube.general.components + +import app.revanced.patches.youtube.utils.resourceid.accountSwitcherAccessibility +import app.revanced.patches.youtube.utils.resourceid.compactLink +import app.revanced.patches.youtube.utils.resourceid.compactListItem +import app.revanced.patches.youtube.utils.resourceid.editSettingsAction +import app.revanced.patches.youtube.utils.resourceid.fab +import app.revanced.patches.youtube.utils.resourceid.toolTipContentView +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionReversed +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val accountListFingerprint = legacyFingerprint( + name = "accountListFingerprint", + returnType = "V", + accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL or AccessFlags.SYNTHETIC, + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.IGET + ) +) + +internal val accountListParentFingerprint = legacyFingerprint( + name = "accountListParentFingerprint", + literals = listOf(compactListItem), +) + +internal val accountMenuFingerprint = legacyFingerprint( + name = "accountMenuFingerprint", + returnType = "V", + opcodes = listOf( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.IGET, + Opcode.AND_INT_LIT16 + ) +) + +internal val accountMenuParentFingerprint = legacyFingerprint( + name = "accountMenuParentFingerprint", + opcodes = listOf( + Opcode.CONST, + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT + ), + literals = listOf(compactLink), +) + +internal val accountSwitcherAccessibilityLabelFingerprint = legacyFingerprint( + name = "accountSwitcherAccessibilityLabelFingerprint", + returnType = "V", + parameters = listOf("L", "Ljava/lang/Object;"), + literals = listOf(accountSwitcherAccessibility), +) + +internal val appBlockingCheckResultToStringFingerprint = legacyFingerprint( + name = "appBlockingCheckResultToStringFingerprint", + returnType = "Ljava/lang/String;", + strings = listOf("AppBlockingCheckResult{intent=") +) + +internal val bottomUiContainerFingerprint = legacyFingerprint( + name = "bottomUiContainerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "L"), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/BottomUiContainer;") + } +) + +internal val floatingMicrophoneFingerprint = legacyFingerprint( + name = "floatingMicrophoneFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.IGET_BOOLEAN, + Opcode.IF_EQZ, + Opcode.RETURN_VOID + ), + literals = listOf(fab), +) + +internal val pipNotificationFingerprint = legacyFingerprint( + name = "pipNotificationFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + literals = listOf(editSettingsAction), +) + +internal val preferenceScreenFingerprint = legacyFingerprint( + name = "preferenceScreenFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + strings = listOf(":android:show_fragment_args"), + customFingerprint = { method, classDef -> + AccessFlags.SYNTHETIC.isSet(classDef.accessFlags) && + indexOfPreferenceScreenInstruction(method) >= 0 + } +) + +internal fun indexOfPreferenceScreenInstruction(method: Method) = + method.indexOfFirstInstructionReversed { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.returnType == "Landroidx/preference/PreferenceScreen;" && + reference.parameterTypes.isEmpty() + } + +internal val tooltipContentFullscreenFingerprint = legacyFingerprint( + name = "tooltipContentFullscreenFingerprint", + returnType = "V", + literals = listOf(45384061L), +) + +internal val tooltipContentViewFingerprint = legacyFingerprint( + name = "tooltipContentViewFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + literals = listOf(toolTipContentView), +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/LayoutComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/LayoutComponentsPatch.kt new file mode 100644 index 000000000..a80c7b44b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/LayoutComponentsPatch.kt @@ -0,0 +1,247 @@ +package app.revanced.patches.youtube.general.components + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.shared.settingmenu.settingsMenuPatch +import app.revanced.patches.shared.viewgroup.viewGroupMarginLayoutParamsHookPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.HIDE_LAYOUT_COMPONENTS +import app.revanced.patches.youtube.utils.resourceid.accountSwitcherAccessibility +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.util.MethodUtil + +private const val EXTENSION_SETTINGS_MENU_DESCRIPTOR = + "$GENERAL_PATH/SettingsMenuPatch;" +private const val CUSTOM_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/CustomFilter;" +private const val LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/LayoutComponentsFilter;" + +@Suppress("unused") +val layoutComponentsPatch = bytecodePatch( + HIDE_LAYOUT_COMPONENTS.title, + HIDE_LAYOUT_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + lithoFilterPatch, + sharedResourceIdPatch, + settingsMenuPatch, + viewGroupMarginLayoutParamsHookPatch, + ) + + execute { + + // region patch for disable pip notification + + pipNotificationFingerprint.matchOrThrow().let { + it.method.apply { + val checkCastCalls = implementation!!.instructions.withIndex() + .filter { instruction -> + (instruction.value as? ReferenceInstruction)?.reference.toString() == "Lcom/google/apps/tiktok/account/AccountId;" + } + + val checkCastCallSize = checkCastCalls.size + if (checkCastCallSize != 3) + throw PatchException("Couldn't find target index, size: $checkCastCallSize") + + arrayOf( + checkCastCalls.elementAt(1).index, + checkCastCalls.elementAt(0).index + ).forEach { index -> + addInstruction( + index + 1, + "return-void" + ) + } + } + } + + // endregion + + // region patch for disable update screen + + appBlockingCheckResultToStringFingerprint.mutableClassOrThrow().methods.first { method -> + MethodUtil.isConstructor(method) && + method.parameters == listOf("Landroid/content/Intent;", "Z") + }.addInstructions( + 1, + "const/4 p1, 0x0" + ) + + // endregion + + // region patch for hide account menu + + // for you tab + accountListFingerprint.matchOrThrow(accountListParentFingerprint).let { + it.method.apply { + val targetIndex = it.patternMatch!!.startIndex + 3 + val targetInstruction = getInstruction(targetIndex) + + addInstruction( + targetIndex, + "invoke-static {v${targetInstruction.registerC}, v${targetInstruction.registerD}}, " + + "$GENERAL_CLASS_DESCRIPTOR->hideAccountList(Landroid/view/View;Ljava/lang/CharSequence;)V" + ) + } + } + + // for tablet and old clients + accountMenuFingerprint.matchOrThrow(accountMenuParentFingerprint).let { + it.method.apply { + val targetIndex = it.patternMatch!!.startIndex + 2 + val targetInstruction = getInstruction(targetIndex) + + addInstruction( + targetIndex, + "invoke-static {v${targetInstruction.registerC}, v${targetInstruction.registerD}}, " + + "$GENERAL_CLASS_DESCRIPTOR->hideAccountMenu(Landroid/view/View;Ljava/lang/CharSequence;)V" + ) + } + } + + // endregion + + // region patch for hide floating microphone + + floatingMicrophoneFingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.patternMatch!!.startIndex + val register = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex + 1, """ + invoke-static {v$register}, $GENERAL_CLASS_DESCRIPTOR->hideFloatingMicrophone(Z)Z + move-result v$register + """ + ) + } + } + + // endregion + + // region patch for hide handle + + accountSwitcherAccessibilityLabelFingerprint.methodOrThrow().apply { + val constIndex = + indexOfFirstLiteralInstructionOrThrow(accountSwitcherAccessibility) + val insertIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.IF_EQZ) + val setVisibilityIndex = indexOfFirstInstructionOrThrow(insertIndex) { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setVisibility" + } + val visibilityRegister = + getInstruction(setVisibilityIndex).registerD + + addInstructions( + insertIndex, """ + invoke-static {v$visibilityRegister}, $GENERAL_CLASS_DESCRIPTOR->hideHandle(I)I + move-result v$visibilityRegister + """ + ) + } + + // endregion + + // region patch for hide setting menus + + preferenceScreenFingerprint.methodOrThrow().apply { + val targetIndex = indexOfPreferenceScreenInstruction(this) + val targetRegister = getInstruction(targetIndex).registerC + val targetReference = getInstruction(targetIndex).reference + + val insertIndex = implementation!!.instructions.lastIndex + + addInstructions( + insertIndex + 1, """ + invoke-virtual {v$targetRegister}, $targetReference + move-result-object v$targetRegister + invoke-static {v$targetRegister}, $EXTENSION_SETTINGS_MENU_DESCRIPTOR->hideSettingsMenu(Landroidx/preference/PreferenceScreen;)V + return-void + """ + ) + removeInstruction(insertIndex) + } + + // endregion + + // region patch for hide snack bar + + bottomUiContainerFingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $GENERAL_CLASS_DESCRIPTOR->hideSnackBar()Z + move-result v0 + if-eqz v0, :show + return-void + """, ExternalLabel("show", getInstruction(0)) + ) + } + + // endregion + + // region patch for hide tooltip content + + tooltipContentFullscreenFingerprint.methodOrThrow().apply { + val literalIndex = indexOfFirstLiteralInstructionOrThrow(45384061L) + val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT) + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "const/4 v$targetRegister, 0x0" + ) + } + + tooltipContentViewFingerprint.methodOrThrow().addInstruction( + 0, + "return-void" + ) + + // endregion + + addLithoFilter(CUSTOM_FILTER_CLASS_DESCRIPTOR) + addLithoFilter(LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR) + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "SETTINGS: HIDE_LAYOUT_COMPONENTS" + ), + HIDE_LAYOUT_COMPONENTS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/dialog/ViewerDiscretionDialogPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/dialog/ViewerDiscretionDialogPatch.kt new file mode 100644 index 000000000..bbeebc0b3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/dialog/ViewerDiscretionDialogPatch.kt @@ -0,0 +1,41 @@ +package app.revanced.patches.youtube.general.dialog + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.dialog.baseViewerDiscretionDialogPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.patch.PatchList.REMOVE_VIEWER_DISCRETION_DIALOG +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch + +@Suppress("unused") +val viewerDiscretionDialogPatch = bytecodePatch( + REMOVE_VIEWER_DISCRETION_DIALOG.title, + REMOVE_VIEWER_DISCRETION_DIALOG.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseViewerDiscretionDialogPatch( + GENERAL_CLASS_DESCRIPTOR, + true + ), + settingsPatch, + ) + + execute { + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "SETTINGS: REMOVE_VIEWER_DISCRETION_DIALOG" + ), + REMOVE_VIEWER_DISCRETION_DIALOG + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/downloads/DownloadActionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/downloads/DownloadActionsPatch.kt new file mode 100644 index 000000000..d6f4d54b4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/downloads/DownloadActionsPatch.kt @@ -0,0 +1,178 @@ +package app.revanced.patches.youtube.general.downloads + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.HOOK_DOWNLOAD_ACTIONS +import app.revanced.patches.youtube.utils.pip.pipStateHookPatch +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$GENERAL_PATH/DownloadActionsPatch;" + +private const val OFFLINE_PLAYLIST_ENDPOINT_OUTER_CLASS_DESCRIPTOR = + "Lcom/google/protos/youtube/api/innertube/OfflinePlaylistEndpointOuterClass${'$'}OfflinePlaylistEndpoint;" + +@Suppress("unused") +val downloadActionsPatch = bytecodePatch( + HOOK_DOWNLOAD_ACTIONS.title, + HOOK_DOWNLOAD_ACTIONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + pipStateHookPatch, + sharedResourceIdPatch, + settingsPatch, + ) + + execute { + + // region patch for hook download actions (video action bar and flyout panel) + + offlineVideoEndpointFingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, """ + invoke-static/range {p3 .. p3}, $EXTENSION_CLASS_DESCRIPTOR->inAppVideoDownloadButtonOnClick(Ljava/lang/String;)Z + move-result v0 + if-eqz v0, :show_native_downloader + return-void + """, ExternalLabel("show_native_downloader", getInstruction(0)) + ) + } + + // endregion + + // region patch for hook download actions (playlist) + + val onClickListenerClass = + downloadPlaylistButtonOnClickFingerprint.methodOrThrow().let { + val playlistDownloadActionInvokeIndex = + indexOfPlaylistDownloadActionInvokeInstruction(it) + + it.instructions.subList( + playlistDownloadActionInvokeIndex - 10, + playlistDownloadActionInvokeIndex, + ).find { instruction -> + instruction.opcode == Opcode.INVOKE_VIRTUAL_RANGE + && instruction.getReference()?.parameterTypes?.first() == "Ljava/lang/String;" + }?.getReference()?.returnType + ?: throw PatchException("Could not find onClickListenerClass") + } + + findMethodOrThrow(onClickListenerClass) { + name == "onClick" + }.apply { + val insertIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_STATIC && + getReference()?.name == "isEmpty" + } + val insertRegister = getInstruction(insertIndex).registerC + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $EXTENSION_CLASS_DESCRIPTOR->inAppPlaylistDownloadButtonOnClick(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$insertRegister + """ + ) + } + + offlinePlaylistEndpointFingerprint.methodOrThrow().apply { + val playlistIdParameter = parameterTypes.indexOf("Ljava/lang/String;") + 1 + if (playlistIdParameter > 0) { + addInstructionsWithLabels( + 0, """ + invoke-static {p$playlistIdParameter}, $EXTENSION_CLASS_DESCRIPTOR->inAppPlaylistDownloadMenuOnClick(Ljava/lang/String;)Z + move-result v0 + if-eqz v0, :show_native_downloader + return-void + """, ExternalLabel("show_native_downloader", getInstruction(0)) + ) + } else { + val freeRegister = implementation!!.registerCount - parameters.size - 2 + + val playlistIdIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + opcode == Opcode.IGET_OBJECT && + reference?.definingClass == OFFLINE_PLAYLIST_ENDPOINT_OUTER_CLASS_DESCRIPTOR && + reference.type == "Ljava/lang/String;" + } + val playlistIdReference = + getInstruction(playlistIdIndex).reference + + val targetIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.CHECK_CAST && + (this as? ReferenceInstruction)?.reference?.toString() == OFFLINE_PLAYLIST_ENDPOINT_OUTER_CLASS_DESCRIPTOR + } + val targetRegister = getInstruction(targetIndex).registerA + + addInstructionsWithLabels( + targetIndex + 1, + """ + iget-object v$freeRegister, v$targetRegister, $playlistIdReference + invoke-static {v$freeRegister}, $EXTENSION_CLASS_DESCRIPTOR->inAppPlaylistDownloadMenuOnClick(Ljava/lang/String;)Z + move-result v$freeRegister + if-eqz v$freeRegister, :show_native_downloader + return-void + """, + ExternalLabel("show_native_downloader", getInstruction(targetIndex + 1)) + ) + } + } + + // endregion + + // region patch for show the playlist download button + + setPlaylistDownloadButtonVisibilityFingerprint + .matchOrThrow(accessibilityOfflineButtonSyncFingerprint).let { + it.method.apply { + val insertIndex = it.patternMatch!!.startIndex + 2 + val insertRegister = + getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->overridePlaylistDownloadButtonVisibility()Z + move-result v$insertRegister + """ + ) + } + } + + // endregion + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "SETTINGS: HOOK_BUTTONS", + "SETTINGS: HOOK_DOWNLOAD_ACTIONS" + ), + HOOK_DOWNLOAD_ACTIONS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/downloads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/downloads/Fingerprints.kt new file mode 100644 index 000000000..3c39b5432 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/downloads/Fingerprints.kt @@ -0,0 +1,90 @@ +package app.revanced.patches.youtube.general.downloads + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import app.revanced.util.parametersEqual +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.util.MethodUtil + +private val ENDS_WITH_PARAMETER_LIST = listOf( + "Lcom/google/android/apps/youtube/app/offline/ui/OfflineArrowView;", + "I", + "Landroid/view/View${'$'}OnClickListener;" +) + +internal val accessibilityOfflineButtonSyncFingerprint = legacyFingerprint( + name = "accessibilityOfflineButtonSyncFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + returnType = "V", + customFingerprint = custom@{ method, _ -> + if (!MethodUtil.isConstructor(method)) { + return@custom false + } + val parameterTypes = method.parameterTypes + val parameterSize = parameterTypes.size + if (parameterSize < 6) { + return@custom false + } + + val endsWithMethodParameterList = parameterTypes.slice(parameterSize - 3.. + indexOfPlaylistDownloadActionInvokeInstruction(method) >= 0 + } +) + +internal fun indexOfPlaylistDownloadActionInvokeInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.parameterTypes == + listOf( + "Ljava/lang/String;", + "Lcom/google/android/apps/youtube/app/offline/ui/OfflineArrowView;", + "I", + "Landroid/view/View${'$'}OnClickListener;" + ) + } + +internal val offlinePlaylistEndpointFingerprint = legacyFingerprint( + name = "offlinePlaylistEndpointFingerprint", + returnType = "V", + strings = listOf("Object is not an offlineable playlist: ") +) + +internal val offlineVideoEndpointFingerprint = legacyFingerprint( + name = "offlineVideoEndpointFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "V", + parameters = listOf( + "Ljava/util/Map;", + "L", + "Ljava/lang/String", // VideoId + "L" + ), + strings = listOf("Object is not an offlineable video: ") +) + +internal val setPlaylistDownloadButtonVisibilityFingerprint = legacyFingerprint( + name = "setPlaylistDownloadButtonVisibilityFingerprint", + returnType = "V", + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_NEZ, + Opcode.IGET, + Opcode.CONST_4 + ) +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/Fingerprints.kt new file mode 100644 index 000000000..f5d8e5b4b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/Fingerprints.kt @@ -0,0 +1,46 @@ +package app.revanced.patches.youtube.general.layoutswitch + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val formFactorEnumConstructorFingerprint = legacyFingerprint( + name = "formFactorEnumConstructorFingerprint", + returnType = "V", + strings = listOf( + "UNKNOWN_FORM_FACTOR", + "SMALL_FORM_FACTOR", + "LARGE_FORM_FACTOR" + ) +) + +internal val layoutSwitchFingerprint = legacyFingerprint( + name = "layoutSwitchFingerprint", + returnType = "I", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("L"), + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.IF_NEZ, + Opcode.CONST_4, + Opcode.RETURN, + Opcode.CONST_16, + Opcode.IF_GE, + Opcode.CONST_4, + Opcode.RETURN, + Opcode.CONST_16, + Opcode.IF_GE, + Opcode.CONST_4, + Opcode.RETURN, + Opcode.CONST_16, + Opcode.IF_GE, + Opcode.CONST_4, + Opcode.RETURN, + Opcode.CONST_4, + Opcode.RETURN + ) +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/LayoutSwitchPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/LayoutSwitchPatch.kt new file mode 100644 index 000000000..ec4891efa --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/LayoutSwitchPatch.kt @@ -0,0 +1,82 @@ +package app.revanced.patches.youtube.general.layoutswitch + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.createPlayerRequestBodyWithModelFingerprint +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.LAYOUT_SWITCH +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.definingClassOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$GENERAL_PATH/LayoutSwitchPatch;" + +@Suppress("unused") +val layoutSwitchPatch = bytecodePatch( + LAYOUT_SWITCH.title, + LAYOUT_SWITCH.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + + val formFactorEnumClass = formFactorEnumConstructorFingerprint + .definingClassOrThrow() + + createPlayerRequestBodyWithModelFingerprint.methodOrThrow().apply { + val index = indexOfFirstInstructionOrThrow { + val reference = getReference() + opcode == Opcode.IGET && + reference?.definingClass == formFactorEnumClass && + reference.type == "I" + } + val register = getInstruction(index).registerA + + addInstructions( + index + 1, """ + invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->getFormFactor(I)I + move-result v$register + """ + ) + } + + layoutSwitchFingerprint.methodOrThrow().apply { + val index = indexOfFirstInstructionReversedOrThrow(Opcode.IF_NEZ) + val register = getInstruction(index).registerA + + addInstructions( + index, """ + invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->getWidthDp(I)I + move-result v$register + """ + ) + } + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "PREFERENCE_CATEGORY: GENERAL_EXPERIMENTAL_FLAGS", + "SETTINGS: LAYOUT_SWITCH" + ), + LAYOUT_SWITCH + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/loadingscreen/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/loadingscreen/Fingerprints.kt new file mode 100644 index 000000000..751583065 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/loadingscreen/Fingerprints.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.youtube.general.loadingscreen + +import app.revanced.util.fingerprint.legacyFingerprint + +internal const val GRADIENT_LOADING_SCREEN_AB_CONSTANT = 45412406L + +internal val useGradientLoadingScreenFingerprint = legacyFingerprint( + name = "gradientLoadingScreenPrimaryFingerprint", + literals = listOf(GRADIENT_LOADING_SCREEN_AB_CONSTANT), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/loadingscreen/GradientLoadingScreenPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/loadingscreen/GradientLoadingScreenPatch.kt new file mode 100644 index 000000000..8f8206de8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/loadingscreen/GradientLoadingScreenPatch.kt @@ -0,0 +1,40 @@ +package app.revanced.patches.youtube.general.loadingscreen + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.patch.PatchList.ENABLE_GRADIENT_LOADING_SCREEN +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall + +@Suppress("unused") +val gradientLoadingScreenPatch = bytecodePatch( + ENABLE_GRADIENT_LOADING_SCREEN.title, + ENABLE_GRADIENT_LOADING_SCREEN.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + + useGradientLoadingScreenFingerprint.injectLiteralInstructionBooleanCall( + GRADIENT_LOADING_SCREEN_AB_CONSTANT, + "$GENERAL_CLASS_DESCRIPTOR->enableGradientLoadingScreen()Z" + ) + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "SETTINGS: ENABLE_GRADIENT_LOADING_SCREEN" + ), + ENABLE_GRADIENT_LOADING_SCREEN + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/miniplayer/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/miniplayer/Fingerprints.kt new file mode 100644 index 000000000..304c4d0f9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/miniplayer/Fingerprints.kt @@ -0,0 +1,178 @@ +@file:Suppress("SpellCheckingInspection") + +package app.revanced.patches.youtube.general.miniplayer + +import app.revanced.patches.youtube.utils.resourceid.floatyBarTopMargin +import app.revanced.patches.youtube.utils.resourceid.miniplayerMaxSize +import app.revanced.patches.youtube.utils.resourceid.modernMiniPlayerClose +import app.revanced.patches.youtube.utils.resourceid.modernMiniPlayerExpand +import app.revanced.patches.youtube.utils.resourceid.modernMiniPlayerForwardButton +import app.revanced.patches.youtube.utils.resourceid.modernMiniPlayerRewindButton +import app.revanced.patches.youtube.utils.resourceid.scrimOverlay +import app.revanced.patches.youtube.utils.resourceid.ytOutlinePictureInPictureWhite +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val miniplayerDimensionsCalculatorParentFingerprint = legacyFingerprint( + name = "miniplayerDimensionsCalculatorParentFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + literals = listOf(floatyBarTopMargin), +) + +/** + * Matches using the class found in [miniplayerModernViewParentFingerprint]. + */ +internal val miniplayerModernAddViewListenerFingerprint = legacyFingerprint( + name = "miniplayerModernAddViewListenerFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "V", + parameters = listOf("Landroid/view/View;") +) + +/** + * Matches using the class found in [miniplayerModernViewParentFingerprint]. + */ +internal val miniplayerModernCloseButtonFingerprint = legacyFingerprint( + name = "miniplayerModernCloseButtonFingerprint", + returnType = "Landroid/widget/ImageView;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(modernMiniPlayerClose), +) + +internal const val MINIPLAYER_MODERN_FEATURE_KEY = 45622882L + +// In later targets this feature flag does nothing and is dead code. +internal const val MINIPLAYER_MODERN_FEATURE_LEGACY_KEY = 45630429L +internal const val MINIPLAYER_DOUBLE_TAP_FEATURE_KEY = 45628823L +internal const val MINIPLAYER_DRAG_DROP_FEATURE_KEY = 45628752L +internal const val MINIPLAYER_HORIZONTAL_DRAG_FEATURE_KEY = 45658112L +internal const val MINIPLAYER_ROUNDED_CORNERS_FEATURE_KEY = 45652224L +internal const val MINIPLAYER_INITIAL_SIZE_FEATURE_KEY = 45640023L +internal const val MINIPLAYER_DISABLED_FEATURE_KEY = 45657015L + +internal val miniplayerModernConstructorFingerprint = legacyFingerprint( + name = "miniplayerModernConstructorFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + parameters = listOf("L"), + literals = listOf(45623000L), +) + +internal val miniplayerOnCloseHandlerFingerprint = legacyFingerprint( + name = "miniplayerOnCloseHandlerFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "Z", + literals = listOf(MINIPLAYER_DISABLED_FEATURE_KEY), +) + +/** + * Matches using the class found in [miniplayerModernViewParentFingerprint]. + */ +internal val miniplayerModernExpandButtonFingerprint = legacyFingerprint( + name = "miniplayerModernExpandButtonFingerprint", + returnType = "Landroid/widget/ImageView;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(modernMiniPlayerExpand), +) + +/** + * Matches using the class found in [miniplayerModernViewParentFingerprint]. + */ +internal val miniplayerModernExpandCloseDrawablesFingerprint = legacyFingerprint( + name = "miniplayerModernExpandCloseDrawablesFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + literals = listOf(ytOutlinePictureInPictureWhite), +) + +/** + * Matches using the class found in [miniplayerModernViewParentFingerprint]. + */ +internal val miniplayerModernForwardButtonFingerprint = legacyFingerprint( + name = "miniplayerModernForwardButtonFingerprint", + returnType = "Landroid/widget/ImageView;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(modernMiniPlayerForwardButton), +) + +/** + * Matches using the class found in [miniplayerModernViewParentFingerprint]. + */ +internal val miniplayerModernOverlayViewFingerprint = legacyFingerprint( + name = "miniplayerModernOverlayViewFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(scrimOverlay), +) + +/** + * Matches using the class found in [miniplayerModernViewParentFingerprint]. + */ +internal val miniplayerModernRewindButtonFingerprint = legacyFingerprint( + name = "miniplayerModernRewindButtonFingerprint", + returnType = "Landroid/widget/ImageView;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(modernMiniPlayerRewindButton), +) + +internal val miniplayerModernViewParentFingerprint = legacyFingerprint( + name = "miniplayerModernViewParentFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "Ljava/lang/String;", + parameters = listOf(), + strings = listOf("player_overlay_modern_mini_player_controls") +) + +internal val miniplayerMinimumSizeFingerprint = legacyFingerprint( + name = "miniplayerMinimumSizeFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf(192L, 128L, miniplayerMaxSize), +) + +internal val miniplayerOverrideFingerprint = legacyFingerprint( + name = "miniplayerOverrideFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + strings = listOf("appName") +) + +internal val miniplayerOverrideNoContextFingerprint = legacyFingerprint( + name = "miniplayerOverrideNoContextFingerprint", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + returnType = "Z", + opcodes = listOf(Opcode.IGET_BOOLEAN), // anchor to insert the instruction +) + +internal val miniplayerResponseModelSizeCheckFingerprint = legacyFingerprint( + name = "miniplayerResponseModelSizeCheckFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "L", + parameters = listOf("Ljava/lang/Object;", "Ljava/lang/Object;"), + opcodes = listOf( + Opcode.RETURN_OBJECT, + Opcode.CHECK_CAST, + Opcode.CHECK_CAST, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.IF_NEZ, + ) +) + +internal const val YOUTUBE_PLAYER_OVERLAYS_LAYOUT_CLASS_NAME = + "Lcom/google/android/apps/youtube/app/common/player/overlay/YouTubePlayerOverlaysLayout;" + +internal val youTubePlayerOverlaysLayoutFingerprint = legacyFingerprint( + name = "youTubePlayerOverlaysLayoutFingerprint", + customFingerprint = { _, classDef -> + classDef.type == YOUTUBE_PLAYER_OVERLAYS_LAYOUT_CLASS_NAME + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/miniplayer/MiniplayerPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/miniplayer/MiniplayerPatch.kt new file mode 100644 index 000000000..cfe0b936c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/miniplayer/MiniplayerPatch.kt @@ -0,0 +1,424 @@ +package app.revanced.patches.youtube.general.miniplayer + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.MINIPLAYER +import app.revanced.patches.youtube.utils.playservice.is_19_15_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_23_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_25_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_26_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_29_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_36_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_43_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.resourceid.modernMiniPlayerClose +import app.revanced.patches.youtube.utils.resourceid.modernMiniPlayerExpand +import app.revanced.patches.youtube.utils.resourceid.modernMiniPlayerForwardButton +import app.revanced.patches.youtube.utils.resourceid.modernMiniPlayerRewindButton +import app.revanced.patches.youtube.utils.resourceid.scrimOverlay +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.resourceid.ytOutlinePictureInPictureWhite +import app.revanced.patches.youtube.utils.resourceid.ytOutlineXWhite +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.addInstructionsAtControlFlowLabel +import app.revanced.util.findInstructionIndicesReversedOrThrow +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$GENERAL_PATH/MiniplayerPatch;" + +// YT uses "Miniplayer" without a space between 'mini' and 'player: https://support.google.com/youtube/answer/9162927. +@Suppress("unused", "SpellCheckingInspection") +val miniplayerPatch = bytecodePatch( + MINIPLAYER.title, + MINIPLAYER.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + sharedResourceIdPatch, + settingsPatch, + versionCheckPatch, + ) + + execute { + + var settingArray = arrayOf( + "PREFERENCE_SCREEN: GENERAL" + ) + + fun Method.findReturnIndicesReversed() = + findInstructionIndicesReversedOrThrow(Opcode.RETURN) + + fun MutableMethod.insertBooleanOverride(index: Int, methodName: String) { + val register = getInstruction(index).registerA + addInstructions( + index, + """ + invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->$methodName(Z)Z + move-result v$register + """ + ) + } + + /** + * Adds an override to force legacy tablet miniplayer to be used or not used. + */ + fun MutableMethod.insertLegacyTabletMiniplayerOverride(index: Int) { + insertBooleanOverride(index, "getLegacyTabletMiniplayerOverride") + } + + /** + * Adds an override to force modern miniplayer to be used or not used. + */ + fun MutableMethod.insertModernMiniplayerOverride(index: Int) { + insertBooleanOverride(index, "getModernMiniplayerOverride") + } + + /** + * Adds an override to specify which modern miniplayer is used. + */ + fun MutableMethod.insertModernMiniplayerTypeOverride(iPutIndex: Int) { + val register = getInstruction(iPutIndex).registerA + + addInstructionsAtControlFlowLabel( + iPutIndex, + """ + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getModernMiniplayerOverrideType(I)I + move-result v$register + """, + ) + } + + fun MutableMethod.hookInflatedView( + literalValue: Long, + hookedClassType: String, + extensionMethodName: String, + ) { + val imageViewIndex = indexOfFirstInstructionOrThrow( + indexOfFirstLiteralInstructionOrThrow(literalValue) + ) { + opcode == Opcode.CHECK_CAST && + getReference()?.type == hookedClassType + } + + val register = getInstruction(imageViewIndex).registerA + addInstruction( + imageViewIndex + 1, + "invoke-static { v$register }, $extensionMethodName" + ) + } + + // Modern mini player is only present and functional in 19.15+. + // Resource is not present in older versions. Using it to determine, if patching an old version. + val isPatchingOldVersion = !is_19_15_or_greater + + // From 19.15 to 19.16 using mixed up drawables for tablet modern. + val shouldFixMixedUpDrawables = ytOutlineXWhite > 0 && ytOutlinePictureInPictureWhite > 0 + + // region Enable tablet miniplayer. + + miniplayerOverrideNoContextFingerprint.methodOrThrow( + miniplayerDimensionsCalculatorParentFingerprint + ).apply { + findReturnIndicesReversed().forEach { index -> + insertLegacyTabletMiniplayerOverride( + index + ) + } + } + + // endregion + + // region Legacy tablet Miniplayer hooks. + + miniplayerOverrideFingerprint.matchOrThrow().let { + val appNameStringIndex = it.stringMatches!!.first().index + 2 + + it.method.apply { + val walkerMethod = getWalkerMethod(appNameStringIndex) + + walkerMethod.apply { + findReturnIndicesReversed().forEach { index -> + insertLegacyTabletMiniplayerOverride( + index + ) + } + } + } + } + + miniplayerResponseModelSizeCheckFingerprint.matchOrThrow().let { + it.method.insertLegacyTabletMiniplayerOverride(it.patternMatch!!.endIndex) + } + + if (isPatchingOldVersion) { + settingArray += "SETTINGS: MINIPLAYER_TYPE_19_14" + addPreference(settingArray, MINIPLAYER) + + // Return here, as patch below is only intended for new versions of the app. + return@execute + } + + // endregion + + // region Enable modern miniplayer. + + miniplayerModernConstructorFingerprint.mutableClassOrThrow().methods.forEach { + it.apply { + if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) { + val iPutIndex = indexOfFirstInstructionOrThrow { + this.opcode == Opcode.IPUT && + this.getReference()?.type == "I" + } + + insertModernMiniplayerTypeOverride(iPutIndex) + } else { + findReturnIndicesReversed().forEach { index -> + insertModernMiniplayerOverride( + index + ) + } + } + } + } + + if (is_19_23_or_greater) { + miniplayerModernConstructorFingerprint.injectLiteralInstructionBooleanCall( + MINIPLAYER_DRAG_DROP_FEATURE_KEY, + "$EXTENSION_CLASS_DESCRIPTOR->enableMiniplayerDragAndDrop(Z)Z" + ) + settingArray += "SETTINGS: MINIPLAYER_DRAG_AND_DROP" + } + + if (is_19_25_or_greater) { + miniplayerModernConstructorFingerprint.injectLiteralInstructionBooleanCall( + MINIPLAYER_MODERN_FEATURE_LEGACY_KEY, + "$EXTENSION_CLASS_DESCRIPTOR->getModernMiniplayerOverride(Z)Z" + ) + + miniplayerModernConstructorFingerprint.injectLiteralInstructionBooleanCall( + MINIPLAYER_MODERN_FEATURE_KEY, + "$EXTENSION_CLASS_DESCRIPTOR->getModernFeatureFlagsActiveOverride(Z)Z" + ) + + miniplayerModernConstructorFingerprint.injectLiteralInstructionBooleanCall( + MINIPLAYER_DOUBLE_TAP_FEATURE_KEY, + "$EXTENSION_CLASS_DESCRIPTOR->enableMiniplayerDoubleTapAction(Z)Z" + ) + + if (!is_19_29_or_greater) { + settingArray += "SETTINGS: MINIPLAYER_DOUBLE_TAP_ACTION" + } + } + + if (is_19_26_or_greater) { + miniplayerModernConstructorFingerprint.methodOrThrow().apply { + val literalIndex = indexOfFirstLiteralInstructionOrThrow( + MINIPLAYER_INITIAL_SIZE_FEATURE_KEY, + ) + val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.LONG_TO_INT) + + val register = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, + """ + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->setMiniplayerDefaultSize(I)I + move-result v$register + """, + ) + } + + // Override a minimum size constant. + miniplayerMinimumSizeFingerprint.methodOrThrow().apply { + val index = indexOfFirstInstructionOrThrow { + opcode == Opcode.CONST_16 && + (this as NarrowLiteralInstruction).narrowLiteral == 192 + } + val register = getInstruction(index).registerA + + // Smaller sizes can be used, but the miniplayer will always start in size 170 if set any smaller. + // The 170 initial limit probably could be patched to allow even smaller initial sizes, + // but 170 is already half the horizontal space and smaller does not seem useful. + replaceInstruction(index, "const/16 v$register, 170") + } + + settingArray += "SETTINGS: MINIPLAYER_WIDTH_DIP" + } else { + settingArray += "SETTINGS: MINIPLAYER_REWIND_FORWARD" + } + + if (is_19_36_or_greater) { + miniplayerModernConstructorFingerprint.injectLiteralInstructionBooleanCall( + MINIPLAYER_ROUNDED_CORNERS_FEATURE_KEY, + "$EXTENSION_CLASS_DESCRIPTOR->setRoundedCorners(Z)Z" + ) + + settingArray += "SETTINGS: MINIPLAYER_ROUNDED_CONERS" + } + + if (is_19_43_or_greater) { + miniplayerOnCloseHandlerFingerprint.injectLiteralInstructionBooleanCall( + MINIPLAYER_DISABLED_FEATURE_KEY, + "$EXTENSION_CLASS_DESCRIPTOR->getMiniplayerOnCloseHandler(Z)Z" + ) + + miniplayerModernConstructorFingerprint.injectLiteralInstructionBooleanCall( + MINIPLAYER_HORIZONTAL_DRAG_FEATURE_KEY, + "$EXTENSION_CLASS_DESCRIPTOR->setHorizontalDrag(Z)Z" + ) + + settingArray += "SETTINGS: MINIPLAYER_HORIZONTAL_DRAG" + settingArray += "SETTINGS: MINIPLAYER_TYPE_19_43" + } else { + settingArray += "SETTINGS: MINIPLAYER_TYPE_19_16" + } + + // endregion + + // region Fix 19.16 using mixed up drawables for tablet modern. + // YT fixed this mistake in 19.17. + // Fix this, by swapping the drawable resource values with each other. + if (shouldFixMixedUpDrawables) { + miniplayerModernExpandCloseDrawablesFingerprint.methodOrThrow( + miniplayerModernViewParentFingerprint + ).apply { + listOf( + ytOutlinePictureInPictureWhite to ytOutlineXWhite, + ytOutlineXWhite to ytOutlinePictureInPictureWhite, + ).forEach { (originalResource, replacementResource) -> + val imageResourceIndex = + indexOfFirstLiteralInstructionOrThrow(originalResource) + val register = + getInstruction(imageResourceIndex).registerA + + replaceInstruction(imageResourceIndex, "const v$register, $replacementResource") + } + } + } + + // endregion + + // region Add hooks to hide tablet modern miniplayer buttons. + + listOf( + Triple( + miniplayerModernExpandButtonFingerprint, + modernMiniPlayerExpand, + "hideMiniplayerExpandClose" + ), + Triple( + miniplayerModernCloseButtonFingerprint, + modernMiniPlayerClose, + "hideMiniplayerExpandClose" + ), + Triple( + miniplayerModernRewindButtonFingerprint, + modernMiniPlayerRewindButton, + "hideMiniplayerRewindForward" + ), + Triple( + miniplayerModernForwardButtonFingerprint, + modernMiniPlayerForwardButton, + "hideMiniplayerRewindForward" + ), + Triple( + miniplayerModernOverlayViewFingerprint, + scrimOverlay, + "adjustMiniplayerOpacity" + ) + ).forEach { (fingerprint, literalValue, methodName) -> + fingerprint.methodOrThrow(miniplayerModernViewParentFingerprint).hookInflatedView( + literalValue, + "Landroid/widget/ImageView;", + "$EXTENSION_CLASS_DESCRIPTOR->$methodName(Landroid/widget/ImageView;)V" + ) + } + + miniplayerModernAddViewListenerFingerprint.methodOrThrow( + miniplayerModernViewParentFingerprint + ).addInstruction( + 0, + "invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->" + + "hideMiniplayerSubTexts(Landroid/view/View;)V", + ) + + // Modern 2 has a broken overlay subtitle view that is always present. + // Modern 2 uses the same overlay controls as the regular video player, + // and the overlay views are added at runtime. + // Add a hook to the overlay class, and pass the added views to extension. + // + // NOTE: Modern 2 uses the same video UI as the regular player except resized to smaller. + // This patch code could be used to hide other player overlays that do not use Litho. + youTubePlayerOverlaysLayoutFingerprint.matchOrThrow().let { + it.method.apply { + it.classDef.methods.add( + ImmutableMethod( + YOUTUBE_PLAYER_OVERLAYS_LAYOUT_CLASS_NAME, + "addView", + listOf( + ImmutableMethodParameter("Landroid/view/View;", annotations, null), + ImmutableMethodParameter("I", annotations, null), + ImmutableMethodParameter( + "Landroid/view/ViewGroup\$LayoutParams;", + annotations, + null + ), + ), + "V", + AccessFlags.PUBLIC.value, + annotations, + null, + MutableMethodImplementation(4), + ).toMutable().apply { + addInstructions( + """ + invoke-super { p0, p1, p2, p3 }, Landroid/view/ViewGroup;->addView(Landroid/view/View;ILandroid/view/ViewGroup${'$'}LayoutParams;)V + invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->playerOverlayGroupCreated(Landroid/view/View;)V + return-void + """, + ) + } + ) + } + } + + // endregion + + settingArray += "SETTINGS: MINIPLAYER_TYPE_MODERN" + + // region add settings + + addPreference(settingArray, MINIPLAYER) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/music/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/music/Fingerprints.kt new file mode 100644 index 000000000..08009eeff --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/music/Fingerprints.kt @@ -0,0 +1,28 @@ +package app.revanced.patches.youtube.general.music + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +internal val appDeepLinkFingerprint = legacyFingerprint( + name = "appDeepLinkFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "Ljava/util/Map;"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.CONST_STRING, + ), + strings = listOf("android.intent.action.VIEW"), + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + getReference()?.name == "appDeepLinkEndpoint" + } >= 0 + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/music/YouTubeMusicActionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/music/YouTubeMusicActionsPatch.kt new file mode 100644 index 000000000..25666dd63 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/music/YouTubeMusicActionsPatch.kt @@ -0,0 +1,110 @@ +package app.revanced.patches.youtube.general.music + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH +import app.revanced.patches.youtube.utils.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.patch.PatchList.GMSCORE_SUPPORT +import app.revanced.patches.youtube.utils.patch.PatchList.HOOK_YOUTUBE_MUSIC_ACTIONS +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.ResourceUtils.getContext +import app.revanced.patches.youtube.utils.settings.ResourceUtils.youtubeMusicPackageName +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.addEntryValues +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$GENERAL_PATH/YouTubeMusicActionsPatch;" + +@Suppress("unused") +val youtubeMusicActionsPatch = bytecodePatch( + HOOK_YOUTUBE_MUSIC_ACTIONS.title, + HOOK_YOUTUBE_MUSIC_ACTIONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + + appDeepLinkFingerprint.matchOrThrow().let { + it.method.apply { + val packageNameIndex = it.patternMatch!!.startIndex + val packageNameField = + getInstruction(packageNameIndex).reference.toString() + + implementation!!.instructions + .withIndex() + .filter { (_, instruction) -> + instruction.opcode == Opcode.IGET_OBJECT && + instruction.getReference() + ?.toString() == packageNameField + } + .map { (index, _) -> index } + .reversed() + .forEach { index -> + val register = getInstruction(index).registerA + + addInstructions( + index + 1, """ + invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->overridePackageName(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$register + """ + ) + } + } + } + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "SETTINGS: HOOK_BUTTONS", + "SETTINGS: HOOK_YOUTUBE_MUSIC_ACTIONS" + ), + HOOK_YOUTUBE_MUSIC_ACTIONS + ) + + // endregion + + } + + finalize { + if (GMSCORE_SUPPORT.included == true) { + getContext().apply { + addEntryValues( + "revanced_third_party_youtube_music_label", + "RVX Music" + ) + addEntryValues( + "revanced_third_party_youtube_music_package_name", + youtubeMusicPackageName + ) + } + findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) { + name == "RVXMusicPackageName" + }.apply { + val replaceIndex = indexOfFirstInstructionOrThrow(Opcode.CONST_STRING) + val replaceRegister = + getInstruction(replaceIndex).registerA + + replaceInstruction( + replaceIndex, + "const-string v$replaceRegister, \"$youtubeMusicPackageName\"" + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/Fingerprints.kt new file mode 100644 index 000000000..c0d2f324e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/Fingerprints.kt @@ -0,0 +1,120 @@ +package app.revanced.patches.youtube.general.navigation + +import app.revanced.patches.youtube.utils.resourceid.ytFillBell +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal const val ANDROID_AUTOMOTIVE_STRING = "Android Automotive" +internal const val TAB_ACTIVITY_CAIRO_STRING = "TAB_ACTIVITY_CAIRO" + +internal val autoMotiveFingerprint = legacyFingerprint( + name = "autoMotiveFingerprint", + opcodes = listOf( + Opcode.GOTO, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ + ), + strings = listOf(ANDROID_AUTOMOTIVE_STRING) +) + +internal val imageEnumConstructorFingerprint = legacyFingerprint( + name = "imageEnumConstructorFingerprint", + returnType = "V", + strings = listOf(TAB_ACTIVITY_CAIRO_STRING) +) + +internal val pivotBarChangedFingerprint = legacyFingerprint( + name = "pivotBarChangedFingerprint", + returnType = "V", + opcodes = listOf( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT + ), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/PivotBar;") + && method.name == "onConfigurationChanged" + } +) + +internal val pivotBarSetTextFingerprint = legacyFingerprint( + name = "pivotBarSetTextFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + parameters = listOf( + "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;", + "Landroid/widget/TextView;", + "Ljava/lang/CharSequence;" + ), + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ), + customFingerprint = { method, _ -> method.name == "" } +) + +internal val pivotBarStyleFingerprint = legacyFingerprint( + name = "pivotBarStyleFingerprint", + returnType = "V", + parameters = listOf("L"), + opcodes = listOf( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.XOR_INT_2ADDR + ), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/PivotBar;") + } +) + +internal val setEnumMapFingerprint = legacyFingerprint( + name = "setEnumMapFingerprint", + literals = listOf(ytFillBell), +) + +internal const val TRANSLUCENT_NAVIGATION_STATUS_BAR_FEATURE_FLAG = 45400535L + +internal val translucentStatusBarFingerprint = legacyFingerprint( + name = "translucentStatusBarFingerprint", + literals = listOf(TRANSLUCENT_NAVIGATION_STATUS_BAR_FEATURE_FLAG), +) + +internal val translucentNavigationStatusBarFeatureFlagFingerprint = legacyFingerprint( + name = "translucentNavigationStatusBarFeatureFlagFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "Z", + literals = listOf(TRANSLUCENT_NAVIGATION_STATUS_BAR_FEATURE_FLAG) +) + +internal const val TRANSLUCENT_NAVIGATION_BUTTONS_FEATURE_FLAG = 45630927L + +internal val translucentNavigationBarFingerprint = legacyFingerprint( + name = "translucentNavigationBarFingerprint", + literals = listOf(TRANSLUCENT_NAVIGATION_BUTTONS_FEATURE_FLAG), +) + +internal val translucentNavigationButtonsFeatureFlagFingerprint = legacyFingerprint( + name = "translucentNavigationButtonsFeatureFlagFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "V", + literals = listOf(TRANSLUCENT_NAVIGATION_BUTTONS_FEATURE_FLAG) +) + +/** + * The device on screen back/home/recent buttons. + */ +internal const val TRANSLUCENT_NAVIGATION_BUTTONS_SYSTEM_FEATURE_FLAG = 45632194L + +internal val translucentNavigationBarSystemFingerprint = legacyFingerprint( + name = "translucentNavigationBarSystemFingerprint", + literals = listOf(TRANSLUCENT_NAVIGATION_BUTTONS_SYSTEM_FEATURE_FLAG), +) + +internal val translucentNavigationButtonsSystemFeatureFlagFingerprint = legacyFingerprint( + name = "translucentNavigationButtonsSystemFeatureFlagFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "Z", + literals = listOf(TRANSLUCENT_NAVIGATION_BUTTONS_SYSTEM_FEATURE_FLAG) +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatch.kt new file mode 100644 index 000000000..a71cd8cc0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatch.kt @@ -0,0 +1,260 @@ +package app.revanced.patches.youtube.general.navigation + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.navigation.addBottomBarContainerHook +import app.revanced.patches.youtube.utils.navigation.hookNavigationButtonCreated +import app.revanced.patches.youtube.utils.navigation.navigationBarHookPatch +import app.revanced.patches.youtube.utils.patch.PatchList.NAVIGATION_BAR_COMPONENTS +import app.revanced.patches.youtube.utils.playservice.is_19_23_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_25_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_28_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.copyResources +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstStringInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private val navigationBarComponentsResourcePatch = resourcePatch( + description = "navigationBarComponentsResourcePatch" +) { + dependsOn(versionCheckPatch) + + execute { + if (is_19_28_or_greater) { + // Since I couldn't get the Cairo notification filled icon anywhere, + // I just made it as close as possible. + arrayOf( + "xxxhdpi", + "xxhdpi", + "xhdpi", + "hdpi", + "mdpi" + ).forEach { dpi -> + copyResources( + "youtube/navigationbuttons", + ResourceGroup( + "drawable-$dpi", + "yt_fill_bell_cairo_black_24.png" + ) + ) + } + } + } +} + +@Suppress("unused") +val navigationBarComponentsPatch = bytecodePatch( + NAVIGATION_BAR_COMPONENTS.title, + NAVIGATION_BAR_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + navigationBarComponentsResourcePatch, + settingsPatch, + sharedResourceIdPatch, + navigationBarHookPatch, + versionCheckPatch, + ) + + execute { + + var settingArray = arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "SETTINGS: HIDE_NAVIGATION_COMPONENTS" + ) + + // region patch for enable translucent navigation bar + + if (is_19_23_or_greater) { + translucentNavigationBarFingerprint.injectLiteralInstructionBooleanCall( + TRANSLUCENT_NAVIGATION_BUTTONS_FEATURE_FLAG, + "$GENERAL_CLASS_DESCRIPTOR->enableTranslucentNavigationBar()Z" + ) + + settingArray += "SETTINGS: TRANSLUCENT_NAVIGATION_BAR" + } + + if (is_19_25_or_greater) { + arrayOf( + Triple( + translucentNavigationStatusBarFeatureFlagFingerprint, + TRANSLUCENT_NAVIGATION_STATUS_BAR_FEATURE_FLAG, + "useTranslucentNavigationStatusBar" + ), + Triple( + translucentNavigationButtonsFeatureFlagFingerprint, + TRANSLUCENT_NAVIGATION_BUTTONS_FEATURE_FLAG, + "useTranslucentNavigationButtons" + ), + Triple( + translucentNavigationButtonsSystemFeatureFlagFingerprint, + TRANSLUCENT_NAVIGATION_BUTTONS_SYSTEM_FEATURE_FLAG, + "useTranslucentNavigationButtons" + ) + ).forEach { + it.first.injectLiteralInstructionBooleanCall( + it.second, + "$GENERAL_CLASS_DESCRIPTOR->${it.third}(Z)Z" + ) + } + + translucentStatusBarFingerprint.injectLiteralInstructionBooleanCall( + TRANSLUCENT_NAVIGATION_STATUS_BAR_FEATURE_FLAG, + "$GENERAL_CLASS_DESCRIPTOR->enableTranslucentStatusBar()Z" + ) + + translucentNavigationBarSystemFingerprint.injectLiteralInstructionBooleanCall( + TRANSLUCENT_NAVIGATION_BUTTONS_SYSTEM_FEATURE_FLAG, + "$GENERAL_CLASS_DESCRIPTOR->enableTranslucentNavigationBar()Z" + ) + + settingArray += "SETTINGS: DISABLE_TRANSLUCENT_STATUS_BAR" + settingArray += "SETTINGS: TRANSLUCENT_NAVIGATION_BAR" + } + + // endregion + + // region patch for enable narrow navigation buttons + + arrayOf( + pivotBarChangedFingerprint, + pivotBarStyleFingerprint + ).forEach { fingerprint -> + fingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.startIndex + 1 + val register = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + invoke-static {v$register}, $GENERAL_CLASS_DESCRIPTOR->enableNarrowNavigationButton(Z)Z + move-result v$register + """ + ) + } + } + } + + // endregion + + // region patch for hide navigation bar + + addBottomBarContainerHook("$GENERAL_CLASS_DESCRIPTOR->hideNavigationBar(Landroid/view/View;)V") + + // endregion + + // region patch for hide navigation buttons + + autoMotiveFingerprint.methodOrThrow().apply { + val insertIndex = indexOfFirstStringInstructionOrThrow(ANDROID_AUTOMOTIVE_STRING) - 1 + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $GENERAL_CLASS_DESCRIPTOR->switchCreateWithNotificationButton(Z)Z + move-result v$insertRegister + """ + ) + } + + // endregion + + // region patch for hide navigation label + + pivotBarSetTextFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setText" + } + val targetRegister = getInstruction(targetIndex).registerC + + addInstruction( + targetIndex, + "invoke-static {v$targetRegister}, $GENERAL_CLASS_DESCRIPTOR->hideNavigationLabel(Landroid/widget/TextView;)V" + ) + } + } + + // endregion + + // region fix for cairo notification icon + + /** + * The Cairo navigation bar was widely rolled out in YouTube 19.28.42. + * + * Unlike Home, Shorts, and Subscriptions, which have Cairo icons, + * Notifications does not have a Cairo icon. + * + * This led to an issue revanced-patches#4046, + * Which was closed as not planned because it was a YouTube issue and not a ReVanced issue. + * + * It was not too hard to fix, so it was implemented as a patch. + */ + if (is_19_28_or_greater) { + val cairoNotificationEnumReference = + with(imageEnumConstructorFingerprint.methodOrThrow()) { + val stringIndex = + indexOfFirstStringInstructionOrThrow(TAB_ACTIVITY_CAIRO_STRING) + val cairoNotificationEnumIndex = indexOfFirstInstructionOrThrow(stringIndex) { + opcode == Opcode.SPUT_OBJECT + } + getInstruction(cairoNotificationEnumIndex).reference + } + + setEnumMapFingerprint.methodOrThrow().apply { + val enumMapIndex = indexOfFirstInstructionReversedOrThrow { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.definingClass == "Ljava/util/EnumMap;" && + reference.name == "put" && + reference.parameterTypes.firstOrNull() == "Ljava/lang/Enum;" + } + val (enumMapRegister, enumRegister) = getInstruction( + enumMapIndex + ).let { + Pair(it.registerC, it.registerD) + } + + addInstructions( + enumMapIndex + 1, """ + sget-object v$enumRegister, $cairoNotificationEnumReference + invoke-static {v$enumMapRegister, v$enumRegister}, $GENERAL_CLASS_DESCRIPTOR->setCairoNotificationFilledIcon(Ljava/util/EnumMap;Ljava/lang/Enum;)V + """ + ) + } + } + + // endregion + + // Hook navigation button created, in order to hide them. + hookNavigationButtonCreated(GENERAL_CLASS_DESCRIPTOR) + + // region add settings + + addPreference(settingArray, NAVIGATION_BAR_COMPONENTS) + + // endregion + } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/snackbar/ForceSnackbarTheme.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/snackbar/ForceSnackbarTheme.kt new file mode 100644 index 000000000..78037ec4d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/snackbar/ForceSnackbarTheme.kt @@ -0,0 +1,126 @@ +package app.revanced.patches.youtube.general.snackbar + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.FORCE_SNACKBAR_THEME +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.doRecursively +import app.revanced.util.insertNode +import org.w3c.dom.Element + +private const val BACKGROUND = "?ytChipBackground" +private const val STROKE = "none" + +@Suppress("unused") +val forceSnackbarTheme = resourcePatch( + FORCE_SNACKBAR_THEME.title, + FORCE_SNACKBAR_THEME.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + + val cornerRadius by stringOption( + key = "cornerRadius", + default = "8.0dip", + title = "Corner radius", + description = "Specify a corner radius for the snackbar." + ) + + val backgroundColor by stringOption( + key = "backgroundColor", + default = BACKGROUND, + values = mapOf( + "Chip" to BACKGROUND, + "Base" to "?ytBaseBackground" + ), + title = "Background color", + description = "Specify a background color for the snackbar. You can specify hex color." + ) + + val strokeColor by stringOption( + key = "strokeColor", + default = STROKE, + values = mapOf( + "None" to STROKE, + "Accent" to "?attr/colorAccent", + "Inverted" to "?attr/ytInvertedBackground" + ), + title = "Stroke color", + description = "Specify a stroke color for the snackbar. You can specify hex color." + ) + + execute { + + fun setAttributes(node: Element, vararg attributesAndValues: String?) { + for (i in attributesAndValues.indices step 2) { + val attribute = attributesAndValues[i] + val value = attributesAndValues[i + 1] + if (attribute != null && value != null) { + node.setAttribute(attribute, value) + } + } + } + + fun editXml(xmlPath: String, tagName: String, vararg attributesAndValues: String?) { + require(attributesAndValues.size % 2 == 0) { "Number of attributes and values must be even." } + + document(xmlPath).use { document -> + document.doRecursively loop@{ node -> + if (node is Element && (tagName.isEmpty() || node.tagName == tagName)) { + setAttributes(node, *attributesAndValues) + } + } + } + } + + fun insert(xmlPath: String, tagName: String, insertTagName: String, vararg attributesAndValues: String?) { + require(attributesAndValues.size % 2 == 0) { "Number of attributes and values must be even." } + + document(xmlPath).use { document -> + document.doRecursively loop@{ node -> + if (node is Element && node.tagName == insertTagName) { + node.insertNode(tagName, node) { + setAttributes(this, *attributesAndValues) + } + } + } + } + } + + if (strokeColor != "none") + insert( + "res/drawable/snackbar_rounded_corners_background.xml", + "stroke", + "corners", + "android:width", + "1dp", + "android:color", + strokeColor + ) + + editXml( + "res/drawable/snackbar_rounded_corners_background.xml", "corners", + "android:bottomLeftRadius", cornerRadius, + "android:bottomRightRadius", cornerRadius, + "android:topLeftRadius", cornerRadius, + "android:topRightRadius", cornerRadius + ) + + editXml("res/drawable/snackbar_rounded_corners_background.xml", "solid", "android:color", backgroundColor) + + try { + listOf( + "res/layout/inset_snackbar.xml", "res/layout/inset_youtube_snackbar.xml", + "res/layout-sw600dp/inset_snackbar.xml", "res/layout-sw600dp/inset_youtube_snackbar.xml" + ) + .forEach { editXml(it, "", "yt:messageTextColor", "?ytTextPrimary") } + } catch (_: Exception) { /* Ignore the error in lower versions */ } + + addPreference(FORCE_SNACKBAR_THEME) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/splashanimation/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/splashanimation/Fingerprints.kt new file mode 100644 index 000000000..6f602ef1c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/splashanimation/Fingerprints.kt @@ -0,0 +1,32 @@ +package app.revanced.patches.youtube.general.splashanimation + +import app.revanced.patches.youtube.utils.resourceid.darkSplashAnimation +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val splashAnimationFingerprint = legacyFingerprint( + name = "splashAnimationFingerprint", + returnType = "V", + parameters = listOf("Landroid/os/Bundle;"), + literals = listOf(darkSplashAnimation), + customFingerprint = { method, _ -> + method.name == "onCreate" + } +) + +internal val startUpResourceIdFingerprint = legacyFingerprint( + name = "startUpResourceIdFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("I"), + literals = listOf(3L, 4L) +) + +internal val startUpResourceIdParentFingerprint = legacyFingerprint( + name = "startUpResourceIdParentFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL or AccessFlags.DECLARED_SYNCHRONIZED, + parameters = listOf("I", "I"), + strings = listOf("early type", "final type") +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/splashanimation/SplashAnimationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/splashanimation/SplashAnimationPatch.kt new file mode 100644 index 000000000..838c4e15e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/splashanimation/SplashAnimationPatch.kt @@ -0,0 +1,72 @@ +package app.revanced.patches.youtube.general.splashanimation + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.patch.PatchList.DISABLE_SPLASH_ANIMATION +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction + +@Suppress("unused") +val splashAnimationPatch = bytecodePatch( + DISABLE_SPLASH_ANIMATION.title, + DISABLE_SPLASH_ANIMATION.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + sharedResourceIdPatch, + settingsPatch, + ) + + execute { + + val startUpResourceIdMethod = + startUpResourceIdFingerprint.methodOrThrow(startUpResourceIdParentFingerprint) + val startUpResourceIdMethodCall = + startUpResourceIdMethod.definingClass + "->" + startUpResourceIdMethod.name + "(I)Z" + + splashAnimationFingerprint.matchOrThrow().let { + it.method.apply { + for (index in implementation!!.instructions.size - 1 downTo 0) { + val instruction = getInstruction(index) + if (instruction.opcode != Opcode.INVOKE_STATIC) + continue + + if ((instruction as ReferenceInstruction).reference.toString() != startUpResourceIdMethodCall) + continue + + val register = getInstruction(index + 1).registerA + + addInstructions( + index + 2, """ + invoke-static {v$register}, $GENERAL_CLASS_DESCRIPTOR->disableSplashAnimation(Z)Z + move-result v$register + """ + ) + } + } + } + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "SETTINGS: DISABLE_SPLASH_ANIMATION" + ), + DISABLE_SPLASH_ANIMATION + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/spoofappversion/SpoofAppVersionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/spoofappversion/SpoofAppVersionPatch.kt new file mode 100644 index 000000000..fd9711fde --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/spoofappversion/SpoofAppVersionPatch.kt @@ -0,0 +1,127 @@ +package app.revanced.patches.youtube.general.spoofappversion + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.spoof.appversion.baseSpoofAppVersionPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.indexOfGetDrawableInstruction +import app.revanced.patches.youtube.utils.patch.PatchList.SPOOF_APP_VERSION +import app.revanced.patches.youtube.utils.playservice.is_18_34_or_greater +import app.revanced.patches.youtube.utils.playservice.is_18_39_or_greater +import app.revanced.patches.youtube.utils.playservice.is_18_49_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_17_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_23_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_28_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_34_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.patches.youtube.utils.toolBarButtonFingerprint +import app.revanced.util.appendAppVersion +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private val spoofAppVersionBytecodePatch = bytecodePatch( + description = "spoofAppVersionBytecodePatch" +) { + + dependsOn(versionCheckPatch) + + execute { + if (!is_19_23_or_greater) { + return@execute + } + + /** + * When spoofing the app version to YouTube 19.20.xx or earlier via Spoof app version on YouTube 19.23.xx+, the Library tab will crash. + * As a temporary workaround, do not set an image in the toolbar when the enum name is UNKNOWN. + */ + toolBarButtonFingerprint.methodOrThrow().apply { + val getDrawableIndex = indexOfGetDrawableInstruction(this) + val enumOrdinalIndex = indexOfFirstInstructionReversedOrThrow(getDrawableIndex) { + opcode == Opcode.INVOKE_INTERFACE && + getReference()?.returnType == "I" + } + val insertIndex = enumOrdinalIndex + 2 + val insertRegister = getInstruction(insertIndex - 1).registerA + val jumpIndex = indexOfFirstInstructionOrThrow(insertIndex) { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setImageDrawable" + } + 1 + + addInstructionsWithLabels( + insertIndex, """ + if-eqz v$insertRegister, :ignore + """, ExternalLabel("ignore", getInstruction(jumpIndex)) + ) + } + } + +} + +@Suppress("unused") +val spoofAppVersionPatch = resourcePatch( + SPOOF_APP_VERSION.title, + SPOOF_APP_VERSION.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseSpoofAppVersionPatch("$GENERAL_CLASS_DESCRIPTOR->getVersionOverride(Ljava/lang/String;)Ljava/lang/String;"), + spoofAppVersionBytecodePatch, + settingsPatch, + versionCheckPatch, + ) + + execute { + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "PREFERENCE_CATEGORY: GENERAL_EXPERIMENTAL_FLAGS", + "SETTINGS: SPOOF_APP_VERSION" + ), + SPOOF_APP_VERSION + ) + + if (!is_19_17_or_greater) { + appendAppVersion("17.41.37") + appendAppVersion("18.05.40") + appendAppVersion("18.17.43") + if (!is_18_34_or_greater) { + return@execute + } + appendAppVersion("18.33.40") + } + + if (!is_18_39_or_greater) { + return@execute + } + appendAppVersion("18.38.45") + + if (!is_18_49_or_greater) { + return@execute + } + appendAppVersion("18.48.39") + + if (!is_19_28_or_greater) { + return@execute + } + appendAppVersion("19.26.42") + + if (!is_19_34_or_greater) { + return@execute + } + appendAppVersion("19.33.37") + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/startpage/ChangeStartPagePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/startpage/ChangeStartPagePatch.kt new file mode 100644 index 000000000..d83f96fa2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/startpage/ChangeStartPagePatch.kt @@ -0,0 +1,69 @@ +package app.revanced.patches.youtube.general.startpage + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.CHANGE_START_PAGE +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.StringReference + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$GENERAL_PATH/ChangeStartPagePatch;" + +@Suppress("unused") +val changeStartPagePatch = bytecodePatch( + CHANGE_START_PAGE.title, + CHANGE_START_PAGE.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + + // Hook browseId. + browseIdFingerprint.methodOrThrow().apply { + val browseIdIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.CONST_STRING && + getReference()?.string == "FEwhat_to_watch" + } + val browseIdRegister = getInstruction(browseIdIndex).registerA + + addInstructions( + browseIdIndex + 1, """ + invoke-static { v$browseIdRegister }, $EXTENSION_CLASS_DESCRIPTOR->overrideBrowseId(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$browseIdRegister + """ + ) + } + + // There is no browseId assigned to Shorts and Search. + // Just hook the Intent action. + intentActionFingerprint.methodOrThrow().addInstruction( + 0, + "invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->overrideIntentAction(Landroid/content/Intent;)V" + ) + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "SETTINGS: CHANGE_START_PAGE" + ), + CHANGE_START_PAGE + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/startpage/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/startpage/Fingerprints.kt new file mode 100644 index 000000000..e67f99af3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/startpage/Fingerprints.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.youtube.general.startpage + +import app.revanced.util.fingerprint.legacyFingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val browseIdFingerprint = legacyFingerprint( + name = "browseIdFingerprint", + returnType = "Lcom/google/android/apps/youtube/app/common/ui/navigation/PaneDescriptor;", + parameters = emptyList(), + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.RETURN_OBJECT, + ), + strings = listOf("FEwhat_to_watch"), +) + +internal val intentActionFingerprint = legacyFingerprint( + name = "intentActionFingerprint", + parameters = listOf("Landroid/content/Intent;"), + strings = listOf("has_handled_intent"), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/Fingerprints.kt new file mode 100644 index 000000000..6440b9e6e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/Fingerprints.kt @@ -0,0 +1,219 @@ +package app.revanced.patches.youtube.general.toolbar + +import app.revanced.patches.youtube.utils.resourceid.actionBarRingo +import app.revanced.patches.youtube.utils.resourceid.actionBarRingoBackground +import app.revanced.patches.youtube.utils.resourceid.drawerContentView +import app.revanced.patches.youtube.utils.resourceid.voiceSearch +import app.revanced.patches.youtube.utils.resourceid.youTubeLogo +import app.revanced.patches.youtube.utils.resourceid.ytOutlineVideoCamera +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.indexOfFirstInstructionReversed +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.util.MethodUtil + +internal val actionBarRingoBackgroundFingerprint = legacyFingerprint( + name = "actionBarRingoBackgroundFingerprint", + returnType = "Landroid/view/View;", + literals = listOf(actionBarRingoBackground), + customFingerprint = { method, _ -> + indexOfActionBarRingoBackgroundTabletInstruction(method) >= 0 + } +) + +internal fun indexOfActionBarRingoBackgroundTabletInstruction(method: Method) = + method.indexOfFirstInstruction { + val reference = getReference() + opcode == Opcode.INVOKE_STATIC && + reference?.parameterTypes?.size == 1 && + reference.parameterTypes.firstOrNull() == "Landroid/content/Context;" && + reference.returnType == "Z" + } + +internal val actionBarRingoConstructorFingerprint = legacyFingerprint( + name = "actionBarRingoConstructorFingerprint", + returnType = "V", + strings = listOf("default"), + customFingerprint = custom@{ method, _ -> + if (!MethodUtil.isConstructor(method)) { + return@custom false + } + + val parameterTypes = method.parameterTypes + parameterTypes.size >= 5 && parameterTypes[0] == "Landroid/content/Context;" + } +) + +internal val actionBarRingoTextFingerprint = legacyFingerprint( + name = "actionBarRingoTextFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + customFingerprint = { method, _ -> + indexOfStartDelayInstruction(method) >= 0 && + indexOfActionBarRingoTextTabletInstructions(method) >= 0 + } +) + +internal fun indexOfStartDelayInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setStartDelay" + } + +internal fun indexOfActionBarRingoTextTabletInstructions(method: Method) = + method.indexOfFirstInstructionReversed(indexOfStartDelayInstruction(method)) { + val reference = getReference() + opcode == Opcode.INVOKE_STATIC && + reference?.parameterTypes?.size == 1 && + reference.parameterTypes.firstOrNull() == "Landroid/content/Context;" && + reference.returnType == "Z" + } + +internal val attributeResolverFingerprint = legacyFingerprint( + name = "attributeResolverFingerprint", + returnType = "Landroid/graphics/drawable/Drawable;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("Landroid/content/Context;", "I"), + strings = listOf("Type of attribute is not a reference to a drawable (attr = %d, value = %s)") +) + +internal val createButtonDrawableFingerprint = legacyFingerprint( + name = "createButtonDrawableFingerprint", + literals = listOf(ytOutlineVideoCamera), +) + +internal val createSearchSuggestionsFingerprint = legacyFingerprint( + name = "createSearchSuggestionsFingerprint", + returnType = "Landroid/view/View;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("I", "Landroid/view/View;", "Landroid/view/ViewGroup;"), + strings = listOf("ss_rds") +) + +internal val drawerContentViewConstructorFingerprint = legacyFingerprint( + name = "drawerContentViewConstructorFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf(drawerContentView), +) + +internal val drawerContentViewFingerprint = legacyFingerprint( + name = "drawerContentViewFingerprint", + returnType = "V", + parameters = listOf("L"), + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.NEW_INSTANCE, + Opcode.INVOKE_DIRECT, + ), + customFingerprint = { method, _ -> + indexOfAddViewInstruction(method) >= 0 + } +) + +internal fun indexOfAddViewInstruction(method: Method) = + method.indexOfFirstInstructionReversed { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "addView" + } + +/** + * This fingerprint is compatible with YouTube v19.07.40+ + */ +internal val imageSearchButtonConfigFingerprint = legacyFingerprint( + name = "imageSearchButtonConfigFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(45617544L), +) + +internal val searchBarFingerprint = legacyFingerprint( + name = "searchBarFingerprint", + returnType = "V", + parameters = listOf("Ljava/lang/String;"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.IF_EQZ, + Opcode.IGET_BOOLEAN, + Opcode.IF_EQZ + ), + customFingerprint = { method, _ -> + method.indexOfFirstInstructionReversed { + getReference()?.name == "isEmpty" + } >= 0 + } +) + +internal val searchBarParentFingerprint = legacyFingerprint( + name = "searchBarParentFingerprint", + returnType = "Landroid/view/View;", + strings = listOf("voz-target-id"), + literals = listOf(voiceSearch), +) + +internal val voiceInputControllerParentFingerprint = legacyFingerprint( + name = "voiceInputControllerParentFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("[B", "Z"), + strings = listOf("VoiceInputController"), +) + +internal val voiceInputControllerFingerprint = legacyFingerprint( + name = "voiceInputControllerFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "resolveActivity" + } >= 0 + }, +) + +internal val searchResultFingerprint = legacyFingerprint( + name = "searchResultFingerprint", + returnType = "Landroid/view/View;", + strings = listOf("search_filter_chip_applied", "search_original_chip_query"), + literals = listOf(voiceSearch), +) + +internal val setActionBarRingoFingerprint = legacyFingerprint( + name = "setActionBarRingoFingerprint", + returnType = "L", + parameters = listOf("L", "L"), + opcodes = listOf( + Opcode.IF_NEZ, + Opcode.SGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.INVOKE_STATIC + ), + literals = listOf(actionBarRingo), +) + +@Suppress("SpellCheckingInspection") +internal val yoodlesImageViewFingerprint = legacyFingerprint( + name = "yoodlesImageViewFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "L"), + returnType = "Landroid/view/View;", + literals = listOf(youTubeLogo) +) + +internal val youActionBarFingerprint = legacyFingerprint( + name = "youActionBarFingerprint", + returnType = "V", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ, + ) +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt new file mode 100644 index 000000000..74f762141 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt @@ -0,0 +1,420 @@ +package app.revanced.patches.youtube.general.toolbar + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.youtube.utils.castbutton.castButtonPatch +import app.revanced.patches.youtube.utils.castbutton.hookToolBarCastButton +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.patch.PatchList.TOOLBAR_COMPONENTS +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.resourceid.actionBarRingoBackground +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.resourceid.ytOutlineVideoCamera +import app.revanced.patches.youtube.utils.resourceid.ytPremiumWordMarkHeader +import app.revanced.patches.youtube.utils.resourceid.ytWordMarkHeader +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.ResourceUtils.getContext +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.patches.youtube.utils.toolbar.hookToolBar +import app.revanced.patches.youtube.utils.toolbar.toolBarHookPatch +import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT +import app.revanced.util.doRecursively +import app.revanced.util.findInstructionIndicesReversedOrThrow +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodCall +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import app.revanced.util.replaceLiteralInstructionCall +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.util.MethodUtil +import org.w3c.dom.Element + +@Suppress("unused") +val toolBarComponentsPatch = bytecodePatch( + TOOLBAR_COMPONENTS.title, + TOOLBAR_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + castButtonPatch, + sharedResourceIdPatch, + settingsPatch, + toolBarHookPatch, + versionCheckPatch, + ) + + execute { + fun MutableMethod.injectSearchBarHook( + insertIndex: Int, + insertRegister: Int, + descriptor: String + ) = + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $GENERAL_CLASS_DESCRIPTOR->$descriptor(Z)Z + move-result v$insertRegister + """ + ) + + fun MutableMethod.injectSearchBarHook( + insertIndex: Int, + descriptor: String + ) = + injectSearchBarHook( + insertIndex, + getInstruction(insertIndex).registerA, + descriptor + ) + + var settingArray = arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "SETTINGS: TOOLBAR_COMPONENTS" + ) + + // region patch for change YouTube header + + // Invoke YouTube's header attribute into extension. + val smaliInstruction = """ + invoke-static {}, $GENERAL_CLASS_DESCRIPTOR->getHeaderAttributeId()I + move-result v$REGISTER_TEMPLATE_REPLACEMENT + """ + + arrayOf( + ytPremiumWordMarkHeader, + ytWordMarkHeader + ).forEach { literal -> + replaceLiteralInstructionCall(literal, smaliInstruction) + } + + // YouTube's headers have the form of AttributeSet, which is decoded from YouTube's built-in classes. + val attributeResolverMethod = attributeResolverFingerprint.methodOrThrow() + val attributeResolverMethodCall = + attributeResolverMethod.definingClass + "->" + attributeResolverMethod.name + "(Landroid/content/Context;I)Landroid/graphics/drawable/Drawable;" + + findMethodOrThrow(GENERAL_CLASS_DESCRIPTOR) { + name == "getHeaderDrawable" + }.addInstructions( + 0, """ + invoke-static {p0, p1}, $attributeResolverMethodCall + move-result-object p0 + return-object p0 + """ + ) + + // The sidebar's header is lithoView. Add a listener to change it. + drawerContentViewFingerprint.methodOrThrow(drawerContentViewConstructorFingerprint).apply { + val insertIndex = indexOfAddViewInstruction(this) + val insertRegister = getInstruction(insertIndex).registerD + + addInstruction( + insertIndex, + "invoke-static {v$insertRegister}, $GENERAL_CLASS_DESCRIPTOR->setDrawerNavigationHeader(Landroid/view/View;)V" + ) + } + + // Override the header in the search bar. + setActionBarRingoFingerprint.mutableClassOrThrow().methods.first { method -> + MethodUtil.isConstructor(method) + }.apply { + val insertIndex = indexOfFirstInstructionOrThrow(Opcode.IPUT_BOOLEAN) + val insertRegister = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex + 1, + "const/4 v$insertRegister, 0x0" + ) + addInstructions( + insertIndex, """ + invoke-static {}, $GENERAL_CLASS_DESCRIPTOR->overridePremiumHeader()Z + move-result v$insertRegister + """ + ) + } + + // endregion + + // region patch for enable wide search bar + + // Limitation: Premium header will not be applied for YouTube Premium users if the user uses the 'Wide search bar with header' option. + // This is because it forces the deprecated search bar to be loaded. + // As a solution to this limitation, 'Change YouTube header' patch is required. + actionBarRingoBackgroundFingerprint.methodOrThrow().apply { + val viewIndex = + indexOfFirstLiteralInstructionOrThrow(actionBarRingoBackground) + 2 + val viewRegister = getInstruction(viewIndex).registerA + + addInstructions( + viewIndex + 1, + "invoke-static {v$viewRegister}, $GENERAL_CLASS_DESCRIPTOR->setWideSearchBarLayout(Landroid/view/View;)V" + ) + + val targetIndex = indexOfActionBarRingoBackgroundTabletInstruction(this) + 1 + val targetRegister = getInstruction(targetIndex).registerA + + injectSearchBarHook( + targetIndex + 1, + targetRegister, + "enableWideSearchBarWithHeaderInverse" + ) + } + + actionBarRingoTextFingerprint.methodOrThrow(actionBarRingoBackgroundFingerprint).apply { + val targetIndex = indexOfActionBarRingoTextTabletInstructions(this) + 1 + val targetRegister = getInstruction(targetIndex).registerA + + injectSearchBarHook( + targetIndex + 1, + targetRegister, + "enableWideSearchBarWithHeader" + ) + } + + actionBarRingoConstructorFingerprint.methodOrThrow().apply { + val staticCalls = implementation!!.instructions + .withIndex() + .filter { (_, instruction) -> + val methodReference = (instruction as? ReferenceInstruction)?.reference + instruction.opcode == Opcode.INVOKE_STATIC && + methodReference is MethodReference && + methodReference.parameterTypes.size == 1 && + methodReference.returnType == "Z" + } + + if (staticCalls.size != 2) + throw PatchException("Size of staticCalls does not match: ${staticCalls.size}") + + mapOf( + staticCalls.elementAt(0).index to "enableWideSearchBar", + staticCalls.elementAt(1).index to "enableWideSearchBarWithHeader" + ).forEach { (index, descriptor) -> + val walkerMethod = getWalkerMethod(index) + + walkerMethod.apply { + injectSearchBarHook( + implementation!!.instructions.lastIndex, + descriptor + ) + } + } + } + + youActionBarFingerprint.matchOrThrow(setActionBarRingoFingerprint).let { + it.method.apply { + injectSearchBarHook( + it.patternMatch!!.endIndex, + "enableWideSearchBarInYouTab" + ) + } + } + + // This attribution cannot be changed in extension, so change it in the xml file. + + getContext().document("res/layout/action_bar_ringo_background.xml").use { document -> + document.doRecursively { node -> + arrayOf("layout_marginStart").forEach replacement@{ replacement -> + if (node !is Element) return@replacement + + node.getAttributeNode("android:$replacement")?.let { attribute -> + attribute.textContent = "0.0dip" + } + } + } + } + + // endregion + + // region patch for hide cast button + + hookToolBarCastButton() + + // endregion + + // region patch for hide create button + + hookToolBar("$GENERAL_CLASS_DESCRIPTOR->hideCreateButton") + + // endregion + + // region patch for hide notification button + + hookToolBar("$GENERAL_CLASS_DESCRIPTOR->hideNotificationButton") + + // endregion + + // region patch for hide search term thumbnail + + createSearchSuggestionsFingerprint.methodOrThrow().apply { + val relativeIndex = indexOfFirstLiteralInstructionOrThrow(40L) + val replaceIndex = indexOfFirstInstructionReversedOrThrow(relativeIndex) { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.toString() == "Landroid/widget/ImageView;->setVisibility(I)V" + } - 1 + + val jumpIndex = indexOfFirstInstructionOrThrow(relativeIndex) { + opcode == Opcode.INVOKE_STATIC && + getReference()?.toString() == "Landroid/net/Uri;->parse(Ljava/lang/String;)Landroid/net/Uri;" + } + 4 + + val replaceIndexInstruction = getInstruction(replaceIndex) + val replaceIndexReference = + getInstruction(replaceIndex).reference + + addInstructionsWithLabels( + replaceIndex + 1, """ + invoke-static { }, $GENERAL_CLASS_DESCRIPTOR->hideSearchTermThumbnail()Z + move-result v${replaceIndexInstruction.registerA} + if-nez v${replaceIndexInstruction.registerA}, :hidden + iget-object v${replaceIndexInstruction.registerA}, v${replaceIndexInstruction.registerB}, $replaceIndexReference + """, ExternalLabel("hidden", getInstruction(jumpIndex)) + ) + removeInstruction(replaceIndex) + } + + // endregion + + /* + // region patch for hide voice search button + + if (is_19_28_or_greater) { + imageSearchButtonConfigFingerprint.injectLiteralInstructionBooleanCall( + 45617544L, + "$GENERAL_CLASS_DESCRIPTOR->hideImageSearchButton(Z)Z" + ) + + updatePatchStatus(PATCH_STATUS_CLASS_DESCRIPTOR, "ImageSearchButton") + + settingArray += "SETTINGS: HIDE_IMAGE_SEARCH_BUTTON" + } + + // endregion + */ + + // region patch for hide voice search button + + searchBarFingerprint.matchOrThrow(searchBarParentFingerprint).let { + it.method.apply { + val startIndex = it.patternMatch!!.startIndex + val setVisibilityIndex = indexOfFirstInstructionOrThrow(startIndex) { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setVisibility" + } + val setVisibilityInstruction = + getInstruction(setVisibilityIndex) + + replaceInstruction( + setVisibilityIndex, + "invoke-static {v${setVisibilityInstruction.registerC}, v${setVisibilityInstruction.registerD}}, " + + "$GENERAL_CLASS_DESCRIPTOR->hideVoiceSearchButton(Landroid/view/View;I)V" + ) + } + } + + searchResultFingerprint.matchOrThrow().let { + it.method.apply { + val voiceInputControllerActivityMethodCall = + voiceInputControllerFingerprint + .methodOrThrow(voiceInputControllerParentFingerprint) + .methodCall() + + val voiceInputControllerActivityIndex = + indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.toString() == voiceInputControllerActivityMethodCall + } + val setOnClickListenerIndex = indexOfFirstInstructionOrThrow(voiceInputControllerActivityIndex) { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setOnClickListener" + } + val viewRegister = + getInstruction(setOnClickListenerIndex).registerC + + addInstruction( + setOnClickListenerIndex + 1, + "invoke-static {v$viewRegister}, $GENERAL_CLASS_DESCRIPTOR->hideVoiceSearchButton(Landroid/view/View;)V" + ) + } + } + + // endregion + + // region patch for hide YouTube Doodles + + yoodlesImageViewFingerprint.methodOrThrow().apply { + findInstructionIndicesReversedOrThrow { + opcode == Opcode.INVOKE_VIRTUAL + && getReference()?.name == "setImageDrawable" + }.forEach { insertIndex -> + val (viewRegister, drawableRegister) = getInstruction( + insertIndex + ).let { + Pair(it.registerC, it.registerD) + } + replaceInstruction( + insertIndex, + "invoke-static {v$viewRegister, v$drawableRegister}, " + + "$GENERAL_CLASS_DESCRIPTOR->hideYouTubeDoodles(Landroid/widget/ImageView;Landroid/graphics/drawable/Drawable;)V" + ) + } + } + + // endregion + + // region patch for replace create button + + createButtonDrawableFingerprint.methodOrThrow().apply { + val index = indexOfFirstLiteralInstructionOrThrow(ytOutlineVideoCamera) + val register = getInstruction(index).registerA + + addInstructions( + index + 1, """ + invoke-static {v$register}, $GENERAL_CLASS_DESCRIPTOR->getCreateButtonDrawableId(I)I + move-result v$register + """ + ) + } + + hookToolBar("$GENERAL_CLASS_DESCRIPTOR->replaceCreateButton") + + findMethodOrThrow( + "Lcom/google/android/apps/youtube/app/application/Shell_SettingsActivity;" + ) { + name == "onCreate" + }.addInstruction( + 0, + "invoke-static {p0}, $GENERAL_CLASS_DESCRIPTOR->setShellActivityTheme(Landroid/app/Activity;)V" + ) + + // endregion + + // region add settings + + addPreference( + settingArray, + TOOLBAR_COMPONENTS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch.kt new file mode 100644 index 000000000..a955491e1 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch.kt @@ -0,0 +1,166 @@ +package app.revanced.patches.youtube.layout.actionbuttons + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.CUSTOM_SHORTS_ACTION_BUTTONS +import app.revanced.patches.youtube.utils.playservice.is_19_36_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.Utils.printInfo +import app.revanced.util.copyResources +import app.revanced.util.inputStreamFromBundledResourceOrThrow +import app.revanced.util.lowerCaseOrThrow +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +private const val DEFAULT_ICON = "cairo" +private const val YOUTUBE_ICON = "youtube" + +private val sizeArray = arrayOf( + "xxxhdpi", + "xxhdpi", + "xhdpi", + "hdpi", + "mdpi" +) + +private val drawableDirectories = sizeArray.map { "drawable-$it" } + +@Suppress("unused") +val shortsActionButtonsPatch = resourcePatch( + CUSTOM_SHORTS_ACTION_BUTTONS.title, + CUSTOM_SHORTS_ACTION_BUTTONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + versionCheckPatch + ) + + val iconTypeOption = stringOption( + key = "iconType", + default = DEFAULT_ICON, + values = mapOf( + "Cairo" to DEFAULT_ICON, + "Outline" to "outline", + "OutlineCircle" to "outlinecircle", + "Round" to "round", + "YoutubeOutline" to "youtubeoutline", + "YouTube" to YOUTUBE_ICON + ), + title = "Shorts icon style ", + description = "The style of the icons for the action buttons in the Shorts player.", + required = true, + ) + + execute { + + // Check patch options first. + val iconType = iconTypeOption + .lowerCaseOrThrow() + + if (iconType == YOUTUBE_ICON) { + printInfo("Shorts action buttons will remain unchanged as it matches the original.") + addPreference(CUSTOM_SHORTS_ACTION_BUTTONS) + return@execute + } + + val sourceResourceDirectory = "youtube/shorts/actionbuttons/$iconType" + + val resourceMap = ShortsActionButtons.entries.map { it.newResource to it.resources } + val res = get("res") + + for ((toFileName, fromResourceArray) in resourceMap) { + fromResourceArray.forEach { fromFileName -> + drawableDirectories.forEach { drawableDirectory -> + val fromFile = "$drawableDirectory/$fromFileName.webp" + val fromPath = res.resolve(fromFile).toPath() + val toFile = "$drawableDirectory/$toFileName.webp" + val toPath = res.resolve(toFile).toPath() + val inputStreamForLegacy = + inputStreamFromBundledResourceOrThrow(sourceResourceDirectory, fromFile) + val inputStreamForNew = + inputStreamFromBundledResourceOrThrow(sourceResourceDirectory, fromFile) + + Files.copy(inputStreamForLegacy, fromPath, StandardCopyOption.REPLACE_EXISTING) + + if (is_19_36_or_greater) { + Files.copy(inputStreamForNew, toPath, StandardCopyOption.REPLACE_EXISTING) + } + } + } + } + + copyResources( + sourceResourceDirectory, + ResourceGroup( + "drawable", + "ic_right_comment_32c.xml", + "ic_right_dislike_off_32c.xml", + "ic_right_like_off_32c.xml", + "ic_right_share_32c.xml" + ) + ) + + addPreference(CUSTOM_SHORTS_ACTION_BUTTONS) + + if (iconType == DEFAULT_ICON) { + return@execute + } + + copyResources( + "youtube/shorts/actionbuttons/shared", + ResourceGroup( + "drawable", + "reel_camera_bold_24dp.xml", + "reel_more_vertical_bold_24dp.xml", + "reel_search_bold_24dp.xml" + ) + ) + } +} + +internal enum class ShortsActionButtons(val newResource: String, vararg val resources: String) { + LIKE( + "youtube_shorts_like_outline_32dp", + // This replaces the new icon. + "ic_right_like_off_shadowed", + ), + LIKE_FILLED( + "youtube_shorts_like_fill_32dp", + "ic_right_like_on_32c", + // This replaces the new icon. + "ic_right_like_on_shadowed", + ), + DISLIKE( + "youtube_shorts_dislike_outline_32dp", + // This replaces the new icon. + "ic_right_dislike_off_shadowed", + ), + DISLIKE_FILLED( + "youtube_shorts_dislike_fill_32dp", + "ic_right_dislike_on_32c", + // This replaces the new icon. + "ic_right_dislike_on_shadowed", + ), + COMMENT( + "youtube_shorts_comment_outline_32dp", + // This replaces the new icon. + "ic_right_comment_shadowed", + ), + SHARE( + "youtube_shorts_share_outline_32dp", + // This replaces the new icon. + "ic_right_share_shadowed", + ), + REMIX( + "youtube_shorts_remix_outline_32dp", + "ic_remix_filled_white_24", + // This replaces the new icon. + "ic_remix_filled_white_shadowed", + ), +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt new file mode 100644 index 000000000..9895fbf2d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt @@ -0,0 +1,279 @@ +package app.revanced.patches.youtube.layout.branding.icon + +import app.revanced.patcher.patch.booleanOption +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.CUSTOM_BRANDING_ICON_FOR_YOUTUBE +import app.revanced.patches.youtube.utils.playservice.is_19_32_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_34_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatusIcon +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.Utils.trimIndentMultiline +import app.revanced.util.copyAdaptiveIcon +import app.revanced.util.copyFile +import app.revanced.util.copyResources +import app.revanced.util.getResourceGroup +import app.revanced.util.underBarOrThrow +import app.revanced.util.valueOrThrow +import org.w3c.dom.Element + +private const val ADAPTIVE_ICON_BACKGROUND_FILE_NAME = + "adaptiveproduct_youtube_background_color_108" +private const val ADAPTIVE_ICON_FOREGROUND_FILE_NAME = + "adaptiveproduct_youtube_foreground_color_108" +private const val ADAPTIVE_ICON_MONOCHROME_FILE_NAME = + "adaptive_monochrome_ic_youtube_launcher" +private const val DEFAULT_ICON = "xisr_yellow" + +private val availableIcon = mapOf( + "AFN Blue" to "afn_blue", + "AFN Red" to "afn_red", + "MMT" to "mmt", + "MMT Blue" to "mmt_blue", + "MMT Green" to "mmt_green", + "MMT Orange" to "mmt_orange", + "MMT Pink" to "mmt_pink", + "MMT Turquoise" to "mmt_turquoise", + "MMT Yellow" to "mmt_yellow", + "Revancify Blue" to "revancify_blue", + "Revancify Red" to "revancify_red", + "Vanced Black" to "vanced_black", + "Vanced Light" to "vanced_light", + "Xisr Yellow" to DEFAULT_ICON, + "YouTube" to "youtube", + "YouTube Black" to "youtube_black", +) + +private val sizeArray = arrayOf( + "xxxhdpi", + "xxhdpi", + "xhdpi", + "hdpi", + "mdpi" +) + +private val drawableDirectories = sizeArray.map { "drawable-$it" } + +private val mipmapDirectories = sizeArray.map { "mipmap-$it" } + +private val launcherIconResourceFileNames = arrayOf( + ADAPTIVE_ICON_BACKGROUND_FILE_NAME, + ADAPTIVE_ICON_FOREGROUND_FILE_NAME, + "ic_launcher", + "ic_launcher_round" +).map { "$it.png" }.toTypedArray() + +private val splashIconResourceFileNames = arrayOf( + "product_logo_youtube_color_24", + "product_logo_youtube_color_36", + "product_logo_youtube_color_144", + "product_logo_youtube_color_192" +).map { "$it.png" }.toTypedArray() + +private val oldSplashAnimationResourceFileNames = arrayOf( + "\$\$avd_anim__1__0", + "\$\$avd_anim__1__1", + "\$\$avd_anim__2__0", + "\$\$avd_anim__2__1", + "\$\$avd_anim__3__0", + "\$\$avd_anim__3__1", + "\$avd_anim__0", + "\$avd_anim__1", + "\$avd_anim__2", + "\$avd_anim__3", + "\$avd_anim__4", + "avd_anim" +).map { "$it.xml" }.toTypedArray() + +private val launcherIconResourceGroups = + mipmapDirectories.getResourceGroup(launcherIconResourceFileNames) + +private val splashIconResourceGroups = + drawableDirectories.getResourceGroup(splashIconResourceFileNames) + +private val oldSplashAnimationResourceGroups = + listOf("drawable").getResourceGroup(oldSplashAnimationResourceFileNames) + +@Suppress("unused") +val customBrandingIconPatch = resourcePatch( + CUSTOM_BRANDING_ICON_FOR_YOUTUBE.title, + CUSTOM_BRANDING_ICON_FOR_YOUTUBE.summary, + false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + versionCheckPatch, + ) + + val appIconOption = stringOption( + key = "appIcon", + default = DEFAULT_ICON, + values = availableIcon, + title = "App icon", + description = """ + The icon to apply to the app. + + If a path to a folder is provided, the folder must contain the following folders: + + ${mipmapDirectories.joinToString("\n") { "- $it" }} + + Each of these folders must contain the following files: + + ${launcherIconResourceFileNames.joinToString("\n") { "- $it" }} + """.trimIndentMultiline(), + required = true, + ) + + val changeSplashIconOption by booleanOption( + key = "changeSplashIcon", + default = true, + title = "Change splash icons", + description = "Apply the custom branding icon to the splash screen.", + required = true + ) + + val restoreOldSplashAnimationOption by booleanOption( + key = "restoreOldSplashAnimation", + default = true, + title = "Restore old splash animation", + description = "Restore the old style splash animation.", + required = true, + ) + + execute { + // Check patch options first. + var appIcon = appIconOption + .underBarOrThrow() + + val appIconResourcePath = "youtube/branding/$appIcon" + + // Check if a custom path is used in the patch options. + if (!availableIcon.containsValue(appIcon)) { + appIcon = appIconOption.valueOrThrow() + val copiedFiles = copyFile( + launcherIconResourceGroups, + appIcon, + "Invalid app icon path: $appIcon. Does not apply patches." + ) + if (copiedFiles) + updatePatchStatusIcon("custom") + } else { + // Change launcher icon. + launcherIconResourceGroups.let { resourceGroups -> + resourceGroups.forEach { + copyResources("$appIconResourcePath/launcher", it) + } + } + + // Change monochrome icon. + arrayOf( + ResourceGroup( + "drawable", + "$ADAPTIVE_ICON_MONOCHROME_FILE_NAME.xml" + ) + ).forEach { resourceGroup -> + copyResources("$appIconResourcePath/monochrome", resourceGroup) + } + + // Change splash icon. + if (changeSplashIconOption == true) { + splashIconResourceGroups.let { resourceGroups -> + resourceGroups.forEach { + copyResources("$appIconResourcePath/splash", it) + } + } + + document("res/values/styles.xml").use { document -> + val resourcesNode = + document.getElementsByTagName("resources").item(0) as Element + val childNodes = resourcesNode.childNodes + + for (i in 0 until childNodes.length) { + val node = childNodes.item(i) as? Element ?: continue + val nodeAttributeName = node.getAttribute("name") + if (nodeAttributeName.startsWith("Theme.YouTube.Launcher")) { + val style = document.createElement("style") + style.setAttribute("name", nodeAttributeName) + style.setAttribute("parent", "@style/Base.Theme.YouTube.Launcher") + + resourcesNode.removeChild(node) + resourcesNode.appendChild(style) + } + } + } + } + + // Change splash screen. + if (restoreOldSplashAnimationOption == true) { + oldSplashAnimationResourceGroups.let { resourceGroups -> + resourceGroups.forEach { + copyResources("$appIconResourcePath/splash", it) + } + } + + val styleMap = mutableMapOf() + styleMap["Base.Theme.YouTube.Launcher"] = + "@style/Theme.AppCompat.DayNight.NoActionBar" + + if (is_19_32_or_greater) { + styleMap["Theme.YouTube.Home"] = "@style/Base.V27.Theme.YouTube.Home" + } + + styleMap.forEach { (nodeAttributeName, nodeAttributeParent) -> + document("res/values-v31/styles.xml").use { document -> + val resourcesNode = + document.getElementsByTagName("resources").item(0) as Element + + val style = document.createElement("style") + style.setAttribute("name", nodeAttributeName) + style.setAttribute("parent", nodeAttributeParent) + + val primaryItem = document.createElement("item") + primaryItem.setAttribute("name", "android:windowSplashScreenAnimatedIcon") + primaryItem.textContent = "@drawable/avd_anim" + val secondaryItem = document.createElement("item") + secondaryItem.setAttribute( + "name", + "android:windowSplashScreenAnimationDuration" + ) + secondaryItem.textContent = if (appIcon.startsWith("revancify")) + "1500" + else + "1000" + + style.appendChild(primaryItem) + style.appendChild(secondaryItem) + + resourcesNode.appendChild(style) + } + } + } + + updatePatchStatusIcon(appIcon) + } + + // region fix app icon + + if (!is_19_34_or_greater) { + return@execute + } + if (appIcon == "youtube") { + return@execute + } + + copyAdaptiveIcon( + ADAPTIVE_ICON_BACKGROUND_FILE_NAME, + ADAPTIVE_ICON_FOREGROUND_FILE_NAME, + mipmapDirectories, + ADAPTIVE_ICON_MONOCHROME_FILE_NAME + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/name/CustomBrandingNamePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/name/CustomBrandingNamePatch.kt new file mode 100644 index 000000000..079e6196a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/name/CustomBrandingNamePatch.kt @@ -0,0 +1,60 @@ +package app.revanced.patches.youtube.layout.branding.name + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.CUSTOM_BRANDING_NAME_FOR_YOUTUBE +import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatusLabel +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.removeStringsElements +import app.revanced.util.valueOrThrow + +private const val APP_NAME = "RVX" + +@Suppress("unused") +val customBrandingNamePatch = resourcePatch( + CUSTOM_BRANDING_NAME_FOR_YOUTUBE.title, + CUSTOM_BRANDING_NAME_FOR_YOUTUBE.summary, + false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + val appNameOption = stringOption( + key = "appName", + default = APP_NAME, + values = mapOf( + "ReVanced Extended" to "ReVanced Extended", + "RVX" to APP_NAME, + "YouTube RVX" to "YouTube RVX", + "YouTube" to "YouTube", + ), + title = "App name", + description = "The name of the app.", + required = true, + ) + + execute { + // Check patch options first. + val appName = appNameOption + .valueOrThrow() + + removeStringsElements( + arrayOf("application_name") + ) + + document("res/values/strings.xml").use { document -> + val stringElement = document.createElement("string") + + stringElement.setAttribute("name", "application_name") + stringElement.textContent = appName + + document.getElementsByTagName("resources").item(0) + .appendChild(stringElement) + } + + updatePatchStatusLabel(appName) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/dimming/ShortsDimmingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/dimming/ShortsDimmingPatch.kt new file mode 100644 index 000000000..a0c3aed8e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/dimming/ShortsDimmingPatch.kt @@ -0,0 +1,34 @@ +package app.revanced.patches.youtube.layout.dimming + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.HIDE_SHORTS_DIMMING +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.removeOverlayBackground + +@Suppress("unused") +val shortsDimmingPatch = resourcePatch( + HIDE_SHORTS_DIMMING.title, + HIDE_SHORTS_DIMMING.summary, + false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + + removeOverlayBackground( + arrayOf("reel_player_overlay_scrims.xml"), + arrayOf("reel_player_overlay_v2_scrims_vertical") + ) + removeOverlayBackground( + arrayOf("reel_watch_fragment.xml"), + arrayOf("reel_scrim_shorts_while_top") + ) + + addPreference(HIDE_SHORTS_DIMMING) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/doubletaplength/DoubleTapLengthPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/doubletaplength/DoubleTapLengthPatch.kt new file mode 100644 index 000000000..f9348f918 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/doubletaplength/DoubleTapLengthPatch.kt @@ -0,0 +1,80 @@ +package app.revanced.patches.youtube.layout.doubletaplength + +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.CUSTOM_DOUBLE_TAP_LENGTH +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.addEntryValues +import app.revanced.util.copyResources +import app.revanced.util.valueOrThrow +import java.nio.file.Files + +@Suppress("unused") +val doubleTapLengthPatch = resourcePatch( + CUSTOM_DOUBLE_TAP_LENGTH.title, + CUSTOM_DOUBLE_TAP_LENGTH.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + val doubleTapLengthArraysOption = stringOption( + key = "doubleTapLengthArrays", + default = "3, 5, 10, 15, 20, 30, 60, 120, 180", + title = "Double-tap to seek values", + description = "A list of custom Double-tap to seek lengths to be added, separated by commas.", + required = true, + ) + + execute { + // Check patch options first. + val doubleTapLengthArrays = doubleTapLengthArraysOption + .valueOrThrow() + + // Check patch options first. + val splits = doubleTapLengthArrays + .replace(" ", "") + .split(",") + if (splits.isEmpty()) throw PatchException("Invalid double-tap length elements") + val lengthElements = splits.map { it } + + val arrayPath = "res/values-v21/arrays.xml" + val entriesName = "double_tap_length_entries" + val entryValueName = "double_tap_length_values" + + val valuesV21Directory = get("res").resolve("values-v21") + if (!valuesV21Directory.isDirectory) + Files.createDirectories(valuesV21Directory.toPath()) + + /** + * Copy arrays + */ + copyResources( + "youtube/doubletap", + ResourceGroup( + "values-v21", + "arrays.xml" + ) + ) + + for (index in 0 until splits.count()) { + addEntryValues( + entryValueName, + lengthElements[index], + path = arrayPath + ) + addEntryValues( + entriesName, + lengthElements[index], + path = arrayPath + ) + } + + addPreference(CUSTOM_DOUBLE_TAP_LENGTH) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/header/ChangeHeaderPatch.kt new file mode 100644 index 000000000..986292470 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/header/ChangeHeaderPatch.kt @@ -0,0 +1,175 @@ +package app.revanced.patches.youtube.layout.header + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.CUSTOM_HEADER_FOR_YOUTUBE +import app.revanced.patches.youtube.utils.settings.ResourceUtils.getIconType +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.Utils.printWarn +import app.revanced.util.Utils.trimIndentMultiline +import app.revanced.util.copyFile +import app.revanced.util.copyResources +import app.revanced.util.underBarOrThrow +import app.revanced.util.valueOrThrow +import java.io.File +import java.nio.file.Files +import kotlin.io.path.copyTo +import kotlin.io.path.exists + +private const val GENERIC_HEADER_FILE_NAME = "yt_wordmark_header" +private const val PREMIUM_HEADER_FILE_NAME = "yt_premium_wordmark_header" + +private const val NEW_GENERIC_HEADER_FILE_NAME = "yt_ringo2_wordmark_header" +private const val NEW_PREMIUM_HEADER_FILE_NAME = "yt_ringo2_premium_wordmark_header" + +private const val DEFAULT_HEADER_KEY = "Custom branding icon" +private const val DEFAULT_HEADER_VALUE = "custom_branding_icon" + +private val genericHeaderResourceDirectoryNames = mapOf( + "xxxhdpi" to "488px x 192px", + "xxhdpi" to "366px x 144px", + "xhdpi" to "244px x 96px", + "hdpi" to "184px x 72px", + "mdpi" to "122px x 48px", +).map { (dpi, dim) -> + "drawable-$dpi" to dim +}.toMap() + +private val premiumHeaderResourceDirectoryNames = mapOf( + "xxxhdpi" to "516px x 192px", + "xxhdpi" to "387px x 144px", + "xhdpi" to "258px x 96px", + "hdpi" to "194px x 72px", + "mdpi" to "129px x 48px", +).map { (dpi, dim) -> + "drawable-$dpi" to dim +}.toMap() + +private val variants = arrayOf("light", "dark") + +private val headerIconResourceGroups = + premiumHeaderResourceDirectoryNames.keys.map { directory -> + ResourceGroup( + directory, + *variants.map { variant -> "${GENERIC_HEADER_FILE_NAME}_$variant.png" } + .toTypedArray(), + *variants.map { variant -> "${PREMIUM_HEADER_FILE_NAME}_$variant.png" } + .toTypedArray(), + ) + } + +@Suppress("unused") +val changeHeaderPatch = resourcePatch( + CUSTOM_HEADER_FOR_YOUTUBE.title, + CUSTOM_HEADER_FOR_YOUTUBE.summary, + false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + val customHeaderOption = stringOption( + key = "customHeader", + default = DEFAULT_HEADER_VALUE, + values = mapOf( + DEFAULT_HEADER_KEY to DEFAULT_HEADER_VALUE + ), + title = "Custom header", + description = """ + The header to apply to the app. + + Patch option '$DEFAULT_HEADER_KEY' applies only when: + + 1. Patch 'Custom branding icon for YouTube' is included. + 2. Patch option for 'Custom branding icon for YouTube' is selected from the preset. + + If a path to a folder is provided, the folder must contain one or more of the following folders, depending on the DPI of the device: + + ${premiumHeaderResourceDirectoryNames.keys.joinToString("\n") { "- $it" }} + + Each of the folders must contain all of the following files: + + [Generic header] + + ${variants.joinToString("\n") { variant -> "- ${GENERIC_HEADER_FILE_NAME}_$variant.png" }} + + The image dimensions must be as follows: + + ${ + genericHeaderResourceDirectoryNames.map { (dpi, dim) -> "- $dpi: $dim" } + .joinToString("\n") + } + + [Premium header] + + ${variants.joinToString("\n") { variant -> "- ${PREMIUM_HEADER_FILE_NAME}_$variant.png" }} + + The image dimensions must be as follows: + ${ + premiumHeaderResourceDirectoryNames.map { (dpi, dim) -> "- $dpi: $dim" } + .joinToString("\n") + } + """.trimIndentMultiline(), + required = true, + ) + + execute { + // Check patch options first. + var customHeader = customHeaderOption + .underBarOrThrow() + + val isPath = customHeader != DEFAULT_HEADER_VALUE + val customBrandingIconType = getIconType() + val customBrandingIconIncluded = + customBrandingIconType != "default" && customBrandingIconType != "custom" + customHeader = customHeaderOption.valueOrThrow() + + val warnings = "Invalid header path: $customHeader. Does not apply patches." + + if (isPath) { + copyFile( + headerIconResourceGroups, + customHeader, + warnings + ) + } else if (customBrandingIconIncluded) { + headerIconResourceGroups.let { resourceGroups -> + resourceGroups.forEach { + copyResources("youtube/branding/$customBrandingIconType/header", it) + } + } + } else { + printWarn(warnings) + return@execute + } + + // The size of the new header is the same, only the file name is different. + // So if custom headers were used the patch will copy them to the new headers. + mapOf( + PREMIUM_HEADER_FILE_NAME to NEW_PREMIUM_HEADER_FILE_NAME, + GENERIC_HEADER_FILE_NAME to NEW_GENERIC_HEADER_FILE_NAME + ).forEach { (original, replacement) -> + premiumHeaderResourceDirectoryNames.keys.forEach { + get("res").resolve(it).takeIf(File::exists)?.toPath()?.let { path -> + variants.forEach { mode -> + val newHeaderPath = path.resolve("${replacement}_$mode.webp") + + if (newHeaderPath.exists()) { + val fromPath = path.resolve("${original}_$mode.png") + val toPath = path.resolve("${replacement}_$mode.png") + + fromPath.copyTo(toPath, true) + + // If the original file is in webp file format, a compilation error will occur. + // Remove it to prevent compilation errors. + Files.delete(newHeaderPath) + } + } + } + } + } + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/playerbuttonbg/PlayerButtonBackgroundPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/playerbuttonbg/PlayerButtonBackgroundPatch.kt new file mode 100644 index 000000000..323083df6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/playerbuttonbg/PlayerButtonBackgroundPatch.kt @@ -0,0 +1,59 @@ +package app.revanced.patches.youtube.layout.playerbuttonbg + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.FORCE_HIDE_PLAYER_BUTTONS_BACKGROUND +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.doRecursively +import org.w3c.dom.Element + +private const val BACKGROUND = "?ytOverlayBackgroundMediumLight" + +@Suppress("unused") +val playerButtonBackgroundPatch = resourcePatch( + FORCE_HIDE_PLAYER_BUTTONS_BACKGROUND.title, + FORCE_HIDE_PLAYER_BUTTONS_BACKGROUND.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + val backgroundColor by stringOption( + key = "BackgroundColor", + default = BACKGROUND, + values = mapOf( + "Default" to BACKGROUND, + "Transparent" to "@android:color/transparent", + "Opacity10" to "#1a000000", + "Opacity20" to "#33000000", + "Opacity30" to "#4d000000", + "Opacity40" to "#66000000", + "Opacity50" to "#80000000", + "Opacity60" to "#99000000", + "Opacity70" to "#b3000000", + "Opacity80" to "#cc000000", + "Opacity90" to "#e6000000", + "Opacity100" to "#ff000000", + ), + title = "Background color", + description = "Specify a background color for player buttons using a hex color code. The first two symbols of the hex code represent the alpha channel, which is used to change the opacity." + ) + + execute { + document("res/drawable/player_button_circle_background.xml").use { document -> + + document.doRecursively node@{ node -> + if (node !is Element) return@node + + node.getAttributeNode("android:color")?.let { attribute -> + attribute.textContent = backgroundColor + } + } + } + + addPreference(FORCE_HIDE_PLAYER_BUTTONS_BACKGROUND) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortcut/ShortcutPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortcut/ShortcutPatch.kt new file mode 100644 index 000000000..47de5f2d9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortcut/ShortcutPatch.kt @@ -0,0 +1,87 @@ +package app.revanced.patches.youtube.layout.shortcut + +import app.revanced.patcher.patch.booleanOption +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.HIDE_SHORTCUTS +import app.revanced.patches.youtube.utils.playservice.is_19_44_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.findElementByAttributeValueOrThrow +import org.w3c.dom.Element + +@Suppress("unused") +val shortcutPatch = resourcePatch( + HIDE_SHORTCUTS.title, + HIDE_SHORTCUTS.summary, + false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + versionCheckPatch + ) + + val explore = booleanOption( + key = "explore", + default = false, + title = "Hide Explore", + description = "Hide Explore from shortcuts.", + required = true + ) + + val subscriptions = booleanOption( + key = "subscriptions", + default = false, + title = "Hide Subscriptions", + description = "Hide Subscriptions from shortcuts.", + required = true + ) + + val search = booleanOption( + key = "search", + default = false, + title = "Hide Search", + description = "Hide Search from shortcuts.", + required = true + ) + + val shorts = booleanOption( + key = "shorts", + default = true, + title = "Hide Shorts", + description = "Hide Shorts from shortcuts.", + required = true + ) + + execute { + var options = listOf( + subscriptions, + search, + shorts + ) + + if (!is_19_44_or_greater) { + options += explore + } + + options.forEach { option -> + if (option.value == true) { + document("res/xml/main_shortcuts.xml").use { document -> + val shortcuts = document.getElementsByTagName("shortcuts").item(0) as Element + val shortsItem = shortcuts.getElementsByTagName("shortcut") + .findElementByAttributeValueOrThrow( + "android:shortcutId", + "${option.key}-shortcut" + ) + shortsItem.parentNode.removeChild(shortsItem) + } + } + } + + addPreference(HIDE_SHORTCUTS) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/MaterialYouPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/MaterialYouPatch.kt new file mode 100644 index 000000000..9688864a0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/MaterialYouPatch.kt @@ -0,0 +1,146 @@ +package app.revanced.patches.youtube.layout.theme + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.MATERIALYOU +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatusTheme +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.copyXmlNode +import org.w3c.dom.Element +import java.nio.file.Files + +@Suppress("unused") +val materialYouPatch = resourcePatch( + MATERIALYOU.title, + MATERIALYOU.summary, + false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + sharedThemePatch, + settingsPatch, + ) + + execute { + fun patchXmlFile( + fromDir: String, + toDir: String, + xmlFileName: String, + parentNode: String, + targetNode: String? = null, + attribute: String, + newValue: String + ) { + val resourceDirectory = get("res") + val fromDirectory = resourceDirectory.resolve(fromDir) + val toDirectory = resourceDirectory.resolve(toDir) + + if (!toDirectory.isDirectory) Files.createDirectories(toDirectory.toPath()) + + val fromXmlFile = fromDirectory.resolve(xmlFileName) + val toXmlFile = toDirectory.resolve(xmlFileName) + + if (!fromXmlFile.exists()) { + return + } + + if (!toXmlFile.exists()) { + Files.copy( + fromXmlFile.toPath(), + toXmlFile.toPath() + ) + } + + document("res/$toDir/$xmlFileName").use { document -> + val parentList = document.getElementsByTagName(parentNode).item(0) as Element + + if (targetNode != null) { + for (i in 0 until parentList.childNodes.length) { + val node = parentList.childNodes.item(i) as? Element ?: continue + + if (node.nodeName == targetNode && node.hasAttribute(attribute)) { + node.getAttributeNode(attribute).textContent = newValue + } + } + } else { + if (parentList.hasAttribute(attribute)) { + parentList.getAttributeNode(attribute).textContent = newValue + } + } + } + } + + patchXmlFile( + "drawable", + "drawable-night-v31", + "new_content_dot_background.xml", + "shape", + "solid", + "android:color", + "@android:color/system_accent1_100" + ) + patchXmlFile( + "drawable", + "drawable-night-v31", + "new_content_dot_background_cairo.xml", + "shape", + "solid", + "android:color", + "@android:color/system_accent1_100" + ) + patchXmlFile( + "drawable", + "drawable-v31", + "new_content_dot_background.xml", + "shape", + "solid", + "android:color", + "@android:color/system_accent1_200" + ) + patchXmlFile( + "drawable", + "drawable-v31", + "new_content_dot_background_cairo.xml", + "shape", + "solid", + "android:color", + "@android:color/system_accent1_200" + ) + patchXmlFile( + "drawable", + "drawable-v31", + "new_content_count_background.xml", + "shape", + "solid", + "android:color", + "@android:color/system_accent1_100" + ) + patchXmlFile( + "drawable", + "drawable-v31", + "new_content_count_background_cairo.xml", + "shape", + "solid", + "android:color", + "@android:color/system_accent1_100" + ) + patchXmlFile( + "layout", + "layout-v31", + "new_content_count.xml", + "TextView", + null, + "android:textColor", + "@android:color/system_neutral1_900" + ) + + copyXmlNode("youtube/materialyou/host", "values-v31/colors.xml", "resources") + + updatePatchStatusTheme("MaterialYou") + + addPreference(MATERIALYOU) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/SharedThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/SharedThemePatch.kt new file mode 100644 index 000000000..d95f7c786 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/SharedThemePatch.kt @@ -0,0 +1,137 @@ +package app.revanced.patches.youtube.layout.theme + +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.shared.drawable.addDrawableColorHook +import app.revanced.patches.shared.drawable.drawableColorHookPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import org.w3c.dom.Element + +private const val SPLASH_SCREEN_COLOR_NAME = "splashScreenColor" +private const val SPLASH_SCREEN_COLOR_ATTRIBUTE = "?attr/$SPLASH_SCREEN_COLOR_NAME" + +val sharedThemePatch = resourcePatch( + description = "sharedThemePatch" +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(drawableColorHookPatch) + + execute { + addDrawableColorHook("$UTILS_PATH/DrawableColorPatch;->getLithoColor(I)I") + + // edit the resource files to change the splash screen color + val attrsResourceFile = "res/values/attrs.xml" + + document(attrsResourceFile).use { document -> + (document.getElementsByTagName("resources").item(0) as Element).appendChild( + document.createElement("attr").apply { + setAttribute("format", "reference") + setAttribute("name", SPLASH_SCREEN_COLOR_NAME) + } + ) + } + + setOf( + "res/values/styles.xml", + "res/values-v31/styles.xml" + ).forEachIndexed { pathIndex, stylesPath -> + document(stylesPath).use { document -> + val childNodes = + (document.getElementsByTagName("resources").item(0) as Element).childNodes + + for (i in 0 until childNodes.length) { + val node = childNodes.item(i) as? Element ?: continue + val nodeAttributeName = node.getAttribute("name") + + document.createElement("item").apply { + setAttribute( + "name", + when (pathIndex) { + 0 -> "splashScreenColor" + 1 -> "android:windowSplashScreenBackground" + else -> "null" + } + ) + + appendChild( + document.createTextNode( + when (pathIndex) { + 0 -> when (nodeAttributeName) { + "Base.Theme.YouTube.Launcher.Dark" -> "@color/yt_black1" + "Base.Theme.YouTube.Launcher.Light" -> "@color/yt_white1" + else -> "null" + } + + 1 -> when (nodeAttributeName) { + "Base.Theme.YouTube.Launcher", + "Base.Theme.YouTube.Launcher.Cairo" -> SPLASH_SCREEN_COLOR_ATTRIBUTE + + else -> "null" + } + + else -> "null" + } + ) + ) + + if (this.textContent != "null") + node.appendChild(this) + } + } + } + } + + var launchScreenArray = emptyArray() + + document("res/values/styles.xml").use { document -> + val resourcesNode = document.getElementsByTagName("resources").item(0) as Element + val childNodes = resourcesNode.childNodes + + for (i in 0 until childNodes.length) { + val node = childNodes.item(i) as? Element ?: continue + val nodeAttributeName = node.getAttribute("name") + if (nodeAttributeName.startsWith("Base.Theme.YouTube.Launcher")) { + val itemNodes = node.childNodes + + for (j in 0 until itemNodes.length) { + val item = itemNodes.item(j) as? Element ?: continue + + val itemAttributeName = item.getAttribute("name") + if (itemAttributeName == "android:windowBackground" && item.textContent != null) { + launchScreenArray += item.textContent.split("/")[1] + } + } + } + } + } + + launchScreenArray + .distinct() + .forEach { fileName -> + arrayOf("drawable", "drawable-sw600dp").forEach editSplashScreen@{ drawable -> + val targetXmlPath = get("res").resolve(drawable).resolve("$fileName.xml") + if (!targetXmlPath.exists()) { + return@editSplashScreen + } + document("res/$drawable/$fileName.xml").use { document -> + val layerList = + document.getElementsByTagName("layer-list").item(0) as Element + + val childNodes = layerList.childNodes + for (i in 0 until childNodes.length) { + val node = childNodes.item(i) + if (node is Element && node.hasAttribute("android:drawable")) { + node.setAttribute("android:drawable", SPLASH_SCREEN_COLOR_ATTRIBUTE) + return@editSplashScreen + } + } + + throw PatchException("Failed to modify launch screen") + } + } + } + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt new file mode 100644 index 000000000..86ba7a41d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt @@ -0,0 +1,133 @@ +package app.revanced.patches.youtube.layout.theme + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.MATERIALYOU +import app.revanced.patches.youtube.utils.patch.PatchList.THEME +import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatusTheme +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.valueOrThrow +import org.w3c.dom.Element + +@Suppress("unused") +val themePatch = resourcePatch( + THEME.title, + THEME.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + sharedThemePatch, + settingsPatch, + ) + + val amoledBlackColor = "@android:color/black" + val whiteColor = "@android:color/white" + + val availableDarkTheme = mapOf( + "Amoled Black" to amoledBlackColor, + "Classic (Old YouTube)" to "#FF212121", + "Catppuccin (Mocha)" to "#FF181825", + "Dark Pink" to "#FF290025", + "Dark Blue" to "#FF001029", + "Dark Green" to "#FF002905", + "Dark Yellow" to "#FF282900", + "Dark Orange" to "#FF291800", + "Dark Red" to "#FF290000", + ) + + val availableLightTheme = mapOf( + "White" to whiteColor, + "Catppuccin (Latte)" to "#FFE6E9EF", + "Light Pink" to "#FFFCCFF3", + "Light Blue" to "#FFD1E0FF", + "Light Green" to "#FFCCFFCC", + "Light Yellow" to "#FFFDFFCC", + "Light Orange" to "#FFFFE6CC", + "Light Red" to "#FFFFD6D6", + "Pale Blue" to "#FFD4FFF8", + "Pale Green" to "#FFD1FFCC", + "Pale Yellow" to "#FFFFE9AA", + ) + + val darkThemeBackgroundColor = stringOption( + key = "darkThemeBackgroundColor", + default = amoledBlackColor, + values = availableDarkTheme, + title = "Dark theme background color", + description = "Can be a hex color (#AARRGGBB) or a color resource reference.", + ) + + val lightThemeBackgroundColor = stringOption( + key = "lightThemeBackgroundColor", + default = whiteColor, + values = availableLightTheme, + title = "Light theme background color", + description = "Can be a hex color (#AARRGGBB) or a color resource reference.", + ) + + execute { + + // Check patch options first. + val darkThemeColor = darkThemeBackgroundColor + .valueOrThrow() + + val lightThemeColor = lightThemeBackgroundColor + .valueOrThrow() + + arrayOf("values", "values-v31").forEach { path -> + document("res/$path/colors.xml").use { document -> + val resourcesNode = document.getElementsByTagName("resources").item(0) as Element + + for (i in 0 until resourcesNode.childNodes.length) { + val node = resourcesNode.childNodes.item(i) as? Element ?: continue + + node.textContent = when (node.getAttribute("name")) { + "yt_black0", "yt_black1", "yt_black1_opacity95", "yt_black1_opacity98", "yt_black2", "yt_black3", + "yt_black4", "yt_status_bar_background_dark", "material_grey_850" -> darkThemeColor + + else -> continue + } + } + } + } + + document("res/values/colors.xml").use { document -> + val resourcesNode = document.getElementsByTagName("resources").item(0) as Element + + val children = resourcesNode.childNodes + for (i in 0 until children.length) { + val node = children.item(i) as? Element ?: continue + + node.textContent = when (node.getAttribute("name")) { + "yt_white1", "yt_white1_opacity95", "yt_white1_opacity98", + "yt_white2", "yt_white3", "yt_white4", + -> lightThemeColor + + else -> continue + } + } + } + + var darkThemeString = "Custom" + var lightThemeString = "Custom" + availableDarkTheme.forEach { (k, v) -> + if (v == darkThemeColor) darkThemeString = k + } + availableLightTheme.forEach { (k, v) -> + if (v == lightThemeColor) lightThemeString = k + } + val themeString = if (lightThemeColor != whiteColor) + "$lightThemeString + $darkThemeString" + else + darkThemeString + val currentTheme = if (MATERIALYOU.included == true) + "MaterialYou + $themeString" + else + themeString + + updatePatchStatusTheme(currentTheme) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/translations/TranslationsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/translations/TranslationsPatch.kt new file mode 100644 index 000000000..ac8489f75 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/translations/TranslationsPatch.kt @@ -0,0 +1,101 @@ +package app.revanced.patches.youtube.layout.translations + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.shared.translations.APP_LANGUAGES +import app.revanced.patches.shared.translations.baseTranslationsPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.TRANSLATIONS_FOR_YOUTUBE +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.doRecursively +import org.w3c.dom.Element +import org.w3c.dom.Node + +// Array of supported translations, each represented by its language code. +private val SUPPORTED_TRANSLATIONS = setOf( + "ar", "bg-rBG", "de-rDE", "el-rGR", "es-rES", "fr-rFR", "hu-rHU", "it-rIT", "ja-rJP", "ko-rKR", + "pl-rPL", "pt-rBR", "ru-rRU", "tr-rTR", "uk-rUA", "vi-rVN", "zh-rCN", "zh-rTW" +) + +@Suppress("unused") +val translationsPatch = resourcePatch( + TRANSLATIONS_FOR_YOUTUBE.title, + TRANSLATIONS_FOR_YOUTUBE.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + val customTranslations by stringOption( + key = "customTranslations", + default = "", + title = "Custom translations", + description = """ + The path to the 'strings.xml' file. + Please note that applying the 'strings.xml' file will overwrite all existing translations. + """.trimIndent(), + required = true, + ) + + val selectedTranslations by stringOption( + key = "selectedTranslations", + default = SUPPORTED_TRANSLATIONS.joinToString(", "), + title = "Translations to add", + description = "A list of translations to be added for the RVX settings, separated by commas.", + required = true, + ) + + val selectedStringResources by stringOption( + key = "selectedStringResources", + default = APP_LANGUAGES.joinToString(", "), + title = "String resources to keep", + description = """ + A list of string resources to be kept, separated by commas. + String resources not in the list will be removed from the app. + + Default string resource, English, is not removed. + """.trimIndent(), + required = true, + ) + + execute { + baseTranslationsPatch( + customTranslations, selectedTranslations, selectedStringResources, + SUPPORTED_TRANSLATIONS, "youtube" + ) + + // Process selected app languages + val selectedAppLanguagesArray = selectedStringResources!!.split(",").map { it.trim() }.toTypedArray() + + // Filter the app languages to include both versions of locales (with and without 'r', en-rGB and en-GB) + // and also handle locales with "b+" prefix + val filteredAppLanguages = selectedAppLanguagesArray.flatMap { language -> + setOf(language, language.replace("-r", "-"), + language.replace("b+", "").replace("+", "-")) + }.toTypedArray() + + // Remove unselected app languages from UI + document("res/xml/locales_config.xml").use { document -> + val nodesToRemove = mutableListOf() + + document.doRecursively loop@{ node -> + if (node !is Element || node.tagName != "locale") return@loop + + node.getAttributeNode("android:name")?.let { attribute -> + if (attribute.textContent != "en" && attribute.textContent !in filteredAppLanguages) { + nodesToRemove.add(node) + } + } + } + + // Remove the collected nodes (avoids NullPointerException) + for (node in nodesToRemove) { + node.parentNode?.removeChild(node) + } + } + + addPreference(TRANSLATIONS_FOR_YOUTUBE) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/visual/VisualPreferencesIconsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/visual/VisualPreferencesIconsPatch.kt new file mode 100644 index 000000000..80f5616d2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/visual/VisualPreferencesIconsPatch.kt @@ -0,0 +1,406 @@ +package app.revanced.patches.youtube.layout.visual + +import app.revanced.patcher.patch.booleanOption +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.youtube.layout.branding.icon.customBrandingIconPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.VISUAL_PREFERENCES_ICONS_FOR_YOUTUBE +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.Utils.trimIndentMultiline +import app.revanced.util.copyResources +import app.revanced.util.copyResourcesWithRename +import app.revanced.util.doRecursively +import app.revanced.util.getStringOptionValue +import app.revanced.util.underBarOrThrow +import org.w3c.dom.Element + +private const val DEFAULT_ICON = "extension" +private const val EMPTY_ICON = "empty_icon" + +@Suppress("unused") +val visualPreferencesIconsPatch = resourcePatch( + VISUAL_PREFERENCES_ICONS_FOR_YOUTUBE.title, + VISUAL_PREFERENCES_ICONS_FOR_YOUTUBE.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + val settingsMenuIconOption = stringOption( + key = "settingsMenuIcon", + default = DEFAULT_ICON, + values = mapOf( + "Custom branding icon" to "custom_branding_icon", + "Extension" to DEFAULT_ICON, + "Gear" to "gear", + "YT alt" to "yt_alt", + "ReVanced" to "revanced", + "ReVanced Colored" to "revanced_colored", + "RVX Letters" to "rvx_letters", + "RVX Letters Bold" to "rvx_letters_bold", + ), + title = "RVX settings menu icon", + description = "The icon for the RVX settings menu.", + required = true, + ) + + val applyToAll by booleanOption( + key = "applyToAll", + default = false, + title = "Apply to all settings menu", + description = """ + Whether to apply Visual preferences icons to all settings menus. + + If true: icons are applied to the parent PreferenceScreen of YouTube settings, the parent PreferenceScreen of RVX settings and the RVX sub-settings (if supported). + + If false: icons are applied only to the parent PreferenceScreen of YouTube settings and RVX settings. + """.trimIndentMultiline(), + required = true + ) + + execute { + // Check patch options first. + val selectedIconType = settingsMenuIconOption + .underBarOrThrow() + + val appIconOption = customBrandingIconPatch + .getStringOptionValue("appIcon") + + val customBrandingIconType = appIconOption + .underBarOrThrow() + + if (applyToAll == true) { + preferenceKey.putAll(rvxPreferenceKey) + } + + // region copy shared resources. + + copyResourcesWithRename("youtube/visual/icons", preferenceKey) + + arrayOf( + ResourceGroup( + "drawable-xxhdpi", + "$EMPTY_ICON.png" + ), + ).forEach { resourceGroup -> + copyResources("youtube/visual/icons", resourceGroup) + } + + // endregion. + + // region copy RVX settings menu icon. + + val fallbackIconPath = "youtube/visual/icons/extension" + val iconPath = when (selectedIconType) { + "custom_branding_icon" -> "youtube/branding/$customBrandingIconType/settings" + else -> "youtube/visual/icons/$selectedIconType" + } + val resourceGroup = ResourceGroup( + "drawable", + "revanced_extended_settings_key_icon.xml" + ) + + try { + copyResources(iconPath, resourceGroup) + } catch (_: Exception) { + // Ignore if resource copy fails + + // Add a fallback extended icon + // It's needed if someone provides custom path to icon(s) folder + // but custom branding icons for Extended setting are predefined, + // so it won't copy custom branding icon + // and will raise an error without fallback icon + copyResources(fallbackIconPath, resourceGroup) + } + + // endregion. + + addPreference(VISUAL_PREFERENCES_ICONS_FOR_YOUTUBE) + + } + + finalize { + // region set visual preferences icon. + + arrayOf( + "res/xml/revanced_prefs.xml", + "res/xml/settings_fragment.xml" + ).forEach { xmlFile -> + document(xmlFile).use { document -> + document.doRecursively loop@{ node -> + if (node !is Element) return@loop + + node.getAttributeNode("android:key") + ?.textContent + ?.removePrefix("@string/") + ?.let { title -> + val drawableName = when (title) { + in preferenceKey.keys -> { + val pathData = preferenceKey[title] + + // If pathData is another title then use it as an icon title + if (preferenceKey.containsKey(pathData)) { + pathData + } else { + title + } + } + + // Add custom RVX settings menu icon + in intentKey -> intentIcon[title] + in emptyTitles -> EMPTY_ICON + else -> null + } + if (drawableName == EMPTY_ICON && + applyToAll == false + ) return@loop + + drawableName?.let { + node.setAttribute("android:icon", "@drawable/$it") + } + } + } + } + } + + // endregion. + } +} + +private var preferenceKey = mutableMapOf( + // YouTube settings + "parent_tools_key" to "M 677.462 535.154 Q 645.289 535.154 622.645 512.509 Q 600 489.865 600 457.692 Q 600 425.52 622.768 402.875 Q 645.535 380.231 677.077 380.231 Q 709.634 380.231 731.894 402.999 Q 754.154 425.766 754.154 457.308 Q 754.154 489.865 731.894 512.509 Q 709.634 535.154 677.462 535.154 Z M 498.769 744.616 L 498.769 710.539 Q 498.769 691.462 507.333 677.169 Q 515.897 662.877 532.308 656 Q 566.333 640.077 602.359 632.231 Q 638.385 624.385 677.462 624.385 Q 714.98 624.385 751.067 632.115 Q 787.154 639.846 822.615 656 Q 838.192 662.923 846.789 677.192 Q 855.385 691.462 855.385 710.539 L 855.385 744.616 L 498.769 744.616 Z M 384.615 455.154 Q 335.115 455.154 302.173 422.212 Q 269.231 389.269 269.231 339.385 Q 269.231 289.5 302.173 256.942 Q 335.115 224.384 384.615 224.384 Q 434.116 224.384 467.058 256.942 Q 500 289.5 500 339.385 Q 500 389.269 467.058 422.212 Q 434.116 455.154 384.615 455.154 Z M 384.615 339.769 Z M 104.615 744.616 L 104.615 686.769 Q 104.615 660.817 118.923 639.062 Q 133.231 617.308 159.205 606.923 Q 216.538 580.769 271.851 567.308 Q 327.164 553.846 384.315 553.846 Q 407.385 553.846 427 554.923 Q 446.616 556 467.923 559.692 Q 461.327 565.904 454.731 573.269 Q 448.135 580.635 441.539 586.846 Q 428.539 586 414.308 585.308 Q 400.077 584.615 384.615 584.615 Q 331.042 584.615 278.79 595.346 Q 226.539 606.077 173.538 634 Q 158.769 641.769 147.077 655.616 Q 135.385 669.462 135.385 686.769 L 135.385 713.846 L 397.231 713.846 L 397.231 744.616 L 104.615 744.616 Z M 397.231 713.846 Z M 384.615 424.385 Q 420.539 424.385 444.885 400.038 Q 469.231 375.692 469.231 339.769 Q 469.231 303.846 444.885 279.5 Q 420.539 255.154 384.615 255.154 Q 348.692 255.154 324.346 279.5 Q 300 303.846 300 339.769 Q 300 375.692 324.346 400.038 Q 348.692 424.385 384.615 424.385 Z", + "general_key" to "M 702.308 766.923 Q 652.877 766.923 618.746 732.793 Q 584.615 698.662 584.615 649.231 Q 584.615 599.8 618.746 565.669 Q 652.877 531.538 702.308 531.538 Q 751.739 531.538 785.869 565.669 Q 820 599.8 820 649.231 Q 820 698.662 785.869 732.793 Q 751.739 766.923 702.308 766.923 Z M 702.121 736.154 Q 738.769 736.154 764 711.11 Q 789.231 686.065 789.231 649.417 Q 789.231 612.769 764.187 587.539 Q 739.142 562.308 702.494 562.308 Q 665.846 562.308 640.615 587.352 Q 615.385 612.396 615.385 649.044 Q 615.385 685.692 640.429 710.923 Q 665.473 736.154 702.121 736.154 Z M 181.538 664.616 L 181.538 633.846 L 489.231 633.846 L 489.231 664.616 L 181.538 664.616 Z M 257.692 428.462 Q 208.261 428.462 174.131 394.331 Q 140 360.2 140 310.769 Q 140 261.338 174.131 227.207 Q 208.261 193.077 257.692 193.077 Q 307.123 193.077 341.254 227.207 Q 375.385 261.338 375.385 310.769 Q 375.385 360.2 341.254 394.331 Q 307.123 428.462 257.692 428.462 Z M 257.506 397.692 Q 294.154 397.692 319.385 372.648 Q 344.615 347.604 344.615 310.956 Q 344.615 274.308 319.571 249.077 Q 294.527 223.846 257.879 223.846 Q 221.231 223.846 196 248.89 Q 170.769 273.935 170.769 310.583 Q 170.769 347.231 195.813 372.461 Q 220.858 397.692 257.506 397.692 Z M 470.769 326.154 L 470.769 295.384 L 778.462 295.384 L 778.462 326.154 L 470.769 326.154 Z M 702.308 649.231 Z M 257.692 310.769 Z", + "account_switcher_key" to "M 480 455.154 Q 430.5 455.154 397.558 422.212 Q 364.615 389.269 364.615 339.385 Q 364.615 289.5 397.558 256.942 Q 430.5 224.384 480 224.384 Q 529.5 224.384 562.443 256.942 Q 595.385 289.5 595.385 339.385 Q 595.385 389.269 562.443 422.212 Q 529.5 455.154 480 455.154 Z M 200 744.616 L 200 686.769 Q 200 660.308 215.154 639.462 Q 230.307 618.615 254.923 606.923 Q 314.231 580.769 369.938 567.308 Q 425.645 553.846 479.976 553.846 Q 534.308 553.846 589.923 567.423 Q 645.539 581 704.425 607.274 Q 729.872 618.771 744.936 639.54 Q 760 660.308 760 686.769 L 760 744.616 L 200 744.616 Z M 230.769 713.846 L 729.231 713.846 L 729.231 686.769 Q 729.231 671.539 718.962 657.423 Q 708.692 643.308 690.846 634 Q 636.846 607.615 585.241 596.115 Q 533.636 584.615 480 584.615 Q 426.364 584.615 374.144 596.115 Q 321.923 607.615 268.923 634 Q 251.077 643.308 240.923 657.423 Q 230.769 671.539 230.769 686.769 L 230.769 713.846 Z M 480 424.385 Q 515.923 424.385 540.269 400.038 Q 564.615 375.692 564.615 339.769 Q 564.615 303.846 540.269 279.5 Q 515.923 255.154 480 255.154 Q 444.077 255.154 419.731 279.5 Q 395.385 303.846 395.385 339.769 Q 395.385 375.692 419.731 400.038 Q 444.077 424.385 480 424.385 Z M 480 339.769 Z M 480 713.846 Z", + "data_saving_settings_key" to "M 120 840 L 840 120 L 840 840 L 120 840 Z M 374.462 809.231 L 809.231 809.231 L 809.231 194 L 374.462 628.769 L 374.462 809.231 Z", + "auto_play_key" to "M 404.615 615.077 L 404.615 344.923 L 615.077 480 L 404.615 615.077 Z M 483.154 880 Q 360.615 880 264.077 816.962 Q 167.538 753.923 110.769 637.616 L 110.769 799.769 L 79.999 799.769 L 79.999 584.615 L 293.385 584.615 L 293.385 615.385 L 134.769 615.385 Q 183.308 724.231 276 786.731 Q 368.692 849.231 483.154 849.231 Q 604.923 849.231 701.154 776.423 Q 797.385 703.615 832.308 586.308 L 862.846 592.385 Q 826 721.462 721.346 800.731 Q 616.692 880.001 483.154 880.001 Z M 82 440 Q 89.769 377.385 110.154 328.423 Q 130.538 279.461 169.692 227.461 L 192.692 248.923 Q 159.231 293.154 140.923 336.615 Q 122.615 380.077 112.769 440 L 81.999 440 Z M 248.692 193.692 L 227.461 170.461 Q 276.077 132.615 330.154 111.077 Q 384.231 89.538 441.231 83.538 L 441.231 114.308 Q 392.308 119.308 343.308 139.038 Q 294.308 158.769 248.692 193.692 Z M 709.769 193.692 Q 671.692 161.538 619.769 140.038 Q 567.846 118.538 520 114.308 L 520 83.538 Q 578 88.769 631.192 111.077 Q 684.385 133.384 731.769 171.231 L 709.769 193.692 Z M 845.692 440 Q 839.154 385.154 818.462 336.615 Q 797.769 288.077 763.308 248.461 L 785.308 226 Q 824.385 273.077 847.539 327.115 Q 870.693 381.154 876.462 440 L 845.692 440 Z", + "video_quality_settings_key" to "M 592.692 651.077 L 623.462 651.077 L 623.462 589.154 L 655.231 589.154 Q 671.758 589.154 683.571 577.407 Q 695.385 565.66 695.385 549.231 L 695.385 411 Q 695.385 394.473 683.571 382.66 Q 671.758 370.846 655.231 370.846 L 563.077 370.846 Q 546.769 370.846 533.385 382.66 Q 520 394.473 520 411 L 520 549.231 Q 520 565.66 533.385 577.407 Q 546.769 589.154 563.077 589.154 L 592.692 589.154 L 592.692 651.077 Z M 264.615 589.154 L 295.385 589.154 L 295.385 504.769 L 409.231 504.769 L 409.231 589.154 L 440 589.154 L 440 370.846 L 409.231 370.846 L 409.231 474 L 295.385 474 L 295.385 370.846 L 264.615 370.846 L 264.615 589.154 Z M 563.077 558.385 Q 558.462 558.385 554.615 554.538 Q 550.769 550.692 550.769 546.077 L 550.769 413.923 Q 550.769 409.308 554.615 405.462 Q 558.462 401.615 563.077 401.615 L 652.308 401.615 Q 656.923 401.615 660.769 405.462 Q 664.615 409.308 664.615 413.923 L 664.615 546.077 Q 664.615 550.692 660.769 554.538 Q 656.923 558.385 652.308 558.385 L 563.077 558.385 Z M 175.384 760 Q 152.327 760 136.163 743.837 Q 120 727.673 120 704.616 L 120 255.384 Q 120 232.327 136.163 216.163 Q 152.327 200 175.384 200 L 784.616 200 Q 807.673 200 823.837 216.163 Q 840 232.327 840 255.384 L 840 704.616 Q 840 727.673 823.837 743.837 Q 807.673 760 784.616 760 L 175.384 760 Z M 175.384 729.231 L 784.616 729.231 Q 793.846 729.231 801.539 721.539 Q 809.231 713.846 809.231 704.616 L 809.231 255.384 Q 809.231 246.154 801.539 238.461 Q 793.846 230.769 784.616 230.769 L 175.384 230.769 Q 166.154 230.769 158.461 238.461 Q 150.769 246.154 150.769 255.384 L 150.769 704.616 Q 150.769 713.846 158.461 721.539 Q 166.154 729.231 175.384 729.231 Z M 150.769 729.231 L 150.769 230.769 L 150.769 729.231 Z", + "offline_key" to "M 120 424.615 L 120 393.846 L 292.615 393.846 L 57.769 160 L 80 137 L 313.846 371.615 L 313.846 200 L 344.615 200 L 344.615 424.615 L 120 424.615 Z M 175.384 760 Q 152.327 760 136.163 743.837 Q 120 727.673 120 704.616 L 120 515.385 L 150.769 515.385 L 150.769 704.616 Q 150.769 715.385 157.692 722.308 Q 164.615 729.231 175.384 729.231 L 470.769 729.231 L 470.769 760 L 175.384 760 Z M 809.231 510.769 L 809.231 255.384 Q 809.231 244.615 802.308 237.692 Q 795.385 230.769 784.616 230.769 L 435.385 230.769 L 435.385 200 L 784.616 200 Q 807.673 200 823.837 216.163 Q 840 232.327 840 255.384 L 840 510.769 L 809.231 510.769 Z M 556.923 760 L 556.923 596.923 L 840 596.923 L 840 760 L 556.923 760 Z", + "pair_with_tv_key" to "M 364.615 800 L 364.615 720 L 175.384 720 Q 152.327 720 136.163 703.837 Q 120 687.673 120 664.616 L 120 215.384 Q 120 192.327 136.163 176.163 Q 152.327 160 175.384 160 L 784.616 160 Q 807.673 160 823.837 176.163 Q 840 192.327 840 215.384 L 840 664.616 Q 840 687.673 823.837 703.837 Q 807.673 720 784.616 720 L 595.385 720 L 595.385 800 L 364.615 800 Z M 175.384 689.231 L 784.616 689.231 Q 793.846 689.231 801.539 681.539 Q 809.231 673.846 809.231 664.616 L 809.231 215.384 Q 809.231 206.154 801.539 198.461 Q 793.846 190.769 784.616 190.769 L 175.384 190.769 Q 166.154 190.769 158.461 198.461 Q 150.769 206.154 150.769 215.384 L 150.769 664.616 Q 150.769 673.846 158.461 681.539 Q 166.154 689.231 175.384 689.231 Z M 150.769 689.231 L 150.769 190.769 L 150.769 689.231 Z", + "history_key" to "M 477 800 Q 350.308 800 259 714.116 Q 167.692 628.231 160 501.308 L 190.769 501.308 Q 200 614 281.385 691.615 Q 362.769 769.231 477 769.231 Q 598.615 769.231 683.154 684.077 Q 767.692 598.923 767.692 477.308 Q 767.692 357.154 682.923 273.961 Q 598.154 190.769 477 190.769 Q 415.923 190.769 361.038 216.385 Q 306.154 242 263.692 286.077 L 360.231 286.077 L 360.231 316.846 L 208.923 316.846 L 208.923 165 L 239.692 165 L 239.692 266.385 Q 286.307 216.923 347.808 188.461 Q 409.308 160 477 160 Q 543.539 160 601.846 184.923 Q 660.154 209.846 703.885 253.346 Q 747.616 296.846 773.039 354.923 Q 798.462 413 798.462 478.769 Q 798.462 545.308 773.039 604 Q 747.616 662.692 703.885 705.923 Q 660.154 749.154 601.846 774.577 Q 543.539 800 477 800 Z M 612.692 631.462 L 465.615 485.615 L 465.615 278.538 L 496.385 278.538 L 496.385 472.923 L 634.923 609.231 L 612.692 631.462 Z", + "your_data_key" to "M 480 800 Q 339.769 800 249.884 764.269 Q 160 728.539 160 672.308 L 160 280 Q 160 230.154 253.577 195.077 Q 347.154 160 480 160 Q 612.846 160 706.423 195.077 Q 800 230.154 800 280 L 800 672.308 Q 800 728.539 710.116 764.269 Q 620.231 800 480 800 Z M 480 351.231 Q 564.462 351.231 652.269 327.038 Q 740.077 302.846 763.769 272.154 Q 739.846 240.923 652.808 215.846 Q 565.769 190.769 480 190.769 Q 394.308 190.769 306.077 214.846 Q 217.846 238.923 194.461 269.077 Q 217.077 301.769 303.923 326.5 Q 390.769 351.231 480 351.231 Z M 479.769 559.846 Q 521 559.846 562.231 555.346 Q 603.462 550.846 640.808 542.231 Q 678.154 533.615 711.231 521 Q 744.308 508.385 769.231 493.154 L 769.231 312.769 Q 743.308 328.769 710.615 341.385 Q 677.923 354 640.077 362.616 Q 602.231 371.231 561.615 375.846 Q 521 380.462 479.769 380.462 Q 437 380.462 395.385 375.462 Q 353.769 370.462 316.423 361.846 Q 279.077 353.231 247.269 341 Q 215.461 328.769 190.769 312.769 L 190.769 493.154 Q 214.692 508.385 246.769 520.615 Q 278.846 532.846 316.192 541.846 Q 353.538 550.846 394.769 555.346 Q 436 559.846 479.769 559.846 Z M 480 769.231 Q 532.615 769.231 580.538 762.654 Q 628.462 756.077 666.731 744.385 Q 705 732.692 731.808 716.462 Q 758.615 700.231 769.231 681.462 L 769.231 524.154 Q 744.308 540.154 711.231 552.269 Q 678.154 564.385 640.808 573 Q 603.462 581.616 562.346 586.116 Q 521.231 590.616 479.769 590.616 Q 436 590.616 394.769 586.116 Q 353.538 581.616 316.192 573 Q 278.846 564.385 247.154 551.885 Q 215.461 539.385 190.769 524.154 L 190.769 681.692 Q 201.154 701 227.846 716.962 Q 254.538 732.923 292.923 744.5 Q 331.308 756.077 379.231 762.654 Q 427.154 769.231 480 769.231 Z", + "privacy_key" to "M 684.729 684.615 Q 710.29 684.615 728.184 666.249 Q 746.077 647.882 746.077 622.905 Q 746.077 597.928 727.959 579.81 Q 709.842 561.692 684.864 561.692 Q 659.887 561.692 641.52 579.493 Q 623.154 597.294 623.154 622.724 Q 623.154 648.154 641.481 666.385 Q 659.808 684.615 684.729 684.615 Z M 684.115 806.539 Q 716.846 806.539 743.462 792.923 Q 770.077 779.308 787.539 754.077 Q 763.077 740.077 737.615 733.077 Q 712.153 726.077 684.654 726.077 Q 657.155 726.077 631.154 733.077 Q 605.154 740.077 581.692 754.077 Q 599.154 779.308 625.269 792.923 Q 651.385 806.539 684.115 806.539 Z M 480 838.462 Q 360.461 803.385 280.231 693.5 Q 200 583.615 200 441.077 L 200 227.461 L 480 122.846 L 760 227.461 L 760 473.615 Q 752.923 471.231 744.231 468.423 Q 735.539 465.615 729.231 464.385 L 729.231 247.923 L 480 156.538 L 230.769 247.923 L 230.769 441.077 Q 230.769 514 253.731 576.077 Q 276.692 638.154 314.308 686.654 Q 351.923 735.154 399.077 767.538 Q 446.231 799.923 495.385 812.385 L 497.692 811.615 Q 500.615 814.385 504.923 819.385 Q 509.231 824.385 511.846 827 Q 503.615 831.231 495.538 833.731 Q 487.462 836.231 480 838.462 Z M 685.947 840 Q 621.893 840 576.908 794.885 Q 531.923 749.769 531.923 686.077 Q 531.923 621.242 576.898 576.082 Q 621.873 530.923 686.447 530.923 Q 750 530.923 795.116 576.082 Q 840.231 621.242 840.231 686.077 Q 840.231 749.769 795.116 794.885 Q 750 840 685.947 840 Z M 480 484.077 Z", + "premium_early_access_browse_page_key" to "M 395.769 531.077 L 427.923 427.313 L 344.461 367.461 L 447.169 367.461 L 480 260 L 511.831 367.461 L 615.539 367.461 L 532.846 427.313 L 564 531.077 L 479.996 467 L 395.769 531.077 Z M 281.692 858.462 L 281.692 596.769 Q 240.538 557.462 220.269 506.077 Q 200 454.692 200 400 Q 200 282.461 281.231 201.231 Q 362.461 120 480 120 Q 597.539 120 678.769 201.231 Q 760 282.461 760 400 Q 760 454.692 739.731 506.077 Q 719.462 557.462 678.308 596.769 L 678.308 858.462 L 480 798.665 L 281.692 858.462 Z M 479.91 649.231 Q 584.385 649.231 656.808 576.898 Q 729.231 504.566 729.231 400.091 Q 729.231 295.615 656.898 223.192 Q 584.566 150.769 480.09 150.769 Q 375.615 150.769 303.192 223.102 Q 230.769 295.434 230.769 399.909 Q 230.769 504.385 303.102 576.808 Q 375.434 649.231 479.91 649.231 Z M 312.461 817.539 L 480 766.385 L 647.539 817.539 L 647.539 622.693 Q 611.385 651.693 568.077 665.846 Q 524.769 680 480 680 Q 435.231 680 391.923 665.846 Q 348.615 651.693 312.461 622.693 L 312.461 817.539 Z M 480 720 Z", + "subscription_product_setting_key" to "M 255.384 840 Q 232.327 840 216.163 823.837 Q 200 807.673 200 784.616 L 200 335.384 Q 200 312.327 216.163 296.163 Q 232.327 280 255.384 280 L 344.615 280 L 344.615 255.384 Q 344.615 198.538 383.885 159.269 Q 423.154 120 480 120 Q 536.846 120 576.115 159.269 Q 615.385 198.538 615.385 255.384 L 615.385 280 L 704.616 280 Q 727.673 280 743.837 296.163 Q 760 312.327 760 335.384 L 760 784.616 Q 760 807.673 743.837 823.837 Q 727.673 840 704.616 840 L 255.384 840 Z M 255.384 809.231 L 704.616 809.231 Q 713.846 809.231 721.539 801.539 Q 729.231 793.846 729.231 784.616 L 729.231 335.384 Q 729.231 326.154 721.539 318.461 Q 713.846 310.769 704.616 310.769 L 615.385 310.769 L 615.385 415.384 Q 615.385 421.961 610.927 426.365 Q 606.468 430.769 599.811 430.769 Q 593.154 430.769 588.885 426.365 Q 584.615 421.961 584.615 415.384 L 584.615 310.769 L 375.385 310.769 L 375.385 415.384 Q 375.385 421.961 370.927 426.365 Q 366.468 430.769 359.811 430.769 Q 353.154 430.769 348.885 426.365 Q 344.615 421.961 344.615 415.384 L 344.615 310.769 L 255.384 310.769 Q 246.154 310.769 238.461 318.461 Q 230.769 326.154 230.769 335.384 L 230.769 784.616 Q 230.769 793.846 238.461 801.539 Q 246.154 809.231 255.384 809.231 Z M 375.385 280 L 584.615 280 L 584.615 255.384 Q 584.615 211.231 554.385 181 Q 524.154 150.769 480 150.769 Q 435.846 150.769 405.615 181 Q 375.385 211.231 375.385 255.384 L 375.385 280 Z M 230.769 809.231 L 230.769 310.769 L 230.769 809.231 Z", + "billing_and_payment_key" to "M 462.538 747.769 L 495.231 747.769 L 495.231 699.615 Q 543.154 698 581.769 670.192 Q 620.385 642.385 620.385 588.308 Q 620.385 541.923 592.539 513.769 Q 564.692 485.615 494.154 458.154 Q 428.462 433.385 408.615 415.154 Q 388.769 396.923 388.769 363.385 Q 388.769 330.846 414.731 309 Q 440.692 287.154 482 287.154 Q 512 287.154 534 300.385 Q 556 313.615 571.769 337 L 599 324.769 Q 582 295.923 555.539 279 Q 529.077 262.077 497.231 259.615 L 497.231 212.461 L 464.538 212.461 L 464.538 259.615 Q 412 266.615 384.423 295.961 Q 356.846 325.308 356.846 363.385 Q 356.846 407.769 384.923 433.692 Q 413 459.615 478.385 484.615 Q 544.615 511.077 566.923 532.039 Q 589.231 553 589.231 588.308 Q 589.231 630.308 557.346 650.577 Q 525.462 670.846 486.231 670.846 Q 449.538 670.846 419.423 650.385 Q 389.308 629.923 372.692 593.461 L 344 605.077 Q 364.231 644.923 393.577 666.423 Q 422.923 687.923 462.538 697.615 L 462.538 747.769 Z M 480 840 Q 405.692 840 340.385 811.577 Q 275.077 783.154 225.961 734.039 Q 176.846 684.923 148.423 619.615 Q 120 554.308 120 480 Q 120 405.461 148.423 339.769 Q 176.846 274.077 225.961 225.461 Q 275.077 176.846 340.385 148.423 Q 405.692 120 480 120 Q 554.539 120 620.231 148.423 Q 685.923 176.846 734.539 225.461 Q 783.154 274.077 811.577 339.769 Q 840 405.461 840 480 Q 840 554.308 811.577 619.615 Q 783.154 684.923 734.539 734.039 Q 685.923 783.154 620.231 811.577 Q 554.539 840 480 840 Z M 480 809.231 Q 617.385 809.231 713.308 713.192 Q 809.231 617.154 809.231 480 Q 809.231 342.615 713.308 246.692 Q 617.385 150.769 480 150.769 Q 342.846 150.769 246.808 246.692 Q 150.769 342.615 150.769 480 Q 150.769 617.154 246.808 713.192 Q 342.846 809.231 480 809.231 Z M 480 480 Z", + "notification_key" to "M 200 750.769 L 200 720 L 264.615 720 L 264.615 392.154 Q 264.615 313.673 313.731 252.875 Q 362.846 192.077 440 178.538 L 440 160 Q 440 143.333 451.64 131.666 Q 463.28 120 479.91 120 Q 496.539 120 508.269 131.666 Q 520 143.333 520 160 L 520 178.923 Q 597.154 192.077 646.269 252.875 Q 695.385 313.673 695.385 392.154 L 695.385 720 L 760 720 L 760 750.769 L 200 750.769 Z M 480 463.385 Z M 479.864 855.385 Q 453.154 855.385 434.269 836.404 Q 415.385 817.423 415.385 790.769 L 544.615 790.769 Q 544.615 817.615 525.595 836.5 Q 506.574 855.385 479.864 855.385 Z M 295.385 720 L 664.615 720 L 664.615 392.154 Q 664.615 315.615 610.577 261.577 Q 556.538 207.539 480 207.539 Q 403.462 207.539 349.423 261.577 Q 295.385 315.615 295.385 392.154 L 295.385 720 Z", + "connected_accounts_browse_page_key" to "M 233.692 769.231 Q 216.077 769.231 203.423 756.577 Q 190.769 743.923 190.769 726.308 Q 190.769 708.692 203.423 696.038 Q 216.077 683.385 233.692 683.385 Q 251.308 683.385 263.962 696.038 Q 276.615 708.692 276.615 726.308 Q 276.615 743.923 263.962 756.577 Q 251.308 769.231 233.692 769.231 Z M 233.692 522.923 Q 216.077 522.923 203.423 510.269 Q 190.769 497.615 190.769 480 Q 190.769 462.385 203.423 449.731 Q 216.077 437.077 233.692 437.077 Q 251.308 437.077 263.962 449.731 Q 276.615 462.385 276.615 480 Q 276.615 497.615 263.962 510.269 Q 251.308 522.923 233.692 522.923 Z M 233.692 276.615 Q 216.077 276.615 203.423 263.962 Q 190.769 251.308 190.769 233.692 Q 190.769 216.077 203.423 203.423 Q 216.077 190.769 233.692 190.769 Q 251.308 190.769 263.962 203.423 Q 276.615 216.077 276.615 233.692 Q 276.615 251.308 263.962 263.962 Q 251.308 276.615 233.692 276.615 Z M 480 769.231 Q 462.385 769.231 449.731 756.577 Q 437.077 743.923 437.077 726.308 Q 437.077 708.692 449.731 696.038 Q 462.385 683.385 480 683.385 Q 497.615 683.385 510.269 696.038 Q 522.923 708.692 522.923 726.308 Q 522.923 743.923 510.269 756.577 Q 497.615 769.231 480 769.231 Z M 480 522.923 Q 462.385 522.923 449.731 510.269 Q 437.077 497.615 437.077 480 Q 437.077 462.385 449.731 449.731 Q 462.385 437.077 480 437.077 Q 497.615 437.077 510.269 449.731 Q 522.923 462.385 522.923 480 Q 522.923 497.615 510.269 510.269 Q 497.615 522.923 480 522.923 Z M 480 276.615 Q 462.385 276.615 449.731 264.04 Q 437.077 251.465 437.077 233.96 Q 437.077 215.692 449.163 203.231 Q 461.249 190.769 478.462 190.769 Q 481.385 190.769 483.923 191.384 Q 486.462 192 489.154 192.461 Q 488.692 194.538 488.577 196.231 Q 488.462 197.923 488.462 200 Q 488.462 219.171 490.039 236.855 Q 491.615 254.539 495.539 272.231 Q 491.385 274.923 487.439 275.769 Q 483.494 276.615 480 276.615 Z M 750 370 Q 683.154 370 636.577 323.423 Q 590 276.846 590 210 Q 590 143.154 636.577 96.577 Q 683.154 50 750 50 Q 816.846 50 863.423 96.577 Q 910 143.154 910 210 Q 910 276.846 863.423 323.423 Q 816.846 370 750 370 Z M 726.308 769.231 Q 708.692 769.231 696.038 756.577 Q 683.385 743.923 683.385 726.308 Q 683.385 708.692 696.038 696.038 Q 708.692 683.385 726.308 683.385 Q 743.923 683.385 756.577 696.038 Q 769.231 708.692 769.231 726.308 Q 769.231 743.923 756.577 756.577 Q 743.923 769.231 726.308 769.231 Z M 726.035 522.923 Q 708.308 522.923 695.846 510.269 Q 683.385 497.615 683.385 480 Q 683.385 476.506 684.231 472.176 Q 685.077 467.846 687.769 464.461 Q 705.461 468.385 723.145 469.961 Q 740.829 471.538 760 471.538 Q 762.077 471.538 763.769 471.423 Q 765.462 471.308 767.539 470.846 Q 768 473.538 768.616 476.077 Q 769.231 478.615 769.231 481.538 Q 769.231 498.751 756.885 510.837 Q 744.539 522.923 726.035 522.923 Z M 750 308.462 Q 758 308.462 763.231 303.231 Q 768.462 298 768.462 290 Q 768.462 282 763.231 276.769 Q 758 271.539 750 271.539 Q 742 271.539 736.769 276.769 Q 731.539 282 731.539 290 Q 731.539 298 736.769 303.231 Q 742 308.462 750 308.462 Z M 734.615 234.615 L 765.385 234.615 L 765.385 114.615 L 734.615 114.615 L 734.615 234.615 Z", + "live_chat_key" to "M 294.399 719.567 L 728.667 719.567 L 789.297 795.635 L 789.297 243.441 C 789.298 243.359 789.298 243.303 789.299 243.266 L 787.391 243.22 L 787.39 243.275 L 787.375 243.275 L 787.375 242.273 L 789.282 242.237 C 789.282 242.237 789.283 242.124 789.283 242.258 L 789.318 243.275 L 815.454 243.275 L 816.175 244.206 L 816.008 244.138 L 816.008 874.264 L 716.373 749.072 L 294.16 749.072 L 294.399 719.567 Z M 143.825 772.927 L 243.46 647.735 L 673.875 647.735 C 686.38 647.663 697.233 642.068 705.989 631.158 C 714.64 620.227 719.037 606.859 719.06 591.352 L 719.06 142.115 C 719.037 126.607 714.643 113.247 705.992 102.316 C 697.238 91.406 686.38 85.807 673.876 85.735 L 189.01 85.735 C 176.505 85.806 165.652 91.402 156.897 102.312 C 148.245 113.243 143.848 126.611 143.825 142.118 L 143.825 772.927 Z M 673.881 617.119 L 231.405 617.119 L 170.536 694.298 L 170.536 142.123 C 170.513 136.117 172.454 130.763 176.448 125.774 C 180.371 120.779 184.394 115.448 189.003 115.51 L 673.859 115.51 C 678.47 115.45 682.51 120.776 686.429 125.765 C 690.435 130.76 692.372 136.109 692.349 142.113 L 692.349 591.347 C 692.371 597.352 690.434 602.708 686.437 607.695 C 682.514 612.692 678.491 617.181 673.881 617.119 Z", + "captions_key" to "M 215.384 760 Q 192.327 760 176.163 743.837 Q 160 727.673 160 704.616 L 160 255.384 Q 160 232.327 176.163 216.163 Q 192.327 200 215.384 200 L 744.616 200 Q 767.673 200 783.837 216.163 Q 800 232.327 800 255.384 L 800 704.616 Q 800 727.673 783.837 743.837 Q 767.673 760 744.616 760 L 215.384 760 Z M 215.384 729.231 L 744.616 729.231 Q 753.846 729.231 761.539 721.539 Q 769.231 713.846 769.231 704.616 L 769.231 255.384 Q 769.231 246.154 761.539 238.461 Q 753.846 230.769 744.616 230.769 L 215.384 230.769 Q 206.154 230.769 198.461 238.461 Q 190.769 246.154 190.769 255.384 L 190.769 704.616 Q 190.769 713.846 198.461 721.539 Q 206.154 729.231 215.384 729.231 Z M 306.154 587.462 L 405.846 587.462 Q 421.654 587.462 432.981 576.135 Q 444.308 564.808 444.308 549 L 444.308 532.385 L 413.538 532.385 L 413.538 544.385 Q 413.538 549 409.692 552.846 Q 405.846 556.692 401.231 556.692 L 310.769 556.692 Q 306.154 556.692 302.308 552.846 Q 298.461 549 298.461 544.385 L 298.461 415.615 Q 298.461 411 302.308 407.154 Q 306.154 403.308 310.769 403.308 L 401.231 403.308 Q 405.846 403.308 409.692 407.154 Q 413.538 411 413.538 415.615 L 413.538 429.154 L 444.308 429.154 L 444.308 411 Q 444.308 395.192 432.981 383.865 Q 421.654 372.538 405.846 372.538 L 306.154 372.538 Q 290.346 372.538 279.019 383.865 Q 267.692 395.192 267.692 411 L 267.692 549 Q 267.692 564.808 279.019 576.135 Q 290.346 587.462 306.154 587.462 Z M 555.154 587.462 L 654.077 587.462 Q 669.923 587.462 681.231 575.76 Q 692.539 564.058 692.539 549 L 692.539 532.385 L 661.769 532.385 L 661.769 544.385 Q 661.769 549 657.923 552.846 Q 654.077 556.692 649.462 556.692 L 559.769 556.692 Q 555.154 556.692 551.308 552.846 Q 547.462 549 547.462 544.385 L 547.462 415.615 Q 547.462 411 551.308 407.154 Q 555.154 403.308 559.769 403.308 L 649.462 403.308 Q 654.077 403.308 657.923 407.154 Q 661.769 411 661.769 415.615 L 661.769 429.154 L 692.539 429.154 L 692.539 411 Q 692.539 395.942 681.231 384.24 Q 669.923 372.538 654.077 372.538 L 555.154 372.538 Q 539.308 372.538 528 384.24 Q 516.692 395.942 516.692 411 L 516.692 549 Q 516.692 564.058 528 575.76 Q 539.308 587.462 555.154 587.462 Z M 190.769 729.231 L 190.769 230.769 L 190.769 729.231 Z", + "accessibility_settings_key" to "M 480.114 146 Q 453.077 146 434.269 127.306 Q 415.461 108.612 415.461 81.575 Q 415.461 54.538 434.156 35.731 Q 452.85 16.923 479.886 16.923 Q 506.923 16.923 525.731 35.617 Q 544.539 54.311 544.539 81.348 Q 544.539 108.385 525.844 127.192 Q 507.15 146 480.114 146 Z M 399.846 755.385 L 399.846 250.536 Q 336.077 245.615 276.308 236.885 Q 216.538 228.154 164.615 215.692 L 171.923 184.923 Q 248.077 203.385 323.346 211.615 Q 398.615 219.846 480 219.846 Q 561.385 219.846 636.654 211.615 Q 711.923 203.385 788.846 184.923 L 795.385 215.692 Q 743.462 228.154 683.835 236.808 Q 624.208 245.462 560.154 250.76 L 560.154 755.385 L 529.385 755.385 L 529.385 512.154 L 430.615 512.154 L 430.615 755.385 L 399.846 755.385 Z M 318.538 950.769 Q 306.154 950.769 297.577 942.267 Q 289 933.764 289 920.92 Q 289 908.077 297.413 899.5 Q 305.827 890.923 318.538 890.923 Q 330.923 890.923 339.885 899.558 Q 348.846 908.192 348.846 921.231 Q 348.846 933.942 339.885 942.356 Q 330.923 950.769 318.538 950.769 Z M 480.231 950.769 Q 467.519 950.769 459.106 942.267 Q 450.692 933.764 450.692 920.92 Q 450.692 908.077 459.195 899.5 Q 467.698 890.923 480.541 890.923 Q 493.385 890.923 501.962 899.558 Q 510.539 908.192 510.539 921.231 Q 510.539 933.942 501.904 942.356 Q 493.269 950.769 480.231 950.769 Z M 642.382 950.769 Q 629.538 950.769 620.962 942.267 Q 612.385 933.764 612.385 920.92 Q 612.385 908.077 621.019 899.5 Q 629.654 890.923 642.692 890.923 Q 655.404 890.923 663.817 899.558 Q 672.231 908.192 672.231 921.231 Q 672.231 933.942 663.728 942.356 Q 655.225 950.769 642.382 950.769 Z", + "about_key" to "M 466.077 660 L 496.846 660 L 496.846 440 L 466.077 440 L 466.077 660 Z M 479.982 386 Q 489 386 495.231 380.069 Q 501.462 374.138 501.462 364.769 Q 501.462 355.319 495.248 349.198 Q 489.035 343.077 480.018 343.077 Q 470.231 343.077 464.385 349.198 Q 458.538 355.319 458.538 364.769 Q 458.538 374.138 464.752 380.069 Q 470.965 386 479.982 386 Z M 480.4 840 Q 405.224 840 340.106 811.661 Q 274.987 783.321 225.859 734.239 Q 176.732 685.157 148.366 620.026 Q 120 554.894 120 479.634 Q 120 405.143 148.339 339.565 Q 176.679 273.987 225.761 225.359 Q 274.843 176.732 339.974 148.366 Q 405.106 120 480.366 120 Q 554.857 120 620.435 148.339 Q 686.013 176.679 734.641 225.261 Q 783.268 273.843 811.634 339.518 Q 840 405.194 840 479.6 Q 840 554.776 811.661 619.894 Q 783.321 685.013 734.739 733.956 Q 686.157 782.9 620.482 811.45 Q 554.806 840 480.4 840 Z M 480.5 809.231 Q 617.385 809.231 713.308 713.192 Q 809.231 617.154 809.231 479.5 Q 809.231 342.615 713.495 246.692 Q 617.76 150.769 480 150.769 Q 342.846 150.769 246.808 246.505 Q 150.769 342.24 150.769 480 Q 150.769 617.154 246.808 713.192 Q 342.846 809.231 480.5 809.231 Z M 480 480 Z", + + // RVX settings + "revanced_preference_screen_general" to "general_key", + "revanced_preference_screen_ads" to "M 721.539 495.385 L 721.539 464.615 L 846.154 464.615 L 846.154 495.385 L 721.539 495.385 Z M 766.154 758.462 L 665.923 683.846 L 685 659.692 L 785.231 734.308 L 766.154 758.462 Z M 683.385 297 L 664.308 272.846 L 763.077 198.461 L 782.154 222.615 L 683.385 297 Z M 224.615 718.462 L 224.615 566.154 L 169.231 566.154 Q 146.788 566.154 130.317 549.683 Q 113.846 533.212 113.846 510.769 L 113.846 449.231 Q 113.846 426.788 130.317 410.317 Q 146.788 393.846 169.231 393.846 L 327.692 393.846 L 486.154 300 L 486.154 660 L 327.692 566.154 L 255.385 566.154 L 255.385 718.462 L 224.615 718.462 Z M 455.385 605.539 L 455.385 354.461 L 336 424.615 L 169.231 424.615 Q 160 424.615 152.308 432.308 Q 144.615 440 144.615 449.231 L 144.615 510.769 Q 144.615 520 152.308 527.692 Q 160 535.385 169.231 535.385 L 336 535.385 L 455.385 605.539 Z M 556.923 595.539 L 556.923 364.461 Q 577 383.077 589.269 413.346 Q 601.539 443.615 601.539 480 Q 601.539 516.385 589.269 546.654 Q 577 576.923 556.923 595.539 Z M 300 480 Z", + "revanced_preference_screen_alt_thumbnails" to "M 175.384 760 Q 152.327 760 136.163 743.837 Q 120 727.673 120 704.616 L 120 255.384 Q 120 232.327 136.163 216.163 Q 152.327 200 175.384 200 L 784.616 200 Q 807.673 200 823.837 216.163 Q 840 232.327 840 255.384 L 840 704.616 Q 840 727.673 823.837 743.837 Q 807.673 760 784.616 760 L 175.384 760 Z M 175.384 729.231 L 784.616 729.231 Q 793.846 729.231 801.539 721.539 Q 809.231 713.846 809.231 704.616 L 809.231 312.461 L 150.769 312.461 L 150.769 704.616 Q 150.769 713.846 158.461 721.539 Q 166.154 729.231 175.384 729.231 Z", + "revanced_preference_screen_feed" to "M 227.26 780 C 214.573 780 203.527 775.28 194.12 765.84 C 184.707 756.4 180 745.337 180 732.65 C 180 720.47 184.72 709.677 194.16 700.27 C 203.6 690.857 214.663 686.15 227.35 686.15 C 239.53 686.15 250.323 690.87 259.73 700.31 C 269.143 709.75 273.85 720.56 273.85 732.74 C 273.85 745.427 269.13 756.473 259.69 765.88 C 250.25 775.293 239.44 780 227.26 780 Z M 735.38 780 C 735.38 703.127 720.917 631.077 691.99 563.85 C 663.057 496.623 623.197 437.87 572.41 387.59 C 522.13 336.797 463.35 296.937 396.07 268.01 C 328.79 239.083 256.767 224.62 180 224.62 L 180 180 C 263.387 180 341.337 195.743 413.85 227.23 C 486.363 258.717 549.773 301.613 604.08 355.92 C 658.387 410.227 701.283 473.587 732.77 546 C 764.257 618.407 780 696.407 780 780 L 735.38 780 Z M 505.85 780 C 505.85 734.36 497.377 691.687 480.43 651.98 C 463.49 612.28 440.14 577.517 410.38 547.69 C 380.38 517.383 345.887 493.653 306.9 476.5 C 267.907 459.347 225.607 450.77 180 450.77 L 180 406.15 C 232.153 406.15 280.483 415.93 324.99 435.49 C 369.503 455.043 408.58 481.6 442.22 515.16 C 476.127 549.413 502.643 589.023 521.77 633.99 C 540.897 678.957 550.46 727.627 550.46 780 L 505.85 780 Z", + "revanced_preference_screen_player" to "M 401.461 618.462 L 618.462 480 L 401.461 341.538 L 401.461 618.462 Z M 480.134 840 Q 405.692 840 340.34 811.661 Q 274.987 783.321 225.859 734.239 Q 176.732 685.157 148.366 619.866 Q 120 554.575 120 480.134 Q 120 405.461 148.339 339.724 Q 176.679 273.987 225.761 225.359 Q 274.843 176.732 340.134 148.366 Q 405.425 120 479.866 120 Q 554.539 120 620.276 148.339 Q 686.013 176.679 734.641 225.261 Q 783.268 273.843 811.634 339.518 Q 840 405.194 840 479.866 Q 840 554.308 811.661 619.66 Q 783.321 685.013 734.739 734.141 Q 686.157 783.268 620.482 811.634 Q 554.806 840 480.134 840 Z M 480 809.231 Q 617.385 809.231 713.308 713.192 Q 809.231 617.154 809.231 480 Q 809.231 342.615 713.308 246.692 Q 617.385 150.769 480 150.769 Q 342.846 150.769 246.808 246.692 Q 150.769 342.615 150.769 480 Q 150.769 617.154 246.808 713.192 Q 342.846 809.231 480 809.231 Z M 480 480 Z", + "revanced_preference_screen_shorts" to "M 407.712 579.273 L 586.008 479.273 L 407.712 379.273 L 407.712 579.273 Z M 304.09 573.07 C 304.09 573.07 304.09 573.07 304.09 573.07 L 271.628 592.775 C 230.982 617.447 204.016 650.91 190.732 693.168 C 177.449 735.427 181.608 774.351 203.209 809.936 C 224.809 845.522 257.396 867.135 300.968 874.776 C 344.54 882.417 386.649 873.9 427.295 849.228 L 719.94 671.592 C 760.586 646.92 787.55 613.456 800.835 571.197 C 814.119 528.94 809.96 490.017 788.359 454.431 C 766.758 418.845 734.171 397.23 690.598 389.589 C 678.93 387.544 667.368 386.657 655.909 386.928 L 688.371 367.224 C 729.018 342.551 755.981 309.088 769.267 266.829 C 782.551 224.572 778.392 185.65 756.791 150.064 C 735.19 114.477 702.603 92.864 659.031 85.223 C 615.459 77.585 573.351 86.1 532.705 110.772 L 240.06 288.407 C 199.414 313.079 172.448 346.543 159.165 388.801 C 145.882 431.06 150.04 469.982 171.64 505.568 C 193.241 541.154 225.827 562.767 269.4 570.408 C 281.069 572.454 292.633 573.341 304.09 573.07 Z M 311.194 550.911 C 310.383 550.821 291.513 549.29 276.802 546.69 C 239.915 540.173 212.325 521.847 194.032 491.71 C 175.74 461.574 172.229 428.666 183.504 392.988 C 194.776 357.308 217.62 329.022 252.033 308.133 L 544.678 130.498 C 579.092 109.608 614.743 102.423 651.63 108.941 C 688.516 115.459 716.107 133.785 734.399 163.922 C 752.692 194.058 756.202 226.966 744.927 262.642 C 733.655 298.323 710.811 326.607 676.397 347.496 C 676.397 347.496 632.576 376.786 629.931 379.384 C 616.83 392.247 624.795 408.44 644.851 409.184 C 649.56 409.36 666.37 410.336 683.197 413.309 C 720.083 419.825 747.675 438.153 765.968 468.29 C 784.26 498.427 787.77 531.334 776.495 567.012 C 765.223 602.692 742.379 630.976 707.966 651.865 L 415.321 829.501 C 380.907 850.39 345.255 857.577 308.37 851.058 C 271.483 844.542 243.893 826.215 225.601 796.078 C 207.308 765.943 203.797 733.033 215.072 697.355 C 226.343 661.676 249.188 633.389 283.601 612.5 C 283.601 612.5 318.492 587.99 322.367 585.392 C 339.459 573.934 349.769 555.207 311.194 550.911 Z", + "revanced_preference_screen_swipe_controls" to "M 196.923 590.231 L 80.923 474.231 L 98.923 456.231 L 179.846 536.923 Q 170.307 499.538 165.154 462.538 Q 160 425.538 160 387.384 Q 160 314.231 183.346 246.461 Q 206.692 178.692 249.846 120 L 268.846 139 Q 229.154 194 207.654 256.961 Q 186.154 319.923 186.154 387.384 Q 186.154 426.769 191.769 465.538 Q 197.385 504.307 208.461 542.461 L 294.923 456.231 L 312.923 474.231 L 196.923 590.231 Z M 646 792.462 Q 629.077 798.923 610.154 798.308 Q 591.231 797.693 573.538 789.231 L 318.077 671.385 L 325.077 653.846 Q 328.538 643.308 336.769 637.077 Q 345 630.846 356.538 629.615 L 484.769 616.231 L 367.769 297.769 Q 365.077 291.154 368.269 285.654 Q 371.461 280.154 378.077 278.231 Q 383.923 275.538 389.538 278.346 Q 395.154 281.154 397.846 287.769 L 528 644.077 L 364.616 659.154 L 586.462 762.154 Q 597.769 767.692 610.846 767.692 Q 623.923 767.692 636 763.923 L 774.231 712.846 Q 817.077 697.308 836.5 656.731 Q 855.923 616.154 840.385 573.308 L 782.231 414.308 Q 779.538 407.692 781.962 402.462 Q 784.385 397.231 791 394.538 Q 797.615 391.846 803.231 394.269 Q 808.846 396.692 811.539 403.308 L 868.693 562.308 Q 889.385 617.615 864.962 669.808 Q 840.539 722 785.231 741.923 L 646 792.462 Z M 579.154 535.769 L 521.923 380.385 Q 519.231 373.769 522.423 368.654 Q 525.615 363.538 532.231 360.846 Q 538.077 358.154 543.577 361.346 Q 549.077 364.538 551.769 370.385 L 608.231 525 L 579.154 535.769 Z M 687.923 495.077 L 645.461 378.462 Q 642.769 371.846 645.577 366.615 Q 648.385 361.385 655 358.692 Q 661.615 356.769 667.115 359.192 Q 672.615 361.615 674.539 368.231 L 718 485.846 L 687.923 495.077 Z M 677.769 612.923 Z", + "revanced_preference_screen_video" to "M 443.231 546.231 L 657.077 409.231 L 443.231 272.231 L 443.231 546.231 Z M 296.923 698.462 Q 273.865 698.462 257.702 682.298 Q 241.538 666.135 241.538 643.077 L 241.538 175.384 Q 241.538 152.327 257.702 136.163 Q 273.865 120 296.923 120 L 764.616 120 Q 787.673 120 803.837 136.163 Q 820 152.327 820 175.384 L 820 643.077 Q 820 666.135 803.837 682.298 Q 787.673 698.462 764.616 698.462 L 296.923 698.462 Z M 296.923 667.693 L 764.616 667.693 Q 773.846 667.693 781.539 660 Q 789.231 652.308 789.231 643.077 L 789.231 175.384 Q 789.231 166.154 781.539 158.461 Q 773.846 150.769 764.616 150.769 L 296.923 150.769 Q 287.692 150.769 280 158.461 Q 272.308 166.154 272.308 175.384 L 272.308 643.077 Q 272.308 652.308 280 660 Q 287.692 667.693 296.923 667.693 Z M 195.384 800 Q 172.327 800 156.163 783.837 Q 140 767.674 140 744.616 L 140 246.154 L 170.769 246.154 L 170.769 744.616 Q 170.769 753.847 178.461 761.539 Q 186.154 769.231 195.384 769.231 L 693.847 769.231 L 693.847 800 L 195.384 800 Z M 272.308 150.769 L 272.308 667.693 L 272.308 150.769 Z", + "revanced_preference_screen_ryd" to "M 262.654 192.307 L 666 192.307 L 666 628.923 L 415.692 880 L 403.602 871.186 Q 398.384 866.308 395.384 859.231 Q 392.384 852.154 392.384 843.769 L 392.384 839.923 L 433.538 628.923 L 136.846 628.923 Q 115.461 628.923 98.461 611.923 Q 81.461 594.923 81.461 573.538 L 81.461 523.176 Q 81.461 517.615 81.115 511.269 Q 80.769 504.923 83 499.461 L 195.154 237.153 Q 202.494 218.211 222.441 205.259 Q 242.388 192.307 262.654 192.307 Z M 635.231 223.077 L 256.692 223.077 Q 248.231 223.077 239.385 227.692 Q 230.538 232.307 225.923 243.077 L 112.231 512.077 L 112.231 573.538 Q 112.231 583.538 119.154 590.846 Q 126.077 598.154 136.846 598.154 L 470.615 598.154 L 424.538 829.461 L 635.231 615.461 L 635.231 223.077 Z M 635.231 615.461 L 635.231 223.077 L 635.231 615.461 Z M 666 628.923 L 666 598.154 L 809 598.154 L 809 223.077 L 666 223.077 L 666 192.307 L 839.769 192.307 L 839.769 628.923 L 666 628.923 Z", + "revanced_preference_screen_return_youtube_username" to "M 480 840 Q 405.46 840 339.72 811.66 Q 273.99 783.32 225.36 734.74 Q 176.73 686.16 148.37 620.48 Q 120 554.81 120 480.13 Q 120 405.46 148.34 339.72 Q 176.68 273.99 225.26 225.36 Q 273.84 176.73 339.52 148.37 Q 405.19 120 479.87 120 Q 554.54 120 620.28 148.35 Q 686.01 176.7 734.64 225.3 Q 783.27 273.9 811.63 339.6 Q 840 405.3 840 480 L 840 516.85 Q 840 565.92 805.78 599.81 Q 771.55 633.69 721.69 633.69 Q 685.4 633.69 655.47 612.73 Q 625.54 591.77 613.92 557.46 Q 591.77 593 556.48 613.35 Q 521.2 633.69 480 633.69 Q 416.24 633.69 371.16 588.92 Q 326.08 544.15 326.08 479.6 Q 326.08 415.05 371.16 370.45 Q 416.24 325.85 480 325.85 Q 543.76 325.85 588.84 370.45 Q 633.92 415.06 633.92 480.09 L 633.92 516.85 Q 633.92 552.84 659.88 577.88 Q 685.85 602.92 721.58 602.92 Q 757.31 602.92 783.27 577.88 Q 809.23 552.84 809.23 516.85 L 809.23 480 Q 809.23 342.13 713.57 246.45 Q 617.91 150.77 480.07 150.77 Q 342.24 150.77 246.5 246.43 Q 150.77 342.09 150.77 479.93 Q 150.77 617.76 246.45 713.5 Q 342.13 809.23 480 809.23 L 686.31 809.23 L 686.31 840 L 480 840 Z M 480.1 602.92 Q 531.46 602.92 567.31 567.07 Q 603.15 531.22 603.15 480 Q 603.15 427.92 567.2 392.27 Q 531.25 356.62 479.9 356.62 Q 428.54 356.62 392.69 392.35 Q 356.85 428.08 356.85 480.27 Q 356.85 530.96 392.8 566.94 Q 428.75 602.92 480.1 602.92 Z", + "revanced_preference_screen_sb" to "M 480 838.231 Q 359.231 801.693 279.615 690.346 Q 200 579 200 440.846 L 200 227.461 L 480 122.846 L 760 227.461 L 760 440.846 Q 760 579 680.385 690.346 Q 600.769 801.693 480 838.231 Z M 480 805.462 Q 588.846 770.539 659.039 668.5 Q 729.231 566.462 729.231 440.846 L 729.231 248.692 L 480 155.308 L 230.769 248.692 L 230.769 440.846 Q 230.769 566.462 300.961 668.5 Q 371.154 770.539 480 805.462 Z M 480 480.769 Z", + "revanced_preference_screen_misc" to "M 658.231 466.308 L 495.231 303.308 L 658.231 140.307 L 821.231 303.308 L 658.231 466.308 Z M 184.615 416.615 L 184.615 184.846 L 415.615 184.846 L 415.615 416.615 L 184.615 416.615 Z M 543.385 775.385 L 543.385 544.385 L 775.154 544.385 L 775.154 775.385 L 543.385 775.385 Z M 184.615 775.385 L 184.615 544.385 L 415.615 544.385 L 415.615 775.385 L 184.615 775.385 Z M 215.384 385.846 L 384.846 385.846 L 384.846 215.615 L 215.384 215.615 L 215.384 385.846 Z M 660.462 425.308 L 780.231 305.538 L 660.462 185 L 539.923 305.538 L 660.462 425.308 Z M 574.154 744.616 L 744.385 744.616 L 744.385 575.154 L 574.154 575.154 L 574.154 744.616 Z M 215.384 744.616 L 384.846 744.616 L 384.846 575.154 L 215.384 575.154 L 215.384 744.616 Z M 384.846 385.846 Z M 539.923 305.538 Z M 384.846 575.154 Z M 574.154 575.154 Z", +) + +private val rvxPreferenceKey = mapOf( + // Hide Settings menu + "revanced_hide_settings_menu_parent_tools" to "parent_tools_key", + "revanced_hide_settings_menu_general" to "general_key", + "revanced_hide_settings_menu_account" to "account_switcher_key", + "revanced_hide_settings_menu_data_saving" to "data_saving_settings_key", + "revanced_hide_settings_menu_auto_play" to "auto_play_key", + "revanced_hide_settings_menu_video_quality" to "video_quality_settings_key", + "revanced_hide_settings_menu_offline" to "offline_key", + "revanced_hide_settings_menu_pair_with_tv" to "pair_with_tv_key", + "revanced_hide_settings_menu_history" to "history_key", + "revanced_hide_settings_menu_your_data" to "your_data_key", + "revanced_hide_settings_menu_privacy" to "privacy_key", + "revanced_hide_settings_menu_premium_early_access" to "premium_early_access_browse_page_key", + "revanced_hide_settings_menu_subscription_product" to "subscription_product_setting_key", + "revanced_hide_settings_menu_billing_and_payment" to "billing_and_payment_key", + "revanced_hide_settings_menu_notification" to "notification_key", + "revanced_hide_settings_menu_connected_accounts" to "connected_accounts_browse_page_key", + "revanced_hide_settings_menu_live_chat" to "live_chat_key", + "revanced_hide_settings_menu_captions" to "captions_key", + "revanced_hide_settings_menu_accessibility" to "accessibility_settings_key", + "revanced_hide_settings_menu_about" to "about_key", + + // Other settings + "gms_core_settings" to "M 432.54 840 C 427.307 840 422.563 838.283 418.31 834.85 C 414.05 831.41 411.51 827.077 410.69 821.85 L 397.23 725.54 C 382.51 720.873 366.357 713.643 348.77 703.85 C 331.177 694.05 316.28 683.537 304.08 672.31 L 216.54 712.31 C 211.307 714.257 206.077 714.5 200.85 713.04 C 195.617 711.58 191.617 708.31 188.85 703.23 L 140.08 618.46 C 137.307 613.387 136.447 608.36 137.5 603.38 C 138.553 598.407 141.54 594.203 146.46 590.77 L 225.69 531.77 C 224.357 523.717 223.267 515.217 222.42 506.27 C 221.573 497.323 221.15 488.823 221.15 480.77 C 221.15 473.23 221.573 464.987 222.42 456.04 C 223.267 447.093 224.357 437.823 225.69 428.23 L 146.46 369.23 C 141.54 365.797 138.68 361.463 137.88 356.23 C 137.087 350.997 138.077 345.843 140.85 340.77 L 188.85 258.31 C 191.617 253.743 195.617 250.6 200.85 248.88 C 206.077 247.167 211.307 247.283 216.54 249.23 L 303.31 287.69 C 317.05 276.463 332.203 266.08 348.77 256.54 C 365.337 247 381.233 239.973 396.46 235.46 L 410.69 138.15 C 411.51 132.923 414.05 128.59 418.31 125.15 C 422.563 121.717 427.307 120 432.54 120 L 527.46 120 C 532.693 120 537.437 121.717 541.69 125.15 C 545.95 128.59 548.49 132.923 549.31 138.15 L 562.77 235.23 C 579.537 241.437 595.473 248.757 610.58 257.19 C 625.68 265.63 640.027 275.797 653.62 287.69 L 744.23 249.23 C 749.463 247.283 754.657 247.167 759.81 248.88 C 764.963 250.6 768.923 253.743 771.69 258.31 L 819.92 341.54 C 822.693 346.613 823.553 351.807 822.5 357.12 C 821.447 362.427 818.46 366.463 813.54 369.23 L 731.23 429.31 C 733.59 438.537 735.063 447.37 735.65 455.81 C 736.243 464.243 736.54 472.307 736.54 480 C 736.54 487.18 736.117 494.95 735.27 503.31 C 734.423 511.67 733.077 520.977 731.23 531.23 L 811.23 590.77 C 816.157 593.537 819.273 597.573 820.58 602.88 C 821.887 608.193 821.153 613.387 818.38 618.46 L 771.15 702.46 C 767.87 707.54 763.487 710.81 758 712.27 C 752.513 713.73 747.41 713.487 742.69 711.54 L 653.62 671.54 C 639.36 683.793 624.627 694.6 609.42 703.96 C 594.22 713.32 578.67 720.257 562.77 724.77 L 549.31 821.85 C 548.49 827.077 545.95 831.41 541.69 834.85 C 537.437 838.283 532.693 840 527.46 840 L 432.54 840 Z M 438.31 809.23 L 520.92 809.23 L 535.69 698 C 556.15 692.667 574.933 685.103 592.04 675.31 C 609.14 665.517 626.46 652.363 644 635.85 L 746.92 680.31 L 786.92 610.62 L 696 543.15 C 698.667 530.797 700.707 519.63 702.12 509.65 C 703.527 499.677 704.23 489.793 704.23 480 C 704.23 469.18 703.563 459.04 702.23 449.58 C 700.897 440.113 698.82 429.713 696 418.38 L 788.46 349.38 L 748.46 279.69 L 643.23 324.15 C 630.463 309.897 613.9 296.553 593.54 284.12 C 573.18 271.68 553.64 264.307 534.92 262 L 521.69 150.77 L 438.31 150.77 L 425.85 261.23 C 404.203 265.383 384.573 272.487 366.96 282.54 C 349.347 292.593 332.103 306.207 315.23 323.38 L 211.54 279.69 L 171.54 349.38 L 262.46 416.08 C 259.28 425.873 256.987 436.14 255.58 446.88 C 254.167 457.627 253.46 468.923 253.46 480.77 C 253.46 491.59 254.167 502.117 255.58 512.35 C 256.987 522.577 259.023 532.843 261.69 543.15 L 171.54 610.62 L 211.54 680.31 L 314.46 636.62 C 330.46 653.127 347.397 666.28 365.27 676.08 C 383.143 685.873 403.08 693.437 425.08 698.77 L 438.31 809.23 Z M 430.15 587.46 L 529.85 587.46 C 540.384 587.46 549.427 583.683 556.98 576.13 C 564.534 568.583 568.31 559.54 568.31 549 L 568.31 544.76 C 568.31 540.353 566.887 536.766 564.04 534 C 561.2 531.233 557.777 529.85 553.77 529.85 L 551.31 529.85 C 547.23 529.85 543.914 531.27 541.36 534.11 C 538.814 536.95 537.54 540.373 537.54 544.38 C 537.54 547.46 536.257 550.283 533.69 552.85 C 531.13 555.41 528.31 556.69 525.23 556.69 L 434.77 556.69 C 431.69 556.69 428.87 555.41 426.31 552.85 C 423.744 550.283 422.46 547.46 422.46 544.38 L 422.46 415.62 C 422.46 412.54 423.744 409.716 426.31 407.15 C 428.87 404.59 431.69 403.31 434.77 403.31 L 525.23 403.31 C 528.31 403.31 531.13 404.59 533.69 407.15 C 536.257 409.716 537.54 412.54 537.54 415.62 C 537.54 419.72 538.814 423.166 541.36 425.96 C 543.914 428.753 547.23 430.15 551.31 430.15 L 553.77 430.15 C 557.777 430.15 561.2 428.766 564.04 426 C 566.887 423.233 568.31 419.646 568.31 415.24 L 568.31 411 C 568.31 400.46 564.534 391.416 556.98 383.87 C 549.427 376.316 540.384 372.54 529.85 372.54 L 430.15 372.54 C 419.617 372.54 410.574 376.316 403.02 383.87 C 395.467 391.416 391.69 400.46 391.69 411 L 391.69 549 C 391.69 559.54 395.467 568.583 403.02 576.13 C 410.574 583.683 419.617 587.46 430.15 587.46 Z", + "revanced_alt_thumbnail_home" to "revanced_hide_navigation_home_button", + "revanced_alt_thumbnail_library" to "revanced_preference_screen_video", + "revanced_alt_thumbnail_player" to "revanced_preference_screen_player", + "revanced_alt_thumbnail_search" to "revanced_hide_shorts_shelf_search", + "revanced_alt_thumbnail_subscriptions" to "revanced_hide_navigation_subscriptions_button", + "revanced_change_player_flyout_menu_toggle" to "M 280 680 Q 196.667 680 138.333 621.72 Q 80 563.439 80 480.181 Q 80 396.923 138.333 338.461 Q 196.667 280 280 280 L 680 280 Q 763.333 280 821.667 338.28 Q 880 396.561 880 479.819 Q 880 563.077 821.667 621.539 Q 763.333 680 680 680 L 280 680 Z M 280 649.231 L 680 649.231 Q 750.558 649.231 799.894 599.93 Q 849.231 550.63 849.231 480.123 Q 849.231 409.615 799.894 360.192 Q 750.558 310.769 680 310.769 L 280 310.769 Q 209.442 310.769 160.106 360.07 Q 110.769 409.37 110.769 479.877 Q 110.769 550.385 160.106 599.808 Q 209.442 649.231 280 649.231 Z M 679.991 571 Q 718.204 571 745.102 544.726 Q 772 518.453 772 480.149 Q 772 441.846 745.251 415.423 Q 718.502 389 680.29 389 Q 642.077 389 615.654 415.274 Q 589.231 441.547 589.231 479.851 Q 589.231 518.154 615.505 544.577 Q 641.778 571 679.991 571 Z M 480 480 Z", + "revanced_change_share_sheet" to "revanced_hide_shorts_share_button", + "revanced_change_shorts_repeat_state" to "M 388.49 535.963 L 388.49 349.766 L 554.481 442.865 L 388.49 535.963 Z M 262.912 527.139 C 222.899 520.08 192.778 500.095 172.885 467.416 C 153.074 434.688 149.225 398.706 161.385 359.882 C 173.612 321.097 198.474 290.237 235.751 267.575 L 503.202 105.231 C 540.509 82.62 579.338 74.772 619.346 81.752 C 659.358 88.811 689.479 108.796 709.373 141.476 C 729.184 174.205 733.034 210.186 720.874 249.008 C 708.644 287.794 683.785 318.654 646.507 341.317 L 619.965 357.427 C 629.295 357.427 637.484 358.041 648.195 359.916 C 688.208 366.976 718.33 386.963 738.223 419.641 C 758.035 452.37 761.894 488.326 749.733 527.15 C 748.835 529.714 746.582 531.657 743.944 532.627 C 741.325 533.592 738.071 533.859 735.152 533.464 C 732.227 533.069 729.33 531.948 727.483 530.239 C 726.533 529.377 725.747 528.222 725.408 526.991 C 725.073 525.775 725.123 524.254 725.571 522.751 C 735.856 490.346 732.696 460.683 716.05 433.346 C 699.483 405.96 674.606 389.443 641.085 383.564 C 625.765 380.854 610.478 379.969 606.184 379.808 C 596.777 379.398 589.881 375.301 587.142 369.799 C 585.758 367.02 585.35 363.697 586.106 360.472 C 586.855 357.279 588.798 353.912 591.876 350.864 C 594.37 348.426 633.578 322.212 634.545 321.565 C 665.867 302.587 686.507 277.029 696.721 244.584 C 707.006 212.18 703.846 182.517 687.2 155.181 C 670.632 127.795 645.757 111.279 612.237 105.398 C 578.711 99.44 546.474 105.941 515.184 124.969 L 247.732 287.312 C 216.41 306.29 195.751 331.862 185.538 364.307 C 175.252 396.712 178.413 426.375 195.058 453.711 C 211.627 481.097 236.501 497.613 270.023 503.493 C 283.421 505.863 300.628 507.259 301.392 507.342 C 310.293 508.342 316.512 510.195 320.452 512.581 C 324.494 515.029 326.399 518.329 326.463 521.613 C 326.554 528.046 319.947 535.368 312.049 540.68 C 308.516 543.048 277.602 564.759 276.554 565.496 C 245.248 584.472 224.601 610.029 214.388 642.473 C 204.103 674.878 207.263 704.543 223.91 731.878 C 240.477 759.263 265.351 775.78 298.872 781.66 C 332.396 787.619 364.637 781.11 395.924 762.089 L 516.401 686.023 L 516.217 715.94 L 407.905 781.828 C 370.599 804.438 331.765 812.321 291.763 805.306 C 251.745 798.288 221.589 778.286 201.737 745.583 C 181.886 712.878 178.075 676.873 190.234 638.048 C 202.463 599.262 227.324 568.403 264.601 545.741 L 291.143 529.63 C 281.814 529.629 273.625 529.015 262.912 527.139 Z M 610.21 863.097 C 611.76 864.921 612.864 867.629 613.106 869.85 L 613.112 869.907 L 612.113 869.939 L 613.119 869.98 L 613.116 870.066 L 613.114 870.09 C 612.994 872.462 611.707 875.448 609.959 877.462 L 609.925 877.502 L 609.887 877.536 C 607.915 879.31 605.139 880.502 602.871 880.704 L 602.751 880.713 L 602.683 880.709 L 602.642 880.707 C 600.318 880.533 597.471 879.237 595.534 877.487 L 554.92 836.873 C 553.564 835.465 552.283 833.539 551.56 831.904 C 550.92 830.248 550.481 828.114 550.481 826.199 C 550.481 824.267 550.995 822.004 551.638 820.395 C 552.363 818.83 553.601 817.068 554.946 815.684 L 595.694 774.948 C 597.526 773.406 600.243 772.3 602.49 772.062 L 602.547 772.056 L 602.579 773.047 L 602.619 772.048 L 602.71 772.051 L 602.734 772.053 C 605.14 772.181 608.154 773.51 610.164 775.308 L 610.205 775.345 L 610.241 775.386 C 611.922 777.335 613.091 780.066 613.289 782.313 L 613.298 782.43 L 613.295 782.494 L 613.292 782.537 C 613.122 784.871 611.817 787.709 610.072 789.638 L 583.564 816.146 L 756.645 816.148 C 758.708 816.28 759.649 815.903 760.787 814.551 L 760.773 814.568 C 760.774 814.566 760.78 814.56 760.787 814.551 L 761.865 815.655 L 760.869 814.501 L 761.817 815.378 L 760.878 814.46 C 760.884 814.455 760.89 814.449 760.898 814.442 C 762.221 813.332 762.507 812.35 762.381 810.288 L 762.384 762.253 C 762.549 759.78 763.786 756.913 765.358 755.124 L 765.371 755.135 L 765.359 755.123 L 765.358 755.124 L 765.358 755.123 C 765.363 755.118 765.378 755.101 765.391 755.088 L 765.362 755.109 L 765.395 755.08 L 765.397 755.082 C 765.401 755.08 765.42 755.066 765.449 755.045 L 765.449 755.046 L 765.397 755.083 L 765.668 755.399 L 765.371 755.135 L 765.978 755.76 L 765.668 755.399 L 766.83 756.431 L 765.449 755.046 L 765.65 754.901 C 765.574 754.956 765.498 755.01 765.449 755.045 L 765.443 755.039 C 767.238 753.487 770.072 752.213 772.603 752.213 C 775.158 752.213 778.112 753.643 779.857 755.208 L 779.185 755.844 L 779.703 755.42 L 778.543 756.709 L 779.944 755.297 C 781.452 757.094 782.531 759.947 782.676 762.382 L 782.676 810.347 C 782.513 817.088 779.68 823.712 774.887 828.724 C 769.876 833.518 763.276 836.279 756.535 836.441 L 583.564 836.441 L 610.21 863.097 Z M 766.859 756.444 L 766.853 756.452 L 765.358 755.124 C 765.366 755.115 765.383 755.097 765.405 755.073 L 766.859 756.444 Z M 599.067 654.475 C 597.017 654.339 596.142 654.767 594.902 656.101 C 593.554 657.346 593.2 658.277 593.336 660.331 L 593.333 708.37 C 593.168 710.843 591.931 713.71 590.36 715.498 L 590.347 715.487 L 589.669 714.789 L 589.997 715.176 L 588.888 714.191 L 590.275 715.583 C 588.479 717.135 585.644 718.409 583.115 718.409 C 580.556 718.409 577.572 716.952 575.825 715.381 C 575.913 715.444 575.895 715.402 575.803 715.297 L 575.775 715.326 C 574.267 713.532 573.186 710.676 573.041 708.241 L 573.041 660.276 C 573.204 653.535 576.037 646.912 580.83 641.899 C 585.843 637.101 592.442 634.343 599.182 634.182 L 772.153 634.182 L 745.523 607.545 C 743.996 605.777 742.849 603.068 742.609 600.752 L 743.539 600.729 L 742.603 600.692 L 742.604 600.673 C 742.604 600.668 742.605 600.663 742.605 600.659 C 742.604 600.652 742.604 600.651 742.604 600.647 L 742.606 600.647 C 742.607 600.642 742.607 600.638 742.608 600.635 L 742.605 600.635 C 742.699 598.164 744.032 595.128 745.762 593.155 L 745.797 593.115 L 745.836 593.08 C 747.791 591.343 750.567 590.123 752.843 589.919 L 752.963 589.91 L 753.03 589.914 L 753.073 589.916 C 755.398 590.089 758.247 591.385 760.184 593.136 L 800.79 633.741 C 802.113 635.1 803.392 636.89 804.2 638.465 C 804.985 640.176 805.544 642.438 805.544 644.423 C 805.544 646.42 804.919 648.77 804.125 650.436 C 803.323 651.934 802.085 653.597 800.773 654.938 L 760.047 695.654 C 758.267 697.181 755.538 698.326 753.199 698.563 L 753.177 697.626 L 753.138 698.569 L 753.123 698.568 C 753.117 698.567 753.112 698.567 753.107 698.567 C 753.101 698.567 753.1 698.568 753.097 698.568 L 753.097 698.566 C 753.089 698.565 753.084 698.564 753.08 698.564 L 753.08 698.567 C 750.576 698.465 747.518 697.092 745.55 695.313 L 745.51 695.277 L 745.476 695.237 C 743.798 693.29 742.625 690.546 742.428 688.3 L 742.419 688.18 L 742.423 688.112 L 742.425 688.071 C 742.6 685.763 743.898 682.921 745.646 680.985 L 772.153 654.477 L 599.067 654.475 Z M 588.859 714.178 L 588.865 714.17 L 590.36 715.498 C 590.352 715.507 590.335 715.525 590.313 715.549 L 588.859 714.178 Z M 577.301 714.034 L 577.301 714.035 L 575.782 715.336 C 575.776 715.329 575.767 715.318 575.764 715.314 L 577.301 714.034 Z M 588.865 714.17 L 590.36 715.499 L 588.865 714.17 Z", + "revanced_custom_player_overlay_opacity" to "revanced_swipe_overlay_background_alpha", + "revanced_default_app_settings" to "revanced_preference_screen_settings_menu", + "revanced_default_playback_speed" to "revanced_overlay_button_speed_dialog", + "revanced_default_video_quality_wifi" to "M 453.923 820 L 453.923 626.538 L 484.692 626.538 L 484.692 708 L 820 708 L 820 738.769 L 484.692 738.769 L 484.692 820 L 453.923 820 Z M 140 738.769 L 140 708 L 343.154 708 L 343.154 738.769 L 140 738.769 Z M 312.385 576.615 L 312.385 495.385 L 140 495.385 L 140 464.615 L 312.385 464.615 L 312.385 382.923 L 343.154 382.923 L 343.154 576.615 L 312.385 576.615 Z M 453.923 495.385 L 453.923 464.615 L 820 464.615 L 820 495.385 L 453.923 495.385 Z M 616.846 332.692 L 616.846 140 L 647.615 140 L 647.615 221.231 L 820 221.231 L 820 252 L 647.615 252 L 647.615 332.692 L 616.846 332.692 Z M 140 252 L 140 221.231 L 506.077 221.231 L 506.077 252 L 140 252 Z", + "revanced_disable_hdr_auto_brightness" to "revanced_disable_hdr_video", + "revanced_disable_hdr_video" to "M 650.615 587.692 L 650.615 372.308 L 772.539 372.308 Q 796.308 372.308 812 388 Q 827.692 403.692 827.692 427.461 L 827.692 452.308 Q 827.692 467.769 816.692 483.231 Q 805.692 498.692 784.077 503.769 L 819.846 587.692 L 787.769 587.692 L 754.462 507.462 L 680.154 507.462 L 680.154 587.692 L 650.615 587.692 Z M 680.154 477.154 L 773.539 477.154 Q 782.769 477.154 790.462 469.462 Q 798.154 461.769 798.154 452.539 L 798.154 426.461 Q 798.154 417.231 790.462 409.538 Q 782.769 401.846 773.539 401.846 L 680.154 401.846 L 680.154 477.154 Z M 132.308 587.692 L 132.308 372.308 L 161.846 372.308 L 161.846 456.692 L 279.846 456.692 L 279.846 372.308 L 309.385 372.308 L 309.385 587.692 L 279.846 587.692 L 279.846 486.231 L 161.846 486.231 L 161.846 587.692 L 132.308 587.692 Z M 391.077 587.692 L 391.077 372.308 L 513.769 372.308 Q 537.539 372.308 553.231 388 Q 568.923 403.692 568.923 427.461 L 568.923 532.539 Q 568.923 556.308 553.231 572 Q 537.539 587.692 513.769 587.692 L 391.077 587.692 Z M 421.385 558.154 L 514 558.154 Q 523.231 558.154 530.923 550.462 Q 538.615 542.769 538.615 533.539 L 538.615 426.461 Q 538.615 417.231 530.923 409.538 Q 523.231 401.846 514 401.846 L 421.385 401.846 L 421.385 558.154 Z", + "revanced_disable_quic_protocol" to "M 701.463 587.69 C 693.203 587.69 686.496 585.023 681.343 579.69 C 676.19 574.357 673.613 567.743 673.613 559.85 L 673.613 400.15 C 673.613 392.257 676.19 385.643 681.343 380.31 C 686.496 374.977 693.203 372.31 701.463 372.31 L 776.773 372.31 C 784.666 372.31 791.28 374.977 796.613 380.31 C 801.946 385.643 804.613 392.267 804.613 400.18 L 804.613 408.92 C 804.613 413.187 803.21 416.717 800.403 419.51 C 797.59 422.297 794.036 423.69 789.743 423.69 C 785.443 423.69 781.923 422.297 779.183 419.51 C 776.443 416.717 775.073 413.187 775.073 408.92 L 775.073 401.85 L 703.153 401.85 L 703.153 558.15 L 775.073 558.15 L 775.073 552.54 C 775.073 547.62 776.48 543.8 779.293 541.08 C 782.1 538.36 785.653 537 789.953 537 C 794.253 537 797.77 538.36 800.503 541.08 C 803.243 543.8 804.613 547.537 804.613 552.29 L 804.613 559.85 C 804.613 567.743 801.946 574.357 796.613 579.69 C 791.28 585.023 784.666 587.69 776.773 587.69 L 701.463 587.69 Z M 198.467 556.92 C 195.387 556.92 192.567 555.64 190.007 553.08 C 187.44 550.513 186.157 547.693 186.157 544.62 L 186.157 412.46 C 186.157 409.387 187.44 406.567 190.007 404 C 192.567 401.44 195.387 400.16 198.467 400.16 L 287.697 400.16 C 290.77 400.16 293.59 401.44 296.157 404 C 298.724 406.567 300.007 409.387 300.007 412.46 L 300.007 544.62 C 300.007 547.693 298.724 550.513 296.157 553.08 C 293.59 555.64 290.77 556.92 287.697 556.92 L 198.467 556.92 Z M 228.077 587.69 L 228.077 634.62 C 228.077 638.92 229.5 642.493 232.347 645.34 C 235.187 648.193 238.75 649.62 243.037 649.62 C 247.324 649.62 251.03 648.193 254.157 645.34 C 257.284 642.493 258.847 638.92 258.847 634.62 L 258.847 587.69 L 290.617 587.69 C 301.637 587.69 311.084 583.777 318.957 575.95 C 326.83 568.117 330.767 558.723 330.767 547.77 L 330.767 409.54 C 330.767 398.52 326.83 389.073 318.957 381.2 C 311.084 373.327 301.637 369.39 290.617 369.39 L 198.467 369.39 C 187.594 369.39 177.694 373.327 168.767 381.2 C 159.847 389.073 155.387 398.52 155.387 409.54 L 155.387 547.77 C 155.387 558.723 159.847 568.117 168.767 575.95 C 177.694 583.777 187.594 587.69 198.467 587.69 L 228.077 587.69 Z M 603.383 587.69 C 607.803 587.69 611.436 586.217 614.283 583.27 C 617.13 580.323 618.553 576.67 618.553 572.31 L 618.553 384.77 C 618.553 380.41 617.056 376.757 614.063 373.81 C 611.076 370.863 607.373 369.39 602.953 369.39 C 598.533 369.39 594.9 370.863 592.053 373.81 C 589.206 376.757 587.783 380.41 587.783 384.77 L 587.783 572.31 C 587.783 576.67 589.276 580.323 592.263 583.27 C 595.256 586.217 598.963 587.69 603.383 587.69 Z M 422.849 587.69 C 412.898 587.474 403.322 583.483 396.251 576.726 C 389.491 569.651 385.598 560.108 385.386 550.164 L 385.391 383.745 C 385.619 380.204 387.416 376.098 389.659 373.551 L 389.726 373.476 L 389.764 373.442 L 389.807 373.403 C 392.36 371.18 396.379 369.39 400.022 369.39 C 403.719 369.39 407.925 371.442 410.4 373.67 L 410.485 373.747 L 410.525 373.794 L 410.558 373.833 C 412.726 376.416 414.251 380.469 414.448 383.936 L 414.445 550.314 C 414.278 552.931 414.659 554.287 416.053 555.678 L 416.085 555.646 L 416.166 555.728 L 416.232 555.661 L 416.841 556.398 L 417.501 557.006 L 417.441 557.069 C 418.83 558.472 420.242 558.785 422.922 558.628 L 497.335 558.631 C 500.001 558.8 501.355 558.41 502.769 556.954 L 502.705 556.883 L 503.363 556.288 L 503.964 555.57 L 504.028 555.635 L 504.358 555.309 C 505.529 554.017 505.788 552.68 505.65 550.154 L 505.655 383.746 C 505.882 380.205 507.682 376.094 509.929 373.545 L 510.026 373.447 L 510.072 373.405 C 512.617 371.198 516.639 369.39 520.288 369.39 C 523.981 369.39 528.187 371.438 530.663 373.67 L 530.753 373.75 L 530.797 373.803 L 530.82 373.831 C 532.992 376.417 534.516 380.468 534.711 383.936 L 534.711 550.227 C 534.495 560.178 530.505 569.753 523.747 576.825 C 516.67 583.587 507.131 587.478 497.184 587.69 L 422.849 587.69 Z", + "revanced_enable_debug_logging" to "M 243.08 447.46 L 243.08 413.51 Q 243.08 352.38 270.69 302.62 Q 298.31 252.85 344.62 220.23 L 281.15 156.77 L 311 126.15 L 382.81 197.85 Q 404.16 187.38 429.23 182.04 Q 454.31 176.69 479.84 176.69 Q 505.38 176.69 530.65 182.04 Q 555.92 187.38 577.31 197.85 L 649 126.15 L 678.85 156.77 L 615.38 220.23 Q 661.69 252.85 689.31 302.67 Q 716.92 352.5 716.92 413.55 L 716.92 447.46 L 243.08 447.46 Z M 581.54 379.77 Q 595.92 379.77 605.65 369.65 Q 615.38 359.54 615.38 345.92 Q 615.38 331.54 605.65 321.81 Q 595.92 312.08 581.54 312.08 Q 567.15 312.08 557.42 321.81 Q 547.69 331.54 547.69 345.92 Q 547.69 359.54 557.42 369.65 Q 567.15 379.77 581.54 379.77 Z M 378.46 379.77 Q 392.85 379.77 402.58 369.65 Q 412.31 359.54 412.31 345.92 Q 412.31 331.54 402.58 321.81 Q 392.85 312.08 378.46 312.08 Q 364.08 312.08 354.35 321.81 Q 344.62 331.54 344.62 345.92 Q 344.62 359.54 354.35 369.65 Q 364.08 379.77 378.46 379.77 Z M 480 853.85 Q 381 853.85 312.04 784.88 Q 243.08 715.92 243.08 616.92 L 243.08 481.31 L 716.92 481.31 L 716.92 617.1 Q 716.92 716.23 647.96 785.04 Q 579 853.85 480 853.85 Z", + "revanced_disable_default_playback_speed_music" to "M 388.351 504.732 C 389.685 483.86 397.531 468.552 411.89 458.809 L 706.121 252.116 L 499.351 546.501 C 490.121 560.501 474.851 568.257 453.543 569.77 C 432.236 571.283 415.71 566.04 403.966 554.04 C 392.223 542.039 387.018 525.603 388.351 504.732 Z M 443.79 133.952 C 480.252 133.952 514.791 138.836 547.406 148.605 C 580.022 158.374 612.509 174.157 644.867 195.952 L 619.021 216.029 C 592.098 198.593 563.521 185.683 533.29 177.298 C 503.06 168.913 473.29 164.721 443.982 164.721 C 352.756 164.721 275.25 196.934 211.466 261.36 C 147.682 325.786 115.79 403.821 115.79 495.465 C 115.79 524.405 119.701 553.311 127.521 582.183 C 135.342 611.054 146.731 637.977 161.688 662.952 L 543.819 662.952 C 548.08 674.91 555.02 685.448 563.853 693.721 L 163.175 693.721 C 157.413 693.721 151.742 692.042 146.161 688.683 C 140.581 685.324 136.149 680.618 132.867 674.567 C 119.637 650.772 108.355 624.298 99.021 595.144 C 89.688 565.99 85.021 532.746 85.021 495.413 C 85.021 446.234 94.347 399.742 112.999 355.939 C 131.651 312.136 157.074 273.828 189.268 241.016 C 221.462 208.204 259.478 182.157 303.316 162.875 C 347.154 143.593 393.979 133.952 443.79 133.952 Z M 774.467 412.847 C 766.903 381.796 754.249 353.629 736.505 328.347 L 757.044 302.501 C 779.3 338.296 794.582 371.565 802.89 402.308 C 802.963 402.579 803.036 402.851 803.109 403.123 C 792.96 406.195 783.516 410.223 774.998 415.049 C 774.823 414.316 774.646 413.582 774.467 412.847 Z M 762.978 475.278 L 874.978 475.278 L 874.978 527.578 L 793.518 527.578 L 793.518 743.348 C 793.518 767.548 785.788 787.381 770.328 802.848 C 754.868 818.314 735.175 826.048 711.248 826.048 C 687.322 826.048 667.615 818.318 652.128 802.858 C 636.642 787.398 628.898 767.704 628.898 743.778 C 628.898 719.851 636.632 700.144 652.098 684.658 C 667.565 669.171 687.398 661.428 711.598 661.428 C 722.418 661.428 732.238 663.018 741.058 666.198 C 749.878 669.378 757.185 674.148 762.978 680.508 L 762.978 475.278 Z", + "revanced_enable_default_playback_speed_shorts" to "M 359.453 489.454 C 360.787 468.582 368.633 453.274 382.992 443.531 L 677.223 236.838 L 470.453 531.223 C 461.223 545.223 445.953 552.979 424.645 554.492 C 403.338 556.005 386.812 550.762 375.068 538.762 C 363.325 526.761 358.12 510.325 359.453 489.454 Z M 745.569 397.569 C 738.005 366.518 725.351 338.351 707.607 313.069 L 728.146 287.223 C 750.402 323.018 765.684 356.287 773.992 387.03 C 774.065 387.301 774.138 387.573 774.211 387.845 C 764.062 390.917 754.618 394.945 746.1 399.771 C 745.925 399.038 745.748 398.304 745.569 397.569 Z M 414.892 118.674 C 451.354 118.674 485.893 123.558 518.508 133.327 C 551.124 143.096 583.611 158.879 615.969 180.674 L 590.123 200.751 C 563.2 183.315 534.623 170.405 504.392 162.02 C 474.162 153.635 444.392 149.443 415.084 149.443 C 323.858 149.443 246.352 181.656 182.568 246.082 C 118.784 310.508 86.892 388.543 86.892 480.187 C 86.892 509.127 90.803 538.033 98.623 566.905 C 106.444 595.776 117.833 622.699 132.79 647.674 L 514.921 647.674 C 519.182 659.632 526.122 670.17 534.955 678.443 L 134.277 678.443 C 128.515 678.443 122.844 676.764 117.263 673.405 C 111.683 670.046 107.251 665.34 103.969 659.289 C 90.739 635.494 79.457 609.02 70.123 579.866 C 60.79 550.712 56.123 517.468 56.123 480.135 C 56.123 430.956 65.449 384.464 84.101 340.661 C 102.753 296.858 128.176 258.55 160.37 225.738 C 192.564 192.926 230.58 166.879 274.418 147.597 C 318.256 128.315 365.081 118.674 414.892 118.674 Z M 682.995 564.381 L 682.995 696.604 L 800.874 630.492 L 682.995 564.381 Z M 612.907 683.92 L 612.908 683.919 C 612.907 683.919 612.907 683.92 612.907 683.92 C 612.909 683.921 612.911 683.921 612.913 683.922 L 612.907 683.92 C 593.933 697.159 580.671 713.919 573.324 736.248 C 566.428 759.47 568.87 783.009 580.461 802.873 C 592.731 822.323 612.392 835.486 636.161 840.05 C 659.897 843.888 684.498 839.032 706.555 825.966 L 855.105 735.796 C 876.874 722.259 892.527 702.812 900.057 679.937 C 906.952 656.708 904.476 633.206 892.932 613.332 C 881.132 594.701 864.919 583.403 844.452 577.804 C 844.45 577.803 844.448 577.803 844.447 577.802 L 844.452 577.804 C 863.431 564.568 876.721 547.808 884.043 525.458 C 890.98 502.224 888.454 478.733 876.911 458.869 C 864.615 439.462 844.975 426.242 821.2 421.672 C 797.466 417.837 772.863 422.689 750.805 435.758 L 602.256 525.926 C 580.488 539.463 564.832 558.911 557.304 581.785 C 550.409 605.015 552.884 628.516 564.428 648.39 C 576.228 667.022 592.442 678.321 612.907 683.92 Z M 643.932 657.897 C 643.129 657.817 634.043 657.083 627.007 655.824 C 610.048 653.202 599.058 645.775 591.149 631.98 C 582.525 618.598 581.333 605.514 586.834 589.291 C 591.749 572.69 601.41 560.899 617.679 551.326 L 766.238 461.145 C 782.227 451.135 797.371 448.141 814.36 451.442 C 831.296 454.03 841.853 461.395 850.093 475.129 C 858.476 488.772 860.01 501.801 854.506 517.969 C 849.6 534.558 839.473 546.631 823.24 556.21 L 801 570.414 L 801.019 570.353 L 793.393 581.429 L 792.678 585.747 L 792.601 586.92 L 792.62 587.034 L 793.009 589.379 C 793.254 590.83 793.656 592.328 794.11 593.48 L 794.11 593.637 L 794.279 593.889 L 794.449 594.144 C 797.503 598.742 805.139 603.076 812.123 603.918 C 814.478 604.013 822.365 604.459 830.355 605.9 C 847.321 608.516 858.303 615.948 866.211 629.745 C 874.836 643.125 876.027 656.207 870.526 672.432 C 865.61 689.025 855.951 700.823 839.682 710.396 L 691.121 800.577 C 675.134 810.587 659.994 813.584 643.005 810.282 C 626.034 807.668 615.079 800.237 607.17 786.441 C 598.544 773.061 597.353 759.972 602.855 743.754 C 607.752 727.171 618.089 714.964 634.311 705.377 C 635.692 704.407 651.771 693.118 653.826 691.735 C 658.863 688.131 664.096 681.167 665.292 675.364 L 665.328 675.185 L 665.34 673.099 L 665.316 672.991 L 665.297 672.901 C 664.995 671.507 664.363 669.906 663.532 668.373 L 658.182 662.064 L 652.56 659.706 C 650.022 658.928 647.023 658.281 643.932 657.897 Z", + "revanced_enable_external_browser" to "M 675.38 697.62 L 675.38 806 Q 675.38 812.6 670.94 816.99 Q 666.5 821.38 659.82 821.38 Q 653.15 821.38 648.88 816.99 Q 644.62 812.6 644.62 806 L 644.62 667.69 Q 644.62 658.27 651.44 651.44 Q 658.27 644.62 667.69 644.62 L 806 644.62 Q 812.6 644.62 816.99 649.06 Q 821.38 653.5 821.38 660.18 Q 821.38 666.85 816.99 671.12 Q 812.6 675.38 806 675.38 L 696.62 675.38 L 829 807.77 Q 833.38 812.15 833.38 818.54 Q 833.38 824.93 829.39 829.16 Q 824.62 834.15 817.92 833.77 Q 811.22 833.38 806.77 829 L 675.38 697.62 Z M 480 840 Q 405.46 840 339.77 811.58 Q 274.08 783.15 225.46 734.54 Q 176.85 685.92 148.42 620.48 Q 120 555.04 120 481.23 Q 120 405.43 148.42 340.07 Q 176.85 274.72 225.46 225.78 Q 274.08 176.85 339.77 148.42 Q 405.46 120 480 120 Q 554.54 120 620.23 148.42 Q 685.92 176.85 734.54 225.78 Q 783.15 274.72 811.58 340.07 Q 840 405.43 840 481.23 Q 840 493.69 839 506.65 Q 838 519.62 836 532.08 Q 835.54 539.15 829.77 544.19 Q 824 549.23 817.93 549.23 Q 811.85 549.23 807.81 545.12 Q 803.77 541 805 534.38 Q 807 521.15 808.12 507.42 Q 809.23 493.69 809.23 481.23 Q 809.23 456.17 805.52 431.12 Q 801.8 406.06 794.37 381.77 L 621.85 381.77 Q 626.15 406.31 628.15 431.28 Q 630.15 456.26 630.15 481.23 Q 630.15 493.69 629.15 506.65 Q 628.15 519.62 627.92 532.85 Q 626.92 538.92 622.28 544.08 Q 617.63 549.23 611.43 549.23 Q 604.46 549.23 600.31 545.23 Q 596.15 541.23 597.15 534.62 Q 598.38 521.38 598.88 507.54 Q 599.38 493.69 599.38 481.23 Q 599.38 456.17 597.5 431.12 Q 595.62 406.06 591.08 381.77 L 369.58 381.77 Q 365.38 406.31 363.12 431.12 Q 360.85 455.92 360.85 480.73 Q 360.85 505.54 363.12 529.85 Q 365.38 554.15 369.15 578.46 L 543.08 578.46 Q 549.67 578.46 554.07 582.91 Q 558.46 587.35 558.46 594.02 Q 558.46 600.69 554.07 604.96 Q 549.67 609.23 543.08 609.23 L 375.54 609.23 Q 391.08 665.31 414.54 717.85 Q 438 770.38 480 810.77 Q 494.56 810.77 507.97 809.27 Q 521.38 807.77 534.85 805.77 Q 540.46 804.54 544.85 808.69 Q 549.23 812.85 549.23 818.46 Q 549.23 824.91 545.08 829.73 Q 540.92 834.54 534.08 835.77 Q 520.62 837.77 507.42 838.88 Q 494.23 840 480 840 Z M 165.63 578.46 L 338.15 578.46 Q 334.12 554.15 332.1 529.85 Q 330.08 505.54 330.08 481.23 Q 330.08 456.17 331.85 431.12 Q 333.62 406.06 337.92 381.77 L 165.56 381.77 Q 158.16 406.11 154.47 431.22 Q 150.77 456.32 150.77 481.43 Q 150.77 505.77 154.48 530.13 Q 158.2 554.49 165.63 578.46 Z M 437.08 808.23 Q 403.38 764.15 380.12 713.77 Q 356.85 663.38 343.77 609.23 L 175.08 609.23 Q 211.15 689.31 280.69 742.46 Q 350.23 795.62 437.08 808.23 Z M 175.08 351 L 343.13 351 Q 356.08 296.08 378.58 245.19 Q 401.08 194.31 438.08 151.54 Q 350.92 165.54 281.65 218.35 Q 212.38 271.15 175.08 351 Z M 375.54 351 L 584.69 351 Q 570.92 294.69 544.85 243.15 Q 518.77 191.62 480 148.77 Q 440.46 191.08 414.77 242.88 Q 389.08 294.69 375.54 351 Z M 617.03 351 L 784.92 351 Q 747.62 271.15 678.46 217.85 Q 609.31 164.54 522.92 151.77 Q 556.85 195.85 578.96 246.73 Q 601.08 297.62 617.03 351 Z", + "revanced_enable_old_quality_layout" to "revanced_default_video_quality_wifi", + "revanced_enable_open_links_directly" to "M 589.23 392 L 589.23 510.62 Q 589.23 517.17 593.7 521.59 Q 598.17 526 604.82 526 Q 611.46 526 615.73 521.59 Q 620 517.17 620 510.62 L 620 367.69 Q 620 355.9 612.05 347.95 Q 604.1 340 592.31 340 L 449.38 340 Q 442.83 340 438.41 344.47 Q 434 348.94 434 355.59 Q 434 362.23 438.41 366.5 Q 442.83 370.77 449.38 370.77 L 567 370.77 L 340.38 597.38 Q 336 601.58 336 608.06 Q 336 614.55 339.99 618.77 Q 344.77 623.77 351.47 623.38 Q 358.16 623 362.62 618.62 L 589.23 392 Z M 480.4 840 Q 405.22 840 340.11 811.66 Q 274.99 783.32 225.86 734.24 Q 176.73 685.16 148.37 620.03 Q 120 554.89 120 479.63 Q 120 405.14 148.34 339.56 Q 176.68 273.99 225.76 225.36 Q 274.84 176.73 339.97 148.37 Q 405.11 120 480.37 120 Q 554.86 120 620.44 148.34 Q 686.01 176.68 734.64 225.26 Q 783.27 273.84 811.63 339.52 Q 840 405.19 840 479.6 Q 840 554.78 811.66 619.89 Q 783.32 685.01 734.74 733.96 Q 686.16 782.9 620.48 811.45 Q 554.81 840 480.4 840 Z M 480.5 809.23 Q 617.38 809.23 713.31 713.19 Q 809.23 617.15 809.23 479.5 Q 809.23 342.62 713.5 246.69 Q 617.76 150.77 480 150.77 Q 342.85 150.77 246.81 246.5 Q 150.77 342.24 150.77 480 Q 150.77 617.15 246.81 713.19 Q 342.85 809.23 480.5 809.23 Z M 480 480 Z", + "revanced_enable_opus_codec" to "M 498.445 225.191 C 409.356 225.838 332.555 239.944 268.01 267.467 C 188.575 301.394 135.618 351.697 109.18 418.362 C 101.242 438.803 96.979 458.062 96.352 476.146 C 88.714 607.252 172.458 681.097 224.169 713.924 C 244.921 727.904 263.372 734.832 263.372 734.832 L 287.071 658.597 L 294.079 639.858 C 325.275 645.791 359.951 649.928 398.303 652.167 C 515.733 659.061 614.148 645.566 693.583 611.631 C 773.017 577.737 825.537 527.842 851.135 461.903 C 876.732 396.008 864.533 342.108 814.505 300.154 L 814.473 300.178 C 775.008 267.112 714.252 244.985 632.37 233.678 L 572.196 426.243 C 570.897 433.312 568.657 441.327 565.19 450.572 C 559.043 466.019 551.647 478.747 542.959 488.769 C 534.258 498.807 524.523 506.634 513.737 512.347 C 502.941 518.017 491.197 521.948 478.518 524.105 C 465.825 526.254 452.715 526.918 439.159 526.125 C 425.585 525.33 414.037 523.471 404.537 520.539 C 395.028 517.61 386.282 512.762 378.304 505.965 C 372.411 499.296 368.985 490.546 368.048 479.692 C 367.107 468.851 369.725 455.441 375.884 439.461 C 381.399 425.573 388.375 413.463 396.83 403.174 C 405.276 392.881 415.157 384.897 426.462 379.237 C 437.255 373.565 448.857 369.761 461.278 367.847 C 473.665 365.944 487.441 365.446 502.556 366.332 C 515.082 367.074 526.741 368.933 537.562 371.95 C 542.691 373.364 547.191 375.238 551.102 377.488 L 597.276 229.622 C 585.876 228.54 574.214 227.586 562.088 226.888 C 540.223 225.599 519.004 225.042 498.445 225.191 Z M 518.855 249.594 C 532.487 249.804 546.427 250.324 560.678 251.16 L 560.698 251.16 C 562.213 251.25 563.612 251.406 565.109 251.5 L 535.4 346.648 C 525.29 344.382 514.888 342.713 503.978 342.062 L 503.954 342.062 C 487.461 341.097 472.038 341.603 457.634 343.816 L 457.626 343.816 C 442.789 346.102 428.615 350.728 415.512 357.582 C 401.195 364.78 388.585 375.037 378.204 387.688 C 367.985 400.129 359.768 414.509 353.446 430.424 L 353.407 430.541 L 353.357 430.663 C 346.385 448.758 342.547 465.348 343.979 481.818 C 345.259 496.642 350.489 511.07 360.254 522.123 L 361.391 523.411 L 362.7 524.521 C 372.993 533.291 384.783 539.883 397.476 543.79 C 409.48 547.489 422.794 549.525 437.751 550.399 C 452.997 551.29 467.958 550.539 482.525 548.071 L 482.54 548.071 C 497.555 545.518 511.798 540.792 524.906 533.909 L 524.946 533.885 L 524.987 533.872 C 538.522 526.703 550.686 516.838 561.168 504.753 C 572.054 492.194 580.69 477.027 587.621 459.617 L 587.714 459.387 L 587.799 459.155 C 591.346 449.699 593.758 441.116 595.389 433.03 L 649.024 261.425 C 717.399 273.095 767.93 292.827 799.018 318.869 L 799.418 319.208 C 820.988 337.418 832.841 356.322 837.627 377.455 C 842.446 398.732 840.147 423.42 828.635 453.052 L 828.635 453.061 C 805.611 512.368 759.271 557.188 684.154 589.241 L 684.147 589.249 C 609.196 621.269 514.533 634.631 399.718 627.888 L 399.71 627.888 C 362.232 625.704 328.564 621.677 298.567 615.969 L 278.621 612.173 L 264.224 650.675 L 248.83 700.171 C 244.984 697.977 241.9 696.607 237.614 693.718 L 237.338 693.538 L 237.059 693.363 C 189.221 662.996 113.458 597.771 120.463 477.563 L 120.487 477.28 L 120.495 476.99 C 121.009 462.165 124.543 445.616 131.66 427.278 C 155.541 367.135 202.398 321.901 277.438 289.85 C 333.686 265.863 400.767 252.328 478.881 249.884 C 491.901 249.475 505.222 249.379 518.855 249.594 Z", + "revanced_enable_save_and_restore_brightness" to "M 618.34 800 L 550.11 800 Q 544.08 800 539.81 795.73 Q 535.54 791.46 535.54 785.77 L 535.54 717.26 L 496.34 677.86 Q 492.23 674.13 492.23 667.81 Q 492.23 661.49 496.46 657.57 L 535.54 618.34 L 535.54 550.11 Q 535.54 544.08 539.81 539.81 Q 544.08 535.54 549.77 535.54 L 618.28 535.54 L 657.68 496.34 Q 661.41 492.23 667.73 492.23 Q 674.05 492.23 678 496.46 L 717.5 535.54 L 786.19 535.54 Q 791.92 535.54 795.96 539.81 Q 800 544.08 800 549.77 L 800 618.28 L 839.2 657.68 Q 843.31 661.41 843.31 667.73 Q 843.31 674.05 839.08 677.97 L 800 717.2 L 800 785.43 Q 800 791.46 795.96 795.73 Q 791.92 800 786.54 800 L 717.56 800 L 677.9 839.2 Q 674.13 843.31 667.81 843.31 Q 661.49 843.31 657.57 839.08 L 618.34 800 Z M 667.77 767.38 Q 708.85 767.38 738.12 738.06 Q 767.38 708.73 767.38 667.57 Q 767.38 626.42 738.07 597.67 Q 708.76 568.92 667.77 568.92 L 667.77 767.38 Z M 480 190.77 Q 359.14 190.77 274.96 274.96 Q 190.77 359.14 190.77 480 Q 190.77 568.98 237.73 639.18 Q 284.69 709.38 360 743.23 L 360 624.62 Q 360 618.08 364.48 613.65 Q 368.97 609.23 375.6 609.23 Q 382.23 609.23 386.5 613.65 Q 390.77 618.08 390.77 624.62 L 390.77 772.31 Q 390.77 784.08 382.81 792.04 Q 374.85 800 363.08 800 L 215.38 800 Q 208.85 800 204.42 795.52 Q 200 791.03 200 784.4 Q 200 777.77 204.42 773.5 Q 208.85 769.23 215.38 769.23 L 344.85 769.23 Q 263.38 729.69 211.69 652.62 Q 160 575.54 160 480 Q 160 413.4 185.04 355.24 Q 210.08 297.08 253.58 253.58 Q 297.08 210.08 355.27 185.04 Q 413.46 160 480.33 160 Q 585.47 160 667.73 221.85 Q 750 283.69 782.46 379.31 Q 784.92 385.15 782.07 390.54 Q 779.22 395.93 773.3 397.74 Q 766.62 400.31 761.38 397 Q 756.15 393.69 753.69 387.85 Q 724.69 302 649.99 246.38 Q 575.28 190.77 480 190.77 Z", + "revanced_enable_swipe_brightness" to "M 480.231 876.385 L 362.75 760 L 200 760 L 200 597.25 L 82.153 480 L 200 362.75 L 200 200 L 362.75 200 L 480.231 82.154 L 597.25 200 L 760 200 L 760 362.75 L 877.846 480 L 760 597.25 L 760 760 L 597.25 760 L 480.231 876.385 Z M 480.231 646.308 Q 549.692 646.308 598.615 597.87 Q 647.539 549.432 647.539 479.901 Q 647.539 410.37 598.601 361.416 Q 549.664 312.461 480.231 312.461 L 480.231 646.308 Z M 480.231 832.385 L 583.981 729.231 L 729.231 729.231 L 729.231 584.513 L 833.615 480 L 729.103 375.487 L 729.103 230.769 L 584.385 230.769 L 480.231 126.385 L 375.615 230.769 L 230.897 230.769 L 230.897 375.487 L 126.385 480 L 230.769 584.513 L 230.769 729.231 L 375.385 729.231 L 480.231 832.385 Z M 480 479.769 Z", + "revanced_enable_swipe_haptic_feedback" to "M 74.06 381.92 Q 80.69 381.92 84.96 386.72 Q 89.23 391.52 89.23 397.31 L 89.23 561.92 Q 89.23 568.46 84.75 572.88 Q 80.26 577.31 73.63 577.31 Q 67 577.31 62.73 572.88 Q 58.46 568.46 58.46 561.92 L 58.46 397.31 Q 58.46 391.52 62.95 386.72 Q 67.43 381.92 74.06 381.92 Z M 184.83 298.54 Q 191.46 298.54 195.73 302.96 Q 200 307.38 200 313.92 L 200 646.08 Q 200 652.62 195.51 657.04 Q 191.03 661.46 184.4 661.46 Q 177.77 661.46 173.5 657.04 Q 169.23 652.62 169.23 646.08 L 169.23 313.92 Q 169.23 307.38 173.72 302.96 Q 178.2 298.54 184.83 298.54 Z M 886.37 381.92 Q 893 381.92 897.27 386.72 Q 901.54 391.52 901.54 397.31 L 901.54 561.92 Q 901.54 568.46 897.05 572.88 Q 892.57 577.31 885.94 577.31 Q 879.31 577.31 875.04 572.88 Q 870.77 568.46 870.77 561.92 L 870.77 397.31 Q 870.77 391.52 875.25 386.72 Q 879.74 381.92 886.37 381.92 Z M 775.6 298.54 Q 782.23 298.54 786.5 302.96 Q 790.77 307.38 790.77 313.92 L 790.77 646.08 Q 790.77 652.62 786.28 657.04 Q 781.8 661.46 775.17 661.46 Q 768.54 661.46 764.27 657.04 Q 760 652.62 760 646.08 L 760 313.92 Q 760 307.38 764.49 302.96 Q 768.97 298.54 775.6 298.54 Z M 335.38 800 Q 313.13 800 296.57 783.43 Q 280 766.87 280 744.62 L 280 215.38 Q 280 193.13 296.57 176.57 Q 313.13 160 335.38 160 L 624.62 160 Q 646.87 160 663.43 176.57 Q 680 193.13 680 215.38 L 680 744.62 Q 680 766.87 663.43 783.43 Q 646.87 800 624.62 800 L 335.38 800 Z M 335.38 769.23 L 624.62 769.23 Q 635.38 769.23 642.31 762.31 Q 649.23 755.38 649.23 744.62 L 649.23 215.38 Q 649.23 204.62 642.31 197.69 Q 635.38 190.77 624.62 190.77 L 335.38 190.77 Q 324.62 190.77 317.69 197.69 Q 310.77 204.62 310.77 215.38 L 310.77 744.62 Q 310.77 755.38 317.69 762.31 Q 324.62 769.23 335.38 769.23 Z M 310.77 769.23 L 310.77 190.77 L 310.77 769.23 Z", + "revanced_enable_swipe_lowest_value_auto_brightness" to "M 326.154 646.539 L 360.385 646.539 L 403.538 534.462 L 558.692 534.462 L 602.846 646.539 L 637.385 646.539 L 490.385 272.231 L 472.385 272.231 L 326.154 646.539 Z M 413.923 504.923 L 477.615 338.077 L 482.231 338.077 L 548.077 504.923 L 413.923 504.923 Z M 480.231 876.385 L 362.923 760 L 200 760 L 200 597.077 L 82.153 480 L 200 362.923 L 200 200 L 362.923 200 L 480.231 82.154 L 597.077 200 L 760 200 L 760 362.923 L 877.846 480 L 760 597.077 L 760 760 L 597.077 760 L 480.231 876.385 Z M 480.231 832.385 L 584.064 729.231 L 729.231 729.231 L 729.231 584.513 L 833.615 480 L 729.103 375.487 L 729.103 230.769 L 584.385 230.769 L 480.231 126.385 L 375.615 230.769 L 230.897 230.769 L 230.897 375.487 L 126.385 480 L 230.769 584.513 L 230.769 729.231 L 375.385 729.231 L 480.231 832.385 Z M 480.231 479.769 Z", + "revanced_enable_swipe_press_to_engage" to "M 458.85 840 Q 425.23 840 394.77 826.46 Q 364.31 812.92 344.77 786.85 L 164.54 547.77 Q 160.62 542.62 160.62 536.23 Q 160.62 529.85 164.77 524.92 L 167.23 523.23 Q 174.31 515.15 184.62 513.19 Q 194.92 511.23 204.85 515.85 L 336.92 578.85 L 336.92 256.92 Q 336.92 250.38 341.41 245.96 Q 345.89 241.54 352.52 241.54 Q 359.15 241.54 363.42 245.96 Q 367.69 250.38 367.69 256.92 L 367.69 583.43 Q 367.69 599.29 354.73 607.22 Q 341.77 615.15 328.31 608.69 L 202.92 548 L 372 769.85 Q 387.46 790.46 410.55 799.85 Q 433.64 809.23 458.85 809.23 L 623.85 809.23 Q 668 809.23 698.62 779 Q 729.23 748.77 729.23 704.62 L 729.23 558.46 Q 729.23 530.35 710.21 511.33 Q 691.19 492.31 663.08 492.31 L 493.85 492.31 Q 487.31 492.31 482.88 487.82 Q 478.46 483.34 478.46 476.71 Q 478.46 470.08 482.88 465.81 Q 487.31 461.54 493.85 461.54 L 663.08 461.54 Q 703.46 461.54 731.73 489.81 Q 760 518.08 760 558.46 L 760 704.56 Q 760 761.46 720.23 800.73 Q 680.45 840 623.85 840 L 458.85 840 Z M 466.46 635.38 Z M 465.6 337.69 Q 458.77 337.69 454.5 333.34 Q 450.23 328.99 450.23 322.56 Q 450.23 320.04 451.92 314.23 Q 459.69 301.46 463.69 286.98 Q 467.69 272.49 467.69 256.45 Q 467.69 208.68 433.99 175.11 Q 400.29 141.54 352.14 141.54 Q 304 141.54 270.46 175.19 Q 236.92 208.85 236.92 256.93 Q 236.92 272.69 240.92 287.08 Q 244.92 301.46 252.69 314.23 Q 253.31 316.27 253.85 317.91 Q 254.38 319.54 254.38 322.68 Q 254.38 329.09 249.97 333.39 Q 245.56 337.69 238.55 337.69 Q 234.62 337.69 230.95 335.62 Q 227.28 333.54 225.68 329.55 Q 215.62 313.46 210.88 294.89 Q 206.15 276.32 206.15 256.27 Q 206.15 195.92 248.96 153.35 Q 291.76 110.77 352.31 110.77 Q 412.86 110.77 455.66 153.35 Q 498.46 195.92 498.46 256.63 Q 498.46 276.38 493.53 295.05 Q 488.59 313.73 479.54 329.38 Q 477.26 333.54 473.64 335.62 Q 470.02 337.69 465.6 337.69 Z", + "revanced_enable_swipe_to_switch_video" to "M 218.38 814.62 Q 157.92 743.54 120.5 659.08 Q 83.08 574.62 83.08 480.77 Q 83.08 386.92 120.5 302.46 Q 157.92 218 218.38 146.15 L 133.08 146.15 Q 127.38 146.15 123.69 142.46 Q 120 138.77 120 133.08 Q 120 127.38 123.69 123.69 Q 127.38 120 133.08 120 L 246.92 120 Q 259.15 120 266.88 127.73 Q 274.62 135.46 274.62 147.69 L 274.62 263.08 Q 274.62 268 271.31 271.69 Q 268 275.38 262.31 275.38 Q 257.38 275.38 253.31 271.31 Q 249.23 267.23 249.23 262.31 L 249.23 150.08 Q 187.31 221.23 147.88 304.19 Q 108.46 387.15 108.46 480.77 Q 108.46 573.62 147.88 656.19 Q 187.31 738.77 249.23 809.15 L 249.23 697.69 Q 249.23 692.77 253.31 689.08 Q 257.38 685.38 262.31 685.38 Q 267.23 685.38 270.92 689.08 Q 274.62 692.77 274.62 697.69 L 274.62 812.31 Q 274.62 824.54 266.88 832.27 Q 259.15 840 246.92 840 L 132.31 840 Q 127.38 840 123.69 835.92 Q 120 831.85 120 826.92 Q 120 822 124.08 818.31 Q 128.15 814.62 133.08 814.62 L 218.38 814.62 Z M 646 792.46 Q 629.08 798.92 610.15 798.31 Q 591.23 797.69 573.54 789.23 L 334.92 679.31 Q 328 675.85 325.42 668.27 Q 322.85 660.69 325.85 653.54 L 325.08 653.85 Q 328.54 643.31 336.77 637.08 Q 345 630.85 356.54 629.62 L 484.77 616.23 L 367.77 297.77 Q 365.08 291.15 368.27 285.65 Q 371.46 280.15 378.08 278.23 Q 383.92 275.54 389.54 278.35 Q 395.15 281.15 397.85 287.77 L 515.77 610.15 Q 520.77 622.62 513.15 634.35 Q 505.54 646.08 492.08 648.08 L 364.62 659.15 L 586.46 762.15 Q 597.77 767.69 610.85 767.69 Q 623.92 767.69 636 763.92 L 774.23 712.85 Q 817.08 697.31 836.5 656.73 Q 855.92 616.15 840.38 573.31 L 782.23 414.31 Q 779.54 407.69 781.96 402.46 Q 784.38 397.23 791 394.54 Q 797.62 391.85 803.23 394.27 Q 808.85 396.69 811.54 403.31 L 868.69 562.31 Q 889.38 617.62 864.96 669.81 Q 840.54 722 785.23 741.92 L 646 792.46 Z M 521.92 380.38 Q 519.23 373.77 522.42 368.65 Q 525.62 363.54 532.23 360.85 Q 538.08 358.15 543.58 361.35 Q 549.08 364.54 551.77 370.38 L 602.85 510.85 Q 605.54 516.46 602.73 522.08 Q 599.92 527.69 594.08 530.38 Q 588.46 533.08 582.46 529.77 Q 576.46 526.46 573.77 520.85 L 521.92 380.38 Z M 645.46 378.46 Q 642.77 371.85 645.58 366.62 Q 648.38 361.38 655 358.69 Q 661.62 356.77 667.12 359.19 Q 672.62 361.62 674.54 368.23 L 712.62 469.92 Q 715.31 475.77 712 482.15 Q 708.69 488.54 702.08 491.23 Q 696.23 492.92 690.73 490.23 Q 685.23 487.54 682.54 480.92 L 645.46 378.46 Z M 677.77 612.92 Z", + "revanced_enable_swipe_volume" to "M 563.077 779.77 L 563.077 747 Q 650.077 716.692 703.5 643.346 Q 756.923 570 756.923 479 Q 756.923 388 703.615 314.154 Q 650.308 240.308 563.077 211 L 563.077 178.23 Q 662.462 210.077 725.077 292.577 Q 787.693 375.077 787.693 479 Q 787.693 582.923 725.077 665.423 Q 662.462 747.923 563.077 779.77 Z M 172.307 560 L 172.307 400 L 309.231 400 L 452.308 256.922 L 452.308 703.078 L 309.231 560 L 172.307 560 Z M 553.846 615.693 L 553.846 342.538 Q 591.923 361.846 612.115 399.231 Q 632.308 436.615 632.308 480 Q 632.308 523.154 611.615 559.769 Q 590.923 596.385 553.846 615.693 Z M 421.538 334.308 L 323.154 430.769 L 203.077 430.769 L 203.077 529.231 L 323.154 529.231 L 421.538 625.923 L 421.538 334.308 Z M 324.462 480 Z", + "revanced_enable_watch_panel_gestures" to "revanced_preference_screen_swipe_controls", + "revanced_hide_clip_button" to "M 691.927 718.775 L 482.213 509.06 L 388.313 602.96 C 393.041 610.46 396.338 617.907 398.204 625.301 C 400.071 632.695 401.004 640.782 401.004 649.563 C 401.004 674.571 392.509 695.576 375.519 712.576 C 358.53 729.576 337.539 738.077 312.546 738.077 C 287.554 738.077 266.544 729.582 249.517 712.592 C 232.49 695.603 223.976 674.612 223.976 649.619 C 223.976 624.627 232.477 603.617 249.477 586.59 C 266.477 569.563 287.482 561.049 312.49 561.049 C 321.164 561.049 329.366 562.204 337.098 564.515 C 344.829 566.825 352.552 570.291 360.267 574.912 L 453.473 481.173 L 359.413 387.114 C 351.912 391.166 344.385 394.152 336.831 396.072 C 329.277 397.991 321.164 398.951 312.49 398.951 C 287.482 398.951 266.477 390.456 249.477 373.466 C 232.477 356.477 223.976 335.487 223.976 310.493 C 223.976 285.501 232.471 264.492 249.461 247.464 C 266.45 230.437 287.441 221.923 312.434 221.923 C 337.426 221.923 358.436 230.424 375.463 247.424 C 392.49 264.425 401.004 285.429 401.004 310.437 C 401.004 319.218 400.16 327.447 398.471 335.125 C 396.783 342.803 393.734 350.108 389.327 357.04 L 736.024 703.737 L 736.024 718.775 L 691.927 718.775 Z M 552.278 441.555 L 522.524 411.801 L 691.927 242.399 L 736.024 242.399 L 736.024 257.275 L 552.278 441.555 Z M 312.49 377.623 C 330.974 377.623 346.793 371.046 359.947 357.894 C 373.098 344.741 379.676 328.922 379.676 310.437 C 379.676 291.953 373.098 276.134 359.947 262.981 C 346.793 249.829 330.974 243.252 312.49 243.252 C 294.006 243.252 278.186 249.829 265.034 262.981 C 251.881 276.134 245.304 291.953 245.304 310.437 C 245.304 328.922 251.881 344.741 265.034 357.894 C 278.186 371.046 294.006 377.623 312.49 377.623 Z M 483.066 481.013 C 482.71 481.013 482.595 481.04 482.72 481.093 L 482.906 481.173 C 482.906 481.528 482.844 481.643 482.72 481.52 C 482.595 481.395 482.71 481.333 483.066 481.333 L 483.146 481.52 C 483.199 481.643 483.226 481.528 483.226 481.173 L 483.146 481.093 L 483.066 481.013 Z M 312.49 716.748 C 330.974 716.748 346.793 710.171 359.947 697.019 C 373.098 683.866 379.676 668.047 379.676 649.563 C 379.676 631.078 373.098 615.259 359.947 602.106 C 346.793 588.954 330.974 582.377 312.49 582.377 C 294.006 582.377 278.186 588.954 265.034 602.106 C 251.881 615.259 245.304 631.078 245.304 649.563 C 245.304 668.047 251.881 683.866 265.034 697.019 C 278.186 710.171 294.006 716.748 312.49 716.748 Z", + "revanced_hide_download_button" to "revanced_overlay_button_external_downloader", + "revanced_hide_keyword_content_comments" to "revanced_hide_quick_actions_comment_button", + "revanced_hide_keyword_content_home" to "revanced_hide_navigation_home_button", + "revanced_hide_keyword_content_search" to "revanced_hide_shorts_shelf_search", + "revanced_hide_keyword_content_subscriptions" to "revanced_hide_navigation_subscriptions_button", + "revanced_hide_like_dislike_button" to "sb_voting_button", + "revanced_hide_navigation_create_button" to "M 466.077 660 L 496.846 660 L 496.846 497.077 L 660 497.077 L 660 466.308 L 496.846 466.308 L 496.846 300 L 466.077 300 L 466.077 466.308 L 300 466.308 L 300 497.077 L 466.077 497.077 L 466.077 660 Z M 480.4 840 Q 405.224 840 340.106 811.661 Q 274.987 783.321 225.859 734.239 Q 176.732 685.157 148.366 620.026 Q 120 554.894 120 479.634 Q 120 405.143 148.339 339.565 Q 176.679 273.987 225.761 225.359 Q 274.843 176.732 339.974 148.366 Q 405.106 120 480.366 120 Q 554.857 120 620.435 148.339 Q 686.013 176.679 734.641 225.261 Q 783.268 273.843 811.634 339.518 Q 840 405.194 840 479.6 Q 840 554.776 811.661 619.894 Q 783.321 685.013 734.739 733.956 Q 686.157 782.9 620.482 811.45 Q 554.806 840 480.4 840 Z M 480.5 809.231 Q 617.385 809.231 713.308 713.192 Q 809.231 617.154 809.231 479.5 Q 809.231 342.615 713.495 246.692 Q 617.76 150.769 480 150.769 Q 342.846 150.769 246.808 246.505 Q 150.769 342.24 150.769 480 Q 150.769 617.154 246.808 713.192 Q 342.846 809.231 480.5 809.231 Z M 480 480 Z", + "revanced_hide_navigation_home_button" to "M 230.769 769.231 L 392.308 769.231 L 392.308 529.231 L 567.692 529.231 L 567.692 769.231 L 729.231 769.231 L 729.231 395.385 L 480 206.538 L 230.769 395.128 L 230.769 769.231 Z M 200 800 L 200 380 L 480 168.461 L 760 380 L 760 800 L 536.923 800 L 536.923 560 L 423.077 560 L 423.077 800 L 200 800 Z M 480 487.769 Z", + "revanced_hide_navigation_library_button" to "revanced_preference_screen_video", + "revanced_hide_navigation_notifications_button" to "notification_key", + "revanced_hide_navigation_shorts_button" to "revanced_preference_screen_shorts", + "revanced_hide_navigation_subscriptions_button" to "M 175.384 840 Q 152.327 840 136.163 823.837 Q 120 807.673 120 784.616 L 120 415.384 Q 120 392.327 136.163 376.163 Q 152.327 360 175.384 360 L 784.616 360 Q 807.673 360 823.837 376.163 Q 840 392.327 840 415.384 L 840 784.616 Q 840 807.673 823.837 823.837 Q 807.673 840 784.616 840 L 175.384 840 Z M 175.384 809.231 L 784.616 809.231 Q 793.846 809.231 801.539 801.539 Q 809.231 793.846 809.231 784.616 L 809.231 415.384 Q 809.231 406.154 801.539 398.461 Q 793.846 390.769 784.616 390.769 L 175.384 390.769 Q 166.154 390.769 158.461 398.461 Q 150.769 406.154 150.769 415.384 L 150.769 784.616 Q 150.769 793.846 158.461 801.539 Q 166.154 809.231 175.384 809.231 Z M 423.154 718.231 L 598.769 600 L 423.154 482.769 L 423.154 718.231 Z M 175.154 280 L 175.154 249.23 L 784.846 249.23 L 784.846 280 L 175.154 280 Z M 300 169.231 L 300 138.461 L 660 138.461 L 660 169.231 L 300 169.231 Z M 150.769 809.231 L 150.769 390.769 L 150.769 809.231 Z", + "revanced_hide_player_autoplay_button" to "revanced_change_player_flyout_menu_toggle", + "revanced_hide_player_captions_button" to "captions_key", + "revanced_hide_player_cast_button" to "M 480.23 480 Z M 840.23 255.38 L 840.23 704.62 Q 840.23 726.87 823.96 743.43 Q 807.69 760 784.85 760 L 594.62 760 Q 588.08 760 583.65 755.52 Q 579.23 751.03 579.23 744.4 Q 579.23 737.77 583.65 733.5 Q 588.08 729.23 594.62 729.23 L 784.85 729.23 Q 795.62 729.23 802.54 722.31 Q 809.46 715.38 809.46 704.62 L 809.46 255.38 Q 809.46 244.62 802.54 237.69 Q 795.62 230.77 784.85 230.77 L 175.62 230.77 Q 165.62 230.77 158.31 237.69 Q 151 244.62 151 255.38 L 151 285.38 Q 151 291.92 146.51 296.35 Q 142.03 300.77 135.4 300.77 Q 128.77 300.77 124.5 296.35 Q 120.23 291.92 120.23 285.38 L 120.23 255.38 Q 120.23 233.13 136.8 216.57 Q 153.37 200 175.62 200 L 784.85 200 Q 807.69 200 823.96 216.57 Q 840.23 233.13 840.23 255.38 Z M 307.62 760 Q 301.11 760 296.92 755.36 Q 292.74 750.73 292.08 743.77 Q 286.15 681.69 241.73 637.42 Q 197.31 593.15 135.46 586.46 Q 129 586.23 124.62 581.44 Q 120.23 576.65 120.23 570.52 Q 120.23 564 124.94 559.62 Q 129.66 555.23 136.23 555.46 Q 211.54 562.15 264.62 615.58 Q 317.69 669 323.08 744.31 Q 323.54 750.71 319.04 755.36 Q 314.54 760 307.62 760 Z M 458.76 760 Q 452.08 760 447.69 753.92 Q 443.31 747.85 443.08 740.46 Q 435.15 616.46 348.42 529.15 Q 261.69 441.85 137.92 434.69 Q 131.21 434.97 125.72 430.34 Q 120.23 425.72 120.23 419.55 Q 120.23 413 126.88 408.73 Q 133.54 404.46 141.38 404.69 Q 277.04 412.23 372.13 508.92 Q 467.23 605.62 474.62 741.62 Q 474.08 749.84 469.73 754.92 Q 465.39 760 458.76 760 Z M 146.12 760 Q 135.08 760 127.42 751.78 Q 119.77 743.55 119.77 732.89 Q 119.77 722.23 127.63 714.19 Q 135.49 706.15 146.88 706.15 Q 157.54 706.15 165.58 714.38 Q 173.62 722.6 173.62 733.26 Q 173.62 743.92 165.39 751.96 Q 157.17 760 146.12 760 Z", + "revanced_hide_player_collapse_button" to "M 480 577.08 Q 474 577.08 469.38 575.08 Q 464.77 573.08 460.54 568.85 L 278.69 387 Q 274.31 382.62 274.04 376.27 Q 273.77 369.92 278.92 364.77 Q 284.08 359.62 290.04 359.62 Q 296 359.62 301.15 364.77 L 480 543.85 L 659.08 364.77 Q 663.46 360.38 669.69 360.12 Q 675.92 359.85 681.08 365 Q 686.23 370.15 686.23 376.12 Q 686.23 382.08 681.08 387.23 L 499.46 568.85 Q 495.23 573.08 490.62 575.08 Q 486 577.08 480 577.08 Z", + "revanced_hide_player_flyout_menu_ambient_mode" to "revanced_preference_screen_ambient_mode", + "revanced_hide_player_flyout_menu_audio_track" to "M 791.794 515.678 C 820.501 485.676 842.887 451.857 859.237 413.47 C 875.575 375.066 883.779 334.716 883.779 292.616 C 883.779 250.514 875.406 210.173 858.734 171.793 C 842.051 133.433 819.502 99.63 790.794 69.629 L 811.614 48.81 C 843.625 81.94 868.25 118.828 885.975 160.533 C 903.713 202.224 912.548 246.131 912.548 292.45 C 912.548 338.771 903.885 382.695 886.521 424.425 C 869.154 466.159 844.686 503.165 812.612 536.496 L 791.794 515.678 Z M 670.064 394.486 L 649.461 373.883 C 659.371 362.671 667.186 350.522 673.197 336.707 C 679.202 322.865 682.241 308.281 682.241 293.154 C 682.241 278.033 679.376 263.295 673.708 249.127 C 668.024 234.964 660.038 222.639 649.49 211.413 L 670.084 191.323 C 683.516 204.625 693.5 219.739 700.502 237.748 C 707.528 255.762 711.01 274.093 711.01 292.923 C 711.01 311.748 707.371 329.943 700.053 347.709 C 692.733 365.48 682.906 380.709 670.064 394.486 Z M 388.462 448 C 412.223 448 431.931 440.039 448.024 423.947 C 464.116 407.854 472.077 388.146 472.077 364.385 C 472.077 340.624 464.145 320.887 448.024 304.822 C 431.959 288.701 412.223 280.769 388.462 280.769 C 364.701 280.769 344.964 288.702 328.899 304.822 C 312.778 320.887 304.846 340.624 304.846 364.385 C 304.846 388.146 312.778 407.883 328.899 423.947 C 344.964 440.068 364.701 448 388.462 448 Z M 636.692 737.462 L 636.692 711.385 C 636.72 699.238 633.237 689.19 626.125 680.917 C 618.991 672.551 609.942 665.45 598.835 659.497 C 570.495 644.093 536.85 632.049 497.782 623.322 C 458.704 614.587 422.3 610.231 388.462 610.231 C 354.624 610.231 318.184 614.664 279.029 623.552 C 239.881 632.433 206.197 644.4 177.855 659.499 C 167.075 664.621 158.131 671.566 150.929 680.428 C 143.732 689.218 140.204 699.432 140.231 711.385 L 140.231 737.462 L 636.692 737.462 Z M 107.461 711.385 C 107.483 691.546 112.513 675.122 122.445 662.421 C 132.375 649.798 145.939 639.155 162.947 630.64 C 195.464 614.822 232.529 601.99 274.039 592.181 C 315.552 582.377 353.732 577.462 388.462 577.462 C 423.193 577.462 461.207 582.374 502.386 592.181 C 543.562 601.987 580.728 614.828 613.755 630.645 C 630.763 639.159 644.354 649.796 654.361 662.418 C 664.37 675.12 669.44 691.545 669.462 711.385 L 669.462 770.231 L 107.461 770.231 L 107.461 711.385 Z M 388.462 480.769 C 355.276 480.769 327.442 469.606 305.312 447.534 C 283.24 425.404 272.077 397.441 272.077 364 C 272.077 330.558 283.242 302.722 305.316 280.847 C 327.446 259.032 355.278 248 388.462 248 C 421.646 248 449.478 259.032 471.607 280.847 C 493.681 302.722 504.846 330.558 504.846 364 C 504.846 397.441 493.712 425.433 471.611 447.534 C 449.51 469.635 421.648 480.769 388.462 480.769 Z", + "revanced_hide_player_flyout_menu_captions" to "captions_key", + "revanced_hide_player_flyout_menu_enhanced_bitrate" to "video_quality_settings_key", + "revanced_hide_player_flyout_menu_help" to "M 484.043 686.077 Q 494.615 686.077 502.154 678.111 Q 509.692 670.144 509.692 659.572 Q 509.692 649.769 502.111 641.846 Q 494.529 633.923 483.957 633.923 Q 473.385 633.923 465.461 641.89 Q 457.538 649.856 457.538 659.659 Q 457.538 670.231 465.505 678.154 Q 473.471 686.077 484.043 686.077 Z M 463.615 557 L 495.692 557 Q 496.462 534.077 504.5 516.423 Q 512.539 498.769 538.077 476.154 Q 566.769 449.385 579 427.461 Q 591.231 405.538 591.231 379.134 Q 591.231 333.308 561.04 304.384 Q 530.848 275.461 485.231 275.461 Q 445.461 275.461 414.5 296.5 Q 383.538 317.538 368.077 348.231 L 398.769 360.538 Q 410.538 334.846 430.615 320.5 Q 450.692 306.154 482.231 306.154 Q 521.615 306.154 541.846 327.731 Q 562.077 349.308 562.077 379.077 Q 562.077 400.308 550.615 417.885 Q 539.154 435.461 518 454.154 Q 489.538 480.154 476.577 505.269 Q 463.615 530.385 463.615 557 Z M 480.134 840 Q 405.692 840 340.34 811.661 Q 274.987 783.321 225.859 734.239 Q 176.732 685.157 148.366 619.866 Q 120 554.575 120 480.134 Q 120 405.461 148.339 339.724 Q 176.679 273.987 225.761 225.359 Q 274.843 176.732 340.134 148.366 Q 405.425 120 479.866 120 Q 554.539 120 620.276 148.339 Q 686.013 176.679 734.641 225.261 Q 783.268 273.843 811.634 339.518 Q 840 405.194 840 479.866 Q 840 554.308 811.661 619.66 Q 783.321 685.013 734.739 734.141 Q 686.157 783.268 620.482 811.634 Q 554.806 840 480.134 840 Z M 480 809.231 Q 617.385 809.231 713.308 713.192 Q 809.231 617.154 809.231 480 Q 809.231 342.615 713.308 246.692 Q 617.385 150.769 480 150.769 Q 342.846 150.769 246.808 246.692 Q 150.769 342.615 150.769 480 Q 150.769 617.154 246.808 713.192 Q 342.846 809.231 480 809.231 Z M 480 480 Z", + "revanced_hide_player_flyout_menu_listen_with_youtube_music" to "revanced_hide_player_youtube_music_button", + "revanced_hide_player_flyout_menu_lock_screen" to "M 255.384 840 Q 232.942 840 216.471 823.529 Q 200 807.058 200 784.616 L 200 418.307 Q 200 395.269 216.471 379.096 Q 232.942 362.923 255.384 362.923 L 324.615 362.923 L 324.615 275.384 Q 324.615 210.453 369.888 165.227 Q 415.161 120 480.158 120 Q 545.154 120 590.269 165.227 Q 635.385 210.453 635.385 275.384 L 635.385 362.923 L 704.616 362.923 Q 727.058 362.923 743.529 379.096 Q 760 395.269 760 418.307 L 760 784.616 Q 760 807.058 743.529 823.529 Q 727.058 840 704.616 840 L 255.384 840 Z M 255.384 809.231 L 704.616 809.231 Q 715.385 809.231 722.308 802.308 Q 729.231 795.385 729.231 784.616 L 729.231 418.307 Q 729.231 407.538 722.308 400.615 Q 715.385 393.692 704.616 393.692 L 255.384 393.692 Q 244.615 393.692 237.692 400.615 Q 230.769 407.538 230.769 418.307 L 230.769 784.616 Q 230.769 795.385 237.692 802.308 Q 244.615 809.231 255.384 809.231 Z M 480.168 660 Q 504.308 660 521.423 642.969 Q 538.539 625.938 538.539 601.923 Q 538.539 578.846 521.255 560.885 Q 503.971 542.923 479.832 542.923 Q 455.692 542.923 438.577 560.885 Q 421.461 578.846 421.461 602.423 Q 421.461 626 438.745 643 Q 456.029 660 480.168 660 Z M 355.385 362.923 L 604.615 362.923 L 604.615 275.384 Q 604.615 223.461 568.317 187.115 Q 532.018 150.769 480.163 150.769 Q 428.308 150.769 391.846 187.115 Q 355.385 223.461 355.385 275.384 L 355.385 362.923 Z M 230.769 809.231 L 230.769 393.692 L 230.769 809.231 Z", + "revanced_hide_player_flyout_menu_loop_video" to "revanced_overlay_button_always_repeat", + "revanced_hide_player_flyout_menu_more_info" to "about_key", + "revanced_hide_player_flyout_menu_pip" to "offline_key", + "revanced_hide_player_flyout_menu_playback_speed" to "M 196 701.385 Q 161.077 657.154 144.192 612.808 Q 127.307 568.462 121.769 515.385 L 153 515.385 Q 158.769 560.846 175.385 602.154 Q 192 643.462 219.692 677.923 L 196 701.385 Z M 121.769 435.385 Q 126.077 382.308 143.577 338.461 Q 161.077 294.615 196 250.154 L 219.692 272.846 Q 192.231 308.538 175.5 349.231 Q 158.769 389.923 153 435.385 L 121.769 435.385 Z M 439.462 834.846 Q 382 825.308 340.077 808.769 Q 298.154 792.231 253.461 759.539 L 277.154 735.385 Q 311.308 760.615 352.385 778.731 Q 393.462 796.846 439.462 804.077 L 439.462 834.846 Z M 279.154 214.615 L 255 190.461 Q 299.923 157.538 342.346 141.5 Q 384.769 125.461 441.692 115.923 L 441.692 146.692 Q 395.462 153.923 354.385 171.038 Q 313.308 188.154 279.154 214.615 Z M 403.154 614.077 L 403.154 336.692 L 621.154 475.385 L 403.154 614.077 Z M 521.692 834.846 L 521.692 804.077 Q 645.846 786.615 727.539 693.577 Q 809.231 600.538 809.231 475.385 Q 809.231 350.231 727.539 257.192 Q 645.846 164.154 521.692 146.692 L 521.692 115.923 Q 659.539 130.154 749.769 233.346 Q 840 336.538 840 475.385 Q 840 614.231 749.769 717.308 Q 659.539 820.385 521.692 834.846 Z", + "revanced_hide_player_flyout_menu_premium_controls" to "premium_early_access_browse_page_key", + "revanced_hide_player_flyout_menu_quality_header" to "revanced_default_video_quality_wifi", + "revanced_hide_player_flyout_menu_report" to "revanced_hide_report_button", + "revanced_hide_player_flyout_menu_stable_volume" to "M 312.692 684.616 L 312.692 275.384 L 343.462 275.384 L 343.462 684.616 L 312.692 684.616 Z M 464.615 840 L 464.615 120 L 495.385 120 L 495.385 840 L 464.615 840 Z M 160 532.308 L 160 427.692 L 190.769 427.692 L 190.769 532.308 L 160 532.308 Z M 617.308 684.616 L 617.308 275.384 L 648.077 275.384 L 648.077 684.616 L 617.308 684.616 Z M 769.231 532.308 L 769.231 427.692 L 800 427.692 L 800 532.308 L 769.231 532.308 Z", + "revanced_hide_player_flyout_menu_stats_for_nerds" to "M 503.538 860 L 357.769 190.154 L 250 697.308 L 100 697.308 L 100 667.538 L 224.846 667.538 L 336.846 138.769 L 378.231 138.769 L 523.231 808.769 L 619.461 381.923 L 664.385 381.923 L 735.385 667.538 L 860 667.538 L 860 697.308 L 711.692 697.308 L 642.154 424 L 542.462 860 L 503.538 860 Z", + "revanced_hide_player_flyout_menu_watch_in_vr" to "M 309.846 680 Q 256.817 680 219.139 643.438 Q 181.461 606.875 181.461 553.231 L 181.461 403 Q 181.461 363.089 206.846 333.737 Q 232.231 304.384 271 296.923 Q 323.352 287.192 375.009 283.211 Q 426.665 279.231 480.028 279.231 Q 533.39 279.231 585.504 282.961 Q 637.618 286.692 689.231 296.923 Q 728 304.384 753.385 333.829 Q 778.769 363.273 778.769 403 L 778.769 553.231 Q 778.769 606.692 741.25 643.346 Q 703.731 680 650.385 680 L 611.462 680 Q 601.18 680 590.897 678 Q 580.615 676 571.154 672.769 L 511.538 653.846 Q 496.178 648.846 480.5 648.846 Q 464.822 648.846 449.462 653.846 L 389.077 672.769 Q 378.615 676 368.41 678 Q 358.205 680 347.713 680 L 309.846 680 Z M 309.846 649.231 L 347.884 649.231 Q 356.277 649.231 363.792 647.731 Q 371.308 646.231 379.308 644.231 Q 404.727 635.69 429.536 626.883 Q 454.344 618.077 480.106 618.077 Q 507.406 618.077 531.831 626.932 Q 556.256 635.788 580.923 644.231 Q 588.692 646.231 596.176 647.731 Q 603.659 649.231 611.718 649.231 L 650.385 649.231 Q 690.827 649.231 719.414 620.938 Q 748 592.644 748 553.231 L 748 403 Q 748 374.234 729.962 353.348 Q 711.923 332.461 683.615 326.692 Q 633.702 316.632 582.428 313.316 Q 531.154 310 480.171 310 Q 428.868 310 377.928 313.487 Q 326.987 316.974 276.615 326.692 Q 248.308 332.37 230.269 353.474 Q 212.231 374.579 212.231 403 L 212.231 553.231 Q 212.231 592.644 240.586 620.938 Q 268.942 649.231 309.846 649.231 Z M 80 553.231 L 80 402.231 L 105.384 402.231 L 105.384 553.231 L 80 553.231 Z M 854.616 553.231 L 854.616 402.231 L 880 402.231 L 880 553.231 L 854.616 553.231 Z M 480.231 479.231 Z", + "revanced_hide_player_fullscreen_button" to "revanced_preference_screen_fullscreen", + "revanced_hide_player_previous_next_button" to "M 719.13 746.68 L 719.125 213.061 C 718.813 207.935 720.001 205.193 723.709 202.005 L 723.797 201.928 L 723.756 201.975 L 723.796 201.929 L 723.848 201.871 L 723.859 201.857 C 726.967 198.246 729.758 197.473 735.282 197.473 C 740.752 197.473 743.528 198.317 746.386 201.864 L 746.468 201.961 L 746.519 202.007 L 746.548 202.034 C 750.052 205.174 751.027 208.058 750.756 213.321 L 750.762 746.94 C 751.076 752.062 749.896 754.793 746.188 757.985 L 746.091 758.068 L 746.088 758.071 L 746.044 758.121 L 746.026 758.143 C 742.92 761.753 740.127 762.527 734.605 762.527 C 729.132 762.527 726.329 761.624 723.48 758.116 L 723.372 757.999 L 723.315 757.945 C 719.831 754.812 718.859 751.942 719.13 746.68 Z M 240.838 694.078 L 558.051 480.026 L 240.838 264.653 L 240.838 694.078 Z M 209.212 688.382 L 209.212 271.495 C 208.895 260.066 212.056 252.545 219.669 246.088 C 227.009 238.96 234.282 236.4 244.418 236.4 C 248.314 236.4 251.616 236.827 255.216 237.866 C 258.55 238.702 261.666 240.428 265.281 243.201 L 569.478 450.038 C 575.269 453.84 578.794 457.694 581.278 462.805 C 584.019 467.776 585.004 472.778 585.004 479.812 C 585.004 486.834 583.9 492.134 581.138 497.278 C 578.656 502.508 575.191 506.219 569.417 510.004 L 264.877 717.091 C 261.295 719.845 258.431 721.323 255.076 722.173 C 251.476 723.217 248.318 723.601 244.418 723.601 C 234.288 723.601 226.786 720.823 219.447 713.71 C 211.819 707.259 208.902 699.82 209.212 688.382 Z", + "revanced_hide_player_youtube_music_button" to "M 480.13 840 C 430.503 840 383.907 830.553 340.34 811.66 C 296.773 792.767 258.613 766.96 225.86 734.24 C 193.107 701.52 167.277 663.397 148.37 619.87 C 129.457 576.343 120 529.763 120 480.13 C 120 430.35 129.447 383.547 148.34 339.72 C 167.233 295.9 193.04 257.78 225.76 225.36 C 258.48 192.94 296.603 167.277 340.13 148.37 C 383.657 129.457 430.237 120 479.87 120 C 529.65 120 576.453 129.447 620.28 148.34 C 664.1 167.233 702.22 192.873 734.64 225.26 C 767.06 257.647 792.723 295.733 811.63 339.52 C 830.543 383.3 840 430.083 840 479.87 C 840 529.497 830.553 576.093 811.66 619.66 C 792.767 663.227 767.127 701.387 734.74 734.14 C 702.353 766.893 664.267 792.723 620.48 811.63 C 576.7 830.543 529.917 840 480.13 840 Z M 480 809.23 C 571.587 809.23 649.357 777.217 713.31 713.19 C 777.257 649.163 809.23 571.433 809.23 480 C 809.23 388.413 777.257 310.643 713.31 246.69 C 649.357 182.743 571.587 150.77 480 150.77 C 388.567 150.77 310.837 182.743 246.81 246.69 C 182.783 310.643 150.77 388.413 150.77 480 C 150.77 571.433 182.783 649.163 246.81 713.19 C 310.837 777.217 388.567 809.23 480 809.23 Z M 480 480 Z M 424.094 607.092 C 426.644 606.802 429.081 605.814 431.994 603.759 L 603.18 493.856 C 608.821 490.61 610.708 486.835 610.708 480.093 C 610.708 473.363 609.083 469.661 603.358 466.262 L 432.308 356.465 C 429.074 354.183 426.322 353.027 423.415 352.875 C 420.557 352.694 418.272 353.255 415.125 355.23 C 411.887 356.868 409.377 358.833 407.887 361.176 C 406.528 363.56 405.941 366.351 406.133 369.962 L 406.143 589.671 C 405.951 593.279 406.426 596.242 407.743 598.645 C 409.158 600.953 411.291 602.824 414.605 604.477 C 417.669 606.381 420.529 607.317 423.43 607.134 L 424.094 607.092 Z M 480 706.26 C 543.296 706.26 596.269 684.61 640.342 640.268 C 684.628 596.14 706.26 543.188 706.26 480 C 706.26 416.704 684.635 363.73 640.35 319.658 C 596.276 275.37 543.296 253.74 480 253.74 C 416.812 253.74 363.863 275.364 319.739 319.654 C 275.397 363.725 253.74 416.704 253.74 480 C 253.74 543.186 275.395 596.135 319.737 640.261 C 363.862 684.606 416.814 706.26 480 706.26 Z M 480.091 735.78 C 444.985 735.78 411.595 728.964 380.727 715.627 C 349.935 702.238 322.628 683.764 299.427 660.641 C 276.277 637.463 257.794 610.178 244.39 579.415 C 231.038 548.574 224.22 515.203 224.22 480.091 C 224.22 444.88 231.033 411.348 244.369 380.302 C 257.763 349.326 276.244 322.037 299.376 299.062 C 322.548 276.153 349.827 257.79 380.586 244.39 C 411.425 231.036 444.797 224.22 479.909 224.22 C 515.12 224.22 548.651 231.033 579.698 244.369 C 610.669 257.758 637.951 276.119 660.923 299.01 C 683.839 321.958 702.212 349.222 715.612 380.167 C 728.963 411.185 735.78 444.692 735.78 479.909 C 735.78 515.015 728.976 548.408 715.628 579.27 C 702.249 610.064 683.9 637.365 661.005 660.558 C 638.063 683.722 610.783 702.209 579.833 715.613 C 548.816 728.962 515.309 735.78 480.091 735.78 Z", + "revanced_hide_playlist_button" to "M 240 780 L 240 212.692 Q 240 190.231 256.163 173.769 Q 272.327 157.307 295.384 157.307 L 664.616 157.307 Q 687.673 157.307 703.837 173.769 Q 720 190.231 720 212.692 L 720 780 L 480 676.923 L 240 780 Z M 270.769 732.077 L 480 642.923 L 689.231 732.077 L 689.231 212.692 Q 689.231 203.461 681.539 195.769 Q 673.846 188.077 664.616 188.077 L 295.384 188.077 Q 286.154 188.077 278.461 195.769 Q 270.769 203.461 270.769 212.692 L 270.769 732.077 Z M 270.769 188.077 L 689.231 188.077 L 270.769 188.077 Z", + "revanced_hide_quick_actions_comment_button" to "M 840 803.077 L 716.923 680 L 175.384 680 Q 152.327 680 136.163 663.837 Q 120 647.673 120 624.616 L 120 175.384 Q 120 152.327 136.163 136.163 Q 152.327 120 175.384 120 L 784.616 120 Q 807.673 120 823.837 136.163 Q 840 152.327 840 175.384 L 840 803.077 Z M 175.384 649.231 L 730.615 649.231 L 809.231 730.769 L 809.231 175.384 Q 809.231 166.154 801.539 158.461 Q 793.846 150.769 784.616 150.769 L 175.384 150.769 Q 166.154 150.769 158.461 158.461 Q 150.769 166.154 150.769 175.384 L 150.769 624.616 Q 150.769 633.846 158.461 641.539 Q 166.154 649.231 175.384 649.231 Z M 150.769 649.231 L 150.769 150.769 L 150.769 649.231 Z", + "revanced_hide_quick_actions_dislike_button" to "revanced_preference_screen_ryd", + "revanced_hide_quick_actions_like_button" to "M 696.769 800 L 293.538 800 L 293.538 363.384 L 543.077 112.307 L 555.842 121.121 Q 561.154 126 564.154 133.077 Q 567.154 140.154 567.154 147.769 L 567.154 152.384 L 525.231 363.384 L 822.693 363.384 Q 844.077 363.384 861.077 380.384 Q 878.077 397.384 878.077 418.769 L 878.077 469.132 Q 878.077 474.692 878.039 481.038 Q 878 487.385 875.769 492.846 L 764.385 755.154 Q 756.004 774.225 736.193 787.112 Q 716.382 800 696.769 800 Z M 324.308 769.231 L 702.846 769.231 Q 710.539 769.231 719.769 764.615 Q 729 760 733.615 749.231 L 847.308 480.231 L 847.308 418.769 Q 847.308 408.769 840 401.461 Q 832.692 394.154 822.693 394.154 L 488.923 394.154 L 534.231 162.846 L 324.308 376.846 L 324.308 769.231 Z M 324.308 376.846 L 324.308 769.231 L 324.308 376.846 Z M 293.538 363.384 L 293.538 394.154 L 150.538 394.154 L 150.538 769.231 L 293.538 769.231 L 293.538 800 L 119.769 800 L 119.769 363.384 L 293.538 363.384 Z", + "revanced_hide_quick_actions_live_chat_button" to "live_chat_key", + "revanced_hide_quick_actions_more_button" to "M 207.858 528 Q 188 528 174 513.858 Q 160 499.717 160 479.858 Q 160 460 174.142 446 Q 188.283 432 208.142 432 Q 228 432 242 446.142 Q 256 460.283 256 480.142 Q 256 500 241.858 514 Q 227.717 528 207.858 528 Z M 479.858 528 Q 460 528 446 513.858 Q 432 499.717 432 479.858 Q 432 460 446.142 446 Q 460.283 432 480.142 432 Q 500 432 514 446.142 Q 528 460.283 528 480.142 Q 528 500 513.858 514 Q 499.717 528 479.858 528 Z M 751.858 528 Q 732 528 718 513.858 Q 704 499.717 704 479.858 Q 704 460 718.142 446 Q 732.283 432 752.142 432 Q 772 432 786 446.142 Q 800 460.283 800 480.142 Q 800 500 785.858 514 Q 771.717 528 751.858 528 Z", + "revanced_hide_quick_actions_save_to_playlist_button" to "revanced_hide_playlist_button", + "revanced_hide_quick_actions_share_button" to "revanced_hide_shorts_share_button", + "revanced_hide_remix_button" to "revanced_hide_shorts_remix_button", + "revanced_hide_report_button" to "M 240 820 L 240 200 L 519.923 200 L 537.385 282.923 L 760 282.923 L 760 589.077 L 563.231 589.077 L 545.887 506.385 L 270.769 506.385 L 270.769 820 L 240 820 Z M 500 394.154 Z M 590.385 558.308 L 729.231 558.308 L 729.231 313.692 L 510.231 313.692 L 492.769 230.769 L 270.769 230.769 L 270.769 475.615 L 572.923 475.615 L 590.385 558.308 Z", + "revanced_hide_rewards_button" to "M 395.77 531.08 L 427.92 427.31 L 344.46 367.46 L 447.17 367.46 L 480 260 L 511.83 367.46 L 615.54 367.46 L 532.85 427.31 L 564 531.08 L 480 467 L 395.77 531.08 Z M 281.69 858.46 L 281.69 596.77 Q 240.54 557.46 220.27 506.08 Q 200 454.69 200 400 Q 200 282.46 281.23 201.23 Q 362.46 120 480 120 Q 597.54 120 678.77 201.23 Q 760 282.46 760 400 Q 760 454.69 739.73 506.08 Q 719.46 557.46 678.31 596.77 L 678.31 858.46 L 480 798.67 L 281.69 858.46 Z M 479.91 649.23 Q 584.38 649.23 656.81 576.9 Q 729.23 504.57 729.23 400.09 Q 729.23 295.62 656.9 223.19 Q 584.57 150.77 480.09 150.77 Q 375.62 150.77 303.19 223.1 Q 230.77 295.43 230.77 399.91 Q 230.77 504.38 303.1 576.81 Q 375.43 649.23 479.91 649.23 Z M 312.46 817.54 L 480 766.38 L 647.54 817.54 L 647.54 622.69 Q 611.38 651.69 568.08 665.85 Q 524.77 680 480 680 Q 435.23 680 391.92 665.85 Q 348.62 651.69 312.46 622.69 L 312.46 817.54 Z M 480 720 Z", + "revanced_hide_share_button" to "revanced_hide_shorts_share_button", + "revanced_hide_shop_button" to "M 314.521 348.199 C 315.177 392.878 332.824 432.132 362.992 462.206 C 393.065 492.373 434.106 510.699 480 510.699 C 525.894 510.699 566.934 492.374 597.007 462.207 C 627.175 432.133 644.823 392.878 645.479 348.199 L 617.761 348.199 C 616.864 384.862 602.173 417.833 577.451 442.648 C 552.636 467.37 517.851 482.999 480 482.999 C 442.149 482.999 407.341 467.394 382.55 442.649 C 357.804 417.857 343.136 384.862 342.239 348.199 L 314.521 348.199 Z M 642.672 280.799 L 642.189 278.391 C 634.633 240.467 614.238 207.648 585.423 183.898 C 556.671 160.065 520.235 145.999 480 145.999 C 439.764 145.999 403.329 160.065 374.577 183.897 C 345.762 207.648 325.366 240.467 317.81 278.39 L 317.648 279.193 L 317.326 280.799 L 213.4 280.799 L 213.4 749.599 C 213.266 767.512 220.432 783.281 232.318 795.078 C 244.114 806.964 259.879 814.133 277.793 813.999 L 682.2 813.999 C 700.112 814.133 715.838 806.921 727.68 795.079 C 739.522 783.237 746.734 767.518 746.6 749.606 L 746.6 280.799 L 642.672 280.799 Z M 480 173.699 C 511.862 173.699 541.701 184.795 565.007 203.027 C 588.259 221.341 605.705 247.444 613.326 277.059 L 614.28 280.799 L 345.72 280.799 L 346.038 279.552 L 346.674 277.058 C 354.295 247.443 371.743 221.34 394.993 203.026 C 418.3 184.794 448.137 173.699 480 173.699 Z M 718.9 749.613 C 718.77 759.485 714.558 768.99 708.119 775.524 C 701.586 781.962 692.072 786.169 682.2 786.299 L 277.786 786.299 C 267.914 786.169 258.409 781.957 251.875 775.518 C 245.437 768.985 241.23 759.471 241.1 749.599 L 241.1 308.499 L 718.9 308.499 L 718.9 749.613 Z", + "revanced_hide_shorts_comments_button" to "revanced_hide_quick_actions_comment_button", + "revanced_hide_shorts_dislike_button" to "revanced_preference_screen_ryd", + "revanced_hide_shorts_like_button" to "revanced_hide_quick_actions_like_button", + "revanced_hide_shorts_navigation_bar" to "revanced_preference_screen_navigation_bar", + "revanced_hide_shorts_remix_button" to "M 380.054 557.441 L 380.054 371.244 L 546.045 464.343 Z M 564.57 719.47 L 674.24 719.47 L 674.24 609.8 L 704.361 609.8 L 704.361 719.47 L 814.031 719.47 L 814.031 749.591 L 704.361 749.591 L 704.361 859.261 L 674.24 859.261 L 674.24 749.591 L 564.57 749.591 Z M 254.476 548.617 C 214.463 541.558 184.342 521.573 164.449 488.894 C 144.638 456.166 140.789 420.184 152.949 381.36 C 165.176 342.575 190.038 311.715 227.315 289.053 L 494.766 126.709 C 532.073 104.098 570.902 96.25 610.91 103.23 C 650.922 110.289 681.043 130.274 700.937 162.954 C 720.748 195.683 724.598 231.664 712.438 270.486 C 700.208 309.272 675.349 340.132 638.071 362.795 L 611.529 378.905 C 620.859 378.905 629.048 379.519 639.759 381.394 C 679.772 388.454 709.894 408.441 729.787 441.119 C 749.599 473.848 753.458 509.804 741.297 548.628 C 740.399 551.192 738.146 553.135 735.508 554.105 C 732.889 555.07 729.635 555.337 726.716 554.942 C 723.791 554.547 720.894 553.426 719.047 551.717 C 718.097 550.855 717.311 549.7 716.972 548.469 C 716.637 547.253 716.687 545.732 717.135 544.229 C 727.42 511.824 724.26 482.161 707.614 454.824 C 691.047 427.438 666.17 410.921 632.649 405.042 C 617.329 402.332 602.042 401.447 597.748 401.286 C 588.341 400.876 581.445 396.779 578.706 391.277 C 577.322 388.498 576.914 385.175 577.67 381.95 C 578.419 378.757 580.362 375.39 583.44 372.342 C 585.934 369.904 625.142 343.69 626.109 343.043 C 657.431 324.065 678.071 298.507 688.285 266.062 C 698.57 233.658 695.41 203.995 678.764 176.659 C 662.196 149.273 637.321 132.757 603.801 126.876 C 570.275 120.918 538.038 127.419 506.748 146.447 L 239.296 308.79 C 207.974 327.768 187.315 353.34 177.102 385.785 C 166.816 418.19 169.977 447.853 186.622 475.189 C 203.191 502.575 228.065 519.091 261.587 524.971 C 274.985 527.341 292.192 528.737 292.956 528.82 C 301.857 529.82 308.076 531.673 312.016 534.059 C 316.058 536.507 317.963 539.807 318.027 543.091 C 318.118 549.524 311.511 556.846 303.613 562.158 C 300.08 564.526 269.166 586.237 268.118 586.974 C 236.812 605.95 216.165 631.507 205.952 663.951 C 195.667 696.356 198.827 726.021 215.474 753.356 C 232.041 780.741 256.915 797.258 290.436 803.138 C 323.96 809.097 356.201 802.588 387.488 783.567 L 507.965 707.501 L 507.781 737.418 L 399.469 803.306 C 362.163 825.916 323.329 833.799 283.327 826.784 C 243.309 819.766 213.153 799.764 193.301 767.061 C 173.45 734.356 169.639 698.351 181.798 659.526 C 194.027 620.74 218.888 589.881 256.165 567.219 L 282.707 551.108 C 273.378 551.107 265.189 550.493 254.476 548.617 Z", + "revanced_hide_shorts_share_button" to "M 582.272 263.479 L 773.48 478.672 L 774.07 479.336 L 774.66 480 L 774.07 480.664 L 773.48 481.328 L 582.272 696.521 L 580.524 698.488 L 578.776 700.455 L 578.776 549.565 L 546.994 549.565 C 413.517 549.519 306.714 583.081 218.867 653.513 L 215.61 656.123 L 212.353 658.734 L 214.074 654.931 L 215.795 651.128 C 278.284 513.232 389.516 433.944 551.428 410.119 L 578.776 405.986 L 578.776 264.807 M 548.994 181.22 L 548.994 380.383 L 547.283 380.633 C 416.228 399.594 324.882 452.742 261.305 525.152 C 197.703 597.506 164.544 683.009 145.423 775.231 C 238.974 645.857 358.678 579.473 546.994 579.347 L 548.994 579.347 L 548.994 778.78 L 814.576 480 L 548.994 181.22 Z", + "revanced_hide_shorts_shelf_history" to "history_key", + "revanced_hide_shorts_shelf_home_related_videos" to "revanced_hide_navigation_home_button", + "revanced_hide_shorts_shelf_search" to "M 793.545 821.183 L 601.814 629.408 C 577.478 651.383 549.139 668.191 516.736 679.649 C 484.347 691.241 451.69 697.008 418.943 697.008 C 340.661 697.008 274.391 670.013 220.131 615.859 C 165.917 561.778 138.787 495.866 138.787 418.063 C 138.787 340.289 165.887 274.317 219.922 220.057 C 273.973 165.948 339.929 138.818 417.658 138.818 C 495.402 138.818 561.583 165.872 616.097 219.953 C 670.685 274.108 697.949 339.99 697.949 417.764 C 697.949 451.481 691.883 484.587 679.753 517.035 C 667.696 549.438 650.815 577.883 629.108 602.114 L 821.213 792.664 L 793.545 821.183 Z M 418.644 659.227 C 486.393 659.227 543.522 635.952 590.147 589.505 C 636.757 543.014 660.108 485.827 660.108 417.928 C 660.108 349.999 636.757 292.782 590.147 246.351 C 543.522 199.815 486.393 176.659 418.644 176.659 C 350.805 176.659 293.453 199.815 246.723 246.351 C 199.979 292.782 176.628 349.999 176.628 417.928 C 176.628 485.827 199.979 543.014 246.723 589.505 C 293.453 635.952 350.805 659.227 418.644 659.227 Z", + "revanced_hide_shorts_shelf_subscriptions" to "revanced_hide_navigation_subscriptions_button", + "revanced_hide_shorts_toolbar" to "revanced_preference_screen_toolbar", + "revanced_hide_thanks_button" to "M 464.405 596.484 L 403.466 596.484 L 403.466 563.961 L 409.952 563.961 L 520.988 563.97 L 521.125 564.006 L 521.263 564.043 L 521.346 564.067 C 521.351 564.068 521.357 564.07 521.362 564.071 C 521.586 563.89 521.906 563.508 522.579 562.747 L 522.725 562.582 L 522.783 562.533 L 522.845 562.479 L 522.907 562.427 C 523.658 561.813 523.974 561.57 524.097 561.427 C 524.075 561.394 524.037 561.298 523.972 561.083 L 523.956 561.029 L 523.898 560.723 L 523.907 498.982 L 523.944 498.845 L 523.98 498.708 L 524.004 498.623 C 524.005 498.618 524.008 498.614 524.009 498.608 C 523.826 498.384 523.444 498.063 522.681 497.388 L 522.505 497.233 L 522.398 497.104 L 522.356 497.055 C 521.749 496.31 521.506 495.995 521.365 495.87 C 521.331 495.893 521.236 495.93 521.026 495.994 L 520.971 496.01 L 520.665 496.068 L 439.403 496.068 C 429.851 495.783 420.102 492.002 413.607 485.841 C 407.443 479.336 403.747 469.558 403.466 460.016 L 403.466 399.453 C 403.769 389.85 407.767 380.089 413.769 373.579 C 420.298 367.569 429.938 363.808 439.518 363.515 L 464.405 363.515 L 464.405 343.113 L 496.927 343.113 L 496.927 363.515 L 556.534 363.515 L 556.534 396.038 L 550.048 396.038 L 438.928 396.029 L 438.677 395.967 L 438.558 395.936 C 438.547 395.933 438.536 395.93 438.526 395.927 C 438.304 396.105 437.988 396.483 437.313 397.247 L 437.156 397.427 L 437.023 397.536 L 436.986 397.568 C 436.237 398.184 435.917 398.432 435.789 398.576 C 435.813 398.611 435.85 398.704 435.913 398.908 L 435.929 398.962 L 435.988 399.271 L 435.98 460.64 L 435.942 460.779 L 435.904 460.92 L 435.883 460.99 C 435.881 460.997 435.879 461.004 435.878 461.01 C 436.059 461.232 436.437 461.55 437.196 462.221 L 437.376 462.378 L 437.486 462.511 L 437.517 462.548 C 438.132 463.296 438.381 463.616 438.527 463.743 C 438.561 463.721 438.653 463.683 438.853 463.621 L 438.911 463.604 L 439.221 463.545 L 439.321 463.545 L 520.503 463.546 C 529.899 463.873 539.6 467.981 546.224 474.043 C 552.34 480.66 556.213 490.214 556.534 499.602 L 556.534 560.554 C 556.214 570.189 552.091 579.989 545.888 586.508 C 539.245 592.474 529.699 596.19 520.369 596.484 L 496.927 596.484 L 496.927 616.886 L 464.405 616.886 L 464.405 596.484 Z M 480 758.66 C 543.073 698.94 594.978 647.783 635.712 605.19 C 676.454 562.597 708.822 525.373 732.818 493.52 C 756.815 461.66 773.544 433.413 783.005 408.78 C 792.466 384.153 797.196 359.827 797.196 335.8 C 797.196 294.267 783.956 259.72 757.473 232.16 C 730.992 204.593 697.862 190.81 658.084 190.81 C 625.488 190.81 595.723 200.67 568.791 220.39 C 541.86 240.103 516.511 270.757 492.746 312.35 L 467.031 312.35 C 442.92 270.91 417.404 240.293 390.486 220.5 C 363.567 200.707 334.044 190.81 301.916 190.81 C 262.633 190.81 229.625 204.593 202.892 232.16 C 176.166 259.72 162.804 294.41 162.804 336.23 C 162.804 360.11 167.572 384.38 177.111 409.04 C 186.655 433.7 203.258 461.953 226.921 493.8 C 250.577 525.653 282.984 562.81 324.143 605.27 C 365.301 647.73 417.254 698.86 480 758.66 Z M 480 799.96 L 458.284 779.12 C 393.809 717.96 340.611 665.44 298.689 621.56 C 256.765 577.673 223.5 539.027 198.895 505.62 C 174.288 472.207 157.18 442.123 147.571 415.37 C 137.962 388.617 133.158 362.047 133.158 335.66 C 133.158 286.033 149.377 244.357 181.813 210.63 C 214.248 176.903 254.353 160.04 302.128 160.04 C 337.261 160.04 369.91 169.81 400.072 189.35 C 430.235 208.89 456.878 237.453 480 275.04 C 504.998 236.473 532.187 207.667 561.565 188.62 C 590.938 169.567 623.04 160.04 657.872 160.04 C 705.647 160.04 745.752 176.903 778.188 210.63 C 810.624 244.357 826.842 286.033 826.842 335.66 C 826.842 362.047 822.038 388.617 812.429 415.37 C 802.82 442.123 785.741 472.15 761.193 505.45 C 736.644 538.75 703.379 577.397 661.399 621.39 C 619.417 665.383 566.19 717.96 501.716 779.12 L 480 799.96 Z", + "revanced_hide_toolbar_cast_button" to "revanced_hide_player_cast_button", + "revanced_hide_toolbar_create_button" to "revanced_hide_navigation_create_button", + "revanced_hide_toolbar_notification_button" to "notification_key", + "revanced_overlay_button_always_repeat" to "M 475.231 586.462 L 475.231 402.846 L 429.231 402.846 L 429.231 372.308 L 505.769 372.308 L 505.769 586.462 L 475.231 586.462 Z M 292.308 840 L 160 707.692 L 292.308 575.385 L 314.308 597.846 L 219.846 692.308 L 701.538 692.308 L 701.538 532.308 L 732.308 532.308 L 732.308 723.077 L 219.846 723.077 L 314.308 817.539 L 292.308 840 Z M 227.692 427.692 L 227.692 236.923 L 740.154 236.923 L 645.692 142.461 L 667.692 120 L 800 252.308 L 667.692 384.615 L 645.692 362.154 L 740.154 267.692 L 258.462 267.692 L 258.462 427.692 L 227.692 427.692 Z", + "revanced_overlay_button_copy_video_url" to "M 356.923 718.462 C 341.551 718.462 328.477 713.074 317.702 702.298 C 306.926 691.523 301.538 678.449 301.538 663.077 L 301.538 195.384 C 301.538 180.013 306.926 166.939 317.702 156.163 C 328.477 145.388 341.551 140 356.923 140 L 704.616 140 C 719.987 140 733.061 145.388 743.837 156.163 C 754.612 166.939 760 180.013 760 195.384 L 760 663.077 C 760 678.449 754.612 691.523 743.837 702.298 C 733.061 713.074 719.987 718.462 704.616 718.462 L 356.923 718.462 Z M 356.923 687.693 L 704.616 687.693 C 710.769 687.693 716.41 685.129 721.539 680 C 726.667 674.872 729.231 669.231 729.231 663.077 L 729.231 195.384 C 729.231 189.231 726.667 183.59 721.539 178.461 C 716.41 173.333 710.769 170.769 704.616 170.769 L 356.923 170.769 C 350.769 170.769 345.128 173.333 340 178.461 C 334.872 183.59 332.308 189.231 332.308 195.384 L 332.308 663.077 C 332.308 669.231 334.872 674.872 340 680 C 345.128 685.129 350.769 687.693 356.923 687.693 Z M 255.384 820 C 240.013 820 226.939 814.612 216.163 803.837 C 205.388 793.062 200 779.988 200 764.616 L 200 266.154 L 230.769 266.154 L 230.769 764.616 C 230.769 770.77 233.333 776.411 238.461 781.539 C 243.59 786.667 249.231 789.231 255.384 789.231 L 633.847 789.231 L 633.847 820 L 255.384 820 Z M 332.308 687.693 L 332.308 170.769 L 332.308 687.693 Z", + "revanced_overlay_button_copy_video_url_timestamp" to "M 680.616 860 C 637.301 860 600.677 845.033 570.744 815.1 C 540.811 785.167 525.844 748.543 525.844 705.228 C 525.844 661.911 540.823 625.287 570.78 595.356 C 600.737 565.423 637.221 550.456 680.232 550.456 C 723.805 550.456 760.557 565.435 790.488 595.392 C 820.421 625.349 835.388 661.833 835.388 704.844 C 835.388 748.417 820.421 785.169 790.488 815.1 C 760.555 845.033 723.931 860 680.616 860 Z M 742.904 795.848 L 764.692 774 L 692 701.308 L 692 592.384 L 661.46 592.384 L 661.46 713 L 742.9 795.848 L 742.904 795.848 Z M 356.924 718.46 C 341.551 718.46 328.477 713.072 317.704 702.296 C 306.928 691.52 301.54 678.447 301.54 663.076 L 301.54 195.384 C 301.54 180.013 306.928 166.94 317.704 156.164 C 328.477 145.388 341.551 140 356.924 140 L 704.616 140 C 719.987 140 733.06 145.388 743.836 156.164 C 754.612 166.94 760 180.013 760 195.384 L 760 462.616 C 754.101 460.717 748.877 458.891 744.328 457.136 C 739.779 455.379 734.745 453.591 729.228 451.772 L 729.228 195.384 C 729.231 189.229 726.668 183.588 721.54 178.46 C 716.412 173.332 710.771 170.768 704.616 170.768 L 356.924 170.768 C 350.769 170.768 345.128 173.332 340 178.46 C 334.872 183.588 332.308 189.229 332.308 195.384 L 332.308 663.076 C 332.308 669.231 334.872 674.872 340 680 C 345.128 685.128 350.769 687.692 356.924 687.692 L 416.924 687.692 L 416.924 718.46 L 356.924 718.46 Z M 255.384 820 C 240.013 820 226.94 814.612 216.164 803.836 C 205.388 793.06 200 779.987 200 764.616 L 200 266.152 L 230.768 266.152 L 230.768 764.616 C 230.768 770.771 233.332 776.412 238.46 781.54 C 243.588 786.668 249.229 789.232 255.384 789.232 L 440 789.232 C 441.795 794.616 443.872 799.705 446.232 804.5 C 448.592 809.295 451.567 814.461 455.2 820 L 255.384 820 Z M 332.308 687.692 L 332.308 170.768 L 332.308 687.692 Z", + "revanced_overlay_button_mute_volume" to "revanced_enable_swipe_volume", + "revanced_overlay_button_external_downloader" to "M 480 626.231 L 341.615 487.846 L 363.846 466.384 L 464.615 566.384 L 464.615 200 L 495.385 200 L 495.385 566.384 L 596.154 466.384 L 618.385 487.846 L 480 626.231 Z M 255.384 760 Q 232.327 760 216.163 743.837 Q 200 727.673 200 704.616 L 200 597 L 230.769 597 L 230.769 704.616 Q 230.769 713.846 238.461 721.539 Q 246.154 729.231 255.384 729.231 L 704.616 729.231 Q 713.846 729.231 721.539 721.539 Q 729.231 713.846 729.231 704.616 L 729.231 597 L 760 597 L 760 704.616 Q 760 727.673 743.837 743.837 Q 727.673 760 704.616 760 L 255.384 760 Z", + "revanced_overlay_button_speed_dialog" to "M 425.461 614.616 Q 443.077 632.616 475.038 630.346 Q 507 628.077 520.846 607.077 L 727.616 312.692 L 433.385 519.385 Q 411.846 534 409.846 565.308 Q 407.846 596.615 425.461 614.616 Z M 478.769 200.231 Q 533.462 200.231 582.385 214.884 Q 631.308 229.538 679.846 262.231 L 654 282.308 Q 613.615 256.154 568.269 243.577 Q 522.923 231 478.961 231 Q 342.121 231 246.445 327.639 Q 150.769 424.278 150.769 561.744 Q 150.769 605.154 162.5 648.462 Q 174.231 691.769 196.667 729.231 L 760.846 729.231 Q 783.615 692.462 795.462 647.923 Q 807.308 603.385 807.308 558.923 Q 807.308 520 795.962 473.423 Q 784.615 426.846 758 388.923 L 778.539 363.077 Q 811.923 416.769 824.385 462.884 Q 836.846 509 838.077 556.769 Q 838.539 609.077 826.846 654.385 Q 815.154 699.692 790.462 743.923 Q 784.616 753.846 777.654 756.923 Q 770.692 760 759.154 760 L 198.154 760 Q 189.511 760 181.14 754.962 Q 172.769 749.923 167.846 740.846 Q 148 705.154 134 661.423 Q 120 617.692 120 561.692 Q 120 487.923 147.978 422.218 Q 175.956 356.513 224.247 307.295 Q 272.538 258.077 338.295 229.154 Q 404.052 200.231 478.769 200.231 Z M 473.615 487.385 Z", + "revanced_overlay_button_play_all" to "M 160 626.154 L 160 595.384 L 434.461 595.384 L 434.461 626.154 L 160 626.154 Z M 160 463.462 L 160 432.692 L 595.308 432.692 L 595.308 463.462 L 160 463.462 Z M 160 301.538 L 160 270.769 L 595.308 270.769 L 595.308 301.538 L 160 301.538 Z M 668.154 800 L 668.154 561.077 L 840.769 681.308 L 668.154 800 Z", + "revanced_overlay_button_whitelist" to "M 803.769 847.462 L 711.846 756.308 Q 664.923 795.385 606.077 817.693 Q 547.231 840 480 840 Q 404.838 840 339.138 812.266 Q 273.438 784.531 224.454 735.546 Q 175.469 686.562 147.734 620.862 Q 120 555.162 120 480 Q 120 412.769 142.307 353.923 Q 164.615 295.077 203.692 248.154 L 112.538 156.231 L 134.769 134.769 L 825.231 825.231 L 803.769 847.462 Z M 480 809.231 Q 540.385 809.231 594 789.5 Q 647.615 769.769 690.385 734.077 L 225.923 269.615 Q 190.231 312.385 170.5 366 Q 150.769 419.615 150.769 480 Q 150.769 618.077 246.346 713.654 Q 341.923 809.231 480 809.231 Z M 780.154 680.308 L 757.923 658.077 Q 782.077 619.923 795.654 574.769 Q 809.231 529.615 809.231 480 Q 809.231 341.923 713.654 246.346 Q 618.077 150.769 480 150.769 Q 430.385 150.769 385.231 164.346 Q 340.077 177.923 301.923 202.077 L 279.692 179.846 Q 322.562 151.375 373.05 135.687 Q 423.538 120 480 120 Q 554.931 120 620.631 147.85 Q 686.331 175.7 735.316 224.684 Q 784.3 273.669 812.15 339.369 Q 840 405.069 840 480 Q 840 536.462 824.313 586.95 Q 808.625 637.438 780.154 680.308 Z M 529.923 430.077 Z M 457.769 502.231 Z", + "revanced_preference_screen_account_menu" to "account_switcher_key", + "revanced_preference_screen_action_buttons" to "M 258.308 662.923 L 701.692 662.923 L 701.692 589.461 L 258.308 589.461 L 258.308 662.923 Z M 175.384 760 Q 152.327 760 136.163 743.837 Q 120 727.673 120 704.616 L 120 255.384 Q 120 232.327 136.163 216.163 Q 152.327 200 175.384 200 L 784.616 200 Q 807.673 200 823.837 216.163 Q 840 232.327 840 255.384 L 840 704.616 Q 840 727.673 823.837 743.837 Q 807.673 760 784.616 760 L 175.384 760 Z M 175.384 729.231 L 784.616 729.231 Q 793.846 729.231 801.539 721.539 Q 809.231 713.846 809.231 704.616 L 809.231 255.384 Q 809.231 246.154 801.539 238.461 Q 793.846 230.769 784.616 230.769 L 175.384 230.769 Q 166.154 230.769 158.461 238.461 Q 150.769 246.154 150.769 255.384 L 150.769 704.616 Q 150.769 713.846 158.461 721.539 Q 166.154 729.231 175.384 729.231 Z M 150.769 729.231 L 150.769 230.769 L 150.769 729.231 Z", + "revanced_preference_screen_ambient_mode" to "M 215.384 680 Q 192.327 680 176.163 663.837 Q 160 647.673 160 624.616 L 160 335.384 Q 160 312.327 176.163 296.163 Q 192.327 280 215.384 280 L 744.616 280 Q 767.673 280 783.837 296.163 Q 800 312.327 800 335.384 L 800 624.616 Q 800 647.673 783.837 663.837 Q 767.673 680 744.616 680 L 215.384 680 Z M 215.384 649.231 L 744.616 649.231 Q 753.846 649.231 761.539 641.539 Q 769.231 633.846 769.231 624.616 L 769.231 335.384 Q 769.231 326.154 761.539 318.461 Q 753.846 310.769 744.616 310.769 L 215.384 310.769 Q 206.154 310.769 198.461 318.461 Q 190.769 326.154 190.769 335.384 L 190.769 624.616 Q 190.769 633.846 198.461 641.539 Q 206.154 649.231 215.384 649.231 Z M 190.769 649.231 L 190.769 310.769 L 190.769 649.231 Z M 191.154 800 L 169.154 778 L 227.154 720 L 249.154 742 L 191.154 800 Z M 768.846 800 L 711.615 742 L 732.846 720 L 790.846 778 L 768.846 800 Z M 465.846 815.616 L 465.846 724.846 L 496.615 724.846 L 496.615 815.616 L 465.846 815.616 Z M 227.154 240.046 L 169.154 182.815 L 191.154 160.815 L 249.154 218.815 L 227.154 240.046 Z M 732.846 240.046 L 711.615 218.815 L 768.846 160.815 L 790.846 182.815 L 732.846 240.046 Z M 465.846 239.97 L 465.846 149.2 L 496.615 149.2 L 496.615 239.97 L 465.846 239.97 Z", + "revanced_preference_screen_category_bar" to "M 342.08 364.15 L 456.77 176.38 Q 461 170.15 467.19 167.04 Q 473.38 163.92 480.23 163.92 Q 487.08 163.92 493.65 167.04 Q 500.23 170.15 504.46 176.38 L 619.15 364.15 Q 623.38 371 623.27 378.81 Q 623.15 386.62 619.54 392.85 Q 615.92 399.08 610.02 402.69 Q 604.11 406.31 595.46 406.31 L 365 406.31 Q 356.61 406.31 350.32 402.47 Q 344.03 398.63 341.31 392.85 Q 338.08 386.86 337.96 379.12 Q 337.85 371.38 342.08 364.15 Z M 702.54 849.23 Q 640.46 849.23 598.54 806.92 Q 556.62 764.62 556.62 702.54 Q 556.62 640.46 598.54 598.54 Q 640.46 556.62 702.54 556.62 Q 764.62 556.62 806.92 598.54 Q 849.23 640.46 849.23 702.54 Q 849.23 764.62 806.92 806.92 Q 764.62 849.23 702.54 849.23 Z M 150.77 798.82 L 150.77 603.13 Q 150.77 592.1 158.74 583.86 Q 166.71 575.62 178.49 575.62 L 374.18 575.62 Q 385.96 575.62 393.83 583.96 Q 401.69 592.3 401.69 603.33 L 401.69 799.03 Q 401.69 810.81 393.72 818.67 Q 385.75 826.54 373.97 826.54 L 178.28 826.54 Q 166.5 826.54 158.63 818.57 Q 150.77 810.6 150.77 798.82 Z M 702.99 818.46 Q 750.92 818.46 784.69 784.63 Q 818.46 750.8 818.46 702.48 Q 818.46 654.15 784.63 620.77 Q 750.8 587.38 702.48 587.38 Q 654.15 587.38 620.77 620.91 Q 587.38 654.43 587.38 702.99 Q 587.38 750.92 620.91 784.69 Q 654.43 818.46 702.99 818.46 Z M 181.54 795.77 L 370.92 795.77 L 370.92 606.38 L 181.54 606.38 L 181.54 795.77 Z M 369.77 375.54 L 591.46 375.54 L 480.23 198.38 L 369.77 375.54 Z M 480.23 375.54 Z M 370.92 606.38 Z M 702.92 702.92 Z", + "revanced_preference_screen_channel_bar" to "account_switcher_key", + "revanced_preference_screen_channel_profile" to "account_switcher_key", + "revanced_preference_screen_comments" to "revanced_hide_quick_actions_comment_button", + "revanced_preference_screen_community_posts" to "M 289.46 649.23 Q 277.85 649.23 268.15 639.42 Q 258.46 629.62 258.46 617.23 L 258.46 569.23 L 719.23 569.23 L 744.62 594.62 L 744.62 240 L 793.85 240 Q 805.46 240 815.04 249.81 Q 824.62 259.62 824.62 272.23 L 824.62 705.46 Q 824.62 724.45 807.65 731.3 Q 790.69 738.15 777.46 724.92 L 701.77 649.23 L 289.46 649.23 Z M 258.46 498.46 L 182.54 574.38 Q 169.31 587.62 152.35 580.76 Q 135.38 573.91 135.38 554.92 L 135.38 152 Q 135.38 139.62 144.96 129.81 Q 154.54 120 166.15 120 L 642.85 120 Q 654.69 120 664.27 129.69 Q 673.85 139.38 673.85 152 L 673.85 466.46 Q 673.85 478.85 664.27 488.65 Q 654.69 498.46 642.85 498.46 L 258.46 498.46 Z M 643.08 467.69 L 643.08 150.77 L 166.15 150.77 L 166.15 523.08 L 221.54 467.69 L 643.08 467.69 Z M 166.15 467.69 L 166.15 150.77 L 166.15 467.69 Z", + "revanced_preference_screen_custom_filter" to "M 470.77 760 Q 457.62 760 448.81 751.19 Q 440 742.38 440 729.23 L 440 506.15 L 221 228.85 Q 213.15 220.31 218.92 210.15 Q 224.69 200 234.92 200 L 725.08 200 Q 735.31 200 741.08 210.15 Q 746.85 220.31 739 228.85 L 520 506.15 L 520 729.23 Q 520 742.38 511.19 751.19 Q 502.38 760 489.23 760 L 470.77 760 Z M 480 507.08 L 697.69 230.77 L 262.31 230.77 L 480 507.08 Z M 480 507.08 Z", + "revanced_preference_screen_feed_flyout_menu" to "revanced_preference_screen_player_flyout_menu", + "revanced_preference_screen_fullscreen" to "M 160 800 L 160 626.231 L 190.769 626.231 L 190.769 769.231 L 333.769 769.231 L 333.769 800 L 160 800 Z M 627 800 L 627 769.231 L 770 769.231 L 770 626.231 L 800.769 626.231 L 800.769 800 L 627 800 Z M 160 333.769 L 160 160 L 333.769 160 L 333.769 190.769 L 190.769 190.769 L 190.769 333.769 L 160 333.769 Z M 770 333.769 L 770 190.769 L 627 190.769 L 627 160 L 800.769 160 L 800.769 333.769 L 770 333.769 Z", + "revanced_preference_screen_haptic_feedback" to "revanced_enable_swipe_haptic_feedback", + "revanced_preference_screen_hook_buttons" to "revanced_preference_screen_player_buttons", + "revanced_preference_screen_import_export" to "M 300.38 743.08 L 258.69 701.38 Q 208.69 649.85 185.27 594.88 Q 161.85 539.92 161.85 484.77 Q 161.85 397.69 206.77 324.96 Q 251.69 252.23 327.31 211.77 Q 333.69 208.85 340.19 209.5 Q 346.69 210.15 349.38 216.54 Q 352.08 222.15 349.5 228.15 Q 346.92 234.15 341.31 237.08 Q 272.46 272.85 232.54 339.27 Q 192.62 405.69 192.62 484.77 Q 192.62 536.77 211.65 584.42 Q 230.69 632.08 272.69 672.38 L 322.69 720.92 L 322.69 606.23 Q 322.69 599.38 326.96 595.12 Q 331.23 590.85 338.08 590.85 Q 344.15 590.85 348.81 595.12 Q 353.46 599.38 353.46 606.23 L 353.46 746.15 Q 353.46 758.38 345.35 766.12 Q 337.23 773.85 325.77 773.85 L 185.85 773.85 Q 179 773.85 174.73 769.58 Q 170.46 765.31 170.46 758.46 Q 170.46 751.62 174.73 747.35 Q 179 743.08 185.85 743.08 L 300.38 743.08 Z M 638.08 239.08 L 638.08 353.77 Q 638.08 360.62 633.42 364.88 Q 628.77 369.15 622.69 369.15 Q 615.85 369.15 611.58 364.88 Q 607.31 360.62 607.31 353.77 L 607.31 213.85 Q 607.31 201.62 615.04 193.88 Q 622.77 186.15 635 186.15 L 774.15 186.15 Q 781 186.15 785.27 190.42 Q 789.54 194.69 789.54 201.54 Q 789.54 208.38 785.27 212.65 Q 781 216.92 774.15 216.92 L 659.38 216.92 L 701.31 258.62 Q 743.92 300.54 766.08 346.69 Q 788.23 392.85 793.31 439.08 L 763.31 439.08 Q 757.46 396.54 739.08 358.42 Q 720.69 320.31 688.08 287.62 L 638.08 239.08 Z M 700.77 832.31 Q 695.54 832.31 691.54 828.81 Q 687.54 825.31 686.54 819.31 L 686.38 803.54 Q 660.23 798.31 640.38 786.42 Q 620.54 774.54 606.77 758.23 L 592.92 766.85 Q 587.92 769.62 582.42 768.62 Q 576.92 767.62 574.69 763.39 L 569.85 756.77 Q 566.08 751.77 567.08 746.38 Q 568.08 741 572.31 737.77 L 586.23 727.31 Q 576.62 701.23 576.62 678.5 Q 576.62 655.77 586.23 629.69 L 572.31 619.23 Q 568.08 616 567.08 610.62 Q 566.08 605.23 569.85 600.23 L 574.69 592.62 Q 576.92 588.38 582.42 587.77 Q 587.92 587.15 592.92 589.92 L 606.77 598.54 Q 620.54 582.69 640.38 570.69 Q 660.23 558.69 686.38 553.46 L 686.54 537.46 Q 687.54 531.46 691.54 527.96 Q 695.54 524.46 700.77 524.46 L 705.31 524.46 Q 710.54 524.46 714.54 527.96 Q 718.54 531.46 719.54 537.46 L 719.69 553.46 Q 745.85 558.69 765.69 570.69 Q 785.54 582.69 799.31 597.77 L 813.92 589.92 Q 818.92 587.15 823.65 587.77 Q 828.38 588.38 831.38 592.62 L 836.23 600.23 Q 840 605.23 839 610.23 Q 838 615.23 833 619.23 L 819.85 629.69 Q 829.46 655.77 829.46 678.12 Q 829.46 700.46 819.85 727.31 L 833.77 737.77 Q 838 741 839 746.38 Q 840 751.77 836.23 756.77 L 831.38 763.39 Q 828.38 767.62 823.27 768.62 Q 818.15 769.62 813.15 766.85 L 799.31 758.23 Q 785.54 774.54 765.69 786.42 Q 745.85 798.31 719.69 803.54 L 719.54 819.31 Q 718.54 825.31 714.54 828.81 Q 710.54 832.31 705.31 832.31 L 700.77 832.31 Z M 702.92 774.23 Q 743.08 774.23 770.92 746.38 Q 798.77 718.54 798.77 678.38 Q 798.77 638.23 770.92 610 Q 743.08 581.77 702.92 581.77 Q 662 581.77 634.15 610 Q 606.31 638.23 606.31 678.38 Q 606.31 718.54 634.15 746.38 Q 662 774.23 702.92 774.23 Z", + "revanced_preference_screen_miniplayer" to "offline_key", + "revanced_preference_screen_navigation_bar" to "M 160 640 L 160 320 L 800 320 L 800 466.154 L 769.231 466.154 L 769.231 350.769 L 190.769 350.769 L 190.769 609.231 L 601.538 609.231 L 601.538 640 L 160 640 Z M 190.769 609.231 L 190.769 350.769 L 190.769 609.231 Z M 871.692 758.308 L 718.462 605.308 L 718.462 740 L 687.692 740 L 687.692 552.308 L 875.385 552.308 L 875.385 583.077 L 740.462 583.077 L 892.923 737.077 L 871.692 758.308 Z", + "revanced_preference_screen_patch_information" to "about_key", + "revanced_preference_screen_player_buttons" to "M 495.308 769.231 L 175.384 769.231 Q 152.154 769.231 136.077 753.154 Q 120 737.077 120 713.846 L 120 255.384 Q 120 232.154 136.077 216.077 Q 152.154 200 175.384 200 L 790.923 200 Q 814.154 200 830.231 216.077 Q 846.308 232.154 846.308 255.384 L 846.308 452.154 L 815.539 452.154 L 815.539 255.384 Q 815.539 244.615 808.616 237.692 Q 801.693 230.769 790.923 230.769 L 175.384 230.769 Q 164.615 230.769 157.692 237.692 Q 150.769 244.615 150.769 255.384 L 150.769 713.846 Q 150.769 724.616 157.692 731.539 Q 164.615 738.462 175.384 738.462 L 495.308 738.462 L 495.308 769.231 Z M 407.308 624.539 L 407.308 344.692 L 621.846 484.615 L 407.308 624.539 Z M 721.539 832.308 L 720.385 803.539 Q 694.231 798.308 674.385 786.423 Q 654.539 774.539 640.769 758.231 L 615.692 772.847 L 595.308 745.77 L 620.231 727.308 Q 610.615 701.231 610.615 678.5 Q 610.615 655.77 620.231 629.693 L 595.308 611.231 L 615.692 583.154 L 640.769 598.539 Q 654.539 582.692 674.385 570.692 Q 694.231 558.692 720.385 553.462 L 721.539 524.462 L 752.308 524.462 L 753.693 553.462 Q 779.846 558.692 799.693 570.692 Q 819.539 582.692 833.308 597.769 L 858.385 583.154 L 878.77 611.231 L 853.846 629.693 Q 863.462 655.77 863.462 678.116 Q 863.462 700.462 853.846 727.308 L 878.77 745.77 L 858.385 772.847 L 833.308 758.231 Q 819.539 774.539 799.693 786.423 Q 779.846 798.308 753.693 803.539 L 752.308 832.308 L 721.539 832.308 Z M 736.923 774.231 Q 777.077 774.231 804.923 746.385 Q 832.769 718.539 832.769 678.385 Q 832.769 638.231 804.923 610 Q 777.077 581.769 736.923 581.769 Q 696 581.769 668.154 610 Q 640.308 638.231 640.308 678.385 Q 640.308 718.539 668.154 746.385 Q 696 774.231 736.923 774.231 Z", + "revanced_preference_screen_player_flyout_menu" to "M 392.385 741.231 L 392.385 710.461 L 800 710.461 L 800 741.231 L 392.385 741.231 Z M 392.385 495.385 L 392.385 464.615 L 800 464.615 L 800 495.385 L 392.385 495.385 Z M 392.385 249.308 L 392.385 218.538 L 800 218.538 L 800 249.308 L 392.385 249.308 Z M 208.299 772.846 Q 188.209 772.846 174.22 759.063 Q 160.231 745.279 160.231 725.577 Q 160.231 705.875 174.13 691.976 Q 188.029 678.077 207.731 678.077 Q 227.433 678.077 241.216 692.451 Q 255 706.825 255 726.462 Q 255 745.273 241.282 759.06 Q 227.563 772.846 208.299 772.846 Z M 208.299 527 Q 188.209 527 174.22 512.92 Q 160.231 498.839 160.231 480 Q 160.231 461.161 174.377 447.08 Q 188.523 433 208.387 433 Q 227.427 433 241.213 447.08 Q 255 461.161 255 480 Q 255 498.839 241.282 512.92 Q 227.563 527 208.299 527 Z M 207.231 280.923 Q 188.391 280.923 174.311 266.843 Q 160.231 252.763 160.231 233.923 Q 160.231 215.084 174.311 201.003 Q 188.391 186.923 207.615 186.923 Q 226.839 186.923 240.92 201.003 Q 255 215.084 255 233.923 Q 255 252.763 240.968 266.843 Q 226.936 280.923 207.231 280.923 Z", + "revanced_preference_screen_seekbar" to "M 175.285 555.385 Q 143.665 555.385 121.832 533.485 Q 100 511.586 100 479.87 Q 100 448.154 121.832 426.385 Q 143.665 404.615 175.285 404.615 Q 202.933 404.615 223.312 421.615 Q 243.692 438.615 249.615 464.615 L 860 464.615 L 860 495.385 L 249.615 495.385 Q 243.692 521.385 223.312 538.385 Q 202.933 555.385 175.285 555.385 Z", + "revanced_preference_screen_settings_menu" to "M 413.384 840 L 397.231 725.539 Q 375.154 718.539 348.769 703.846 Q 322.385 689.154 304.077 672.308 L 198.384 720.154 L 131.538 601.538 L 225.692 531.769 Q 223.692 519.692 222.423 506.269 Q 221.154 492.846 221.154 480.769 Q 221.154 469.462 222.423 456.038 Q 223.692 442.615 225.692 428.231 L 131.538 357.692 L 198.384 241.384 L 303.308 287.692 Q 323.923 270.846 348.769 256.538 Q 373.615 242.231 396.461 235.461 L 413.384 120 L 546.616 120 L 562.769 235.231 Q 587.923 244.538 610.577 257.192 Q 633.231 269.846 653.615 287.692 L 762.385 241.384 L 828.462 357.692 L 731.231 429.308 Q 734.769 443.154 735.654 455.808 Q 736.539 468.462 736.539 480 Q 736.539 490.769 735.269 503.308 Q 734 515.846 731.231 531.231 L 827.693 601.538 L 760.846 720.154 L 653.615 671.539 Q 632.231 689.923 609.423 703.962 Q 586.616 718 562.769 724.769 L 546.616 840 L 413.384 840 Z M 438.308 809.231 L 520.923 809.231 L 535.692 698 Q 566.385 690 592.039 675.308 Q 617.692 660.615 644 635.846 L 746.923 680.308 L 786.923 610.615 L 696 543.154 Q 700 524.615 702.115 509.654 Q 704.231 494.692 704.231 480 Q 704.231 463.769 702.231 449.577 Q 700.231 435.385 696 418.385 L 788.462 349.385 L 748.462 279.692 L 643.231 324.154 Q 624.077 302.769 593.539 284.115 Q 563 265.461 534.923 262 L 521.692 150.769 L 438.308 150.769 L 425.846 261.231 Q 393.385 267.461 366.961 282.538 Q 340.538 297.615 315.231 323.385 L 211.538 279.692 L 171.538 349.385 L 262.461 416.077 Q 257.692 430.769 255.577 446.885 Q 253.461 463 253.461 480.769 Q 253.461 497 255.577 512.346 Q 257.692 527.692 261.692 543.154 L 171.538 610.615 L 211.538 680.308 L 314.461 636.615 Q 338.461 661.385 365.269 676.077 Q 392.077 690.769 425.077 698.769 L 438.308 809.231 Z M 477.692 575.385 Q 517.846 575.385 545.462 547.769 Q 573.077 520.154 573.077 480 Q 573.077 439.846 545.462 412.231 Q 517.846 384.615 477.692 384.615 Q 438.308 384.615 410.308 412.231 Q 382.307 439.846 382.307 480 Q 382.307 520.154 410.308 547.769 Q 438.308 575.385 477.692 575.385 Z M 480 480 Z", + "revanced_preference_screen_shorts_player" to "revanced_preference_screen_shorts", + "revanced_preference_screen_spoof_streaming_data" to "M 270.77 793.85 L 270.77 824.62 Q 270.77 833.85 278.46 841.54 Q 286.15 849.23 295.38 849.23 L 664.62 849.23 Q 673.85 849.23 681.54 841.54 Q 689.23 833.85 689.23 824.62 L 689.23 793.85 L 270.77 793.85 Z M 687.54 569.69 Q 669.38 566.69 656.69 558.77 Q 644 550.85 635.46 539.77 L 620.54 547.38 Q 615.54 550.15 610.42 548.77 Q 605.31 547.38 602.31 543.15 L 600.38 540.92 Q 597.38 536.69 598.94 531.55 Q 600.49 526.41 604.38 523.23 L 618.62 512.69 Q 612.08 497.08 612.08 479.54 Q 612.08 462 618.62 445.62 L 604.38 435.08 Q 600.49 432.1 598.94 426.86 Q 597.38 421.62 600.41 416.72 L 602.31 415.15 Q 604.54 410.15 609.65 409.15 Q 614.77 408.15 619.77 410.92 L 635.46 418.54 Q 644 407.46 656.46 399.54 Q 668.92 391.62 687.54 388.62 L 689.23 370.23 Q 689.16 364.45 692.93 360.84 Q 696.71 357.23 702.35 357.23 L 706.46 357.23 Q 711.83 357.23 715.27 360.93 Q 718.7 364.62 719.54 370.23 L 721.23 388.62 Q 739.85 391.62 751.92 399.54 Q 764 407.46 773.31 418.54 L 788.23 410.92 Q 793.23 408.15 798.35 409.54 Q 803.46 410.92 806.46 415.92 L 808.38 417.38 Q 810.62 421.62 809.45 426.86 Q 808.28 432.1 804.38 435.08 L 790.15 445.62 Q 796.69 462 796.69 479.54 Q 796.69 497.08 790.15 512.69 L 804.38 523.23 Q 808.28 527.18 809.45 532.32 Q 810.62 537.46 807.62 541.69 L 805.69 543.92 Q 803.46 548.15 798.35 549.15 Q 793.23 550.15 788.23 547.38 L 773.31 539.77 Q 764 550.85 751.31 558.77 Q 738.62 566.69 721.23 569.69 L 719.54 588.08 Q 718.67 593.34 715.07 596.82 Q 711.46 600.31 706.08 600.31 L 701.85 600.31 Q 696.46 600.31 692.79 596.7 Q 689.13 593.09 689.23 588.08 L 687.54 569.69 Z M 704 543.23 Q 730.85 543.23 749.85 524.62 Q 768.85 506 768.85 479.15 Q 768.85 452.31 749.95 433.69 Q 731.05 415.08 704.38 415.08 Q 677.15 415.08 658.54 433.76 Q 639.92 452.44 639.92 478.77 Q 639.92 506 658.54 524.62 Q 677.15 543.23 704 543.23 Z M 270.77 166.15 L 689.23 166.15 L 689.23 135.38 Q 689.23 126.15 681.54 118.46 Q 673.85 110.77 664.62 110.77 L 295.38 110.77 Q 286.15 110.77 278.46 118.46 Q 270.77 126.15 270.77 135.38 L 270.77 166.15 Z M 270.77 166.15 L 270.77 110.77 L 270.77 166.15 Z M 270.77 793.85 L 270.77 849.23 L 270.77 793.85 Z M 295.38 880 Q 272.94 880 256.47 863.53 Q 240 847.06 240 824.62 L 240 135.38 Q 240 112.94 256.47 96.47 Q 272.94 80 295.38 80 L 664.62 80 Q 687.06 80 703.53 96.47 Q 720 112.94 720 135.38 L 720 246 Q 720 252.58 715.54 256.98 Q 711.08 261.38 704.43 261.38 Q 697.77 261.38 693.5 256.98 Q 689.23 252.58 689.23 246 L 689.23 196.92 L 270.77 196.92 L 270.77 763.08 L 689.23 763.08 L 689.23 714 Q 689.23 707.42 693.69 703.02 Q 698.15 698.62 704.8 698.62 Q 711.46 698.62 715.73 703.02 Q 720 707.42 720 714 L 720 824.62 Q 720 847.06 703.53 863.53 Q 687.06 880 664.62 880 L 295.38 880 Z", + "revanced_preference_screen_toolbar" to "M 215.38 800 Q 192.33 800 176.16 783.84 Q 160 767.67 160 744.62 L 160 215.38 Q 160 192.33 176.16 176.16 Q 192.33 160 215.38 160 L 744.62 160 Q 767.67 160 783.84 176.16 Q 800 192.33 800 215.38 L 800 744.62 Q 800 767.67 783.84 783.84 Q 767.67 800 744.62 800 L 215.38 800 Z M 190.77 323.15 L 769.23 323.15 L 769.23 215.38 Q 769.23 206.15 761.54 198.46 Q 753.85 190.77 744.62 190.77 L 215.38 190.77 Q 206.15 190.77 198.46 198.46 Q 190.77 206.15 190.77 215.38 L 190.77 323.15 Z M 769.23 353.92 L 190.77 353.92 L 190.77 744.62 Q 190.77 753.85 198.46 761.54 Q 206.15 769.23 215.38 769.23 L 744.62 769.23 Q 753.85 769.23 761.54 761.54 Q 769.23 753.85 769.23 744.62 L 769.23 353.92 Z M 190.77 323.15 L 190.77 353.92 L 190.77 323.15 Z M 190.77 323.15 L 190.77 190.77 L 190.77 323.15 Z M 190.77 353.92 L 190.77 769.23 L 190.77 353.92 Z", + "revanced_preference_screen_video_description" to "M 389.15 530.77 L 527.23 530.77 Q 533.81 530.77 538.21 526.31 Q 542.62 521.85 542.62 515.2 Q 542.62 508.54 538.21 504.27 Q 533.81 500 527.23 500 L 389.15 500 Q 382.58 500 378.17 504.46 Q 373.77 508.92 373.77 515.57 Q 373.77 522.23 378.17 526.5 Q 382.58 530.77 389.15 530.77 Z M 389.15 424.62 L 671.08 424.62 Q 677.65 424.62 682.06 420.16 Q 686.46 415.7 686.46 409.04 Q 686.46 402.38 682.06 398.12 Q 677.65 393.85 671.08 393.85 L 389.15 393.85 Q 382.58 393.85 378.17 398.3 Q 373.77 402.76 373.77 409.42 Q 373.77 416.08 378.17 420.35 Q 382.58 424.62 389.15 424.62 Z M 389.15 318.46 L 671.08 318.46 Q 677.65 318.46 682.06 314 Q 686.46 309.55 686.46 302.89 Q 686.46 296.23 682.06 291.96 Q 677.65 287.69 671.08 287.69 L 389.15 287.69 Q 382.58 287.69 378.17 292.15 Q 373.77 296.61 373.77 303.27 Q 373.77 309.92 378.17 314.19 Q 382.58 318.46 389.15 318.46 Z M 296.92 698.46 Q 273.87 698.46 257.7 682.3 Q 241.54 666.13 241.54 643.08 L 241.54 175.38 Q 241.54 152.33 257.7 136.16 Q 273.87 120 296.92 120 L 764.62 120 Q 787.67 120 803.84 136.16 Q 820 152.33 820 175.38 L 820 643.08 Q 820 666.13 803.84 682.3 Q 787.67 698.46 764.62 698.46 L 296.92 698.46 Z M 296.92 667.69 L 764.62 667.69 Q 773.85 667.69 781.54 660 Q 789.23 652.31 789.23 643.08 L 789.23 175.38 Q 789.23 166.15 781.54 158.46 Q 773.85 150.77 764.62 150.77 L 296.92 150.77 Q 287.69 150.77 280 158.46 Q 272.31 166.15 272.31 175.38 L 272.31 643.08 Q 272.31 652.31 280 660 Q 287.69 667.69 296.92 667.69 Z M 195.38 800 Q 172.33 800 156.16 783.84 Q 140 767.67 140 744.62 L 140 261.54 Q 140 254.96 144.46 250.56 Q 148.92 246.15 155.57 246.15 Q 162.23 246.15 166.5 250.56 Q 170.77 254.96 170.77 261.54 L 170.77 744.62 Q 170.77 753.85 178.46 761.54 Q 186.15 769.23 195.38 769.23 L 678.46 769.23 Q 685.04 769.23 689.44 773.69 Q 693.85 778.15 693.85 784.8 Q 693.85 791.46 689.44 795.73 Q 685.04 800 678.46 800 L 195.38 800 Z M 272.31 150.77 L 272.31 667.69 L 272.31 150.77 Z", + "revanced_preference_screen_video_filter" to "revanced_preference_screen_video", + "revanced_preference_screen_watch_history" to "history_key", + "revanced_sanitize_sharing_links" to "M 264.874 586.16 C 219.754 586.16 181.294 570.263 149.494 538.47 C 117.694 506.677 101.794 468.227 101.794 423.12 C 101.794 378.013 117.694 339.55 149.494 307.73 C 181.294 275.91 219.754 260 264.874 260 L 395.644 260 C 399.998 260 403.651 261.497 406.604 264.49 C 409.551 267.477 411.024 271.18 411.024 275.6 C 411.024 280.02 409.551 283.653 406.604 286.5 C 403.651 289.347 399.998 290.77 395.644 290.77 L 264.784 290.77 C 228.004 290.77 196.771 303.59 171.084 329.23 C 145.404 354.87 132.564 386.073 132.564 422.84 C 132.564 459.613 145.404 490.9 171.084 516.7 C 196.771 542.493 228.004 555.39 264.784 555.39 L 395.644 555.39 C 399.998 555.39 403.651 556.883 406.604 559.87 C 409.551 562.863 411.024 566.57 411.024 570.99 C 411.024 575.41 409.551 579.043 406.604 581.89 C 403.651 584.737 399.998 586.16 395.644 586.16 L 264.874 586.16 Z M 774.144 538.511 C 771.086 540.683 766.86 540.783 764.544 540.57 C 760.366 540.185 758.078 538.357 754.98 535.666 C 751.882 532.975 751.63 531.654 750.649 528.904 C 750.182 527.596 748.871 524.358 751.415 519.354 C 751.415 519.354 791.024 460.087 791.024 423.32 C 791.024 386.547 778.184 355.26 752.504 329.46 C 726.817 303.667 695.584 290.77 658.804 290.77 L 527.944 290.77 C 523.591 290.77 519.937 289.277 516.984 286.29 C 514.037 283.297 512.564 279.59 512.564 275.17 C 512.564 270.75 514.037 267.117 516.984 264.27 C 519.937 261.423 523.591 260 527.944 260 L 658.714 260 C 703.834 260 742.294 275.897 774.094 307.69 C 805.894 339.483 821.794 377.933 821.794 423.04 C 821.794 468.147 775.114 537.606 774.094 538.43 C 773.074 539.254 774.144 538.511 774.144 538.511 Z M 339.874 438.46 C 335.514 438.46 331.861 436.967 328.914 433.98 C 325.961 430.987 324.484 427.28 324.484 422.86 C 324.484 418.44 325.961 414.807 328.914 411.96 C 331.861 409.12 335.514 407.7 339.874 407.7 L 584.484 407.7 C 588.344 407.7 591.874 409.193 595.074 412.18 C 598.274 415.173 599.874 418.88 599.874 423.3 C 599.874 427.72 598.274 431.353 595.074 434.2 C 591.874 437.04 588.344 438.46 584.484 438.46 L 339.874 438.46 Z M 646.735 588.639 L 693.414 588.639 L 693.414 455.293 C 693.625 448.262 691.73 443.428 686.819 438.764 C 682.285 433.815 677.325 431.938 670.075 431.938 C 662.807 431.938 658.004 433.624 653.477 438.498 C 648.582 443.064 646.523 448.007 646.735 455.196 L 646.735 588.639 Z M 535.979 661.988 L 804.17 661.988 L 804.17 621.073 C 804.243 619.885 804.252 619.077 804.117 618.501 C 803.933 617.98 803.629 617.584 802.916 617.122 L 802.882 617.102 L 802.939 617.136 L 802.837 617.054 L 802.682 616.929 L 802.549 616.765 C 802.173 616.141 801.725 615.567 801.043 615.343 C 800.452 615.148 799.673 615.201 798.63 615.302 L 541.758 615.309 C 540.691 615.221 539.923 615.135 539.324 615.274 C 538.608 615.44 538.111 616 537.717 616.642 L 537.652 616.721 L 537.57 616.815 L 537.447 616.924 C 536.071 617.969 535.788 618.595 535.971 620.804 L 535.979 661.988 Z M 508.72 774.053 L 508.78 774.118 L 508.859 774.245 C 509.474 775.283 509.976 775.881 510.571 776.247 C 511.209 776.521 511.95 776.583 513.159 776.417 L 513.22 776.409 L 513.152 776.418 L 565.057 776.41 L 565.057 732.567 C 565.284 729.457 566.793 725.542 568.823 723.197 L 568.859 723.155 L 568.892 723.117 L 568.98 723.039 C 571.239 721.103 575.188 719.199 578.494 719.199 C 581.796 719.199 585.587 720.982 587.831 722.966 L 587.879 723.011 L 587.933 723.06 L 587.966 723.091 L 588.065 723.203 C 589.949 725.489 591.494 729.213 591.72 732.322 L 591.726 776.41 L 656.742 776.41 L 656.742 732.568 C 656.974 729.451 658.491 725.532 660.502 723.204 L 660.527 723.174 L 660.572 723.122 L 660.672 723.035 C 662.928 721.096 666.87 719.199 670.179 719.199 C 673.482 719.199 677.271 720.98 679.514 722.964 L 679.567 723.014 L 679.616 723.059 L 679.65 723.09 L 679.746 723.198 C 681.629 725.486 683.175 729.217 683.401 732.322 L 683.407 776.41 L 748.423 776.41 L 748.423 732.568 C 748.656 729.451 750.165 725.553 752.17 723.221 L 752.225 723.161 L 752.262 723.12 L 752.344 723.048 C 754.601 721.106 758.552 719.199 761.86 719.199 C 765.214 719.199 768.948 721.002 771.135 722.905 L 771.253 723.013 L 771.299 723.055 L 771.418 723.196 C 773.249 725.43 774.834 729.143 775.085 732.292 L 775.093 776.41 L 826.747 776.41 L 826.743 776.41 C 828.006 776.549 828.813 776.524 829.478 776.288 C 830.101 775.964 830.588 775.454 831.148 774.44 L 831.197 774.357 L 831.239 774.29 L 831.34 774.176 C 832.184 773.282 832.647 772.626 832.853 771.97 C 832.961 771.295 832.846 770.602 832.392 769.542 L 832.395 769.548 L 807.926 684.253 L 532.224 684.253 L 507.807 769.382 L 507.811 769.372 C 507.368 770.472 507.205 771.203 507.272 771.877 C 507.439 772.532 507.826 773.138 508.662 773.991 L 508.72 774.053 Z M 823.872 803.08 L 516.178 803.08 C 505.681 802.767 495.733 797.822 488.81 789.629 C 482.33 780.953 480.227 769.972 482.969 759.447 L 509.309 668.961 L 509.309 623.173 C 509.536 614.027 513.263 605.083 519.403 598.606 C 525.882 592.467 534.786 588.865 543.927 588.639 L 620.065 588.639 L 620.065 455.228 C 620.295 441.882 625.518 429.339 634.704 419.822 C 644.207 410.638 656.692 405.268 670.075 405.268 C 683.46 405.268 696.029 410.726 705.528 419.906 C 714.719 429.427 719.854 441.944 720.084 455.286 L 720.084 588.639 L 796.305 588.639 C 805.448 588.867 814.4 592.6 820.877 598.737 C 827.014 605.213 830.615 614.113 830.84 623.256 L 830.84 668.97 L 857.238 760.388 C 859.706 770.645 857.386 781.446 851.039 789.956 C 844.225 798.038 834.36 802.775 823.872 803.08 Z", + "revanced_swipe_gestures_lock_mode" to "revanced_hide_player_flyout_menu_lock_screen", + "revanced_swipe_magnitude_threshold" to "M 324.62 809.23 L 182 809.23 Q 169.77 809.23 161.15 800.12 Q 152.54 791 154.31 778.77 Q 166.61 660.31 235.58 578.85 Q 304.54 497.38 424.62 476.69 L 424.62 348.46 Q 386.46 344.23 345.08 333.92 Q 303.69 323.62 267.19 304.35 Q 230.69 285.08 202.5 256.46 Q 174.31 227.85 162.31 185.46 Q 158.31 172.23 166.92 161.5 Q 175.54 150.77 189.77 150.77 L 770.23 150.77 Q 784.46 150.77 793.08 161.88 Q 801.69 173 797.69 186.23 Q 784.92 228.61 757.12 257.23 Q 729.31 285.85 692.81 304.73 Q 656.31 323.62 615.31 333.92 Q 574.31 344.23 535.38 348.46 L 535.38 476.69 Q 655.46 497.38 724.42 578.85 Q 793.39 660.31 805.69 778.77 Q 807.46 791 798.85 800.12 Q 790.23 809.23 778 809.23 L 635.38 809.23 Q 628.54 809.23 624.27 804.96 Q 620 800.69 620 793.85 Q 620 787 624.27 782.73 Q 628.54 778.46 635.38 778.46 L 778.54 778.46 Q 757.46 638.77 675.62 570.88 Q 593.77 503 480 503 Q 366.23 503 284.38 570.88 Q 202.54 638.77 181.46 778.46 L 324.62 778.46 Q 331.46 778.46 335.73 782.73 Q 340 787 340 793.85 Q 340 800.69 335.73 804.96 Q 331.46 809.23 324.62 809.23 Z M 480 320.38 Q 613.69 320.38 685.23 273.12 Q 756.77 225.85 768.08 181.54 L 191.92 181.54 Q 202.46 225.85 274.38 273.12 Q 346.31 320.38 480 320.38 Z M 480 809.23 Q 454.69 809.23 437.35 791.89 Q 420 774.54 420 749.23 Q 420 736.85 424.58 726.31 Q 429.15 715.77 437.85 707.85 Q 446 699.69 463.54 689.88 Q 481.08 680.08 505.46 668.62 L 570.85 639.92 Q 579.85 635.15 586.46 642.27 Q 593.08 649.38 589.08 658.38 L 560.38 723 Q 548.92 747.38 539.23 765.31 Q 529.54 783.23 521.38 791.39 Q 513.46 800.08 502.92 804.65 Q 492.38 809.23 480 809.23 Z M 480 320.38 Z", + "revanced_swipe_overlay_background_alpha" to "M 480 800 Q 363.15 800 281.58 721.04 Q 200 642.08 200 525.23 Q 200 468.46 222.81 418.77 Q 245.62 369.08 281.92 329.69 L 451.08 164.08 Q 457.31 157.85 465.04 154.85 Q 472.77 151.85 480 151.85 Q 487.23 151.85 494.96 154.85 Q 502.69 157.85 508.92 164.08 L 678.08 329.69 Q 714.38 369.08 737.19 418.77 Q 760 468.46 760 525.23 Q 760 642.08 678.42 721.04 Q 596.85 800 480 800 Z M 233.77 560 L 726.23 560 Q 739.23 488.69 712 434.15 Q 684.77 379.62 657.54 352.62 L 480 178.08 L 302.46 352.62 Q 275.23 379.62 248.77 434.15 Q 222.31 488.69 233.77 560 Z", + "revanced_swipe_overlay_rect_size" to "M 712.62 632.15 L 603.15 632.15 Q 596.58 632.15 592.17 636.61 Q 587.77 641.07 587.77 647.73 Q 587.77 654.38 592.17 658.65 Q 596.58 662.92 603.15 662.92 L 715.69 662.92 Q 727.65 662.92 735.52 654.68 Q 743.38 646.44 743.38 635.23 L 743.38 521.23 Q 743.38 514.65 738.93 510.25 Q 734.47 505.85 727.81 505.85 Q 721.15 505.85 716.88 510.25 Q 712.62 514.65 712.62 521.23 L 712.62 632.15 Z M 247.62 327.85 L 357.08 327.85 Q 363.65 327.85 368.06 323.39 Q 372.46 318.93 372.46 312.27 Q 372.46 305.62 368.06 301.35 Q 363.65 297.08 357.08 297.08 L 244.54 297.08 Q 232.58 297.08 224.71 305.32 Q 216.85 313.56 216.85 324.77 L 216.85 438.77 Q 216.85 445.35 221.3 449.75 Q 225.76 454.15 232.42 454.15 Q 239.08 454.15 243.35 449.75 Q 247.62 445.35 247.62 438.77 L 247.62 327.85 Z M 175.38 760 Q 152.33 760 136.16 743.84 Q 120 727.67 120 704.62 L 120 255.38 Q 120 232.33 136.16 216.16 Q 152.33 200 175.38 200 L 784.62 200 Q 807.67 200 823.84 216.16 Q 840 232.33 840 255.38 L 840 704.62 Q 840 727.67 823.84 743.84 Q 807.67 760 784.62 760 L 175.38 760 Z M 175.38 729.23 L 784.62 729.23 Q 793.85 729.23 801.54 721.54 Q 809.23 713.85 809.23 704.62 L 809.23 255.38 Q 809.23 246.15 801.54 238.46 Q 793.85 230.77 784.62 230.77 L 175.38 230.77 Q 166.15 230.77 158.46 238.46 Q 150.77 246.15 150.77 255.38 L 150.77 704.62 Q 150.77 713.85 158.46 721.54 Q 166.15 729.23 175.38 729.23 Z M 150.77 729.23 L 150.77 230.77 L 150.77 729.23 Z", + "revanced_swipe_overlay_text_size" to "M 597.69 240.77 L 416.15 240.77 Q 407.82 240.77 401.99 234.86 Q 396.15 228.95 396.15 220.52 Q 396.15 212.08 401.99 206.04 Q 407.82 200 416.15 200 L 820 200 Q 828.33 200 834.17 205.91 Q 840 211.81 840 220.25 Q 840 228.69 834.17 234.73 Q 828.33 240.77 820 240.77 L 638.46 240.77 L 638.46 740 Q 638.46 748.33 632.55 754.17 Q 626.65 760 618.21 760 Q 609.77 760 603.73 753.94 Q 597.69 747.88 597.69 739.23 L 597.69 240.77 Z M 243.08 436.92 L 140 436.92 Q 131.67 436.92 125.83 431.02 Q 120 425.11 120 416.67 Q 120 408.23 125.83 402.19 Q 131.67 396.15 140 396.15 L 386.15 396.15 Q 394.49 396.15 400.32 402.06 Q 406.15 407.97 406.15 416.41 Q 406.15 424.85 400.32 430.88 Q 394.49 436.92 386.15 436.92 L 283.08 436.92 L 283.08 740 Q 283.08 748.33 277.17 754.17 Q 271.26 760 262.82 760 Q 254.38 760 248.73 754.17 Q 243.08 748.33 243.08 740 L 243.08 436.92 Z", + "revanced_swipe_overlay_timeout" to "M 390.77 90.77 Q 384.23 90.77 379.81 86.28 Q 375.38 81.8 375.38 75.17 Q 375.38 68.54 379.81 64.27 Q 384.23 60 390.77 60 L 569.23 60 Q 575.77 60 580.19 64.49 Q 584.62 68.97 584.62 75.6 Q 584.62 82.23 580.19 86.5 Q 575.77 90.77 569.23 90.77 L 390.77 90.77 Z M 480.22 538.54 Q 486.85 538.54 491.12 534.12 Q 495.38 529.69 495.38 523.15 L 495.38 349.31 Q 495.38 342.77 490.9 338.35 Q 486.41 333.92 479.78 333.92 Q 473.15 333.92 468.88 338.35 Q 464.62 342.77 464.62 349.31 L 464.62 523.15 Q 464.62 529.69 469.1 534.12 Q 473.59 538.54 480.22 538.54 Z M 480 839.77 Q 414.05 839.77 355.68 814.35 Q 297.31 788.92 253.69 745.69 Q 210.08 702.46 185.04 643.7 Q 160 584.95 160 519.38 Q 160 453.82 185.04 395.45 Q 210.08 337.08 253.69 293.46 Q 297.31 249.85 355.68 224.81 Q 414.05 199.77 480 199.77 Q 540.85 199.77 596 221.5 Q 651.15 243.23 694.85 282.46 L 724.85 251.69 Q 729.23 247.31 735.46 246.92 Q 741.69 246.54 746.85 251.69 Q 752 256.85 752 262.69 Q 752 268.54 746.85 273.69 L 716.08 304.46 Q 753.62 344.46 776.81 398.77 Q 800 453.08 800 519.77 Q 800 584.95 774.96 643.7 Q 749.92 702.46 706.31 745.69 Q 662.69 788.92 604.32 814.35 Q 545.95 839.77 480 839.77 Z M 479.89 809 Q 600.38 809 684.81 724.69 Q 769.23 640.37 769.23 519.88 Q 769.23 399.38 684.92 314.96 Q 600.6 230.54 480.11 230.54 Q 359.62 230.54 275.19 314.85 Q 190.77 399.17 190.77 519.66 Q 190.77 640.15 275.08 724.58 Q 359.4 809 479.89 809 Z M 480 520 Z", + "revanced_switch_create_with_notifications_button" to "M 711.151 709.31 C 732.88 681.759 748.56 654.391 762.089 616.613 C 775.674 578.86 782.368 539.017 782.368 496.408 C 782.368 453.799 775.675 414.021 762.089 376.397 C 748.561 338.749 732.883 311.578 711.151 284.333 L 711.151 367.652 L 681.298 367.652 L 681.298 237.276 L 811.674 237.276 L 811.674 267.128 L 735.523 267.128 C 759.053 297.544 776.706 329.215 790.905 369.257 C 805.051 409.33 812.22 452.118 812.22 496.954 C 812.22 541.79 805.048 584.581 790.905 624.655 C 776.706 664.694 759.053 696.365 735.523 726.78 L 811.674 726.78 L 811.674 756.633 L 681.298 756.633 L 681.298 626.257 L 711.151 626.257 L 711.151 709.31 Z M 186.009 387.238 C 175.304 386.138 165.331 381.726 158.099 375.062 C 151.031 367.907 147.35 358.463 147.819 349.288 L 167.944 119.272 C 169.098 110.129 174.536 101.506 182.694 95.721 C 191.004 90.395 201.474 87.912 212.188 88.696 L 498.283 113.726 C 508.988 114.826 518.961 119.242 526.196 125.902 C 533.261 133.057 536.943 142.502 536.475 151.676 L 528.527 242.515 L 611.361 187.087 L 596.297 359.269 L 524.347 290.298 L 516.35 381.692 C 515.196 390.834 509.755 399.458 501.599 405.242 C 493.286 410.57 482.817 413.052 472.105 412.268 L 186.009 387.238 Z M 180.977 356.489 L 181.023 356.538 C 182.852 358.497 184.789 359.365 188.368 359.532 L 474.518 384.567 C 478.196 385.003 480.033 384.733 482.034 383.155 L 482.077 383.12 L 482.132 383.09 C 484.329 381.845 484.932 381.225 484.796 379.126 L 504.923 148.952 C 505.35 146.619 505.26 145.9 503.388 144.532 L 503.343 144.5 L 503.292 144.446 C 501.454 142.486 499.499 141.595 495.928 141.432 L 209.774 116.397 C 206.081 115.963 204.269 116.195 202.271 117.802 L 202.225 117.839 L 202.167 117.872 C 199.967 119.117 199.36 119.739 199.497 121.839 L 179.369 352.011 C 178.929 354.289 179.094 355.056 180.937 356.459 L 180.977 356.489 Z M 222.281 625.758 C 219.914 596.441 227.116 568.395 243.336 543.983 C 259.892 519.723 279.976 505.416 305.355 497.109 L 306.108 496.863 L 305.725 492.458 C 305.362 485.183 307.959 477.277 312.369 471.579 C 317.17 466.251 324.349 462.247 331.693 461.605 C 339.037 460.962 347.041 463.845 352.716 468.248 C 358.096 473.104 361.832 480.403 362.734 487.624 L 363.122 492.059 L 363.911 492.169 C 390.329 495.826 412.616 506.39 433.14 527.41 C 453.35 548.638 465.287 575.002 468.047 604.284 L 482.872 773.738 L 517.644 770.696 L 520.313 801.203 L 205.005 828.789 L 202.336 798.282 L 237.109 795.24 L 222.281 625.758 Z M 367.51 871.301 C 356.555 872.259 345.383 868.378 336.881 861.544 C 328.702 854.291 323.406 844.043 322.187 833.214 L 321.548 826.229 L 405.095 818.92 L 405.429 822.922 L 405.685 825.983 C 406.369 836.938 402.763 848.064 395.905 856.604 C 388.666 864.769 378.456 870.343 367.51 871.301 Z M 267.613 792.571 L 452.367 776.407 L 437.542 606.955 C 435.521 580.822 425.033 560.619 404.817 544.002 C 384.958 526.988 363.22 520.369 337.082 522.656 C 310.944 524.943 290.737 535.169 274.134 555.379 C 257.117 575.251 250.237 596.993 252.785 623.079 L 267.613 792.571 Z", + "sb_create_new_segment" to "revanced_preference_screen_sb", + "sb_voting_button" to "M 154.549 522.836 C 141.802 522.76 130.526 518.188 121.587 509.249 C 112.648 500.31 108.076 489.034 108 476.288 L 108 275.82 C 108.018 272.336 108.684 268.793 109.932 265.561 C 111.233 262.309 113.607 258.829 116.76 255.527 L 265.165 107.122 L 266.577 108.636 L 277.46 120.297 C 279.338 122.448 281.014 124.528 282.445 126.484 C 283.964 128.61 284.859 130.909 284.935 132.787 L 284.935 138.502 L 256.798 261.445 L 415.652 261.445 C 429.902 261.515 442.178 266.323 451.606 275.751 C 461.035 285.18 465.843 297.458 465.913 311.705 L 465.913 319.564 C 465.909 323.12 465.613 326.217 465.055 328.622 C 464.496 330.97 463.687 333.457 462.714 335.834 L 390.388 502.885 C 387.809 508.903 383.763 513.903 378.598 517.459 C 373.355 520.95 366.752 522.8 359.348 522.836 Z M 362.98 494.087 L 437.164 321.379 L 437.164 307.993 C 437.232 302.584 435.671 298.552 432.187 295.183 C 428.817 291.698 424.775 290.126 419.364 290.194 L 221.183 290.194 L 248.351 164.585 L 136.749 276.953 L 136.749 476.288 C 136.683 481.703 138.242 485.735 141.725 489.098 C 145.09 492.582 149.134 494.153 154.549 494.087 Z M 693.423 851.364 L 682.539 839.702 C 680.643 837.526 679.027 835.383 677.78 833.395 C 676.49 831.279 675.74 829.041 675.684 827.212 L 675.684 821.502 L 703.214 698.555 L 544.349 698.555 C 530.097 698.485 517.82 693.676 508.393 684.249 C 498.966 674.822 494.157 662.544 494.087 648.294 L 494.087 640.436 C 494.091 636.88 494.387 633.783 494.945 631.377 C 495.503 629.029 496.313 626.546 497.287 624.168 L 569.608 456.505 C 572.092 450.522 576.136 445.614 581.374 442.232 C 586.659 438.938 593.264 437.196 600.652 437.164 L 805.451 437.164 C 818.198 437.24 829.474 441.812 838.413 450.751 C 847.352 459.69 851.924 470.966 852 483.712 L 852 684.18 C 851.982 687.665 851.316 691.206 850.067 694.439 C 848.767 697.69 846.393 701.171 843.24 704.473 L 694.835 852.878 Z M 597.018 465.913 L 522.836 638.002 L 522.836 652.007 C 522.768 657.416 524.329 661.448 527.813 664.817 C 531.183 668.302 535.225 669.874 540.636 669.806 L 739.451 669.806 L 711.693 794.779 L 823.251 683.042 L 823.251 483.711 C 823.319 478.302 821.758 474.27 818.273 470.901 C 814.904 467.416 810.862 465.844 805.451 465.913 Z", +) + +private val intentKey = setOf( + "revanced_extended_settings_key", +) + +val intentIcon = intentKey.associateWith { "${it}_icon" } + +private val emptyTitles = setOf( + "external_downloader", + "revanced_custom_playback_speeds", + "revanced_custom_playback_speed_menu_type", + "revanced_default_video_quality_mobile", + "revanced_disable_like_dislike_glow", + "revanced_disable_default_playback_speed_live", + "revanced_enable_custom_playback_speed", + "revanced_enable_debug_buffer_logging", + "revanced_gms_show_dialog", + "revanced_hide_shorts_comments_disabled_button", + "revanced_hide_player_flyout_menu_captions_footer", + "revanced_hide_player_flyout_menu_quality_footer", + "revanced_overlay_button_play_all_type", + "revanced_remember_playback_speed_last_selected", + "revanced_remember_playback_speed_last_selected_toast", + "revanced_remember_video_quality_last_selected", + "revanced_remember_video_quality_last_selected_toast", + "revanced_restore_old_video_quality_menu", + "revanced_sb_guidelines_preference", + "revanced_whitelist_settings", + "sb_create_new_segment_step", +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt new file mode 100644 index 000000000..cf3729409 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt @@ -0,0 +1,91 @@ +package app.revanced.patches.youtube.misc.backgroundplayback + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.MISC_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.REMOVE_BACKGROUND_PLAYBACK_RESTRICTIONS +import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.addInstructionsAtControlFlowLabel +import app.revanced.util.findInstructionIndicesReversedOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.originalMethodOrThrow +import app.revanced.util.getReference +import app.revanced.util.returnEarly +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$MISC_PATH/BackgroundPlaybackPatch;" + +@Suppress("unused") +val backgroundPlaybackPatch = bytecodePatch( + REMOVE_BACKGROUND_PLAYBACK_RESTRICTIONS.title, + REMOVE_BACKGROUND_PLAYBACK_RESTRICTIONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + playerTypeHookPatch, + settingsPatch, + ) + + execute { + + arrayOf( + backgroundPlaybackManagerFingerprint to "isBackgroundPlaybackAllowed", + backgroundPlaybackManagerShortsFingerprint to "isBackgroundShortsPlaybackAllowed", + ).forEach { (fingerprint, integrationsMethod) -> + fingerprint.methodOrThrow().apply { + findInstructionIndicesReversedOrThrow(Opcode.RETURN).forEach { index -> + val register = getInstruction(index).registerA + + addInstructionsAtControlFlowLabel( + index, + """ + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->$integrationsMethod(Z)Z + move-result v$register + """, + ) + } + } + } + + // Enable background playback option in YouTube settings + backgroundPlaybackSettingsFingerprint.originalMethodOrThrow().apply { + val booleanCalls = instructions.withIndex().filter { + it.value.getReference()?.returnType == "Z" + } + + val settingsBooleanIndex = booleanCalls.elementAt(1).index + val settingsBooleanMethod by navigate(this).to(settingsBooleanIndex) + + settingsBooleanMethod.returnEarly(true) + } + + // Force allowing background play for Shorts. + shortsBackgroundPlaybackFeatureFlagFingerprint.methodOrThrow().returnEarly(true) + + // Force allowing background play for videos labeled for kids. + kidsBackgroundPlaybackPolicyControllerFingerprint.methodOrThrow( + kidsBackgroundPlaybackPolicyControllerParentFingerprint + ).returnEarly() + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: SHORTS", + "SETTINGS: DISABLE_SHORTS_BACKGROUND_PLAYBACK" + ), + REMOVE_BACKGROUND_PLAYBACK_RESTRICTIONS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/Fingerprints.kt new file mode 100644 index 000000000..7fd5299c8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/Fingerprints.kt @@ -0,0 +1,73 @@ +package app.revanced.patches.youtube.misc.backgroundplayback + +import app.revanced.patches.youtube.utils.PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.resourceid.backgroundCategory +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +internal val backgroundPlaybackManagerFingerprint = legacyFingerprint( + name = "backgroundPlaybackManagerFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("L"), + opcodes = listOf(Opcode.AND_INT_LIT16), + literals = listOf(64657230L), +) + +internal val backgroundPlaybackSettingsFingerprint = legacyFingerprint( + name = "backgroundPlaybackSettingsFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ, + Opcode.IF_NEZ, + Opcode.GOTO + ), + literals = listOf(backgroundCategory), +) + +internal val kidsBackgroundPlaybackPolicyControllerFingerprint = legacyFingerprint( + name = "kidsBackgroundPlaybackPolicyControllerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("I", "L", "L"), + literals = listOf(5L), +) + +internal val kidsBackgroundPlaybackPolicyControllerParentFingerprint = legacyFingerprint( + name = "kidsBackgroundPlaybackPolicyControllerParentFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf(PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR), + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + opcode == Opcode.SGET_OBJECT + && getReference()?.name == "miniplayerRenderer" + } >= 0 + } +) + +internal val backgroundPlaybackManagerShortsFingerprint = legacyFingerprint( + name = "backgroundPlaybackManagerShortsFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + returnType = "Z", + parameters = listOf("L"), + literals = listOf(151635310L), +) + +internal val shortsBackgroundPlaybackFeatureFlagFingerprint = legacyFingerprint( + name = "shortsBackgroundPlaybackFeatureFlagFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "Z", + parameters = emptyList(), + literals = listOf(45415425L), +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/codecs/OpusCodecPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/codecs/OpusCodecPatch.kt new file mode 100644 index 000000000..341dcea9a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/codecs/OpusCodecPatch.kt @@ -0,0 +1,40 @@ +package app.revanced.patches.youtube.misc.codecs + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.opus.baseOpusCodecsPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.MISC_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.ENABLE_OPUS_CODEC +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch + +@Suppress("unused") +val opusCodecPatch = bytecodePatch( + ENABLE_OPUS_CODEC.title, + ENABLE_OPUS_CODEC.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseOpusCodecsPatch( + "$MISC_PATH/OpusCodecPatch;->enableOpusCodec()Z" + ), + settingsPatch, + ) + + execute { + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_CATEGORY: MISC_EXPERIMENTAL_FLAGS", + "SETTINGS: ENABLE_OPUS_CODEC" + ), + ENABLE_OPUS_CODEC + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/DebuggingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/DebuggingPatch.kt new file mode 100644 index 000000000..0cdede5f7 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/DebuggingPatch.kt @@ -0,0 +1,32 @@ +package app.revanced.patches.youtube.misc.debugging + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.ENABLE_DEBUG_LOGGING +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch + +@Suppress("unused") +val debuggingPatch = bytecodePatch( + ENABLE_DEBUG_LOGGING.title, + ENABLE_DEBUG_LOGGING.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + + // region add settings + + addPreference( + arrayOf( + "SETTINGS: ENABLE_DEBUG_LOGGING" + ), + ENABLE_DEBUG_LOGGING + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/externalbrowser/OpenLinksExternallyPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/externalbrowser/OpenLinksExternallyPatch.kt new file mode 100644 index 000000000..020594c8a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/externalbrowser/OpenLinksExternallyPatch.kt @@ -0,0 +1,62 @@ +package app.revanced.patches.youtube.misc.externalbrowser + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.transformation.transformInstructionsPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.MISC_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.ENABLE_EXTERNAL_BROWSER +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.StringReference + +@Suppress("unused") +val openLinksExternallyPatch = bytecodePatch( + ENABLE_EXTERNAL_BROWSER.title, + ENABLE_EXTERNAL_BROWSER.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + transformInstructionsPatch( + filterMap = filterMap@{ _, _, instruction, instructionIndex -> + if (instruction !is ReferenceInstruction) return@filterMap null + val reference = instruction.reference as? StringReference ?: return@filterMap null + + if (reference.string != "android.support.customtabs.action.CustomTabsService") return@filterMap null + + return@filterMap instructionIndex to (instruction as OneRegisterInstruction).registerA + }, + transform = { mutableMethod, entry -> + val (intentStringIndex, register) = entry + + // Hook the intent string. + mutableMethod.addInstructions( + intentStringIndex + 1, + """ + invoke-static {v$register}, $MISC_PATH/ExternalBrowserPatch;->enableExternalBrowser(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$register + """, + ) + }, + ), + settingsPatch, + ) + + execute { + + // region add settings + + addPreference( + arrayOf( + "SETTINGS: ENABLE_EXTERNAL_BROWSER" + ), + ENABLE_EXTERNAL_BROWSER + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/openlinksdirectly/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/openlinksdirectly/Fingerprints.kt new file mode 100644 index 000000000..1b33eeab6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/openlinksdirectly/Fingerprints.kt @@ -0,0 +1,47 @@ +package app.revanced.patches.youtube.misc.openlinksdirectly + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +internal val openLinksDirectlyFingerprintPrimary = legacyFingerprint( + name = "openLinksDirectlyFingerprintPrimary", + returnType = "Ljava/lang/Object", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Ljava/lang/Object"), + opcodes = listOf( + Opcode.CHECK_CAST, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.RETURN_OBJECT + ), + customFingerprint = { method, _ -> + method.name == "a" && + method.implementation + ?.instructions + ?.withIndex() + ?.filter { (_, instruction) -> + val reference = (instruction as? ReferenceInstruction)?.reference + reference is FieldReference && + instruction.opcode == Opcode.SGET_OBJECT && + reference.name == "webviewEndpoint" + } + ?.map { (index, _) -> index } + ?.size == 1 + } +) + +internal val openLinksDirectlyFingerprintSecondary = legacyFingerprint( + name = "openLinksDirectlyFingerprintSecondary", + returnType = "Landroid/net/Uri", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("Ljava/lang/String"), + opcodes = listOf( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT + ), + strings = listOf("://") +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/openlinksdirectly/OpenLinksDirectlyPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/openlinksdirectly/OpenLinksDirectlyPatch.kt new file mode 100644 index 000000000..61ffa6c1d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/openlinksdirectly/OpenLinksDirectlyPatch.kt @@ -0,0 +1,60 @@ +package app.revanced.patches.youtube.misc.openlinksdirectly + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.MISC_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.ENABLE_OPEN_LINKS_DIRECTLY +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +@Suppress("unused") +val openLinksDirectlyPatch = bytecodePatch( + ENABLE_OPEN_LINKS_DIRECTLY.title, + ENABLE_OPEN_LINKS_DIRECTLY.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + + arrayOf( + openLinksDirectlyFingerprintPrimary, + openLinksDirectlyFingerprintSecondary + ).forEach { fingerprint -> + fingerprint.methodOrThrow().apply { + val insertIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_STATIC && + getReference()?.name == "parse" + } + val insertRegister = + getInstruction(insertIndex).registerC + + replaceInstruction( + insertIndex, + "invoke-static {v$insertRegister}, $MISC_PATH/OpenLinksDirectlyPatch;->enableBypassRedirect(Ljava/lang/String;)Landroid/net/Uri;" + ) + } + } + + // region add settings + + addPreference( + arrayOf( + "SETTINGS: ENABLE_OPEN_LINKS_DIRECTLY" + ), + ENABLE_OPEN_LINKS_DIRECTLY + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/quic/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/quic/Fingerprints.kt new file mode 100644 index 000000000..807b46700 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/quic/Fingerprints.kt @@ -0,0 +1,28 @@ +@file:Suppress("SpellCheckingInspection") + +package app.revanced.patches.youtube.misc.quic + +import app.revanced.util.fingerprint.legacyFingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val cronetEngineBuilderFingerprint = legacyFingerprint( + name = "cronetEngineBuilderFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC.value, + parameters = listOf("Z"), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/CronetEngine\$Builder;") && + method.name == "enableQuic" + } +) + +internal val experimentalCronetEngineBuilderFingerprint = legacyFingerprint( + name = "experimentalCronetEngineBuilderFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC.value, + parameters = listOf("Z"), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/ExperimentalCronetEngine\$Builder;") && + method.name == "enableQuic" + } +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/quic/QUICProtocolPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/quic/QUICProtocolPatch.kt new file mode 100644 index 000000000..5c0ba3aaa --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/quic/QUICProtocolPatch.kt @@ -0,0 +1,47 @@ +package app.revanced.patches.youtube.misc.quic + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.MISC_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.DISABLE_QUIC_PROTOCOL +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.methodOrThrow + +@Suppress("unused") +val quicProtocolPatch = bytecodePatch( + DISABLE_QUIC_PROTOCOL.title, + DISABLE_QUIC_PROTOCOL.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + + arrayOf( + cronetEngineBuilderFingerprint, + experimentalCronetEngineBuilderFingerprint + ).forEach { + it.methodOrThrow().addInstructions( + 0, """ + invoke-static {p1}, $MISC_PATH/QUICProtocolPatch;->disableQUICProtocol(Z)Z + move-result p1 + """ + ) + } + + // region add settings + + addPreference( + arrayOf( + "SETTINGS: DISABLE_QUIC_PROTOCOL" + ), + DISABLE_QUIC_PROTOCOL + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/share/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/share/Fingerprints.kt new file mode 100644 index 000000000..a01217b02 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/share/Fingerprints.kt @@ -0,0 +1,38 @@ +package app.revanced.patches.youtube.misc.share + +import app.revanced.patches.youtube.utils.resourceid.bottomSheetRecyclerView +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +internal val bottomSheetRecyclerViewFingerprint = legacyFingerprint( + name = "bottomSheetRecyclerViewFingerprint", + returnType = "Lj${'$'}/util/Optional;", + accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(bottomSheetRecyclerView), +) + +internal val updateShareSheetCommandFingerprint = legacyFingerprint( + name = "updateShareSheetCommandFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "Ljava/util/Map;"), + opcodes = listOf( + Opcode.IF_EQZ, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + Opcode.IGET_OBJECT + ), + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + opcode == Opcode.SGET_OBJECT && + getReference()?.name == "updateShareSheetCommand" + } >= 0 + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/share/ShareSheetPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/share/ShareSheetPatch.kt new file mode 100644 index 000000000..201edf064 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/share/ShareSheetPatch.kt @@ -0,0 +1,88 @@ +package app.revanced.patches.youtube.misc.share + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.youtube.utils.extension.Constants.MISC_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.CHANGE_SHARE_SHEET +import app.revanced.patches.youtube.utils.resourceid.bottomSheetRecyclerView +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$MISC_PATH/ShareSheetPatch;" + +private const val FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/ShareSheetMenuFilter;" + +@Suppress("unused") +val shareSheetPatch = bytecodePatch( + CHANGE_SHARE_SHEET.title, + CHANGE_SHARE_SHEET.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + lithoFilterPatch, + sharedResourceIdPatch, + ) + + execute { + + // Detects that the Share sheet panel has been invoked. + bottomSheetRecyclerViewFingerprint.methodOrThrow().apply { + val constIndex = indexOfFirstLiteralInstructionOrThrow(bottomSheetRecyclerView) + val targetIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.CHECK_CAST) + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $EXTENSION_CLASS_DESCRIPTOR->onShareSheetMenuCreate(Landroid/support/v7/widget/RecyclerView;)V" + ) + } + + // Remove the app list from the Share sheet panel on YouTube. + updateShareSheetCommandFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.endIndex + val targetRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + invoke-static {v$targetRegister}, $EXTENSION_CLASS_DESCRIPTOR->overridePackageName(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$targetRegister + """ + ) + } + } + + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_CATEGORY: MISC_EXPERIMENTAL_FLAGS", + "SETTINGS: CHANGE_SHARE_SHEET" + ), + CHANGE_SHARE_SHEET + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/tracking/SanitizeUrlQueryPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/tracking/SanitizeUrlQueryPatch.kt new file mode 100644 index 000000000..6e8a824d1 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/tracking/SanitizeUrlQueryPatch.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.youtube.misc.tracking + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.tracking.baseSanitizeUrlQueryPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.SANITIZE_SHARING_LINKS +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch + +@Suppress("unused") +val sanitizeUrlQueryPatch = bytecodePatch( + SANITIZE_SHARING_LINKS.title, + SANITIZE_SHARING_LINKS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseSanitizeUrlQueryPatch, + settingsPatch, + ) + + execute { + + // region add settings + + addPreference( + arrayOf( + "SETTINGS: SANITIZE_SHARING_LINKS" + ), + SANITIZE_SHARING_LINKS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/watchhistory/WatchHistoryPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/watchhistory/WatchHistoryPatch.kt new file mode 100644 index 000000000..5916130c9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/watchhistory/WatchHistoryPatch.kt @@ -0,0 +1,40 @@ +package app.revanced.patches.youtube.misc.watchhistory + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.MISC_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.WATCH_HISTORY +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.patches.youtube.utils.trackingurlhook.hookTrackingUrl +import app.revanced.patches.youtube.utils.trackingurlhook.trackingUrlHookPatch + +@Suppress("unused") +val watchHistoryPatch = bytecodePatch( + WATCH_HISTORY.title, + WATCH_HISTORY.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + trackingUrlHookPatch, + ) + + execute { + + hookTrackingUrl("$MISC_PATH/WatchHistoryPatch;->replaceTrackingUrl(Landroid/net/Uri;)Landroid/net/Uri;") + + // region add settings + + addPreference( + arrayOf( + "SETTINGS: WATCH_HISTORY" + ), + WATCH_HISTORY + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/action/ActionButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/action/ActionButtonsPatch.kt new file mode 100644 index 000000000..91f46f888 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/action/ActionButtonsPatch.kt @@ -0,0 +1,43 @@ +package app.revanced.patches.youtube.player.action + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.HIDE_ACTION_BUTTONS +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch + +private const val FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/ActionButtonsFilter;" + +@Suppress("unused") +val actionButtonsPatch = bytecodePatch( + HIDE_ACTION_BUTTONS.title, + HIDE_ACTION_BUTTONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + lithoFilterPatch, + ) + + execute { + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: PLAYER", + "SETTINGS: HIDE_ACTION_BUTTONS" + ), + HIDE_ACTION_BUTTONS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/ambientmode/AmbientModeSwitchPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/ambientmode/AmbientModeSwitchPatch.kt new file mode 100644 index 000000000..67f11cd14 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/ambientmode/AmbientModeSwitchPatch.kt @@ -0,0 +1,133 @@ +package app.revanced.patches.youtube.player.ambientmode + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.patch.PatchList.AMBIENT_MODE_CONTROL +import app.revanced.patches.youtube.utils.playservice.is_19_34_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_41_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstStringInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +@Suppress("unused") +val ambientModeSwitchPatch = bytecodePatch( + AMBIENT_MODE_CONTROL.title, + AMBIENT_MODE_CONTROL.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + versionCheckPatch, + ) + + execute { + // region patch for bypass ambient mode restrictions + + var syntheticClassList = emptyArray() + + mapOf( + powerSaveModeBroadcastReceiverFingerprint to false, + powerSaveModeSyntheticFingerprint to true + ).forEach { (fingerprint, reversed) -> + fingerprint.methodOrThrow().apply { + val stringIndex = + indexOfFirstStringInstructionOrThrow("android.os.action.POWER_SAVE_MODE_CHANGED") + val targetIndex = + if (reversed) + indexOfFirstInstructionReversedOrThrow(stringIndex, Opcode.INVOKE_DIRECT) + else + indexOfFirstInstructionOrThrow(stringIndex, Opcode.INVOKE_DIRECT) + val targetClass = + (getInstruction(targetIndex).reference as MethodReference).definingClass + + syntheticClassList += targetClass + } + } + + syntheticClassList.distinct().forEach { className -> + findMethodOrThrow(className) { + name == "accept" + }.apply { + implementation!!.instructions + .withIndex() + .filter { (_, instruction) -> + val reference = (instruction as? ReferenceInstruction)?.reference + instruction.opcode == Opcode.INVOKE_VIRTUAL && + reference is MethodReference && + reference.name == "isPowerSaveMode" + } + .map { (index, _) -> index } + .reversed() + .forEach { index -> + val register = getInstruction(index + 1).registerA + + addInstructions( + index + 2, """ + invoke-static {v$register}, $PLAYER_CLASS_DESCRIPTOR->bypassAmbientModeRestrictions(Z)Z + move-result v$register + """ + ) + } + } + } + + // endregion + + // region patch for disable ambient mode in fullscreen + + if (!is_19_41_or_greater) { + ambientModeInFullscreenFingerprint.injectLiteralInstructionBooleanCall( + AMBIENT_MODE_IN_FULLSCREEN_FEATURE_FLAG, + "$PLAYER_CLASS_DESCRIPTOR->disableAmbientModeInFullscreen()Z" + ) + } + + if (is_19_34_or_greater) { + setFullScreenBackgroundColorFingerprint.methodOrThrow().apply { + val insertIndex = indexOfFirstInstructionReversedOrThrow { + getReference()?.name == "setBackgroundColor" + } + val register = getInstruction(insertIndex).registerD + + addInstructions( + insertIndex, + """ + invoke-static { v$register }, $PLAYER_CLASS_DESCRIPTOR->getFullScreenBackgroundColor(I)I + move-result v$register + """, + ) + } + } + + // endregion + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: PLAYER", + "SETTINGS: AMBIENT_MODE_CONTROLS" + ), + AMBIENT_MODE_CONTROL + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/ambientmode/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/ambientmode/Fingerprints.kt new file mode 100644 index 000000000..2ca53ebf6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/ambientmode/Fingerprints.kt @@ -0,0 +1,46 @@ +package app.revanced.patches.youtube.player.ambientmode + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal const val AMBIENT_MODE_IN_FULLSCREEN_FEATURE_FLAG = 45389368L + +internal val ambientModeInFullscreenFingerprint = legacyFingerprint( + name = "ambientModeInFullscreenFingerprint", + returnType = "V", + literals = listOf(AMBIENT_MODE_IN_FULLSCREEN_FEATURE_FLAG), +) + +internal val powerSaveModeBroadcastReceiverFingerprint = legacyFingerprint( + name = "powerSaveModeBroadcastReceiverFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/content/Context;", "Landroid/content/Intent;"), + strings = listOf("android.os.action.POWER_SAVE_MODE_CHANGED"), + // There are two classes that inherit [BroadcastReceiver]. + // Check the method count to find the correct class. + customFingerprint = { _, classDef -> + classDef.superclass == "Landroid/content/BroadcastReceiver;" && + classDef.methods.count() == 2 + } +) + +internal val powerSaveModeSyntheticFingerprint = legacyFingerprint( + name = "powerSaveModeSyntheticFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Ljava/lang/Object;"), + strings = listOf("android.os.action.POWER_SAVE_MODE_CHANGED") +) + +internal val setFullScreenBackgroundColorFingerprint = legacyFingerprint( + name = "setFullScreenBackgroundColorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL, + parameters = listOf("Z", "I", "I", "I", "I"), + customFingerprint = { method, classDef -> + classDef.type.endsWith("/YouTubePlayerViewNotForReflection;") + && method.name == "onLayout" + }, +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/buttons/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/buttons/Fingerprints.kt new file mode 100644 index 000000000..5a73bf4cd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/buttons/Fingerprints.kt @@ -0,0 +1,78 @@ +package app.revanced.patches.youtube.player.buttons + +import app.revanced.patches.youtube.utils.resourceid.cfFullscreenButton +import app.revanced.patches.youtube.utils.resourceid.fadeDurationFast +import app.revanced.patches.youtube.utils.resourceid.fullScreenButton +import app.revanced.patches.youtube.utils.resourceid.musicAppDeeplinkButtonView +import app.revanced.patches.youtube.utils.resourceid.playerCollapseButton +import app.revanced.patches.youtube.utils.resourceid.titleAnchor +import app.revanced.patches.youtube.utils.resourceid.youTubeControlsOverlaySubtitleButton +import app.revanced.util.containsLiteralInstruction +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val fullScreenButtonFingerprint = legacyFingerprint( + name = "fullScreenButtonFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/View;"), + customFingerprint = handler@{ method, _ -> + if (!method.containsLiteralInstruction(fullScreenButton)) + return@handler false + + method.containsLiteralInstruction(fadeDurationFast) // YouTube 18.29.38 ~ YouTube 19.18.41 + || method.containsLiteralInstruction(cfFullscreenButton) // YouTube 19.19.39 ~ + }, +) + +/** + * Added in YouTube v18.31.40 + * + * When this value is TRUE, litho subtitle button is used. + * In this case, the empty area remains, so set this value to FALSE. + */ +internal val lithoSubtitleButtonConfigFingerprint = legacyFingerprint( + name = "lithoSubtitleButtonConfigFingerprint", + returnType = "Z", + literals = listOf(45421555L), +) + +internal val musicAppDeeplinkButtonFingerprint = legacyFingerprint( + name = "musicAppDeeplinkButtonFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Z", "Z") +) + +internal val musicAppDeeplinkButtonParentFingerprint = legacyFingerprint( + name = "musicAppDeeplinkButtonParentFingerprint", + returnType = "V", + literals = listOf(musicAppDeeplinkButtonView), +) + +internal val playerControlsVisibilityModelFingerprint = legacyFingerprint( + name = "playerControlsVisibilityModelFingerprint", + opcodes = listOf(Opcode.INVOKE_DIRECT_RANGE), + strings = listOf("Missing required properties:", "hasNext", "hasPrevious") +) + +internal val titleAnchorFingerprint = legacyFingerprint( + name = "titleAnchorFingerprint", + returnType = "V", + literals = listOf(playerCollapseButton, titleAnchor), +) + +/** + * The parameters of the method have changed in YouTube v18.31.40. + * Therefore, this fingerprint does not check the method's parameters. + * + * This fingerprint is compatible from YouTube v18.25.40 to YouTube v18.45.43 + */ +internal val youtubeControlsOverlaySubtitleButtonFingerprint = legacyFingerprint( + name = "youtubeControlsOverlaySubtitleButtonFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + literals = listOf(youTubeControlsOverlaySubtitleButton), +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/buttons/PlayerButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/buttons/PlayerButtonsPatch.kt new file mode 100644 index 000000000..979950f6c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/buttons/PlayerButtonsPatch.kt @@ -0,0 +1,239 @@ +package app.revanced.patches.youtube.player.buttons + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.youtube.utils.castbutton.castButtonPatch +import app.revanced.patches.youtube.utils.castbutton.hookPlayerCastButton +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.fix.bottomui.cfBottomUIPatch +import app.revanced.patches.youtube.utils.layoutConstructorFingerprint +import app.revanced.patches.youtube.utils.patch.PatchList.HIDE_PLAYER_BUTTONS +import app.revanced.patches.youtube.utils.playservice.is_18_31_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_34_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.resourceid.autoNavToggle +import app.revanced.patches.youtube.utils.resourceid.fullScreenButton +import app.revanced.patches.youtube.utils.resourceid.playerCollapseButton +import app.revanced.patches.youtube.utils.resourceid.playerControlPreviousButtonTouchArea +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.resourceid.titleAnchor +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction +import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val HAS_NEXT = 5 +private const val HAS_PREVIOUS = 6 + +@Suppress("unused") +val playerButtonsPatch = bytecodePatch( + HIDE_PLAYER_BUTTONS.title, + HIDE_PLAYER_BUTTONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + castButtonPatch, + cfBottomUIPatch, + sharedResourceIdPatch, + settingsPatch, + versionCheckPatch, + ) + + execute { + + // region patch for hide autoplay button + + layoutConstructorFingerprint.methodOrThrow().apply { + val constIndex = indexOfFirstLiteralInstructionOrThrow(autoNavToggle) + val constRegister = getInstruction(constIndex).registerA + val jumpIndex = + indexOfFirstInstructionOrThrow(constIndex + 2, Opcode.INVOKE_VIRTUAL) + 1 + + addInstructionsWithLabels( + constIndex, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->hideAutoPlayButton()Z + move-result v$constRegister + if-nez v$constRegister, :hidden + """, ExternalLabel("hidden", getInstruction(jumpIndex)) + ) + } + + // endregion + + // region patch for hide captions button + + if (is_18_31_or_greater) { + lithoSubtitleButtonConfigFingerprint.methodOrThrow().apply { + val insertIndex = implementation!!.instructions.lastIndex + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $PLAYER_CLASS_DESCRIPTOR->hideCaptionsButton(Z)Z + move-result v$insertRegister + """ + ) + } + } + + + youtubeControlsOverlaySubtitleButtonFingerprint.methodOrThrow().apply { + val insertIndex = implementation!!.instructions.lastIndex + val insertRegister = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex, + "invoke-static {v$insertRegister}, $PLAYER_CLASS_DESCRIPTOR->hideCaptionsButton(Landroid/view/View;)V" + ) + } + + // endregion + + // region patch for hide cast button + + hookPlayerCastButton() + + // endregion + + // region patch for hide collapse button + + titleAnchorFingerprint.methodOrThrow().apply { + val titleAnchorConstIndex = indexOfFirstLiteralInstructionOrThrow(titleAnchor) + val titleAnchorIndex = + indexOfFirstInstructionOrThrow(titleAnchorConstIndex, Opcode.MOVE_RESULT_OBJECT) + val titleAnchorRegister = + getInstruction(titleAnchorIndex).registerA + + addInstruction( + titleAnchorIndex + 1, + "invoke-static {v$titleAnchorRegister}, $PLAYER_CLASS_DESCRIPTOR->setTitleAnchorStartMargin(Landroid/view/View;)V" + ) + + val playerCollapseButtonConstIndex = + indexOfFirstLiteralInstructionOrThrow(playerCollapseButton) + val playerCollapseButtonIndex = + indexOfFirstInstructionOrThrow(playerCollapseButtonConstIndex, Opcode.CHECK_CAST) + val playerCollapseButtonRegister = + getInstruction(playerCollapseButtonIndex).registerA + + addInstruction( + playerCollapseButtonIndex + 1, + "invoke-static {v$playerCollapseButtonRegister}, $PLAYER_CLASS_DESCRIPTOR->hideCollapseButton(Landroid/widget/ImageView;)V" + ) + } + + // endregion + + // region patch for hide fullscreen button + + fullScreenButtonFingerprint.matchOrThrow().let { + it.method.apply { + val buttonCalls = implementation!!.instructions.withIndex() + .filter { instruction -> + (instruction.value as? WideLiteralInstruction)?.wideLiteral == fullScreenButton + } + val constIndex = buttonCalls.elementAt(buttonCalls.size - 1).index + val castIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.CHECK_CAST) + val insertIndex = castIndex + 1 + val insertRegister = getInstruction(castIndex).registerA + + addInstructionsWithLabels( + insertIndex, """ + invoke-static {v$insertRegister}, $PLAYER_CLASS_DESCRIPTOR->hideFullscreenButton(Landroid/widget/ImageView;)Landroid/widget/ImageView; + move-result-object v$insertRegister + if-nez v$insertRegister, :show + return-void + """, ExternalLabel("show", getInstruction(insertIndex)) + ) + } + } + + // endregion + + // region patch for hide previous and next button + + if (is_19_34_or_greater) { + layoutConstructorFingerprint.methodOrThrow().apply { + val resourceIndex = + indexOfFirstLiteralInstructionOrThrow(playerControlPreviousButtonTouchArea) + + val insertIndex = indexOfFirstInstructionOrThrow(resourceIndex) { + opcode == Opcode.INVOKE_STATIC && + getReference()?.parameterTypes?.firstOrNull() == "Landroid/view/View;" + } + + val viewRegister = getInstruction(insertIndex).registerC + + addInstruction( + insertIndex, + "invoke-static { v$viewRegister }, $PLAYER_CLASS_DESCRIPTOR" + + "->hidePreviousNextButtons(Landroid/view/View;)V", + ) + } + } else { + playerControlsVisibilityModelFingerprint.methodOrThrow().apply { + val callIndex = indexOfFirstInstructionOrThrow(Opcode.INVOKE_DIRECT_RANGE) + val callInstruction = getInstruction(callIndex) + + val hasNextParameterRegister = callInstruction.startRegister + HAS_NEXT + val hasPreviousParameterRegister = callInstruction.startRegister + HAS_PREVIOUS + + addInstructions( + callIndex, """ + invoke-static { v$hasNextParameterRegister }, $PLAYER_CLASS_DESCRIPTOR->hidePreviousNextButton(Z)Z + move-result v$hasNextParameterRegister + invoke-static { v$hasPreviousParameterRegister }, $PLAYER_CLASS_DESCRIPTOR->hidePreviousNextButton(Z)Z + move-result v$hasPreviousParameterRegister + """ + ) + } + } + + // endregion + + // region patch for hide youtube music button + + musicAppDeeplinkButtonFingerprint.methodOrThrow(musicAppDeeplinkButtonParentFingerprint) + .apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->hideMusicButton()Z + move-result v0 + if-nez v0, :hidden + """, + ExternalLabel("hidden", getInstruction(implementation!!.instructions.lastIndex)) + ) + } + + // endregion + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: PLAYER", + "PREFERENCE_SCREENS: PLAYER_BUTTONS", + "SETTINGS: HIDE_PLAYER_BUTTONS" + ), + HIDE_PLAYER_BUTTONS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/comments/CommentsComponentPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/comments/CommentsComponentPatch.kt new file mode 100644 index 000000000..06497b17b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/comments/CommentsComponentPatch.kt @@ -0,0 +1,99 @@ +package app.revanced.patches.youtube.player.comments + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.shared.spans.addSpanFilter +import app.revanced.patches.shared.spans.inclusiveSpanPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.SPANS_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.HIDE_COMMENTS_COMPONENTS +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private const val COMMENTS_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/CommentsFilter;" +private const val SEARCH_LINKS_FILTER_CLASS_DESCRIPTOR = + "$SPANS_PATH/SearchLinksFilter;" + +@Suppress("unused") +val commentsComponentPatch = bytecodePatch( + HIDE_COMMENTS_COMPONENTS.title, + HIDE_COMMENTS_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + inclusiveSpanPatch, + lithoFilterPatch, + sharedResourceIdPatch, + ) + + execute { + + // region patch for emoji picker button in shorts + + shortsLiveStreamEmojiPickerOpacityFingerprint.methodOrThrow().apply { + val insertIndex = implementation!!.instructions.lastIndex + val insertRegister = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex, + "invoke-static {v$insertRegister}, $PLAYER_CLASS_DESCRIPTOR->changeEmojiPickerOpacity(Landroid/widget/ImageView;)V" + ) + } + + shortsLiveStreamEmojiPickerOnClickListenerFingerprint.methodOrThrow().apply { + val emojiPickerEndpointIndex = + indexOfFirstLiteralInstructionOrThrow(126326492L) + val emojiPickerOnClickListenerIndex = + indexOfFirstInstructionOrThrow(emojiPickerEndpointIndex, Opcode.INVOKE_DIRECT) + val emojiPickerOnClickListenerMethod = + getWalkerMethod(emojiPickerOnClickListenerIndex) + + emojiPickerOnClickListenerMethod.apply { + val insertIndex = indexOfFirstInstructionOrThrow(Opcode.IF_EQZ) + val insertRegister = + getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $PLAYER_CLASS_DESCRIPTOR->disableEmojiPickerOnClickListener(Ljava/lang/Object;)Ljava/lang/Object; + move-result-object v$insertRegister + """ + ) + } + } + + // endregion + + addSpanFilter(SEARCH_LINKS_FILTER_CLASS_DESCRIPTOR) + addLithoFilter(COMMENTS_FILTER_CLASS_DESCRIPTOR) + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: PLAYER", + "SETTINGS: HIDE_COMMENTS_COMPONENTS" + ), + HIDE_COMMENTS_COMPONENTS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/comments/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/comments/Fingerprints.kt new file mode 100644 index 000000000..95bb87ec9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/comments/Fingerprints.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.youtube.player.comments + +import app.revanced.patches.youtube.utils.resourceid.emojiPickerIcon +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val shortsLiveStreamEmojiPickerOnClickListenerFingerprint = legacyFingerprint( + name = "shortsLiveStreamEmojiPickerOnClickListenerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC.value, + parameters = listOf("L"), + literals = listOf(126326492L), +) + +internal val shortsLiveStreamEmojiPickerOpacityFingerprint = legacyFingerprint( + name = "shortsLiveStreamEmojiPickerOpacityFingerprint", + returnType = "Landroid/widget/ImageView;", + accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(emojiPickerIcon), +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/Fingerprints.kt new file mode 100644 index 000000000..33098e261 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/Fingerprints.kt @@ -0,0 +1,281 @@ +@file:Suppress("SpellCheckingInspection") + +package app.revanced.patches.youtube.player.components + +import app.revanced.patches.youtube.utils.resourceid.componentLongClickListener +import app.revanced.patches.youtube.utils.resourceid.darkBackground +import app.revanced.patches.youtube.utils.resourceid.donationCompanion +import app.revanced.patches.youtube.utils.resourceid.easySeekEduContainer +import app.revanced.patches.youtube.utils.resourceid.endScreenElementLayoutCircle +import app.revanced.patches.youtube.utils.resourceid.endScreenElementLayoutIcon +import app.revanced.patches.youtube.utils.resourceid.endScreenElementLayoutVideo +import app.revanced.patches.youtube.utils.resourceid.offlineActionsVideoDeletedUndoSnackbarText +import app.revanced.patches.youtube.utils.resourceid.scrubbing +import app.revanced.patches.youtube.utils.resourceid.seekEasyHorizontalTouchOffsetToStartScrubbing +import app.revanced.patches.youtube.utils.resourceid.suggestedAction +import app.revanced.patches.youtube.utils.resourceid.tapBloomView +import app.revanced.patches.youtube.utils.resourceid.touchArea +import app.revanced.patches.youtube.utils.resourceid.videoZoomSnapIndicator +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val horizontalTouchOffsetConstructorFingerprint = legacyFingerprint( + name = "horizontalTouchOffsetConstructorFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf(seekEasyHorizontalTouchOffsetToStartScrubbing), +) + +internal val nextGenWatchLayoutFingerprint = legacyFingerprint( + name = "nextGenWatchLayoutFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + customFingerprint = handler@{ method, _ -> + if (method.definingClass != "Lcom/google/android/apps/youtube/app/watch/nextgenwatch/ui/NextGenWatchLayout;") + return@handler false + + method.indexOfFirstInstruction { + getReference()?.name == "booleanValue" + } >= 0 + } +) + +/** + * This value restores the 'Slide to seek' behavior. + * Deprecated in YouTube v19.18.41+. + */ +internal val restoreSlideToSeekBehaviorFingerprint = legacyFingerprint( + name = "restoreSlideToSeekBehaviorFingerprint", + returnType = "Z", + parameters = emptyList(), + opcodes = listOf(Opcode.MOVE_RESULT), + literals = listOf(45411329L), +) + +internal val slideToSeekMotionEventFingerprint = legacyFingerprint( + name = "slideToSeekMotionEventFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/View;", "Landroid/view/MotionEvent;"), + opcodes = listOf( + Opcode.SUB_FLOAT_2ADDR, + Opcode.INVOKE_VIRTUAL, // SlideToSeek Boolean method + Opcode.MOVE_RESULT, + Opcode.IF_NEZ, + Opcode.IGET_OBJECT, // insert index + Opcode.INVOKE_VIRTUAL + ) +) + +/** + * This value disables 'Playing at 2x speed' while holding down. + * Deprecated in YouTube v19.18.41+. + */ +internal val speedOverlayFingerprint = legacyFingerprint( + name = "speedOverlayFingerprint", + returnType = "Z", + parameters = emptyList(), + opcodes = listOf(Opcode.MOVE_RESULT), + literals = listOf(45411330L), +) + +/** + * This value is the key for the playback speed overlay value. + * Deprecated in YouTube v19.18.41+. + */ +internal val speedOverlayFloatValueFingerprint = legacyFingerprint( + name = "speedOverlayFloatValueFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + opcodes = listOf(Opcode.DOUBLE_TO_FLOAT), + literals = listOf(45411328L), +) + +internal val speedOverlayTextValueFingerprint = legacyFingerprint( + name = "speedOverlayTextValueFingerprint", + returnType = "V", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf(Opcode.CONST_WIDE_HIGH16), + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + getReference()?.toString() == "Ljava/math/BigDecimal;->signum()I" + } >= 0 + } +) + +internal val crowdfundingBoxFingerprint = legacyFingerprint( + name = "crowdfundingBoxFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IPUT_OBJECT + ), + literals = listOf(donationCompanion), +) + +internal val filmStripOverlayConfigFingerprint = legacyFingerprint( + name = "filmStripOverlayConfigFingerprint", + returnType = "Z", + parameters = emptyList(), + literals = listOf(45381958L), +) + +internal val filmStripOverlayInteractionFingerprint = legacyFingerprint( + name = "filmStripOverlayInteractionFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("L") +) + +internal val filmStripOverlayParentFingerprint = legacyFingerprint( + name = "filmStripOverlayParentFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf(scrubbing), +) + +internal val filmStripOverlayPreviewFingerprint = legacyFingerprint( + name = "filmStripOverlayPreviewFingerprint", + returnType = "Z", + parameters = listOf("F"), + opcodes = listOf( + Opcode.SUB_FLOAT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT + ) +) + +internal val infoCardsIncognitoFingerprint = legacyFingerprint( + name = "infoCardsIncognitoFingerprint", + returnType = "Ljava/lang/Boolean;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "J"), + opcodes = listOf(Opcode.IGET_BOOLEAN), + strings = listOf("vibrator") +) + +internal val layoutCircleFingerprint = legacyFingerprint( + name = "layoutCircleFingerprint", + returnType = "Landroid/view/View;", + opcodes = listOf( + Opcode.CONST, + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + ), + literals = listOf(endScreenElementLayoutCircle), +) + +internal val layoutIconFingerprint = legacyFingerprint( + name = "layoutIconFingerprint", + returnType = "Landroid/view/View;", + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + ), + literals = listOf(endScreenElementLayoutIcon), +) + +internal val layoutVideoFingerprint = legacyFingerprint( + name = "layoutVideoFingerprint", + returnType = "Landroid/view/View;", + opcodes = listOf( + Opcode.CONST, + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + ), + literals = listOf(endScreenElementLayoutVideo), +) + +internal val lithoComponentOnClickListenerFingerprint = legacyFingerprint( + name = "lithoComponentOnClickListenerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PRIVATE or AccessFlags.STATIC, + parameters = listOf("L"), + literals = listOf(componentLongClickListener), +) + +internal val engagementPanelPlaylistSyntheticFingerprint = legacyFingerprint( + name = "engagementPanelPlaylistSyntheticFingerprint", + strings = listOf("engagement-panel-playlist"), + customFingerprint = { _, classDef -> + classDef.interfaces.contains("Landroid/view/View${'$'}OnClickListener;") + } +) + +internal val offlineActionsOnClickListenerFingerprint = legacyFingerprint( + name = "offlineActionsOnClickListenerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Ljava/lang/String;"), + literals = listOf(offlineActionsVideoDeletedUndoSnackbarText), +) + +internal val quickSeekOverlayFingerprint = legacyFingerprint( + name = "quickSeekOverlayFingerprint", + returnType = "V", + parameters = emptyList(), + literals = listOf(darkBackground, tapBloomView), +) + +internal val seekEduContainerFingerprint = legacyFingerprint( + name = "seekEduContainerFingerprint", + returnType = "V", + literals = listOf(easySeekEduContainer), +) + +internal val suggestedActionsFingerprint = legacyFingerprint( + name = "suggestedActionsFingerprint", + returnType = "V", + opcodes = listOf( + Opcode.CONST, + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT + ), + literals = listOf(suggestedAction), +) + +internal val touchAreaOnClickListenerFingerprint = legacyFingerprint( + name = "touchAreaOnClickListenerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf(touchArea), +) + +internal val videoZoomSnapIndicatorFingerprint = legacyFingerprint( + name = "videoZoomSnapIndicatorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(videoZoomSnapIndicator), +) + +internal val watermarkFingerprint = legacyFingerprint( + name = "watermarkFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "L"), + opcodes = listOf( + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, + Opcode.IGET_BOOLEAN + ) +) + +internal val watermarkParentFingerprint = legacyFingerprint( + name = "watermarkParentFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + strings = listOf("player_overlay_in_video_programming") +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/PlayerComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/PlayerComponentsPatch.kt new file mode 100644 index 000000000..3934b2277 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/PlayerComponentsPatch.kt @@ -0,0 +1,665 @@ +package app.revanced.patches.youtube.player.components + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.shared.spans.addSpanFilter +import app.revanced.patches.shared.spans.inclusiveSpanPatch +import app.revanced.patches.shared.startVideoInformerFingerprint +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.controlsoverlay.controlsOverlayConfigPatch +import app.revanced.patches.youtube.utils.engagementPanelBuilderFingerprint +import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.SPANS_PATH +import app.revanced.patches.youtube.utils.fix.suggestedvideoendscreen.suggestedVideoEndScreenPatch +import app.revanced.patches.youtube.utils.patch.PatchList.PLAYER_COMPONENTS +import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.utils.resourceid.darkBackground +import app.revanced.patches.youtube.utils.resourceid.fadeDurationFast +import app.revanced.patches.youtube.utils.resourceid.scrimOverlay +import app.revanced.patches.youtube.utils.resourceid.seekUndoEduOverlayStub +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.resourceid.tapBloomView +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.patches.youtube.utils.youtubeControlsOverlayFingerprint +import app.revanced.patches.youtube.video.information.hookVideoInformation +import app.revanced.patches.youtube.video.information.videoInformationPatch +import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT +import app.revanced.util.Utils.printWarn +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.injectLiteralInstructionViewCall +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.fingerprint.resolvable +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private val speedOverlayPatch = bytecodePatch( + description = "speedOverlayPatch" +) { + dependsOn(sharedResourceIdPatch) + + execute { + fun MutableMethod.hookSpeedOverlay( + insertIndex: Int, + insertRegister: Int, + jumpIndex: Int + ) { + addInstructionsWithLabels( + insertIndex, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->disableSpeedOverlay()Z + move-result v$insertRegister + if-eqz v$insertRegister, :disable + """, ExternalLabel("disable", getInstruction(jumpIndex)) + ) + } + + val resolvable = restoreSlideToSeekBehaviorFingerprint.resolvable() && + speedOverlayFingerprint.resolvable() && + speedOverlayFloatValueFingerprint.resolvable() + + if (resolvable) { + // Used on YouTube 18.29.38 ~ YouTube 19.17.41 + + // region patch for Disable speed overlay (Enable slide to seek) + + mapOf( + restoreSlideToSeekBehaviorFingerprint to 45411329L, + speedOverlayFingerprint to 45411330L + ).forEach { (fingerprint, literal) -> + fingerprint.injectLiteralInstructionBooleanCall( + literal, + "$PLAYER_CLASS_DESCRIPTOR->disableSpeedOverlay(Z)Z" + ) + } + + // endregion + + // region patch for Custom speed overlay float value + + speedOverlayFloatValueFingerprint.matchOrThrow().let { + it.method.apply { + val index = it.patternMatch!!.startIndex + val register = getInstruction(index).registerA + + addInstructions( + index + 1, """ + invoke-static {v$register}, $PLAYER_CLASS_DESCRIPTOR->speedOverlayValue(F)F + move-result v$register + """ + ) + } + } + + // endregion + + } else { + // Used on YouTube 19.18.41~ + + // region patch for Disable speed overlay (Enable slide to seek) + + nextGenWatchLayoutFingerprint.methodOrThrow().apply { + val booleanValueIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "booleanValue" + } + val insertIndex = indexOfFirstInstructionOrThrow(booleanValueIndex - 10) { + opcode == Opcode.IGET_OBJECT && + getReference()?.definingClass == definingClass + } + val insertInstruction = getInstruction(insertIndex) + val insertReference = getInstruction(insertIndex).reference + + addInstruction( + insertIndex + 1, + "iget-object v${insertInstruction.registerA}, v${insertInstruction.registerB}, $insertReference" + ) + + val jumpIndex = indexOfFirstInstructionOrThrow(booleanValueIndex) { + opcode == Opcode.IGET_OBJECT && + getReference()?.definingClass == definingClass + } + + hookSpeedOverlay(insertIndex + 1, insertInstruction.registerA, jumpIndex) + } + + val (slideToSeekBooleanMethod, slideToSeekSyntheticMethod) = + slideToSeekMotionEventFingerprint.matchOrThrow( + horizontalTouchOffsetConstructorFingerprint + ).let { + with(it.method) { + val patternMatch = it.patternMatch!! + val jumpIndex = patternMatch.endIndex + 1 + val insertIndex = patternMatch.endIndex - 1 + val insertRegister = + getInstruction(insertIndex).registerA + + hookSpeedOverlay(insertIndex, insertRegister, jumpIndex) + + val slideToSeekBooleanMethod = + getWalkerMethod(patternMatch.startIndex + 1) + + val slideToSeekConstructorMethod = + findMethodOrThrow(slideToSeekBooleanMethod.definingClass) + + val slideToSeekSyntheticIndex = slideToSeekConstructorMethod + .indexOfFirstInstructionReversedOrThrow { + opcode == Opcode.NEW_INSTANCE + } + + val slideToSeekSyntheticClass = slideToSeekConstructorMethod + .getInstruction(slideToSeekSyntheticIndex) + .reference + .toString() + + val slideToSeekSyntheticMethod = + findMethodOrThrow(slideToSeekSyntheticClass) { + name == "run" + } + + Pair(slideToSeekBooleanMethod, slideToSeekSyntheticMethod) + } + } + + slideToSeekBooleanMethod.apply { + val insertIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.IGET_OBJECT + } + val insertRegister = getInstruction(insertIndex).registerA + val jumpIndex = indexOfFirstInstructionReversedOrThrow { + opcode == Opcode.INVOKE_VIRTUAL + } + + hookSpeedOverlay(insertIndex, insertRegister, jumpIndex) + } + + slideToSeekSyntheticMethod.apply { + val speedOverlayFloatValueIndex = indexOfFirstInstructionOrThrow { + (this as? NarrowLiteralInstruction)?.narrowLiteral == 2.0f.toRawBits() + } + val insertIndex = + indexOfFirstInstructionReversedOrThrow(speedOverlayFloatValueIndex) { + getReference()?.name == "removeCallbacks" + } + 1 + val insertRegister = + getInstruction(insertIndex - 1).registerC + val jumpIndex = + indexOfFirstInstructionOrThrow( + speedOverlayFloatValueIndex, + Opcode.RETURN_VOID + ) + 1 + + hookSpeedOverlay(insertIndex, insertRegister, jumpIndex) + } + + // endregion + + // region patch for Custom speed overlay float value + + slideToSeekSyntheticMethod.apply { + val speedOverlayFloatValueIndex = indexOfFirstInstructionOrThrow { + (this as? NarrowLiteralInstruction)?.narrowLiteral == 2.0f.toRawBits() + } + val speedOverlayFloatValueRegister = + getInstruction(speedOverlayFloatValueIndex).registerA + + addInstructions( + speedOverlayFloatValueIndex + 1, """ + invoke-static {v$speedOverlayFloatValueRegister}, $PLAYER_CLASS_DESCRIPTOR->speedOverlayValue(F)F + move-result v$speedOverlayFloatValueRegister + """ + ) + } + + speedOverlayTextValueFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.startIndex + val targetRegister = + getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->speedOverlayValue()D + move-result-wide v$targetRegister + """ + ) + } + } + + // endregion + + } + } +} + +private const val PLAYER_COMPONENTS_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/PlayerComponentsFilter;" +private const val SANITIZE_VIDEO_SUBTITLE_FILTER_CLASS_DESCRIPTOR = + "$SPANS_PATH/SanitizeVideoSubtitleFilter;" + +@Suppress("unused") +val playerComponentsPatch = bytecodePatch( + PLAYER_COMPONENTS.title, + PLAYER_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + controlsOverlayConfigPatch, + inclusiveSpanPatch, + lithoFilterPatch, + playerTypeHookPatch, + sharedResourceIdPatch, + speedOverlayPatch, + suggestedVideoEndScreenPatch, + videoInformationPatch, + ) + + execute { + fun MutableMethod.getAllLiteralComponent( + startIndex: Int, + endIndex: Int + ): String { + var literalComponent = "" + for (index in startIndex..endIndex) { + val opcode = getInstruction(index).opcode + if (opcode != Opcode.CONST_16 && opcode != Opcode.CONST_4) + continue + + val register = getInstruction(index).registerA + val value = getInstruction(index).wideLiteral.toInt() + + val line = """ + const/16 v$register, $value + + """.trimIndent() + + literalComponent += line + } + + return literalComponent + } + + fun MutableMethod.getFirstLiteralComponent( + startIndex: Int, + endIndex: Int + ): String { + val constRegister = + getInstruction(endIndex).registerE + + for (index in endIndex downTo startIndex) { + val instruction = getInstruction(index) + if (instruction.opcode != Opcode.CONST_16 && instruction.opcode != Opcode.CONST_4) + continue + + if ((instruction as OneRegisterInstruction).registerA != constRegister) + continue + + val constValue = (instruction as WideLiteralInstruction).wideLiteral.toInt() + + return "const/16 v$constRegister, $constValue" + } + return "" + } + + fun MutableMethod.hookFilmstripOverlay() { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->hideFilmstripOverlay()Z + move-result v0 + if-eqz v0, :shown + const/4 v0, 0x0 + return v0 + """, ExternalLabel("shown", getInstruction(0)) + ) + } + + // region patch for custom player overlay opacity + + youtubeControlsOverlayFingerprint.methodOrThrow().apply { + val constIndex = indexOfFirstLiteralInstructionOrThrow(scrimOverlay) + val targetIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.CHECK_CAST) + val targetParameter = getInstruction(targetIndex).reference + val targetRegister = getInstruction(targetIndex).registerA + + if (!targetParameter.toString().endsWith("Landroid/widget/ImageView;")) + throw PatchException("Method signature parameter did not match: $targetParameter") + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $PLAYER_CLASS_DESCRIPTOR->changeOpacity(Landroid/widget/ImageView;)V" + ) + } + + // endregion + + // region patch for disable auto player popup panels + + fun MutableMethod.hookInitVideoPanel(initVideoPanel: Int) = + addInstructions( + 0, """ + const/4 v0, $initVideoPanel + invoke-static {v0}, $PLAYER_CLASS_DESCRIPTOR->setInitVideoPanel(Z)V + """ + ) + + arrayOf( + lithoComponentOnClickListenerFingerprint, + offlineActionsOnClickListenerFingerprint, + ).forEach { fingerprint -> + fingerprint.methodOrThrow().apply { + val syntheticIndex = + indexOfFirstInstruction(Opcode.NEW_INSTANCE) + if (syntheticIndex >= 0) { + val syntheticReference = + getInstruction(syntheticIndex).reference.toString() + + findMethodOrThrow(syntheticReference) { + name == "onClick" + }.hookInitVideoPanel(0) + } else { + printWarn("target Opcode not found in ${fingerprint.first}") + } + } + } + + findMethodOrThrow( + engagementPanelPlaylistSyntheticFingerprint.methodOrThrow().definingClass + ) { + name == "onClick" + }.hookInitVideoPanel(0) + + startVideoInformerFingerprint.methodOrThrow().hookInitVideoPanel(1) + + engagementPanelBuilderFingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, """ + move/from16 v0, p4 + invoke-static {v0}, $PLAYER_CLASS_DESCRIPTOR->disableAutoPlayerPopupPanels(Z)Z + move-result v0 + if-eqz v0, :shown + const/4 v0, 0x0 + return-object v0 + """, ExternalLabel("shown", getInstruction(0)) + ) + } + + // endregion + + // region patch for disable auto switch mix playlists + + hookVideoInformation("$PLAYER_CLASS_DESCRIPTOR->disableAutoSwitchMixPlaylists(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") + + // endregion + + // region patch for hide channel watermark + + watermarkFingerprint.matchOrThrow(watermarkParentFingerprint).let { + it.method.apply { + val insertIndex = it.patternMatch!!.endIndex + val register = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex + 1, """ + invoke-static {v$register}, $PLAYER_CLASS_DESCRIPTOR->hideChannelWatermark(Z)Z + move-result v$register + """ + ) + } + } + + // endregion + + // region patch for hide crowdfunding box + + crowdfundingBoxFingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.patternMatch!!.endIndex + val register = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex, + "invoke-static {v$register}, $PLAYER_CLASS_DESCRIPTOR->hideCrowdfundingBox(Landroid/view/View;)V" + ) + } + } + + // endregion + + // region patch for hide double-tap overlay filter + + val smaliInstruction = """ + invoke-static {v$REGISTER_TEMPLATE_REPLACEMENT}, $PLAYER_CLASS_DESCRIPTOR->hideDoubleTapOverlayFilter(Landroid/view/View;)V + """ + + arrayOf( + darkBackground, + tapBloomView + ).forEach { literal -> + quickSeekOverlayFingerprint.injectLiteralInstructionViewCall( + literal, + smaliInstruction + ) + } + + // endregion + + // region patch for hide end screen cards + + listOf( + layoutCircleFingerprint, + layoutIconFingerprint, + layoutVideoFingerprint + ).forEach { fingerprint -> + fingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.patternMatch!!.endIndex + val viewRegister = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex + 1, + "invoke-static { v$viewRegister }, $PLAYER_CLASS_DESCRIPTOR->hideEndScreenCards(Landroid/view/View;)V" + ) + } + } + } + + // endregion + + // region patch for hide filmstrip overlay + + arrayOf( + filmStripOverlayConfigFingerprint, + filmStripOverlayInteractionFingerprint, + filmStripOverlayPreviewFingerprint + ).forEach { fingerprint -> + fingerprint.methodOrThrow(filmStripOverlayParentFingerprint).hookFilmstripOverlay() + } + + youtubeControlsOverlayFingerprint.methodOrThrow().apply { + val constIndex = indexOfFirstLiteralInstructionOrThrow(fadeDurationFast) + val constRegister = getInstruction(constIndex).registerA + val insertIndex = + indexOfFirstInstructionReversedOrThrow(constIndex, Opcode.INVOKE_VIRTUAL) + 1 + val jumpIndex = implementation!!.instructions.let { instruction -> + insertIndex + instruction.subList(insertIndex, instruction.size - 1) + .indexOfFirst { instructions -> + instructions.opcode == Opcode.GOTO || instructions.opcode == Opcode.GOTO_16 + } + } + + val replaceInstruction = getInstruction(insertIndex) + val replaceReference = + getInstruction(insertIndex).reference + + addInstructionsWithLabels( + insertIndex + 1, getAllLiteralComponent(insertIndex, jumpIndex - 1) + """ + const v$constRegister, $fadeDurationFast + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->hideFilmstripOverlay()Z + move-result v${replaceInstruction.registerA} + if-nez v${replaceInstruction.registerA}, :hidden + iget-object v${replaceInstruction.registerA}, v${replaceInstruction.registerB}, $replaceReference + """, ExternalLabel("hidden", getInstruction(jumpIndex)) + ) + removeInstruction(insertIndex) + } + + // endregion + + // region patch for hide info cards + + infoCardsIncognitoFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.startIndex + val targetRegister = + getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + invoke-static {v$targetRegister}, $PLAYER_CLASS_DESCRIPTOR->hideInfoCard(Z)Z + move-result v$targetRegister + """ + ) + } + } + + // endregion + + // region patch for hide seek message + + seekEduContainerFingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->hideSeekMessage()Z + move-result v0 + if-eqz v0, :default + return-void + """, ExternalLabel("default", getInstruction(0)) + ) + } + + youtubeControlsOverlayFingerprint.methodOrThrow().apply { + val insertIndex = + indexOfFirstLiteralInstructionOrThrow(seekUndoEduOverlayStub) + val insertRegister = getInstruction(insertIndex).registerA + + val onClickListenerIndex = indexOfFirstInstructionOrThrow(insertIndex) { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setOnClickListener" + } + val constComponent = getFirstLiteralComponent(insertIndex, onClickListenerIndex - 1) + + if (constComponent.isNotEmpty()) { + addInstruction( + onClickListenerIndex + 2, + constComponent + ) + } + addInstructionsWithLabels( + insertIndex, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->hideSeekUndoMessage()Z + move-result v$insertRegister + if-nez v$insertRegister, :default + """, ExternalLabel("default", getInstruction(onClickListenerIndex + 1)) + ) + } + + // endregion + + // region patch for hide suggested actions + + suggestedActionsFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.endIndex + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $PLAYER_CLASS_DESCRIPTOR->hideSuggestedActions(Landroid/view/View;)V" + + ) + } + } + + // endregion + + // region patch for skip autoplay countdown + + // This patch works fine when the [SuggestedVideoEndScreenPatch] patch is included. + touchAreaOnClickListenerFingerprint.mutableClassOrThrow().let { + it.methods.find { method -> + method.parameters == listOf("Landroid/view/View${'$'}OnClickListener;") + }?.apply { + val setOnClickListenerIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setOnClickListener" + } + val setOnClickListenerRegister = + getInstruction(setOnClickListenerIndex).registerC + + addInstruction( + setOnClickListenerIndex + 1, + "invoke-static {v$setOnClickListenerRegister}, $PLAYER_CLASS_DESCRIPTOR->skipAutoPlayCountdown(Landroid/view/View;)V" + ) + } ?: throw PatchException("Failed to find setOnClickListener method") + } + + // endregion + + // region patch for hide video zoom overlay + + videoZoomSnapIndicatorFingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->hideZoomOverlay()Z + move-result v0 + if-eqz v0, :shown + return-void + """, ExternalLabel("shown", getInstruction(0)) + ) + } + + // endregion + + addSpanFilter(SANITIZE_VIDEO_SUBTITLE_FILTER_CLASS_DESCRIPTOR) + addLithoFilter(PLAYER_COMPONENTS_FILTER_CLASS_DESCRIPTOR) + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: PLAYER", + "SETTINGS: PLAYER_COMPONENTS" + ), + PLAYER_COMPONENTS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/descriptions/DescriptionComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/descriptions/DescriptionComponentsPatch.kt new file mode 100644 index 000000000..7cfda01cf --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/descriptions/DescriptionComponentsPatch.kt @@ -0,0 +1,136 @@ +package app.revanced.patches.youtube.player.descriptions + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.patch.PatchList.DESCRIPTION_COMPONENTS +import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.utils.playservice.is_18_49_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_02_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.recyclerview.recyclerViewTreeObserverHook +import app.revanced.patches.youtube.utils.recyclerview.recyclerViewTreeObserverPatch +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.rollingNumberTextViewAnimationUpdateFingerprint +import app.revanced.patches.youtube.utils.rollingNumberTextViewFingerprint +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/DescriptionsFilter;" + +@Suppress("unused") +val descriptionComponentsPatch = bytecodePatch( + DESCRIPTION_COMPONENTS.title, + DESCRIPTION_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + lithoFilterPatch, + playerTypeHookPatch, + recyclerViewTreeObserverPatch, + sharedResourceIdPatch, + versionCheckPatch, + ) + + execute { + + var settingArray = arrayOf( + "PREFERENCE_SCREEN: PLAYER", + "SETTINGS: DESCRIPTION_COMPONENTS" + ) + + // region patch for disable rolling number animation + + // RollingNumber is applied to YouTube v18.49.37+. + // In order to maintain compatibility with YouTube v18.48.39 or previous versions, + // This patch is applied only to the version after YouTube v18.49.37. + if (is_18_49_or_greater) { + rollingNumberTextViewAnimationUpdateFingerprint.matchOrThrow( + rollingNumberTextViewFingerprint + ).let { + it.method.apply { + val freeRegister = implementation!!.registerCount - parameters.size - 2 + val imageSpanIndex = it.patternMatch!!.startIndex + val setTextIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setText" + } + addInstruction(setTextIndex, "nop") + addInstructionsWithLabels( + imageSpanIndex, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->disableRollingNumberAnimations()Z + move-result v$freeRegister + if-nez v$freeRegister, :disable_animations + """, ExternalLabel("disable_animations", getInstruction(setTextIndex)) + ) + } + } + + settingArray += "SETTINGS: DISABLE_ROLLING_NUMBER_ANIMATIONS" + } + + // endregion + + // region patch for disable video description interaction and expand video description + + // since these patches are still A/B tested, they are classified as 'Experimental flags'. + if (is_19_02_or_greater) { + textViewComponentFingerprint.methodOrThrow().apply { + val insertIndex = indexOfTextIsSelectableInstruction(this) + val insertInstruction = getInstruction(insertIndex) + + replaceInstruction( + insertIndex, + "invoke-static {v${insertInstruction.registerC}, v${insertInstruction.registerD}}, " + + "$PLAYER_CLASS_DESCRIPTOR->disableVideoDescriptionInteraction(Landroid/widget/TextView;Z)V" + ) + } + + engagementPanelTitleFingerprint.methodOrThrow(engagementPanelTitleParentFingerprint) + .apply { + val contentDescriptionIndex = indexOfContentDescriptionInstruction(this) + val contentDescriptionRegister = + getInstruction(contentDescriptionIndex).registerD + + addInstruction( + contentDescriptionIndex, + "invoke-static {v$contentDescriptionRegister}," + + "$PLAYER_CLASS_DESCRIPTOR->setContentDescription(Ljava/lang/String;)V" + ) + } + + recyclerViewTreeObserverHook("$PLAYER_CLASS_DESCRIPTOR->onVideoDescriptionCreate(Landroid/support/v7/widget/RecyclerView;)V") + + settingArray += "SETTINGS: DESCRIPTION_INTERACTION" + } + + // endregion + + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + + // region add settings + + addPreference(settingArray, DESCRIPTION_COMPONENTS) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/descriptions/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/descriptions/Fingerprints.kt new file mode 100644 index 000000000..82fd8fd5f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/descriptions/Fingerprints.kt @@ -0,0 +1,50 @@ +package app.revanced.patches.youtube.player.descriptions + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.indexOfFirstInstructionReversed +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val engagementPanelTitleFingerprint = legacyFingerprint( + name = "engagementPanelTitleFingerprint", + strings = listOf(". "), + customFingerprint = { method, _ -> + indexOfContentDescriptionInstruction(method) >= 0 + } +) + +internal fun indexOfContentDescriptionInstruction(method: Method) = + method.indexOfFirstInstructionReversed { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setContentDescription" + } + +internal val engagementPanelTitleParentFingerprint = legacyFingerprint( + name = "engagementPanelTitleParentFingerprint", + strings = listOf("[EngagementPanelTitleHeader] Cannot remove action buttons from header as the child count is out of sync. Buttons to remove exceed current header child count.") +) + +/** + * This fingerprint is compatible with YouTube v18.35.xx~ + * Nonetheless, the patch works in YouTube v19.02.xx~ + */ +internal val textViewComponentFingerprint = legacyFingerprint( + name = "textViewComponentFingerprint", + returnType = "V", + opcodes = listOf(Opcode.CMPL_FLOAT), + customFingerprint = { method, _ -> + method.implementation != null && + indexOfTextIsSelectableInstruction(method) >= 0 + }, +) + +internal fun indexOfTextIsSelectableInstruction(method: Method) = + method.indexOfFirstInstruction { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.name == "setTextIsSelectable" && + reference.definingClass != "Landroid/widget/TextView;" + } \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/Fingerprints.kt new file mode 100644 index 000000000..19470906e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/Fingerprints.kt @@ -0,0 +1,132 @@ +package app.revanced.patches.youtube.player.flyoutmenu.hide + +import app.revanced.patches.youtube.utils.resourceid.bottomSheetFooterText +import app.revanced.patches.youtube.utils.resourceid.subtitleMenuSettingsFooterInfo +import app.revanced.patches.youtube.utils.resourceid.videoQualityBottomSheet +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import app.revanced.util.parametersEqual +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val advancedQualityBottomSheetFingerprint = legacyFingerprint( + name = "advancedQualityBottomSheetFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "L", "L"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.CONST, + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CONST_16, + Opcode.INVOKE_VIRTUAL, + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IGET_OBJECT, + Opcode.CONST_STRING + ), + literals = listOf(videoQualityBottomSheet), +) + +internal val captionsBottomSheetFingerprint = legacyFingerprint( + name = "captionsBottomSheetFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(bottomSheetFooterText, subtitleMenuSettingsFooterInfo), +) + +/** + * This fingerprint is compatible with YouTube v18.39.xx+ + */ +internal val pipModeConfigFingerprint = legacyFingerprint( + name = "pipModeConfigFingerprint", + literals = listOf(45427407L), +) + +internal const val SLEEP_TIMER_CONSTRUCTOR_FEATURE_FLAG = 45640654L + +internal val sleepTimerConstructorFingerprint = legacyFingerprint( + name = "sleepTimerConstructorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf(SLEEP_TIMER_CONSTRUCTOR_FEATURE_FLAG), +) + +internal const val SLEEP_TIMER_FEATURE_FLAG = 45630421L + +internal val sleepTimerFingerprint = legacyFingerprint( + name = "sleepTimerConstructorFingerprint", + returnType = "Z", + literals = listOf(SLEEP_TIMER_FEATURE_FLAG), +) + +internal val videoQualityArrayFingerprint = legacyFingerprint( + name = "videoQualityArrayFingerprint", + returnType = "[Lcom/google/android/libraries/youtube/innertube/model/media/VideoQuality;", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + // 18.29 and earlier parameters are: + // "Ljava/util/List;", + // "Ljava/lang/String;" + // "L" + + // 18.31+ parameters are: + // "Ljava/util/List;", + // "Ljava/util/Collection;", + // "Ljava/lang/String;" + // "L" + customFingerprint = custom@{ method, _ -> + val parameterTypes = method.parameterTypes + val parameterSize = parameterTypes.size + if (parameterSize != 3 && parameterSize != 4) { + return@custom false + } + + val startsWithMethodParameterList = parameterTypes.slice(0..0) + val endsWithMethodParameterList = parameterTypes.slice(parameterSize - 2..= 0 + } +) + +private val VIDEO_QUALITY_ARRAY_STARTS_WITH_PARAMETER_LIST = listOf( + "Ljava/util/List;" +) +private val VIDEO_QUALITY_ARRAY_ENDS_WITH_PARAMETER_LIST = listOf( + "Ljava/lang/String;", + "L" +) + +internal fun indexOfQualityLabelInstruction(method: Method) = + method.indexOfFirstInstruction { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.returnType == "Ljava/lang/String;" && + reference.parameterTypes.size == 0 && + reference.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;" + } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/PlayerFlyoutMenuPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/PlayerFlyoutMenuPatch.kt new file mode 100644 index 000000000..07c2df304 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/PlayerFlyoutMenuPatch.kt @@ -0,0 +1,157 @@ +package app.revanced.patches.youtube.player.flyoutmenu.hide + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.patch.PatchList.HIDE_PLAYER_FLYOUT_MENU +import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.utils.playservice.is_18_39_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_30_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.qualityMenuViewInflateFingerprint +import app.revanced.patches.youtube.utils.resourceid.bottomSheetFooterText +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.injectLiteralInstructionViewCall +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val PANELS_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/PlayerFlyoutMenuFilter;" + +@Suppress("unused") +val playerFlyoutMenuPatch = bytecodePatch( + HIDE_PLAYER_FLYOUT_MENU.title, + HIDE_PLAYER_FLYOUT_MENU.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + lithoFilterPatch, + playerTypeHookPatch, + sharedResourceIdPatch, + versionCheckPatch + ) + + execute { + var settingArray = arrayOf( + "PREFERENCE_SCREEN: PLAYER", + "PREFERENCE_SCREENS: FLYOUT_MENU", + "SETTINGS: HIDE_PLAYER_FLYOUT_MENU" + ) + + // region hide player flyout menu header, footer (non-litho) + + mapOf( + advancedQualityBottomSheetFingerprint to "hidePlayerFlyoutMenuQualityFooter", + captionsBottomSheetFingerprint to "hidePlayerFlyoutMenuCaptionsFooter", + qualityMenuViewInflateFingerprint to "hidePlayerFlyoutMenuQualityFooter" + ).forEach { (fingerprint, name) -> + val smaliInstruction = """ + invoke-static {v$REGISTER_TEMPLATE_REPLACEMENT}, $PLAYER_CLASS_DESCRIPTOR->$name(Landroid/view/View;)V + """ + fingerprint.injectLiteralInstructionViewCall(bottomSheetFooterText, smaliInstruction) + } + + arrayOf( + advancedQualityBottomSheetFingerprint, + qualityMenuViewInflateFingerprint + ).forEach { fingerprint -> + fingerprint.methodOrThrow().apply { + val insertIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "addHeaderView" + } + val insertRegister = getInstruction(insertIndex).registerD + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $PLAYER_CLASS_DESCRIPTOR->hidePlayerFlyoutMenuQualityHeader(Landroid/view/View;)Landroid/view/View; + move-result-object v$insertRegister + """ + ) + } + } + + // endregion + + // region patch for hide '1080p Premium' label + + videoQualityArrayFingerprint.methodOrThrow().apply { + val qualityLabelIndex = indexOfQualityLabelInstruction(this) + 1 + val qualityLabelRegister = + getInstruction(qualityLabelIndex).registerA + val jumpIndex = indexOfFirstInstructionReversedOrThrow(qualityLabelIndex) { + opcode == Opcode.INVOKE_INTERFACE && + getReference()?.name == "hasNext" + } + + addInstructionsWithLabels( + qualityLabelIndex + 1, """ + invoke-static {v$qualityLabelRegister}, $PLAYER_CLASS_DESCRIPTOR->hidePlayerFlyoutMenuEnhancedBitrate(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$qualityLabelRegister + if-eqz v$qualityLabelRegister, :jump + """, ExternalLabel("jump", getInstruction(jumpIndex)) + ) + } + + // endregion + + // region patch for hide pip mode menu + + if (is_18_39_or_greater) { + pipModeConfigFingerprint.injectLiteralInstructionBooleanCall( + 45427407L, + "$PLAYER_CLASS_DESCRIPTOR->hidePiPModeMenu(Z)Z" + ) + settingArray += "SETTINGS: HIDE_PIP_MODE_MENU" + } + + // endregion + + // region patch for hide sleep timer menu + + if (is_19_30_or_greater) { + // Sleep timer menu in Additional settings (deprecated) + // TODO: A patch will be implemented to assign this deprecated menu to another action. + // mapOf( + // sleepTimerConstructorFingerprint to SLEEP_TIMER_CONSTRUCTOR_FEATURE_FLAG, + // sleepTimerFingerprint to SLEEP_TIMER_FEATURE_FLAG + // ).forEach { (fingerprint, literal) -> + // fingerprint.injectLiteralInstructionBooleanCall( + // literal, + // "$PLAYER_CLASS_DESCRIPTOR->hideDeprecatedSleepTimerMenu(Z)Z" + // ) + // } + settingArray += "SETTINGS: HIDE_SLEEP_TIMER_MENU" + } + + // endregion + + addLithoFilter(PANELS_FILTER_CLASS_DESCRIPTOR) + + // region add settings + + addPreference(settingArray, HIDE_PLAYER_FLYOUT_MENU) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/toggle/ChangeTogglePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/toggle/ChangeTogglePatch.kt new file mode 100644 index 000000000..3c94fcd09 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/toggle/ChangeTogglePatch.kt @@ -0,0 +1,183 @@ +package app.revanced.patches.youtube.player.flyoutmenu.toggle + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.patch.PatchList.CHANGE_PLAYER_FLYOUT_MENU_TOGGLES +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.resolvable +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstStringInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +@Suppress("unused") +val changeTogglePatch = bytecodePatch( + CHANGE_PLAYER_FLYOUT_MENU_TOGGLES.title, + CHANGE_PLAYER_FLYOUT_MENU_TOGGLES.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + fun changeToggleCinematicLightingHook() { + val stableVolumeMethod = stableVolumeFingerprint.methodOrThrow() + + val stringReferenceIndex = stableVolumeMethod.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + (this as ReferenceInstruction).reference.toString() + .endsWith("(Ljava/lang/String;Ljava/lang/String;)V") + } + if (stringReferenceIndex < 0) + throw PatchException("Target reference was not found in stableVolumeFingerprint.") + + val stringReference = + stableVolumeMethod.getInstruction(stringReferenceIndex).reference + + cinematicLightingFingerprint.methodOrThrow().apply { + val iGetIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.IGET && + getReference()?.definingClass == definingClass + } + val classRegister = getInstruction(iGetIndex).registerB + + val stringIndex = + indexOfFirstStringInstructionOrThrow("menu_item_cinematic_lighting") + + val checkCastIndex = + indexOfFirstInstructionReversedOrThrow(stringIndex, Opcode.CHECK_CAST) + val iGetObjectPrimaryIndex = + indexOfFirstInstructionReversedOrThrow(checkCastIndex, Opcode.IGET_OBJECT) + val iGetObjectSecondaryIndex = + indexOfFirstInstructionOrThrow(checkCastIndex, Opcode.IGET_OBJECT) + + val checkCastReference = + getInstruction(checkCastIndex).reference + val iGetObjectPrimaryReference = + getInstruction(iGetObjectPrimaryIndex).reference + val iGetObjectSecondaryReference = + getInstruction(iGetObjectSecondaryIndex).reference + + val invokeVirtualIndex = + indexOfFirstInstructionOrThrow(stringIndex, Opcode.INVOKE_VIRTUAL) + val invokeVirtualInstruction = + getInstruction(invokeVirtualIndex) + val freeRegisterC = invokeVirtualInstruction.registerC + val freeRegisterD = invokeVirtualInstruction.registerD + val freeRegisterE = invokeVirtualInstruction.registerE + + val insertIndex = indexOfFirstInstructionOrThrow(stringIndex, Opcode.RETURN_VOID) + + addInstructionsWithLabels( + insertIndex, """ + const/4 v$freeRegisterC, 0x1 + invoke-static {v$freeRegisterC}, $PLAYER_CLASS_DESCRIPTOR->changeSwitchToggle(Z)Z + move-result v$freeRegisterC + if-nez v$freeRegisterC, :ignore + sget-object v$freeRegisterC, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean; + if-eq v$freeRegisterC, v$freeRegisterE, :toggle_off + const-string v$freeRegisterE, "stable_volume_on" + invoke-static {v$freeRegisterE}, $PLAYER_CLASS_DESCRIPTOR->getToggleString(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$freeRegisterE + goto :set_string + :toggle_off + const-string v$freeRegisterE, "stable_volume_off" + invoke-static {v$freeRegisterE}, $PLAYER_CLASS_DESCRIPTOR->getToggleString(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$freeRegisterE + :set_string + iget-object v$freeRegisterC, v$classRegister, $iGetObjectPrimaryReference + check-cast v$freeRegisterC, $checkCastReference + iget-object v$freeRegisterC, v$freeRegisterC, $iGetObjectSecondaryReference + const-string v$freeRegisterD, "menu_item_cinematic_lighting" + invoke-virtual {v$freeRegisterC, v$freeRegisterD, v$freeRegisterE}, $stringReference + """, ExternalLabel("ignore", getInstruction(insertIndex)) + ) + } + } + + fun changeToggleHook( + fingerprint: Pair, + methodToCall: String + ) { + val method = if (fingerprint == playbackLoopInitFingerprint) + fingerprint.methodOrThrow(playbackLoopOnClickListenerFingerprint) + else + fingerprint.methodOrThrow() + + method.apply { + val referenceIndex = indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + (this as ReferenceInstruction).reference.toString() + .endsWith(methodToCall) + } + if (referenceIndex > 0) { + val insertRegister = + getInstruction(referenceIndex + 1).registerA + + addInstructions( + referenceIndex + 2, """ + invoke-static {v$insertRegister}, $PLAYER_CLASS_DESCRIPTOR->changeSwitchToggle(Z)Z + move-result v$insertRegister + """ + ) + } else { + if (fingerprint == cinematicLightingFingerprint) + changeToggleCinematicLightingHook() + else + throw PatchException("Target reference was not found in ${fingerprint.first}.") + } + } + } + + + val additionalSettingsConfigMethod = + additionalSettingsConfigFingerprint.methodOrThrow() + val methodToCall = + additionalSettingsConfigMethod.definingClass + "->" + additionalSettingsConfigMethod.name + "()Z" + + var fingerprintArray = arrayOf( + cinematicLightingFingerprint, + playbackLoopInitFingerprint, + playbackLoopOnClickListenerFingerprint, + stableVolumeFingerprint + ) + + if (pipFingerprint.resolvable()) { + fingerprintArray += pipFingerprint + } + + fingerprintArray.forEach { fingerprint -> + changeToggleHook(fingerprint, methodToCall) + } + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: PLAYER", + "PREFERENCE_SCREENS: FLYOUT_MENU", + "SETTINGS: CHANGE_PLAYER_FLYOUT_MENU_TOGGLE" + ), + CHANGE_PLAYER_FLYOUT_MENU_TOGGLES + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/toggle/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/toggle/Fingerprints.kt new file mode 100644 index 000000000..ea57bf81e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/toggle/Fingerprints.kt @@ -0,0 +1,54 @@ +package app.revanced.patches.youtube.player.flyoutmenu.toggle + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val additionalSettingsConfigFingerprint = legacyFingerprint( + name = "additionalSettingsConfigFingerprint", + returnType = "Z", + literals = listOf(45412662L), +) + +internal val cinematicLightingFingerprint = legacyFingerprint( + name = "cinematicLightingFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Ljava/lang/Object;"), + strings = listOf("menu_item_cinematic_lighting") +) + +internal val pipFingerprint = legacyFingerprint( + name = "pipFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + strings = listOf("menu_item_picture_in_picture"), + customFingerprint = { _, classDef -> + classDef.methods.count() > 5 + } +) + +internal val playbackLoopInitFingerprint = legacyFingerprint( + name = "playbackLoopInitFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + strings = listOf("menu_item_single_video_playback_loop") +) + +internal val playbackLoopOnClickListenerFingerprint = legacyFingerprint( + name = "playbackLoopOnClickListenerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("I", "Z"), + strings = listOf("menu_item_single_video_playback_loop") +) + +internal val stableVolumeFingerprint = legacyFingerprint( + name = "stableVolumeFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Ljava/lang/Object;"), + strings = listOf("menu_item_stable_volume") +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/Fingerprints.kt new file mode 100644 index 000000000..d89496da6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/Fingerprints.kt @@ -0,0 +1,81 @@ +package app.revanced.patches.youtube.player.fullscreen + +import app.revanced.patches.youtube.utils.resourceid.appRelatedEndScreenResults +import app.revanced.patches.youtube.utils.resourceid.fullScreenEngagementPanel +import app.revanced.patches.youtube.utils.resourceid.playerVideoTitleView +import app.revanced.patches.youtube.utils.resourceid.quickActionsElementContainer +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val broadcastReceiverFingerprint = legacyFingerprint( + name = "broadcastReceiverFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/content/Context;", "Landroid/content/Intent;"), + strings = listOf( + "android.intent.action.SCREEN_ON", + "android.intent.action.SCREEN_OFF", + "android.intent.action.BATTERY_CHANGED" + ), + customFingerprint = { _, classDef -> + classDef.superclass == "Landroid/content/BroadcastReceiver;" + } +) + +internal val clientSettingEndpointFingerprint = legacyFingerprint( + name = "clientSettingEndpointFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "Ljava/util/Map;"), + strings = listOf( + "OVERRIDE_EXIT_FULLSCREEN_TO_MAXIMIZED", + "force_fullscreen", + "start_watch_minimized", + "watch" + ) +) + +internal val engagementPanelFingerprint = legacyFingerprint( + name = "engagementPanelFingerprint", + returnType = "L", + parameters = listOf("L"), + literals = listOf(fullScreenEngagementPanel), +) + +/** + * This fingerprint is compatible with YouTube v18.42.41+ + */ +internal val landScapeModeConfigFingerprint = legacyFingerprint( + name = "landScapeModeConfigFingerprint", + returnType = "Z", + literals = listOf(45446428L), +) + +internal val playerTitleViewFingerprint = legacyFingerprint( + name = "playerTitleViewFingerprint", + returnType = "V", + literals = listOf(playerVideoTitleView), +) + +internal val quickActionsElementFingerprint = legacyFingerprint( + name = "quickActionsElementFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/View;"), + literals = listOf(quickActionsElementContainer), +) + +internal val relatedEndScreenResultsFingerprint = legacyFingerprint( + name = "relatedEndScreenResultsFingerprint", + returnType = "V", + literals = listOf(appRelatedEndScreenResults), +) + +internal val videoPortraitParentFingerprint = legacyFingerprint( + name = "videoPortraitParentFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "Ljava/util/Map;"), + strings = listOf("Acquiring NetLatencyActionLogger failed. taskId=") +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt new file mode 100644 index 000000000..f00ad80bd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt @@ -0,0 +1,339 @@ +package app.revanced.patches.youtube.player.fullscreen + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.shared.mainactivity.onConfigurationChangedMethod +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.youtube.utils.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.layoutConstructorFingerprint +import app.revanced.patches.youtube.utils.mainactivity.mainActivityResolvePatch +import app.revanced.patches.youtube.utils.patch.PatchList.FULLSCREEN_COMPONENTS +import app.revanced.patches.youtube.utils.playservice.is_18_42_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_41_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.resourceid.autoNavPreviewStub +import app.revanced.patches.youtube.utils.resourceid.fullScreenEngagementPanel +import app.revanced.patches.youtube.utils.resourceid.quickActionsElementContainer +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.patches.youtube.utils.youtubeControlsOverlayFingerprint +import app.revanced.util.Utils.printWarn +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import app.revanced.util.indexOfFirstStringInstructionOrThrow +import app.revanced.util.updatePatchStatus +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/QuickActionFilter;" + +@Suppress("unused") +val fullscreenComponentsPatch = bytecodePatch( + FULLSCREEN_COMPONENTS.title, + FULLSCREEN_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + lithoFilterPatch, + mainActivityResolvePatch, + sharedResourceIdPatch, + versionCheckPatch, + ) + + execute { + + var settingArray = arrayOf( + "PREFERENCE_SCREEN: PLAYER", + "SETTINGS: FULLSCREEN_COMPONENTS" + ) + + // region patch for disable engagement panel + + engagementPanelFingerprint.methodOrThrow().apply { + val literalIndex = + indexOfFirstLiteralInstructionOrThrow(fullScreenEngagementPanel) + val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.CHECK_CAST) + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, " + + "$PLAYER_CLASS_DESCRIPTOR->disableEngagementPanels(Landroidx/coordinatorlayout/widget/CoordinatorLayout;)V" + ) + + } + + playerTitleViewFingerprint.methodOrThrow().apply { + val insertIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "addView" + } + val insertReference = + getInstruction(insertIndex).reference.toString() + if (!insertReference.startsWith("Landroid/widget/FrameLayout;")) + throw PatchException("Reference does not match: $insertReference") + val insertInstruction = getInstruction(insertIndex) + + replaceInstruction( + insertIndex, + "invoke-static { v${insertInstruction.registerC}, v${insertInstruction.registerD} }, " + + "$PLAYER_CLASS_DESCRIPTOR->showVideoTitleSection(Landroid/widget/FrameLayout;Landroid/view/View;)V" + ) + } + + // endregion + + // region patch for hide autoplay preview + + layoutConstructorFingerprint.methodOrThrow().apply { + val constIndex = indexOfFirstLiteralInstructionOrThrow(autoNavPreviewStub) + val constRegister = getInstruction(constIndex).registerA + val jumpIndex = + indexOfFirstInstructionOrThrow(constIndex + 2, Opcode.INVOKE_VIRTUAL) + 1 + + addInstructionsWithLabels( + constIndex, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->hideAutoPlayPreview()Z + move-result v$constRegister + if-nez v$constRegister, :hidden + """, ExternalLabel("hidden", getInstruction(jumpIndex)) + ) + } + + // endregion + + // region patch for hide related video overlay + + relatedEndScreenResultsFingerprint.mutableClassOrThrow().let { + it.methods.find { method -> method.parameters == listOf("I", "Z", "I") } + ?.apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->hideRelatedVideoOverlay()Z + move-result v0 + if-eqz v0, :show + return-void + """, ExternalLabel("show", getInstruction(0)) + ) + } ?: throw PatchException("Could not find targetMethod") + } + + // endregion + + // region patch for quick actions + + quickActionsElementFingerprint.methodOrThrow().apply { + val containerCalls = implementation!!.instructions.withIndex() + .filter { instruction -> + (instruction.value as? WideLiteralInstruction)?.wideLiteral == quickActionsElementContainer + } + val constIndex = containerCalls.elementAt(containerCalls.size - 1).index + + val checkCastIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.CHECK_CAST) + val insertRegister = + getInstruction(checkCastIndex).registerA + + addInstruction( + checkCastIndex + 1, + "invoke-static {v$insertRegister}, $PLAYER_CLASS_DESCRIPTOR->setQuickActionMargin(Landroid/view/View;)V" + ) + + addInstruction( + checkCastIndex, + "invoke-static {v$insertRegister}, $PLAYER_CLASS_DESCRIPTOR->hideQuickActions(Landroid/view/View;)V" + ) + } + + updatePatchStatus(PATCH_STATUS_CLASS_DESCRIPTOR, "QuickActions") + + // endregion + + // region patch for compact control overlay + + youtubeControlsOverlayFingerprint.methodOrThrow().apply { + val targetIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setFocusableInTouchMode" + } + val walkerIndex = indexOfFirstInstructionOrThrow(targetIndex, Opcode.INVOKE_STATIC) + + val walkerMethod = getWalkerMethod(walkerIndex) + walkerMethod.apply { + val insertIndex = implementation!!.instructions.size - 1 + val targetRegister = + getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$targetRegister}, $PLAYER_CLASS_DESCRIPTOR->enableCompactControlsOverlay(Z)Z + move-result v$targetRegister + """ + ) + } + } + + // endregion + + // region patch for force fullscreen + + clientSettingEndpointFingerprint.methodOrThrow().apply { + val getActivityIndex = indexOfFirstStringInstructionOrThrow("watch") + 2 + val getActivityReference = + getInstruction(getActivityIndex).reference + val classRegister = + getInstruction(getActivityIndex).registerB + + val watchDescriptorMethodIndex = + indexOfFirstStringInstructionOrThrow("start_watch_minimized") - 1 + val watchDescriptorRegister = + getInstruction(watchDescriptorMethodIndex).registerD + + addInstructions( + watchDescriptorMethodIndex, """ + invoke-static {v$watchDescriptorRegister}, $PLAYER_CLASS_DESCRIPTOR->forceFullscreen(Z)Z + move-result v$watchDescriptorRegister + """ + ) + + // hooks Activity. + val insertIndex = indexOfFirstStringInstructionOrThrow("force_fullscreen") + val freeRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + iget-object v$freeRegister, v$classRegister, $getActivityReference + check-cast v$freeRegister, Landroid/app/Activity; + invoke-static {v$freeRegister}, $PLAYER_CLASS_DESCRIPTOR->setWatchDescriptorActivity(Landroid/app/Activity;)V + """ + ) + } + + videoPortraitParentFingerprint.methodOrThrow().apply { + val stringIndex = + indexOfFirstStringInstructionOrThrow("Acquiring NetLatencyActionLogger failed. taskId=") + val invokeIndex = + indexOfFirstInstructionOrThrow(stringIndex, Opcode.INVOKE_INTERFACE) + val targetIndex = indexOfFirstInstructionOrThrow(invokeIndex, Opcode.CHECK_CAST) + val targetClass = + getInstruction(targetIndex).reference.toString() + + // add an instruction to check the vertical video + findMethodOrThrow(targetClass) { + parameters == listOf("I", "I", "Z") + }.addInstruction( + 1, + "invoke-static {p1, p2}, $PLAYER_CLASS_DESCRIPTOR->setVideoPortrait(II)V" + ) + } + + // endregion + + // region patch for disable landscape mode + + onConfigurationChangedMethod.apply { + val walkerIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + reference?.parameterTypes == listOf("Landroid/content/res/Configuration;") && + reference.returnType == "V" && + reference.name != "onConfigurationChanged" + } + + val walkerMethod = getWalkerMethod(walkerIndex) + val constructorMethod = + findMethodOrThrow(walkerMethod.definingClass) { + name == "" && + parameterTypes == listOf("Landroid/app/Activity;") + } + + arrayOf( + walkerMethod, + constructorMethod + ).forEach { method -> + method.apply { + val index = indexOfFirstInstructionOrThrow { + val reference = getReference() + reference?.parameterTypes == listOf("Landroid/content/Context;") && + reference.returnType == "Z" + } + 1 + val register = getInstruction(index).registerA + + addInstructions( + index + 1, """ + invoke-static {v$register}, $PLAYER_CLASS_DESCRIPTOR->disableLandScapeMode(Z)Z + move-result v$register + """ + ) + } + } + } + + // endregion + + // region patch for keep landscape mode + + if (is_18_42_or_greater && !is_19_41_or_greater) { + landScapeModeConfigFingerprint.methodOrThrow().apply { + val insertIndex = implementation!!.instructions.lastIndex + val insertRegister = + getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $PLAYER_CLASS_DESCRIPTOR->keepFullscreen(Z)Z + move-result v$insertRegister + """ + ) + } + broadcastReceiverFingerprint.methodOrThrow().apply { + val stringIndex = + indexOfFirstStringInstructionOrThrow("android.intent.action.SCREEN_ON") + val insertIndex = + indexOfFirstInstructionOrThrow(stringIndex, Opcode.IF_EQZ) + 1 + + addInstruction( + insertIndex, + "invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->setScreenOn()V" + ) + } + + settingArray += "SETTINGS: KEEP_LANDSCAPE_MODE" + } else { + printWarn("\"Keep landscape mode\" is not supported in this version. Use YouTube 19.16.39 or earlier.") + } + + // endregion + + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + + // region add settings + + addPreference(settingArray, FULLSCREEN_COMPONENTS) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/hapticfeedback/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/hapticfeedback/Fingerprints.kt new file mode 100644 index 000000000..48b63bb9e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/hapticfeedback/Fingerprints.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.youtube.player.hapticfeedback + +import app.revanced.util.fingerprint.legacyFingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val markerHapticsFingerprint = legacyFingerprint( + name = "markerHapticsFingerprint", + returnType = "V", + strings = listOf("Failed to execute markers haptics vibrate.") +) + +internal val scrubbingHapticsFingerprint = legacyFingerprint( + name = "scrubbingHapticsFingerprint", + returnType = "V", + strings = listOf("Failed to haptics vibrate for fine scrubbing.") +) + +internal val seekHapticsFingerprint = legacyFingerprint( + name = "seekHapticsFingerprint", + returnType = "V", + opcodes = listOf(Opcode.SGET), + strings = listOf("Failed to easy seek haptics vibrate."), + customFingerprint = { method, _ -> method.name == "run" } +) + +internal val seekUndoHapticsFingerprint = legacyFingerprint( + name = "seekUndoHapticsFingerprint", + returnType = "V", + strings = listOf("Failed to execute seek undo haptics vibrate.") +) + +internal val zoomHapticsFingerprint = legacyFingerprint( + name = "zoomHapticsFingerprint", + returnType = "V", + strings = listOf("Failed to haptics vibrate for video zoom") +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/hapticfeedback/hapticFeedbackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/hapticfeedback/hapticFeedbackPatch.kt new file mode 100644 index 000000000..22888a480 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/hapticfeedback/hapticFeedbackPatch.kt @@ -0,0 +1,71 @@ +package app.revanced.patches.youtube.player.hapticfeedback + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.patch.PatchList.DISABLE_HAPTIC_FEEDBACK +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +@Suppress("unused") +val hapticFeedbackPatch = bytecodePatch( + DISABLE_HAPTIC_FEEDBACK.title, + DISABLE_HAPTIC_FEEDBACK.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + fun Pair.hookHapticFeedback(methodName: String) = + methodOrThrow().apply { + var index = 0 + var register = 0 + + if (name == "run") { + index = indexOfFirstInstructionOrThrow(Opcode.SGET) + register = getInstruction(index).registerA + } + + addInstructionsWithLabels( + index, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->$methodName()Z + move-result v$register + if-eqz v$register, :vibrate + return-void + """, ExternalLabel("vibrate", getInstruction(index)) + ) + } + + arrayOf( + seekHapticsFingerprint to "disableSeekVibrate", + seekUndoHapticsFingerprint to "disableSeekUndoVibrate", + scrubbingHapticsFingerprint to "disableScrubbingVibrate", + markerHapticsFingerprint to "disableChapterVibrate", + zoomHapticsFingerprint to "disableZoomVibrate" + ).map { (fingerprint, methodName) -> + fingerprint.hookHapticFeedback(methodName) + } + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: PLAYER", + "SETTINGS: DISABLE_HAPTIC_FEEDBACK" + ), + DISABLE_HAPTIC_FEEDBACK + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt new file mode 100644 index 000000000..9c20c69a1 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt @@ -0,0 +1,298 @@ +package app.revanced.patches.youtube.player.overlaybuttons + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.booleanOption +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.OVERLAY_BUTTONS_PATH +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.youtube.utils.fix.bottomui.cfBottomUIPatch +import app.revanced.patches.youtube.utils.patch.PatchList.OVERLAY_BUTTONS +import app.revanced.patches.youtube.utils.pip.pipStateHookPatch +import app.revanced.patches.youtube.utils.playercontrols.hookBottomControlButton +import app.revanced.patches.youtube.utils.playercontrols.playerControlsPatch +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.patches.youtube.video.information.videoEndMethod +import app.revanced.patches.youtube.video.information.videoInformationPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.copyResources +import app.revanced.util.copyXmlNode +import app.revanced.util.doRecursively +import app.revanced.util.lowerCaseOrThrow +import org.w3c.dom.Element + +private const val EXTENSION_ALWAYS_REPEAT_CLASS_DESCRIPTOR = + "$UTILS_PATH/AlwaysRepeatPatch;" + +private val overlayButtonsBytecodePatch = bytecodePatch( + description = "overlayButtonsBytecodePatch" +) { + dependsOn(videoInformationPatch) + + execute { + + // region patch for always repeat + + videoEndMethod.apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $EXTENSION_ALWAYS_REPEAT_CLASS_DESCRIPTOR->alwaysRepeat()Z + move-result v0 + if-eqz v0, :end + return-void + """, ExternalLabel("end", getInstruction(0)) + ) + } + + // endregion + + } +} + +private const val MARGIN_NONE = "0.0dip" +private const val MARGIN_DEFAULT = "2.5dip" +private const val MARGIN_WIDER = "5.0dip" + +private const val DEFAULT_ICON = "rounded" + +@Suppress("unused") +val overlayButtonsPatch = resourcePatch( + OVERLAY_BUTTONS.title, + OVERLAY_BUTTONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + overlayButtonsBytecodePatch, + cfBottomUIPatch, + pipStateHookPatch, + playerControlsPatch, + sharedResourceIdPatch, + settingsPatch, + ) + + val iconTypeOption = stringOption( + key = "iconType", + default = DEFAULT_ICON, + values = mapOf( + "Bold" to "bold", + "Rounded" to DEFAULT_ICON, + "Thin" to "thin" + ), + title = "Icon type", + description = "The icon type.", + required = true + ) + + val bottomMarginOption = stringOption( + key = "bottomMargin", + default = MARGIN_DEFAULT, + values = mapOf( + "Default" to MARGIN_DEFAULT, + "None" to MARGIN_NONE, + "Wider" to MARGIN_WIDER, + ), + title = "Bottom margin", + description = "The bottom margin for the overlay buttons and timestamp.", + required = true + ) + + val widerButtonsSpace by booleanOption( + key = "widerButtonsSpace", + default = false, + title = "Wider between-buttons space", + description = "Prevent adjacent button presses by increasing the horizontal spacing between buttons.", + required = true + ) + + val changeTopButtons by booleanOption( + key = "changeTopButtons", + default = false, + title = "Change top buttons", + description = "Change the icons at the top of the player.", + required = true + ) + + execute { + + // Check patch options first. + val iconType = iconTypeOption + .lowerCaseOrThrow() + + val marginBottom = bottomMarginOption + .lowerCaseOrThrow() + + // Inject hooks for overlay buttons. + setOf( + "AlwaysRepeat;", + "CopyVideoUrl;", + "CopyVideoUrlTimestamp;", + "MuteVolume;", + "ExternalDownload;", + "PlayAll;", + "SpeedDialog;", + "Whitelists;" + ).forEach { className -> + hookBottomControlButton("$OVERLAY_BUTTONS_PATH/$className") + } + + // Copy necessary resources for the overlay buttons. + copyResources( + "youtube/overlaybuttons/shared", + ResourceGroup( + "drawable", + "playlist_repeat_button.xml", + "playlist_shuffle_button.xml", + "revanced_repeat_button.xml", + "revanced_mute_volume_button.xml", + ) + ) + + // Apply the selected icon type to the overlay buttons. + arrayOf( + "xxxhdpi", + "xxhdpi", + "xhdpi", + "hdpi", + "mdpi" + ).forEach { dpi -> + copyResources( + "youtube/overlaybuttons/$iconType", + ResourceGroup( + "drawable-$dpi", + "ic_vr.png", + "quantum_ic_fullscreen_exit_grey600_24.png", + "quantum_ic_fullscreen_exit_white_24.png", + "quantum_ic_fullscreen_grey600_24.png", + "quantum_ic_fullscreen_white_24.png", + "revanced_copy_button.png", + "revanced_copy_timestamp_button.png", + "revanced_download_button.png", + "revanced_play_all_button.png", + "revanced_speed_button.png", + "revanced_volume_muted_button.png", + "revanced_volume_unmuted_button.png", + "revanced_whitelist_button.png", + "yt_fill_arrow_repeat_white_24.png", + "yt_outline_arrow_repeat_1_white_24.png", + "yt_outline_arrow_shuffle_1_white_24.png", + "yt_outline_screen_full_exit_white_24.png", + "yt_outline_screen_full_white_24.png", + "yt_outline_screen_full_vd_theme_24.png", + "yt_outline_screen_vertical_vd_theme_24.png" + ), + ResourceGroup( + "drawable", + "yt_outline_screen_vertical_vd_theme_24.xml" + ) + ) + } + + // Merge XML nodes from the host to their respective XML files. + copyXmlNode( + "youtube/overlaybuttons/shared/host", + "layout/youtube_controls_bottom_ui_container.xml", + "android.support.constraint.ConstraintLayout" + ) + + // Note: Do not modify fullscreen button and multiview button + document("res/layout/youtube_controls_bottom_ui_container.xml").use { document -> + document.doRecursively loop@{ node -> + if (node !is Element) return@loop + + // Change the relationship between buttons + node.getAttributeNode("yt:layout_constraintRight_toLeftOf") + ?.let { attribute -> + if (attribute.textContent == "@id/fullscreen_button") { + attribute.textContent = "@+id/speed_dialog_button" + } + } + + val (id, height, width) = Triple( + node.getAttribute("android:id"), + node.getAttribute("android:layout_height"), + node.getAttribute("android:layout_width") + ) + val (heightIsNotZero, widthIsNotZero, isButton) = Triple( + height != "0.0dip", + width != "0.0dip", + id.endsWith("_button") && id != "@id/multiview_button" + ) + + // Adjust TimeBar and Chapter bottom padding + val timBarItem = mutableMapOf( + "@id/time_bar_chapter_title" to "16.0dip", + "@id/timestamps_container" to "14.0dip" + ) + + val layoutHeightWidth = if (widerButtonsSpace == true) + "56.0dip" + else + "48.0dip" + + if (isButton) { + node.setAttribute("android:layout_marginBottom", marginBottom) + node.setAttribute("android:paddingLeft", "0.0dip") + node.setAttribute("android:paddingRight", "0.0dip") + node.setAttribute("android:paddingBottom", "22.0dip") + if (heightIsNotZero && widthIsNotZero) { + node.setAttribute("android:layout_height", layoutHeightWidth) + node.setAttribute("android:layout_width", layoutHeightWidth) + } + } else if (timBarItem.containsKey(id)) { + node.setAttribute("android:layout_marginBottom", marginBottom) + if (widerButtonsSpace != true) { + node.setAttribute("android:paddingBottom", timBarItem.getValue(id)) + } + } + + if (id.equals("@id/youtube_controls_fullscreen_button_stub")) { + node.setAttribute("android:layout_width", layoutHeightWidth) + } + } + } + + if (changeTopButtons == true) { + // Apply the selected icon type to the top buttons. + arrayOf( + "xxxhdpi", + "xxhdpi", + "xhdpi", + "hdpi", + "mdpi" + ).forEach { dpi -> + copyResources( + "youtube/overlaybuttons/$iconType", + ResourceGroup( + "drawable-$dpi", + "yt_outline_gear_white_24.png", + "yt_outline_chevron_down_white_24.png", + "quantum_ic_closed_caption_off_grey600_24.png", + "quantum_ic_closed_caption_off_white_24.png", + "quantum_ic_closed_caption_white_24.png" + ) + ) + } + } + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: PLAYER", + "PREFERENCE_SCREENS: PLAYER_BUTTONS", + "SETTINGS: OVERLAY_BUTTONS" + ), + OVERLAY_BUTTONS + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/Fingerprints.kt new file mode 100644 index 000000000..cbfe4ca31 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/Fingerprints.kt @@ -0,0 +1,118 @@ +package app.revanced.patches.youtube.player.seekbar + +import app.revanced.patches.youtube.utils.resourceid.reelTimeBarPlayedColor +import app.revanced.util.containsLiteralInstruction +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal const val PLAYER_SEEKBAR_GRADIENT_FEATURE_FLAG = 45617850L + +internal val playerSeekbarGradientConfigFingerprint = legacyFingerprint( + name = "playerSeekbarGradientConfigFingerprint", + returnType = "Z", + parameters = emptyList(), + literals = listOf(PLAYER_SEEKBAR_GRADIENT_FEATURE_FLAG), +) + +internal val lithoLinearGradientFingerprint = legacyFingerprint( + name = "lithoLinearGradientFingerprint", + accessFlags = AccessFlags.STATIC.value, + returnType = "Landroid/graphics/LinearGradient;", + parameters = listOf("F", "F", "F", "F", "[I", "[F") +) +internal const val launchScreenLayoutTypeLotteFeatureFlag = 268507948L + +internal val launchScreenLayoutTypeFingerprint = legacyFingerprint( + name = "launchScreenLayoutTypeFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + returnType = "V", + customFingerprint = { method, _ -> + val firstParameter = method.parameterTypes.firstOrNull() + // 19.25 - 19.45 + (firstParameter == "Lcom/google/android/apps/youtube/app/watchwhile/MainActivity;" + || firstParameter == "Landroid/app/Activity;") // 19.46+ + && method.containsLiteralInstruction(launchScreenLayoutTypeLotteFeatureFlag) + } +) + +internal val cairoSeekbarConfigFingerprint = legacyFingerprint( + name = "cairoSeekbarConfigFingerprint", + returnType = "Z", + parameters = emptyList(), + literals = listOf(45617850L), +) + +internal val controlsOverlayStyleFingerprint = legacyFingerprint( + name = "controlsOverlayStyleFingerprint", + opcodes = listOf(Opcode.CONST_HIGH16), + strings = listOf("YOUTUBE", "PREROLL", "POSTROLL"), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/ControlsOverlayStyle;") + } +) + +internal val seekbarTappingFingerprint = legacyFingerprint( + name = "seekbarTappingFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.IPUT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ, + Opcode.INVOKE_VIRTUAL, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ, + Opcode.INT_TO_FLOAT, + Opcode.INT_TO_FLOAT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ + ), + customFingerprint = { method, _ -> method.name == "onTouchEvent" } +) + +internal val seekbarThumbnailsQualityFingerprint = legacyFingerprint( + name = "seekbarThumbnailsQualityFingerprint", + returnType = "Z", + parameters = emptyList(), + literals = listOf(45399684L), +) + +internal val shortsSeekbarColorFingerprint = legacyFingerprint( + name = "shortsSeekbarColorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf(reelTimeBarPlayedColor), +) + +internal val thumbnailPreviewConfigFingerprint = legacyFingerprint( + name = "thumbnailPreviewConfigFingerprint", + returnType = "Z", + parameters = emptyList(), + literals = listOf(45398577L), +) + +internal val timeCounterFingerprint = legacyFingerprint( + name = "timeCounterFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + returnType = "V", + opcodes = listOf( + Opcode.SUB_LONG_2ADDR, + Opcode.IGET_WIDE, + Opcode.SUB_LONG_2ADDR + ) +) + +internal val timelineMarkerArrayFingerprint = legacyFingerprint( + name = "timelineMarkerArrayFingerprint", + returnType = "[Lcom/google/android/libraries/youtube/player/features/overlay/timebar/TimelineMarker;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt new file mode 100644 index 000000000..7ff457117 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt @@ -0,0 +1,520 @@ +package app.revanced.patches.youtube.player.seekbar + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.drawable.addDrawableColorHook +import app.revanced.patches.shared.drawable.drawableColorHookPatch +import app.revanced.patches.shared.mainactivity.onCreateMethod +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_PATH +import app.revanced.patches.youtube.utils.flyoutmenu.flyoutMenuHookPatch +import app.revanced.patches.youtube.utils.mainactivity.mainActivityResolvePatch +import app.revanced.patches.youtube.utils.patch.PatchList.SEEKBAR_COMPONENTS +import app.revanced.patches.youtube.utils.playerButtonsResourcesFingerprint +import app.revanced.patches.youtube.utils.playerButtonsVisibilityFingerprint +import app.revanced.patches.youtube.utils.playerSeekbarColorFingerprint +import app.revanced.patches.youtube.utils.playservice.is_19_23_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_25_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_46_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.resourceid.inlineTimeBarColorizedBarPlayedColorDark +import app.revanced.patches.youtube.utils.resourceid.inlineTimeBarPlayedNotHighlightedColor +import app.revanced.patches.youtube.utils.resourceid.reelTimeBarPlayedColor +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.seekbarFingerprint +import app.revanced.patches.youtube.utils.seekbarOnDrawFingerprint +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.ResourceUtils.getContext +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.patches.youtube.utils.totalTimeFingerprint +import app.revanced.patches.youtube.video.information.videoInformationPatch +import app.revanced.util.* +import app.revanced.util.Utils.printWarn +import app.revanced.util.findElementByAttributeValueOrThrow +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.resolvable +import app.revanced.util.inputStreamFromBundledResource +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import org.w3c.dom.Element +import java.io.ByteArrayInputStream + +internal const val splashSeekbarColorAttributeName = "splash_custom_seekbar_color" + +/** + * Generate a style xml with all combinations of 9-bit colors. + */ +private fun create9BitSeekbarColorStyles(): String = StringBuilder().apply { + append("") + append("\n") + + for (red in 0..7) { + for (green in 0..7) { + for (blue in 0..7) { + val name = "${red}_${green}_${blue}" + + fun roundTo3BitHex(channel8Bits: Int) = + (channel8Bits * 255 / 7).toString(16).padStart(2, '0') + + val r = roundTo3BitHex(red) + val g = roundTo3BitHex(green) + val b = roundTo3BitHex(blue) + val color = "#ff$r$g$b" + + append( + """ + + """ + ) + } + } + } + + append("") +}.toString() + +private const val EXTENSION_SEEKBAR_COLOR_CLASS_DESCRIPTOR = + "$PLAYER_PATH/SeekbarColorPatch;" + +@Suppress("unused") +val seekbarComponentsPatch = bytecodePatch( + SEEKBAR_COMPONENTS.title, + SEEKBAR_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + drawableColorHookPatch, + flyoutMenuHookPatch, + mainActivityResolvePatch, + sharedResourceIdPatch, + settingsPatch, + videoInformationPatch, + versionCheckPatch, + ) + + val cairoStartColor by stringOption( + key = "cairoStartColor", + default = "#FFFF2791", + title = "Cairo start color", + description = "Set Cairo start color for the seekbar." + ) + + val cairoEndColor by stringOption( + key = "cairoEndColor", + default = "#FFFF0033", + title = "Cairo end color", + description = "Set Cairo end color for the seekbar." + ) + + execute { + + var settingArray = arrayOf( + "PREFERENCE_SCREEN: PLAYER", + "SETTINGS: SEEKBAR_COMPONENTS" + ) + + // region patch for enable seekbar tapping patch + + seekbarTappingFingerprint.matchOrThrow().let { + it.method.apply { + val tapSeekIndex = it.patternMatch!!.startIndex + 1 + val tapSeekClass = getInstruction(tapSeekIndex) + .getReference()!! + .definingClass + + val tapSeekMethods = findMethodsOrThrow(tapSeekClass) + var pMethodCall = "" + var oMethodCall = "" + + for (method in tapSeekMethods) { + if (method.implementation == null) + continue + + val instructions = method.implementation!!.instructions + // here we make sure we actually find the method because it has more than 7 instructions + if (instructions.count() != 10) + continue + + // we know that the 7th instruction has the opcode CONST_4 + val instruction = instructions.elementAt(6) + if (instruction.opcode != Opcode.CONST_4) + continue + + // the literal for this instruction has to be either 1 or 2 + val literal = (instruction as NarrowLiteralInstruction).narrowLiteral + + // method founds + if (literal == 1) + pMethodCall = "${method.definingClass}->${method.name}(I)V" + else if (literal == 2) + oMethodCall = "${method.definingClass}->${method.name}(I)V" + } + + if (pMethodCall.isEmpty()) { + throw PatchException("pMethod not found") + } + if (oMethodCall.isEmpty()) { + throw PatchException("oMethod not found") + } + + val insertIndex = it.patternMatch!!.startIndex + 2 + + addInstructionsWithLabels( + insertIndex, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->enableSeekbarTapping()Z + move-result v0 + if-eqz v0, :disabled + invoke-virtual { p0, v2 }, $pMethodCall + invoke-virtual { p0, v2 }, $oMethodCall + """, ExternalLabel("disabled", getInstruction(insertIndex)) + ) + } + } + + // endregion + + // region patch for append time stamps information + + totalTimeFingerprint.methodOrThrow().apply { + val charSequenceIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "getString" + } + 1 + val charSequenceRegister = + getInstruction(charSequenceIndex).registerA + val textViewIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "getText" + } + val textViewRegister = + getInstruction(textViewIndex).registerC + + addInstructions( + textViewIndex, """ + invoke-static {v$textViewRegister}, $PLAYER_CLASS_DESCRIPTOR->setContainerClickListener(Landroid/view/View;)V + invoke-static {v$charSequenceRegister}, $PLAYER_CLASS_DESCRIPTOR->appendTimeStampInformation(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$charSequenceRegister + """ + ) + } + + // endregion + + // region patch for seekbar color + + fun MutableMethod.addColorChangeInstructions(literal: Long) { + val insertIndex = indexOfFirstLiteralInstructionOrThrow(literal) + 2 + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex + 1, """ + invoke-static {v$insertRegister}, $EXTENSION_SEEKBAR_COLOR_CLASS_DESCRIPTOR->getVideoPlayerSeekbarColor(I)I + move-result v$insertRegister + """ + ) + } + + playerSeekbarColorFingerprint.methodOrThrow().apply { + addColorChangeInstructions(inlineTimeBarColorizedBarPlayedColorDark) + addColorChangeInstructions(inlineTimeBarPlayedNotHighlightedColor) + } + + shortsSeekbarColorFingerprint.methodOrThrow().apply { + addColorChangeInstructions(reelTimeBarPlayedColor) + } + + controlsOverlayStyleFingerprint.matchOrThrow().let { + val walkerMethod = + it.getWalkerMethod(it.patternMatch!!.startIndex + 1) + walkerMethod.apply { + val colorRegister = getInstruction(0).registerA + + addInstructions( + 0, """ + invoke-static {v$colorRegister}, $EXTENSION_SEEKBAR_COLOR_CLASS_DESCRIPTOR->getVideoPlayerSeekbarClickedColor(I)I + move-result v$colorRegister + """ + ) + } + } + + addDrawableColorHook("$EXTENSION_SEEKBAR_COLOR_CLASS_DESCRIPTOR->getLithoColor(I)I") + + if (is_19_25_or_greater) { + playerSeekbarGradientConfigFingerprint.injectLiteralInstructionBooleanCall( + PLAYER_SEEKBAR_GRADIENT_FEATURE_FLAG, + "$EXTENSION_SEEKBAR_COLOR_CLASS_DESCRIPTOR->playerSeekbarGradientEnabled(Z)Z" + ) + + lithoLinearGradientFingerprint.methodOrThrow().addInstruction( + 0, + "invoke-static/range { p4 .. p5 }, $EXTENSION_SEEKBAR_COLOR_CLASS_DESCRIPTOR->setLinearGradient([I[F)V" + ) + + // Don't use the lotte splash screen layout if using custom seekbar. + arrayOf( + launchScreenLayoutTypeFingerprint.methodOrThrow(), + onCreateMethod + ).forEach { method -> + method.apply { + val literalIndex = + indexOfFirstLiteralInstructionOrThrow(launchScreenLayoutTypeLotteFeatureFlag) + val resultIndex = + indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT) + val register = getInstruction(resultIndex).registerA + + addInstructions( + resultIndex + 1, + """ + invoke-static { v$register }, $EXTENSION_SEEKBAR_COLOR_CLASS_DESCRIPTOR->useLotteLaunchSplashScreen(Z)Z + move-result v$register + """ + ) + } + } + + // Hook the splash animation drawable to set the a seekbar color theme. + onCreateMethod.apply { + val drawableIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + reference?.definingClass == "Landroid/widget/ImageView;" && + reference.name == "getDrawable" + } + val checkCastIndex = + indexOfFirstInstructionOrThrow(drawableIndex, Opcode.CHECK_CAST) + val drawableRegister = + getInstruction(checkCastIndex).registerA + + addInstruction( + checkCastIndex + 1, + "invoke-static { v$drawableRegister }, $EXTENSION_SEEKBAR_COLOR_CLASS_DESCRIPTOR->" + + "setSplashAnimationDrawableTheme(Landroid/graphics/drawable/AnimatedVectorDrawable;)V" + ) + } + + } + + val context = getContext() + + context.document("res/drawable/resume_playback_progressbar_drawable.xml") + .use { document -> + val layerList = document.getElementsByTagName("layer-list").item(0) as Element + val progressNode = layerList.getElementsByTagName("item").item(1) as Element + if (!progressNode.getAttributeNode("android:id").value.endsWith("progress")) { + throw PatchException("Could not find progress bar") + } + val scaleNode = progressNode.getElementsByTagName("scale").item(0) as Element + val shapeNode = scaleNode.getElementsByTagName("shape").item(0) as Element + val replacementNode = document.createElement( + "app.revanced.extension.youtube.patches.utils.ProgressBarDrawable" + ) + scaleNode.replaceChild(replacementNode, shapeNode) + } + + context.document("res/values/colors.xml").use { document -> + document.doRecursively loop@{ node -> + if (node is Element && node.tagName == "color") { + if (node.getAttribute("name") == "yt_youtube_red_cairo") { + node.textContent = cairoStartColor + } + if (node.getAttribute("name") == "yt_youtube_magenta") { + node.textContent = cairoEndColor + } + } + } + } + + if (is_19_25_or_greater) { + // Add attribute and styles for splash screen custom color. + // Using a style is the only way to selectively change just the seekbar fill color. + // + // Because the style colors must be hard coded for all color possibilities, + // instead of allowing 24 bit color the style is restricted to 9-bit (3 bits per color channel) + // and the style color closest to the users custom color is used for the splash screen. + arrayOf( + inputStreamFromBundledResource( + "youtube/seekbar/values", + "attrs.xml" + )!! to "res/values/attrs.xml", + ByteArrayInputStream(create9BitSeekbarColorStyles().toByteArray()) to "res/values/styles.xml" + ).forEach { (source, destination) -> + "resources".copyXmlNode( + context.document(source), + context.document(destination), + ).close() + } + + fun setSplashDrawablePathFillColor( + xmlFileNames: Iterable, + vararg resourceNames: String + ) { + xmlFileNames.forEach { xmlFileName -> + context.document(xmlFileName).use { document -> + resourceNames.forEach { elementId -> + val element = document.childNodes.findElementByAttributeValueOrThrow( + "android:name", + elementId + ) + + val attribute = "android:fillColor" + if (!element.hasAttribute(attribute)) { + throw PatchException("Could not find $attribute for $elementId") + } + + element.setAttribute( + attribute, + "?attr/$splashSeekbarColorAttributeName" + ) + } + } + } + } + + setSplashDrawablePathFillColor( + listOf( + "res/drawable/\$startup_animation_light__0.xml", + "res/drawable/\$startup_animation_dark__0.xml" + ), + "_R_G_L_10_G_D_0_P_0" + ) + + if (!is_19_46_or_greater) { + // Resources removed in 19.46+ + setSplashDrawablePathFillColor( + listOf( + "res/drawable/\$buenos_aires_animation_light__0.xml", + "res/drawable/\$buenos_aires_animation_dark__0.xml" + ), + "_R_G_L_8_G_D_0_P_0" + ) + } + } + + // endregion + + // region patch for high quality thumbnails + + // TODO: This will be added when support for newer YouTube versions is added. + // seekbarThumbnailsQualityFingerprint.injectLiteralInstructionBooleanCall( + // 45399684L, + // "$PLAYER_CLASS_DESCRIPTOR->enableHighQualityFullscreenThumbnails()Z" + // ) + + // endregion + + // region patch for hide chapter + + timelineMarkerArrayFingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->disableSeekbarChapters()Z + move-result v0 + if-eqz v0, :show + const/4 v0, 0x0 + new-array v0, v0, [Lcom/google/android/libraries/youtube/player/features/overlay/timebar/TimelineMarker; + return-object v0 + """, ExternalLabel("show", getInstruction(0)) + ) + } + + playerButtonsVisibilityFingerprint.methodOrThrow(playerButtonsResourcesFingerprint).apply { + val freeRegister = implementation!!.registerCount - parameters.size - 2 + val viewIndex = indexOfFirstInstructionOrThrow(Opcode.INVOKE_INTERFACE) + val viewRegister = getInstruction(viewIndex).registerD + + addInstructionsWithLabels( + viewIndex, """ + invoke-static {v$viewRegister}, $PLAYER_CLASS_DESCRIPTOR->hideSeekbarChapterLabel(Landroid/view/View;)Z + move-result v$freeRegister + if-eqz v$freeRegister, :ignore + return-void + """, ExternalLabel("ignore", getInstruction(viewIndex)) + ) + } + + // endregion + + // region patch for hide seekbar + + seekbarOnDrawFingerprint.methodOrThrow(seekbarFingerprint).apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->hideSeekbar()Z + move-result v0 + if-eqz v0, :show + return-void + """, ExternalLabel("show", getInstruction(0)) + ) + } + + // endregion + + // region patch for hide time stamp + + timeCounterFingerprint.methodOrThrow(playerSeekbarColorFingerprint).apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->hideTimeStamp()Z + move-result v0 + if-eqz v0, :show + return-void + """, ExternalLabel("show", getInstruction(0)) + ) + } + + // endregion + + // region patch for restore old seekbar thumbnails + + if (thumbnailPreviewConfigFingerprint.resolvable()) { + thumbnailPreviewConfigFingerprint.injectLiteralInstructionBooleanCall( + 45398577L, + "$PLAYER_CLASS_DESCRIPTOR->restoreOldSeekbarThumbnails()Z" + ) + + settingArray += "SETTINGS: RESTORE_OLD_SEEKBAR_THUMBNAILS" + + updatePatchStatus(PATCH_STATUS_CLASS_DESCRIPTOR, "OldSeekbarThumbnailsDefaultBoolean") + } else { + printWarn("\"Restore old seekbar thumbnails\" is not supported in this version. Use YouTube 19.16.39 or earlier.") + } + + // endregion + + // region patch for enable cairo seekbar + + if (is_19_23_or_greater) { + cairoSeekbarConfigFingerprint.injectLiteralInstructionBooleanCall( + 45617850L, + "$PLAYER_CLASS_DESCRIPTOR->enableCairoSeekbar()Z" + ) + + settingArray += "SETTINGS: ENABLE_CAIRO_SEEKBAR" + } + + // endregion + + // region add settings + + addPreference(settingArray, SEEKBAR_COMPONENTS) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/Fingerprints.kt new file mode 100644 index 000000000..3ebc3e3db --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/Fingerprints.kt @@ -0,0 +1,189 @@ +package app.revanced.patches.youtube.shorts.components + +import app.revanced.patches.youtube.utils.resourceid.badgeLabel +import app.revanced.patches.youtube.utils.resourceid.metaPanel +import app.revanced.patches.youtube.utils.resourceid.reelDynRemix +import app.revanced.patches.youtube.utils.resourceid.reelDynShare +import app.revanced.patches.youtube.utils.resourceid.reelFeedbackLike +import app.revanced.patches.youtube.utils.resourceid.reelFeedbackPause +import app.revanced.patches.youtube.utils.resourceid.reelFeedbackPlay +import app.revanced.patches.youtube.utils.resourceid.reelForcedMuteButton +import app.revanced.patches.youtube.utils.resourceid.reelPlayerFooter +import app.revanced.patches.youtube.utils.resourceid.reelRightDislikeIcon +import app.revanced.patches.youtube.utils.resourceid.reelRightLikeIcon +import app.revanced.patches.youtube.utils.resourceid.reelVodTimeStampsContainer +import app.revanced.patches.youtube.utils.resourceid.rightComment +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val bottomSheetMenuListBuilderFingerprint = legacyFingerprint( + name = "bottomSheetMenuListBuilderFingerprint", + returnType = "L", + accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ, + ), + strings = listOf("Bottom Sheet Menu is empty. No menu items were supported."), +) + +internal val liveHeaderElementsContainerFingerprint = legacyFingerprint( + name = "liveHeaderElementsContainerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/ViewGroup;", "L"), + strings = listOf("Header container is null, header cannot be presented."), + customFingerprint = { method, _ -> + indexOfAddLiveHeaderElementsContainerInstruction(method) >= 0 + }, +) + +fun indexOfAddLiveHeaderElementsContainerInstruction(method: Method) = + method.indexOfFirstInstruction { + getReference()?.name == "addView" + } + +internal val reelEnumConstructorFingerprint = legacyFingerprint( + name = "reelEnumConstructorFingerprint", + returnType = "V", + strings = listOf( + "REEL_LOOP_BEHAVIOR_UNKNOWN", + "REEL_LOOP_BEHAVIOR_SINGLE_PLAY", + "REEL_LOOP_BEHAVIOR_REPEAT", + "REEL_LOOP_BEHAVIOR_END_SCREEN" + ) +) + +internal val reelEnumStaticFingerprint = legacyFingerprint( + name = "reelEnumStaticFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("I"), + returnType = "L" +) + +internal val reelFeedbackFingerprint = legacyFingerprint( + name = "reelFeedbackFingerprint", + returnType = "V", + literals = listOf(reelFeedbackLike, reelFeedbackPause, reelFeedbackPlay), +) + +internal val shortsButtonFingerprint = legacyFingerprint( + name = "shortsButtonFingerprint", + returnType = "V", + literals = listOf( + reelDynRemix, + reelDynShare, + reelRightDislikeIcon, + reelRightLikeIcon, + rightComment + ), +) + +/** + * The method by which patches are applied is different between the minimum supported version and the maximum supported version. + * There are two classes where R.id.badge_label[badgeLabel] is used, + * but due to the structure of ReVanced Patcher, the patch is applied to the method found first. + */ +internal val shortsPaidPromotionFingerprint = legacyFingerprint( + name = "shortsPaidPromotionFingerprint", + literals = listOf(badgeLabel), +) + +internal val shortsPausedHeaderFingerprint = legacyFingerprint( + name = "shortsPausedHeaderFingerprint", + returnType = "Landroid/view/View;", + opcodes = listOf( + Opcode.IF_NEZ, + Opcode.IGET_OBJECT, + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT + ), + strings = listOf("r_pfcv") +) + +internal val shortsPivotLegacyFingerprint = legacyFingerprint( + name = "shortsPivotLegacyFingerprint", + returnType = "V", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + parameters = listOf("Z", "Z", "L"), + literals = listOf(reelForcedMuteButton), +) + +internal val shortsSubscriptionsTabletFingerprint = legacyFingerprint( + name = "shortsSubscriptionsTabletFingerprint", + returnType = "V", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + parameters = listOf("L", "L", "Z"), + opcodes = listOf( + Opcode.INVOKE_STATIC, + Opcode.IGET, + Opcode.IF_EQZ + ) +) + +internal val shortsSubscriptionsTabletParentFingerprint = legacyFingerprint( + name = "shortsSubscriptionsTabletParentFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(reelPlayerFooter), +) + +internal val shortsTimeStampConstructorFingerprint = legacyFingerprint( + name = "shortsTimeStampConstructorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf(reelVodTimeStampsContainer), +) + +internal val shortsTimeStampMetaPanelFingerprint = legacyFingerprint( + name = "shortsTimeStampMetaPanelFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(metaPanel), +) + +internal val shortsTimeStampPrimaryFingerprint = legacyFingerprint( + name = "shortsTimeStampPrimaryFingerprint", + returnType = "I", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("I"), + literals = listOf(45627350L, 45638282L, 10002L), +) + +internal val shortsTimeStampSecondaryFingerprint = legacyFingerprint( + name = "shortsTimeStampSecondaryFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(45638187L), +) + +internal val shortsToolBarFingerprint = legacyFingerprint( + name = "shortsToolBarFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + opcodes = listOf(Opcode.IPUT_BOOLEAN), + strings = listOf("Null topBarButtons"), + customFingerprint = { method, _ -> + method.parameterTypes.firstOrNull() == "Z" + } +) + +internal const val FULLSCREEN_FEATURE_FLAG = 45398938L + +internal val shortsFullscreenFeatureFingerprint = legacyFingerprint( + name = "shortsFullscreenFeatureFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(FULLSCREEN_FEATURE_FLAG), +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsComponentPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsComponentPatch.kt new file mode 100644 index 000000000..76363b6df --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsComponentPatch.kt @@ -0,0 +1,888 @@ +package app.revanced.patches.youtube.shorts.components + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.shared.mainactivity.injectOnCreateMethodCall +import app.revanced.patches.shared.textcomponent.hookSpannableString +import app.revanced.patches.shared.textcomponent.textComponentPatch +import app.revanced.patches.youtube.utils.bottomSheetMenuItemBuilderFingerprint +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.youtube.utils.extension.Constants.SHORTS_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.SHORTS_PATH +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.youtube.utils.indexOfSpannedCharSequenceInstruction +import app.revanced.patches.youtube.utils.lottie.LOTTIE_ANIMATION_VIEW_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.lottie.lottieAnimationViewHookPatch +import app.revanced.patches.youtube.utils.mainactivity.mainActivityResolvePatch +import app.revanced.patches.youtube.utils.navigation.addBottomBarContainerHook +import app.revanced.patches.youtube.utils.navigation.navigationBarHookPatch +import app.revanced.patches.youtube.utils.patch.PatchList.HIDE_FEED_FLYOUT_MENU +import app.revanced.patches.youtube.utils.patch.PatchList.SHORTS_COMPONENTS +import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.utils.playservice.is_18_31_or_greater +import app.revanced.patches.youtube.utils.playservice.is_18_34_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_02_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_25_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_28_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_34_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.recyclerview.recyclerViewTreeObserverHook +import app.revanced.patches.youtube.utils.recyclerview.recyclerViewTreeObserverPatch +import app.revanced.patches.youtube.utils.resourceid.bottomBarContainer +import app.revanced.patches.youtube.utils.resourceid.metaPanel +import app.revanced.patches.youtube.utils.resourceid.reelDynRemix +import app.revanced.patches.youtube.utils.resourceid.reelDynShare +import app.revanced.patches.youtube.utils.resourceid.reelFeedbackLike +import app.revanced.patches.youtube.utils.resourceid.reelFeedbackPause +import app.revanced.patches.youtube.utils.resourceid.reelFeedbackPlay +import app.revanced.patches.youtube.utils.resourceid.reelForcedMuteButton +import app.revanced.patches.youtube.utils.resourceid.reelPlayerFooter +import app.revanced.patches.youtube.utils.resourceid.reelPlayerRightPivotV2Size +import app.revanced.patches.youtube.utils.resourceid.reelRightDislikeIcon +import app.revanced.patches.youtube.utils.resourceid.reelRightLikeIcon +import app.revanced.patches.youtube.utils.resourceid.reelVodTimeStampsContainer +import app.revanced.patches.youtube.utils.resourceid.rightComment +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.ResourceUtils.getContext +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.patches.youtube.utils.toolbar.hookToolBar +import app.revanced.patches.youtube.utils.toolbar.toolBarHookPatch +import app.revanced.patches.youtube.video.information.hookShortsVideoInformation +import app.revanced.patches.youtube.video.information.videoInformationPatch +import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId +import app.revanced.patches.youtube.video.videoid.videoIdPatch +import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT +import app.revanced.util.ResourceGroup +import app.revanced.util.cloneMutable +import app.revanced.util.copyResources +import app.revanced.util.findMethodOrThrow +import app.revanced.util.findMutableMethodOf +import app.revanced.util.fingerprint.definingClassOrThrow +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.resolvable +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstLiteralInstruction +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import app.revanced.util.indexOfFirstStringInstruction +import app.revanced.util.indexOfFirstStringInstructionOrThrow +import app.revanced.util.injectLiteralInstructionViewCall +import app.revanced.util.or +import app.revanced.util.replaceLiteralInstructionCall +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.RegisterRangeInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.util.MethodUtil + +private const val EXTENSION_ANIMATION_FEEDBACK_CLASS_DESCRIPTOR = + "$SHORTS_PATH/AnimationFeedbackPatch;" + +private val shortsAnimationPatch = bytecodePatch( + description = "shortsAnimationPatch" +) { + dependsOn( + lottieAnimationViewHookPatch, + settingsPatch, + ) + + execute { + reelFeedbackFingerprint.methodOrThrow().apply { + mapOf( + reelFeedbackLike to "setShortsLikeFeedback", + reelFeedbackPause to "setShortsPauseFeedback", + reelFeedbackPlay to "setShortsPlayFeedback", + ).forEach { (literal, methodName) -> + val literalIndex = indexOfFirstLiteralInstructionOrThrow(literal) + val viewIndex = indexOfFirstInstructionOrThrow(literalIndex) { + opcode == Opcode.CHECK_CAST && + (this as? ReferenceInstruction)?.reference?.toString() == LOTTIE_ANIMATION_VIEW_CLASS_DESCRIPTOR + } + val viewRegister = getInstruction(viewIndex).registerA + val methodCall = "invoke-static {v$viewRegister}, " + + EXTENSION_ANIMATION_FEEDBACK_CLASS_DESCRIPTOR + + "->" + + methodName + + "($LOTTIE_ANIMATION_VIEW_CLASS_DESCRIPTOR)V" + + addInstruction( + viewIndex + 1, + methodCall + ) + } + } + + getContext().copyResources( + "youtube/shorts/feedback", + ResourceGroup( + "raw", + "like_tap_feedback_cairo.json", + "like_tap_feedback_heart.json", + "like_tap_feedback_heart_tint.json", + "like_tap_feedback_hidden.json", + "pause_tap_feedback_hidden.json", + "play_tap_feedback_hidden.json" + ) + ) + } +} + +private const val SHORTS_PLAYER_FLYOUT_MENU_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/ShortsCustomActionsFilter;" +private const val EXTENSION_CUSTOM_ACTIONS_CLASS_DESCRIPTOR = + "$SHORTS_PATH/CustomActionsPatch;" + +private val shortsCustomActionsPatch = bytecodePatch( + description = "shortsCustomActionsPatch" +) { + dependsOn( + lithoFilterPatch, + playerTypeHookPatch, + recyclerViewTreeObserverPatch, + toolBarHookPatch, + videoIdPatch, + videoInformationPatch, + versionCheckPatch, + ) + + execute { + if (!is_18_34_or_greater) { + return@execute + } + + // region hook toolbar more button + + hookToolBar("$EXTENSION_CUSTOM_ACTIONS_CLASS_DESCRIPTOR->setToolbarMenu") + + // toolbar in Shorts livestream + liveHeaderElementsContainerFingerprint.methodOrThrow().apply { + val addViewIndex = indexOfAddLiveHeaderElementsContainerInstruction(this) + val viewRegister = getInstruction(addViewIndex).registerD + + addInstruction( + addViewIndex + 1, + "invoke-static {v$viewRegister}, " + + "$EXTENSION_CUSTOM_ACTIONS_CLASS_DESCRIPTOR->onLiveHeaderElementsContainerCreate(Landroid/view/View;)V" + ) + } + + // endregion + + // region add litho filter + + hookPlayerResponseVideoId("$SHORTS_PLAYER_FLYOUT_MENU_FILTER_CLASS_DESCRIPTOR->newPlayerResponseVideoId(Ljava/lang/String;Z)V") + hookShortsVideoInformation("$SHORTS_PLAYER_FLYOUT_MENU_FILTER_CLASS_DESCRIPTOR->newShortsVideoStarted(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") + + addLithoFilter(SHORTS_PLAYER_FLYOUT_MENU_FILTER_CLASS_DESCRIPTOR) + + // endregion + + if (!is_19_02_or_greater) { + return@execute + } + + // region hook flyout menu + + bottomSheetMenuListBuilderFingerprint.matchOrThrow().let { + it.method.apply { + val addListIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "add" + } + val addListReference = getInstruction(addListIndex).reference + + val getObjectIndex = indexOfFirstInstructionReversedOrThrow(addListIndex) { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.returnType == "Ljava/lang/Object;" + } + val getObjectReference = + getInstruction(getObjectIndex).reference as MethodReference + + val bottomSheetMenuInitializeIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + opcode == Opcode.INVOKE_STATIC_RANGE && + reference?.returnType == "V" && + reference.parameterTypes[1] == "Ljava/lang/Object;" + } + val bottomSheetMenuObjectRegister = + getInstruction(bottomSheetMenuInitializeIndex).startRegister + val bottomSheetMenuObject = + (getInstruction(bottomSheetMenuInitializeIndex).reference as MethodReference).parameterTypes[0]!! + + val bottomSheetMenuListIndex = it.patternMatch!!.startIndex + val bottomSheetMenuListField = + (getInstruction(bottomSheetMenuListIndex).reference as FieldReference) + + val bottomSheetMenuClass = bottomSheetMenuListField.definingClass + val bottomSheetMenuList = bottomSheetMenuListField.type + + val bottomSheetMenuClassRegister = + getInstruction(bottomSheetMenuListIndex).registerB + val bottomSheetMenuListRegister = + getInstruction(bottomSheetMenuListIndex).registerA + + addInstruction( + bottomSheetMenuListIndex + 1, + "invoke-static {v$bottomSheetMenuClassRegister, v$bottomSheetMenuListRegister}, " + + "$EXTENSION_CUSTOM_ACTIONS_CLASS_DESCRIPTOR->addFlyoutMenu(Ljava/lang/Object;Ljava/lang/Object;)V" + ) + + addInstruction( + bottomSheetMenuInitializeIndex + 1, + "invoke-static {v$bottomSheetMenuObjectRegister}, " + + "$EXTENSION_CUSTOM_ACTIONS_CLASS_DESCRIPTOR->setFlyoutMenuObject(Ljava/lang/Object;)V" + ) + + val addFlyoutMenuMethod = + findMethodOrThrow(EXTENSION_CUSTOM_ACTIONS_CLASS_DESCRIPTOR) { + name == "addFlyoutMenu" && + accessFlags == AccessFlags.PRIVATE or AccessFlags.STATIC + } + + val customActionClass = with(addFlyoutMenuMethod) { + val thirdParameter = parameters[2] + + addInstructions( + 3, """ + check-cast p0, $bottomSheetMenuClass + check-cast v0, $bottomSheetMenuObject + invoke-virtual {p0, v0, p2}, $bottomSheetMenuClass->buildFlyoutMenu(${bottomSheetMenuObject}${thirdParameter})${getObjectReference.definingClass} + move-result-object v0 + invoke-virtual {v0}, $getObjectReference + move-result-object v0 + check-cast p1, $bottomSheetMenuList + invoke-virtual {p1, v0}, $addListReference + return-void + """ + ) + + thirdParameter + } + + val bottomSheetMenuItemBuilderMethod = bottomSheetMenuItemBuilderFingerprint + .methodOrThrow() + + val newParameter = + bottomSheetMenuItemBuilderMethod.parameters + listOf(customActionClass) + + it.classDef.methods.add( + bottomSheetMenuItemBuilderMethod + .cloneMutable( + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + name = "buildFlyoutMenu", + registerCount = bottomSheetMenuItemBuilderMethod.implementation!!.registerCount + 1, + parameters = newParameter, + ).apply { + val drawableIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_DIRECT && + getReference()?.returnType == "Landroid/graphics/drawable/Drawable;" + } + val drawableRegister = + getInstruction(drawableIndex + 1).registerA + + addInstructions( + drawableIndex + 2, """ + invoke-virtual {p2}, $customActionClass->getDrawable()Landroid/graphics/drawable/Drawable; + move-result-object v$drawableRegister + """ + ) + + val charSequenceIndex = indexOfSpannedCharSequenceInstruction(this) + val charSequenceRegister = + getInstruction(charSequenceIndex + 1).registerA + + val insertIndex = charSequenceIndex + 2 + + if (HIDE_FEED_FLYOUT_MENU.included == true) + removeInstructions(insertIndex, 2) + + addInstructions( + insertIndex, """ + invoke-virtual {p2}, $customActionClass->getLabel()Ljava/lang/String; + move-result-object v$charSequenceRegister + """ + ) + } + ) + } + } + + recyclerViewTreeObserverHook("$EXTENSION_CUSTOM_ACTIONS_CLASS_DESCRIPTOR->onFlyoutMenuCreate(Landroid/support/v7/widget/RecyclerView;)V") + + // endregion + + } +} + +private val shortsNavigationBarPatch = bytecodePatch( + description = "shortsNavigationBarPatch" +) { + dependsOn( + navigationBarHookPatch, + playerTypeHookPatch, + ) + + execute { + var count = 0 + classes.forEach { classDef -> + classDef.methods.filter { method -> + method.returnType == "V" && + method.accessFlags == AccessFlags.PUBLIC or AccessFlags.FINAL && + method.parameters == listOf("Landroid/view/View;", "Landroid/os/Bundle;") && + method.indexOfFirstStringInstruction("r_pfvc") >= 0 && + method.indexOfFirstLiteralInstruction(bottomBarContainer) >= 0 + }.forEach { method -> + proxy(classDef) + .mutableClass + .findMutableMethodOf(method).apply { + val constIndex = indexOfFirstLiteralInstruction(bottomBarContainer) + val targetIndex = indexOfFirstInstructionOrThrow(constIndex) { + getReference()?.name == "getHeight" + } + 1 + val heightRegister = + getInstruction(targetIndex).registerA + addInstructions( + targetIndex + 1, """ + invoke-static {v$heightRegister}, $SHORTS_CLASS_DESCRIPTOR->setNavigationBarHeight(I)I + move-result v$heightRegister + """ + ) + count++ + } + } + } + + if (count == 0) throw PatchException("shortsNavigationBarPatch failed") + + addBottomBarContainerHook("$SHORTS_CLASS_DESCRIPTOR->setNavigationBar(Landroid/view/View;)V") + } +} + +private const val EXTENSION_REPEAT_STATE_CLASS_DESCRIPTOR = + "$SHORTS_PATH/ShortsRepeatStatePatch;" + +private val shortsRepeatPatch = bytecodePatch( + description = "shortsRepeatPatch" +) { + execute { + dependsOn(mainActivityResolvePatch) + + injectOnCreateMethodCall( + EXTENSION_REPEAT_STATE_CLASS_DESCRIPTOR, + "setMainActivity" + ) + + val reelEnumClass = reelEnumConstructorFingerprint.definingClassOrThrow() + + reelEnumConstructorFingerprint.methodOrThrow().apply { + val insertIndex = indexOfFirstInstructionOrThrow(Opcode.RETURN_VOID) + + addInstructions( + insertIndex, + """ + # Pass the first enum value to extension. + # Any enum value of this type will work. + sget-object v0, $reelEnumClass->a:$reelEnumClass + invoke-static { v0 }, $EXTENSION_REPEAT_STATE_CLASS_DESCRIPTOR->setYTShortsRepeatEnum(Ljava/lang/Enum;)V + """, + ) + + val endScreenStringIndex = + indexOfFirstStringInstructionOrThrow("REEL_LOOP_BEHAVIOR_END_SCREEN") + val endScreenReferenceIndex = + indexOfFirstInstructionOrThrow(endScreenStringIndex, Opcode.SPUT_OBJECT) + val endScreenReference = + getInstruction(endScreenReferenceIndex).reference.toString() + + val enumMethod = reelEnumStaticFingerprint.methodOrThrow(reelEnumConstructorFingerprint) + + classes.forEach { classDef -> + classDef.methods.filter { method -> + method.parameters.size == 1 && + method.parameters[0].startsWith("L") && + method.returnType == "V" && + method.indexOfFirstInstruction { + getReference()?.toString() == endScreenReference + } >= 0 + }.forEach { targetMethod -> + proxy(classDef) + .mutableClass + .findMutableMethodOf(targetMethod) + .apply { + implementation!!.instructions + .withIndex() + .filter { (_, instruction) -> + val reference = + (instruction as? ReferenceInstruction)?.reference + reference is MethodReference && + MethodUtil.methodSignaturesMatch(enumMethod, reference) + } + .map { (index, _) -> index } + .reversed() + .forEach { index -> + val register = + getInstruction(index + 1).registerA + + addInstructions( + index + 2, """ + invoke-static {v$register}, $EXTENSION_REPEAT_STATE_CLASS_DESCRIPTOR->changeShortsRepeatBehavior(Ljava/lang/Enum;)Ljava/lang/Enum; + move-result-object v$register + """ + ) + } + } + } + } + } + } +} + +private val shortsTimeStampPatch = bytecodePatch( + description = "shortsTimeStampPatch" +) { + dependsOn(versionCheckPatch) + + execute { + + if (!is_19_25_or_greater || is_19_28_or_greater) return@execute + + // region patch for enable time stamp + + mapOf( + shortsTimeStampPrimaryFingerprint to 45627350L, + shortsTimeStampPrimaryFingerprint to 45638282L, + shortsTimeStampSecondaryFingerprint to 45638187L + ).forEach { (fingerprint, literal) -> + fingerprint.injectLiteralInstructionBooleanCall( + literal, + "$SHORTS_CLASS_DESCRIPTOR->enableShortsTimeStamp(Z)Z" + ) + } + + shortsTimeStampPrimaryFingerprint.methodOrThrow().apply { + val literalIndex = indexOfFirstLiteralInstructionOrThrow(10002L) + val literalRegister = getInstruction(literalIndex).registerA + + addInstructions( + literalIndex + 1, """ + invoke-static {v$literalRegister}, $SHORTS_CLASS_DESCRIPTOR->enableShortsTimeStamp(I)I + move-result v$literalRegister + """ + ) + } + + // endregion + + // region patch for timestamp long press action and meta panel bottom margin + + listOf( + Triple( + shortsTimeStampConstructorFingerprint.methodOrThrow(), + reelVodTimeStampsContainer, + "setShortsTimeStampChangeRepeatState" + ), + Triple( + shortsTimeStampMetaPanelFingerprint.methodOrThrow( + shortsTimeStampConstructorFingerprint + ), + metaPanel, + "setShortsMetaPanelBottomMargin" + ) + ).forEach { (method, literalValue, methodName) -> + val smaliInstruction = """ + invoke-static {v$REGISTER_TEMPLATE_REPLACEMENT}, $SHORTS_CLASS_DESCRIPTOR->$methodName(Landroid/view/View;)V + """ + + method.injectLiteralInstructionViewCall(literalValue, smaliInstruction) + } + } +} + +private val shortsToolBarPatch = bytecodePatch( + description = "shortsToolBarPatch" +) { + execute { + shortsToolBarFingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.patternMatch!!.startIndex + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $SHORTS_CLASS_DESCRIPTOR->hideShortsToolBar(Z)Z + move-result v$insertRegister + """ + ) + } + } + } +} + +private const val EXTENSION_RETURN_YOUTUBE_CHANNEL_NAME_CLASS_DESCRIPTOR = + "$UTILS_PATH/ReturnYouTubeChannelNamePatch;" + +private const val BUTTON_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/ShortsButtonFilter;" +private const val SHELF_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/ShortsShelfFilter;" +private const val RETURN_YOUTUBE_CHANNEL_NAME_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/ReturnYouTubeChannelNameFilterPatch;" + +@Suppress("unused") +val shortsComponentPatch = bytecodePatch( + SHORTS_COMPONENTS.title, + SHORTS_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + + shortsAnimationPatch, + shortsCustomActionsPatch, + shortsNavigationBarPatch, + shortsRepeatPatch, + shortsTimeStampPatch, + shortsToolBarPatch, + + lithoFilterPatch, + playerTypeHookPatch, + sharedResourceIdPatch, + textComponentPatch, + versionCheckPatch, + videoInformationPatch, + ) + + execute { + fun MutableMethod.hideButtons( + insertIndex: Int, + descriptor: String + ) { + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex + 1, """ + invoke-static {v$insertRegister}, $SHORTS_CLASS_DESCRIPTOR->$descriptor + move-result-object v$insertRegister + """ + ) + } + + fun Pair.hideButton( + id: Long, + descriptor: String, + reversed: Boolean + ) = + methodOrThrow().apply { + val constIndex = indexOfFirstLiteralInstructionOrThrow(id) + val insertIndex = if (reversed) + indexOfFirstInstructionReversedOrThrow(constIndex, Opcode.CHECK_CAST) + else + indexOfFirstInstructionOrThrow(constIndex, Opcode.CHECK_CAST) + val insertRegister = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex, + "invoke-static {v$insertRegister}, $SHORTS_CLASS_DESCRIPTOR->$descriptor(Landroid/view/View;)V" + ) + } + + fun Pair.hideButtons( + id: Long, + descriptor: String + ) = + methodOrThrow().apply { + val constIndex = indexOfFirstLiteralInstructionOrThrow(id) + val insertIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.CHECK_CAST) + + hideButtons(insertIndex, descriptor) + } + + var settingArray = arrayOf( + "PREFERENCE_SCREEN: SHORTS", + "SETTINGS: SHORTS_COMPONENTS" + ) + + if (is_19_25_or_greater && !is_19_28_or_greater) { + settingArray += "SETTINGS: SHORTS_TIME_STAMP" + } + + if (is_18_34_or_greater) { + settingArray += "SETTINGS: SHORTS_CUSTOM_ACTIONS_SHARED" + settingArray += "SETTINGS: SHORTS_CUSTOM_ACTIONS_TOOLBAR" + } + + if (is_19_02_or_greater) { + settingArray += "SETTINGS: SHORTS_CUSTOM_ACTIONS_FLYOUT_MENU" + } + + if (is_19_34_or_greater) { + settingArray += "SETTINGS: SHORTS_REPEAT_STATE_BACKGROUND" + } + + // region patch for hide comments button (non-litho) + + shortsButtonFingerprint.hideButton(rightComment, "hideShortsCommentsButton", false) + + // endregion + + // region patch for hide dislike button (non-litho) + + shortsButtonFingerprint.methodOrThrow().apply { + val constIndex = + indexOfFirstLiteralInstructionOrThrow(reelRightDislikeIcon) + val constRegister = getInstruction(constIndex).registerA + + val jumpIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.CONST_CLASS) + 2 + + addInstructionsWithLabels( + constIndex + 1, """ + invoke-static {}, $SHORTS_CLASS_DESCRIPTOR->hideShortsDislikeButton()Z + move-result v$constRegister + if-nez v$constRegister, :hide + const v$constRegister, $reelRightDislikeIcon + """, ExternalLabel("hide", getInstruction(jumpIndex)) + ) + } + + // endregion + + // region patch for hide like button (non-litho) + + shortsButtonFingerprint.methodOrThrow().apply { + val insertIndex = indexOfFirstLiteralInstructionOrThrow(reelRightLikeIcon) + val insertRegister = getInstruction(insertIndex).registerA + val jumpIndex = indexOfFirstInstructionOrThrow(insertIndex, Opcode.CONST_CLASS) + 2 + + addInstructionsWithLabels( + insertIndex + 1, """ + invoke-static {}, $SHORTS_CLASS_DESCRIPTOR->hideShortsLikeButton()Z + move-result v$insertRegister + if-nez v$insertRegister, :hide + const v$insertRegister, $reelRightLikeIcon + """, ExternalLabel("hide", getInstruction(jumpIndex)) + ) + } + + // endregion + + // region patch for hide sound button + + if (shortsPivotLegacyFingerprint.resolvable()) { + // Legacy method. + shortsPivotLegacyFingerprint.methodOrThrow().apply { + val targetIndex = + indexOfFirstLiteralInstructionOrThrow(reelForcedMuteButton) + val targetRegister = getInstruction(targetIndex).registerA + + val insertIndex = indexOfFirstInstructionReversedOrThrow(targetIndex, Opcode.IF_EQZ) + val jumpIndex = indexOfFirstInstructionOrThrow(targetIndex, Opcode.GOTO) + + addInstructionsWithLabels( + insertIndex, """ + invoke-static {}, $SHORTS_CLASS_DESCRIPTOR->hideShortsSoundButton()Z + move-result v$targetRegister + if-nez v$targetRegister, :hide + """, ExternalLabel("hide", getInstruction(jumpIndex)) + ) + } + } else if (reelPlayerRightPivotV2Size != -1L) { + // Invoke Sound button dimen into extension. + val smaliInstruction = """ + invoke-static {v$REGISTER_TEMPLATE_REPLACEMENT}, $SHORTS_CLASS_DESCRIPTOR->getShortsSoundButtonDimenId(I)I + move-result v$REGISTER_TEMPLATE_REPLACEMENT + """ + + replaceLiteralInstructionCall( + reelPlayerRightPivotV2Size, + smaliInstruction + ) + } else { + throw PatchException("ReelPlayerRightPivotV2Size is not found") + } + + // endregion + + // region patch for hide remix button (non-litho) + + shortsButtonFingerprint.hideButton(reelDynRemix, "hideShortsRemixButton", true) + + // endregion + + // region patch for hide share button (non-litho) + + shortsButtonFingerprint.hideButton(reelDynShare, "hideShortsShareButton", true) + + // endregion + + // region patch for hide paid promotion label (non-litho) + + shortsPaidPromotionFingerprint.methodOrThrow().apply { + when (returnType) { + "Landroid/widget/TextView;" -> { + val insertIndex = implementation!!.instructions.lastIndex + val insertRegister = + getInstruction(insertIndex).registerA + + addInstructions( + insertIndex + 1, """ + invoke-static {v$insertRegister}, $SHORTS_CLASS_DESCRIPTOR->hideShortsPaidPromotionLabel(Landroid/widget/TextView;)V + return-object v$insertRegister + """ + ) + removeInstruction(insertIndex) + } + + "V" -> { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $SHORTS_CLASS_DESCRIPTOR->hideShortsPaidPromotionLabel()Z + move-result v0 + if-eqz v0, :show + return-void + """, ExternalLabel("show", getInstruction(0)) + ) + } + + else -> { + throw PatchException("Unknown returnType: $returnType") + } + } + } + + // endregion + + // region patch for hide subscribe button (non-litho) + + // This method is deprecated since YouTube v18.31.xx. + if (!is_18_31_or_greater) { + val subscriptionFieldReference = + with(shortsSubscriptionsTabletParentFingerprint.methodOrThrow()) { + val targetIndex = + indexOfFirstLiteralInstructionOrThrow(reelPlayerFooter) - 1 + (getInstruction(targetIndex)).reference as FieldReference + } + shortsSubscriptionsTabletFingerprint.methodOrThrow( + shortsSubscriptionsTabletParentFingerprint + ).apply { + implementation!!.instructions.filter { instruction -> + val fieldReference = + (instruction as? ReferenceInstruction)?.reference as? FieldReference + instruction.opcode == Opcode.IGET && + fieldReference == subscriptionFieldReference + }.forEach { instruction -> + val insertIndex = implementation!!.instructions.indexOf(instruction) + 1 + val register = (instruction as TwoRegisterInstruction).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$register}, $SHORTS_CLASS_DESCRIPTOR->hideShortsSubscribeButton(I)I + move-result v$register + """ + ) + } + } + } + + // endregion + + // region patch for hide paused header + + shortsPausedHeaderFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.endIndex + 1 + val targetInstruction = getInstruction(targetIndex) + val targetReference = + (targetInstruction as? ReferenceInstruction)?.reference as? MethodReference + val useMethodWalker = targetInstruction.opcode == Opcode.INVOKE_VIRTUAL && + targetReference?.returnType == "V" && + targetReference.parameterTypes.firstOrNull() == "Landroid/view/View;" + + if (useMethodWalker) { + // YouTube 18.29.38 ~ YouTube 19.28.42 + getWalkerMethod(targetIndex).apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $SHORTS_CLASS_DESCRIPTOR->hideShortsPausedHeader()Z + move-result v0 + if-eqz v0, :show + return-void + """, ExternalLabel("show", getInstruction(0)) + ) + } + } else { + // YouTube 19.29.42 ~ + val insertIndex = it.patternMatch!!.startIndex + val insertRegister = + getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $SHORTS_CLASS_DESCRIPTOR->hideShortsPausedHeader(Z)Z + move-result v$insertRegister + """ + ) + } + } + } + + // endregion + + // region patch for return shorts channel name + + hookSpannableString( + EXTENSION_RETURN_YOUTUBE_CHANNEL_NAME_CLASS_DESCRIPTOR, + "onCharSequenceLoaded" + ) + + hookShortsVideoInformation("$EXTENSION_RETURN_YOUTUBE_CHANNEL_NAME_CLASS_DESCRIPTOR->newShortsVideoStarted(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") + + // endregion + + // region patch for restore shorts old player layout + + if (!is_19_25_or_greater) { + shortsFullscreenFeatureFingerprint.injectLiteralInstructionBooleanCall( + FULLSCREEN_FEATURE_FLAG, + "$SHORTS_CLASS_DESCRIPTOR->restoreShortsOldPlayerLayout()Z" + ) + settingArray += "SETTINGS: RESTORE_SHORTS_OLD_PLAYER_LAYOUT" + } + + // endregion + + addLithoFilter(BUTTON_FILTER_CLASS_DESCRIPTOR) + addLithoFilter(SHELF_FILTER_CLASS_DESCRIPTOR) + addLithoFilter(RETURN_YOUTUBE_CHANNEL_NAME_FILTER_CLASS_DESCRIPTOR) + + // region add settings + + addPreference(settingArray, SHORTS_COMPONENTS) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/startupshortsreset/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/startupshortsreset/Fingerprints.kt new file mode 100644 index 000000000..1ce5d5dde --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/startupshortsreset/Fingerprints.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.youtube.shorts.startupshortsreset + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +/** + * This fingerprint is compatible with all YouTube versions after v18.15.40. + */ +internal val userWasInShortsABConfigFingerprint = legacyFingerprint( + name = "userWasInShortsABConfigFingerprint", + returnType = "V", + strings = listOf("Failed to get offline response: "), + customFingerprint = { method, _ -> + indexOfOptionalInstruction(method) >= 0 + } +) + +internal fun indexOfOptionalInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_STATIC && + getReference().toString() == "Lj${'$'}/util/Optional;->of(Ljava/lang/Object;)Lj${'$'}/util/Optional;" + } + +internal val userWasInShortsFingerprint = legacyFingerprint( + name = "userWasInShortsFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Ljava/lang/Object;"), + strings = listOf("Failed to read user_was_in_shorts proto after successful warmup") +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/startupshortsreset/ResumingShortsOnStartupPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/startupshortsreset/ResumingShortsOnStartupPatch.kt new file mode 100644 index 000000000..ea7fe70ea --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/startupshortsreset/ResumingShortsOnStartupPatch.kt @@ -0,0 +1,105 @@ +package app.revanced.patches.youtube.shorts.startupshortsreset + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.SHORTS_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.patch.PatchList.DISABLE_RESUMING_SHORTS_ON_STARTUP +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +@Suppress("unused") +val resumingShortsOnStartupPatch = bytecodePatch( + DISABLE_RESUMING_SHORTS_ON_STARTUP.title, + DISABLE_RESUMING_SHORTS_ON_STARTUP.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + + userWasInShortsABConfigFingerprint.methodOrThrow().apply { + val startIndex = indexOfOptionalInstruction(this) + val walkerIndex = implementation!!.instructions.let { + val subListIndex = + it.subList(startIndex, startIndex + 20).indexOfFirst { instruction -> + val reference = instruction.getReference() + instruction.opcode == Opcode.INVOKE_VIRTUAL && + reference?.returnType == "Z" && + reference.definingClass != "Lj${'$'}/util/Optional;" && + reference.parameterTypes.isEmpty() + } + if (subListIndex < 0) + throw PatchException("subListIndex not found") + + startIndex + subListIndex + } + val walkerMethod = getWalkerMethod(walkerIndex) + + // This method will only be called for the user being A/B tested. + // Presumably a method that processes the ProtoDataStore value (boolean) for the 'user_was_in_shorts' key. + walkerMethod.apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $SHORTS_CLASS_DESCRIPTOR->disableResumingStartupShortsPlayer()Z + move-result v0 + if-eqz v0, :show + const/4 v0, 0x0 + return v0 + """, ExternalLabel("show", getInstruction(0)) + ) + } + } + + userWasInShortsFingerprint.methodOrThrow().apply { + val listenableInstructionIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_INTERFACE && + getReference()?.definingClass == "Lcom/google/common/util/concurrent/ListenableFuture;" && + getReference()?.name == "isDone" + } + val originalInstructionRegister = + getInstruction(listenableInstructionIndex).registerC + val freeRegister = + getInstruction(listenableInstructionIndex + 1).registerA + + addInstructionsWithLabels( + listenableInstructionIndex + 1, + """ + invoke-static {}, $SHORTS_CLASS_DESCRIPTOR->disableResumingStartupShortsPlayer()Z + move-result v$freeRegister + if-eqz v$freeRegister, :show + return-void + :show + invoke-interface {v$originalInstructionRegister}, Lcom/google/common/util/concurrent/ListenableFuture;->isDone()Z + """ + ) + removeInstruction(listenableInstructionIndex) + } + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: SHORTS", + "SETTINGS: DISABLE_RESUMING_SHORTS_PLAYER" + ), + DISABLE_RESUMING_SHORTS_ON_STARTUP + ) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/Fingerprints.kt new file mode 100644 index 000000000..1447cc97c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/Fingerprints.kt @@ -0,0 +1,56 @@ +package app.revanced.patches.youtube.swipe.controls + +import app.revanced.patches.youtube.utils.extension.Constants.EXTENSION_PATH +import app.revanced.patches.youtube.utils.resourceid.autoNavScrollCancelPadding +import app.revanced.patches.youtube.utils.resourceid.fullScreenEngagementOverlay +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val fullScreenEngagementOverlayFingerprint = legacyFingerprint( + name = "fullScreenEngagementOverlayFingerprint", + returnType = "V", + literals = listOf(fullScreenEngagementOverlay), +) + +internal val hdrBrightnessFingerprint = legacyFingerprint( + name = "hdrBrightnessFingerprint", + returnType = "V", + strings = listOf("mediaViewambientBrightnessSensor") +) + +internal val swipeControlsHostActivityFingerprint = legacyFingerprint( + name = "swipeControlsHostActivityFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + parameters = emptyList(), + customFingerprint = { _, classDef -> + classDef.type == "$EXTENSION_PATH/swipecontrols/SwipeControlsHostActivity;" + } +) + +internal const val SWIPE_TO_SWITCH_VIDEO_FEATURE_FLAG = 45631116L + +/** + * This fingerprint is compatible with YouTube v19.19.39+ + */ +internal val swipeToSwitchVideoFingerprint = legacyFingerprint( + name = "swipeToSwitchVideoFingerprint", + returnType = "V", + literals = listOf(SWIPE_TO_SWITCH_VIDEO_FEATURE_FLAG), +) + +internal const val WATCH_PANEL_GESTURES_FEATURE_FLAG = 45372793L + +/** + * This fingerprint is compatible with YouTube v18.29.38 ~ v19.34.42 + */ +internal val watchPanelGesturesFingerprint = legacyFingerprint( + name = "watchPanelGesturesFingerprint", + returnType = "V", + literals = listOf(WATCH_PANEL_GESTURES_FEATURE_FLAG), +) + +internal val watchPanelGesturesAlternativeFingerprint = legacyFingerprint( + name = "watchPanelGesturesAlternativeFingerprint", + literals = listOf(autoNavScrollCancelPadding), +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/SwipeControlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/SwipeControlsPatch.kt new file mode 100644 index 000000000..3d404f4b7 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/SwipeControlsPatch.kt @@ -0,0 +1,224 @@ +package app.revanced.patches.youtube.swipe.controls + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.mainactivity.mainActivityMutableClass +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.SWIPE_PATH +import app.revanced.patches.youtube.utils.lockmodestate.lockModeStateHookPatch +import app.revanced.patches.youtube.utils.mainactivity.mainActivityResolvePatch +import app.revanced.patches.youtube.utils.patch.PatchList.SWIPE_CONTROLS +import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.utils.playservice.is_19_09_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_23_or_greater +import app.revanced.patches.youtube.utils.playservice.is_19_36_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.resourceid.autoNavScrollCancelPadding +import app.revanced.patches.youtube.utils.resourceid.fullScreenEngagementOverlay +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.ResourceUtils.getContext +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.copyResources +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstruction +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import app.revanced.util.transformMethods +import app.revanced.util.traverseClassHierarchy +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod + +private const val EXTENSION_SWIPE_CONTROLS_PATCH_CLASS_DESCRIPTOR = + "$SWIPE_PATH/SwipeControlsPatch;" + +@Suppress("unused") +val swipeControlsPatch = bytecodePatch( + SWIPE_CONTROLS.title, + SWIPE_CONTROLS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + lockModeStateHookPatch, + mainActivityResolvePatch, + playerTypeHookPatch, + sharedResourceIdPatch, + settingsPatch, + versionCheckPatch, + ) + + execute { + + // region patch for swipe controls patch + + val hostActivityClass = swipeControlsHostActivityFingerprint.mutableClassOrThrow() + val mainActivityClass = mainActivityMutableClass + + // inject the wrapper class from extension into the class hierarchy of MainActivity (WatchWhileActivity) + hostActivityClass.setSuperClass(mainActivityClass.superclass) + mainActivityClass.setSuperClass(hostActivityClass.type) + + // ensure all classes and methods in the hierarchy are non-final, so we can override them in extension + traverseClassHierarchy(mainActivityClass) { + accessFlags = accessFlags and AccessFlags.FINAL.value.inv() + transformMethods { + ImmutableMethod( + definingClass, + name, + parameters, + returnType, + accessFlags and AccessFlags.FINAL.value.inv(), + annotations, + hiddenApiRestrictions, + implementation + ).toMutable() + } + } + + fullScreenEngagementOverlayFingerprint.methodOrThrow().apply { + val viewIndex = + indexOfFirstLiteralInstructionOrThrow(fullScreenEngagementOverlay) + 3 + val viewRegister = getInstruction(viewIndex).registerA + + addInstruction( + viewIndex + 1, + "invoke-static {v$viewRegister}, $EXTENSION_SWIPE_CONTROLS_PATCH_CLASS_DESCRIPTOR->setFullscreenEngagementOverlayView(Landroid/view/View;)V" + ) + } + + // endregion + + var settingArray = arrayOf( + "PREFERENCE_SCREEN: SWIPE_CONTROLS", + "SETTINGS: DISABLE_WATCH_PANEL_GESTURES" + ) + + // region patch for disable HDR auto brightness + + // Since it does not support all versions, + // add settings only if the patch is successful. + if (!is_19_09_or_greater) { + hdrBrightnessFingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $EXTENSION_SWIPE_CONTROLS_PATCH_CLASS_DESCRIPTOR->disableHDRAutoBrightness()Z + move-result v0 + if-eqz v0, :default + return-void + """, ExternalLabel("default", getInstruction(0)) + ) + settingArray += "SETTINGS: DISABLE_HDR_BRIGHTNESS" + } + } + + // endregion + + // region patch for disable swipe to switch video + + if (is_19_23_or_greater) { + swipeToSwitchVideoFingerprint.injectLiteralInstructionBooleanCall( + SWIPE_TO_SWITCH_VIDEO_FEATURE_FLAG, + "$EXTENSION_SWIPE_CONTROLS_PATCH_CLASS_DESCRIPTOR->disableSwipeToSwitchVideo()Z" + ) + + settingArray += "SETTINGS: DISABLE_SWIPE_TO_SWITCH_VIDEO" + } + + // endregion + + // region patch for disable watch panel gestures + + if (!is_19_36_or_greater) { + watchPanelGesturesFingerprint.injectLiteralInstructionBooleanCall( + WATCH_PANEL_GESTURES_FEATURE_FLAG, + "$EXTENSION_SWIPE_CONTROLS_PATCH_CLASS_DESCRIPTOR->disableWatchPanelGestures()Z" + ) + } else { + watchPanelGesturesAlternativeFingerprint.methodOrThrow().apply { + val literalIndex = indexOfFirstLiteralInstruction(autoNavScrollCancelPadding) + val middleIndex = indexOfFirstInstructionOrThrow(literalIndex) { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.returnType == "V" && + reference.parameterTypes.size == 0 + } + val targetIndex = indexOfFirstInstructionOrThrow(middleIndex + 1) { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.returnType == "V" && + reference.parameterTypes.size == 0 + } + if (getInstruction(targetIndex - 1).opcode != Opcode.IGET_OBJECT) { + throw PatchException( + "Previous Opcode pattern does not match: ${ + getInstruction( + targetIndex - 1 + ).opcode + }" + ) + } + if (getInstruction(targetIndex + 1).opcode != Opcode.IF_EQZ) { + throw PatchException( + "Next Opcode pattern does not match: ${ + getInstruction( + targetIndex + 1 + ).opcode + }" + ) + } + val fieldReference = getInstruction(targetIndex - 1).reference + val fieldInstruction = getInstruction(targetIndex - 1) + + addInstructionsWithLabels( + targetIndex, """ + invoke-static {}, $EXTENSION_SWIPE_CONTROLS_PATCH_CLASS_DESCRIPTOR->disableWatchPanelGestures()Z + move-result v${fieldInstruction.registerA} + if-eqz v${fieldInstruction.registerA}, :disable + iget-object v${fieldInstruction.registerA}, v${fieldInstruction.registerB}, $fieldReference + """, ExternalLabel("disable", getInstruction(targetIndex + 1)) + ) + removeInstruction(targetIndex - 1) + } + } + + // endregion + + // region copy resources + + getContext().copyResources( + "youtube/swipecontrols", + ResourceGroup( + "drawable", + "ic_sc_brightness_auto.xml", + "ic_sc_brightness_manual.xml", + "ic_sc_volume_mute.xml", + "ic_sc_volume_normal.xml" + ) + ) + + // endregion + + // region add settings + + addPreference(settingArray, SWIPE_CONTROLS) + + // endregion + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/Fingerprints.kt new file mode 100644 index 000000000..d55486456 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/Fingerprints.kt @@ -0,0 +1,259 @@ +package app.revanced.patches.youtube.utils + +import app.revanced.patches.youtube.player.components.playerComponentsPatch +import app.revanced.patches.youtube.utils.resourceid.autoNavPreviewStub +import app.revanced.patches.youtube.utils.resourceid.autoNavToggle +import app.revanced.patches.youtube.utils.resourceid.fadeDurationFast +import app.revanced.patches.youtube.utils.resourceid.inlineTimeBarColorizedBarPlayedColorDark +import app.revanced.patches.youtube.utils.resourceid.inlineTimeBarPlayedNotHighlightedColor +import app.revanced.patches.youtube.utils.resourceid.insetOverlayViewLayout +import app.revanced.patches.youtube.utils.resourceid.menuItemView +import app.revanced.patches.youtube.utils.resourceid.playerControlNextButtonTouchArea +import app.revanced.patches.youtube.utils.resourceid.playerControlPreviousButtonTouchArea +import app.revanced.patches.youtube.utils.resourceid.scrimOverlay +import app.revanced.patches.youtube.utils.resourceid.seekUndoEduOverlayStub +import app.revanced.patches.youtube.utils.resourceid.totalTime +import app.revanced.patches.youtube.utils.resourceid.varispeedUnavailableTitle +import app.revanced.patches.youtube.utils.resourceid.videoQualityBottomSheet +import app.revanced.patches.youtube.utils.sponsorblock.sponsorBlockBytecodePatch +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val bottomSheetMenuItemBuilderFingerprint = legacyFingerprint( + name = "bottomSheetMenuItemBuilderFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "L", + parameters = listOf("L"), + opcodes = listOf( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT + ), + strings = listOf("Text missing for BottomSheetMenuItem."), + customFingerprint = { method, _ -> + indexOfSpannedCharSequenceInstruction(method) >= 0 + } +) + +fun indexOfSpannedCharSequenceInstruction(method: Method) = + method.indexOfFirstInstruction { + val reference = getReference() + opcode == Opcode.INVOKE_STATIC && + reference?.parameterTypes?.size == 1 && + reference.returnType == "Ljava/lang/CharSequence;" + } + +internal val engagementPanelBuilderFingerprint = legacyFingerprint( + name = "engagementPanelBuilderFingerprint", + returnType = "L", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + parameters = listOf("L", "L", "Z", "Z"), + strings = listOf( + "EngagementPanelController: cannot show EngagementPanel before EngagementPanelController.init() has been called.", + "[EngagementPanel] Cannot show EngagementPanel before EngagementPanelController.init() has been called." + ) +) + +internal val layoutConstructorFingerprint = legacyFingerprint( + name = "layoutConstructorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + strings = listOf("1.0x"), + literals = listOf( + autoNavToggle, + autoNavPreviewStub, + playerControlPreviousButtonTouchArea, + playerControlNextButtonTouchArea + ), +) + +internal val playbackRateBottomSheetBuilderFingerprint = legacyFingerprint( + name = "playbackRateBottomSheetBuilderFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.IGET_BOOLEAN, + Opcode.IF_EQZ, + ), + literals = listOf(varispeedUnavailableTitle), +) + +internal val playerButtonsResourcesFingerprint = legacyFingerprint( + name = "playerButtonsResourcesFingerprint", + returnType = "I", + parameters = listOf("Landroid/content/res/Resources;"), + literals = listOf(17694721L), +) + +internal val playerButtonsVisibilityFingerprint = legacyFingerprint( + name = "playerButtonsVisibilityFingerprint", + returnType = "V", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE + ), + parameters = listOf("Z", "Z") +) + +internal val playerSeekbarColorFingerprint = legacyFingerprint( + name = "playerSeekbarColorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf( + inlineTimeBarColorizedBarPlayedColorDark, + inlineTimeBarPlayedNotHighlightedColor + ), +) + +internal val qualityMenuViewInflateFingerprint = legacyFingerprint( + name = "qualityMenuViewInflateFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "L", "L"), + opcodes = listOf( + Opcode.INVOKE_SUPER, + Opcode.CONST, + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CONST_16, + Opcode.INVOKE_VIRTUAL, + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST + ), + literals = listOf(videoQualityBottomSheet), +) + +internal val rollingNumberTextViewAnimationUpdateFingerprint = legacyFingerprint( + name = "rollingNumberTextViewAnimationUpdateFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/graphics/Bitmap;"), + opcodes = listOf( + Opcode.NEW_INSTANCE, // bitmap ImageSpan + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT + ) +) + +/** + * This fingerprint is compatible with YouTube v18.32.39+ + */ +internal val rollingNumberTextViewFingerprint = legacyFingerprint( + name = "rollingNumberTextViewFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "F", "F"), + opcodes = listOf( + Opcode.IPUT, + null, // invoke-direct or invoke-virtual + Opcode.IPUT_OBJECT, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ), + customFingerprint = custom@{ _, classDef -> + classDef.superclass == "Landroid/support/v7/widget/AppCompatTextView;" + || classDef.superclass == "Lcom/google/android/libraries/youtube/rendering/ui/spec/typography/YouTubeAppCompatTextView;" + } +) + +internal val scrollTopParentFingerprint = legacyFingerprint( + name = "scrollTopParentFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + opcodes = listOf( + Opcode.IPUT_OBJECT, + Opcode.IPUT_OBJECT, + Opcode.IPUT_OBJECT, + Opcode.IPUT_OBJECT, + Opcode.CONST_16, + Opcode.INVOKE_VIRTUAL, + Opcode.NEW_INSTANCE, + Opcode.INVOKE_DIRECT, + Opcode.IPUT_OBJECT, + Opcode.RETURN_VOID + ), + customFingerprint = { method, _ -> method.name == "" } +) + +internal val seekbarFingerprint = legacyFingerprint( + name = "seekbarFingerprint", + returnType = "V", + strings = listOf("timed_markers_width") +) + +internal val seekbarOnDrawFingerprint = legacyFingerprint( + name = "seekbarOnDrawFingerprint", + customFingerprint = { method, _ -> method.name == "onDraw" } +) + +internal fun indexOfGetDrawableInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.toString() == "Landroid/content/res/Resources;->getDrawable(I)Landroid/graphics/drawable/Drawable;" + } + +internal val toolBarButtonFingerprint = legacyFingerprint( + name = "toolBarButtonFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/MenuItem;"), + literals = listOf(menuItemView), + customFingerprint = { method, _ -> + indexOfGetDrawableInstruction(method) >= 0 + } +) + +internal val totalTimeFingerprint = legacyFingerprint( + name = "totalTimeFingerprint", + returnType = "V", + literals = listOf(totalTime), +) + +internal val videoEndFingerprint = legacyFingerprint( + name = "videoEndFingerprint", + strings = listOf("Attempting to seek during an ad"), + literals = listOf(45368273L), +) + +/** + * Several instructions are added to this method by different patches. + * Therefore, patches using this fingerprint should not use the [Opcode] pattern, + * and must access the index through the resourceId. + * + * The patches and resourceIds that use this fingerprint are as follows: + * - [playerComponentsPatch] uses [fadeDurationFast], [scrimOverlay] and [seekUndoEduOverlayStub]. + * - [sponsorBlockBytecodePatch] uses [insetOverlayViewLayout]. + */ +internal val youtubeControlsOverlayFingerprint = legacyFingerprint( + name = "youtubeControlsOverlayFingerprint", + returnType = "V", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf( + fadeDurationFast, + insetOverlayViewLayout, + scrimOverlay, + seekUndoEduOverlayStub + ), +) + +const val PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR = + "Lcom/google/android/libraries/youtube/innertube/model/player/PlayerResponseModel;" \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/bottomsheet/BottomSheetHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/bottomsheet/BottomSheetHookPatch.kt new file mode 100644 index 000000000..568e118f6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/bottomsheet/BottomSheetHookPatch.kt @@ -0,0 +1,31 @@ +package app.revanced.patches.youtube.utils.bottomsheet + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.definingClassOrThrow + +private const val EXTENSION_BOTTOM_SHEET_HOOK_CLASS_DESCRIPTOR = + "$UTILS_PATH/BottomSheetHookPatch;" + +val bottomSheetHookPatch = bytecodePatch( + description = "bottomSheetHookPatch" +) { + execute { + val bottomSheetClass = + bottomSheetBehaviorFingerprint.definingClassOrThrow() + + arrayOf( + "onAttachedToWindow", + "onDetachedFromWindow" + ).forEach { methodName -> + findMethodOrThrow(bottomSheetClass) { + name == methodName + }.addInstruction( + 1, + "invoke-static {}, $EXTENSION_BOTTOM_SHEET_HOOK_CLASS_DESCRIPTOR->$methodName()V" + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/bottomsheet/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/bottomsheet/Fingerprints.kt new file mode 100644 index 000000000..fa50d0791 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/bottomsheet/Fingerprints.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.youtube.utils.bottomsheet + +import app.revanced.patches.youtube.utils.resourceid.designBottomSheet +import app.revanced.util.fingerprint.legacyFingerprint + +internal val bottomSheetBehaviorFingerprint = legacyFingerprint( + name = "bottomSheetBehaviorFingerprint", + returnType = "V", + literals = listOf(designBottomSheet), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/castbutton/CastButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/castbutton/CastButtonPatch.kt new file mode 100644 index 000000000..cce33c8d2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/castbutton/CastButtonPatch.kt @@ -0,0 +1,97 @@ +@file:Suppress("CONTEXT_RECEIVERS_DEPRECATED") + +package app.revanced.patches.youtube.utils.castbutton + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.youtube.utils.extension.Constants.GENERAL_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.updatePatchStatus +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$UTILS_PATH/CastButtonPatch;" + +private lateinit var playerButtonMethod: MutableMethod +private lateinit var toolbarMenuItemInitializeMethod: MutableMethod +private lateinit var toolbarMenuItemVisibilityMethod: MutableMethod + +val castButtonPatch = bytecodePatch( + description = "castButtonPatch" +) { + dependsOn(sharedResourceIdPatch) + + execute { + toolbarMenuItemInitializeMethod = menuItemInitializeFingerprint.methodOrThrow() + toolbarMenuItemVisibilityMethod = + menuItemVisibilityFingerprint.methodOrThrow(menuItemInitializeFingerprint) + + playerButtonMethod = playerButtonFingerprint.methodOrThrow() + + findMethodOrThrow("Landroidx/mediarouter/app/MediaRouteButton;") { + name == "setVisibility" + }.addInstructions( + 0, """ + invoke-static {p1}, $EXTENSION_CLASS_DESCRIPTOR->hideCastButton(I)I + move-result p1 + """ + ) + } +} + +context(BytecodePatchContext) +internal fun hookPlayerCastButton() { + playerButtonMethod.apply { + val index = indexOfFirstInstructionOrThrow { + getReference()?.name == "setVisibility" + } + val instruction = getInstruction(index) + val viewRegister = instruction.registerC + val visibilityRegister = instruction.registerD + val reference = getInstruction(index).reference + + addInstructions( + index + 1, """ + invoke-static {v$visibilityRegister}, $PLAYER_CLASS_DESCRIPTOR->hideCastButton(I)I + move-result v$visibilityRegister + invoke-virtual {v$viewRegister, v$visibilityRegister}, $reference + """ + ) + removeInstruction(index) + } + updatePatchStatus(PATCH_STATUS_CLASS_DESCRIPTOR, "PlayerButtons") +} + +context(BytecodePatchContext) +internal fun hookToolBarCastButton() { + toolbarMenuItemInitializeMethod.apply { + val index = indexOfFirstInstructionOrThrow { + getReference()?.name == "setShowAsAction" + } + 1 + addInstruction( + index, + "invoke-static {p1}, $GENERAL_CLASS_DESCRIPTOR->hideCastButton(Landroid/view/MenuItem;)V" + ) + } + toolbarMenuItemVisibilityMethod.addInstructions( + 0, """ + invoke-static {p1}, $GENERAL_CLASS_DESCRIPTOR->hideCastButton(Z)Z + move-result p1 + """ + ) + updatePatchStatus(PATCH_STATUS_CLASS_DESCRIPTOR, "ToolBarComponents") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/castbutton/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/castbutton/Fingerprints.kt new file mode 100644 index 000000000..e4a603927 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/castbutton/Fingerprints.kt @@ -0,0 +1,37 @@ +package app.revanced.patches.youtube.utils.castbutton + +import app.revanced.patches.youtube.utils.resourceid.castMediaRouteButton +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val menuItemInitializeFingerprint = legacyFingerprint( + name = "menuItemInitializeFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/MenuItem;"), + literals = listOf(castMediaRouteButton), +) + +internal val menuItemVisibilityFingerprint = legacyFingerprint( + name = "menuItemVisibilityFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Z"), + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + getReference()?.name == "setVisible" + } >= 0 + } +) + +internal val playerButtonFingerprint = legacyFingerprint( + name = "playerButtonFingerprint", + returnType = "V", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(11208L), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/compatibility/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/compatibility/Constants.kt new file mode 100644 index 000000000..0ac2b447c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/compatibility/Constants.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.youtube.utils.compatibility + +import app.revanced.patcher.patch.PackageName +import app.revanced.patcher.patch.VersionName + +internal object Constants { + internal const val YOUTUBE_PACKAGE_NAME = "com.google.android.youtube" + + val COMPATIBLE_PACKAGE: Pair?> = Pair( + YOUTUBE_PACKAGE_NAME, + setOf( + "18.29.38", // This is the last version where the 'Zoomed to fill' setting works. + "18.33.40", // This is the last version that do not use litho components in Shorts. + "18.38.44", // This is the last version with no delay in applying video quality on the server side. + "18.48.39", // This is the last version that do not use Rolling Number. + "19.05.36", // This is the last version with the least YouTube experimental flag. + "19.16.39", // This is the last version where the 'Restore old seekbar thumbnails' setting works. + "19.44.39", // This is the latest version supported by the RVX patch. + ) + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/controlsoverlay/ControlsOverlayConfigPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/controlsoverlay/ControlsOverlayConfigPatch.kt new file mode 100644 index 000000000..2e585519c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/controlsoverlay/ControlsOverlayConfigPatch.kt @@ -0,0 +1,33 @@ +package app.revanced.patches.youtube.utils.controlsoverlay + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.resolvable +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +val controlsOverlayConfigPatch = bytecodePatch( + description = "controlsOverlayConfigPatch" +) { + + execute { + /** + * Added in YouTube v18.39.41 + * + * No exception even if fail to resolve fingerprints. + * For compatibility with YouTube v18.25.40 ~ YouTube v18.38.44. + */ + if (controlsOverlayConfigFingerprint.resolvable()) { + controlsOverlayConfigFingerprint.methodOrThrow().apply { + val targetIndex = implementation!!.instructions.size - 1 + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex, + "const/4 v$targetRegister, 0x0" + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/controlsoverlay/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/controlsoverlay/Fingerprints.kt new file mode 100644 index 000000000..83d494511 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/controlsoverlay/Fingerprints.kt @@ -0,0 +1,15 @@ +package app.revanced.patches.youtube.utils.controlsoverlay + +import app.revanced.util.fingerprint.legacyFingerprint + +/** + * Added in YouTube v18.39.41 + * + * When this value is TRUE, new control overlay is used. + * In this case, the associated patches no longer work, so set this value to FALSE. + */ +internal val controlsOverlayConfigFingerprint = legacyFingerprint( + name = "controlsOverlayConfigFingerprint", + returnType = "Z", + literals = listOf(45427491L), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/Constants.kt new file mode 100644 index 000000000..b0c20cdfe --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/Constants.kt @@ -0,0 +1,32 @@ +package app.revanced.patches.youtube.utils.extension + +@Suppress("MemberVisibilityCanBePrivate") +internal object Constants { + const val EXTENSION_PATH = "Lapp/revanced/extension/youtube" + const val SHARED_PATH = "$EXTENSION_PATH/shared" + const val PATCHES_PATH = "$EXTENSION_PATH/patches" + + const val ADS_PATH = "$PATCHES_PATH/ads" + const val ALTERNATIVE_THUMBNAILS_PATH = "$PATCHES_PATH/alternativethumbnails" + const val COMPONENTS_PATH = "$PATCHES_PATH/components" + const val FEED_PATH = "$PATCHES_PATH/feed" + const val GENERAL_PATH = "$PATCHES_PATH/general" + const val MISC_PATH = "$PATCHES_PATH/misc" + const val OVERLAY_BUTTONS_PATH = "$PATCHES_PATH/overlaybutton" + const val PLAYER_PATH = "$PATCHES_PATH/player" + const val SHORTS_PATH = "$PATCHES_PATH/shorts" + const val SPANS_PATH = "$PATCHES_PATH/spans" + const val SWIPE_PATH = "$PATCHES_PATH/swipe" + const val UTILS_PATH = "$PATCHES_PATH/utils" + const val VIDEO_PATH = "$PATCHES_PATH/video" + + const val ADS_CLASS_DESCRIPTOR = "$ADS_PATH/AdsPatch;" + const val ALTERNATIVE_THUMBNAILS_CLASS_DESCRIPTOR = + "$ALTERNATIVE_THUMBNAILS_PATH/AlternativeThumbnailsPatch;" + const val FEED_CLASS_DESCRIPTOR = "$FEED_PATH/FeedPatch;" + const val GENERAL_CLASS_DESCRIPTOR = "$GENERAL_PATH/GeneralPatch;" + const val PLAYER_CLASS_DESCRIPTOR = "$PLAYER_PATH/PlayerPatch;" + const val SHORTS_CLASS_DESCRIPTOR = "$SHORTS_PATH/ShortsPatch;" + + const val PATCH_STATUS_CLASS_DESCRIPTOR = "$UTILS_PATH/PatchStatus;" +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/SharedExtensionPatch.kt new file mode 100644 index 000000000..bab6ee3da --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/SharedExtensionPatch.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.youtube.utils.extension + +import app.revanced.patches.shared.extension.sharedExtensionPatch +import app.revanced.patches.youtube.utils.extension.hooks.applicationInitHook + +// TODO: Move this to a "Hook.kt" file. Same for other extension hook patches. +val sharedExtensionPatch = sharedExtensionPatch( + applicationInitHook, +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/hooks/ApplicationInitHook.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/hooks/ApplicationInitHook.kt new file mode 100644 index 000000000..82a9254dc --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/hooks/ApplicationInitHook.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.youtube.utils.extension.hooks + +import app.revanced.patches.shared.extension.extensionHook + +/** + * Hooks the context when the app is launched as a regular application (and is not an embedded video playback). + */ +// Extension context is the Activity itself. +internal val applicationInitHook = extensionHook { + strings("Application creation", "Application.onCreate") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/bottomui/CfBottomUIPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/bottomui/CfBottomUIPatch.kt new file mode 100644 index 000000000..9e219e03c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/bottomui/CfBottomUIPatch.kt @@ -0,0 +1,31 @@ +package app.revanced.patches.youtube.utils.fix.bottomui + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.resolvable + +val cfBottomUIPatch = bytecodePatch( + description = "cfBottomUIPatch" +) { + + execute { + /** + * This issue only affects some versions of YouTube. + * Therefore, this patch only applies to versions that can resolve this fingerprint. + */ + mapOf( + fullscreenButtonPositionFingerprint to FULLSCREEN_BUTTON_POSITION_FEATURE_FLAG, + fullscreenButtonViewStubFingerprint to FULLSCREEN_BUTTON_VIEW_STUB_FEATURE_FLAG, + playerBottomControlsExploderFeatureFlagFingerprint to PLAYER_BOTTOM_CONTROLS_EXPLODER_FEATURE_FLAG, + ).forEach { (fingerprint, literalValue) -> + if (fingerprint.resolvable()) { + fingerprint.injectLiteralInstructionBooleanCall( + literalValue, + "0x0" + ) + } + } + + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/bottomui/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/bottomui/Fingerprints.kt new file mode 100644 index 000000000..14d43d74c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/bottomui/Fingerprints.kt @@ -0,0 +1,25 @@ +package app.revanced.patches.youtube.utils.fix.bottomui + +import app.revanced.util.fingerprint.legacyFingerprint + +internal const val FULLSCREEN_BUTTON_POSITION_FEATURE_FLAG = 45627640L +internal const val FULLSCREEN_BUTTON_VIEW_STUB_FEATURE_FLAG = 45617294L +internal const val PLAYER_BOTTOM_CONTROLS_EXPLODER_FEATURE_FLAG = 45643739L + +internal val fullscreenButtonPositionFingerprint = legacyFingerprint( + name = "fullscreenButtonPositionFingerprint", + returnType = "Z", + literals = listOf(FULLSCREEN_BUTTON_POSITION_FEATURE_FLAG), +) + +internal val fullscreenButtonViewStubFingerprint = legacyFingerprint( + name = "fullscreenButtonViewStubFingerprint", + returnType = "Z", + literals = listOf(FULLSCREEN_BUTTON_VIEW_STUB_FEATURE_FLAG), +) + +internal val playerBottomControlsExploderFeatureFlagFingerprint = legacyFingerprint( + name = "playerBottomControlsExploderFeatureFlagFingerprint", + returnType = "Z", + literals = listOf(PLAYER_BOTTOM_CONTROLS_EXPLODER_FEATURE_FLAG), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/cairo/CairoSettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/cairo/CairoSettingsPatch.kt new file mode 100644 index 000000000..c4474b53f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/cairo/CairoSettingsPatch.kt @@ -0,0 +1,34 @@ +package app.revanced.patches.youtube.utils.fix.cairo + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.misc.backgroundplayback.backgroundPlaybackPatch +import app.revanced.patches.youtube.utils.playservice.is_19_04_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall + +val cairoSettingsPatch = bytecodePatch( + description = "cairoSettingsPatch" +) { + dependsOn(versionCheckPatch) + + execute { + if (!is_19_04_or_greater) { + return@execute + } + + /** + * Cairo Fragment was added since YouTube v19.04.38. + * Disable this for the following reasons: + * 1. [backgroundPlaybackPatch] does not activate the Minimized playback setting of Cairo Fragment. + * 2. Some patches implemented in RVX do not yet support Cairo Fragments. + * + * See ReVanced_Extended#2099 + * or uYouPlus#1468 + * for screenshots of the Cairo Fragment. + */ + carioFragmentConfigFingerprint.injectLiteralInstructionBooleanCall( + CAIRO_FRAGMENT_FEATURE_FLAG, + "0x0" + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/cairo/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/cairo/Fingerprints.kt new file mode 100644 index 000000000..11ef1f932 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/cairo/Fingerprints.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.youtube.utils.fix.cairo + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +/** + * Added in YouTube v19.04.38 + * + * When this value is TRUE, Cairo Fragment is used. + * In this case, some of patches may be broken, so set this value to FALSE. + */ +internal const val CAIRO_FRAGMENT_FEATURE_FLAG = 45532100L + +internal val carioFragmentConfigFingerprint = legacyFingerprint( + name = "carioFragmentConfigFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(CAIRO_FRAGMENT_FEATURE_FLAG), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/doublebacktoclose/DoubleBackToClosePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/doublebacktoclose/DoubleBackToClosePatch.kt new file mode 100644 index 000000000..b937e3e06 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/doublebacktoclose/DoubleBackToClosePatch.kt @@ -0,0 +1,55 @@ +package app.revanced.patches.youtube.utils.fix.doublebacktoclose + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.shared.mainactivity.injectOnBackPressedMethodCall +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.youtube.utils.scrollTopParentFingerprint +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.getWalkerMethod + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$UTILS_PATH/DoubleBackToClosePatch;" + +val doubleBackToClosePatch = bytecodePatch( + description = "doubleBackToClosePatch" +) { + execute { + fun MutableMethod.injectScrollView( + index: Int, + descriptor: String + ) = addInstruction( + index, + "invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->$descriptor()V" + ) + + /** + * Hook onBackPressed method inside MainActivity (WatchWhileActivity) + */ + injectOnBackPressedMethodCall( + EXTENSION_CLASS_DESCRIPTOR, + "closeActivityOnBackPressed" + ) + + /** + * Inject the methods which start of ScrollView + */ + scrollPositionFingerprint.matchOrThrow().let { + val walkerMethod = + it.getWalkerMethod(it.patternMatch!!.startIndex + 1) + val insertIndex = walkerMethod.implementation!!.instructions.size - 1 - 1 + + walkerMethod.injectScrollView(insertIndex, "onStartScrollView") + } + + /** + * Inject the methods which stop of ScrollView + */ + scrollTopFingerprint.matchOrThrow(scrollTopParentFingerprint).let { + val insertIndex = it.patternMatch!!.endIndex + + it.method.injectScrollView(insertIndex, "onStopScrollView") + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/doublebacktoclose/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/doublebacktoclose/Fingerprints.kt new file mode 100644 index 000000000..d844c4a96 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/doublebacktoclose/Fingerprints.kt @@ -0,0 +1,34 @@ +package app.revanced.patches.youtube.utils.fix.doublebacktoclose + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val scrollPositionFingerprint = legacyFingerprint( + name = "scrollPositionFingerprint", + returnType = "V", + accessFlags = AccessFlags.PROTECTED or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.IF_NEZ, + Opcode.INVOKE_DIRECT, + Opcode.RETURN_VOID + ), + strings = listOf("scroll_position") +) + +internal val scrollTopFingerprint = legacyFingerprint( + name = "scrollTopFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.CHECK_CAST, + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL, + Opcode.GOTO, + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE + ) +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/playbackspeed/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/playbackspeed/Fingerprints.kt new file mode 100644 index 000000000..392efbce5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/playbackspeed/Fingerprints.kt @@ -0,0 +1,42 @@ +package app.revanced.patches.youtube.utils.fix.playbackspeed + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionReversed +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +/** + * This fingerprint is compatible with YouTube 17.34.36 ~ 19.50.40. + * + * This method is usually used to set the initial speed (1.0x) when playback starts from the feed. + * For some reason, in the latest YouTube, it is invoked even after the video has already started. + */ +internal val playbackSpeedInFeedsFingerprint = legacyFingerprint( + name = "playbackSpeedInFeedsFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.IGET, + Opcode.MUL_INT_LIT16, + Opcode.IGET_WIDE, + Opcode.CONST_WIDE_16, + Opcode.CMP_LONG, + Opcode.IF_EQZ, + Opcode.IF_LEZ, + Opcode.SUB_LONG_2ADDR, + ), + customFingerprint = { method, _ -> + indexOfGetPlaybackSpeedInstruction(method) >= 0 + } +) + +fun indexOfGetPlaybackSpeedInstruction(method: Method) = + method.indexOfFirstInstructionReversed { + opcode == Opcode.IGET && + getReference()?.type == "F" + } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/playbackspeed/PlaybackSpeedWhilePlayingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/playbackspeed/PlaybackSpeedWhilePlayingPatch.kt new file mode 100644 index 000000000..26e4b110e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/playbackspeed/PlaybackSpeedWhilePlayingPatch.kt @@ -0,0 +1,61 @@ +package app.revanced.patches.youtube.utils.fix.playbackspeed + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.youtube.utils.extension.sharedExtensionPatch +import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.utils.playservice.is_19_34_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$UTILS_PATH/PlaybackSpeedWhilePlayingPatch;" + +val playbackSpeedWhilePlayingPatch = bytecodePatch( + description = "playbackSpeedWhilePlayingPatch" +) { + dependsOn( + sharedExtensionPatch, + playerTypeHookPatch, + versionCheckPatch, + ) + + execute { + if (!is_19_34_or_greater) { + return@execute + } + + /** + * There is an issue where sometimes when you click on a comment in a video and press the back button or click on the timestamp of the comment, the playback speed will change to 1.0x. + * + * This can be reproduced on unpatched YouTube 19.34.42+ by following these steps: + * 1. After the video starts, manually change the playback speed to something other than 1.0x. + * 2. If enough time has passed since the video started, open the comments panel. + * 3. Click on a comment and press the back button, or click on the timestamp of the comment. + * 4. Sometimes the playback speed will change to 1.0x. + * + * This is an issue that Google should fix, but it is not that hard to fix, so it has been implemented in the patch. + */ + playbackSpeedInFeedsFingerprint.methodOrThrow().apply { + val freeRegister = implementation!!.registerCount - parameters.size - 2 + val playbackSpeedIndex = indexOfGetPlaybackSpeedInstruction(this) + val playbackSpeedRegister = + getInstruction(playbackSpeedIndex).registerA + val jumpIndex = indexOfFirstInstructionOrThrow(playbackSpeedIndex, Opcode.RETURN_VOID) + + addInstructionsWithLabels( + playbackSpeedIndex + 1, """ + invoke-static { v$playbackSpeedRegister }, $EXTENSION_CLASS_DESCRIPTOR->playbackSpeedChanged(F)Z + move-result v$freeRegister + if-nez v$freeRegister, :do_not_change + """, ExternalLabel("do_not_change", getInstruction(jumpIndex)) + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/shortsplayback/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/shortsplayback/Fingerprints.kt new file mode 100644 index 000000000..59772d028 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/shortsplayback/Fingerprints.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.youtube.utils.fix.shortsplayback + +import app.revanced.util.fingerprint.legacyFingerprint + +internal const val SHORTS_PLAYBACK_PRIMARY_FEATURE_FLAG = 45387052L + +internal val shortsPlaybackPrimaryFingerprint = legacyFingerprint( + name = "shortsPlaybackPrimaryFingerprint", + returnType = "Z", + literals = listOf(SHORTS_PLAYBACK_PRIMARY_FEATURE_FLAG), +) + +internal const val SHORTS_PLAYBACK_SECONDARY_FEATURE_FLAG = 45378771L + +internal val shortsPlaybackSecondaryFingerprint = legacyFingerprint( + name = "shortsPlaybackSecondaryFingerprint", + returnType = "L", + literals = listOf(SHORTS_PLAYBACK_SECONDARY_FEATURE_FLAG), +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/shortsplayback/ShortsPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/shortsplayback/ShortsPlaybackPatch.kt new file mode 100644 index 000000000..a2d39d5d9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/shortsplayback/ShortsPlaybackPatch.kt @@ -0,0 +1,27 @@ +package app.revanced.patches.youtube.utils.fix.shortsplayback + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall + +val shortsPlaybackPatch = bytecodePatch( + description = "shortsPlaybackPatch" +) { + + execute { + /** + * This issue only affects some versions of YouTube. + * Therefore, this patch only applies to versions that can resolve this fingerprint. + * + * RVX applies default video quality to Shorts as well, so this patch is required. + */ + mapOf( + shortsPlaybackPrimaryFingerprint to SHORTS_PLAYBACK_PRIMARY_FEATURE_FLAG, + shortsPlaybackSecondaryFingerprint to SHORTS_PLAYBACK_SECONDARY_FEATURE_FLAG + ).forEach { (fingerprint, literal) -> + fingerprint.injectLiteralInstructionBooleanCall( + literal, + "0x0" + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/splash/DarkModeSplashScreenPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/splash/DarkModeSplashScreenPatch.kt new file mode 100644 index 000000000..515dd45af --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/splash/DarkModeSplashScreenPatch.kt @@ -0,0 +1,52 @@ +package app.revanced.patches.youtube.utils.fix.splash + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.youtube.utils.playservice.is_19_32_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import org.w3c.dom.Element + +val darkModeSplashScreenPatch = resourcePatch( + description = "darkModeSplashScreenPatch" +) { + dependsOn(versionCheckPatch) + + execute { + if (!is_19_32_or_greater) { + return@execute + } + + /** + * Fix the splash screen dark mode background color. + * In earlier versions of the app this is white and makes no sense for dark mode. + * This is only required for 19.32 and greater, but is applied to all targets. + * Only dark mode needs this fix as light mode correctly uses the custom color. + * + * This is a bug in unpatched YouTube. + * Should always be applied even if the `Theme` patch is excluded. + */ + document("res/values-night/styles.xml").use { document -> + val resourcesNode = document.getElementsByTagName("resources").item(0) as Element + val childNodes = resourcesNode.childNodes + + for (i in 0 until childNodes.length) { + val node = childNodes.item(i) as? Element ?: continue + val nodeAttributeName = node.getAttribute("name") + if (nodeAttributeName.startsWith("Theme.YouTube.Launcher")) { + val nodeAttributeParent = node.getAttribute("parent") + + val style = document.createElement("style") + style.setAttribute("name", "Theme.YouTube.Home") + style.setAttribute("parent", nodeAttributeParent) + + val windowItem = document.createElement("item") + windowItem.setAttribute("name", "android:windowBackground") + windowItem.textContent = "@color/yt_black1" + style.appendChild(windowItem) + + resourcesNode.removeChild(node) + resourcesNode.appendChild(style) + } + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt new file mode 100644 index 000000000..c82bbb612 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt @@ -0,0 +1,39 @@ +package app.revanced.patches.youtube.utils.fix.streamingdata + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patches.shared.extension.Constants.PATCHES_PATH +import app.revanced.patches.shared.spoof.streamingdata.baseSpoofStreamingDataPatch +import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.compatibility.Constants.YOUTUBE_PACKAGE_NAME +import app.revanced.patches.youtube.utils.patch.PatchList.SPOOF_STREAMING_DATA +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.findMethodOrThrow + +val spoofStreamingDataPatch = baseSpoofStreamingDataPatch( + { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseSpoofUserAgentPatch(YOUTUBE_PACKAGE_NAME), + settingsPatch + ) + }, + { + findMethodOrThrow("$PATCHES_PATH/PatchStatus;") { + name == "SpoofStreamingDataAndroidOnlyDefaultBoolean" + }.replaceInstruction( + 0, + "const/4 v0, 0x1" + ) + + addPreference( + arrayOf( + "SETTINGS: SPOOF_STREAMING_DATA" + ), + SPOOF_STREAMING_DATA + ) + + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/Fingerprints.kt new file mode 100644 index 000000000..210b9d45f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/Fingerprints.kt @@ -0,0 +1,45 @@ +package app.revanced.patches.youtube.utils.fix.suggestedvideoendscreen + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val autoNavConstructorFingerprint = legacyFingerprint( + name = "autoNavConstructorFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + strings = listOf("main_app_autonav"), +) + +internal val autoNavStatusFingerprint = legacyFingerprint( + name = "autoNavStatusFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "Z", + parameters = emptyList() +) + +/** + * This fingerprint is also compatible with very old YouTube versions. + * Tested on YouTube v16.40.36, v18.29.38, v19.16.39. + */ +internal val removeOnLayoutChangeListenerFingerprint = legacyFingerprint( + name = "removeOnLayoutChangeListenerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.IPUT, + Opcode.INVOKE_VIRTUAL + ), + // This is the only reference present in the entire smali. + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + getReference()?.toString() + ?.endsWith("YouTubePlayerOverlaysLayout;->removeOnLayoutChangeListener(Landroid/view/View${'$'}OnLayoutChangeListener;)V") == true + } >= 0 + } +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/SuggestedVideoEndScreenPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/SuggestedVideoEndScreenPatch.kt new file mode 100644 index 000000000..422b64f36 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/SuggestedVideoEndScreenPatch.kt @@ -0,0 +1,78 @@ +package app.revanced.patches.youtube.utils.fix.suggestedvideoendscreen + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +val suggestedVideoEndScreenPatch = bytecodePatch( + description = "suggestedVideoEndScreenPatch" +) { + execute { + + /** + * The reasons why this patch is classified as a patch that fixes a 'bug' are as follows: + * 1. In YouTube v18.29.38, the suggested video end screen was only shown when the autoplay setting was turned on. + * 2. Starting from YouTube v18.35.36, the suggested video end screen is shown regardless of whether autoplay setting was turned on or off. + * + * This patch changes the suggested video end screen to be shown only when the autoplay setting is turned on. + * Automatically closing the suggested video end screen is not appropriate as it will disable the autoplay behavior. + */ + removeOnLayoutChangeListenerFingerprint.matchOrThrow().let { + val walkerIndex = + it.getWalkerMethod(it.patternMatch!!.endIndex) + + walkerIndex.apply { + val autoNavStatusMethodName = + autoNavStatusFingerprint.methodOrThrow(autoNavConstructorFingerprint).name + val invokeIndex = + indexOfFirstInstructionOrThrow { + val reference = getReference() + reference?.returnType == "Z" && + reference.parameterTypes.isEmpty() && + reference.name == autoNavStatusMethodName + } + val iGetObjectIndex = + indexOfFirstInstructionReversedOrThrow(invokeIndex, Opcode.IGET_OBJECT) + + val invokeReference = getInstruction(invokeIndex).reference + val iGetObjectReference = + getInstruction(iGetObjectIndex).reference + val opcodeName = getInstruction(invokeIndex).opcode.name + + addInstructionsWithLabels( + 0, + """ + invoke-static {}, $PLAYER_CLASS_DESCRIPTOR->hideSuggestedVideoEndScreen()Z + move-result v0 + if-eqz v0, :show_suggested_video_end_screen + + iget-object v0, p0, $iGetObjectReference + + # This reference checks whether autoplay is turned on. + $opcodeName {v0}, $invokeReference + move-result v0 + + # Hide suggested video end screen only when autoplay is turned off. + if-nez v0, :show_suggested_video_end_screen + return-void + """, + ExternalLabel( + "show_suggested_video_end_screen", + getInstruction(0) + ) + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/swiperefresh/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/swiperefresh/Fingerprints.kt new file mode 100644 index 000000000..2e8c41796 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/swiperefresh/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.youtube.utils.fix.swiperefresh + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val swipeRefreshLayoutFingerprint = legacyFingerprint( + name = "swipeRefreshLayoutFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.RETURN, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.RETURN + ), + customFingerprint = { method, _ -> method.definingClass.endsWith("/SwipeRefreshLayout;") } +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/swiperefresh/SwipeRefreshPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/swiperefresh/SwipeRefreshPatch.kt new file mode 100644 index 000000000..7086ff040 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/swiperefresh/SwipeRefreshPatch.kt @@ -0,0 +1,27 @@ +package app.revanced.patches.youtube.utils.fix.swiperefresh + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.fingerprint.matchOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +val swipeRefreshPatch = bytecodePatch( + description = "swipeRefreshPatch" +) { + execute { + + swipeRefreshLayoutFingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.patternMatch!!.endIndex + val register = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex, + "const/4 v$register, 0x0" + ) + } + } + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/flyoutmenu/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/flyoutmenu/Fingerprints.kt new file mode 100644 index 000000000..3465a9044 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/flyoutmenu/Fingerprints.kt @@ -0,0 +1,14 @@ +package app.revanced.patches.youtube.utils.flyoutmenu + +import app.revanced.patches.youtube.utils.resourceid.videoQualityUnavailableAnnouncement +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val videoQualityBottomSheetClassFingerprint = legacyFingerprint( + name = "videoQualityBottomSheetClassFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Z"), + literals = listOf(videoQualityUnavailableAnnouncement), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/flyoutmenu/FlyoutMenuHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/flyoutmenu/FlyoutMenuHookPatch.kt new file mode 100644 index 000000000..fdcd07b28 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/flyoutmenu/FlyoutMenuHookPatch.kt @@ -0,0 +1,59 @@ +package app.revanced.patches.youtube.utils.flyoutmenu + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.EXTENSION_PATH +import app.revanced.patches.youtube.utils.playbackRateBottomSheetBuilderFingerprint +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.util.addStaticFieldToExtension +import app.revanced.util.fingerprint.methodOrThrow + +private const val EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR = + "$EXTENSION_PATH/utils/VideoUtils;" + +val flyoutMenuHookPatch = bytecodePatch( + description = "flyoutMenuHookPatch", +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(sharedResourceIdPatch) + + execute { + playbackRateBottomSheetBuilderFingerprint.methodOrThrow().apply { + val smaliInstructions = + """ + if-eqz v0, :ignore + invoke-virtual {v0}, $definingClass->$name()V + :ignore + return-void + """ + + addStaticFieldToExtension( + EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR, + "showPlaybackSpeedFlyoutMenu", + "playbackRateBottomSheetClass", + definingClass, + smaliInstructions + ) + } + + videoQualityBottomSheetClassFingerprint.methodOrThrow().apply { + val smaliInstructions = + """ + if-eqz v0, :ignore + const/4 v1, 0x1 + invoke-virtual {v0, v1}, $definingClass->$name(Z)V + :ignore + return-void + """ + + addStaticFieldToExtension( + EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR, + "showVideoQualityFlyoutMenu", + "videoQualityBottomSheetClass", + definingClass, + smaliInstructions + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/gms/GmsCoreSupportPatch.kt new file mode 100644 index 000000000..db74edbfa --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/gms/GmsCoreSupportPatch.kt @@ -0,0 +1,61 @@ +package app.revanced.patches.youtube.utils.gms + +import app.revanced.patcher.patch.Option +import app.revanced.patches.shared.gms.gmsCoreSupportPatch +import app.revanced.patches.shared.spoof.useragent.baseSpoofUserAgentPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.compatibility.Constants.YOUTUBE_PACKAGE_NAME +import app.revanced.patches.youtube.utils.extension.sharedExtensionPatch +import app.revanced.patches.youtube.utils.fix.streamingdata.spoofStreamingDataPatch +import app.revanced.patches.youtube.utils.mainactivity.mainActivityFingerprint +import app.revanced.patches.youtube.utils.patch.PatchList.GMSCORE_SUPPORT +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.ResourceUtils.updateGmsCorePackageName +import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePackageName +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.valueOrThrow + +@Suppress("unused") +val gmsCoreSupportPatch = gmsCoreSupportPatch( + fromPackageName = YOUTUBE_PACKAGE_NAME, + mainActivityOnCreateFingerprint = mainActivityFingerprint.second, + extensionPatch = sharedExtensionPatch, + gmsCoreSupportResourcePatchFactory = ::gmsCoreSupportResourcePatch, +) { + compatibleWith(COMPATIBLE_PACKAGE) +} + +private fun gmsCoreSupportResourcePatch( + gmsCoreVendorGroupIdOption: Option, + packageNameYouTubeOption: Option, + packageNameYouTubeMusicOption: Option, +) = app.revanced.patches.shared.gms.gmsCoreSupportResourcePatch( + fromPackageName = YOUTUBE_PACKAGE_NAME, + spoofedPackageSignature = "24bb24c05e47e0aefa68a58a766179d9b613a600", + gmsCoreVendorGroupIdOption = gmsCoreVendorGroupIdOption, + packageNameYouTubeOption = packageNameYouTubeOption, + packageNameYouTubeMusicOption = packageNameYouTubeMusicOption, + executeBlock = { + updatePackageName( + YOUTUBE_PACKAGE_NAME, + packageNameYouTubeOption.valueOrThrow(), + packageNameYouTubeMusicOption.valueOrThrow() + ) + updateGmsCorePackageName( + "app.revanced", + gmsCoreVendorGroupIdOption.valueOrThrow() + ) + addPreference( + arrayOf( + "PREFERENCE: GMS_CORE_SETTINGS" + ), + GMSCORE_SUPPORT + ) + }, +) { + dependsOn( + baseSpoofUserAgentPatch(YOUTUBE_PACKAGE_NAME), + spoofStreamingDataPatch, + settingsPatch, + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/lockmodestate/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/lockmodestate/Fingerprints.kt new file mode 100644 index 000000000..ee4d987f5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/lockmodestate/Fingerprints.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.youtube.utils.lockmodestate + +import app.revanced.util.fingerprint.legacyFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val lockModeStateFingerprint = legacyFingerprint( + name = "lockModeStateFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC.value, + parameters = emptyList(), + opcodes = listOf(Opcode.RETURN_OBJECT), + customFingerprint = { method, _ -> + method.name == "getLockModeStateEnum" + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/lockmodestate/LockModeStateHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/lockmodestate/LockModeStateHookPatch.kt new file mode 100644 index 000000000..26fba91a4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/lockmodestate/LockModeStateHookPatch.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.youtube.utils.lockmodestate + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.util.fingerprint.matchOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$UTILS_PATH/LockModeStateHookPatch;" + +val lockModeStateHookPatch = bytecodePatch( + description = "lockModeStateHookPatch" +) { + + execute { + + lockModeStateFingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.patternMatch!!.endIndex + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex + 1, """ + invoke-static {v$insertRegister}, $EXTENSION_CLASS_DESCRIPTOR->setLockModeState(Ljava/lang/Enum;)V + return-object v$insertRegister + """ + ) + removeInstruction(insertIndex) + } + } + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/lottie/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/lottie/Fingerprints.kt new file mode 100644 index 000000000..39bf96cb9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/lottie/Fingerprints.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.youtube.utils.lottie + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal const val LOTTIE_ANIMATION_VIEW_CLASS_DESCRIPTOR = + "Lcom/airbnb/lottie/LottieAnimationView;" + +internal val setAnimationFingerprint = legacyFingerprint( + name = "setAnimationFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("I"), + opcodes = listOf( + Opcode.IF_EQZ, + Opcode.NEW_INSTANCE, + Opcode.NEW_INSTANCE, + ), + customFingerprint = { method, _ -> + method.definingClass == LOTTIE_ANIMATION_VIEW_CLASS_DESCRIPTOR + } +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/lottie/LottieAnimationViewHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/lottie/LottieAnimationViewHookPatch.kt new file mode 100644 index 000000000..2af67d85e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/lottie/LottieAnimationViewHookPatch.kt @@ -0,0 +1,29 @@ +package app.revanced.patches.youtube.utils.lottie + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.methodOrThrow + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$UTILS_PATH/LottieAnimationViewPatch;" + +val lottieAnimationViewHookPatch = bytecodePatch( + description = "lottieAnimationViewHookPatch", +) { + execute { + + findMethodOrThrow(EXTENSION_CLASS_DESCRIPTOR) { + name == "setAnimation" + }.addInstruction( + 0, + "invoke-virtual {p0, p1}, " + + LOTTIE_ANIMATION_VIEW_CLASS_DESCRIPTOR + + "->" + + setAnimationFingerprint.methodOrThrow().name + + "(I)V" + ) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/mainactivity/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/mainactivity/Fingerprints.kt new file mode 100644 index 000000000..07cb445ec --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/mainactivity/Fingerprints.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.youtube.utils.mainactivity + +import app.revanced.util.fingerprint.legacyFingerprint + +/** + * 'WatchWhileActivity' has been renamed to 'MainActivity' in YouTube v18.48.xx+ + * This fingerprint was added to prepare for YouTube v18.48.xx+ + */ +internal val mainActivityFingerprint = legacyFingerprint( + name = "mainActivityFingerprint", + returnType = "V", + parameters = listOf("Landroid/os/Bundle;"), + strings = listOf("PostCreateCalledKey"), + customFingerprint = { method, _ -> + method.definingClass.endsWith("Activity;") + && method.name == "onCreate" + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/mainactivity/MainActivityResolvePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/mainactivity/MainActivityResolvePatch.kt new file mode 100644 index 000000000..c1003b93e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/mainactivity/MainActivityResolvePatch.kt @@ -0,0 +1,5 @@ +package app.revanced.patches.youtube.utils.mainactivity + +import app.revanced.patches.shared.mainactivity.baseMainActivityResolvePatch + +val mainActivityResolvePatch = baseMainActivityResolvePatch(mainActivityFingerprint) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/navigation/Fingerprints.kt new file mode 100644 index 000000000..7b218c8f0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/navigation/Fingerprints.kt @@ -0,0 +1,114 @@ +package app.revanced.patches.youtube.utils.navigation + +import app.revanced.patches.youtube.general.navigation.navigationBarComponentsPatch +import app.revanced.patches.youtube.utils.resourceid.bottomBarContainer +import app.revanced.patches.youtube.utils.resourceid.imageOnlyTab +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val initializeBottomBarContainerFingerprint = legacyFingerprint( + name = "initializeBottomBarContainerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(bottomBarContainer), + customFingerprint = { method, classDef -> + AccessFlags.SYNTHETIC.isSet(classDef.accessFlags) && + indexOfLayoutChangeListenerInstruction(method) >= 0 + }, +) + +internal fun indexOfLayoutChangeListenerInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.toString() == "Landroid/view/View;->addOnLayoutChangeListener(Landroid/view/View${'$'}OnLayoutChangeListener;)V" + } + +internal val initializeButtonsFingerprint = legacyFingerprint( + name = "initializeButtonsFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(imageOnlyTab), +) + +/** + * Extension method, used for callback into to other patches. + * Specifically, [navigationBarComponentsPatch]. + */ +internal val navigationBarHookCallbackFingerprint = legacyFingerprint( + name = "navigationBarHookCallbackFingerprint", + accessFlags = AccessFlags.PRIVATE or AccessFlags.STATIC, + returnType = "V", + parameters = listOf(EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR, "Landroid/view/View;"), + customFingerprint = { method, _ -> + method.name == "navigationTabCreatedCallback" && + method.definingClass == EXTENSION_CLASS_DESCRIPTOR + } +) + +/** + * Resolves to the Enum class that looks up ordinal -> instance. + */ +internal val navigationEnumFingerprint = legacyFingerprint( + name = "navigationEnumFingerprint", + accessFlags = AccessFlags.STATIC or AccessFlags.CONSTRUCTOR, + strings = listOf( + "PIVOT_HOME", + "TAB_SHORTS", + "CREATION_TAB_LARGE", + "PIVOT_SUBSCRIPTIONS", + "TAB_ACTIVITY", + "VIDEO_LIBRARY_WHITE", + "INCOGNITO_CIRCLE" + ) +) + +internal val pivotBarButtonsCreateDrawableViewFingerprint = legacyFingerprint( + name = "pivotBarButtonsCreateDrawableViewFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + // Method has different number of parameters in some app targets. + // Parameters are checked in custom fingerprint. + returnType = "Landroid/view/View;", + customFingerprint = { method, classDef -> + classDef.type == "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;" && + // Only one method has a Drawable parameter. + method.parameterTypes.firstOrNull() == "Landroid/graphics/drawable/Drawable;" + } +) + +internal val pivotBarButtonsCreateResourceViewFingerprint = legacyFingerprint( + name = "pivotBarButtonsCreateResourceViewFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "Z", "I", "L"), + returnType = "Landroid/view/View;", + customFingerprint = { _, classDef -> + classDef.type == "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;" + } +) + +internal fun indexOfSetViewSelectedInstruction(method: Method) = method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && getReference()?.name == "setSelected" +} + +internal val pivotBarButtonsViewSetSelectedFingerprint = legacyFingerprint( + name = "pivotBarButtonsViewSetSelectedFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("I", "Z"), + customFingerprint = { method, _ -> + indexOfSetViewSelectedInstruction(method) >= 0 && + method.definingClass == "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;" + } +) + +internal val pivotBarConstructorFingerprint = legacyFingerprint( + name = "pivotBarConstructorFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + strings = listOf("com.google.android.apps.youtube.app.endpoint.flags") +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/navigation/NavigationBarHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/navigation/NavigationBarHookPatch.kt new file mode 100644 index 000000000..27a267ce9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/navigation/NavigationBarHookPatch.kt @@ -0,0 +1,140 @@ +package app.revanced.patches.youtube.utils.navigation + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.shared.mainactivity.injectOnBackPressedMethodCall +import app.revanced.patches.youtube.utils.extension.Constants.SHARED_PATH +import app.revanced.patches.youtube.utils.extension.sharedExtensionPatch +import app.revanced.patches.youtube.utils.mainactivity.mainActivityResolvePatch +import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.Instruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.util.MethodUtil + +internal const val EXTENSION_CLASS_DESCRIPTOR = + "$SHARED_PATH/NavigationBar;" +internal const val EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR = + "$SHARED_PATH/NavigationBar\$NavigationButton;" + +private lateinit var bottomBarContainerMethod: MutableMethod +private var bottomBarContainerOffset = 0 + +lateinit var hookNavigationButtonCreated: (String) -> Unit + +val navigationBarHookPatch = bytecodePatch( + description = "navigationBarHookPatch", +) { + dependsOn( + sharedExtensionPatch, + mainActivityResolvePatch, + playerTypeHookPatch, + sharedResourceIdPatch, + ) + + execute { + fun MutableMethod.addHook(hook: Hook, insertPredicate: Instruction.() -> Boolean) { + val filtered = instructions.filter(insertPredicate) + if (filtered.isEmpty()) throw PatchException("Could not find insert indexes") + filtered.forEach { + val insertIndex = it.location.index + 2 + val register = getInstruction(insertIndex - 1).registerA + + addInstruction( + insertIndex, + "invoke-static { v$register }, " + + "$EXTENSION_CLASS_DESCRIPTOR->${hook.methodName}(${hook.parameters})V", + ) + } + } + + initializeButtonsFingerprint.methodOrThrow(pivotBarConstructorFingerprint).apply { + // Hook the current navigation bar enum value. Note, the 'You' tab does not have an enum value. + val navigationEnumClassName = navigationEnumFingerprint.mutableClassOrThrow().type + addHook(Hook.SET_LAST_APP_NAVIGATION_ENUM) { + opcode == Opcode.INVOKE_STATIC && + getReference()?.definingClass == navigationEnumClassName + } + + // Hook the creation of navigation tab views. + val drawableTabMethod = + pivotBarButtonsCreateDrawableViewFingerprint.methodOrThrow() + addHook(Hook.NAVIGATION_TAB_LOADED) predicate@{ + MethodUtil.methodSignaturesMatch( + getReference() ?: return@predicate false, + drawableTabMethod, + ) + } + + val imageResourceTabMethod = + pivotBarButtonsCreateResourceViewFingerprint.methodOrThrow() + addHook(Hook.NAVIGATION_IMAGE_RESOURCE_TAB_LOADED) predicate@{ + MethodUtil.methodSignaturesMatch( + getReference() ?: return@predicate false, + imageResourceTabMethod, + ) + } + } + + pivotBarButtonsViewSetSelectedFingerprint.methodOrThrow().apply { + val index = indexOfSetViewSelectedInstruction(this) + val instruction = getInstruction(index) + val viewRegister = instruction.registerC + val isSelectedRegister = instruction.registerD + + addInstruction( + index + 1, + "invoke-static { v$viewRegister, v$isSelectedRegister }, " + + "$EXTENSION_CLASS_DESCRIPTOR->navigationTabSelected(Landroid/view/View;Z)V", + ) + } + + injectOnBackPressedMethodCall( + EXTENSION_CLASS_DESCRIPTOR, + "onBackPressed" + ) + + bottomBarContainerMethod = initializeBottomBarContainerFingerprint.methodOrThrow() + + hookNavigationButtonCreated = { extensionClassDescriptor -> + navigationBarHookCallbackFingerprint.methodOrThrow().addInstruction( + 0, + "invoke-static { p0, p1 }, " + + "$extensionClassDescriptor->navigationTabCreated" + + "(${EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR}Landroid/view/View;)V", + ) + } + } +} + +fun addBottomBarContainerHook(descriptor: String) { + bottomBarContainerMethod.apply { + val layoutChangeListenerIndex = indexOfLayoutChangeListenerInstruction(this) + val bottomBarContainerRegister = + getInstruction(layoutChangeListenerIndex).registerC + + addInstruction( + layoutChangeListenerIndex + bottomBarContainerOffset--, + "invoke-static { v$bottomBarContainerRegister }, $descriptor" + ) + } +} + +private enum class Hook(val methodName: String, val parameters: String) { + SET_LAST_APP_NAVIGATION_ENUM("setLastAppNavigationEnum", "Ljava/lang/Enum;"), + NAVIGATION_TAB_LOADED("navigationTabLoaded", "Landroid/view/View;"), + NAVIGATION_IMAGE_RESOURCE_TAB_LOADED( + "navigationImageResourceTabLoaded", + "Landroid/view/View;" + ), +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt new file mode 100644 index 000000000..d151517d7 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt @@ -0,0 +1,260 @@ +package app.revanced.patches.youtube.utils.patch + +internal enum class PatchList( + val title: String, + val summary: String, + var included: Boolean? = false +) { + ALTERNATIVE_THUMBNAILS( + "Alternative thumbnails", + "Adds options to replace video thumbnails using the DeArrow API or image captures from the video." + ), + AMBIENT_MODE_CONTROL( + "Ambient mode control", + "Adds options to disable Ambient mode and to bypass Ambient mode restrictions." + ), + BYPASS_IMAGE_REGION_RESTRICTIONS( + "Bypass image region restrictions", + "Adds an option to use a different host for static images, so that images blocked in some countries can be received." + ), + CHANGE_PLAYER_FLYOUT_MENU_TOGGLES( + "Change player flyout menu toggles", + "Adds an option to use text toggles instead of switch toggles within the additional settings menu." + ), + CHANGE_SHARE_SHEET( + "Change share sheet", + "Add option to change from in-app share sheet to system share sheet." + ), + CHANGE_START_PAGE( + "Change start page", + "Adds an option to set which page the app opens in instead of the homepage." + ), + CUSTOM_SHORTS_ACTION_BUTTONS( + "Custom Shorts action buttons", + "Changes, at compile time, the icon of the action buttons of the Shorts player." + ), + CUSTOM_BRANDING_ICON_FOR_YOUTUBE( + "Custom branding icon for YouTube", + "Changes the YouTube app icon to the icon specified in patch options." + ), + CUSTOM_BRANDING_NAME_FOR_YOUTUBE( + "Custom branding name for YouTube", + "Renames the YouTube app to the name specified in patch options." + ), + CUSTOM_DOUBLE_TAP_LENGTH( + "Custom double tap length", + "Adds Double-tap to seek values that are specified in patch options." + ), + CUSTOM_HEADER_FOR_YOUTUBE( + "Custom header for YouTube", + "Applies a custom header in the top left corner within the app." + ), + DESCRIPTION_COMPONENTS( + "Description components", + "Adds options to hide and disable description components." + ), + DISABLE_QUIC_PROTOCOL( + "Disable QUIC protocol", + "Adds an option to disable CronetEngine's QUIC protocol." + ), + DISABLE_AUTO_AUDIO_TRACKS( + "Disable auto audio tracks", + "Adds an option to disable audio tracks from being automatically enabled." + ), + DISABLE_AUTO_CAPTIONS( + "Disable auto captions", + "Adds an option to disable captions from being automatically enabled." + ), + DISABLE_HAPTIC_FEEDBACK( + "Disable haptic feedback", + "Adds options to disable haptic feedback when swiping in the video player." + ), + DISABLE_RESUMING_SHORTS_ON_STARTUP( + "Disable resuming Shorts on startup", + "Adds an option to disable the Shorts player from resuming on app startup when Shorts were last being watched." + ), + DISABLE_SPLASH_ANIMATION( + "Disable splash animation", + "Adds an option to disable the splash animation on app startup." + ), + ENABLE_OPUS_CODEC( + "Enable OPUS codec", + "Adds an options to enable the OPUS audio codec if the player response includes." + ), + ENABLE_DEBUG_LOGGING( + "Enable debug logging", + "Adds an option to enable debug logging." + ), + ENABLE_EXTERNAL_BROWSER( + "Enable external browser", + "Adds an option to always open links in your browser instead of in the in-app-browser." + ), + ENABLE_GRADIENT_LOADING_SCREEN( + "Enable gradient loading screen", + "Adds an option to enable the gradient loading screen." + ), + ENABLE_OPEN_LINKS_DIRECTLY( + "Enable open links directly", + "Adds an option to skip over redirection URLs in external links." + ), + FORCE_HIDE_PLAYER_BUTTONS_BACKGROUND( + "Force player buttons background", + "Changes the dark background surrounding the video player controls at compile time." + ), + FORCE_SNACKBAR_THEME( + "Force snackbar theme", + "Changes snackbar background color to match selected theme at compile time." + ), + FULLSCREEN_COMPONENTS( + "Fullscreen components", + "Adds options to hide or change components related to fullscreen." + ), + GMSCORE_SUPPORT( + "GmsCore support", + "Allows patched Google apps to run without root and under a different package name by using GmsCore instead of Google Play Services." + ), + HIDE_SHORTS_DIMMING( + "Hide Shorts dimming", + "Removes, at compile time, the dimming effect at the top and bottom of Shorts videos." + ), + HIDE_ACTION_BUTTONS( + "Hide action buttons", + "Adds options to hide action buttons under videos." + ), + HIDE_ADS( + "Hide ads", + "Adds options to hide ads." + ), + HIDE_COMMENTS_COMPONENTS( + "Hide comments components", + "Adds options to hide components related to comments." + ), + HIDE_FEED_COMPONENTS( + "Hide feed components", + "Adds options to hide components related to feeds." + ), + HIDE_FEED_FLYOUT_MENU( + "Hide feed flyout menu", + "Adds the ability to hide feed flyout menu components using a custom filter." + ), + HIDE_LAYOUT_COMPONENTS( + "Hide layout components", + "Adds options to hide general layout components." + ), + HIDE_PLAYER_BUTTONS( + "Hide player buttons", + "Adds options to hide buttons in the video player." + ), + HIDE_PLAYER_FLYOUT_MENU( + "Hide player flyout menu", + "Adds options to hide player flyout menu components." + ), + HIDE_SHORTCUTS( + "Hide shortcuts", + "Remove, at compile time, the app shortcuts that appears when app icon is long pressed." + ), + HOOK_YOUTUBE_MUSIC_ACTIONS( + "Hook YouTube Music actions", + "Adds support for opening music in RVX Music using the in-app YouTube Music button." + ), + HOOK_DOWNLOAD_ACTIONS( + "Hook download actions", + "Adds support to download videos with an external downloader app using the in-app download button." + ), + LAYOUT_SWITCH( + "Layout switch", + "Adds an option to spoof the dpi in order to use a tablet or phone layout." + ), + MATERIALYOU( + "MaterialYou", + "Applies the MaterialYou theme for Android 12+ devices." + ), + MINIPLAYER( + "Miniplayer", + "Adds options to change the in app minimized player, and if patching target 19.16+ adds options to use modern miniplayers." + ), + NAVIGATION_BAR_COMPONENTS( + "Navigation bar components", + "Adds options to hide or change components related to the navigation bar." + ), + OVERLAY_BUTTONS( + "Overlay buttons", + "Adds options to display overlay buttons in the video player." + ), + PLAYER_COMPONENTS( + "Player components", + "Adds options to hide or change components related to the video player." + ), + REMOVE_BACKGROUND_PLAYBACK_RESTRICTIONS( + "Remove background playback restrictions", + "Removes restrictions on background playback, including for music and kids videos." + ), + REMOVE_VIEWER_DISCRETION_DIALOG( + "Remove viewer discretion dialog", + "Adds an option to remove the dialog that appears when opening a video that has been age-restricted by accepting it automatically. This does not bypass the age restriction." + ), + RETURN_YOUTUBE_DISLIKE( + "Return YouTube Dislike", + "Adds an option to show the dislike count of videos using the Return YouTube Dislike API." + ), + RETURN_YOUTUBE_USERNAME( + "Return YouTube Username", + "Adds an option to replace YouTube handles with usernames in comments using YouTube Data API v3." + ), + SANITIZE_SHARING_LINKS( + "Sanitize sharing links", + "Adds an option to remove tracking query parameters from URLs when sharing links." + ), + SEEKBAR_COMPONENTS( + "Seekbar components", + "Adds options to hide or change components related to the seekbar." + ), + SETTINGS_FOR_YOUTUBE( + "Settings for YouTube", + "Applies mandatory patches to implement ReVanced Extended settings into the application." + ), + SHORTS_COMPONENTS( + "Shorts components", + "Adds options to hide or change components related to YouTube Shorts." + ), + SPONSORBLOCK( + "SponsorBlock", + "Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as sponsored content." + ), + SPOOF_APP_VERSION( + "Spoof app version", + "Adds options to spoof the YouTube client version. This can be used to restore old UI elements and features." + ), + SPOOF_STREAMING_DATA( + "Spoof streaming data", + "Adds options to spoof the streaming data to allow playback." + ), + SWIPE_CONTROLS( + "Swipe controls", + "Adds options for controlling volume and brightness with swiping, and whether to enter fullscreen when swiping down below the player." + ), + THEME( + "Theme", + "Changes the app's theme to the values specified in patch options." + ), + TOOLBAR_COMPONENTS( + "Toolbar components", + "Adds options to hide or change components located on the toolbar, such as toolbar buttons, search bar, and header." + ), + TRANSLATIONS_FOR_YOUTUBE( + "Translations for YouTube", + "Add translations or remove string resources." + ), + VIDEO_PLAYBACK( + "Video playback", + "Adds options to customize settings related to video playback, such as default video quality and playback speed." + ), + VISUAL_PREFERENCES_ICONS_FOR_YOUTUBE( + "Visual preferences icons for YouTube", + "Adds icons to specific preferences in the settings." + ), + WATCH_HISTORY( + "Spoof watch history", + "Adds an option to change the domain of the watch history or check its status." + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/pip/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/pip/Fingerprints.kt new file mode 100644 index 000000000..6eea42b60 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/pip/Fingerprints.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.youtube.utils.pip + +import app.revanced.patches.youtube.utils.PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR +import app.revanced.util.fingerprint.legacyFingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val pipPlaybackFingerprint = legacyFingerprint( + name = "pipPlaybackFingerprint", + returnType = "Z", + parameters = listOf(PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR), + opcodes = listOf( + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.IF_NEZ + ) +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/pip/PiPStateHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/pip/PiPStateHookPatch.kt new file mode 100644 index 000000000..3ddff71dd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/pip/PiPStateHookPatch.kt @@ -0,0 +1,31 @@ +package app.revanced.patches.youtube.utils.pip + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.utils.extension.Constants.EXTENSION_PATH +import app.revanced.util.fingerprint.matchOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private const val EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR = + "$EXTENSION_PATH/utils/VideoUtils;" + +val pipStateHookPatch = bytecodePatch( + description = "pipStateHookPatch", +) { + execute { + pipPlaybackFingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.patternMatch!!.endIndex + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $EXTENSION_VIDEO_UTILS_CLASS_DESCRIPTOR->getExternalDownloaderLaunchedState(Z)Z + move-result v$insertRegister + """ + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playercontrols/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playercontrols/Fingerprints.kt new file mode 100644 index 000000000..c788732e1 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playercontrols/Fingerprints.kt @@ -0,0 +1,79 @@ +package app.revanced.patches.youtube.utils.playercontrols + +import app.revanced.patches.youtube.utils.resourceid.bottomUiContainerStub +import app.revanced.patches.youtube.utils.resourceid.controlsLayoutStub +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionReversed +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val bottomControlsInflateFingerprint = legacyFingerprint( + name = "bottomControlsInflateFingerprint", + returnType = "Ljava/lang/Object;", + parameters = emptyList(), + opcodes = listOf( + Opcode.CHECK_CAST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT + ), + literals = listOf(bottomUiContainerStub), +) + +internal val controlsLayoutInflateFingerprint = legacyFingerprint( + name = "controlsLayoutInflateFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT + ), + literals = listOf(controlsLayoutStub), +) + +internal val motionEventFingerprint = legacyFingerprint( + name = "motionEventFingerprint", + returnType = "V", + parameters = listOf("Landroid/view/MotionEvent;"), + customFingerprint = { method, _ -> + indexOfTranslationInstruction(method) >= 0 + } +) + +internal fun indexOfTranslationInstruction(method: Method) = + method.indexOfFirstInstructionReversed { + getReference()?.name == "setTranslationY" + } + +internal val playerControlsVisibilityEntityModelFingerprint = legacyFingerprint( + name = "playerControlsVisibilityEntityModelFingerprint", + accessFlags = AccessFlags.PUBLIC.value, + parameters = emptyList(), + opcodes = listOf( + Opcode.IGET, + Opcode.INVOKE_STATIC + ), + customFingerprint = { method, _ -> method.name == "getPlayerControlsVisibility" } +) + +internal val playerControlsVisibilityFingerprint = legacyFingerprint( + name = "playerControlsVisibilityFingerprint", + returnType = "V", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + parameters = listOf("Z", "Z") +) + +internal const val PLAYER_TOP_CONTROLS_EXPERIMENTAL_LAYOUT_FEATURE_FLAG = 45629424L + +internal val playerTopControlsExperimentalLayoutFeatureFlagFingerprint = legacyFingerprint( + name = "playerTopControlsExperimentalLayoutFeatureFlagFingerprint", + returnType = "I", + literals = listOf(PLAYER_TOP_CONTROLS_EXPERIMENTAL_LAYOUT_FEATURE_FLAG), +) \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playercontrols/PlayerControlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playercontrols/PlayerControlsPatch.kt new file mode 100644 index 000000000..8a648f0f4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playercontrols/PlayerControlsPatch.kt @@ -0,0 +1,253 @@ +package app.revanced.patches.youtube.utils.playercontrols + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.youtube.utils.extension.sharedExtensionPatch +import app.revanced.patches.youtube.utils.playerButtonsResourcesFingerprint +import app.revanced.patches.youtube.utils.playerButtonsVisibilityFingerprint +import app.revanced.patches.youtube.utils.playservice.is_19_25_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.youtubeControlsOverlayFingerprint +import app.revanced.util.copyXmlNode +import app.revanced.util.findElementByAttributeValueOrThrow +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.inputStreamFromBundledResourceOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +private const val EXTENSION_PLAYER_CONTROLS_CLASS_DESCRIPTOR = + "$UTILS_PATH/PlayerControlsPatch;" + +private const val EXTENSION_PLAYER_CONTROLS_VISIBILITY_HOOK_CLASS_DESCRIPTOR = + "$UTILS_PATH/PlayerControlsVisibilityHookPatch;" + +lateinit var changeVisibilityMethod: MutableMethod +lateinit var changeVisibilityNegatedImmediatelyMethod: MutableMethod +lateinit var initializeBottomControlButtonMethod: MutableMethod +lateinit var initializeTopControlButtonMethod: MutableMethod + +private val playerControlsBytecodePatch = bytecodePatch( + description = "playerControlsBytecodePatch" +) { + dependsOn( + sharedExtensionPatch, + sharedResourceIdPatch, + versionCheckPatch, + ) + + execute { + + // region patch for hook player controls visibility + + playerControlsVisibilityEntityModelFingerprint.matchOrThrow().let { + it.method.apply { + val startIndex = it.patternMatch!!.startIndex + val iGetReference = getInstruction(startIndex).reference + val staticReference = getInstruction(startIndex + 1).reference + + it.classDef.methods.find { method -> method.name == "" }?.apply { + val targetIndex = indexOfFirstInstructionOrThrow(Opcode.IPUT_OBJECT) + val targetRegister = + getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + iget v$targetRegister, v$targetRegister, $iGetReference + invoke-static {v$targetRegister}, $staticReference + move-result-object v$targetRegister + invoke-static {v$targetRegister}, $EXTENSION_PLAYER_CONTROLS_VISIBILITY_HOOK_CLASS_DESCRIPTOR->setPlayerControlsVisibility(Ljava/lang/Enum;)V + """ + ) + } ?: throw PatchException("Constructor method not found") + } + } + + // endregion + + // region patch for hook visibility of play control buttons (e.g. pause, play button, etc) + + playerButtonsVisibilityFingerprint.methodOrThrow(playerButtonsResourcesFingerprint).apply { + val viewIndex = indexOfFirstInstructionOrThrow(Opcode.INVOKE_INTERFACE) + val viewRegister = getInstruction(viewIndex).registerD + + addInstruction( + viewIndex + 1, + "invoke-static {p1, p2, v$viewRegister}, $EXTENSION_PLAYER_CONTROLS_CLASS_DESCRIPTOR->changeVisibility(ZZLandroid/view/View;)V" + ) + } + + // endregion + + // region patch for hook visibility of play controls layout + + playerControlsVisibilityFingerprint.methodOrThrow(youtubeControlsOverlayFingerprint) + .addInstruction( + 0, + "invoke-static {p1}, $EXTENSION_PLAYER_CONTROLS_CLASS_DESCRIPTOR->changeVisibility(Z)V" + ) + + // endregion + + // region patch for detecting motion events in play controls layout + + motionEventFingerprint.methodOrThrow(youtubeControlsOverlayFingerprint).apply { + val insertIndex = indexOfTranslationInstruction(this) + 1 + + addInstruction( + insertIndex, + "invoke-static {}, $EXTENSION_PLAYER_CONTROLS_CLASS_DESCRIPTOR->changeVisibilityNegatedImmediate()V" + ) + } + + // endregion + + // region patch initialize of overlay button or SponsorBlock button + + mapOf( + bottomControlsInflateFingerprint to "initializeBottomControlButton", + controlsLayoutInflateFingerprint to "initializeTopControlButton" + ).forEach { (fingerprint, methodName) -> + fingerprint.matchOrThrow().let { + it.method.apply { + val endIndex = it.patternMatch!!.endIndex + val viewRegister = getInstruction(endIndex).registerA + + addInstruction( + endIndex + 1, + "invoke-static {v$viewRegister}, $EXTENSION_PLAYER_CONTROLS_CLASS_DESCRIPTOR->$methodName(Landroid/view/View;)V" + ) + } + } + } + + // endregion + + // region set methods to inject into extension + + changeVisibilityMethod = + findMethodOrThrow(EXTENSION_PLAYER_CONTROLS_CLASS_DESCRIPTOR) { + name == "changeVisibility" && + parameters == listOf("Z", "Z") + } + + changeVisibilityNegatedImmediatelyMethod = + findMethodOrThrow(EXTENSION_PLAYER_CONTROLS_CLASS_DESCRIPTOR) { + name == "changeVisibilityNegatedImmediately" + } + + initializeBottomControlButtonMethod = + findMethodOrThrow(EXTENSION_PLAYER_CONTROLS_CLASS_DESCRIPTOR) { + name == "initializeBottomControlButton" + } + + initializeTopControlButtonMethod = + findMethodOrThrow(EXTENSION_PLAYER_CONTROLS_CLASS_DESCRIPTOR) { + name == "initializeTopControlButton" + } + + // endregion + + if (is_19_25_or_greater) { + playerTopControlsExperimentalLayoutFeatureFlagFingerprint.methodOrThrow().apply { + val index = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT_OBJECT) + val register = getInstruction(index).registerA + + addInstructions( + index + 1, + """ + invoke-static { v$register }, $EXTENSION_PLAYER_CONTROLS_CLASS_DESCRIPTOR->getPlayerTopControlsLayoutResourceName(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$register + """, + ) + } + } + } +} + +private fun MutableMethod.initializeHook(classDescriptor: String) = + addInstruction( + 0, + "invoke-static {p0}, $classDescriptor->initialize(Landroid/view/View;)V" + ) + +private fun changeVisibilityHook(classDescriptor: String) = + changeVisibilityMethod.addInstruction( + 0, + "invoke-static {p0, p1}, $classDescriptor->changeVisibility(ZZ)V" + ) + +private fun changeVisibilityNegatedImmediateHook(classDescriptor: String) = + changeVisibilityNegatedImmediatelyMethod.addInstruction( + 0, + "invoke-static {}, $classDescriptor->changeVisibilityNegatedImmediate()V" + ) + +fun hookBottomControlButton(classDescriptor: String) { + initializeBottomControlButtonMethod.initializeHook(classDescriptor) + changeVisibilityHook(classDescriptor) + changeVisibilityNegatedImmediateHook(classDescriptor) +} + +fun hookTopControlButton(classDescriptor: String) { + initializeTopControlButtonMethod.initializeHook(classDescriptor) + changeVisibilityHook(classDescriptor) + changeVisibilityNegatedImmediateHook(classDescriptor) +} + +/** + * Add a new top to the bottom of the YouTube player. + * + * @param resourceDirectoryName The name of the directory containing the hosting resource. + */ +@Suppress("KDocUnresolvedReference") +// Internal until this is modified to work with any patch (and not just SponsorBlock). +internal lateinit var addTopControl: (String) -> Unit + private set + +val playerControlsPatch = resourcePatch( + description = "playerControlsPatch" +) { + dependsOn(playerControlsBytecodePatch) + + execute { + addTopControl = { resourceDirectoryName -> + val resourceFileName = "shared/host/layout/youtube_controls_layout.xml" + val hostingResourceStream = inputStreamFromBundledResourceOrThrow( + resourceDirectoryName, + resourceFileName, + ) + + val document = document("res/layout/youtube_controls_layout.xml") + + "RelativeLayout".copyXmlNode( + document(hostingResourceStream), + document, + ).use { + val element = document.childNodes.findElementByAttributeValueOrThrow( + "android:id", + "@id/player_video_heading", + ) + + // FIXME: This uses hard coded values that only works with SponsorBlock. + // If other top buttons are added by other patches, this code must be changed. + // voting button id from the voting button view from the youtube_controls_layout.xml host file + val votingButtonId = "@+id/revanced_sb_voting_button" + element.attributes.getNamedItem("android:layout_toStartOf").nodeValue = + votingButtonId + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/Fingerprints.kt new file mode 100644 index 000000000..f7208bf7b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/Fingerprints.kt @@ -0,0 +1,81 @@ +package app.revanced.patches.youtube.utils.playertype + +import app.revanced.patches.youtube.utils.resourceid.reelWatchPlayer +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val browseIdClassFingerprint = legacyFingerprint( + name = "browseIdClassFingerprint", + returnType = "Ljava/lang/Object;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL or AccessFlags.SYNTHETIC, + parameters = listOf("Ljava/lang/Object;", "L"), + strings = listOf("VL") +) + +internal val playerTypeFingerprint = legacyFingerprint( + name = "playerTypeFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.IF_NE, + Opcode.RETURN_VOID + ), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/YouTubePlayerOverlaysLayout;") + } +) + +internal val reelWatchPagerFingerprint = legacyFingerprint( + name = "reelWatchPagerFingerprint", + returnType = "Landroid/view/View;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(reelWatchPlayer), +) + +internal fun indexOfStringIsEmptyInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference().toString() == "Ljava/lang/String;->isEmpty()Z" + } + +internal val searchQueryClassFingerprint = legacyFingerprint( + name = "searchQueryClassFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "Ljava/util/Map;"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + ), + strings = listOf("force_enable_sticky_browsy_bars"), + customFingerprint = { method, _ -> + indexOfStringIsEmptyInstruction(method) >= 0 + } +) + +internal val videoStateFingerprint = legacyFingerprint( + name = "videoStateFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Lcom/google/android/libraries/youtube/player/features/overlay/controls/ControlsState;"), + opcodes = listOf( + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, // obfuscated parameter field name + Opcode.IGET_OBJECT, + Opcode.IF_NE, + ), + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "equals" + } >= 0 + }, +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatch.kt new file mode 100644 index 000000000..3bcb4e63f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatch.kt @@ -0,0 +1,170 @@ +package app.revanced.patches.youtube.utils.playertype + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.youtube.utils.extension.Constants.SHARED_PATH +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.youtube.utils.extension.sharedExtensionPatch +import app.revanced.patches.youtube.utils.resourceid.reelWatchPlayer +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.util.addStaticFieldToExtension +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import app.revanced.util.indexOfFirstStringInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +private const val EXTENSION_PLAYER_TYPE_HOOK_CLASS_DESCRIPTOR = + "$UTILS_PATH/PlayerTypeHookPatch;" + +private const val EXTENSION_ROOT_VIEW_HOOK_CLASS_DESCRIPTOR = + "$SHARED_PATH/RootView;" + +private const val FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/RelatedVideoFilter;" + +val playerTypeHookPatch = bytecodePatch( + description = "playerTypeHookPatch" +) { + dependsOn( + sharedExtensionPatch, + sharedResourceIdPatch, + lithoFilterPatch, + ) + + execute { + + // region patch for set player type + + playerTypeFingerprint.methodOrThrow().addInstruction( + 0, + "invoke-static {p1}, " + + "$EXTENSION_PLAYER_TYPE_HOOK_CLASS_DESCRIPTOR->setPlayerType(Ljava/lang/Enum;)V" + ) + + // endregion + + // region patch for set shorts player state + + reelWatchPagerFingerprint.methodOrThrow().apply { + val literIndex = indexOfFirstLiteralInstructionOrThrow(reelWatchPlayer) + 2 + val registerIndex = indexOfFirstInstructionOrThrow(literIndex) { + opcode == Opcode.MOVE_RESULT_OBJECT + } + val viewRegister = getInstruction(registerIndex).registerA + + addInstruction( + registerIndex + 1, + "invoke-static {v$viewRegister}, " + + "$EXTENSION_PLAYER_TYPE_HOOK_CLASS_DESCRIPTOR->onShortsCreate(Landroid/view/View;)V" + ) + } + + // endregion + + // region patch for set video state + + videoStateFingerprint.matchOrThrow().let { + it.method.apply { + val endIndex = it.patternMatch!!.startIndex + 1 + val videoStateFieldName = + getInstruction(endIndex).reference + + addInstructions( + 0, """ + iget-object v0, p1, $videoStateFieldName # copyvideoState parameter field + invoke-static {v0}, $EXTENSION_PLAYER_TYPE_HOOK_CLASS_DESCRIPTOR->setVideoState(Ljava/lang/Enum;)V + """ + ) + } + } + + // endregion + + // region patch for hook browse id + + browseIdClassFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = indexOfFirstStringInstructionOrThrow("VL") - 1 + val targetClass = getInstruction(targetIndex) + .getReference() + ?.definingClass + ?: throw PatchException("Could not find browseId class") + + findMethodOrThrow(targetClass).apply { + val browseIdFieldReference = getInstruction( + indexOfFirstInstructionOrThrow(Opcode.IPUT_OBJECT) + ).reference + val browseIdFieldName = (browseIdFieldReference as FieldReference).name + + val smaliInstructions = + """ + if-eqz v0, :ignore + iget-object v0, v0, $definingClass->$browseIdFieldName:Ljava/lang/String; + if-eqz v0, :ignore + return-object v0 + :ignore + const-string v0, "" + return-object v0 + """ + + addStaticFieldToExtension( + EXTENSION_ROOT_VIEW_HOOK_CLASS_DESCRIPTOR, + "getBrowseId", + "browseIdClass", + definingClass, + smaliInstructions + ) + } + } + } + + // endregion + + // region patch for hook search bar + + searchQueryClassFingerprint.methodOrThrow().apply { + val searchQueryIndex = indexOfStringIsEmptyInstruction(this) - 1 + val searchQueryFieldReference = + getInstruction(searchQueryIndex).reference + val searchQueryClass = (searchQueryFieldReference as FieldReference).definingClass + + findMethodOrThrow(searchQueryClass).apply { + val smaliInstructions = + """ + if-eqz v0, :ignore + iget-object v0, v0, $searchQueryFieldReference + if-eqz v0, :ignore + return-object v0 + :ignore + const-string v0, "" + return-object v0 + """ + + addStaticFieldToExtension( + EXTENSION_ROOT_VIEW_HOOK_CLASS_DESCRIPTOR, + "getSearchQuery", + "searchQueryClass", + definingClass, + smaliInstructions + ) + } + } + + // endregion + + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playservice/VersionCheckPatch.kt new file mode 100644 index 000000000..f7cdcea1f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playservice/VersionCheckPatch.kt @@ -0,0 +1,93 @@ +@file:Suppress("ktlint:standard:property-naming") + +package app.revanced.patches.youtube.utils.playservice + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.util.findElementByAttributeValueOrThrow + +var is_18_31_or_greater = false + private set +var is_18_34_or_greater = false + private set +var is_18_39_or_greater = false + private set +var is_18_42_or_greater = false + private set +var is_18_49_or_greater = false + private set +var is_19_02_or_greater = false + private set +var is_19_04_or_greater = false + private set +var is_19_09_or_greater = false + private set +var is_19_15_or_greater = false + private set +var is_19_17_or_greater = false + private set +var is_19_23_or_greater = false + private set +var is_19_25_or_greater = false + private set +var is_19_26_or_greater = false + private set +var is_19_28_or_greater = false + private set +var is_19_29_or_greater = false + private set +var is_19_30_or_greater = false + private set +var is_19_32_or_greater = false + private set +var is_19_34_or_greater = false + private set +var is_19_36_or_greater = false + private set +var is_19_41_or_greater = false + private set +var is_19_43_or_greater = false + private set +var is_19_44_or_greater = false + private set +var is_19_46_or_greater = false + private set + +val versionCheckPatch = resourcePatch( + description = "versionCheckPatch", +) { + execute { + // The app version is missing from the decompiled manifest, + // so instead use the Google Play services version and compare against specific releases. + val playStoreServicesVersion = document("res/values/integers.xml").use { document -> + document.documentElement.childNodes.findElementByAttributeValueOrThrow( + "name", + "google_play_services_version", + ).textContent.toInt() + } + + // All bug fix releases always seem to use the same play store version as the minor version. + is_18_31_or_greater = 233200000 <= playStoreServicesVersion + is_18_34_or_greater = 233500000 <= playStoreServicesVersion + is_18_39_or_greater = 234000000 <= playStoreServicesVersion + is_18_42_or_greater = 234302000 <= playStoreServicesVersion + is_18_49_or_greater = 235000000 <= playStoreServicesVersion + is_19_02_or_greater = 240204000 < playStoreServicesVersion + is_19_04_or_greater = 240502000 <= playStoreServicesVersion + is_19_09_or_greater = 241002000 <= playStoreServicesVersion + is_19_15_or_greater = 241602000 <= playStoreServicesVersion + is_19_17_or_greater = 241802000 <= playStoreServicesVersion + is_19_23_or_greater = 242402000 <= playStoreServicesVersion + is_19_25_or_greater = 242599000 <= playStoreServicesVersion + is_19_26_or_greater = 242705000 <= playStoreServicesVersion + is_19_28_or_greater = 242905000 <= playStoreServicesVersion + is_19_29_or_greater = 243005000 <= playStoreServicesVersion + is_19_30_or_greater = 243105000 <= playStoreServicesVersion + is_19_32_or_greater = 243305000 <= playStoreServicesVersion + is_19_34_or_greater = 243499000 <= playStoreServicesVersion + is_19_36_or_greater = 243705000 <= playStoreServicesVersion + is_19_41_or_greater = 244305000 <= playStoreServicesVersion + is_19_43_or_greater = 244405000 <= playStoreServicesVersion + is_19_44_or_greater = 244505000 <= playStoreServicesVersion + is_19_46_or_greater = 244705000 <= playStoreServicesVersion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/recyclerview/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/recyclerview/Fingerprints.kt new file mode 100644 index 000000000..5b6bb8741 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/recyclerview/Fingerprints.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.youtube.utils.recyclerview + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal const val RECYCLER_VIEW_BUILDER_FEATURE_FLAG = 45382015L + +internal val recyclerViewBuilderFingerprint = legacyFingerprint( + name = "recyclerViewBuilderFingerprint", + literals = listOf(RECYCLER_VIEW_BUILDER_FEATURE_FLAG), +) + +internal val recyclerViewTreeObserverFingerprint = legacyFingerprint( + name = "recyclerViewTreeObserverFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + strings = listOf("LithoRVSLCBinder"), + customFingerprint = { method, _ -> + val parameterTypes = method.parameterTypes + parameterTypes.size > 2 && + parameterTypes[1] == "Landroid/support/v7/widget/RecyclerView;" + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/recyclerview/RecyclerViewTreeObserverPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/recyclerview/RecyclerViewTreeObserverPatch.kt new file mode 100644 index 000000000..3626d5d5f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/recyclerview/RecyclerViewTreeObserverPatch.kt @@ -0,0 +1,49 @@ +package app.revanced.patches.youtube.utils.recyclerview + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +private lateinit var recyclerViewTreeObserverMutableMethod: MutableMethod +private var recyclerViewTreeObserverInsertIndex = 0 + +val recyclerViewTreeObserverPatch = bytecodePatch( + description = "recyclerViewTreeObserverPatch" +) { + execute { + /** + * If this value is false, RecyclerViewTreeObserver is not initialized. + * This value is usually true so this patch is not strictly necessary, + * But in very rare cases this value may be false. + * Therefore, we need to force this to be true. + */ + recyclerViewBuilderFingerprint.injectLiteralInstructionBooleanCall( + RECYCLER_VIEW_BUILDER_FEATURE_FLAG, + "0x1" + ) + + recyclerViewTreeObserverFingerprint.methodOrThrow().apply { + recyclerViewTreeObserverMutableMethod = this + + val onDrawListenerIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.IPUT_OBJECT && + getReference()?.type == "Landroid/view/ViewTreeObserver${'$'}OnDrawListener;" + } + recyclerViewTreeObserverInsertIndex = + indexOfFirstInstructionReversedOrThrow(onDrawListenerIndex, Opcode.CHECK_CAST) + 1 + } + } +} + +fun recyclerViewTreeObserverHook(descriptor: String) = + recyclerViewTreeObserverMutableMethod.addInstruction( + recyclerViewTreeObserverInsertIndex++, + "invoke-static/range { p2 .. p2 }, $descriptor" + ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt new file mode 100644 index 000000000..ddbf22d3d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt @@ -0,0 +1,680 @@ +package app.revanced.patches.youtube.utils.resourceid + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.shared.mapping.ResourceType.ATTR +import app.revanced.patches.shared.mapping.ResourceType.COLOR +import app.revanced.patches.shared.mapping.ResourceType.DIMEN +import app.revanced.patches.shared.mapping.ResourceType.DRAWABLE +import app.revanced.patches.shared.mapping.ResourceType.ID +import app.revanced.patches.shared.mapping.ResourceType.INTEGER +import app.revanced.patches.shared.mapping.ResourceType.LAYOUT +import app.revanced.patches.shared.mapping.ResourceType.STRING +import app.revanced.patches.shared.mapping.ResourceType.STYLE +import app.revanced.patches.shared.mapping.get +import app.revanced.patches.shared.mapping.resourceMappingPatch +import app.revanced.patches.shared.mapping.resourceMappings + +var accountSwitcherAccessibility = -1L + private set +var actionBarRingo = -1L + private set +var actionBarRingoBackground = -1L + private set +var adAttribution = -1L + private set +var appearance = -1L + private set +var appRelatedEndScreenResults = -1L + private set +var autoNavPreviewStub = -1L + private set +var autoNavScrollCancelPadding = -1L + private set +var autoNavToggle = -1L + private set +var backgroundCategory = -1L + private set +var badgeLabel = -1L + private set +var bar = -1L + private set +var barContainerHeight = -1L + private set +var bottomBarContainer = -1L + private set +var bottomSheetFooterText = -1L + private set +var bottomSheetRecyclerView = -1L + private set +var bottomUiContainerStub = -1L + private set +var captionToggleContainer = -1L + private set +var castMediaRouteButton = -1L + private set +var cfFullscreenButton = -1L + private set +var channelListSubMenu = -1L + private set +var compactLink = -1L + private set +var compactListItem = -1L + private set +var componentLongClickListener = -1L + private set +var contentPill = -1L + private set +var controlsLayoutStub = -1L + private set +var darkBackground = -1L + private set +var darkSplashAnimation = -1L + private set +var designBottomSheet = -1L + private set +var donationCompanion = -1L + private set +var drawerContentView = -1L + private set +var drawerResults = -1L + private set +var easySeekEduContainer = -1L + private set +var editSettingsAction = -1L + private set +var endScreenElementLayoutCircle = -1L + private set +var endScreenElementLayoutIcon = -1L + private set +var endScreenElementLayoutVideo = -1L + private set +var emojiPickerIcon = -1L + private set +var expandButtonDown = -1L + private set +var fab = -1L + private set +var fadeDurationFast = -1L + private set +var filterBarHeight = -1L + private set +var floatyBarTopMargin = -1L + private set +var fullScreenButton = -1L + private set +var fullScreenEngagementOverlay = -1L + private set +var fullScreenEngagementPanel = -1L + private set +var horizontalCardList = -1L + private set +var imageOnlyTab = -1L + private set +var inlineTimeBarColorizedBarPlayedColorDark = -1L + private set +var inlineTimeBarPlayedNotHighlightedColor = -1L + private set +var insetOverlayViewLayout = -1L + private set +var interstitialsContainer = -1L + private set +var menuItemView = -1L + private set +var metaPanel = -1L + private set +var miniplayerMaxSize = -1L + private set +var modernMiniPlayerClose = -1L + private set +var modernMiniPlayerExpand = -1L + private set +var modernMiniPlayerForwardButton = -1L + private set +var modernMiniPlayerRewindButton = -1L + private set +var musicAppDeeplinkButtonView = -1L + private set +var notificationBigPictureIconWidth = -1L + private set +var offlineActionsVideoDeletedUndoSnackbarText = -1L + private set +var playerCollapseButton = -1L + private set +var playerControlPreviousButtonTouchArea = -1L + private set +var playerControlNextButtonTouchArea = -1L + private set +var playerVideoTitleView = -1L + private set +var posterArtWidthDefault = -1L + private set +var qualityAuto = -1L + private set +var quickActionsElementContainer = -1L + private set +var reelDynRemix = -1L + private set +var reelDynShare = -1L + private set +var reelFeedbackLike = -1L + private set +var reelFeedbackPause = -1L + private set +var reelFeedbackPlay = -1L + private set +var reelForcedMuteButton = -1L + private set +var reelPlayerFooter = -1L + private set +var reelPlayerRightPivotV2Size = -1L + private set +var reelRightDislikeIcon = -1L + private set +var reelRightLikeIcon = -1L + private set +var reelTimeBarPlayedColor = -1L + private set +var reelVodTimeStampsContainer = -1L + private set +var reelWatchPlayer = -1L + private set +var relatedChipCloudMargin = -1L + private set +var rightComment = -1L + private set +var scrimOverlay = -1L + private set +var scrubbing = -1L + private set +var seekEasyHorizontalTouchOffsetToStartScrubbing = -1L + private set +var seekUndoEduOverlayStub = -1L + private set +var slidingDialogAnimation = -1L + private set +var subtitleMenuSettingsFooterInfo = -1L + private set +var suggestedAction = -1L + private set +var tapBloomView = -1L + private set +var titleAnchor = -1L + private set +var toolTipContentView = -1L + private set +var totalTime = -1L + private set +var touchArea = -1L + private set +var videoQualityBottomSheet = -1L + private set +var varispeedUnavailableTitle = -1L + private set +var videoQualityUnavailableAnnouncement = -1L + private set +var videoZoomSnapIndicator = -1L + private set +var voiceSearch = -1L + private set +var youTubeControlsOverlaySubtitleButton = -1L + private set +var youTubeLogo = -1L + private set +var ytFillBell = -1L + private set +var ytOutlinePictureInPictureWhite = -1L + private set +var ytOutlineVideoCamera = -1L + private set +var ytOutlineXWhite = -1L + private set +var ytPremiumWordMarkHeader = -1L + private set +var ytWordMarkHeader = -1L + private set + + +internal val sharedResourceIdPatch = resourcePatch( + description = "sharedResourceIdPatch" +) { + dependsOn(resourceMappingPatch) + + execute { + accountSwitcherAccessibility = resourceMappings[ + STRING, + "account_switcher_accessibility_label" + ] + actionBarRingo = resourceMappings[ + LAYOUT, + "action_bar_ringo" + ] + actionBarRingoBackground = resourceMappings[ + LAYOUT, + "action_bar_ringo_background" + ] + adAttribution = resourceMappings[ + ID, + "ad_attribution" + ] + appearance = resourceMappings[ + STRING, + "app_theme_appearance_dark" + ] + appRelatedEndScreenResults = resourceMappings[ + LAYOUT, + "app_related_endscreen_results" + ] + autoNavPreviewStub = resourceMappings[ + ID, + "autonav_preview_stub" + ] + autoNavScrollCancelPadding = resourceMappings[ + DIMEN, + "autonav_scroll_cancel_padding" + ] + autoNavToggle = resourceMappings[ + ID, + "autonav_toggle" + ] + backgroundCategory = resourceMappings[ + STRING, + "pref_background_and_offline_category" + ] + badgeLabel = resourceMappings[ + ID, + "badge_label" + ] + bar = resourceMappings[ + LAYOUT, + "bar" + ] + barContainerHeight = resourceMappings[ + DIMEN, + "bar_container_height" + ] + bottomBarContainer = resourceMappings[ + ID, + "bottom_bar_container" + ] + bottomSheetFooterText = resourceMappings[ + ID, + "bottom_sheet_footer_text" + ] + bottomSheetRecyclerView = resourceMappings[ + LAYOUT, + "bottom_sheet_recycler_view" + ] + bottomUiContainerStub = resourceMappings[ + ID, + "bottom_ui_container_stub" + ] + captionToggleContainer = resourceMappings[ + ID, + "caption_toggle_container" + ] + castMediaRouteButton = resourceMappings[ + LAYOUT, + "castmediaroutebutton" + ] + cfFullscreenButton = resourceMappings[ + ID, + "cf_fullscreen_button" + ] + channelListSubMenu = resourceMappings[ + LAYOUT, + "channel_list_sub_menu" + ] + compactLink = resourceMappings[ + LAYOUT, + "compact_link" + ] + compactListItem = resourceMappings[ + LAYOUT, + "compact_list_item" + ] + componentLongClickListener = resourceMappings[ + ID, + "component_long_click_listener" + ] + contentPill = resourceMappings[ + LAYOUT, + "content_pill" + ] + controlsLayoutStub = resourceMappings[ + ID, + "controls_layout_stub" + ] + darkBackground = resourceMappings[ + ID, + "dark_background" + ] + darkSplashAnimation = resourceMappings[ + ID, + "dark_splash_animation" + ] + designBottomSheet = resourceMappings[ + ID, + "design_bottom_sheet" + ] + donationCompanion = resourceMappings[ + LAYOUT, + "donation_companion" + ] + drawerContentView = resourceMappings[ + ID, + "drawer_content_view" + ] + drawerResults = resourceMappings[ + ID, + "drawer_results" + ] + easySeekEduContainer = resourceMappings[ + ID, + "easy_seek_edu_container" + ] + editSettingsAction = resourceMappings[ + STRING, + "edit_settings_action" + ] + endScreenElementLayoutCircle = resourceMappings[ + LAYOUT, + "endscreen_element_layout_circle" + ] + endScreenElementLayoutIcon = resourceMappings[ + LAYOUT, + "endscreen_element_layout_icon" + ] + endScreenElementLayoutVideo = resourceMappings[ + LAYOUT, + "endscreen_element_layout_video" + ] + emojiPickerIcon = resourceMappings[ + ID, + "emoji_picker_icon" + ] + expandButtonDown = resourceMappings[ + LAYOUT, + "expand_button_down" + ] + fab = resourceMappings[ + ID, + "fab" + ] + fadeDurationFast = resourceMappings[ + INTEGER, + "fade_duration_fast" + ] + filterBarHeight = resourceMappings[ + DIMEN, + "filter_bar_height" + ] + floatyBarTopMargin = resourceMappings[ + DIMEN, + "floaty_bar_button_top_margin" + ] + fullScreenButton = resourceMappings[ + ID, + "fullscreen_button" + ] + fullScreenEngagementOverlay = resourceMappings[ + LAYOUT, + "fullscreen_engagement_overlay" + ] + fullScreenEngagementPanel = resourceMappings[ + ID, + "fullscreen_engagement_panel_holder" + ] + horizontalCardList = resourceMappings[ + LAYOUT, + "horizontal_card_list" + ] + imageOnlyTab = resourceMappings[ + LAYOUT, + "image_only_tab" + ] + inlineTimeBarColorizedBarPlayedColorDark = resourceMappings[ + COLOR, + "inline_time_bar_colorized_bar_played_color_dark" + ] + inlineTimeBarPlayedNotHighlightedColor = resourceMappings[ + COLOR, + "inline_time_bar_played_not_highlighted_color" + ] + insetOverlayViewLayout = resourceMappings[ + ID, + "inset_overlay_view_layout" + ] + interstitialsContainer = resourceMappings[ + ID, + "interstitials_container" + ] + menuItemView = resourceMappings[ + ID, + "menu_item_view" + ] + metaPanel = resourceMappings[ + ID, + "metapanel" + ] + miniplayerMaxSize = resourceMappings[ + DIMEN, + "miniplayer_max_size", + ] + modernMiniPlayerClose = resourceMappings[ + ID, + "modern_miniplayer_close" + ] + modernMiniPlayerExpand = resourceMappings[ + ID, + "modern_miniplayer_expand" + ] + modernMiniPlayerForwardButton = resourceMappings[ + ID, + "modern_miniplayer_forward_button" + ] + modernMiniPlayerRewindButton = resourceMappings[ + ID, + "modern_miniplayer_rewind_button" + ] + musicAppDeeplinkButtonView = resourceMappings[ + ID, + "music_app_deeplink_button_view" + ] + notificationBigPictureIconWidth = resourceMappings[ + DIMEN, + "notification_big_picture_icon_width" + ] + offlineActionsVideoDeletedUndoSnackbarText = resourceMappings[ + STRING, + "offline_actions_video_deleted_undo_snackbar_text" + ] + playerCollapseButton = resourceMappings[ + ID, + "player_collapse_button" + ] + playerControlPreviousButtonTouchArea = resourceMappings[ + ID, + "player_control_previous_button_touch_area" + ] + playerControlNextButtonTouchArea = resourceMappings[ + ID, + "player_control_next_button_touch_area" + ] + playerVideoTitleView = resourceMappings[ + ID, + "player_video_title_view" + ] + posterArtWidthDefault = resourceMappings[ + DIMEN, + "poster_art_width_default" + ] + qualityAuto = resourceMappings[ + STRING, + "quality_auto" + ] + quickActionsElementContainer = resourceMappings[ + ID, + "quick_actions_element_container" + ] + reelDynRemix = resourceMappings[ + ID, + "reel_dyn_remix" + ] + reelDynShare = resourceMappings[ + ID, + "reel_dyn_share" + ] + reelFeedbackLike = resourceMappings[ + ID, + "reel_feedback_like" + ] + reelFeedbackPause = resourceMappings[ + ID, + "reel_feedback_pause" + ] + reelFeedbackPlay = resourceMappings[ + ID, + "reel_feedback_play" + ] + reelForcedMuteButton = resourceMappings[ + ID, + "reel_player_forced_mute_button" + ] + reelPlayerFooter = resourceMappings[ + LAYOUT, + "reel_player_dyn_footer_vert_stories3" + ] + reelPlayerRightPivotV2Size = resourceMappings[ + DIMEN, + "reel_player_right_pivot_v2_size" + ] + reelRightDislikeIcon = resourceMappings[ + DRAWABLE, + "reel_right_dislike_icon" + ] + reelRightLikeIcon = resourceMappings[ + DRAWABLE, + "reel_right_like_icon" + ] + reelTimeBarPlayedColor = resourceMappings[ + COLOR, + "reel_time_bar_played_color" + ] + reelVodTimeStampsContainer = resourceMappings[ + ID, + "reel_vod_timestamps_container" + ] + reelWatchPlayer = resourceMappings[ + ID, + "reel_watch_player" + ] + relatedChipCloudMargin = resourceMappings[ + LAYOUT, + "related_chip_cloud_reduced_margins" + ] + rightComment = resourceMappings[ + DRAWABLE, + "ic_right_comment_32c" + ] + scrimOverlay = resourceMappings[ + ID, + "scrim_overlay" + ] + scrubbing = resourceMappings[ + DIMEN, + "vertical_touch_offset_to_enter_fine_scrubbing" + ] + seekEasyHorizontalTouchOffsetToStartScrubbing = resourceMappings[ + DIMEN, + "seek_easy_horizontal_touch_offset_to_start_scrubbing" + ] + seekUndoEduOverlayStub = resourceMappings[ + ID, + "seek_undo_edu_overlay_stub" + ] + slidingDialogAnimation = resourceMappings[ + STYLE, + "SlidingDialogAnimation" + ] + subtitleMenuSettingsFooterInfo = resourceMappings[ + STRING, + "subtitle_menu_settings_footer_info" + ] + suggestedAction = resourceMappings[ + LAYOUT, + "suggested_action" + ] + tapBloomView = resourceMappings[ + ID, + "tap_bloom_view" + ] + titleAnchor = resourceMappings[ + ID, + "title_anchor" + ] + toolTipContentView = resourceMappings[ + LAYOUT, + "tooltip_content_view" + ] + totalTime = resourceMappings[ + STRING, + "total_time" + ] + touchArea = resourceMappings[ + ID, + "touch_area" + ] + videoQualityBottomSheet = resourceMappings[ + LAYOUT, + "video_quality_bottom_sheet_list_fragment_title" + ] + varispeedUnavailableTitle = resourceMappings[ + STRING, + "varispeed_unavailable_title" + ] + videoQualityUnavailableAnnouncement = resourceMappings[ + STRING, + "video_quality_unavailable_announcement" + ] + videoZoomSnapIndicator = resourceMappings[ + ID, + "video_zoom_snap_indicator" + ] + voiceSearch = resourceMappings[ + ID, + "voice_search" + ] + youTubeControlsOverlaySubtitleButton = resourceMappings[ + LAYOUT, + "youtube_controls_overlay_subtitle_button" + ] + youTubeLogo = resourceMappings[ + ID, + "youtube_logo" + ] + ytFillBell = resourceMappings[ + DRAWABLE, + "yt_fill_bell_black_24" + ] + ytOutlinePictureInPictureWhite = resourceMappings[ + DRAWABLE, + "yt_outline_picture_in_picture_white_24" + ] + ytOutlineVideoCamera = resourceMappings[ + DRAWABLE, + "yt_outline_video_camera_black_24" + ] + ytOutlineXWhite = resourceMappings[ + DRAWABLE, + "yt_outline_x_white_24" + ] + ytPremiumWordMarkHeader = resourceMappings[ + ATTR, + "ytPremiumWordmarkHeader" + ] + ytWordMarkHeader = resourceMappings[ + ATTR, + "ytWordmarkHeader" + ] + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/returnyoutubedislike/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/returnyoutubedislike/Fingerprints.kt new file mode 100644 index 000000000..0a8ea07c6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/returnyoutubedislike/Fingerprints.kt @@ -0,0 +1,100 @@ +package app.revanced.patches.youtube.utils.returnyoutubedislike + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +/** + * This fingerprint is compatible with YouTube v18.30.xx+ + */ +internal val rollingNumberMeasureAnimatedTextFingerprint = legacyFingerprint( + name = "rollingNumberMeasureAnimatedTextFingerprint", + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.ADD_FLOAT_2ADDR, // measuredTextWidth + Opcode.ADD_INT_LIT8, + Opcode.GOTO + ), + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + getReference()?.toString() == "Landroid/text/TextPaint;->measureText([CII)F" + } >= 0 + } +) + +internal val rollingNumberMeasureStaticLabelFingerprint = legacyFingerprint( + name = "rollingNumberMeasureStaticLabelFingerprint", + returnType = "F", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Ljava/lang/String;"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.RETURN + ) +) + +internal val rollingNumberMeasureTextParentFingerprint = legacyFingerprint( + name = "rollingNumberMeasureTextParentFingerprint", + returnType = "Ljava/lang/String;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf(), + strings = listOf("RollingNumberFontProperties{paint=") +) + +/** + * This fingerprint is compatible with YouTube v18.29.38+ + */ +internal val rollingNumberSetterFingerprint = legacyFingerprint( + name = "rollingNumberSetterFingerprint", + opcodes = listOf(Opcode.CHECK_CAST), + literals = listOf(45427773L), +) + +internal val shortsTextViewFingerprint = legacyFingerprint( + name = "shortsTextViewFingerprint", + returnType = "V", + parameters = listOf("L", "L"), + opcodes = listOf( + Opcode.IF_EQZ, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.GOTO, + Opcode.IGET, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IF_NEZ, + Opcode.SGET_OBJECT, + Opcode.SGET_OBJECT, + Opcode.IF_NE, + Opcode.IGET, + Opcode.AND_INT_LIT8, + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, + Opcode.IF_NEZ, + Opcode.SGET_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.GOTO, + Opcode.IGET, + Opcode.AND_INT_LIT8, + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, + Opcode.IF_NEZ, + Opcode.SGET_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ), + customFingerprint = { _, classDef -> + classDef.methods.count() == 3 + } +) + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/returnyoutubedislike/ReturnYouTubeDislikePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/returnyoutubedislike/ReturnYouTubeDislikePatch.kt new file mode 100644 index 000000000..73c61c3d9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/returnyoutubedislike/ReturnYouTubeDislikePatch.kt @@ -0,0 +1,292 @@ +package app.revanced.patches.youtube.utils.returnyoutubedislike + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.dislikeFingerprint +import app.revanced.patches.shared.likeFingerprint +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.shared.removeLikeFingerprint +import app.revanced.patches.shared.textcomponent.hookSpannableString +import app.revanced.patches.shared.textcomponent.hookTextComponent +import app.revanced.patches.shared.textcomponent.textComponentPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.youtube.utils.patch.PatchList.RETURN_YOUTUBE_DISLIKE +import app.revanced.patches.youtube.utils.playservice.is_18_34_or_greater +import app.revanced.patches.youtube.utils.playservice.is_18_49_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.rollingNumberTextViewAnimationUpdateFingerprint +import app.revanced.patches.youtube.utils.rollingNumberTextViewFingerprint +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.patches.youtube.video.information.hookShortsVideoInformation +import app.revanced.patches.youtube.video.information.videoInformationPatch +import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId +import app.revanced.patches.youtube.video.videoid.hookVideoId +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val EXTENSION_RYD_CLASS_DESCRIPTOR = + "$UTILS_PATH/ReturnYouTubeDislikePatch;" + +private val returnYouTubeDislikeRollingNumberPatch = bytecodePatch( + description = "returnYouTubeDislikeRollingNumberPatch" +) { + dependsOn(versionCheckPatch) + + execute { + if (!is_18_49_or_greater) { + return@execute + } + + rollingNumberSetterFingerprint.matchOrThrow().let { + it.method.apply { + val rollingNumberClassIndex = it.patternMatch!!.startIndex + val rollingNumberClassReference = + getInstruction(rollingNumberClassIndex).reference.toString() + val rollingNumberConstructorMethod = + findMethodOrThrow(rollingNumberClassReference) + val charSequenceFieldReference = with(rollingNumberConstructorMethod) { + getInstruction( + indexOfFirstInstructionOrThrow(Opcode.IPUT_OBJECT) + ).reference + } + + val insertIndex = rollingNumberClassIndex + 1 + val charSequenceInstanceRegister = + getInstruction(rollingNumberClassIndex).registerA + val registerCount = implementation!!.registerCount + + // This register is being overwritten, so it is free to use. + val freeRegister = registerCount - 1 + val conversionContextRegister = registerCount - parameters.size + 1 + + addInstructions( + insertIndex, """ + iget-object v$freeRegister, v$charSequenceInstanceRegister, $charSequenceFieldReference + invoke-static {v$conversionContextRegister, v$freeRegister}, $EXTENSION_RYD_CLASS_DESCRIPTOR->onRollingNumberLoaded(Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/String; + move-result-object v$freeRegister + iput-object v$freeRegister, v$charSequenceInstanceRegister, $charSequenceFieldReference + """ + ) + } + } + + // Rolling Number text views use the measured width of the raw string for layout. + // Modify the measure text calculation to include the left drawable separator if needed. + rollingNumberMeasureAnimatedTextFingerprint.matchOrThrow().let { + it.method.apply { + val endIndex = it.patternMatch!!.endIndex + val measuredTextWidthIndex = endIndex - 2 + val measuredTextWidthRegister = + getInstruction(measuredTextWidthIndex).registerA + + addInstructions( + endIndex + 1, """ + invoke-static {p1, v$measuredTextWidthRegister}, $EXTENSION_RYD_CLASS_DESCRIPTOR->onRollingNumberMeasured(Ljava/lang/String;F)F + move-result v$measuredTextWidthRegister + """ + ) + + val ifGeIndex = indexOfFirstInstructionOrThrow(Opcode.IF_GE) + val ifGeInstruction = getInstruction(ifGeIndex) + + removeInstruction(ifGeIndex) + addInstructionsWithLabels( + ifGeIndex, """ + if-ge v${ifGeInstruction.registerA}, v${ifGeInstruction.registerB}, :jump + """, ExternalLabel("jump", getInstruction(endIndex)) + ) + } + } + + rollingNumberMeasureStaticLabelFingerprint.matchOrThrow( + rollingNumberMeasureTextParentFingerprint + ).let { + it.method.apply { + val measureTextIndex = it.patternMatch!!.startIndex + 1 + val freeRegister = getInstruction(0).registerA + + addInstructions( + measureTextIndex + 1, """ + move-result v$freeRegister + invoke-static {p1, v$freeRegister}, $EXTENSION_RYD_CLASS_DESCRIPTOR->onRollingNumberMeasured(Ljava/lang/String;F)F + """ + ) + } + } + + // The rolling number Span is missing styling since it's initially set as a String. + // Modify the UI text view and use the styled like/dislike Span. + arrayOf( + // Initial TextView is set in this method. + rollingNumberTextViewFingerprint + .methodOrThrow(), + + // Video less than 24 hours after uploaded, like counts will be updated in real time. + // Whenever like counts are updated, TextView is set in this method. + rollingNumberTextViewAnimationUpdateFingerprint + .methodOrThrow(rollingNumberTextViewFingerprint) + ).forEach { method -> + method.apply { + val setTextIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "setText" + } + val textViewRegister = + getInstruction(setTextIndex).registerC + val textSpanRegister = + getInstruction(setTextIndex).registerD + + addInstructions( + setTextIndex, """ + invoke-static {v$textViewRegister, v$textSpanRegister}, $EXTENSION_RYD_CLASS_DESCRIPTOR->updateRollingNumber(Landroid/widget/TextView;Ljava/lang/CharSequence;)Ljava/lang/CharSequence; + move-result-object v$textSpanRegister + """ + ) + } + } + } +} + +private val returnYouTubeDislikeShortsPatch = bytecodePatch( + description = "returnYouTubeDislikeShortsPatch" +) { + dependsOn( + textComponentPatch, + versionCheckPatch + ) + + execute { + shortsTextViewFingerprint.matchOrThrow().let { + it.method.apply { + val startIndex = it.patternMatch!!.startIndex + + val isDisLikesBooleanIndex = + indexOfFirstInstructionReversedOrThrow(startIndex, Opcode.IGET_BOOLEAN) + val textViewFieldIndex = + indexOfFirstInstructionReversedOrThrow(startIndex, Opcode.IGET_OBJECT) + + // If the field is true, the TextView is for a dislike button. + val isDisLikesBooleanReference = + getInstruction(isDisLikesBooleanIndex).reference + + val textViewFieldReference = // Like/Dislike button TextView field + getInstruction(textViewFieldIndex).reference + + // Check if the hooked TextView object is that of the dislike button. + // If RYD is disabled, or the TextView object is not that of the dislike button, the execution flow is not interrupted. + // Otherwise, the TextView object is modified, and the execution flow is interrupted to prevent it from being changed afterward. + val insertIndex = indexOfFirstInstructionOrThrow(Opcode.CHECK_CAST) + 1 + + addInstructionsWithLabels( + insertIndex, """ + # Check, if the TextView is for a dislike button + iget-boolean v0, p0, $isDisLikesBooleanReference + if-eqz v0, :ryd_disabled + + # Hook the TextView, if it is for the dislike button + iget-object v0, p0, $textViewFieldReference + invoke-static {v0}, $EXTENSION_RYD_CLASS_DESCRIPTOR->setShortsDislikes(Landroid/view/View;)Z + move-result v0 + if-eqz v0, :ryd_disabled + return-void + """, ExternalLabel("ryd_disabled", getInstruction(insertIndex)) + ) + } + } + + if (is_18_34_or_greater) { + hookSpannableString( + EXTENSION_RYD_CLASS_DESCRIPTOR, + "onCharSequenceLoaded" + ) + } + } +} + +private const val FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/ReturnYouTubeDislikeFilterPatch;" + +@Suppress("unused") +val returnYouTubeDislikePatch = bytecodePatch( + RETURN_YOUTUBE_DISLIKE.title, + RETURN_YOUTUBE_DISLIKE.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + returnYouTubeDislikeRollingNumberPatch, + returnYouTubeDislikeShortsPatch, + lithoFilterPatch, + videoInformationPatch, + ) + + execute { + mapOf( + likeFingerprint to Vote.LIKE, + dislikeFingerprint to Vote.DISLIKE, + removeLikeFingerprint to Vote.REMOVE_LIKE, + ).forEach { (fingerprint, vote) -> + fingerprint.methodOrThrow().addInstructions( + 0, + """ + const/4 v0, ${vote.value} + invoke-static {v0}, $EXTENSION_RYD_CLASS_DESCRIPTOR->sendVote(I)V + """, + ) + } + + hookTextComponent(EXTENSION_RYD_CLASS_DESCRIPTOR) + + // region Inject newVideoLoaded event handler to update dislikes when a new video is loaded. + hookVideoId("$EXTENSION_RYD_CLASS_DESCRIPTOR->newVideoLoaded(Ljava/lang/String;)V") + + // Hook the player response video id, to start loading RYD sooner in the background. + hookPlayerResponseVideoId("$EXTENSION_RYD_CLASS_DESCRIPTOR->preloadVideoId(Ljava/lang/String;Z)V") + + // endregion + + // Player response video id is needed to search for the video ids in Shorts litho components. + if (is_18_34_or_greater) { + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + hookPlayerResponseVideoId("$FILTER_CLASS_DESCRIPTOR->newPlayerResponseVideoId(Ljava/lang/String;Z)V") + hookShortsVideoInformation("$FILTER_CLASS_DESCRIPTOR->newShortsVideoStarted(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") + } + + // endregion + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: RETURN_YOUTUBE_DISLIKE" + ), + RETURN_YOUTUBE_DISLIKE + ) + + // endregion + } +} + +enum class Vote(val value: Int) { + LIKE(1), + DISLIKE(-1), + REMOVE_LIKE(0), +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/returnyoutubeusername/ReturnYouTubeUsernamePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/returnyoutubeusername/ReturnYouTubeUsernamePatch.kt new file mode 100644 index 000000000..6a1722682 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/returnyoutubeusername/ReturnYouTubeUsernamePatch.kt @@ -0,0 +1,35 @@ +package app.revanced.patches.youtube.utils.returnyoutubeusername + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.shared.returnyoutubeusername.baseReturnYouTubeUsernamePatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.patch.PatchList.RETURN_YOUTUBE_USERNAME +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch + +@Suppress("unused") +val returnYouTubeUsernamePatch = bytecodePatch( + RETURN_YOUTUBE_USERNAME.title, + RETURN_YOUTUBE_USERNAME.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseReturnYouTubeUsernamePatch, + settingsPatch, + ) + + execute { + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: RETURN_YOUTUBE_USERNAME" + ), + RETURN_YOUTUBE_USERNAME + ) + + // endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/Fingerprints.kt new file mode 100644 index 000000000..79bdce50a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/Fingerprints.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.youtube.utils.settings + +import app.revanced.patches.youtube.utils.resourceid.appearance +import app.revanced.util.fingerprint.legacyFingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val themeSetterSystemFingerprint = legacyFingerprint( + name = "themeSetterSystemFingerprint", + returnType = "L", + opcodes = listOf(Opcode.RETURN_OBJECT), + literals = listOf(appearance), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/ResourceUtils.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/ResourceUtils.kt new file mode 100644 index 000000000..7b84b5f93 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/ResourceUtils.kt @@ -0,0 +1,162 @@ +package app.revanced.patches.youtube.utils.settings + +import app.revanced.patcher.patch.ResourcePatchContext +import app.revanced.patches.music.utils.compatibility.Constants.YOUTUBE_MUSIC_PACKAGE_NAME +import app.revanced.patches.youtube.utils.compatibility.Constants.YOUTUBE_PACKAGE_NAME +import app.revanced.patches.youtube.utils.patch.PatchList +import app.revanced.util.doRecursively +import app.revanced.util.insertNode +import org.w3c.dom.Element +import java.io.File + +internal object ResourceUtils { + private lateinit var context: ResourcePatchContext + private lateinit var youtubeSettingFile: File + private lateinit var rvxSettingFile: File + + fun setContext(context: ResourcePatchContext) { + this.context = context + this.youtubeSettingFile = context[YOUTUBE_SETTINGS_PATH] + this.rvxSettingFile = context[RVX_PREFERENCE_PATH] + } + + fun getContext() = context + + const val RVX_PREFERENCE_PATH = "res/xml/revanced_prefs.xml" + const val YOUTUBE_SETTINGS_PATH = "res/xml/settings_fragment.xml" + + var youtubeMusicPackageName = YOUTUBE_MUSIC_PACKAGE_NAME + var youtubePackageName = YOUTUBE_PACKAGE_NAME + + private var iconType = "default" + fun getIconType() = iconType + + fun updatePackageName( + fromPackageName: String, + toPackageName: String, + musicPackageName: String + ) { + youtubeMusicPackageName = musicPackageName + youtubePackageName = toPackageName + + youtubeSettingFile.writeText( + youtubeSettingFile.readText() + .replace( + "android:targetPackage=\"$fromPackageName", + "android:targetPackage=\"$toPackageName" + ) + ) + } + + fun updateGmsCorePackageName( + fromPackageName: String, + toPackageName: String + ) { + rvxSettingFile.writeText( + rvxSettingFile.readText() + .replace( + "android:targetPackage=\"$fromPackageName", + "android:targetPackage=\"$toPackageName" + ) + ) + } + + fun addPreference(patch: PatchList) { + patch.included = true + updatePatchStatus(patch.title.replace(" for YouTube", "")) + } + + fun addPreference(settingArray: Array, patch: PatchList) { + settingArray.forEach preferenceLoop@{ preference -> + rvxSettingFile.writeText( + rvxSettingFile.readText() + .replace("", "") + ) + } + + addPreference(patch) + } + + fun updatePatchStatus(patchTitle: String) { + updatePatchStatusSettings(patchTitle, "@string/revanced_patches_included") + } + + fun updatePatchStatusIcon(iconName: String) { + iconType = iconName + updatePatchStatusSettings("Icon", "@string/revanced_icon_$iconName") + } + + fun updatePatchStatusLabel(appName: String) = + updatePatchStatusSettings("Label", appName) + + fun updatePatchStatusTheme(themeName: String) = + updatePatchStatusSettings("Theme", themeName) + + fun updatePatchStatusSettings( + patchTitle: String, + updateText: String + ) = context.apply { + document(RVX_PREFERENCE_PATH).use { document -> + document.doRecursively loop@{ + if (it !is Element) return@loop + + it.getAttributeNode("android:title")?.let { attribute -> + if (attribute.textContent == patchTitle) { + it.getAttributeNode("android:summary").textContent = updateText + } + } + } + } + } + + fun addPreferenceFragment(key: String, insertKey: String) = context.apply { + val targetClass = + "com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity" + + document(YOUTUBE_SETTINGS_PATH).use { document -> + with(document) { + val processedKeys = mutableSetOf() // To track processed keys + + doRecursively loop@{ node -> + if (node !is Element) return@loop // Skip if not an element + + val attributeNode = node.getAttributeNode("android:key") + ?: return@loop // Skip if no key attribute + val currentKey = attributeNode.textContent + + // Check if the current key has already been processed + if (processedKeys.contains(currentKey)) { + return@loop // Skip if already processed + } else { + processedKeys.add(currentKey) // Add the current key to processedKeys + } + + when (currentKey) { + insertKey -> { + node.insertNode("Preference", node) { + setAttribute("android:key", "${key}_key") + setAttribute("android:title", "@string/${key}_title") + this.appendChild( + ownerDocument.createElement("intent").also { intentNode -> + intentNode.setAttribute( + "android:targetPackage", + youtubePackageName + ) + intentNode.setAttribute("android:data", key + "_intent") + intentNode.setAttribute("android:targetClass", targetClass) + } + ) + } + node.setAttribute("app:iconSpaceReserved", "true") + } + + "true" -> { + attributeNode.textContent = "false" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt new file mode 100644 index 000000000..197059311 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt @@ -0,0 +1,337 @@ +package app.revanced.patches.youtube.utils.settings + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.booleanOption +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.shared.extension.Constants.EXTENSION_UTILS_CLASS_DESCRIPTOR +import app.revanced.patches.shared.extension.Constants.EXTENSION_UTILS_PATH +import app.revanced.patches.shared.mainactivity.injectConstructorMethodCall +import app.revanced.patches.shared.mainactivity.injectOnCreateMethodCall +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.youtube.utils.extension.sharedExtensionPatch +import app.revanced.patches.youtube.utils.fix.cairo.cairoSettingsPatch +import app.revanced.patches.youtube.utils.fix.playbackspeed.playbackSpeedWhilePlayingPatch +import app.revanced.patches.youtube.utils.fix.splash.darkModeSplashScreenPatch +import app.revanced.patches.youtube.utils.mainactivity.mainActivityResolvePatch +import app.revanced.patches.youtube.utils.patch.PatchList.SETTINGS_FOR_YOUTUBE +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.copyResources +import app.revanced.util.copyXmlNode +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.removeStringsElements +import app.revanced.util.valueOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import org.w3c.dom.Element +import java.nio.file.Files +import java.util.jar.Manifest + +private const val EXTENSION_INITIALIZATION_CLASS_DESCRIPTOR = + "$UTILS_PATH/InitializationPatch;" + +private const val EXTENSION_THEME_METHOD_DESCRIPTOR = + "$EXTENSION_UTILS_PATH/BaseThemeUtils;->setTheme(Ljava/lang/Enum;)V" + +private val settingsBytecodePatch = bytecodePatch( + description = "settingsBytecodePatch" +) { + dependsOn( + sharedExtensionPatch, + sharedResourceIdPatch, + mainActivityResolvePatch, + versionCheckPatch, + ) + + execute { + fun MutableMethod.injectCall(index: Int) { + val register = getInstruction(index).registerA + + addInstructions( + index + 1, """ + invoke-static {v$register}, $EXTENSION_THEME_METHOD_DESCRIPTOR + return-object v$register + """ + ) + removeInstruction(index) + } + + // apply the current theme of the settings page + themeSetterSystemFingerprint.matchOrThrow().let { + it.method.apply { + injectCall(implementation!!.instructions.size - 1) + injectCall(it.patternMatch!!.startIndex) + } + } + + injectOnCreateMethodCall( + EXTENSION_INITIALIZATION_CLASS_DESCRIPTOR, + "setExtendedUtils" + ) + injectOnCreateMethodCall( + EXTENSION_INITIALIZATION_CLASS_DESCRIPTOR, + "onCreate" + ) + injectConstructorMethodCall( + EXTENSION_UTILS_CLASS_DESCRIPTOR, + "setActivity" + ) + } +} + +private const val DEFAULT_ELEMENT = "@string/about_key" +private const val DEFAULT_LABEL = "ReVanced Extended" + +private val SETTINGS_ELEMENTS_MAP = mapOf( + "Parent settings" to "@string/parent_tools_key", + "General" to "@string/general_key", + "Account" to "@string/account_switcher_key", + "Data saving" to "@string/data_saving_settings_key", + "Autoplay" to "@string/auto_play_key", + "Video quality preferences" to "@string/video_quality_settings_key", + "Background" to "@string/offline_key", + "Watch on TV" to "@string/pair_with_tv_key", + "Manage all history" to "@string/history_key", + "Your data in YouTube" to "@string/your_data_key", + "Privacy" to "@string/privacy_key", + "History & privacy" to "@string/privacy_key", + "Try experimental new features" to "@string/premium_early_access_browse_page_key", + "Purchases and memberships" to "@string/subscription_product_setting_key", + "Billing & payments" to "@string/billing_and_payment_key", + "Billing and payments" to "@string/billing_and_payment_key", + "Notifications" to "@string/notification_key", + "Connected apps" to "@string/connected_accounts_browse_page_key", + "Live chat" to "@string/live_chat_key", + "Captions" to "@string/captions_key", + "Accessibility" to "@string/accessibility_settings_key", + "About" to DEFAULT_ELEMENT +) + +private lateinit var customName: String + +val settingsPatch = resourcePatch( + SETTINGS_FOR_YOUTUBE.title, + SETTINGS_FOR_YOUTUBE.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsBytecodePatch, + cairoSettingsPatch, + darkModeSplashScreenPatch, + playbackSpeedWhilePlayingPatch, + ) + + val insertPosition = stringOption( + key = "insertPosition", + default = DEFAULT_ELEMENT, + values = SETTINGS_ELEMENTS_MAP, + title = "Insert position", + description = "The settings menu name that the RVX settings menu should be above.", + required = true, + ) + + val settingsLabel = stringOption( + key = "settingsLabel", + default = DEFAULT_LABEL, + title = "RVX settings label", + description = "The name of the RVX settings menu.", + required = true, + ) + + val settingsSummaries by booleanOption( + key = "settingsSummaries", + default = true, + title = "RVX settings summaries", + description = "Shows the summary / description of each RVX setting. If set to false, no descriptions will be provided.", + required = true, + ) + + execute { + /** + * check patch options + */ + customName = settingsLabel + .valueOrThrow() + + val insertKey = insertPosition + .valueOrThrow() + + ResourceUtils.setContext(this) + + /** + * remove strings duplicated with RVX resources + * + * YouTube does not provide translations for these strings. + * That's why it's been added to RVX resources. + * This string also exists in RVX resources, so it must be removed to avoid being duplicated. + */ + removeStringsElements( + arrayOf("values"), + arrayOf( + "accessibility_settings_edu_opt_in_text", + "accessibility_settings_edu_opt_out_text" + ) + ) + + /** + * copy arrays, strings and preference + */ + arrayOf( + "arrays.xml", + "dimens.xml", + "strings.xml", + "styles.xml" + ).forEach { xmlFile -> + copyXmlNode("youtube/settings/host", "values/$xmlFile", "resources") + } + + val valuesV21Directory = get("res").resolve("values-v21") + if (!valuesV21Directory.isDirectory) + Files.createDirectories(valuesV21Directory.toPath()) + + copyResources( + "youtube/settings", + ResourceGroup( + "values-v21", + "strings.xml" + ) + ) + + arrayOf( + ResourceGroup( + "drawable", + "revanced_cursor.xml", + ), + ResourceGroup( + "layout", + "revanced_settings_preferences_category.xml", + "revanced_settings_with_toolbar.xml", + ), + ResourceGroup( + "xml", + "revanced_prefs.xml", + ) + ).forEach { resourceGroup -> + copyResources("youtube/settings", resourceGroup) + } + + /** + * initialize ReVanced Extended Settings + */ + ResourceUtils.addPreferenceFragment( + "revanced_extended_settings", + insertKey + ) + + /** + * remove ReVanced Extended Settings divider + */ + arrayOf("Theme.YouTube.Settings", "Theme.YouTube.Settings.Dark").forEach { themeName -> + document("res/values/styles.xml").use { document -> + with(document) { + val resourcesNode = getElementsByTagName("resources").item(0) as Element + + val newElement: Element = createElement("item") + newElement.setAttribute("name", "android:listDivider") + + for (i in 0 until resourcesNode.childNodes.length) { + val node = resourcesNode.childNodes.item(i) as? Element ?: continue + + if (node.getAttribute("name") == themeName) { + newElement.appendChild(createTextNode("@null")) + + node.appendChild(newElement) + } + } + } + } + } + + /** + * set revanced-patches version + */ + val patchManifest = object {}.javaClass.classLoader.getResources("META-INF/MANIFEST.MF") + while (patchManifest.hasMoreElements()) + ResourceUtils.updatePatchStatusSettings( + "ReVanced Patches", + Manifest(patchManifest.nextElement().openStream()) + .mainAttributes + .getValue("Version") + "" + ) + } + + finalize { + /** + * change RVX settings menu name + * since it must be invoked after the Translations patch, it must be the last in the order. + */ + if (customName != DEFAULT_LABEL) { + removeStringsElements( + arrayOf("revanced_extended_settings_title") + ) + document("res/values/strings.xml").use { document -> + mapOf( + "revanced_extended_settings_title" to customName + ).forEach { (k, v) -> + val stringElement = document.createElement("string") + + stringElement.setAttribute("name", k) + stringElement.textContent = v + + document.getElementsByTagName("resources").item(0) + .appendChild(stringElement) + } + } + } + + /** + * remove summaries from RVX settings + */ + if (settingsSummaries == false) { + document("res/xml/revanced_prefs.xml").use { document -> + with(document) { + // Get the root node of the XML document (in this case, "PreferenceScreen") + val rootElement = getElementsByTagName("PreferenceScreen").item(0) as Element + + // List of attributes to remove + val attributesToRemove = listOf("android:summary", "android:summaryOn", "android:summaryOff") + + // Define a recursive function to process each element + fun processElement(element: Element) { + // Skip elements with the HtmlPreference attribute to avoid errors + if (element.tagName == "app.revanced.extension.shared.settings.preference.HtmlPreference") { + return + } + + // Remove specified attributes if they exist + attributesToRemove.forEach { attribute -> + if (element.hasAttribute(attribute)) { + element.removeAttribute(attribute) + } + } + + // Process all child elements recursively + for (i in 0 until element.childNodes.length) { + val childNode = element.childNodes.item(i) + + if (childNode is Element) { + processElement(childNode) + } + } + } + + // Start processing from the root element + processElement(rootElement) + } + } + + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/Fingerprints.kt new file mode 100644 index 000000000..4a741bf40 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/Fingerprints.kt @@ -0,0 +1,37 @@ +package app.revanced.patches.youtube.utils.sponsorblock + +import app.revanced.patches.youtube.utils.extension.Constants.EXTENSION_PATH +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionReversed +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val rectangleFieldInvalidatorFingerprint = legacyFingerprint( + name = "rectangleFieldInvalidatorFingerprint", + returnType = "V", + parameters = emptyList(), + customFingerprint = { method, _ -> + indexOfInvalidateInstruction(method) >= 0 + } +) + +internal val segmentPlaybackControllerFingerprint = legacyFingerprint( + name = "segmentPlaybackControllerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("Ljava/lang/Object;"), + opcodes = listOf(Opcode.CONST_STRING), + customFingerprint = { method, _ -> + method.definingClass == "$EXTENSION_PATH/sponsorblock/SegmentPlaybackController;" + && method.name == "setSponsorBarRect" + } +) + +internal fun indexOfInvalidateInstruction(method: Method) = + method.indexOfFirstInstructionReversed { + getReference()?.name == "invalidate" + } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/SponsorBlockPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/SponsorBlockPatch.kt new file mode 100644 index 000000000..6e0a30d91 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/SponsorBlockPatch.kt @@ -0,0 +1,304 @@ +package app.revanced.patches.youtube.utils.sponsorblock + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.booleanOption +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.EXTENSION_PATH +import app.revanced.patches.youtube.utils.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.patch.PatchList.SPONSORBLOCK +import app.revanced.patches.youtube.utils.playercontrols.addTopControl +import app.revanced.patches.youtube.utils.playercontrols.hookTopControlButton +import app.revanced.patches.youtube.utils.playercontrols.playerControlsPatch +import app.revanced.patches.youtube.utils.resourceid.insetOverlayViewLayout +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.seekbarFingerprint +import app.revanced.patches.youtube.utils.seekbarOnDrawFingerprint +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.patches.youtube.utils.totalTimeFingerprint +import app.revanced.patches.youtube.utils.youtubeControlsOverlayFingerprint +import app.revanced.patches.youtube.video.information.hookVideoInformation +import app.revanced.patches.youtube.video.information.onCreateHook +import app.revanced.patches.youtube.video.information.videoEndMethod +import app.revanced.patches.youtube.video.information.videoInformationPatch +import app.revanced.patches.youtube.video.information.videoTimeHook +import app.revanced.util.* +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import org.w3c.dom.Element + +private const val EXTENSION_SPONSOR_BLOCK_PATH = + "$EXTENSION_PATH/sponsorblock" + +private const val EXTENSION_SPONSOR_BLOCK_UI_PATH = + "$EXTENSION_SPONSOR_BLOCK_PATH/ui" + +private const val EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR = + "$EXTENSION_SPONSOR_BLOCK_PATH/SegmentPlaybackController;" + +private const val EXTENSION_SPONSOR_BLOCK_VIEW_CONTROLLER_CLASS_DESCRIPTOR = + "$EXTENSION_SPONSOR_BLOCK_UI_PATH/SponsorBlockViewController;" + +val sponsorBlockBytecodePatch = bytecodePatch( + description = "sponsorBlockBytecodePatch" +) { + dependsOn( + sharedResourceIdPatch, + videoInformationPatch, + ) + + execute { + // Hook the video time method + videoTimeHook( + EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR, + "setVideoTime" + ) + // Initialize the player controller + onCreateHook( + EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR, + "initialize" + ) + + + seekbarOnDrawFingerprint.methodOrThrow(seekbarFingerprint).apply { + // Get left and right of seekbar rectangle + val moveObjectIndex = indexOfFirstInstructionOrThrow(Opcode.MOVE_OBJECT_FROM16) + + addInstruction( + moveObjectIndex + 1, + "invoke-static/range {p0 .. p0}, " + + "$EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->setSponsorBarRect(Ljava/lang/Object;)V" + ) + + // Set seekbar thickness + val roundIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "round" + } + 1 + val roundRegister = getInstruction(roundIndex).registerA + + addInstruction( + roundIndex + 1, + "invoke-static {v$roundRegister}, " + + "$EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->setSponsorBarThickness(I)V" + ) + + // Draw segment + val drawCircleIndex = indexOfFirstInstructionReversedOrThrow { + getReference()?.name == "drawCircle" + } + val drawCircleInstruction = getInstruction(drawCircleIndex) + addInstruction( + drawCircleIndex, + "invoke-static {v${drawCircleInstruction.registerC}, v${drawCircleInstruction.registerE}}, " + + "$EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->drawSponsorTimeBars(Landroid/graphics/Canvas;F)V" + ) + } + + // Voting & Shield button + setOf("CreateSegmentButtonController;", "VotingButtonController;").forEach { className -> + hookTopControlButton("$EXTENSION_SPONSOR_BLOCK_UI_PATH/$className") + } + + // Append timestamp + totalTimeFingerprint.methodOrThrow().apply { + val targetIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "getString" + } + 1 + val targetRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, """ + invoke-static {v$targetRegister}, $EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->appendTimeWithoutSegments(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$targetRegister + """ + ) + } + + // Initialize the SponsorBlock view + youtubeControlsOverlayFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = + indexOfFirstLiteralInstructionOrThrow(insetOverlayViewLayout) + val checkCastIndex = indexOfFirstInstructionOrThrow(targetIndex, Opcode.CHECK_CAST) + val targetRegister = + getInstruction(checkCastIndex).registerA + + addInstruction( + checkCastIndex + 1, + "invoke-static {v$targetRegister}, $EXTENSION_SPONSOR_BLOCK_VIEW_CONTROLLER_CLASS_DESCRIPTOR->initialize(Landroid/view/ViewGroup;)V" + ) + } + } + + // Replace strings + rectangleFieldInvalidatorFingerprint.methodOrThrow(seekbarFingerprint).apply { + val invalidateIndex = indexOfInvalidateInstruction(this) + val rectangleIndex = indexOfFirstInstructionReversedOrThrow(invalidateIndex + 1) { + getReference()?.type == "Landroid/graphics/Rect;" + } + val rectangleFieldName = + (getInstruction(rectangleIndex).reference as FieldReference).name + + segmentPlaybackControllerFingerprint.matchOrThrow().let { + it.method.apply { + val replaceIndex = it.patternMatch!!.startIndex + val replaceRegister = + getInstruction(replaceIndex).registerA + + replaceInstruction( + replaceIndex, + "const-string v$replaceRegister, \"$rectangleFieldName\"" + ) + } + } + } + + // The vote and create segment buttons automatically change their visibility when appropriate, + // but if buttons are showing when the end of the video is reached then they will not automatically hide. + // Add a hook to forcefully hide when the end of the video is reached. + videoEndMethod.addInstruction( + 0, + "invoke-static {}, $EXTENSION_SPONSOR_BLOCK_VIEW_CONTROLLER_CLASS_DESCRIPTOR->endOfVideoReached()V" + ) + + // Set current video id + hookVideoInformation("$EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->newVideoStarted(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") + + updatePatchStatus(PATCH_STATUS_CLASS_DESCRIPTOR, "SponsorBlock") + } +} + +private const val RIGHT = "right" + +@Suppress("unused") +val sponsorBlockPatch = resourcePatch( + SPONSORBLOCK.title, + SPONSORBLOCK.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + playerControlsPatch, + sponsorBlockBytecodePatch, + settingsPatch + ) + + val outlineIcon by booleanOption( + key = "outlineIcon", + default = true, + title = "Outline icons", + description = "Apply the outline icon.", + required = true + ) + + val newSegmentAlignment by stringOption( + key = "NewSegmentAlignment", + default = RIGHT, + values = mapOf( + "Right" to RIGHT, + "Left" to "left", + ), + title = "New segment alignment", + description = "Align new segment window.", + required = true + ) + + execute { + /** + * merge SponsorBlock drawables to main drawables + */ + arrayOf( + ResourceGroup( + "layout", + "revanced_sb_inline_sponsor_overlay.xml", + "revanced_sb_skip_sponsor_button.xml" + ), + ResourceGroup( + "drawable", + "revanced_sb_drag_handle.xml", + "revanced_sb_new_segment_background.xml", + "revanced_sb_skip_sponsor_button_background.xml" + ) + ).forEach { resourceGroup -> + copyResources("youtube/sponsorblock/shared", resourceGroup) + } + + if (outlineIcon == true) { + arrayOf( + ResourceGroup( + "layout", + "revanced_sb_new_segment.xml" + ), + ResourceGroup( + "drawable", + "revanced_sb_adjust.xml", + "revanced_sb_backward.xml", + "revanced_sb_compare.xml", + "revanced_sb_edit.xml", + "revanced_sb_forward.xml", + "revanced_sb_logo.xml", + "revanced_sb_publish.xml", + "revanced_sb_voting.xml" + ) + ).forEach { resourceGroup -> + copyResources("youtube/sponsorblock/outline", resourceGroup) + } + } else { + arrayOf( + ResourceGroup( + "layout", + "revanced_sb_new_segment.xml" + ), + ResourceGroup( + "drawable", + "revanced_sb_adjust.xml", + "revanced_sb_compare.xml", + "revanced_sb_edit.xml", + "revanced_sb_logo.xml", + "revanced_sb_publish.xml", + "revanced_sb_voting.xml" + ) + ).forEach { resourceGroup -> + copyResources("youtube/sponsorblock/default", resourceGroup) + } + } + + if (newSegmentAlignment == "left") { + document("res/layout/revanced_sb_inline_sponsor_overlay.xml").use { document -> + document.doRecursively loop@{ node -> + if (node is Element && node.tagName == "app.revanced.integrations.youtube.sponsorblock.ui.NewSegmentLayout") { + node.setAttribute("android:layout_alignParentRight", "false") + node.setAttribute("android:layout_alignParentLeft", "true") + } + } + } + } + + /** + * merge xml nodes from the host to their real xml files + */ + addTopControl("youtube/sponsorblock") + + /** + * Add settings + */ + addPreference( + arrayOf( + "PREFERENCE_SCREEN: SPONSOR_BLOCK" + ), + SPONSORBLOCK + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/Fingerprints.kt new file mode 100644 index 000000000..94c741f2e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/Fingerprints.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.youtube.utils.toolbar + +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val toolBarPatchFingerprint = legacyFingerprint( + name = "toolBarPatchFingerprint", + accessFlags = AccessFlags.PRIVATE or AccessFlags.STATIC, + customFingerprint = { method, _ -> + method.definingClass == "$UTILS_PATH/ToolBarPatch;" + && method.name == "hookToolBar" + } +) + + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/ToolBarHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/ToolBarHookPatch.kt new file mode 100644 index 000000000..feee4c16e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/ToolBarHookPatch.kt @@ -0,0 +1,73 @@ +package app.revanced.patches.youtube.utils.toolbar + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.youtube.utils.indexOfGetDrawableInstruction +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.toolBarButtonFingerprint +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$UTILS_PATH/ToolBarPatch;" + +private lateinit var toolbarMethod: MutableMethod + +val toolBarHookPatch = bytecodePatch( + description = "toolBarHookPatch" +) { + dependsOn(sharedResourceIdPatch) + + execute { + toolBarButtonFingerprint.methodOrThrow().apply { + val getDrawableIndex = indexOfGetDrawableInstruction(this) + val enumOrdinalIndex = indexOfFirstInstructionReversedOrThrow(getDrawableIndex) { + opcode == Opcode.INVOKE_INTERFACE && + getReference()?.returnType == "I" + } + val freeIndex = getDrawableIndex - 1 + + val replaceReference = getInstruction(enumOrdinalIndex).reference + val replaceRegister = + getInstruction(enumOrdinalIndex).registerC + val enumRegister = getInstruction(enumOrdinalIndex).registerD + val freeRegister = getInstruction(freeIndex).registerA + + val imageViewIndex = indexOfFirstInstructionOrThrow(enumOrdinalIndex) { + opcode == Opcode.IGET_OBJECT && + getReference()?.type == "Landroid/widget/ImageView;" + } + val imageViewReference = + getInstruction(imageViewIndex).reference + + addInstructions( + enumOrdinalIndex + 1, """ + iget-object v$freeRegister, p0, $imageViewReference + invoke-static {v$enumRegister, v$freeRegister}, $EXTENSION_CLASS_DESCRIPTOR->hookToolBar(Ljava/lang/Enum;Landroid/widget/ImageView;)V + invoke-interface {v$replaceRegister, v$enumRegister}, $replaceReference + """ + ) + removeInstruction(enumOrdinalIndex) + } + + toolbarMethod = toolBarPatchFingerprint.methodOrThrow() + } +} + +internal fun hookToolBar(descriptor: String) = + toolbarMethod.addInstructions( + 0, + "invoke-static {p0, p1}, $descriptor(Ljava/lang/String;Landroid/view/View;)V" + ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/trackingurlhook/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/trackingurlhook/Fingerprints.kt new file mode 100644 index 000000000..1da109cbe --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/trackingurlhook/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.youtube.utils.trackingurlhook + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val trackingUrlModelFingerprint = legacyFingerprint( + name = "trackingUrlModelFingerprint", + returnType = "Landroid/net/Uri;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + ), + customFingerprint = { method, _ -> + method.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/player/TrackingUrlModel;" + } +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/trackingurlhook/TrackingUrlHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/trackingurlhook/TrackingUrlHookPatch.kt new file mode 100644 index 000000000..cd9761c7b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/trackingurlhook/TrackingUrlHookPatch.kt @@ -0,0 +1,46 @@ +package app.revanced.patches.youtube.utils.trackingurlhook + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private lateinit var trackingUrlMethod: MutableMethod + +val trackingUrlHookPatch = bytecodePatch( + description = "trackingUrlHookPatch" +) { + execute { + trackingUrlMethod = trackingUrlModelFingerprint.methodOrThrow() + } +} + +internal fun hookTrackingUrl( + descriptor: String +) = trackingUrlMethod.apply { + val targetIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_STATIC && + getReference()?.name == "parse" + } + 1 + val targetRegister = getInstruction(targetIndex).registerA + + var smaliInstruction = "invoke-static {v$targetRegister}, $descriptor" + + if (!descriptor.endsWith("V")) { + smaliInstruction += """ + move-result-object v$targetRegister + + """.trimIndent() + } + + addInstructions( + targetIndex + 1, + smaliInstruction + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt new file mode 100644 index 000000000..9c2e47803 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt @@ -0,0 +1,189 @@ +package app.revanced.patches.youtube.video.information + +import app.revanced.patches.youtube.utils.PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.resourceid.notificationBigPictureIconWidth +import app.revanced.patches.youtube.utils.resourceid.qualityAuto +import app.revanced.util.containsLiteralInstruction +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val channelIdFingerprint = legacyFingerprint( + name = "channelIdFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Ljava/lang/Object;"), + strings = listOf("com.google.android.apps.youtube.mdx.watch.LAST_MEALBAR_PROMOTED_LIVE_FEED_CHANNELS") +) + +internal val channelNameFingerprint = legacyFingerprint( + name = "channelNameFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + strings = listOf( + "setMetadata may only be called once", + "Person", + ) +) + +internal val onPlaybackSpeedItemClickFingerprint = legacyFingerprint( + name = "onPlaybackSpeedItemClickFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "V", + parameters = listOf("Landroid/widget/AdapterView;", "Landroid/view/View;", "I", "J"), + customFingerprint = { method, _ -> + method.name == "onItemClick" && + method.indexOfFirstInstruction { + opcode == Opcode.IGET_OBJECT && + getReference()?.type == PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR + } >= 0 + } +) + +internal val playbackInitializationFingerprint = legacyFingerprint( + name = "playbackInitializationFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + strings = listOf("play() called when the player wasn\'t loaded."), + customFingerprint = { method, _ -> + indexOfPlayerResponseModelDirectInstruction(method) >= 0 + } +) + +internal fun indexOfPlayerResponseModelDirectInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_DIRECT && + getReference()?.returnType == PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR + } + +internal val playbackSpeedClassFingerprint = legacyFingerprint( + name = "playbackSpeedClassFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("L"), + opcodes = listOf(Opcode.RETURN_OBJECT), + strings = listOf("PLAYBACK_RATE_MENU_BOTTOM_SHEET_FRAGMENT") +) + +internal val playerControllerSetTimeReferenceFingerprint = legacyFingerprint( + name = "playerControllerSetTimeReferenceFingerprint", + opcodes = listOf( + Opcode.INVOKE_DIRECT_RANGE, + Opcode.IGET_OBJECT + ), + strings = listOf("Media progress reported outside media playback: ") +) + +internal val seekRelativeFingerprint = legacyFingerprint( + name = "seekRelativeFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + // returnType = "Z", ~ YouTube 19.39.39 + // returnType = "V", YouTube 19.40.xx ~ + parameters = listOf("J", "L"), + opcodes = listOf( + Opcode.ADD_LONG_2ADDR, + Opcode.INVOKE_VIRTUAL, + ) +) + +internal val videoIdFingerprint = legacyFingerprint( + name = "videoIdFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + strings = listOf("Failed to download video (IllegalStateException): %s") +) + +/** + * Renamed from VideoIdWithoutShortsFingerprint + */ +internal val videoIdFingerprintBackgroundPlay = legacyFingerprint( + name = "videoIdFingerprintBackgroundPlay", + returnType = "V", + parameters = listOf("L"), + opcodes = listOf( + Opcode.IF_EQZ, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IPUT_OBJECT, + Opcode.MONITOR_EXIT, + Opcode.RETURN_VOID, + Opcode.MONITOR_EXIT, + Opcode.RETURN_VOID + ), + customFingerprint = { method, classDef -> + method.name == "l" && + classDef.methods.count() == 17 && + method.implementation != null && + indexOfPlayerResponseModelInterfaceInstruction(method) >= 0 + } +) + +fun indexOfPlayerResponseModelInterfaceInstruction(methodDef: Method) = + methodDef.indexOfFirstInstruction { + opcode == Opcode.INVOKE_INTERFACE && + getReference()?.definingClass == PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR + } + +/** + * This fingerprint is compatible with all versions of YouTube starting from v18.29.38 to supported versions. + * This method is invoked only in Shorts. + * Accurate video information is invoked even when the user moves Shorts upward or downward. + */ +internal val videoIdFingerprintShorts = legacyFingerprint( + name = "videoIdFingerprintShorts", + returnType = "V", + parameters = listOf(PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR), + opcodes = listOf( + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT + ), + customFingerprint = custom@{ method, _ -> + if (method.containsLiteralInstruction(45365621L)) + return@custom true + + method.indexOfFirstInstruction { + getReference()?.name == "reelWatchEndpoint" + } >= 0 + } +) + +internal val videoQualityListFingerprint = legacyFingerprint( + name = "videoQualityListFingerprint", + returnType = "V", + parameters = listOf("L"), + opcodes = listOf( + Opcode.INVOKE_INTERFACE, + Opcode.RETURN_VOID + ), + literals = listOf(qualityAuto), +) + +internal val videoQualityTextFingerprint = legacyFingerprint( + name = "videoQualityTextFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("[L", "I", "Z"), + opcodes = listOf( + Opcode.IF_GE, + Opcode.AGET_OBJECT, + Opcode.IGET_OBJECT + ), + strings = listOf("menu_item_video_quality") +) + +internal val videoTitleFingerprint = legacyFingerprint( + name = "videoTitleFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(notificationBigPictureIconWidth), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt new file mode 100644 index 000000000..288fc190c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt @@ -0,0 +1,655 @@ +package app.revanced.patches.youtube.video.information + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableClass +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patcher.util.smali.toInstructions +import app.revanced.patches.shared.mdxPlayerDirectorSetVideoStageFingerprint +import app.revanced.patches.shared.videoLengthFingerprint +import app.revanced.patches.youtube.utils.PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.SHARED_PATH +import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.videoEndFingerprint +import app.revanced.patches.youtube.video.playerresponse.Hook +import app.revanced.patches.youtube.video.playerresponse.addPlayerResponseMethodHook +import app.revanced.patches.youtube.video.playerresponse.playerResponseMethodHookPatch +import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId +import app.revanced.patches.youtube.video.videoid.hookVideoId +import app.revanced.patches.youtube.video.videoid.videoIdPatch +import app.revanced.util.addStaticFieldToExtension +import app.revanced.util.cloneMutable +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.mutableClassOrThrow +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$SHARED_PATH/VideoInformation;" + +private const val REGISTER_PLAYER_RESPONSE_MODEL = 8 + +private const val REGISTER_CHANNEL_ID = 0 +private const val REGISTER_CHANNEL_NAME = 1 +private const val REGISTER_VIDEO_ID = 2 +private const val REGISTER_VIDEO_TITLE = 3 +private const val REGISTER_VIDEO_LENGTH = 4 + +@Suppress("unused") +private const val REGISTER_VIDEO_LENGTH_DUMMY = 5 +private const val REGISTER_VIDEO_IS_LIVE = 6 + +private lateinit var channelIdMethodCall: String +private lateinit var channelNameMethodCall: String +private lateinit var videoIdMethodCall: String +private lateinit var videoTitleMethodCall: String +private lateinit var videoLengthMethodCall: String +private lateinit var videoIsLiveMethodCall: String + +private lateinit var videoInformationMethod: MutableMethod +private lateinit var backgroundVideoInformationMethod: MutableMethod +private lateinit var shortsVideoInformationMethod: MutableMethod + +/** + * Used in [videoEndFingerprint] and [mdxPlayerDirectorSetVideoStageFingerprint]. + * Since both classes are inherited from the same class, + * [videoEndFingerprint] and [mdxPlayerDirectorSetVideoStageFingerprint] always have the same [seekSourceEnumType] and [seekSourceMethodName]. + */ +private var seekSourceEnumType = "" +private var seekSourceMethodName = "" +private var seekRelativeSourceMethodName = "" +private var cloneSeekRelativeSourceMethod = false + +private lateinit var playerConstructorMethod: MutableMethod +private var playerConstructorInsertIndex = -1 + +private lateinit var mdxConstructorMethod: MutableMethod +private var mdxConstructorInsertIndex = -1 + +private lateinit var videoTimeConstructorMethod: MutableMethod +private var videoTimeConstructorInsertIndex = 2 + +// Used by other patches. +internal lateinit var speedSelectionInsertMethod: MutableMethod +internal lateinit var videoEndMethod: MutableMethod + +val videoInformationPatch = bytecodePatch( + description = "videoInformationPatch", +) { + dependsOn( + playerResponseMethodHookPatch, + playerTypeHookPatch, + sharedResourceIdPatch, + videoIdPatch + ) + + execute { + fun cloneSeekRelativeSourceMethod(mutableClass: MutableClass) { + if (!cloneSeekRelativeSourceMethod) return + + val methods = mutableClass.methods + + methods.find { method -> + method.name == seekRelativeSourceMethodName + }?.apply { + methods.add( + cloneMutable( + returnType = "Z" + ).apply { + val lastIndex = implementation!!.instructions.lastIndex + + removeInstruction(lastIndex) + addInstructions( + lastIndex, """ + move-result p1 + return p1 + """ + ) + } + ) + } + } + + fun addSeekInterfaceMethods( + targetClass: MutableClass, + targetMethod: MutableMethod, + seekMethodName: String, + methodName: String, + fieldMethodName: String, + fieldName: String + ) { + targetMethod.apply { + targetClass.methods.add( + ImmutableMethod( + definingClass, + fieldMethodName, + listOf(ImmutableMethodParameter("J", annotations, "time")), + "Z", + AccessFlags.PUBLIC or AccessFlags.FINAL, + annotations, + null, + ImmutableMethodImplementation( + 4, """ + # first enum (field a) is SEEK_SOURCE_UNKNOWN + sget-object v0, $seekSourceEnumType->a:$seekSourceEnumType + invoke-virtual {p0, p1, p2, v0}, $definingClass->$seekMethodName(J$seekSourceEnumType)Z + move-result p1 + return p1 + """.toInstructions(), + null, + null + ) + ).toMutable() + ) + + val smaliInstructions = + """ + if-eqz v0, :ignore + invoke-virtual {v0, p0, p1}, $definingClass->$fieldMethodName(J)Z + move-result v0 + return v0 + :ignore + const/4 v0, 0x0 + return v0 + """ + + addStaticFieldToExtension( + EXTENSION_CLASS_DESCRIPTOR, + methodName, + fieldName, + definingClass, + smaliInstructions + ) + } + } + + fun Pair.getPlayerResponseInstruction( + returnType: String, + fromString: Boolean? = null + ): String { + methodOrThrow().apply { + val startIndex = if (fromString == true) + matchOrThrow().stringMatches!!.first().index + else + 0 + val targetReference = getInstruction( + indexOfFirstInstructionOrThrow(startIndex) { + val reference = getReference() + (opcode == Opcode.INVOKE_INTERFACE_RANGE || opcode == Opcode.INVOKE_INTERFACE) && + reference?.definingClass == PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR && + reference.returnType == returnType + } + ).reference + + return "invoke-interface {v$REGISTER_PLAYER_RESPONSE_MODEL}, $targetReference" + } + } + + videoEndFingerprint.methodOrThrow().apply { + findMethodOrThrow(definingClass).let { + playerConstructorMethod = it + playerConstructorInsertIndex = it.indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_DIRECT && getReference()?.name == "" + } + 1 + } + + // hook the player controller for use through extension + onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "initialize") + + seekSourceEnumType = parameterTypes[1].toString() + seekSourceMethodName = name + + seekRelativeFingerprint.methodOrThrow(videoEndFingerprint).also { method -> + seekRelativeSourceMethodName = method.name + cloneSeekRelativeSourceMethod = method.returnType == "V" + } + + cloneSeekRelativeSourceMethod(videoEndFingerprint.mutableClassOrThrow()) + + // Create extension interface methods. + addSeekInterfaceMethods( + videoEndFingerprint.mutableClassOrThrow(), + this, + seekSourceMethodName, + "overrideVideoTime", + "seekTo", + "videoInformationClass" + ) + addSeekInterfaceMethods( + seekRelativeFingerprint.mutableClassOrThrow(), + this, + seekRelativeSourceMethodName, + "overrideVideoTimeRelative", + "seekToRelative", + "videoInformationClass" + ) + + val literalIndex = indexOfFirstLiteralInstructionOrThrow(45368273L) + val walkerIndex = + indexOfFirstInstructionReversedOrThrow( + literalIndex, + Opcode.INVOKE_VIRTUAL_RANGE + ) + + videoEndMethod = getWalkerMethod(walkerIndex) + } + + mdxPlayerDirectorSetVideoStageFingerprint.methodOrThrow().apply { + findMethodOrThrow(definingClass).let { + mdxConstructorMethod = it + mdxConstructorInsertIndex = it.indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_DIRECT && getReference()?.name == "" + } + 1 + } + + // hook the MDX director for use through extension + onCreateHookMdx(EXTENSION_CLASS_DESCRIPTOR, "initializeMdx") + + cloneSeekRelativeSourceMethod(mdxPlayerDirectorSetVideoStageFingerprint.mutableClassOrThrow()) + + // Create extension interface methods. + addSeekInterfaceMethods( + mdxPlayerDirectorSetVideoStageFingerprint.mutableClassOrThrow(), + this, + seekSourceMethodName, + "overrideMDXVideoTime", + "seekTo", + "videoInformationMDXClass" + ) + addSeekInterfaceMethods( + mdxPlayerDirectorSetVideoStageFingerprint.mutableClassOrThrow(), + this, + seekRelativeSourceMethodName, + "overrideMDXVideoTimeRelative", + "seekToRelative", + "videoInformationMDXClass" + ) + } + + /** + * Set current video information + */ + channelIdMethodCall = + channelIdFingerprint.getPlayerResponseInstruction("Ljava/lang/String;") + channelNameMethodCall = + channelNameFingerprint.getPlayerResponseInstruction("Ljava/lang/String;", true) + videoIdMethodCall = videoIdFingerprint.getPlayerResponseInstruction("Ljava/lang/String;") + videoTitleMethodCall = + videoTitleFingerprint.getPlayerResponseInstruction("Ljava/lang/String;") + videoLengthMethodCall = videoLengthFingerprint.getPlayerResponseInstruction("J") + videoIsLiveMethodCall = channelIdFingerprint.getPlayerResponseInstruction("Z") + + playbackInitializationFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = indexOfPlayerResponseModelDirectInstruction(this) + 1 + val targetRegister = getInstruction(targetIndex).registerA + + addInstruction( + targetIndex + 1, + "invoke-direct {p0, v$targetRegister}, $definingClass->setVideoInformation($PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR)V" + ) + + videoInformationMethod = getVideoInformationMethod() + it.classDef.methods.add(videoInformationMethod) + + hookVideoInformation("$EXTENSION_CLASS_DESCRIPTOR->setVideoInformation(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") + } + } + + videoIdFingerprintBackgroundPlay.matchOrThrow().let { + it.method.apply { + val targetIndex = indexOfPlayerResponseModelInterfaceInstruction(this) + val targetRegister = getInstruction(targetIndex).registerC + + addInstruction( + targetIndex, + "invoke-direct {p0, v$targetRegister}, $definingClass->setVideoInformation($PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR)V" + ) + + backgroundVideoInformationMethod = getVideoInformationMethod() + it.classDef.methods.add(backgroundVideoInformationMethod) + } + } + + videoIdFingerprintShorts.matchOrThrow().let { + it.method.apply { + val targetIndex = indexOfPlayerResponseModelInterfaceInstruction(this) + val targetRegister = getInstruction(targetIndex).registerC + + addInstruction( + targetIndex, + "invoke-direct {p0, v$targetRegister}, $definingClass->setVideoInformation($PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR)V" + ) + + shortsVideoInformationMethod = getVideoInformationMethod() + it.classDef.methods.add(shortsVideoInformationMethod) + } + } + + /** + * Set current video time method + */ + playerControllerSetTimeReferenceFingerprint.matchOrThrow().let { + videoTimeConstructorMethod = + it.getWalkerMethod(it.patternMatch!!.startIndex) + } + + /** + * Set current video time + */ + videoTimeHook(EXTENSION_CLASS_DESCRIPTOR, "setVideoTime") + + /** + * Set current video id + */ + hookVideoId("$EXTENSION_CLASS_DESCRIPTOR->setVideoId(Ljava/lang/String;)V") + hookPlayerResponseVideoId( + "$EXTENSION_CLASS_DESCRIPTOR->setPlayerResponseVideoId(Ljava/lang/String;Z)V" + ) + // Call before any other video id hooks, + // so they can use VideoInformation and check if the video id is for a Short. + addPlayerResponseMethodHook( + Hook.PlayerParameterBeforeVideoId( + "$EXTENSION_CLASS_DESCRIPTOR->newPlayerResponseParameter(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)Ljava/lang/String;" + ) + ) + + /** + * Hook current playback speed + */ + onPlaybackSpeedItemClickFingerprint.matchOrThrow().let { + it.method.apply { + speedSelectionInsertMethod = this + val speedSelectionValueInstructionIndex = + indexOfFirstInstructionOrThrow(Opcode.IGET) + + val setPlaybackSpeedContainerClassFieldIndex = + indexOfFirstInstructionReversedOrThrow( + speedSelectionValueInstructionIndex, + Opcode.IGET_OBJECT + ) + val setPlaybackSpeedContainerClassFieldReference = + getInstruction(setPlaybackSpeedContainerClassFieldIndex).reference.toString() + + val setPlaybackSpeedClassFieldReference = + getInstruction(speedSelectionValueInstructionIndex + 1).reference.toString() + val setPlaybackSpeedMethodReference = + getInstruction(speedSelectionValueInstructionIndex + 2).reference.toString() + + // add override playback speed method + it.classDef.methods.add( + ImmutableMethod( + definingClass, + "overridePlaybackSpeed", + listOf(ImmutableMethodParameter("F", annotations, null)), + "V", + AccessFlags.PUBLIC or AccessFlags.PUBLIC, + annotations, + null, + ImmutableMethodImplementation( + 4, """ + # Check if the playback speed is not auto (-2.0f) + const/4 v0, 0x0 + cmpg-float v0, v3, v0 + if-lez v0, :ignore + + # Get the container class field. + iget-object v0, v2, $setPlaybackSpeedContainerClassFieldReference + + # Get the field from its class. + iget-object v1, v0, $setPlaybackSpeedClassFieldReference + + # Invoke setPlaybackSpeed on that class. + invoke-virtual {v1, v3}, $setPlaybackSpeedMethodReference + + :ignore + return-void + """.toInstructions(), null, null + ) + ).toMutable() + ) + + // set current playback speed + val walkerMethod = getWalkerMethod(speedSelectionValueInstructionIndex + 2) + walkerMethod.apply { + addInstruction( + this.implementation!!.instructions.size - 1, + "invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->setPlaybackSpeed(F)V" + ) + } + } + } + + playbackSpeedClassFingerprint.matchOrThrow().let { result -> + result.method.apply { + val index = result.patternMatch!!.endIndex + val register = getInstruction(index).registerA + val playbackSpeedClass = this.returnType + + // set playback speed class + replaceInstruction( + index, + "sput-object v$register, $EXTENSION_CLASS_DESCRIPTOR->playbackSpeedClass:$playbackSpeedClass" + ) + addInstruction( + index + 1, + "return-object v$register" + ) + + val smaliInstructions = + """ + if-eqz v0, :ignore + invoke-virtual {v0, p0}, $playbackSpeedClass->overridePlaybackSpeed(F)V + :ignore + return-void + """ + + addStaticFieldToExtension( + EXTENSION_CLASS_DESCRIPTOR, + "overridePlaybackSpeed", + "playbackSpeedClass", + playbackSpeedClass, + smaliInstructions, + false + ) + } + } + + /** + * Hook current video quality + */ + videoQualityListFingerprint.matchOrThrow().let { + val overrideMethod = + it.classDef.methods.find { method -> method.parameterTypes.first() == "I" } + + val videoQualityClass = it.method.definingClass + val videoQualityMethodName = overrideMethod?.name + ?: throw PatchException("Failed to find hook method") + + // set video quality array + it.method.apply { + val listIndex = it.patternMatch!!.startIndex + val listRegister = getInstruction(listIndex).registerD + + addInstruction( + listIndex, + "invoke-static {v$listRegister}, $EXTENSION_CLASS_DESCRIPTOR->setVideoQualityList([Ljava/lang/Object;)V" + ) + } + + val smaliInstructions = + """ + if-eqz v0, :ignore + invoke-virtual {v0, p0}, $videoQualityClass->$videoQualityMethodName(I)V + :ignore + return-void + """ + + addStaticFieldToExtension( + EXTENSION_CLASS_DESCRIPTOR, + "overrideVideoQuality", + "videoQualityClass", + videoQualityClass, + smaliInstructions + ) + } + + // set current video quality + videoQualityTextFingerprint.matchOrThrow().let { + it.method.apply { + val textIndex = it.patternMatch!!.endIndex + val textRegister = getInstruction(textIndex).registerA + + addInstruction( + textIndex + 1, + "invoke-static {v$textRegister}, $EXTENSION_CLASS_DESCRIPTOR->setVideoQuality(Ljava/lang/String;)V" + ) + } + } + } +} + +private fun MutableMethod.getVideoInformationMethod(): MutableMethod = + ImmutableMethod( + definingClass, + "setVideoInformation", + listOf( + ImmutableMethodParameter( + PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR, + annotations, + null + ) + ), + "V", + AccessFlags.PRIVATE or AccessFlags.FINAL, + annotations, + null, + ImmutableMethodImplementation( + REGISTER_PLAYER_RESPONSE_MODEL + 1, """ + $channelIdMethodCall + move-result-object v$REGISTER_CHANNEL_ID + $channelNameMethodCall + move-result-object v$REGISTER_CHANNEL_NAME + $videoIdMethodCall + move-result-object v$REGISTER_VIDEO_ID + $videoTitleMethodCall + move-result-object v$REGISTER_VIDEO_TITLE + $videoLengthMethodCall + move-result-wide v$REGISTER_VIDEO_LENGTH + $videoIsLiveMethodCall + move-result v$REGISTER_VIDEO_IS_LIVE + return-void + """.toInstructions(), + null, + null + ) + ).toMutable() + +private fun MutableMethod.insert(insertIndex: Int, register: String, descriptor: String) = + addInstruction(insertIndex, "invoke-static/range { $register }, $descriptor") + +/** + * Hook the player controller. Called when a video is opened or the current video is changed. + * + * Note: This hook is called very early and is called before the video id, video time, video length, + * and many other data fields are set. + * + * @param targetMethodClass The descriptor for the class to invoke when the player controller is created. + * @param targetMethodName The name of the static method to invoke when the player controller is created. + */ +internal fun onCreateHook(targetMethodClass: String, targetMethodName: String) = + playerConstructorMethod.addInstruction( + playerConstructorInsertIndex++, + "invoke-static { }, $targetMethodClass->$targetMethodName()V" + ) + +/** + * Hook the MDX player director. Called when playing videos while casting to a big screen device. + * + * @param targetMethodClass The descriptor for the class to invoke when the player controller is created. + * @param targetMethodName The name of the static method to invoke when the player controller is created. + */ +internal fun onCreateHookMdx(targetMethodClass: String, targetMethodName: String) = + mdxConstructorMethod.addInstruction( + mdxConstructorInsertIndex++, + "invoke-static { }, $targetMethodClass->$targetMethodName()V" + ) + +/** + * Hook the video time. + * The hook is usually called once per second. + * + * @param targetMethodClass The descriptor for the static method to invoke when the player controller is created. + * @param targetMethodName The name of the static method to invoke when the player controller is created. + */ +internal fun videoTimeHook(targetMethodClass: String, targetMethodName: String) = + videoTimeConstructorMethod.addInstruction( + videoTimeConstructorInsertIndex++, + "invoke-static { p1, p2 }, $targetMethodClass->$targetMethodName(J)V" + ) + +/** + * This method is invoked on both regular videos and Shorts. + */ +internal fun hookVideoInformation(descriptor: String) = + videoInformationMethod.apply { + val index = implementation!!.instructions.lastIndex + + insert( + index, + "v$REGISTER_CHANNEL_ID .. v$REGISTER_VIDEO_IS_LIVE", + descriptor + ) + } + +/** + * This method is invoked only in regular videos. + */ +internal fun hookBackgroundPlayVideoInformation(descriptor: String) = + backgroundVideoInformationMethod.apply { + val index = implementation!!.instructions.lastIndex + + insert( + index, + "v$REGISTER_CHANNEL_ID .. v$REGISTER_VIDEO_IS_LIVE", + descriptor + ) + } + +/** + * This method is invoked only in shorts videos. + */ +internal fun hookShortsVideoInformation(descriptor: String) = + shortsVideoInformationMethod.apply { + val index = implementation!!.instructions.lastIndex + + insert( + index, + "v$REGISTER_CHANNEL_ID .. v$REGISTER_VIDEO_IS_LIVE", + descriptor + ) + } \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playback/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playback/Fingerprints.kt new file mode 100644 index 000000000..28ede6d2b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playback/Fingerprints.kt @@ -0,0 +1,130 @@ +package app.revanced.patches.youtube.video.playback + +import app.revanced.util.containsLiteralInstruction +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val av1CodecFingerprint = legacyFingerprint( + name = "av1CodecFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + returnType = "L", + strings = listOf("AtomParsers", "video/av01"), + customFingerprint = { method, _ -> + method.returnType != "Ljava/util/List;" && + method.containsLiteralInstruction(1987076931L) + } +) + +internal val byteBufferArrayFingerprint = legacyFingerprint( + name = "byteBufferArrayFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "I", + parameters = emptyList(), + opcodes = listOf( + Opcode.SHL_INT_LIT8, + Opcode.SHL_INT_LIT8, + Opcode.OR_INT_2ADDR, + Opcode.SHL_INT_LIT8, + Opcode.OR_INT_2ADDR, + Opcode.OR_INT_2ADDR, + Opcode.RETURN + ) +) + +internal val byteBufferArrayParentFingerprint = legacyFingerprint( + name = "byteBufferArrayParentFingerprint", + accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, + returnType = "C", + parameters = listOf("Ljava/nio/charset/Charset;", "[C") +) + +internal val deviceDimensionsModelToStringFingerprint = legacyFingerprint( + name = "deviceDimensionsModelToStringFingerprint", + returnType = "L", + strings = listOf("minh.", ";maxh.") +) + +internal val hdrCapabilityFingerprint = legacyFingerprint( + name = "hdrCapabilityFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + strings = listOf( + "av1_profile_main_10_hdr_10_plus_supported", + "video/av01" + ) +) + +internal val playbackSpeedChangedFromRecyclerViewFingerprint = legacyFingerprint( + name = "playbackSpeedChangedFromRecyclerViewFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IGET, + Opcode.INVOKE_VIRTUAL + ) +) + +internal val playbackSpeedInitializeFingerprint = legacyFingerprint( + name = "playbackSpeedInitializeFingerprint", + returnType = "F", + accessFlags = AccessFlags.PRIVATE or AccessFlags.STATIC, + parameters = listOf("L"), + opcodes = listOf( + Opcode.IGET, + Opcode.RETURN + ) +) + +internal val qualityChangedFromRecyclerViewFingerprint = legacyFingerprint( + name = "qualityChangedFromRecyclerViewFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.IGET, // Video resolution (human readable). + Opcode.IGET_OBJECT, + Opcode.IGET_BOOLEAN, + Opcode.IGET_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_DIRECT, + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.GOTO, + Opcode.CONST_4, + Opcode.IF_NE, + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IGET, + ) +) + +internal val qualitySetterFingerprint = legacyFingerprint( + name = "qualitySetterFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + strings = listOf("VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT") +) + +internal val vp9CapabilityFingerprint = legacyFingerprint( + name = "vp9CapabilityFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "Z", + strings = listOf( + "vp9_supported", + "video/x-vnd.on2.vp9" + ) +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playback/VideoPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playback/VideoPlaybackPatch.kt new file mode 100644 index 000000000..ccebc248a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playback/VideoPlaybackPatch.kt @@ -0,0 +1,337 @@ +package app.revanced.patches.youtube.video.playback + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.shared.customspeed.customPlaybackSpeedPatch +import app.revanced.patches.shared.litho.addLithoFilter +import app.revanced.patches.shared.litho.lithoFilterPatch +import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.youtube.utils.extension.Constants.COMPONENTS_PATH +import app.revanced.patches.youtube.utils.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.VIDEO_PATH +import app.revanced.patches.youtube.utils.fix.shortsplayback.shortsPlaybackPatch +import app.revanced.patches.youtube.utils.flyoutmenu.flyoutMenuHookPatch +import app.revanced.patches.youtube.utils.patch.PatchList.VIDEO_PLAYBACK +import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.utils.qualityMenuViewInflateFingerprint +import app.revanced.patches.youtube.utils.recyclerview.recyclerViewTreeObserverHook +import app.revanced.patches.youtube.utils.recyclerview.recyclerViewTreeObserverPatch +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.patches.youtube.utils.videoEndFingerprint +import app.revanced.patches.youtube.video.information.hookBackgroundPlayVideoInformation +import app.revanced.patches.youtube.video.information.hookVideoInformation +import app.revanced.patches.youtube.video.information.onCreateHook +import app.revanced.patches.youtube.video.information.speedSelectionInsertMethod +import app.revanced.patches.youtube.video.information.videoInformationPatch +import app.revanced.patches.youtube.video.videoid.hookPlayerResponseVideoId +import app.revanced.patches.youtube.video.videoid.videoIdPatch +import app.revanced.util.findMethodOrThrow +import app.revanced.util.fingerprint.definingClassOrThrow +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.resolvable +import app.revanced.util.getReference +import app.revanced.util.getWalkerMethod +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstStringInstructionOrThrow +import app.revanced.util.updatePatchStatus +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val PLAYBACK_SPEED_MENU_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/PlaybackSpeedMenuFilter;" +private const val VIDEO_QUALITY_MENU_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/VideoQualityMenuFilter;" +private const val EXTENSION_AV1_CODEC_CLASS_DESCRIPTOR = + "$VIDEO_PATH/AV1CodecPatch;" +private const val EXTENSION_VP9_CODEC_CLASS_DESCRIPTOR = + "$VIDEO_PATH/VP9CodecPatch;" +private const val EXTENSION_CUSTOM_PLAYBACK_SPEED_CLASS_DESCRIPTOR = + "$VIDEO_PATH/CustomPlaybackSpeedPatch;" +private const val EXTENSION_HDR_VIDEO_CLASS_DESCRIPTOR = + "$VIDEO_PATH/HDRVideoPatch;" +private const val EXTENSION_PLAYBACK_SPEED_CLASS_DESCRIPTOR = + "$VIDEO_PATH/PlaybackSpeedPatch;" +private const val EXTENSION_RELOAD_VIDEO_CLASS_DESCRIPTOR = + "$VIDEO_PATH/ReloadVideoPatch;" +private const val EXTENSION_RESTORE_OLD_VIDEO_QUALITY_MENU_CLASS_DESCRIPTOR = + "$VIDEO_PATH/RestoreOldVideoQualityMenuPatch;" +private const val EXTENSION_SPOOF_DEVICE_DIMENSIONS_CLASS_DESCRIPTOR = + "$VIDEO_PATH/SpoofDeviceDimensionsPatch;" +private const val EXTENSION_VIDEO_QUALITY_CLASS_DESCRIPTOR = + "$VIDEO_PATH/VideoQualityPatch;" + +@Suppress("unused") +val videoPlaybackPatch = bytecodePatch( + VIDEO_PLAYBACK.title, + VIDEO_PLAYBACK.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + customPlaybackSpeedPatch( + "$VIDEO_PATH/CustomPlaybackSpeedPatch;", + 8.0f + ), + flyoutMenuHookPatch, + lithoFilterPatch, + playerTypeHookPatch, + recyclerViewTreeObserverPatch, + shortsPlaybackPatch, + videoIdPatch, + videoInformationPatch, + sharedResourceIdPatch, + ) + + execute { + + var settingArray = arrayOf( + "PREFERENCE_SCREEN: VIDEO" + ) + + // region patch for custom playback speed + + recyclerViewTreeObserverHook("$EXTENSION_CUSTOM_PLAYBACK_SPEED_CLASS_DESCRIPTOR->onFlyoutMenuCreate(Landroid/support/v7/widget/RecyclerView;)V") + addLithoFilter(PLAYBACK_SPEED_MENU_FILTER_CLASS_DESCRIPTOR) + + // endregion + + // region patch for disable HDR video + + hdrCapabilityFingerprint.methodOrThrow().apply { + val stringIndex = + indexOfFirstStringInstructionOrThrow("av1_profile_main_10_hdr_10_plus_supported") + val walkerIndex = indexOfFirstInstructionOrThrow(stringIndex) { + val reference = getReference() + reference?.parameterTypes == listOf("I", "Landroid/view/Display;") && + reference.returnType == "Z" + } + + val walkerMethod = getWalkerMethod(walkerIndex) + walkerMethod.apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $EXTENSION_HDR_VIDEO_CLASS_DESCRIPTOR->disableHDRVideo()Z + move-result v0 + if-nez v0, :default + return v0 + """, ExternalLabel("default", getInstruction(0)) + ) + } + } + + // endregion + + // region patch for default playback speed + + val newMethod = + playbackSpeedChangedFromRecyclerViewFingerprint.methodOrThrow( + qualityChangedFromRecyclerViewFingerprint + ) + + arrayOf( + newMethod, + speedSelectionInsertMethod + ).forEach { + it.apply { + val speedSelectionValueInstructionIndex = + indexOfFirstInstructionOrThrow(Opcode.IGET) + val speedSelectionValueRegister = + getInstruction(speedSelectionValueInstructionIndex).registerA + + addInstruction( + speedSelectionValueInstructionIndex + 1, + "invoke-static {v$speedSelectionValueRegister}, " + + "$EXTENSION_PLAYBACK_SPEED_CLASS_DESCRIPTOR->userSelectedPlaybackSpeed(F)V" + ) + } + } + + playbackSpeedInitializeFingerprint.matchOrThrow(videoEndFingerprint).let { + it.method.apply { + val insertIndex = it.patternMatch!!.endIndex + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $EXTENSION_PLAYBACK_SPEED_CLASS_DESCRIPTOR->getPlaybackSpeedInShorts(F)F + move-result v$insertRegister + """ + ) + } + } + + hookBackgroundPlayVideoInformation("$EXTENSION_PLAYBACK_SPEED_CLASS_DESCRIPTOR->newVideoStarted(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") + hookPlayerResponseVideoId("$EXTENSION_PLAYBACK_SPEED_CLASS_DESCRIPTOR->fetchPlaylistData(Ljava/lang/String;Z)V") + + updatePatchStatus(PATCH_STATUS_CLASS_DESCRIPTOR, "RememberPlaybackSpeed") + + // endregion + + // region patch for default video quality + + qualityChangedFromRecyclerViewFingerprint.matchOrThrow().let { + it.method.apply { + val index = it.patternMatch!!.startIndex + + addInstruction( + index + 1, + "invoke-static {}, $EXTENSION_VIDEO_QUALITY_CLASS_DESCRIPTOR->userSelectedVideoQuality()V" + ) + + } + } + + qualitySetterFingerprint.matchOrThrow().let { + val onItemClickMethod = + it.classDef.methods.find { method -> method.name == "onItemClick" } + + onItemClickMethod?.apply { + addInstruction( + 0, + "invoke-static {}, $EXTENSION_VIDEO_QUALITY_CLASS_DESCRIPTOR->userSelectedVideoQuality()V" + ) + } ?: throw PatchException("Failed to find onItemClick method") + } + + hookBackgroundPlayVideoInformation("$EXTENSION_RELOAD_VIDEO_CLASS_DESCRIPTOR->newVideoStarted(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") + hookVideoInformation("$EXTENSION_VIDEO_QUALITY_CLASS_DESCRIPTOR->newVideoStarted(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") + onCreateHook( + EXTENSION_VIDEO_QUALITY_CLASS_DESCRIPTOR, + "newVideoStarted" + ) + + // endregion + + // region patch for restore old video quality menu + + qualityMenuViewInflateFingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = indexOfFirstInstructionOrThrow(Opcode.CHECK_CAST) + val insertRegister = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex + 1, + "invoke-static { v$insertRegister }, " + + "$EXTENSION_RESTORE_OLD_VIDEO_QUALITY_MENU_CLASS_DESCRIPTOR->restoreOldVideoQualityMenu(Landroid/widget/ListView;)V" + ) + } + val onItemClickMethod = + it.classDef.methods.find { method -> method.name == "onItemClick" } + + onItemClickMethod?.apply { + val insertIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT) + val insertRegister = getInstruction(insertIndex).registerA + + val jumpIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.IGET_OBJECT + && this.getReference()?.type == qualitySetterFingerprint.definingClassOrThrow() + } + + addInstructionsWithLabels( + insertIndex, """ + invoke-static {}, $EXTENSION_RESTORE_OLD_VIDEO_QUALITY_MENU_CLASS_DESCRIPTOR->restoreOldVideoQualityMenu()Z + move-result v$insertRegister + if-nez v$insertRegister, :show + """, ExternalLabel("show", getInstruction(jumpIndex)) + ) + } ?: throw PatchException("Failed to find onItemClick method") + } + + recyclerViewTreeObserverHook("$EXTENSION_RESTORE_OLD_VIDEO_QUALITY_MENU_CLASS_DESCRIPTOR->onFlyoutMenuCreate(Landroid/support/v7/widget/RecyclerView;)V") + addLithoFilter(VIDEO_QUALITY_MENU_FILTER_CLASS_DESCRIPTOR) + + // endregion + + // region patch for spoof device dimensions + + findMethodOrThrow( + deviceDimensionsModelToStringFingerprint.definingClassOrThrow() + ).addInstructions( + 1, // Add after super call. + mapOf( + 1 to "MinHeightOrWidth", // p1 = min height + 2 to "MaxHeightOrWidth", // p2 = max height + 3 to "MinHeightOrWidth", // p3 = min width + 4 to "MaxHeightOrWidth" // p4 = max width + ).map { (parameter, method) -> + """ + invoke-static { p$parameter }, $EXTENSION_SPOOF_DEVICE_DIMENSIONS_CLASS_DESCRIPTOR->get$method(I)I + move-result p$parameter + """ + }.joinToString("\n") { it } + ) + + // endregion + + // region patch for disable AV1 codec + + // replace av1 codec + + if (av1CodecFingerprint.resolvable()) { + av1CodecFingerprint.methodOrThrow().apply { + val insertIndex = indexOfFirstStringInstructionOrThrow("video/av01") + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex + 1, """ + invoke-static/range {v$insertRegister .. v$insertRegister}, $EXTENSION_AV1_CODEC_CLASS_DESCRIPTOR->replaceCodec(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$insertRegister + """ + ) + } + settingArray += "SETTINGS: REPLACE_AV1_CODEC" + } + + // reject av1 codec response + + byteBufferArrayFingerprint.matchOrThrow(byteBufferArrayParentFingerprint).let { + it.method.apply { + val insertIndex = it.patternMatch!!.endIndex + val insertRegister = + getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, """ + invoke-static {v$insertRegister}, $EXTENSION_AV1_CODEC_CLASS_DESCRIPTOR->rejectResponse(I)I + move-result v$insertRegister + """ + ) + } + } + + // endregion + + // region patch for disable VP9 codec + + vp9CapabilityFingerprint.methodOrThrow().apply { + addInstructionsWithLabels( + 0, """ + invoke-static {}, $EXTENSION_VP9_CODEC_CLASS_DESCRIPTOR->disableVP9Codec()Z + move-result v0 + if-nez v0, :default + return v0 + """, ExternalLabel("default", getInstruction(0)) + ) + } + + // endregion + + // region add settings + + addPreference(settingArray, VIDEO_PLAYBACK) + + // endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt new file mode 100644 index 000000000..02e4b048f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt @@ -0,0 +1,79 @@ +package app.revanced.patches.youtube.video.playerresponse + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import app.revanced.util.parametersEqual +import com.android.tools.smali.dexlib2.AccessFlags + +private val PLAYER_PARAMETER_STARTS_WITH_PARAMETER_LIST = listOf( + "Ljava/lang/String;", // VideoId. + "[B", + "Ljava/lang/String;", // Player parameters proto buffer. + "Ljava/lang/String;", // PlaylistId. + "I", + "I" +) +private val PLAYER_PARAMETER_ENDS_WITH_PARAMETER_LIST = listOf( + "Ljava/util/Set;", + "Ljava/lang/String;", + "Ljava/lang/String;", + "L", + "Z", // Appears to indicate if the video id is being opened or is currently playing. + "Z", + "Z" +) + +internal val playerParameterBuilderFingerprint = legacyFingerprint( + name = "playerParameterBuilderFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "L", + // 19.22 and earlier parameters are: + // "Ljava/lang/String;", // VideoId. + // "[B", + // "Ljava/lang/String;", // Player parameters proto buffer. + // "Ljava/lang/String;", // PlaylistId. + // "I", + // "I", + // "Ljava/util/Set;", + // "Ljava/lang/String;", + // "Ljava/lang/String;", + // "L", + // "Z", // Appears to indicate if the video id is being opened or is currently playing. + // "Z", + // "Z" + + // 19.23+ parameters are: + // "Ljava/lang/String;", // VideoId. + // "[B", + // "Ljava/lang/String;", // Player parameters proto buffer. + // "Ljava/lang/String;", // PlaylistId. + // "I", + // "I", + // "L", + // "Ljava/util/Set;", + // "Ljava/lang/String;", + // "Ljava/lang/String;", + // "L", + // "Z", // Appears to indicate if the video id is being opened or is currently playing. + // "Z", + // "Z" + customFingerprint = custom@{ method, _ -> + val parameterTypes = method.parameterTypes + val parameterSize = parameterTypes.size + if (parameterSize != 13 && parameterSize != 14) { + return@custom false + } + + val startsWithMethodParameterList = parameterTypes.slice(0..5) + val endsWithMethodParameterList = parameterTypes.slice(parameterSize - 7..() + +fun addPlayerResponseMethodHook(hook: Hook) { + hooks += hook +} + +// Parameter numbers of the patched method. +private var parameterVideoId = 1 +private var parameterPlayerParameter = 3 +private var parameterPlaylistId = 4 +private var parameterIsShortAndOpeningOrPlaying by Delegates.notNull() + +// Registers used to pass the parameters to the extension. +private var playerResponseMethodCopyRegisters = false +private lateinit var registerVideoId: String +private lateinit var registerPlayerParameter: String +private lateinit var registerPlaylistId: String +private lateinit var registerIsShortAndOpeningOrPlaying: String + +private lateinit var playerResponseMethod: MutableMethod +private var numberOfInstructionsAdded = 0 + +val playerResponseMethodHookPatch = bytecodePatch( + description = "playerResponseMethodHookPatch" +) { + execute { + playerParameterBuilderFingerprint.methodOrThrow().apply { + playerResponseMethod = this + parameterIsShortAndOpeningOrPlaying = parameters.size - 2 + // On some app targets the method has too many registers pushing the parameters past v15. + // If needed, move the parameters to 4-bit registers so they can be passed to extension. + playerResponseMethodCopyRegisters = implementation!!.registerCount - + parameterTypes.size + parameterIsShortAndOpeningOrPlaying > 15 + } + + if (playerResponseMethodCopyRegisters) { + registerVideoId = "v0" + registerPlayerParameter = "v1" + registerPlaylistId = "v2" + registerIsShortAndOpeningOrPlaying = "v3" + } else { + registerVideoId = "p$parameterVideoId" + registerPlayerParameter = "p$parameterPlayerParameter" + registerPlaylistId = "p$parameterPlaylistId" + registerIsShortAndOpeningOrPlaying = "p$parameterIsShortAndOpeningOrPlaying" + } + } + + finalize { + fun hookVideoId(hook: Hook) { + playerResponseMethod.addInstruction( + 0, + "invoke-static {$registerVideoId, $registerIsShortAndOpeningOrPlaying}, $hook", + ) + numberOfInstructionsAdded++ + } + + fun hookPlayerParameter(hook: Hook) { + playerResponseMethod.addInstructions( + 0, + """ + invoke-static {$registerVideoId, $registerPlayerParameter, $registerPlaylistId, $registerIsShortAndOpeningOrPlaying}, $hook + move-result-object $registerPlayerParameter + """, + ) + numberOfInstructionsAdded += 2 + } + + // Reverse the order in order to preserve insertion order of the hooks. + val beforeVideoIdHooks = + hooks.filterIsInstance().asReversed() + val videoIdHooks = hooks.filterIsInstance().asReversed() + val afterVideoIdHooks = hooks.filterIsInstance().asReversed() + + // Add the hooks in this specific order as they insert instructions at the beginning of the method. + afterVideoIdHooks.forEach(::hookPlayerParameter) + videoIdHooks.forEach(::hookVideoId) + beforeVideoIdHooks.forEach(::hookPlayerParameter) + + if (playerResponseMethodCopyRegisters) { + playerResponseMethod.apply { + addInstructions( + 0, + """ + move-object/from16 $registerVideoId, p$parameterVideoId + move-object/from16 $registerPlayerParameter, p$parameterPlayerParameter + move-object/from16 $registerPlaylistId, p$parameterPlaylistId + move/from16 $registerIsShortAndOpeningOrPlaying, p$parameterIsShortAndOpeningOrPlaying + """, + ) + + numberOfInstructionsAdded += 4 + + // Move the modified register back. + addInstruction( + numberOfInstructionsAdded, + "move-object/from16 p$parameterPlayerParameter, $registerPlayerParameter" + ) + } + } + } +} + +sealed class Hook(private val methodDescriptor: String) { + class VideoId(methodDescriptor: String) : Hook(methodDescriptor) + + class PlayerParameter(methodDescriptor: String) : Hook(methodDescriptor) + class PlayerParameterBeforeVideoId(methodDescriptor: String) : Hook(methodDescriptor) + + override fun toString() = methodDescriptor +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/Fingerprints.kt new file mode 100644 index 000000000..3567bf0fd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/Fingerprints.kt @@ -0,0 +1,50 @@ +package app.revanced.patches.youtube.video.videoid + +import app.revanced.patches.youtube.utils.PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val videoIdFingerprint = legacyFingerprint( + name = "videoIdFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + opcodes = listOf( + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT + ), + customFingerprint = custom@{ method, classDef -> + if (!classDef.fields.any { it.type == "Lcom/google/android/libraries/youtube/player/subtitles/model/SubtitleTrack;" }) { + return@custom false + } + val implementation = method.implementation + ?: return@custom false + val instructions = implementation.instructions + val instructionCount = instructions.count() + if (instructionCount < 30) { + return@custom false + } + + val reference = + (instructions.elementAt(instructionCount - 2) as? ReferenceInstruction)?.reference.toString() + if (reference != "Ljava/util/Map;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;") { + return@custom false + } + + method.indexOfFirstInstruction { + val methodReference = getReference() + opcode == Opcode.INVOKE_INTERFACE && + methodReference?.returnType == "Ljava/lang/String;" && + methodReference.parameterTypes.isEmpty() && + methodReference.definingClass == PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR + } >= 0 + }, +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/VideoIdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/VideoIdPatch.kt new file mode 100644 index 000000000..57034e60d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/VideoIdPatch.kt @@ -0,0 +1,94 @@ +package app.revanced.patches.youtube.video.videoid + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.youtube.utils.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.video.playerresponse.Hook +import app.revanced.patches.youtube.video.playerresponse.addPlayerResponseMethodHook +import app.revanced.patches.youtube.video.playerresponse.playerResponseMethodHookPatch +import app.revanced.util.fingerprint.matchOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private var videoIdRegister = 0 +private var videoIdInsertIndex = 0 +private lateinit var videoIdMethod: MutableMethod + +val videoIdPatch = bytecodePatch( + description = "videoIdPatch", +) { + dependsOn(playerResponseMethodHookPatch) + + execute { + /** + * Supplies the method and register index of the video id register. + * + * @param consumer Consumer that receives the method, insert index and video id register index. + */ + fun Pair.setFields(consumer: (MutableMethod, Int, Int) -> Unit) = + matchOrThrow().let { result -> + val videoIdRegisterIndex = result.patternMatch!!.endIndex + + result.method.let { + val videoIdRegister = + it.getInstruction(videoIdRegisterIndex).registerA + val insertIndex = videoIdRegisterIndex + 1 + consumer(it, insertIndex, videoIdRegister) + } + } + + videoIdFingerprint.setFields { method, index, register -> + videoIdMethod = method + videoIdInsertIndex = index + videoIdRegister = register + } + } +} + +/** + * Hooks the new video id when the video changes. + * + * Supports all videos (regular videos and Shorts). + * + * _Does not function if playing in the background with no video visible_. + * + * Be aware, this can be called multiple times for the same video id. + * + * @param methodDescriptor which method to call. Params have to be `Ljava/lang/String;` + */ +internal fun hookVideoId( + methodDescriptor: String +) = videoIdMethod.addInstruction( + videoIdInsertIndex++, + "invoke-static {v$videoIdRegister}, $methodDescriptor" +) + +/** + * Hooks the video id of every video when loaded. + * Supports all videos and functions in all situations. + * + * First parameter is the video id. + * Second parameter is if the video is a Short AND it is being opened or is currently playing. + * + * Hook is always called off the main thread. + * + * This hook is called as soon as the player response is parsed, + * and called before many other hooks are updated such as [playerTypeHookPatch]. + * + * Note: The video id returned here may not be the current video that's being played. + * It's common for multiple Shorts to load at once in preparation + * for the user swiping to the next Short. + * + * For most use cases, you probably want to use [hookVideoId] instead. + * + * Be aware, this can be called multiple times for the same video id. + * + * @param methodDescriptor which method to call. Params must be `Ljava/lang/String;Z` + */ +internal fun hookPlayerResponseVideoId(methodDescriptor: String) = addPlayerResponseMethodHook( + Hook.VideoId( + methodDescriptor, + ), +) diff --git a/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt new file mode 100644 index 000000000..9f9c0989d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -0,0 +1,727 @@ +@file:Suppress("CONTEXT_RECEIVERS_DEPRECATED") + +package app.revanced.util + +import app.revanced.patcher.FingerprintBuilder +import app.revanced.patcher.Match +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.util.proxy.mutableTypes.MutableClass +import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patches.shared.mapping.get +import app.revanced.patches.shared.mapping.resourceMappingPatch +import app.revanced.patches.shared.mapping.resourceMappings +import app.revanced.util.Utils.printWarn +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.MethodParameter +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.Instruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction31i +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.Reference +import com.android.tools.smali.dexlib2.iface.reference.StringReference +import com.android.tools.smali.dexlib2.immutable.ImmutableField +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation +import com.android.tools.smali.dexlib2.util.MethodUtil + +const val REGISTER_TEMPLATE_REPLACEMENT: String = "REGISTER_INDEX" + +fun parametersEqual( + parameters1: Iterable, + parameters2: Iterable +): Boolean { + if (parameters1.count() != parameters2.count()) return false + val iterator1 = parameters1.iterator() + parameters2.forEach { + if (!it.startsWith(iterator1.next())) return false + } + return true +} + +/** + * Find the [MutableMethod] from a given [Method] in a [MutableClass]. + * + * @param method The [Method] to find. + * @return The [MutableMethod]. + */ +fun MutableClass.findMutableMethodOf(method: Method) = this.methods.first { + MethodUtil.methodSignaturesMatch(it, method) +} + +/** + * Apply a transform to all methods of the class. + * + * @param transform The transformation function. Accepts a [MutableMethod] and returns a transformed [MutableMethod]. + */ +fun MutableClass.transformMethods(transform: MutableMethod.() -> MutableMethod) { + val transformedMethods = methods.map { it.transform() } + methods.clear() + methods.addAll(transformedMethods) +} + +/** + * Inject a call to a method that hides a view. + * + * @param insertIndex The index to insert the call at. + * @param viewRegister The register of the view to hide. + * @param classDescriptor The descriptor of the class that contains the method. + * @param targetMethod The name of the method to call. + */ +fun MutableMethod.injectHideViewCall( + insertIndex: Int, + viewRegister: Int, + classDescriptor: String, + targetMethod: String, +) = addInstruction( + insertIndex, + "invoke-static { v$viewRegister }, $classDescriptor->$targetMethod(Landroid/view/View;)V", +) + +/** + * Inserts instructions at a given index, using the existing control flow label at that index. + * Inserted instructions can have it's own control flow labels as well. + * + * Effectively this changes the code from: + * :label + * (original code) + * + * Into: + * :label + * (patch code) + * (original code) + */ +internal fun MutableMethod.addInstructionsAtControlFlowLabel( + insertIndex: Int, + instructions: String, +) { + // Duplicate original instruction and add to +1 index. + addInstruction(insertIndex + 1, getInstruction(insertIndex)) + + // Add patch code at same index as duplicated instruction, + // so it uses the original instruction control flow label. + addInstructionsWithLabels(insertIndex + 1, instructions) + + // Remove original non duplicated instruction. + removeInstruction(insertIndex) + + // Original instruction is now after the inserted patch instructions, + // and the original control flow label is on the first instruction of the patch code. +} + +/** + * Get the index of the first instruction with the id of the given resource id name. + * + * Requires [resourceMappingPatch] as a dependency. + * + * @param resourceName the name of the resource to find the id for. + * @return the index of the first instruction with the id of the given resource name, or -1 if not found. + * @throws PatchException if the resource cannot be found. + * @see [indexOfFirstResourceIdOrThrow], [indexOfFirstLiteralInstructionReversed] + */ +fun Method.indexOfFirstResourceId(resourceName: String): Int { + val resourceId = resourceMappings["id", resourceName] + if (resourceId == -1L) { + printWarn("Could not find resource type: id name: $name") + return -1 + } + return indexOfFirstLiteralInstruction(resourceId) +} + +/** + * Get the index of the first instruction with the id of the given resource name or throw a [PatchException]. + * + * Requires [resourceMappingPatch] as a dependency. + * + * @throws [PatchException] if the resource is not found, or the method does not contain the resource id literal value. + * @see [indexOfFirstResourceId], [indexOfFirstLiteralInstructionReversedOrThrow] + */ +fun Method.indexOfFirstResourceIdOrThrow(resourceName: String): Int { + val index = indexOfFirstResourceId(resourceName) + if (index < 0) { + throw PatchException("Found resource id for: '$resourceName' but method does not contain the id: $this") + } + + return index +} + +/** + * Find the index of the first literal instruction with the given value. + * + * @return the first literal instruction with the value, or -1 if not found. + * @see indexOfFirstLiteralInstructionOrThrow + */ +fun Method.indexOfFirstLiteralInstruction(literal: Long) = implementation?.let { + it.instructions.indexOfFirst { instruction -> + (instruction as? WideLiteralInstruction)?.wideLiteral == literal + } +} ?: -1 + +/** + * Find the index of the first literal instruction with the given value, + * or throw an exception if not found. + * + * @return the first literal instruction with the value, or throws [PatchException] if not found. + */ +fun Method.indexOfFirstLiteralInstructionOrThrow(literal: Long): Int { + val index = indexOfFirstLiteralInstruction(literal) + if (index < 0) throw PatchException("Could not find literal value: $literal") + return index +} + +/** + * Find the index of the last literal instruction with the given value. + * + * @return the last literal instruction with the value, or -1 if not found. + * @see indexOfFirstLiteralInstructionOrThrow + */ +fun Method.indexOfFirstLiteralInstructionReversed(literal: Long) = implementation?.let { + it.instructions.indexOfLast { instruction -> + (instruction as? WideLiteralInstruction)?.wideLiteral == literal + } +} ?: -1 + +/** + * Find the index of the last wide literal instruction with the given value, + * or throw an exception if not found. + * + * @return the last literal instruction with the value, or throws [PatchException] if not found. + */ +fun Method.indexOfFirstLiteralInstructionReversedOrThrow(literal: Long): Int { + val index = indexOfFirstLiteralInstructionReversed(literal) + if (index < 0) throw PatchException("Could not find literal value: $literal") + return index +} + +fun Method.indexOfFirstStringInstruction(str: String) = + indexOfFirstInstruction { + opcode == Opcode.CONST_STRING && + getReference()?.string == str + } + +fun Method.indexOfFirstStringInstructionOrThrow(str: String): Int { + val index = indexOfFirstStringInstruction(str) + if (index < 0) { + throw PatchException("Found string value for: '$str' but method does not contain the id: $this") + } + + return index +} + +/** + * Check if the method contains a literal with the given value. + * + * @return if the method contains a literal with the given value. + */ +fun Method.containsLiteralInstruction(literal: Long) = + indexOfFirstLiteralInstruction(literal) >= 0 + +/** + * Traverse the class hierarchy starting from the given root class. + * + * @param targetClass the class to start traversing the class hierarchy from. + * @param callback function that is called for every class in the hierarchy. + */ +fun BytecodePatchContext.traverseClassHierarchy( + targetClass: MutableClass, + callback: MutableClass.() -> Unit +) { + callback(targetClass) + + targetClass.superclass ?: return + + classBy { targetClass.superclass == it.type }?.mutableClass?.let { + traverseClassHierarchy(it, callback) + } +} + +fun MutableMethod.injectLiteralInstructionViewCall( + literal: Long, + smaliInstruction: String +) { + val literalIndex = indexOfFirstLiteralInstructionOrThrow(literal) + val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT_OBJECT) + val targetRegister = getInstruction(targetIndex).registerA.toString() + + addInstructions( + targetIndex + 1, + smaliInstruction.replace(REGISTER_TEMPLATE_REPLACEMENT, targetRegister) + ) +} + +fun BytecodePatchContext.replaceLiteralInstructionCall( + literal: Long, + smaliInstruction: String +) { + classes.forEach { classDef -> + classDef.methods.forEach { method -> + method.implementation.apply { + this?.instructions?.forEachIndexed { _, instruction -> + if (instruction.opcode != Opcode.CONST) + return@forEachIndexed + if ((instruction as Instruction31i).wideLiteral != literal) + return@forEachIndexed + + proxy(classDef) + .mutableClass + .findMutableMethodOf(method).apply { + val index = indexOfFirstLiteralInstructionOrThrow(literal) + val register = + (instruction as OneRegisterInstruction).registerA.toString() + + addInstructions( + index + 1, + smaliInstruction.replace(REGISTER_TEMPLATE_REPLACEMENT, register) + ) + } + } + } + } + } +} + +/** + * Get the [Reference] of an [Instruction] as [T]. + * + * @param T The type of [Reference] to cast to. + * @return The [Reference] as [T] or null + * if the [Instruction] is not a [ReferenceInstruction] or the [Reference] is not of type [T]. + * @see ReferenceInstruction + */ +inline fun Instruction.getReference() = + (this as? ReferenceInstruction)?.reference as? T + +/** + * @return The index of the first opcode specified, or -1 if not found. + * @see indexOfFirstInstructionOrThrow + */ +fun Method.indexOfFirstInstruction(targetOpcode: Opcode): Int = + indexOfFirstInstruction(0, targetOpcode) + +/** + * @param startIndex Optional starting index to start searching from. + * @return The index of the first opcode specified, or -1 if not found. + * @see indexOfFirstInstructionOrThrow + */ +fun Method.indexOfFirstInstruction(startIndex: Int = 0, targetOpcode: Opcode): Int = + indexOfFirstInstruction(startIndex) { + opcode == targetOpcode + } + +/** + * Get the index of the first [Instruction] that matches the predicate, starting from [startIndex]. + * + * @param startIndex Optional starting index to start searching from. + * @return -1 if the instruction is not found. + * @see indexOfFirstInstructionOrThrow + */ +fun Method.indexOfFirstInstruction(startIndex: Int = 0, filter: Instruction.() -> Boolean): Int { + if (implementation == null) { + return -1 + } + var instructions = this.implementation!!.instructions + if (startIndex != 0) { + instructions = instructions.drop(startIndex) + } + val index = instructions.indexOfFirst(filter) + + return if (index >= 0) { + startIndex + index + } else { + -1 + } +} + +/** + * @return The index of the first opcode specified + * @throws PatchException + * @see indexOfFirstInstruction + */ +fun Method.indexOfFirstInstructionOrThrow(targetOpcode: Opcode): Int = + indexOfFirstInstructionOrThrow(0, targetOpcode) + +/** + * @return The index of the first opcode specified, starting from the index specified. + * @throws PatchException + * @see indexOfFirstInstruction + */ +fun Method.indexOfFirstInstructionOrThrow(startIndex: Int = 0, targetOpcode: Opcode): Int = + indexOfFirstInstructionOrThrow(startIndex) { + opcode == targetOpcode + } + +/** + * Get the index of the first [Instruction] that matches the predicate, starting from [startIndex]. + * + * @return the index of the instruction + * @throws PatchException + * @see indexOfFirstInstruction + */ +fun Method.indexOfFirstInstructionOrThrow( + startIndex: Int = 0, + filter: Instruction.() -> Boolean +): Int { + val index = indexOfFirstInstruction(startIndex, filter) + if (index < 0) { + throw PatchException("Could not find instruction index") + } + + return index +} + +/** + * Get the index of matching instruction, + * starting from and [startIndex] and searching down. + * + * @param startIndex Optional starting index to search down from. Searching includes the start index. + * @return -1 if the instruction is not found. + * @see indexOfFirstInstructionReversedOrThrow + */ +fun Method.indexOfFirstInstructionReversed(startIndex: Int? = null, targetOpcode: Opcode): Int = + indexOfFirstInstructionReversed(startIndex) { + opcode == targetOpcode + } + +/** + * Get the index of matching instruction, + * starting from and [startIndex] and searching down. + * + * @param startIndex Optional starting index to search down from. Searching includes the start index. + * @return -1 if the instruction is not found. + * @see indexOfFirstInstructionReversedOrThrow + */ +fun Method.indexOfFirstInstructionReversed( + startIndex: Int? = null, + filter: Instruction.() -> Boolean +): Int { + if (implementation == null) { + return -1 + } + var instructions = this.implementation!!.instructions + if (startIndex != null) { + instructions = instructions.take(startIndex + 1) + } + + return instructions.indexOfLast(filter) +} + +fun Method.indexOfFirstInstructionReversedOrThrow(opcode: Opcode): Int = + indexOfFirstInstructionReversedOrThrow(null, opcode) + +/** + * Get the index of matching instruction, + * starting from and [startIndex] and searching down. + * + * @param startIndex Optional starting index to search down from. Searching includes the start index. + * @return -1 if the instruction is not found. + * @see indexOfFirstInstructionReversed + */ +fun Method.indexOfFirstInstructionReversedOrThrow( + startIndex: Int? = null, + targetOpcode: Opcode +): Int = + indexOfFirstInstructionReversedOrThrow(startIndex) { + opcode == targetOpcode + } + +/** + * Get the index of matching instruction, + * starting from and [startIndex] and searching down. + * + * @param startIndex Optional starting index to search down from. Searching includes the start index. + * @return -1 if the instruction is not found. + * @see indexOfFirstInstructionReversed + */ +fun Method.indexOfFirstInstructionReversedOrThrow( + startIndex: Int? = null, + filter: Instruction.() -> Boolean +): Int { + val index = indexOfFirstInstructionReversed(startIndex, filter) + + if (index < 0) { + throw PatchException("Could not find instruction index") + } + + return index +} + +/** + * @return An immutable list of indices of the instructions in reverse order. + * _Returns an empty list if no indices are found_ + * @see findInstructionIndicesReversedOrThrow + */ +fun Method.findInstructionIndicesReversed(filter: Instruction.() -> Boolean): List = + instructions + .withIndex() + .filter { (_, instruction) -> filter(instruction) } + .map { (index, _) -> index } + .asReversed() + +/** + * @return An immutable list of indices of the instructions in reverse order. + * @throws PatchException if no matching indices are found. + */ +fun Method.findInstructionIndicesReversedOrThrow(filter: Instruction.() -> Boolean): List { + val indexes = findInstructionIndicesReversed(filter) + if (indexes.isEmpty()) throw PatchException("No matching instructions found in: $this") + + return indexes +} + +/** + * @return An immutable list of indices of the opcode in reverse order. + * _Returns an empty list if no indices are found_ + * @see findInstructionIndicesReversedOrThrow + */ +fun Method.findInstructionIndicesReversed(opcode: Opcode): List = + findInstructionIndicesReversed { this.opcode == opcode } + +/** + * @return An immutable list of indices of the opcode in reverse order. + * @throws PatchException if no matching indices are found. + */ +fun Method.findInstructionIndicesReversedOrThrow(opcode: Opcode): List { + val instructions = findInstructionIndicesReversed(opcode) + if (instructions.isEmpty()) throw PatchException("Could not find opcode: $opcode in: $this") + + return instructions +} + +/** + * Called for _all_ instructions with the given literal value. + */ +fun BytecodePatchContext.forEachLiteralValueInstruction( + literal: Long, + block: MutableMethod.(literalInstructionIndex: Int) -> Unit, +) { + classes.forEach { classDef -> + classDef.methods.forEach { method -> + method.implementation?.instructions?.forEachIndexed { index, instruction -> + if (instruction.opcode == Opcode.CONST && + (instruction as WideLiteralInstruction).wideLiteral == literal + ) { + val mutableMethod = proxy(classDef).mutableClass.findMutableMethodOf(method) + block.invoke(mutableMethod, index) + } + } + } + } +} + +context(BytecodePatchContext) +fun Match.getWalkerMethod(offset: Int) = + method.getWalkerMethod(offset) + +context(BytecodePatchContext) +fun MutableMethod.getWalkerMethod(offset: Int): MutableMethod { + val newMethod = getInstruction(offset).reference as MethodReference + return findMethodOrThrow(newMethod.definingClass) { + MethodUtil.methodSignaturesMatch(this, newMethod) + } +} + +/** + * Taken from BiliRoamingX: + * https://github.com/BiliRoamingX/BiliRoamingX/blob/ae58109f3acdd53ec2d2b3fb439c2a2ef1886221/patches/src/main/kotlin/app/revanced/patches/bilibili/utils/Extenstions.kt#L151 + */ +fun MutableMethod.getFiveRegisters(index: Int) = + with(getInstruction(index)) { + arrayOf(registerC, registerD, registerE, registerF, registerG) + .take(registerCount).joinToString(",") { "v$it" } + } + +context(BytecodePatchContext) +fun addStaticFieldToExtension( + className: String, + methodName: String, + fieldName: String, + objectClass: String, + smaliInstructions: String, + shouldAddConstructor: Boolean = true +) { + val classDef = classes.find { classDef -> classDef.type == className } + ?: throw PatchException("No matching methods found in: $className") + val mutableClass = proxy(classDef).mutableClass + + val objectCall = "$mutableClass->$fieldName:$objectClass" + + mutableClass.apply { + methods.first { method -> method.name == methodName }.apply { + staticFields.add( + ImmutableField( + definingClass, + fieldName, + objectClass, + AccessFlags.PUBLIC or AccessFlags.STATIC, + null, + annotations, + null + ).toMutable() + ) + + addInstructionsWithLabels( + 0, + """ + sget-object v0, $objectCall + """ + smaliInstructions + ) + } + } + + if (!shouldAddConstructor) return + + findMethodsOrThrow(objectClass) + .filter { method -> MethodUtil.isConstructor(method) } + .forEach { mutableMethod -> + mutableMethod.apply { + val initializeIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_DIRECT && + getReference()?.name == "" + } + val insertIndex = if (initializeIndex == -1) + 1 + else + initializeIndex + 1 + + val initializeRegister = if (initializeIndex == -1) + "p0" + else + "v${getInstruction(initializeIndex).registerC}" + + addInstruction( + insertIndex, + "sput-object $initializeRegister, $objectCall" + ) + } + } +} + +context(BytecodePatchContext) +fun findMethodOrThrow( + reference: String, + methodPredicate: Method.() -> Boolean = { MethodUtil.isConstructor(this) } +) = findMethodsOrThrow(reference).first(methodPredicate) + +context(BytecodePatchContext) +fun findMethodsOrThrow(reference: String): MutableSet { + val classDef = classes.find { classDef -> classDef.type == reference } + ?: throw PatchException("No matching methods found in: $reference") + return proxy(classDef) + .mutableClass + .methods +} + +context(BytecodePatchContext) +fun updatePatchStatus( + className: String, + methodName: String +) = findMethodOrThrow(className) { name == methodName } + .replaceInstruction( + 0, + "const/4 v0, 0x1" + ) + +/** + * Taken from BiliRoamingX: + * https://github.com/BiliRoamingX/BiliRoamingX/blob/ae58109f3acdd53ec2d2b3fb439c2a2ef1886221/patches/src/main/kotlin/app/revanced/patches/bilibili/utils/Extenstions.kt#L51 + */ +fun Method.cloneMutable( + registerCount: Int = implementation?.registerCount ?: 0, + clearImplementation: Boolean = false, + name: String = this.name, + accessFlags: Int = this.accessFlags, + parameters: List = this.parameters, + returnType: String = this.returnType +): MutableMethod { + val clonedImplementation = implementation?.let { + ImmutableMethodImplementation( + registerCount, + if (clearImplementation) emptyList() else it.instructions, + if (clearImplementation) emptyList() else it.tryBlocks, + if (clearImplementation) emptyList() else it.debugItems, + ) + } + return ImmutableMethod( + definingClass, + name, + parameters, + returnType, + accessFlags, + annotations, + hiddenApiRestrictions, + clonedImplementation + ).toMutable() +} + +/** + * Return the method early. + */ +fun MutableMethod.returnEarly(bool: Boolean = false) { + val const = if (bool) "0x1" else "0x0" + + val stringInstructions = when (returnType.first()) { + 'L' -> + """ + const/4 v0, $const + return-object v0 + """ + + 'V' -> "return-void" + 'I', 'Z' -> + """ + const/4 v0, $const + return v0 + """ + + else -> throw Exception("This case should never happen.") + } + + addInstructions(0, stringInstructions) +} + +/** + * Set the custom condition for this fingerprint to check for a literal value. + * + * @param literalSupplier The supplier for the literal value to check for. + */ +// TODO: add a way for subclasses to also use their own custom fingerprint. +fun FingerprintBuilder.literal(literalSupplier: () -> Long) { + custom { method, _ -> + method.containsLiteralInstruction(literalSupplier()) + } +} + +/** + * Perform a bitwise OR operation between an [AccessFlags] and an [Int]. + * + * @param other The [Int] to perform the operation with. + */ +infix fun Int.or(other: AccessFlags) = this or other.value + +/** + * Perform a bitwise OR operation between two [AccessFlags]. + * + * @param other The other [AccessFlags] to perform the operation with. + */ +infix fun AccessFlags.or(other: AccessFlags) = value or other.value + +/** + * Perform a bitwise OR operation between an [Int] and an [AccessFlags]. + * + * @param other The [AccessFlags] to perform the operation with. + */ +infix fun AccessFlags.or(other: Int) = value or other \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt b/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt new file mode 100644 index 000000000..10f6d3474 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt @@ -0,0 +1,458 @@ +package app.revanced.util + +import app.revanced.patcher.patch.Option +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.ResourcePatchContext +import app.revanced.patcher.util.Document +import app.revanced.util.Utils.printWarn +import org.w3c.dom.Element +import org.w3c.dom.Node +import org.w3c.dom.NodeList +import java.io.File +import java.io.InputStream +import java.nio.file.Files +import java.nio.file.StandardCopyOption + +private val classLoader = object {}.javaClass.classLoader + +@Suppress("UNCHECKED_CAST") +fun Patch<*>.getStringOptionValue(key: String) = + options[key] as Option + +fun Option.valueOrThrow() = value + ?: throw PatchException("Invalid patch option: $title.") + +fun Option.valueOrThrow() = value + ?: throw PatchException("Invalid patch option: $title.") + +fun Option.lowerCaseOrThrow() = valueOrThrow() + .lowercase() + +fun Option.underBarOrThrow() = lowerCaseOrThrow() + .replace(" ", "_") + +fun Node.adoptChild(tagName: String, block: Element.() -> Unit) { + val child = ownerDocument.createElement(tagName) + child.block() + appendChild(child) +} + +fun Node.cloneNodes(parent: Node) { + val node = cloneNode(true) + parent.appendChild(node) + parent.removeChild(this) +} + +/** + * Recursively traverse the DOM tree starting from the given root node. + * + * @param action function that is called for every node in the tree. + */ +fun Node.doRecursively(action: (Node) -> Unit) { + action(this) + for (i in 0 until this.childNodes.length) this.childNodes.item(i).doRecursively(action) +} + +fun List.getResourceGroup(fileNames: Array) = map { directory -> + ResourceGroup( + directory, *fileNames + ) +} + +private fun ResourcePatchContext.getMipMapPath(): String { + var path: String + document("AndroidManifest.xml").use { document -> + val manifestElement = document.getNode("application") as Element + val mipmapResourceFile = manifestElement.getAttribute("android:icon").split("/")[1] + path = "res/mipmap-anydpi/$mipmapResourceFile.xml" + } + return path +} + +private fun ResourcePatchContext.getAdaptiveIconResourceFile(tag: String): String { + val path = getMipMapPath() + document(path).use { document -> + val adaptiveIcon = document + .getElementsByTagName("adaptive-icon") + .item(0) as Element + + val childNodes = adaptiveIcon.childNodes + for (i in 0 until childNodes.length) { + val node = childNodes.item(i) + if (node is Element && node.tagName == tag && node.hasAttribute("android:drawable")) { + return node.getAttribute("android:drawable").split("/")[1] + } + } + throw PatchException("Element not found: $tag") + } +} + +private fun ResourcePatchContext.getAdaptiveIconBackgroundResourceFile() = + getAdaptiveIconResourceFile("background") + +private fun ResourcePatchContext.getAdaptiveIconForegroundResourceFile() = + getAdaptiveIconResourceFile("foreground") + +private fun ResourcePatchContext.getAdaptiveIconMonoChromeResourceFile() = + getAdaptiveIconResourceFile("monochrome") + +fun ResourcePatchContext.copyAdaptiveIcon( + adaptiveIconBackgroundFileName: String, + adaptiveIconForegroundFileName: String, + mipmapDirectories: List, + adaptiveIconMonoChromeFileName: String? = null, +) { + mapOf( + adaptiveIconBackgroundFileName to getAdaptiveIconBackgroundResourceFile(), + adaptiveIconForegroundFileName to getAdaptiveIconForegroundResourceFile() + ).forEach { (oldIconResourceFile, newIconResourceFile) -> + if (oldIconResourceFile != newIconResourceFile) { + mipmapDirectories.forEach { + val mipmapDirectory = get("res").resolve(it) + Files.copy( + mipmapDirectory + .resolve("$oldIconResourceFile.png") + .toPath(), + mipmapDirectory + .resolve("$newIconResourceFile.png") + .toPath(), + StandardCopyOption.REPLACE_EXISTING + ) + } + } + } + + if (adaptiveIconMonoChromeFileName != null && + adaptiveIconMonoChromeFileName != getAdaptiveIconMonoChromeResourceFile() + ) { + val drawableDirectory = get("res").resolve("drawable") + Files.copy( + drawableDirectory + .resolve("$adaptiveIconMonoChromeFileName.xml") + .toPath(), + drawableDirectory + .resolve("${getAdaptiveIconMonoChromeResourceFile()}.xml") + .toPath(), + StandardCopyOption.REPLACE_EXISTING + ) + } +} + +fun ResourcePatchContext.appendAppVersion(appVersion: String) { + addEntryValues( + "revanced_spoof_app_version_target_entries", + "@string/revanced_spoof_app_version_target_entry_" + appVersion.replace(".", "_"), + prepend = false + ) + addEntryValues( + "revanced_spoof_app_version_target_entry_values", + appVersion, + prepend = false + ) +} + +fun ResourcePatchContext.addEntryValues( + attributeName: String, + attributeValue: String, + path: String = "res/values/arrays.xml", + prepend: Boolean = true, +) { + document(path).use { document -> + with(document) { + val resourcesNode = getElementsByTagName("resources").item(0) as Element + val newElement: Element = createElement("item") + for (i in 0 until resourcesNode.childNodes.length) { + val node = resourcesNode.childNodes.item(i) as? Element ?: continue + + if (node.getAttribute("name") == attributeName) { + newElement.appendChild(createTextNode(attributeValue)) + + if (prepend) { + node.appendChild(newElement) + } else { + node.insertBefore(newElement, node.firstChild) + } + } + } + } + } +} + +fun ResourcePatchContext.copyFile( + resourceGroup: List, + path: String, + warning: String +): Boolean { + resourceGroup.let { resourceGroups -> + try { + val filePath = File(path) + val resourceDirectory = get("res") + + resourceGroups.forEach { group -> + val fromDirectory = filePath.resolve(group.resourceDirectoryName) + val toDirectory = resourceDirectory.resolve(group.resourceDirectoryName) + + group.resources.forEach { iconFileName -> + Files.write( + toDirectory.resolve(iconFileName).toPath(), + fromDirectory.resolve(iconFileName).readBytes() + ) + } + } + + return true + } catch (_: Exception) { + printWarn(warning) + } + } + return false +} + +fun ResourcePatchContext.removeOverlayBackground( + files: Array, + targetId: Array, +) { + files.forEach { file -> + val resourceDirectory = get("res") + val targetXmlPath = resourceDirectory.resolve("layout").resolve(file) + + if (targetXmlPath.exists()) { + targetId.forEach { identifier -> + document("res/layout/$file").use { document -> + document.doRecursively { + arrayOf("height", "width").forEach replacement@{ replacement -> + if (it !is Element) return@replacement + + if (it.attributes.getNamedItem("android:id")?.nodeValue?.endsWith( + identifier + ) == true + ) { + it.getAttributeNode("android:layout_$replacement") + ?.let { attribute -> + attribute.textContent = "0.0dip" + } + } + } + } + } + } + } + } +} + +fun ResourcePatchContext.removeStringsElements( + replacements: Array +) { + var languageList = emptyArray() + val resourceDirectory = get("res") + val dir = resourceDirectory.listFiles() + for (file in dir!!) { + val path = file.name + if (path.startsWith("values")) { + val targetXml = resourceDirectory.resolve(path).resolve("strings.xml") + if (targetXml.exists()) languageList += path + } + } + removeStringsElements(languageList, replacements) +} + +fun ResourcePatchContext.removeStringsElements( + paths: Array, + replacements: Array +) { + paths.forEach { path -> + val resourceDirectory = get("res") + val targetXmlPath = resourceDirectory.resolve(path).resolve("strings.xml") + + if (targetXmlPath.exists()) { + val targetXml = get("res/$path/strings.xml") + + replacements.forEach replacementsLoop@{ replacement -> + targetXml.writeText( + targetXml.readText() + .replaceFirst(""" {4} Unit) { + val child = ownerDocument.createElement(tagName) + child.block() + parentNode.insertBefore(child, targetNode) +} + +/** + * Copy resources from the current class loader to the resource directory. + * + * @param sourceResourceDirectory The source resource directory name. + * @param resources The resources to copy. + */ +fun ResourcePatchContext.copyResources( + sourceResourceDirectory: String, + vararg resources: ResourceGroup, + createDirectoryIfNotExist: Boolean = false, +) { + val resourceDirectory = get("res") + + for (resourceGroup in resources) { + resourceGroup.resources.forEach { resource -> + val resourceDirectoryName = resourceGroup.resourceDirectoryName + if (createDirectoryIfNotExist) { + val targetDirectory = resourceDirectory.resolve(resourceDirectoryName) + if (!targetDirectory.isDirectory) Files.createDirectories(targetDirectory.toPath()) + } + val resourceFile = "$resourceDirectoryName/$resource" + inputStreamFromBundledResource( + sourceResourceDirectory, + resourceFile + )?.let { inputStream -> + Files.copy( + inputStream, + resourceDirectory.resolve(resourceFile).toPath(), + StandardCopyOption.REPLACE_EXISTING, + ) + } + } + } +} + +/** + * Copy resources from the current class loader to the resource directory with the option to rename. + * + * @param sourceResourceDirectory The source resource directory name. + * @param resourceMap The map containing resource titles and their respective path data. + */ +fun ResourcePatchContext.copyResourcesWithRename( + sourceResourceDirectory: String, + resourceMap: Map +) { + val targetResourceDirectory = this["res"] + + for ((title, pathData) in resourceMap) { + // Check if pathData is another title + if (resourceMap.containsKey(pathData)) { + continue // Skip copying if the pathData is another title + } + + val resourceFile = "drawable/icon.xml" + val inputStream = inputStreamFromBundledResource(sourceResourceDirectory, resourceFile)!! + val targetFile = targetResourceDirectory.resolve("drawable/$title.xml").toPath() + + Files.copy(inputStream, targetFile, StandardCopyOption.REPLACE_EXISTING) + + // Update the XML with the new path data + document(targetFile.toString()).use { document -> + updatePathData(document, pathData) + } + } +} + +/** + * Update the `android:pathData` attribute in the XML document. + * + * @param document The XML document. + * @param pathData The new path data to set. + */ +fun updatePathData(document: org.w3c.dom.Document, pathData: String) { + val elements = document.getElementsByTagName("path") + for (i in 0 until elements.length) { + val pathElement = elements.item(i) as? Element + pathElement?.setAttribute("android:pathData", pathData) + } +} + +internal fun inputStreamFromBundledResourceOrThrow( + sourceResourceDirectory: String, + resourceFile: String, +) = classLoader.getResourceAsStream("$sourceResourceDirectory/$resourceFile") + ?: throw PatchException("Could not find $resourceFile") + +internal fun inputStreamFromBundledResource( + sourceResourceDirectory: String, + resourceFile: String, +): InputStream? = classLoader.getResourceAsStream("$sourceResourceDirectory/$resourceFile") + +/** + * Resource names mapped to their corresponding resource data. + * @param resourceDirectoryName The name of the directory of the resource. + * @param resources A list of resource names. + */ +class ResourceGroup(val resourceDirectoryName: String, vararg val resources: String) + +/** + * Copy resources from the current class loader to the resource directory. + * @param resourceDirectory The directory of the resource. + * @param targetResource The target resource. + * @param elementTag The element to copy. + */ +fun ResourcePatchContext.copyXmlNode( + resourceDirectory: String, + targetResource: String, + elementTag: String +) = inputStreamFromBundledResource( + resourceDirectory, + targetResource +)?.let { inputStream -> + // Copy nodes from the resources node to the real resource node + elementTag.copyXmlNode( + document(inputStream), + document("res/$targetResource"), + ).close() +} + +/** + * Copies the specified node of the source [Document] to the target [Document]. + * @param source the source [Document]. + * @param target the target [Document]- + * @return AutoCloseable that closes the [Document]s. + */ +fun String.copyXmlNode( + source: Document, + target: Document, +): AutoCloseable { + val hostNodes = source.getElementsByTagName(this).item(0).childNodes + + val destinationNode = target.getElementsByTagName(this).item(0) + + for (index in 0 until hostNodes.length) { + val node = hostNodes.item(index).cloneNode(true) + target.adoptNode(node) + destinationNode.appendChild(node) + } + + return AutoCloseable { + source.close() + target.close() + } +} + +internal fun org.w3c.dom.Document.getNode(tagName: String) = + this.getElementsByTagName(tagName).item(0) + +internal fun NodeList.findElementByAttributeValue(attributeName: String, value: String): Element? { + for (i in 0 until length) { + val node = item(i) + if (node.nodeType == Node.ELEMENT_NODE) { + val element = node as Element + + if (element.getAttribute(attributeName) == value) { + return element + } + + // Recursively search. + val found = element.childNodes.findElementByAttributeValue(attributeName, value) + if (found != null) { + return found + } + } + } + + return null +} + +internal fun NodeList.findElementByAttributeValueOrThrow(attributeName: String, value: String) = + findElementByAttributeValue(attributeName, value) + ?: throw PatchException("Could not find: $attributeName $value") diff --git a/patches/src/main/kotlin/app/revanced/util/Utils.kt b/patches/src/main/kotlin/app/revanced/util/Utils.kt new file mode 100644 index 000000000..e56cce162 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/util/Utils.kt @@ -0,0 +1,18 @@ +package app.revanced.util + +import java.util.logging.Logger + +internal object Utils { + internal fun String.trimIndentMultiline() = + this.split("\n") + .joinToString("\n") { it.trimIndent() } // Remove the leading whitespace from each line. + .trimIndent() // Remove the leading newline. + + private val logger = Logger.getLogger(this::class.java.name) + + internal fun printInfo(msg: String) = + logger.info(msg) + + internal fun printWarn(msg: String) = + logger.warning(msg) +} diff --git a/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt b/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt new file mode 100644 index 000000000..ef7e9dbcb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt @@ -0,0 +1,176 @@ +@file:Suppress("CONTEXT_RECEIVERS_DEPRECATED") + +package app.revanced.util.fingerprint + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.Match +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.fingerprint +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.util.proxy.mutableTypes.MutableClass +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.util.containsLiteralInstruction +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstruction +import app.revanced.util.injectLiteralInstructionViewCall +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.ClassDef +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private val String.exception + get() = PatchException("Failed to resolve $this") + +context(BytecodePatchContext) +internal fun Pair.resolvable(): Boolean = + second.methodOrNull != null + +context(BytecodePatchContext) +internal fun Pair.definingClassOrThrow(): String = + second.classDefOrNull?.type ?: throw first.exception + +context(BytecodePatchContext) +internal fun Pair.matchOrThrow(): Match = + second.match(mutableClassOrThrow()) + +context(BytecodePatchContext) +internal fun Pair.matchOrThrow(parentFingerprint: Pair): Match { + val parentClassDef = parentFingerprint.second.classDefOrNull + ?: throw parentFingerprint.first.exception + return second.matchOrNull(parentClassDef) + ?: throw first.exception +} + +context(BytecodePatchContext) +internal fun Pair.matchOrNull(): Match? = + second.classDefOrNull?.let { + second.matchOrNull(it) + } + +context(BytecodePatchContext) +internal fun Pair.matchOrNull(parentFingerprint: Pair): Match? = + parentFingerprint.second.classDefOrNull?.let { parentClassDef -> + second.matchOrNull(parentClassDef) + } + +context(BytecodePatchContext) +internal fun Pair.methodOrNull(): MutableMethod? = + matchOrNull()?.method + +context(BytecodePatchContext) +internal fun Pair.methodOrThrow(): MutableMethod = + second.methodOrNull ?: throw first.exception + +context(BytecodePatchContext) +internal fun Pair.methodOrThrow(parentFingerprint: Pair): MutableMethod = + matchOrThrow(parentFingerprint).method + +context(BytecodePatchContext) +internal fun Pair.originalMethodOrThrow(): Method = + second.originalMethodOrNull ?: throw first.exception + +context(BytecodePatchContext) +internal fun Pair.originalMethodOrThrow(parentFingerprint: Pair): Method = + matchOrThrow(parentFingerprint).originalMethod + +context(BytecodePatchContext) +internal fun Pair.mutableClassOrThrow(): MutableClass = + second.classDefOrNull ?: throw first.exception + +context(BytecodePatchContext) +internal fun Pair.methodCall() = + methodOrThrow().methodCall() + +context(BytecodePatchContext) +internal fun MutableMethod.methodCall(): String { + var methodCall = "$definingClass->$name(" + for (i in 0 until parameters.size) { + methodCall += parameterTypes[i] + } + methodCall += ")$returnType" + return methodCall +} + +context(BytecodePatchContext) +fun Pair.injectLiteralInstructionBooleanCall( + literal: Long, + descriptor: String +) { + methodOrThrow().apply { + val literalIndex = indexOfFirstLiteralInstruction(literal) + val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT) + val targetRegister = getInstruction(targetIndex).registerA + + val smaliInstruction = + if (descriptor.startsWith("0x")) """ + const/16 v$targetRegister, $descriptor + """ + else if (descriptor.endsWith("(Z)Z")) """ + invoke-static {v$targetRegister}, $descriptor + move-result v$targetRegister + """ + else """ + invoke-static {}, $descriptor + move-result v$targetRegister + """ + + addInstructions( + targetIndex + 1, + smaliInstruction + ) + } +} + +context(BytecodePatchContext) +fun Pair.injectLiteralInstructionViewCall( + literal: Long, + smaliInstruction: String +) { + val method = methodOrThrow() + method.injectLiteralInstructionViewCall(literal, smaliInstruction) +} + +internal fun legacyFingerprint( + name: String, + accessFlags: Int? = null, + returnType: String? = null, + parameters: List? = null, + opcodes: List? = null, + strings: List? = null, + literals: List? = null, + customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null +) = Pair( + name, + fingerprint { + if (accessFlags != null) { + accessFlags(accessFlags) + } + if (returnType != null) { + returns(returnType) + } + if (parameters != null) { + parameters(*parameters.toTypedArray()) + } + if (opcodes != null) { + opcodes(*opcodes.toTypedArray()) + } + if (strings != null) { + strings(*strings.toTypedArray()) + } + custom { method, classDef -> + if (literals != null) { + for (literal in literals) + if (!method.containsLiteralInstruction(literal)) + return@custom false + } + if (customFingerprint != null && !customFingerprint(method, classDef)) { + return@custom false + } + + return@custom true + } + } +) + diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-hdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-hdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-hdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-hdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-hdpi/logo_music.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-hdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-hdpi/logo_music.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-hdpi/logo_music.png diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-hdpi/ytm_logo.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-hdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-hdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-hdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-mdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-mdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-mdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-mdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-mdpi/logo_music.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-mdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-mdpi/logo_music.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-mdpi/logo_music.png diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-mdpi/ytm_logo.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-mdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-mdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-mdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-xhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-xhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-xhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-xhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-xhdpi/logo_music.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-xhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-xhdpi/logo_music.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-xhdpi/logo_music.png diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-xhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-xhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-xhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-xhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-xxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-xxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-xxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-xxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-xxhdpi/logo_music.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-xxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-xxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-xxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-xxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-xxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-xxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-xxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-xxxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-xxxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-xxxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-xxxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-xxxhdpi/logo_music.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-xxxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-xxxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-xxxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/afn_blue/header/drawable-xxxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/afn_blue/header/drawable-xxxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/header/drawable-xxxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/afn_blue/header/drawable-xxxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-hdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-hdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-hdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-hdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-mdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-mdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-mdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-mdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-xhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-xhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-xxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/launcher/mipmap-xxxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/afn_blue/launcher/mipmap-xxxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/afn_blue/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/patches/src/main/resources/music/branding/afn_blue/monochrome/drawable/ic_app_icons_themed_youtube_music.xml similarity index 100% rename from src/main/resources/music/branding/afn_blue/monochrome/drawable/ic_app_icons_themed_youtube_music.xml rename to patches/src/main/resources/music/branding/afn_blue/monochrome/drawable/ic_app_icons_themed_youtube_music.xml diff --git a/patches/src/main/resources/music/branding/afn_blue/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/afn_blue/settings/drawable/revanced_extended_settings_key_icon.xml new file mode 100644 index 000000000..b34305bd0 --- /dev/null +++ b/patches/src/main/resources/music/branding/afn_blue/settings/drawable/revanced_extended_settings_key_icon.xml @@ -0,0 +1,1011 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/music/branding/afn_blue/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/afn_blue/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/afn_blue/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/afn_blue/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/afn_blue/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/afn_blue/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/afn_blue/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/afn_blue/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/afn_blue/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/afn_blue/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/afn_blue/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/afn_blue/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/afn_blue/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/afn_blue/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/afn_blue/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/afn_blue/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/afn_blue/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/afn_blue/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/afn_blue/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/afn_blue/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/afn_blue/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/afn_blue/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/afn_blue/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/afn_blue/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/afn_blue/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/afn_blue/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/afn_blue/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/afn_blue/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/afn_blue/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_blue/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/afn_blue/splash/drawable-xxhdpi/record.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-hdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-hdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-hdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-hdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-hdpi/logo_music.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-hdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-hdpi/logo_music.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-hdpi/logo_music.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-hdpi/ytm_logo.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-hdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-hdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-hdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-mdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-mdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-mdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-mdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-mdpi/logo_music.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-mdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-mdpi/logo_music.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-mdpi/logo_music.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-mdpi/ytm_logo.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-mdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-mdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-mdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-xhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-xhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-xhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-xhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-xhdpi/logo_music.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-xhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-xhdpi/logo_music.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-xhdpi/logo_music.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-xhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-xhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-xhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-xhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-xxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-xxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-xxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-xxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-xxhdpi/logo_music.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-xxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-xxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-xxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-xxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-xxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-xxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-xxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-xxxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-xxxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-xxxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-xxxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-xxxhdpi/logo_music.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-xxxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-xxxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-xxxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/afn_red/header/drawable-xxxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/afn_red/header/drawable-xxxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/afn_red/header/drawable-xxxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/afn_red/header/drawable-xxxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-hdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-hdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-hdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-hdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-mdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-mdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-mdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-mdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-xhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-xhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-xxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-xxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/afn_red/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xxxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/afn_red/launcher/mipmap-xxxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/afn_red/launcher/mipmap-xxxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/afn_red/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/patches/src/main/resources/music/branding/afn_red/monochrome/drawable/ic_app_icons_themed_youtube_music.xml similarity index 100% rename from src/main/resources/music/branding/afn_red/monochrome/drawable/ic_app_icons_themed_youtube_music.xml rename to patches/src/main/resources/music/branding/afn_red/monochrome/drawable/ic_app_icons_themed_youtube_music.xml diff --git a/patches/src/main/resources/music/branding/afn_red/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/afn_red/settings/drawable/revanced_extended_settings_key_icon.xml new file mode 100644 index 000000000..ae0a78405 --- /dev/null +++ b/patches/src/main/resources/music/branding/afn_red/settings/drawable/revanced_extended_settings_key_icon.xml @@ -0,0 +1,1007 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/music/branding/afn_red/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/afn_red/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/afn_red/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/afn_red/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/afn_red/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/afn_red/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_red/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/afn_red/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/afn_red/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/afn_red/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_red/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/afn_red/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/afn_red/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/afn_red/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_red/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/afn_red/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/afn_red/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/afn_red/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_red/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/afn_red/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/afn_red/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/afn_red/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_red/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/afn_red/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/afn_red/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/afn_red/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_red/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/afn_red/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/afn_red/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/afn_red/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_red/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/afn_red/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/afn_red/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/afn_red/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_red/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/afn_red/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/afn_red/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/afn_red/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/afn_red/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/afn_red/splash/drawable-xxhdpi/record.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-hdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt/header/drawable-hdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-hdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-hdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-hdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt/header/drawable-hdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-hdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-hdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-hdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt/header/drawable-hdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-hdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-hdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-mdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt/header/drawable-mdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-mdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-mdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-mdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt/header/drawable-mdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-mdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-mdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-mdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt/header/drawable-mdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-mdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-mdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-xhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt/header/drawable-xhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-xhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-xhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-xhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt/header/drawable-xhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-xhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-xhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-xhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt/header/drawable-xhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-xhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-xhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-xxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt/header/drawable-xxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-xxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-xxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-xxhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt/header/drawable-xxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-xxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-xxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-xxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt/header/drawable-xxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-xxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-xxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-xxxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt/header/drawable-xxxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-xxxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-xxxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-xxxhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt/header/drawable-xxxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-xxxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-xxxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt/header/drawable-xxxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt/header/drawable-xxxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt/header/drawable-xxxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt/header/drawable-xxxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-hdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-hdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-hdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-hdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-mdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-mdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-mdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-mdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-xhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-xhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-xhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-xhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-xxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-xxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-xxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-xxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt/launcher/mipmap-xxxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt/launcher/mipmap-xxxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt/launcher/mipmap-xxxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/patches/src/main/resources/music/branding/mmt/monochrome/drawable/ic_app_icons_themed_youtube_music.xml similarity index 100% rename from src/main/resources/music/branding/mmt/monochrome/drawable/ic_app_icons_themed_youtube_music.xml rename to patches/src/main/resources/music/branding/mmt/monochrome/drawable/ic_app_icons_themed_youtube_music.xml diff --git a/patches/src/main/resources/music/branding/mmt/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/mmt/settings/drawable/revanced_extended_settings_key_icon.xml new file mode 100644 index 000000000..6280d6026 --- /dev/null +++ b/patches/src/main/resources/music/branding/mmt/settings/drawable/revanced_extended_settings_key_icon.xml @@ -0,0 +1,1023 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/music/branding/mmt/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/mmt/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/mmt/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/mmt/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/mmt/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/mmt/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/mmt/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/mmt/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/mmt/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/mmt/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/mmt/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/mmt/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/mmt/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/mmt/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/mmt/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/mmt/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/mmt/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/mmt/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/mmt/splash/drawable-xxhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-hdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-hdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-hdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-hdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-hdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-hdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-hdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-hdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-hdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-hdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-hdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-hdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-mdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-mdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-mdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-mdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-mdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-mdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-mdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-mdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-mdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-mdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-mdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-mdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-xhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-xhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-xhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-xhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-xhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-xhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-xhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-xhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-xhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-xhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-xhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-xhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-xxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-xxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-xxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-xxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-xxhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-xxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-xxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-xxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-xxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-xxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-xxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-xxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-xxxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-xxxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-xxxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-xxxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-xxxhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-xxxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-xxxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-xxxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_blue/header/drawable-xxxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_blue/header/drawable-xxxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/header/drawable-xxxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_blue/header/drawable-xxxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-hdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-hdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-hdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-hdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-mdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-mdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-mdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-mdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-xhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_blue/launcher/mipmap-xxxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_blue/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/patches/src/main/resources/music/branding/mmt_blue/monochrome/drawable/ic_app_icons_themed_youtube_music.xml similarity index 100% rename from src/main/resources/music/branding/mmt_blue/monochrome/drawable/ic_app_icons_themed_youtube_music.xml rename to patches/src/main/resources/music/branding/mmt_blue/monochrome/drawable/ic_app_icons_themed_youtube_music.xml diff --git a/src/main/resources/music/branding/mmt_blue/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/mmt_blue/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/music/branding/mmt_blue/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/music/branding/mmt_blue/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/music/branding/mmt_blue/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/mmt_blue/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/mmt_blue/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/mmt_blue/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_blue/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_blue/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_blue/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_blue/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_blue/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_blue/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_blue/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_blue/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_blue/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/mmt_blue/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_blue/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_blue/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_blue/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_blue/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_blue/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/mmt_blue/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_blue/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_blue/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_blue/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_blue/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_blue/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_blue/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_blue/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_blue/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/mmt_blue/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_blue/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_blue/splash/drawable-xxhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-hdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-hdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-hdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-hdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-hdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-hdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-hdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-hdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-hdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-hdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-hdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-hdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-mdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-mdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-mdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-mdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-mdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-mdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-mdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-mdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-mdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-mdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-mdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-mdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-xhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-xhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-xhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-xhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-xhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-xhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-xhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-xhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-xhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-xhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-xhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-xhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-xxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-xxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-xxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-xxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-xxhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-xxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-xxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-xxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-xxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-xxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-xxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-xxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-xxxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-xxxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-xxxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-xxxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-xxxhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-xxxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-xxxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-xxxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_green/header/drawable-xxxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_green/header/drawable-xxxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/header/drawable-xxxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_green/header/drawable-xxxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-hdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-hdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-hdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-hdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-mdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-mdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-mdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-mdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-xhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-xhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-xxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/launcher/mipmap-xxxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_green/launcher/mipmap-xxxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_green/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/patches/src/main/resources/music/branding/mmt_green/monochrome/drawable/ic_app_icons_themed_youtube_music.xml similarity index 100% rename from src/main/resources/music/branding/mmt_green/monochrome/drawable/ic_app_icons_themed_youtube_music.xml rename to patches/src/main/resources/music/branding/mmt_green/monochrome/drawable/ic_app_icons_themed_youtube_music.xml diff --git a/src/main/resources/music/branding/mmt_green/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/mmt_green/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/music/branding/mmt_green/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/music/branding/mmt_green/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/music/branding/mmt_green/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/mmt_green/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/mmt_green/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/mmt_green/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_green/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_green/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_green/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_green/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_green/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_green/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_green/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_green/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_green/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/mmt_green/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_green/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_green/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_green/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_green/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_green/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/mmt_green/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_green/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_green/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_green/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_green/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_green/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_green/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_green/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_green/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/mmt_green/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_green/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_green/splash/drawable-xxhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-hdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-hdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-hdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-hdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-hdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-hdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-hdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-hdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-hdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-hdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-hdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-hdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-mdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-mdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-mdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-mdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-mdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-mdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-mdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-mdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-mdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-mdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-mdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-mdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-xhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-xhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-xhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-xhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-xhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-xhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-xhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-xhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-xhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-xhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-xhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-xhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-xxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-xxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-xxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-xxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-xxhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-xxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-xxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-xxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-xxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-xxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-xxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-xxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-xxxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-xxxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-xxxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-xxxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-xxxhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-xxxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-xxxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-xxxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_orange/header/drawable-xxxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_orange/header/drawable-xxxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/header/drawable-xxxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_orange/header/drawable-xxxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-hdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-hdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-hdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-hdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-mdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-mdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-mdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-mdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-xhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_orange/launcher/mipmap-xxxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_orange/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/patches/src/main/resources/music/branding/mmt_orange/monochrome/drawable/ic_app_icons_themed_youtube_music.xml similarity index 100% rename from src/main/resources/music/branding/mmt_orange/monochrome/drawable/ic_app_icons_themed_youtube_music.xml rename to patches/src/main/resources/music/branding/mmt_orange/monochrome/drawable/ic_app_icons_themed_youtube_music.xml diff --git a/src/main/resources/music/branding/mmt_orange/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/mmt_orange/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/music/branding/mmt_orange/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/music/branding/mmt_orange/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/music/branding/mmt_orange/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/mmt_orange/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/mmt_orange/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/mmt_orange/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_orange/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_orange/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_orange/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_orange/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_orange/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_orange/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_orange/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_orange/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_orange/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/mmt_orange/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_orange/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_orange/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_orange/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_orange/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_orange/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/mmt_orange/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_orange/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_orange/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_orange/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_orange/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_orange/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_orange/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_orange/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_orange/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/mmt_orange/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_orange/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_orange/splash/drawable-xxhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-hdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-hdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-hdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-hdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-hdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-hdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-hdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-hdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-hdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-hdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-hdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-hdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-mdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-mdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-mdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-mdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-mdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-mdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-mdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-mdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-mdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-mdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-mdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-mdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-xhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-xhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-xhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-xhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-xhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-xhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-xhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-xhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-xhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-xhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-xhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-xhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-xxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-xxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-xxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-xxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-xxhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-xxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-xxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-xxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-xxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-xxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-xxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-xxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-xxxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-xxxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-xxxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-xxxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-xxxhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-xxxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-xxxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-xxxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_pink/header/drawable-xxxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_pink/header/drawable-xxxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/header/drawable-xxxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_pink/header/drawable-xxxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-hdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-hdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-hdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-hdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-mdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-mdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-mdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-mdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-xhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_pink/launcher/mipmap-xxxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_pink/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/patches/src/main/resources/music/branding/mmt_pink/monochrome/drawable/ic_app_icons_themed_youtube_music.xml similarity index 100% rename from src/main/resources/music/branding/mmt_pink/monochrome/drawable/ic_app_icons_themed_youtube_music.xml rename to patches/src/main/resources/music/branding/mmt_pink/monochrome/drawable/ic_app_icons_themed_youtube_music.xml diff --git a/src/main/resources/music/branding/mmt_pink/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/mmt_pink/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/music/branding/mmt_pink/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/music/branding/mmt_pink/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/music/branding/mmt_pink/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/mmt_pink/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/mmt_pink/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/mmt_pink/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_pink/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_pink/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_pink/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_pink/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_pink/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_pink/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_pink/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_pink/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_pink/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/mmt_pink/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_pink/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_pink/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_pink/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_pink/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_pink/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/mmt_pink/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_pink/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_pink/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_pink/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_pink/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_pink/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_pink/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_pink/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_pink/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/mmt_pink/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_pink/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_pink/splash/drawable-xxhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-hdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-hdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-hdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-hdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-hdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-hdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-hdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-hdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-hdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-hdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-hdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-hdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-mdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-mdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-mdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-mdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-mdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-mdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-mdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-mdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-mdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-mdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-mdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-mdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-xhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-xhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-xhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-xhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-xhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-xhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-xxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-xxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-xxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-xxxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxxhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-xxxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/header/drawable-xxxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_turquoise/header/drawable-xxxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-hdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-hdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-hdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-hdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-mdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-mdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-mdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-mdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_turquoise/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/patches/src/main/resources/music/branding/mmt_turquoise/monochrome/drawable/ic_app_icons_themed_youtube_music.xml similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/monochrome/drawable/ic_app_icons_themed_youtube_music.xml rename to patches/src/main/resources/music/branding/mmt_turquoise/monochrome/drawable/ic_app_icons_themed_youtube_music.xml diff --git a/src/main/resources/music/branding/mmt_turquoise/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/mmt_turquoise/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/music/branding/mmt_turquoise/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/music/branding/mmt_turquoise/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/mmt_turquoise/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_turquoise/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_turquoise/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_turquoise/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_turquoise/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_turquoise/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_turquoise/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_turquoise/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_turquoise/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_turquoise/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_turquoise/splash/drawable-xxhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-hdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-hdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-hdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-hdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-hdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-hdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-hdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-hdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-hdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-hdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-hdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-hdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-mdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-mdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-mdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-mdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-mdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-mdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-mdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-mdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-mdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-mdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-mdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-mdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-xhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-xhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-xhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-xhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-xhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-xhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-xxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-xxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-xxhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-xxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-xxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-xxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-xxxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xxxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-xxxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xxxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-xxxhdpi/logo_music.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xxxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-xxxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xxxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/mmt_yellow/header/drawable-xxxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xxxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/header/drawable-xxxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/mmt_yellow/header/drawable-xxxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-hdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-hdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-hdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-hdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-mdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-mdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-mdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-mdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/mmt_yellow/launcher/mipmap-xxxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/mmt_yellow/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/patches/src/main/resources/music/branding/mmt_yellow/monochrome/drawable/ic_app_icons_themed_youtube_music.xml similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/monochrome/drawable/ic_app_icons_themed_youtube_music.xml rename to patches/src/main/resources/music/branding/mmt_yellow/monochrome/drawable/ic_app_icons_themed_youtube_music.xml diff --git a/src/main/resources/music/branding/mmt_yellow/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/mmt_yellow/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/music/branding/mmt_yellow/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/music/branding/mmt_yellow/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/mmt_yellow/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_yellow/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_yellow/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_yellow/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_yellow/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_yellow/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/mmt_yellow/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/mmt_yellow/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/mmt_yellow/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/mmt_yellow/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/mmt_yellow/splash/drawable-xxhdpi/record.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-hdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-hdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-hdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-hdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-hdpi/logo_music.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-hdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-hdpi/logo_music.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-hdpi/logo_music.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-hdpi/ytm_logo.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-hdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-hdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-hdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-mdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-mdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-mdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-mdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-mdpi/logo_music.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-mdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-mdpi/logo_music.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-mdpi/logo_music.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-mdpi/ytm_logo.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-mdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-mdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-mdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-xhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-xhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-xhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-xhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-xhdpi/logo_music.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-xhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-xhdpi/logo_music.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-xhdpi/logo_music.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-xhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-xhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-xhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-xhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-xxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-xxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-xxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-xxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-xxhdpi/logo_music.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-xxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-xxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-xxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-xxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-xxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-xxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-xxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-xxxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-xxxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-xxxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-xxxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-xxxhdpi/logo_music.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-xxxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-xxxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-xxxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/revancify_blue/header/drawable-xxxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/revancify_blue/header/drawable-xxxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/header/drawable-xxxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/revancify_blue/header/drawable-xxxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-hdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-hdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-hdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-hdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-mdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-mdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-mdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-mdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-xhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/revancify_blue/launcher/mipmap-xxxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/revancify_blue/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/patches/src/main/resources/music/branding/revancify_blue/monochrome/drawable/ic_app_icons_themed_youtube_music.xml similarity index 100% rename from src/main/resources/music/branding/revancify_blue/monochrome/drawable/ic_app_icons_themed_youtube_music.xml rename to patches/src/main/resources/music/branding/revancify_blue/monochrome/drawable/ic_app_icons_themed_youtube_music.xml diff --git a/patches/src/main/resources/music/branding/revancify_blue/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/revancify_blue/settings/drawable/revanced_extended_settings_key_icon.xml new file mode 100644 index 000000000..31a5a9517 --- /dev/null +++ b/patches/src/main/resources/music/branding/revancify_blue/settings/drawable/revanced_extended_settings_key_icon.xml @@ -0,0 +1,1027 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/music/branding/revancify_blue/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/revancify_blue/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/revancify_blue/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/revancify_blue/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/revancify_blue/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/revancify_blue/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/revancify_blue/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/revancify_blue/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/revancify_blue/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/revancify_blue/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/revancify_blue/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/revancify_blue/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/revancify_blue/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/revancify_blue/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/revancify_blue/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/revancify_blue/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/revancify_blue/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/revancify_blue/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/revancify_blue/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/revancify_blue/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/revancify_blue/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/revancify_blue/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/revancify_blue/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/revancify_blue/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/revancify_blue/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/revancify_blue/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/revancify_blue/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/revancify_blue/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/revancify_blue/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_blue/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/revancify_blue/splash/drawable-xxhdpi/record.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-hdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-hdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-hdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-hdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-hdpi/logo_music.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-hdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-hdpi/logo_music.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-hdpi/logo_music.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-hdpi/ytm_logo.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-hdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-hdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-hdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-mdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-mdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-mdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-mdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-mdpi/logo_music.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-mdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-mdpi/logo_music.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-mdpi/logo_music.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-mdpi/ytm_logo.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-mdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-mdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-mdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-xhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-xhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-xhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-xhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-xhdpi/logo_music.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-xhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-xhdpi/logo_music.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-xhdpi/logo_music.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-xhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-xhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-xhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-xhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-xxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-xxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-xxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-xxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-xxhdpi/logo_music.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-xxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-xxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-xxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-xxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-xxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-xxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-xxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-xxxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-xxxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-xxxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-xxxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-xxxhdpi/logo_music.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-xxxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-xxxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-xxxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/revancify_red/header/drawable-xxxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/revancify_red/header/drawable-xxxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/header/drawable-xxxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/revancify_red/header/drawable-xxxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-hdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-hdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-hdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-hdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-mdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-mdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-mdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-mdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-xhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-xhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-xxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/launcher/mipmap-xxxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/revancify_red/launcher/mipmap-xxxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/revancify_red/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/patches/src/main/resources/music/branding/revancify_red/monochrome/drawable/ic_app_icons_themed_youtube_music.xml similarity index 100% rename from src/main/resources/music/branding/revancify_red/monochrome/drawable/ic_app_icons_themed_youtube_music.xml rename to patches/src/main/resources/music/branding/revancify_red/monochrome/drawable/ic_app_icons_themed_youtube_music.xml diff --git a/patches/src/main/resources/music/branding/revancify_red/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/revancify_red/settings/drawable/revanced_extended_settings_key_icon.xml new file mode 100644 index 000000000..4e9a3e24c --- /dev/null +++ b/patches/src/main/resources/music/branding/revancify_red/settings/drawable/revanced_extended_settings_key_icon.xml @@ -0,0 +1,1023 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/music/branding/revancify_red/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/revancify_red/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/revancify_red/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/revancify_red/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/revancify_red/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/revancify_red/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/revancify_red/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/revancify_red/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/revancify_red/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/revancify_red/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/revancify_red/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/revancify_red/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/revancify_red/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/revancify_red/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/revancify_red/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/revancify_red/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/revancify_red/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/revancify_red/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/revancify_red/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/revancify_red/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/revancify_red/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/revancify_red/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/revancify_red/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/revancify_red/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/revancify_red/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/revancify_red/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/revancify_red/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/revancify_red/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/revancify_red/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/revancify_red/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/revancify_red/splash/drawable-xxhdpi/record.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-hdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-hdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-hdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-hdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-hdpi/logo_music.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-hdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-hdpi/logo_music.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-hdpi/logo_music.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-hdpi/ytm_logo.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-hdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-hdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-hdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-mdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-mdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-mdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-mdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-mdpi/logo_music.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-mdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-mdpi/logo_music.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-mdpi/logo_music.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-mdpi/ytm_logo.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-mdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-mdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-mdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-xhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-xhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-xhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-xhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-xhdpi/logo_music.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-xhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-xhdpi/logo_music.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-xhdpi/logo_music.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-xhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-xhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-xhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-xhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-xxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-xxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-xxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-xxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-xxhdpi/logo_music.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-xxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-xxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-xxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-xxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-xxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-xxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-xxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-xxxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-xxxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-xxxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-xxxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-xxxhdpi/logo_music.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-xxxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-xxxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-xxxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/vanced_black/header/drawable-xxxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/vanced_black/header/drawable-xxxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/header/drawable-xxxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/vanced_black/header/drawable-xxxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-hdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-hdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-hdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-hdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-mdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-mdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-mdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-mdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-xhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-xhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-xxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/launcher/mipmap-xxxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/vanced_black/launcher/mipmap-xxxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/vanced_black/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/patches/src/main/resources/music/branding/vanced_black/monochrome/drawable/ic_app_icons_themed_youtube_music.xml similarity index 100% rename from src/main/resources/music/branding/vanced_black/monochrome/drawable/ic_app_icons_themed_youtube_music.xml rename to patches/src/main/resources/music/branding/vanced_black/monochrome/drawable/ic_app_icons_themed_youtube_music.xml diff --git a/src/main/resources/music/branding/vanced_black/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/vanced_black/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/music/branding/vanced_black/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/music/branding/vanced_black/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/music/branding/vanced_black/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/vanced_black/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/vanced_black/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/vanced_black/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/vanced_black/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/vanced_black/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/vanced_black/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/vanced_black/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/vanced_black/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/vanced_black/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/vanced_black/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/vanced_black/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/vanced_black/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/vanced_black/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/vanced_black/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/vanced_black/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/vanced_black/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/vanced_black/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/vanced_black/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/vanced_black/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/vanced_black/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/vanced_black/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/vanced_black/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/vanced_black/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/vanced_black/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/vanced_black/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/vanced_black/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/vanced_black/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/vanced_black/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_black/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/vanced_black/splash/drawable-xxhdpi/record.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-hdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-hdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-hdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-hdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-hdpi/logo_music.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-hdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-hdpi/logo_music.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-hdpi/logo_music.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-hdpi/ytm_logo.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-hdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-hdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-hdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-mdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-mdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-mdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-mdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-mdpi/logo_music.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-mdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-mdpi/logo_music.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-mdpi/logo_music.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-mdpi/ytm_logo.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-mdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-mdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-mdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-xhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-xhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-xhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-xhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-xhdpi/logo_music.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-xhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-xhdpi/logo_music.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-xhdpi/logo_music.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-xhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-xhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-xhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-xhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-xxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-xxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-xxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-xxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-xxhdpi/logo_music.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-xxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-xxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-xxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-xxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-xxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-xxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-xxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-xxxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-xxxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-xxxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-xxxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-xxxhdpi/logo_music.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-xxxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-xxxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-xxxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/vanced_light/header/drawable-xxxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/vanced_light/header/drawable-xxxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/header/drawable-xxxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/vanced_light/header/drawable-xxxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-hdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-hdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-hdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-hdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-mdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-mdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-mdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-mdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-xhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-xhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-xxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/launcher/mipmap-xxxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/vanced_light/launcher/mipmap-xxxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/vanced_light/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/patches/src/main/resources/music/branding/vanced_light/monochrome/drawable/ic_app_icons_themed_youtube_music.xml similarity index 100% rename from src/main/resources/music/branding/vanced_light/monochrome/drawable/ic_app_icons_themed_youtube_music.xml rename to patches/src/main/resources/music/branding/vanced_light/monochrome/drawable/ic_app_icons_themed_youtube_music.xml diff --git a/src/main/resources/music/branding/vanced_light/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/vanced_light/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/music/branding/vanced_light/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/music/branding/vanced_light/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/music/branding/vanced_light/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/vanced_light/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/vanced_light/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/vanced_light/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/vanced_light/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/vanced_light/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/vanced_light/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/vanced_light/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/vanced_light/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/vanced_light/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/vanced_light/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/vanced_light/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/vanced_light/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/vanced_light/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/vanced_light/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/vanced_light/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/vanced_light/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/vanced_light/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/vanced_light/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/vanced_light/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/vanced_light/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/vanced_light/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/vanced_light/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/vanced_light/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/vanced_light/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/vanced_light/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/vanced_light/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/vanced_light/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/vanced_light/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/vanced_light/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/vanced_light/splash/drawable-xxhdpi/record.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-hdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-hdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-hdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-hdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-hdpi/logo_music.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-hdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-hdpi/logo_music.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-hdpi/logo_music.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-hdpi/ytm_logo.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-hdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-hdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-hdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-mdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-mdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-mdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-mdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-mdpi/logo_music.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-mdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-mdpi/logo_music.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-mdpi/logo_music.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-mdpi/ytm_logo.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-mdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-mdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-mdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-xhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-xhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-xhdpi/logo_music.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-xhdpi/logo_music.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xhdpi/logo_music.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-xhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-xhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-xxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-xxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-xxhdpi/logo_music.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-xxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-xxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-xxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-xxxhdpi/action_bar_logo.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xxxhdpi/action_bar_logo.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-xxxhdpi/action_bar_logo.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xxxhdpi/action_bar_logo.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-xxxhdpi/logo_music.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xxxhdpi/logo_music.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-xxxhdpi/logo_music.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xxxhdpi/logo_music.png diff --git a/src/main/resources/music/branding/xisr_yellow/header/drawable-xxxhdpi/ytm_logo.png b/patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xxxhdpi/ytm_logo.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/header/drawable-xxxhdpi/ytm_logo.png rename to patches/src/main/resources/music/branding/xisr_yellow/header/drawable-xxxhdpi/ytm_logo.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-hdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-hdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-hdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-hdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-mdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-mdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-mdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-mdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png diff --git a/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxxhdpi/ic_launcher_release.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxxhdpi/ic_launcher_release.png rename to patches/src/main/resources/music/branding/xisr_yellow/launcher/mipmap-xxxhdpi/ic_launcher_release.png diff --git a/src/main/resources/music/branding/revancify_yellow/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/patches/src/main/resources/music/branding/xisr_yellow/monochrome/drawable/ic_app_icons_themed_youtube_music.xml similarity index 100% rename from src/main/resources/music/branding/revancify_yellow/monochrome/drawable/ic_app_icons_themed_youtube_music.xml rename to patches/src/main/resources/music/branding/xisr_yellow/monochrome/drawable/ic_app_icons_themed_youtube_music.xml diff --git a/src/main/resources/music/branding/xisr_yellow/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/xisr_yellow/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/music/branding/xisr_yellow/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/music/branding/xisr_yellow/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/xisr_yellow/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/xisr_yellow/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/xisr_yellow/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/xisr_yellow/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/xisr_yellow/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/xisr_yellow/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/xisr_yellow/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/xisr_yellow/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/xisr_yellow/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/xisr_yellow/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/xisr_yellow/splash/drawable-xxhdpi/record.png diff --git a/patches/src/main/resources/music/branding/youtube_music/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/branding/youtube_music/settings/drawable/revanced_extended_settings_key_icon.xml new file mode 100644 index 000000000..3cbd7311a --- /dev/null +++ b/patches/src/main/resources/music/branding/youtube_music/settings/drawable/revanced_extended_settings_key_icon.xml @@ -0,0 +1,31 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/music/branding/youtube_music/splash/drawable-hdpi/action_bar_logo_release.png b/patches/src/main/resources/music/branding/youtube_music/splash/drawable-hdpi/action_bar_logo_release.png similarity index 100% rename from src/main/resources/music/branding/youtube_music/splash/drawable-hdpi/action_bar_logo_release.png rename to patches/src/main/resources/music/branding/youtube_music/splash/drawable-hdpi/action_bar_logo_release.png diff --git a/src/main/resources/music/branding/youtube_music/splash/drawable-hdpi/record.png b/patches/src/main/resources/music/branding/youtube_music/splash/drawable-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/youtube_music/splash/drawable-hdpi/record.png rename to patches/src/main/resources/music/branding/youtube_music/splash/drawable-hdpi/record.png diff --git a/src/main/resources/music/branding/youtube_music/splash/drawable-large-hdpi/record.png b/patches/src/main/resources/music/branding/youtube_music/splash/drawable-large-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/youtube_music/splash/drawable-large-hdpi/record.png rename to patches/src/main/resources/music/branding/youtube_music/splash/drawable-large-hdpi/record.png diff --git a/src/main/resources/music/branding/youtube_music/splash/drawable-large-mdpi/record.png b/patches/src/main/resources/music/branding/youtube_music/splash/drawable-large-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/youtube_music/splash/drawable-large-mdpi/record.png rename to patches/src/main/resources/music/branding/youtube_music/splash/drawable-large-mdpi/record.png diff --git a/src/main/resources/music/branding/youtube_music/splash/drawable-large-xhdpi/record.png b/patches/src/main/resources/music/branding/youtube_music/splash/drawable-large-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/youtube_music/splash/drawable-large-xhdpi/record.png rename to patches/src/main/resources/music/branding/youtube_music/splash/drawable-large-xhdpi/record.png diff --git a/src/main/resources/music/branding/youtube_music/splash/drawable-mdpi/record.png b/patches/src/main/resources/music/branding/youtube_music/splash/drawable-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/youtube_music/splash/drawable-mdpi/record.png rename to patches/src/main/resources/music/branding/youtube_music/splash/drawable-mdpi/record.png diff --git a/src/main/resources/music/branding/youtube_music/splash/drawable-xhdpi/record.png b/patches/src/main/resources/music/branding/youtube_music/splash/drawable-xhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/youtube_music/splash/drawable-xhdpi/record.png rename to patches/src/main/resources/music/branding/youtube_music/splash/drawable-xhdpi/record.png diff --git a/src/main/resources/music/branding/youtube_music/splash/drawable-xlarge-hdpi/record.png b/patches/src/main/resources/music/branding/youtube_music/splash/drawable-xlarge-hdpi/record.png similarity index 100% rename from src/main/resources/music/branding/youtube_music/splash/drawable-xlarge-hdpi/record.png rename to patches/src/main/resources/music/branding/youtube_music/splash/drawable-xlarge-hdpi/record.png diff --git a/src/main/resources/music/branding/youtube_music/splash/drawable-xlarge-mdpi/record.png b/patches/src/main/resources/music/branding/youtube_music/splash/drawable-xlarge-mdpi/record.png similarity index 100% rename from src/main/resources/music/branding/youtube_music/splash/drawable-xlarge-mdpi/record.png rename to patches/src/main/resources/music/branding/youtube_music/splash/drawable-xlarge-mdpi/record.png diff --git a/src/main/resources/music/branding/youtube_music/splash/drawable-xxhdpi/record.png b/patches/src/main/resources/music/branding/youtube_music/splash/drawable-xxhdpi/record.png similarity index 100% rename from src/main/resources/music/branding/youtube_music/splash/drawable-xxhdpi/record.png rename to patches/src/main/resources/music/branding/youtube_music/splash/drawable-xxhdpi/record.png diff --git a/src/main/resources/music/flyout/drawable-hdpi/yt_outline_play_arrow_half_circle_black_24.png b/patches/src/main/resources/music/flyout/drawable-hdpi/yt_outline_play_arrow_half_circle_black_24.png similarity index 100% rename from src/main/resources/music/flyout/drawable-hdpi/yt_outline_play_arrow_half_circle_black_24.png rename to patches/src/main/resources/music/flyout/drawable-hdpi/yt_outline_play_arrow_half_circle_black_24.png diff --git a/src/main/resources/music/flyout/drawable-mdpi/yt_outline_play_arrow_half_circle_black_24.png b/patches/src/main/resources/music/flyout/drawable-mdpi/yt_outline_play_arrow_half_circle_black_24.png similarity index 100% rename from src/main/resources/music/flyout/drawable-mdpi/yt_outline_play_arrow_half_circle_black_24.png rename to patches/src/main/resources/music/flyout/drawable-mdpi/yt_outline_play_arrow_half_circle_black_24.png diff --git a/src/main/resources/music/flyout/drawable-xhdpi/yt_outline_play_arrow_half_circle_black_24.png b/patches/src/main/resources/music/flyout/drawable-xhdpi/yt_outline_play_arrow_half_circle_black_24.png similarity index 100% rename from src/main/resources/music/flyout/drawable-xhdpi/yt_outline_play_arrow_half_circle_black_24.png rename to patches/src/main/resources/music/flyout/drawable-xhdpi/yt_outline_play_arrow_half_circle_black_24.png diff --git a/src/main/resources/music/flyout/drawable-xxhdpi/yt_outline_play_arrow_half_circle_black_24.png b/patches/src/main/resources/music/flyout/drawable-xxhdpi/yt_outline_play_arrow_half_circle_black_24.png similarity index 100% rename from src/main/resources/music/flyout/drawable-xxhdpi/yt_outline_play_arrow_half_circle_black_24.png rename to patches/src/main/resources/music/flyout/drawable-xxhdpi/yt_outline_play_arrow_half_circle_black_24.png diff --git a/src/main/resources/music/flyout/drawable-xxxhdpi/yt_outline_play_arrow_half_circle_black_24.png b/patches/src/main/resources/music/flyout/drawable-xxxhdpi/yt_outline_play_arrow_half_circle_black_24.png similarity index 100% rename from src/main/resources/music/flyout/drawable-xxxhdpi/yt_outline_play_arrow_half_circle_black_24.png rename to patches/src/main/resources/music/flyout/drawable-xxxhdpi/yt_outline_play_arrow_half_circle_black_24.png diff --git a/patches/src/main/resources/music/settings/host/values/arrays.xml b/patches/src/main/resources/music/settings/host/values/arrays.xml new file mode 100644 index 000000000..81986ed4c --- /dev/null +++ b/patches/src/main/resources/music/settings/host/values/arrays.xml @@ -0,0 +1,78 @@ + + + + @string/revanced_change_start_page_entry_chart + @string/revanced_change_start_page_entry_explore + @string/revanced_change_start_page_entry_home + @string/revanced_change_start_page_entry_library + @string/revanced_change_start_page_entry_subscription + + + FEmusic_charts + FEmusic_explore + FEmusic_home + FEmusic_library_landing + FEmusic_library_corpus_artists + + + @string/revanced_extended_settings_export_as_file + @string/revanced_extended_settings_import_as_file + @string/revanced_extended_settings_import_export_as_text + + + NewPipe + Seal + Tubular + YTDLnis + + + org.schabi.newpipe + com.junkfood.seal + org.polymorphicshade.tubular + com.deniscerri.ytdl + + + https://github.com/TeamNewPipe/NewPipe/releases/latest + https://github.com/JunkFood02/Seal/releases/latest + https://github.com/polymorphicshade/Tubular/releases/latest + https://github.com/deniscerri/ytdlnis/releases/latest + + + @string/revanced_return_youtube_username_display_format_username_only + @string/revanced_return_youtube_username_display_format_username_handle + @string/revanced_return_youtube_username_display_format_handle_username + + + USERNAME_ONLY + USERNAME_HANDLE + HANDLE_USERNAME + + + @string/revanced_spoof_app_version_target_entry_6_11_52 + @string/revanced_spoof_app_version_target_entry_4_27_53 + + + 6.11.52 + 4.27.53 + + + @string/revanced_spoof_client_type_entry_ios_music_6_21 + @string/revanced_spoof_client_type_entry_android_music_5_29 + @string/revanced_spoof_client_type_entry_android_music_4_27 + + + IOS_MUSIC_6_21 + ANDROID_MUSIC_5_29 + ANDROID_MUSIC_4_27 + + + @string/revanced_spoof_streaming_data_type_entry_android_vr + @string/revanced_spoof_streaming_data_type_entry_ios + @string/revanced_spoof_streaming_data_type_entry_ios_music + + + ANDROID_VR + IOS + IOS_MUSIC + + diff --git a/src/main/resources/music/settings/host/values/colors.xml b/patches/src/main/resources/music/settings/host/values/colors.xml similarity index 100% rename from src/main/resources/music/settings/host/values/colors.xml rename to patches/src/main/resources/music/settings/host/values/colors.xml diff --git a/patches/src/main/resources/music/settings/host/values/strings.xml b/patches/src/main/resources/music/settings/host/values/strings.xml new file mode 100644 index 000000000..f71f8f832 --- /dev/null +++ b/patches/src/main/resources/music/settings/host/values/strings.xml @@ -0,0 +1,435 @@ + + + Continue + Don\'t show again + "GmsCore does not have permission to run in the background. + +Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. + +This is required for the app to work." + "GmsCore battery optimizations must be disabled to prevent issues. + +Disabling battery optimizations for GmsCore will not negatively affect battery usage. + +Tap the continue button and allow optimization changes." + Open website + Action needed + Enable cloud messaging to receive notifications. + Open GmsCore + GmsCore is not installed. Install it. + Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. + Bypass image region restrictions + Change from in-app share sheet to system share sheet. + Change share sheet + Charts + Explore + Home + Library + Subscriptions + Select which page the app opens in. + Change start page + List of component path builder strings to filter, separated by new lines. + Custom filter + Enables the custom filter to hide layout components. + Enable custom filter + Invalid custom filter: %s. + Custom speeds must be less than %sx. + Invalid custom playback speeds. + Add or change available playback speeds. + Edit custom playback speeds + To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. + Open default app settings + Disables captions from being automatically enabled. + Disable forced auto captions + Disables Cairo splash animation when the app starts up. + Disable Cairo splash animation + Disables redirection to the next track when clicking the Dislike button. + Disable dislike redirection + Disables DRC (Dynamic Range Compression) applied to audio. + Disable DRC audio + Disable swipe to change tracks in the miniplayer. + Disable miniplayer gesture + Disable swipe to change tracks in the player. + Disable player gesture + Sets the navigation bar color to black. + Enable black navigation bar + Changes the player background color to black. + Enable black player background + Matches the color of the miniplayer to the fullscreen player. + Enable color match player + "Enables the compact flyout menu on phones. + +Limitations: +• Album art in the Library tab becomes smaller when organized in a grid. +• Sleep timer layout may appear unusual." + Enable compact dialog + Includes the buffer in the debug log. + Enable debug buffer logging + Prints the debug log. + Enable debug logging + Keeps the player minimized even when another track is played. + Enable force minimized player + Enables landscape mode when rotating the screen on phones. + Enable landscape mode + Adds a next track button to the miniplayer. + Add miniplayer next button + Adds a previous track button to the miniplayer. + Add miniplayer previous button + "Enable the OPUS codec if the player response includes the OPUS codec. + +Info: +• Latest YouTube Music clients use the OPUS audio codec by default. +• This is only valid for users spoofing with very old clients." + Enable OPUS codec + Enables swipe down to dismiss miniplayer. + Enable swipe to dismiss miniplayer + "Adds a Trim silence switch to the playback speed flyout menu. + +Info: +• This feature is for podcasts. +• This feature is still in development, so it may be unstable." + Add Trim silence switch + Also enables Zen mode for podcasts. + Enable Zen mode in podcasts + Changes the player background color to light grey to reduce eye strain. + Enable Zen mode + Reset to default values. + Restart to load the layout normally + Refresh and restart + Export settings to file + Failed to export settings. + Settings were successfully exported. + Import + Import settings from file + Copy + Import / Export settings as text + Import or export settings. + Import / Export settings + Import failed: %s. + Settings reset to default. + Imported %d settings. + Reset + ReVanced Extended + "Download button opens your external downloader. + +• Only overrides the Download action button in the player. +• Does not override the Download button in the flyout menu or Library tab." + Override Download action button + External downloader + "%1$s is not installed. +Please download %2$s from the website." + Warning + %s is not installed. Please install it. + Package name of your installed external downloader app, such as NewPipe or YTDLnis. + External downloader package name + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + Hides empty components in the account menu. + Hide empty components + List of account menu names to filter, separated by new lines. + Account menu filter + Hides account menu elements using the custom filter. + Hide account menu + Hides the Save button. + Hide Save button + Hides the Comments button. + Hide Comments button + Hides the Download button. + Hide Download button + Hides the labels of the action buttons. + Hide action button labels + Hides the Like and Dislike buttons. It does not work in the old player layout. + Hide Like and Dislike buttons + Hides the Radio button. + Hide Radio button + Hides the Share button. + Hide Share button + Hides the Audio / Video toggle in the player. + Hide Audio / Video toggle + Hides the button shelf in the feed. + Hide button shelf + Hides the carousel shelf in the feed. + Hide carousel shelf + Hides the Cast button. + Hide Cast button + Hides the category bar. + Hide category bar + Hides the channel guidelines at the top of the comments section. + Hide channel guidelines + Hides the timestamp and emoji buttons when typing comments. + Hide timestamp and emoji buttons + Hides dark overlay that appears when double-tapping to seek. + Hide double-tap overlay filter + Hides the floating button in the Library tab. + Hide floating button + Hide 3-column component + Hide Add to queue menu + Hide Captions menu + Hide Delete playlist menu + Hide Dismiss queue menu + Hide Download menu + Hide Edit playlist menu + Hide Go to album menu + Hide Go to artist menu + Hide Go to episode menu + Hide Go to podcast menu + Hide Help & feedback menu + Hide Like and Dislike buttons + Hide Pin to Speed dial menu + Hide Play next menu + Hide Quality menu + Hide Remove from library menu + Hide Remove from playlist menu + Hide Report menu + Hide Save episode for later menu + Hide Save to library menu + Hide Save to playlist menu + Hide Share menu + Hide Shuffle play menu + Hide Sleep timer menu + Hide Start radio menu + Hide Stats for nerds menu + Hide Subscribe / Unsubscribe menu + Hide Unpin from Speed dial menu + Hide View song credits menu + "Hides fullscreen ads. + +Limitations: +• Sometimes you may see a blank black screen instead of the home feed." + Hide fullscreen ads + Hides the Share button in the fullscreen player. + Hide fullscreen Share button + Hides general ads. + Hide general ads + Hides the handle in the account menu. + Hide handle + Hides the History button in the toolbar. + Hide History button + Hides ads before playing media. + Hide media ads + Hides the navigation bar. + Hide navigation bar + Hides the Explore button. + Hide Explore button + Hides the Home button. + Hide Home button + Hides labels below the navigation buttons. + Hide navigation labels + Hides the Library button. + Hide Library button + Hides the Samples button. + Hide Samples button + Hides the Upgrade button. + Hide Upgrade button + Hides the Notifications button in the toolbar. + Hide Notifications button + Hides the paid promotion label. + Hide paid promotion label + Hides the playlist card shelf in the feed. + Hide playlist card shelf + Hides premium promotion popups. + Hide premium promotion popups + Hides the premium renewal banner. + Hide premium renewal banner + Hides the promotion alert banner. + Hide promotion alert banner + Hides the Samples shelf in the feed. + Hide Samples shelf + Hide About menu + Hide Data saving menu + Hide Downloads & storage menu + Hide General menu + Hide Notifications menu + Hide Get Music premium menu + Hide Family Center menu + Hide Playback menu + Hide Privacy & data menu + Hide Recommendations menu + "Hide elements of the settings menu. +This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." + Hide settings menu + Hides the sound search button in the search bar. + Hide sound search button + Hides the Tap to update button. + Hide Tap to update button + Hides the Terms of Service container. + Hide terms container + Hides the voice search button in the search bar. + Hide voice search button + Account + Action Bar + Ads + Flyout Menu + General + Miscellaneous + Navigation Bar + Player + Return YouTube Username + Return YouTube Dislike + SponsorBlock + Settings menu + Video + Remembers the last playback speed selected. + Remember playback speed changes + Show a toast when changing the default playback speed. + Show a toast + Changing default speed to %s. + Remembers the state of the repeat toggle. + Remember repeat state + Remembers the state of the shuffle toggle. + Remember shuffle state + Remembers the last video quality selected. + Remember video quality changes + Show a toast when changing the default video quality. + Show a toast + Changing default mobile data quality to %s. + Failed to set quality. + Changing default Wi-Fi quality to %s. + "Removes the viewer discretion dialog. +This does not bypass the age restriction. It just accepts it automatically." + Remove viewer discretion dialog + Continues the video from the current time when switching to YouTube. + Continue watching + Replaces the Dismiss queue menu with the Watch on YouTube menu. + Replace Dismiss queue menu + Watch on YouTube + Invalid video url. + Keeps the Report menu in the comments section intact. + Keep Report in comments + Replaces the Report menu with the Playback speed menu. + Replace Report menu + Returns the comments popup panels to the old style. + Restore old comments popup panels + Returns the player background to the old style. + Restore old player background + "Returns the player layout to the old style. +Some features may not work properly in the old player layout." + Restore old player layout + Returns the Library tab to the old style. (Experimental) + Restore old style library shelf + @handle (Username) + Select the username display format. + Display format + Username (@handle) + Username + Replaces handles with usernames in comments. + Enable Return YouTube Username + "A YouTube Data API v3 Developer Key is required to replace handles with usernames. + +The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. + +Click to see how to issue a API key." + About YouTube Data API key + The developer key for using the YouTube Data API v3. + YouTube Data API key + 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. + Issue YouTube Data API v3 developer key + About + Data is provided by the Return YouTube Dislike API. Tap here to learn more. + ReturnYouTubeDislike.com + Hides the separator of the like button. + Compact like button + Displays the percentage of dislikes instead of the dislike count. + Dislikes as percentage + Shows the dislike count of videos. + Enable Return YouTube Dislike + Shows the estimated like count of videos. + Show estimated likes + Dislikes are unavailable (client API limit reached). + Dislikes are unavailable (status %d). + Dislikes are temporarily unavailable (API timed out). + Dislikes are unavailable (%s). + Shows a toast if the Return YouTube Dislike API is unavailable. + Show a toast if API is unavailable + Hidden + Removes tracking query parameters from URLs when sharing links. + Sanitize sharing links + About + sponsor.ajay.app + Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. + Change API URL + API URL changed. + API URL is invalid. + API URL reset. + The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. + Color changed. + Color: + Invalid color code. + Color reset. + Change segment behavior + Enable SponsorBlock + SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. + Reset color + Filler Tangent / Jokes + Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. + Interaction Reminder (Subscribe) + A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. + Intermission / Intro Animation + An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. + Music: Non-Music Section + Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. + Endcards / Credits + Credits or when the YouTube endcards appear. Not for conclusions with information. + Preview / Recap / Hook + Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. + Unpaid / Self Promotion + Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. + Sponsor + Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. + Skip automatically + Disable + Skipped filler. + Skipped annoying reminder. + Skipped intro. + Skipped intermission. + Skipped intermission. + Skipped multiple segments. + Skipped a non-music section. + Skipped outro. + Skipped preview. + Skipped recap. + Skipped preview. + Skipped self promotion. + Skipped sponsor. + SponsorBlock is temporarily unavailable. + SponsorBlock is temporarily unavailable (status %d). + SponsorBlock is temporarily unavailable (API timed out). + Show a toast if API is unavailable + Shows a toast if the SponsorBlock API is unavailable. + Show a toast when skipping automatically + Shows a toast when a segment is automatically skipped. + Settings copied to clipboard. + "Spoofs the client version to an older version. + +• This will change the appearance of the app, but unknown side effects may occur. +• If later disabled, the old UI may remain until the app data is cleared." + 4.27.53 - Disable Radio mode in Canadian regions + 6.11.52 - Disable real-time lyrics + 7.16.53 - Restore old action bar + Select the spoof app version target. + Spoof app version target + Spoof app version + "Spoof the client to prevent playback issues. + +※ When used with 'Spoofing streaming data', playback issues may occur." + Spoof client + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + Android TV + Android VR + iOS + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/src/main/resources/music/settings/icons/drawable-xxhdpi/empty_icon.png b/patches/src/main/resources/music/settings/icons/drawable-xxhdpi/empty_icon.png similarity index 100% rename from src/main/resources/music/settings/icons/drawable-xxhdpi/empty_icon.png rename to patches/src/main/resources/music/settings/icons/drawable-xxhdpi/empty_icon.png diff --git a/src/main/resources/music/settings/icons/drawable/icon.xml b/patches/src/main/resources/music/settings/icons/drawable/icon.xml similarity index 100% rename from src/main/resources/music/settings/icons/drawable/icon.xml rename to patches/src/main/resources/music/settings/icons/drawable/icon.xml diff --git a/src/main/resources/music/settings/icons/extension/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/settings/icons/extension/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/settings/icons/extension/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/music/settings/icons/extension/drawable/revanced_extended_settings_icon.xml diff --git a/src/main/resources/music/settings/icons/gear/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/settings/icons/gear/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/settings/icons/gear/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/music/settings/icons/gear/drawable/revanced_extended_settings_icon.xml diff --git a/src/main/resources/music/settings/icons/revanced/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/settings/icons/revanced/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/settings/icons/revanced/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/music/settings/icons/revanced/drawable/revanced_extended_settings_icon.xml diff --git a/src/main/resources/music/settings/icons/revanced_colored/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/settings/icons/revanced_colored/drawable/revanced_extended_settings_icon.xml old mode 100755 new mode 100644 similarity index 100% rename from src/main/resources/music/settings/icons/revanced_colored/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/music/settings/icons/revanced_colored/drawable/revanced_extended_settings_icon.xml diff --git a/patches/src/main/resources/music/translations/bg-rBG/missing_strings.xml b/patches/src/main/resources/music/translations/bg-rBG/missing_strings.xml new file mode 100644 index 000000000..737711dba --- /dev/null +++ b/patches/src/main/resources/music/translations/bg-rBG/missing_strings.xml @@ -0,0 +1,236 @@ + + + Continue + Don\'t show again + "GmsCore does not have permission to run in the background. + +Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. + +This is required for the app to work." + "GmsCore battery optimizations must be disabled to prevent issues. + +Disabling battery optimizations for GmsCore will not negatively affect battery usage. + +Tap the continue button and allow optimization changes." + Open website + Action needed + Enable cloud messaging to receive notifications. + Open GmsCore + GmsCore is not installed. Install it. + Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. + Bypass image region restrictions + Change from in-app share sheet to system share sheet. + Change share sheet + Invalid custom playback speeds. + To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. + Open default app settings + Disables Cairo splash animation when the app starts up. + Disable Cairo splash animation + Disables DRC (Dynamic Range Compression) applied to audio. + Disable DRC audio + Disable swipe to change tracks in the miniplayer. + Disable miniplayer gesture + Disable swipe to change tracks in the player. + Disable player gesture + Changes the player background color to black. + Enable black player background + Includes the buffer in the debug log. + Enable debug buffer logging + Prints the debug log. + Adds a next track button to the miniplayer. + Add miniplayer next button + Adds a previous track button to the miniplayer. + Add miniplayer previous button + "Enable the OPUS codec if the player response includes the OPUS codec. + +Info: +• Latest YouTube Music clients use the OPUS audio codec by default. +• This is only valid for users spoofing with very old clients." + Enables swipe down to dismiss miniplayer. + Enable swipe to dismiss miniplayer + Also enables Zen mode for podcasts. + Enable Zen mode in podcasts + Reset to default values. + Export settings to file + Failed to export settings. + Settings were successfully exported. + Import settings from file + Import / Export settings as text + Import / Export settings + Import failed: %s. + Settings reset to default. + Imported %d settings. + Reset + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + Hides the Audio / Video toggle in the player. + Hide Audio / Video toggle + Hides the channel guidelines at the top of the comments section. + Hide channel guidelines + Hides the timestamp and emoji buttons when typing comments. + Hide timestamp and emoji buttons + Hides dark overlay that appears when double-tapping to seek. + Hide double-tap overlay filter + Hide Pin to Speed dial menu + Hide Unpin from Speed dial menu + Hides the Share button in the fullscreen player. + Hide fullscreen Share button + Hides labels below the navigation buttons. + Hides the Library button. + Hides the Samples button. + Hide Samples button + Hides the Upgrade button. + Hide Upgrade button + Hides the promotion alert banner. + Hide promotion alert banner + Hide About menu + Hide Data saving menu + Hide Downloads & storage menu + Hide General menu + Hide Notifications menu + Hide Get Music premium menu + Hide Family Center menu + Hide Playback menu + Hide Privacy & data menu + Hide Recommendations menu + Miscellaneous + Return YouTube Username + Return YouTube Dislike + SponsorBlock + Settings menu + Video + Remembers the last playback speed selected. + Remember playback speed changes + Show a toast when changing the default playback speed. + Show a toast + Changing default speed to %s. + Remembers the state of the repeat toggle. + Remember repeat state + Remembers the state of the shuffle toggle. + Remember shuffle state + Remembers the last video quality selected. + Remember video quality changes + Show a toast when changing the default video quality. + Show a toast + Changing default mobile data quality to %s. + Failed to set quality. + Changing default Wi-Fi quality to %s. + Returns the comments popup panels to the old style. + Restore old comments popup panels + Returns the player background to the old style. + Restore old player background + "Returns the player layout to the old style. +Some features may not work properly in the old player layout." + Restore old player layout + @handle (Username) + Select the username display format. + Display format + Username (@handle) + Username + Replaces handles with usernames in comments. + Enable Return YouTube Username + "A YouTube Data API v3 Developer Key is required to replace handles with usernames. + +The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. + +Click to see how to issue a API key." + About YouTube Data API key + The developer key for using the YouTube Data API v3. + YouTube Data API key + 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. + Issue YouTube Data API v3 developer key + ReturnYouTubeDislike.com + Hides the separator of the like button. + Displays the percentage of dislikes instead of the dislike count. + Shows the dislike count of videos. + Enable Return YouTube Dislike + Shows the estimated like count of videos. + Show estimated likes + Dislikes are unavailable (status %d). + Dislikes are temporarily unavailable (API timed out). + Dislikes are unavailable (%s). + Shows a toast if the Return YouTube Dislike API is unavailable. + Show a toast if API is unavailable + Hidden + Removes tracking query parameters from URLs when sharing links. + Sanitize sharing links + About + sponsor.ajay.app + Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. + Change API URL + API URL changed. + API URL is invalid. + API URL reset. + The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. + Color changed. + Color: + Invalid color code. + Color reset. + Change segment behavior + Enable SponsorBlock + SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. + Reset color + Filler Tangent / Jokes + Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. + Interaction Reminder (Subscribe) + A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. + Intermission / Intro Animation + An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. + Music: Non-Music Section + Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. + Endcards / Credits + Credits or when the YouTube endcards appear. Not for conclusions with information. + Preview / Recap / Hook + Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. + Unpaid / Self Promotion + Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. + Sponsor + Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. + Skip automatically + Disable + Skipped filler. + Skipped annoying reminder. + Skipped intro. + Skipped intermission. + Skipped intermission. + Skipped multiple segments. + Skipped a non-music section. + Skipped outro. + Skipped preview. + Skipped recap. + Skipped preview. + Skipped self promotion. + Skipped sponsor. + SponsorBlock is temporarily unavailable. + SponsorBlock is temporarily unavailable (status %d). + SponsorBlock is temporarily unavailable (API timed out). + Show a toast if API is unavailable + Shows a toast if the SponsorBlock API is unavailable. + Show a toast when skipping automatically + Shows a toast when a segment is automatically skipped. + Settings copied to clipboard. + 7.16.53 - Restore old action bar + "Spoof the client to prevent playback issues. + +※ When used with 'Spoofing streaming data', playback issues may occur." + Spoof client + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + Android TV + Android VR + iOS + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/bg-rBG/strings.xml b/patches/src/main/resources/music/translations/bg-rBG/strings.xml new file mode 100644 index 000000000..9f758276b --- /dev/null +++ b/patches/src/main/resources/music/translations/bg-rBG/strings.xml @@ -0,0 +1,198 @@ + + + Хит-парад + Преглед + Начало + Библиотека + Абонаменти + Изберете на коя страница да се отвори приложението. + Промяна на началната страница + Филтриране на компонентите по имена. + Редактиране на потребителския филтъра + Активира персонализиран филтър за скриване на компонентите на оформлението. + Вкл. на филтър по избор + Невалиден потребителски филтър: %s. + Невалидна скорост на видеото. Връщане на стойности по подразбиране. + Добавяне или смяна на възможните скорости + Редактиране на скоростите по избор на видеото + Изкл. принудителни автоматични субтититри. + Изкл. принудителни автоматични субтититри + Деактивира пренасочването към следващата песен, когато щракнете върху бутона „Не харесвам“. + Désactiver la redirection du bouton \"Je n\'aime pas\" + Задава цвета на лентата за навигация на черен. + Включване на черна навигационна лента + Цветът на плейъра на цял екран съответства с цвета на минимизирания. + A játékos színmegfelelésének engedélyezése + "Активира компактно изскачащо меню на телефони. + +Известни проблеми: +• Скрийнсейвърите на албуми в раздела \"Библиотека\" стават по-малки в мрежа. +• Интерфейсът за автоматично изключване може да изглежда необичайно." + Компактен изглед на прозореца + Вкл. отчети за грешки + Tartsa a lejátszót mindig minimálisra, még akkor is, ha egy másik számot játszik le. + Kapcsolja be az állandó összeomlott lejátszót + Активира пейзажен режим при завъртане на телефона. + Позволи Пейзажен Режим + Включване на OPUS аудио кодек + "Добавя „Скриване на мълчанията“ към падащото меню „Скорост на възпроизвеждане“. + +Информация: +• Тази функция е предназначена за подкасти. +• Тази функция все още е в процес на разработка, така че може да е нестабилна." + Добавете опция „Скриване на мълчанията“ + Добавя сив оттенък към видеоплейъра, за да намали напрежението на очите. + Включване на zen режим + Рестартирайте, за да заредите оформлението нормално + Опреснете и рестартирайте + Внасяне + Копиране + Импортирайте или експортирайте настройки като текст. + ReVanced Extended + "Бутонът \"Изтегляне\" отваря външната програма за изтегляне. + +• Заменя само бутона за изтегляне в плейъра. +• Не отменя бутона за изтегляне в изскачащото меню или библиотеката." + Замяна на бутона за изтегляне + Външна програма за изтегляне + "%1$s не е инсталиран. +Моля, изтеглете %2$s от уебсайта." + Внимание + %s не е инсталирано. Моля инсталирайте го. + Име на пакета на приложението за изтегляне като NewPipe или YTDLnis. + Име на пакета на външно приложение за изтегляне + Скрива празните компоненти в менюто на акаунта. + Скриване на празни компоненти + Списък с имена на менюта на акаунти за филтриране, разделени с нови редове. + Промяна на филтъра на менюто на акаунта + Скрива елементи от менюто на акаунта в персонализиран филтър. + Скриване на менюто на акаунта + Скрива бутона \"Запазване\". + Бутон \"Запазване\" + Скрийте бутона „Коментари“. + Скриване на бутона за коментари + Скрива бутона „Изтегляне“. + Скриване на бутона за изтегляне + Скрива. етикетите на бутоните за действие. + Скриване на етикетите на бутоните за действие + Скрива бутоните „Харесвам“ и „Не харесвам“. Не работи в стария интерфейс на плейъра. + Скриване на бутоните за харесване и нехаресване + Скрива бутона \"Радио\". + Скрийте бутона \"Радио\" + Скрива бутона „Споделяне“. + Скриване на бутона за споделяне + Скриване на секцията с бутони в емисията. + Скриване на секцията с бутони + Скриване на рафтовете с предложения в емисиите. + Скриване на рафта с Препоръчани + Скрива бутона \"Излъчване\". + Скриване на бутона за предаване на Тв + Скриване на панела с категории. + Скриване на панела с Категории + Скрива плаващите бутони в библиотеката. + Скриване на изскачащ бутон + Скриване на компонента с 3 колони + Скрийте бутона „Добавяне към опашката“ + Скриване на менюто за субтитри + Скрийте менюто „Изтриване на плейлист“ + Скрийте менюто „Изтриване на опашката“ + Скрийте менюто „Изтегляне“ + Скрийте менюто „Редактиране на плейлист“ + Скрийте менюто „Отиди на албум“ + Скрийте менюто „Отидете на страницата на изпълнителя“ + Скрийте менюто „Отидете на епизод“ + Скрийте менюто „Отидете на подкаст“ + Скриване на менюто & за помощ + Скриване на бутоните за харесване и нехаресване + Скрийте менюто „Пусни следващия клип“ + Скрийте менюто „Качество“ + Скрийте менюто „Премахване от библиотеката“ + Скрийте менюто „Изтриване на плейлист“ + Меню за докладване + Скрийте менюто „Запазване за гледане по-късно“ + Скрийте менюто „Запазване в библиотеката“ + Скрийте менюто „Запазване в плейлист“ + Скрийте менюто „Споделяне“ + Скрийте бутона „Разбъркване“ + Скрийте менюто „Изчакване на заспиване“ + Скрийте менюто „Стартиране на радио“ + Меню \"Статистика за сис. администратори\" + Скрийте менюто „Абониране“ / „Отписване“ + Скрийте менюто „Подробности за заглавие“ + "Скриване на рекламите в режим на цял екран." + Скриване на рекламите в режим на цял екран + Скриване на общите реклами. + Скриване на общите реклами + Скрива имейл/@ник в менюто за промяна на акаунта. + Скриване на връзки + Скрива бутона \"История\" от лентата с инструменти. + Скрийте бутона \"История\" + Скрива реклами преди възпроизвеждане на музика. + Скриване на музикални реклами + Скриване лентата за навигация. + Скриване лентата за навигация + Скрива. бутона за Преглед. + Скриване на бутона \"навигация\" + Скрива бутона \"Начало\". + Скриване на бутон за Начало + Скриване на навигационен панел + Бутона за Библиотека + Скрива бутона „Известие“ от лентата с инструменти. + Бутон за Известия + Скриване на платените промоции. + Скриване на платените промоции + Скрива рафтовете с карти „Списъци за изпълнение“ в емисии. + Скрийте рафтовете „Списъци за изпълнение“ + Скрива изскачащи реклами Premium. + Скриване на изскачащи реклами Premium + Скриване на банера за подновяване на Premium. + Скриване на банера за подновяване на Premium + Скриване на рафтовете с Семпли в емисиите. + Скрийте рафта „Семпли“ + "Скриване на елементи от менюто с настройки. +Това не само скрива менюто с настройки на YT Music, но и менюто с разширени настройки на ReVanced." + Скрийте менюто „Настройки“ + Скрива бутона „звуково търсене на музика“ от лентата за търсене. + Бутон за \"Звуково търсене\" + Скриване на бутона „Докоснете за актуализиране“. + Скрийте бутона „Докоснете за актуализиране“ + Скриване на подробностите за поверителност / правила и условия. + Скриване на информацията за поверителност + Скрива бутона „Гласово търсене“ от лентата за търсене. + Бутон за \"гласово търсене\" + Акаунт + Лента с действия + Реклами + Падащо меню + Главни + Лента за навигация + Плеър + "Премахва диалоговите прозорци. Това не заобикаля възрастовите ограничения, но ги приема автоматично." + Скриване на прозореца за възрастово ограничение + Видеото продължава от текущото време на гледане, когато отидете в YouTube. + Продължете да гледате + Заменя менюто „Премахване от опашката“ с менюто „Гледайте в YouTube“. + Заменете менюто „Премахване от опашката“ + Гледайте в YouTube + Невалиден Url адрес на видеото. + Запазва менюто Доклад в раздела за коментари непокътнато. + Запазете доклада в коментарите + Заменя менюто Доклад с менюто Скорост на възпроизвеждане. + Заменете менюто „Докладвай“ + Възстановява стария стил на рафт \"Библиотека\". (Експериментално) + Възстановете стария стил на рафта „Библиотека“ + Относно + Данните за нехаресване са от Return YouTube Dislike API. Докоснете за да научите повече. + Компактен бутон за харесване + Нехаресвания като процент + Нехаресванията не са достъпни (достигнат лимит на API). + "Заменя клиентската версия със старата. + +• Това ще промени външния вид на приложението, но може да възникнат неизвестни проблеми. +• Ако деактивирате тази опция, след като я активирате, старият интерфейс може да остане, докато данните на приложението не бъдат изчистени." + 4.27.53 - Деактивира радио режима в регионите на Канада + 6.11.52 -Изключва речта в реално време + Задайте желаната фалшива версия на приложението. + Подлъгване за версията на приложението + Подлъгване за версията на приложението + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/bn/missing_strings.xml b/patches/src/main/resources/music/translations/bn/missing_strings.xml new file mode 100644 index 000000000..41dac670b --- /dev/null +++ b/patches/src/main/resources/music/translations/bn/missing_strings.xml @@ -0,0 +1,367 @@ + + + Continue + Don\'t show again + "GmsCore does not have permission to run in the background. + +Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. + +This is required for the app to work." + "GmsCore battery optimizations must be disabled to prevent issues. + +Disabling battery optimizations for GmsCore will not negatively affect battery usage. + +Tap the continue button and allow optimization changes." + Open website + Action needed + Enable cloud messaging to receive notifications. + Open GmsCore + GmsCore is not installed. Install it. + Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. + Bypass image region restrictions + Change from in-app share sheet to system share sheet. + Change share sheet + Charts + Explore + Home + Library + Subscriptions + Select which page the app opens in. + Change start page + Invalid custom filter: %s. + Invalid custom playback speeds. + To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. + Open default app settings + Disables Cairo splash animation when the app starts up. + Disable Cairo splash animation + Disables redirection to the next track when clicking the Dislike button. + Disable dislike redirection + Disables DRC (Dynamic Range Compression) applied to audio. + Disable DRC audio + Disable swipe to change tracks in the miniplayer. + Disable miniplayer gesture + Disable swipe to change tracks in the player. + Disable player gesture + Changes the player background color to black. + Enable black player background + "Enables the compact flyout menu on phones. + +Limitations: +• Album art in the Library tab becomes smaller when organized in a grid. +• Sleep timer layout may appear unusual." + Includes the buffer in the debug log. + Enable debug buffer logging + Adds a next track button to the miniplayer. + Add miniplayer next button + Adds a previous track button to the miniplayer. + Add miniplayer previous button + Enables swipe down to dismiss miniplayer. + Enable swipe to dismiss miniplayer + "Adds a Trim silence switch to the playback speed flyout menu. + +Info: +• This feature is for podcasts. +• This feature is still in development, so it may be unstable." + Add Trim silence switch + Also enables Zen mode for podcasts. + Enable Zen mode in podcasts + Reset to default values. + Restart to load the layout normally + Refresh and restart + Export settings to file + Failed to export settings. + Settings were successfully exported. + Import settings from file + Import / Export settings as text + Import failed: %s. + Reset + ReVanced Extended + "Download button opens your external downloader. + +• Only overrides the Download action button in the player. +• Does not override the Download button in the flyout menu or Library tab." + Override Download action button + External downloader + "%1$s is not installed. +Please download %2$s from the website." + Warning + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + List of account menu names to filter, separated by new lines. + Account menu filter + Hides the Save button. + Hide Save button + Hides the Comments button. + Hide Comments button + Hides the Download button. + Hide Download button + Hides the labels of the action buttons. + Hide action button labels + Hides the Like and Dislike buttons. It does not work in the old player layout. + Hide Like and Dislike buttons + Hides the Radio button. + Hide Radio button + Hides the Share button. + Hide Share button + Hides the Audio / Video toggle in the player. + Hide Audio / Video toggle + Hides the channel guidelines at the top of the comments section. + Hide channel guidelines + Hides the timestamp and emoji buttons when typing comments. + Hide timestamp and emoji buttons + Hides dark overlay that appears when double-tapping to seek. + Hide double-tap overlay filter + Hides the floating button in the Library tab. + Hide floating button + Hide 3-column component + Hide Add to queue menu + Hide Captions menu + Hide Delete playlist menu + Hide Dismiss queue menu + Hide Download menu + Hide Edit playlist menu + Hide Go to album menu + Hide Go to artist menu + Hide Go to episode menu + Hide Go to podcast menu + Hide Help & feedback menu + Hide Like and Dislike buttons + Hide Pin to Speed dial menu + Hide Play next menu + Hide Quality menu + Hide Remove from library menu + Hide Remove from playlist menu + Hide Report menu + Hide Save episode for later menu + Hide Save to library menu + Hide Save to playlist menu + Hide Share menu + Hide Shuffle play menu + Hide Sleep timer menu + Hide Start radio menu + Hide Stats for nerds menu + Hide Subscribe / Unsubscribe menu + Hide Unpin from Speed dial menu + Hide View song credits menu + "Hides fullscreen ads. + +Limitations: +• Sometimes you may see a blank black screen instead of the home feed." + Hide fullscreen ads + Hides the Share button in the fullscreen player. + Hide fullscreen Share button + Hides general ads. + Hide general ads + Hides the History button in the toolbar. + Hide History button + Hides the Explore button. + Hide Explore button + Hides the Home button. + Hide Home button + Hides the Library button. + Hide Library button + Hides the Samples button. + Hide Samples button + Hides the Upgrade button. + Hide Upgrade button + Hides the Notifications button in the toolbar. + Hide Notifications button + Hides the paid promotion label. + Hide paid promotion label + Hides the playlist card shelf in the feed. + Hide playlist card shelf + Hides premium promotion popups. + Hide premium promotion popups + Hides the premium renewal banner. + Hide premium renewal banner + Hides the promotion alert banner. + Hide promotion alert banner + Hides the Samples shelf in the feed. + Hide Samples shelf + Hide About menu + Hide Data saving menu + Hide Downloads & storage menu + Hide General menu + Hide Notifications menu + Hide Get Music premium menu + Hide Family Center menu + Hide Playback menu + Hide Privacy & data menu + Hide Recommendations menu + "Hide elements of the settings menu. +This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." + Hide settings menu + Hides the sound search button in the search bar. + Hide sound search button + Hides the Tap to update button. + Hide Tap to update button + Hides the voice search button in the search bar. + Hide voice search button + Account + Action Bar + Ads + Flyout Menu + General + Miscellaneous + Navigation Bar + Player + Return YouTube Username + Return YouTube Dislike + SponsorBlock + Settings menu + Video + Remembers the last playback speed selected. + Remember playback speed changes + Show a toast when changing the default playback speed. + Show a toast + Changing default speed to %s. + Remembers the state of the repeat toggle. + Remember repeat state + Remembers the state of the shuffle toggle. + Remember shuffle state + Remembers the last video quality selected. + Remember video quality changes + Show a toast when changing the default video quality. + Show a toast + Changing default mobile data quality to %s. + Failed to set quality. + Changing default Wi-Fi quality to %s. + "Removes the viewer discretion dialog. +This does not bypass the age restriction. It just accepts it automatically." + Remove viewer discretion dialog + Continues the video from the current time when switching to YouTube. + Continue watching + Replaces the Dismiss queue menu with the Watch on YouTube menu. + Replace Dismiss queue menu + Watch on YouTube + Invalid video url. + Keeps the Report menu in the comments section intact. + Keep Report in comments + Replaces the Report menu with the Playback speed menu. + Replace Report menu + Returns the comments popup panels to the old style. + Restore old comments popup panels + Returns the player background to the old style. + Restore old player background + "Returns the player layout to the old style. +Some features may not work properly in the old player layout." + Restore old player layout + Returns the Library tab to the old style. (Experimental) + Restore old style library shelf + @handle (Username) + Select the username display format. + Display format + Username (@handle) + Username + Replaces handles with usernames in comments. + Enable Return YouTube Username + "A YouTube Data API v3 Developer Key is required to replace handles with usernames. + +The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. + +Click to see how to issue a API key." + About YouTube Data API key + The developer key for using the YouTube Data API v3. + YouTube Data API key + 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. + Issue YouTube Data API v3 developer key + ReturnYouTubeDislike.com + Enable Return YouTube Dislike + Shows the estimated like count of videos. + Show estimated likes + Dislikes are unavailable (status %d). + Dislikes are temporarily unavailable (API timed out). + Dislikes are unavailable (%s). + Shows a toast if the Return YouTube Dislike API is unavailable. + Show a toast if API is unavailable + Hidden + Removes tracking query parameters from URLs when sharing links. + Sanitize sharing links + About + sponsor.ajay.app + Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. + Change API URL + API URL changed. + API URL is invalid. + API URL reset. + The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. + Color changed. + Color: + Invalid color code. + Color reset. + Change segment behavior + Enable SponsorBlock + SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. + Reset color + Filler Tangent / Jokes + Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. + Interaction Reminder (Subscribe) + A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. + Intermission / Intro Animation + An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. + Music: Non-Music Section + Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. + Endcards / Credits + Credits or when the YouTube endcards appear. Not for conclusions with information. + Preview / Recap / Hook + Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. + Unpaid / Self Promotion + Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. + Sponsor + Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. + Skip automatically + Disable + Skipped filler. + Skipped annoying reminder. + Skipped intro. + Skipped intermission. + Skipped intermission. + Skipped multiple segments. + Skipped a non-music section. + Skipped outro. + Skipped preview. + Skipped recap. + Skipped preview. + Skipped self promotion. + Skipped sponsor. + SponsorBlock is temporarily unavailable. + SponsorBlock is temporarily unavailable (status %d). + SponsorBlock is temporarily unavailable (API timed out). + Show a toast if API is unavailable + Shows a toast if the SponsorBlock API is unavailable. + Show a toast when skipping automatically + Shows a toast when a segment is automatically skipped. + Settings copied to clipboard. + "Spoofs the client version to an older version. + +• This will change the appearance of the app, but unknown side effects may occur. +• If later disabled, the old UI may remain until the app data is cleared." + 4.27.53 - Disable Radio mode in Canadian regions + 6.11.52 - Disable real-time lyrics + 7.16.53 - Restore old action bar + Select the spoof app version target. + Spoof app version target + "Spoof the client to prevent playback issues. + +※ When used with 'Spoofing streaming data', playback issues may occur." + Spoof client + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + Android TV + Android VR + iOS + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/bn/strings.xml b/patches/src/main/resources/music/translations/bn/strings.xml new file mode 100644 index 000000000..cb4608773 --- /dev/null +++ b/patches/src/main/resources/music/translations/bn/strings.xml @@ -0,0 +1,67 @@ + + + ভিন্ন লাইনে ফিল্টারযোগ্য উপাদানের নাম লিখুন। + কাস্টম ফিল্টার সম্পাদনা করুন + কাস্টম ফিল্টার সক্রিয় করুন + কাস্টম ফিল্টার সক্রিয় করুন + ত্রুটিপূর্ণ কাস্টম প্লেব্যাক স্পিড! পুর্বনির্ধারিত ভ্যালুতে আবার সেট করুন। + পাওয়া যাচ্ছে এমন প্লেব্যাক স্পিড যুক্ত বা পরিবর্তন করুন + কাস্টম প্লেব্যাক স্পিড সম্পাদনা করুন + ভিডিও প্লেয়ারে স্বয়ংক্রিয়ভাবে চালু হওয়া বাধ্যতামূলক ক্যাপশনগুলি অকার্যকর করুন। + স্বয়ংক্রিয় ক্যাপশন বন্ধ করুন + নেভিগেমন বার এর রং কালো করে। + কালো নেভিগেশন বার সক্রিয় করুন + পূর্ণস্ক্রীন প্লেয়ারের রং মিনিমাইজ করা প্লেয়ারের রং এর সাথে মিলবে। + প্লেয়ারের রং মিলানো সক্রিয় করুন + কম্প্যাক্ট ডায়ালগ সক্রিয় করুন + ডিবাগ লগ প্রিন্ট করে + ডিবাগ লগ সক্রিয় করুন + প্লেয়ারকে স্থায়ীভাবে মিনিমাইজ করে রাখুন এমনকি যদি অন্য ট্র্যাক চালানো হয়। + জোরপূর্বক মিনিমাইজড প্লেয়ার সক্রিয় করুন + ফোনের স্ক্রিন ঘুরিয়ে আড়াআড়ি মোডে প্রবেশ সক্রিয় করে। + আড়াআড়ি মোড সক্রিয় করুন + "অডিও প্লে করার সময় ২৫০/২৫১ অপাস কোডেক সক্রিয় করুন।" + Opus কোডেক সক্রিয় করুন + চোখের চাপ কমাতে ভিডিও প্লেয়ারে ধূসর আভা যোগ করুন। + জেন মোড সক্রিয় করুন + আমদানি করুন + কপি করুন + টেক্সট আকারে সেটিং আমদানি বা রপ্তানি করুন। + আমদানি / রপ্তানি + সেটিং পূর্ব নির্ধারিততে ফিরে গিয়েছে + %d সেটিং আমদানি হয়েছে + %s ইনস্টল করা হয়নি। অনুগ্রপূর্বক এটি ইনস্টল করুন। + আপনার ইনস্টল করা বাইরের ডাউনলোডার অ্যাপের প্যাকেজ নাম, যেমন NewPipe বা Seal + বাহিরের ডাউনলোডারের প্যাকেজ নাম + অ্যাকাউন্ট মেনুতে ফাঁকা উপাদানগুলো লুকান + ফাঁকা উপাদান লুকান + অ্যাকাউন্ট মেনু উপাদানগুলো লুকান। + অ্যাকাউন্ট মেনু লুকান + প্রধান পাতা ও এক্সপ্লোরার থেকে বাটন শেলফ লুকান। + বাটন শেলফ লুকান + প্রধান পাতা ও এক্সপ্লোরার থেকে ক্যারোসেল শেলফ লুকান। + ক্যারোসেল শেলফ লুকান + মূল পাতা এবং প্লেয়ারের উপর থেকে কাস্ট বাটন লুকিয়ে রাখে। + কাস্ট বাটন লুকান + প্রধান পাতার উপর থেকে বিভাগ বার লুকান। + বিভাগ বার লুকান + অ্যাকাউন্ট পরিবর্তনের হ্যান্ডল লুকায়। + হ্যান্ডল লুকান + ট্র্যাক চালু হওয়ার আগে বিজ্ঞাপনগুলি লুকান। + সঙ্গীতের বিজ্ঞাপন লুকান + নেভিগেশন বার লুকায়। + নেভিগেশন বার লুকান + নেভিগেশন বার থেকে লেবেল হাইড করুন। + নেভিগেশন বারের লেবেল লুকান + সার্ভিস কন্টেইনারের নীতিমালা লুকায়। + নীতিমালা কন্টেইনার লুকান + সম্পর্কে + তথ্য প্রদান করা হয় Return YouTube Dislike API দ্বারা। আরও জানতে আলতো চাপুন। + পছন্দ বাটনের বিভাজক লুকায়। + কমপ্যাক্ট পছন্দ বাটন + অপছন্দ সংখ্যার পরিবর্তে, অপছন্দের শতাংশ দেখায়। + শতাংশ অনুযায়ী অপছন্দ + ভিডিওর অপছন্দ কাউন্ট দেখায়। + অপছন্দ পাওয়া যাচ্ছে না (ক্লায়েন্ট API সর্বোচ্চ সীমা পৌঁছেছে) + অ্যাপ সংস্করণ স্পুফ করুন + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/cs-rCZ/missing_strings.xml b/patches/src/main/resources/music/translations/cs-rCZ/missing_strings.xml new file mode 100644 index 000000000..44d62d20d --- /dev/null +++ b/patches/src/main/resources/music/translations/cs-rCZ/missing_strings.xml @@ -0,0 +1,387 @@ + + + Continue + Don\'t show again + "GmsCore does not have permission to run in the background. + +Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. + +This is required for the app to work." + "GmsCore battery optimizations must be disabled to prevent issues. + +Disabling battery optimizations for GmsCore will not negatively affect battery usage. + +Tap the continue button and allow optimization changes." + Open website + Action needed + Enable cloud messaging to receive notifications. + Open GmsCore + GmsCore is not installed. Install it. + Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. + Bypass image region restrictions + Change from in-app share sheet to system share sheet. + Change share sheet + Charts + Explore + Home + Library + Subscriptions + Select which page the app opens in. + Change start page + List of component path builder strings to filter, separated by new lines. + Invalid custom filter: %s. + Custom speeds must be less than %sx. + Invalid custom playback speeds. + Add or change available playback speeds. + Edit custom playback speeds + To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. + Open default app settings + Disables Cairo splash animation when the app starts up. + Disable Cairo splash animation + Disables redirection to the next track when clicking the Dislike button. + Disable dislike redirection + Disables DRC (Dynamic Range Compression) applied to audio. + Disable DRC audio + Disable swipe to change tracks in the miniplayer. + Disable miniplayer gesture + Disable swipe to change tracks in the player. + Disable player gesture + Changes the player background color to black. + Enable black player background + "Enables the compact flyout menu on phones. + +Limitations: +• Album art in the Library tab becomes smaller when organized in a grid. +• Sleep timer layout may appear unusual." + Includes the buffer in the debug log. + Enable debug buffer logging + Adds a next track button to the miniplayer. + Add miniplayer next button + Adds a previous track button to the miniplayer. + Add miniplayer previous button + Enables swipe down to dismiss miniplayer. + Enable swipe to dismiss miniplayer + "Adds a Trim silence switch to the playback speed flyout menu. + +Info: +• This feature is for podcasts. +• This feature is still in development, so it may be unstable." + Add Trim silence switch + Also enables Zen mode for podcasts. + Enable Zen mode in podcasts + Export settings to file + Failed to export settings. + Settings were successfully exported. + Import + Import settings from file + Copy + Import / Export settings as text + Import or export settings. + Import / Export settings + Import failed: %s. + Settings reset to default. + Imported %d settings. + Reset + "Download button opens your external downloader. + +• Only overrides the Download action button in the player. +• Does not override the Download button in the flyout menu or Library tab." + Override Download action button + External downloader + "%1$s is not installed. +Please download %2$s from the website." + Warning + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + Hides empty components in the account menu. + Hide empty components + List of account menu names to filter, separated by new lines. + Hides the Save button. + Hide Save button + Hides the Comments button. + Hide Comments button + Hides the Download button. + Hide Download button + Hides the labels of the action buttons. + Hide action button labels + Hides the Like and Dislike buttons. It does not work in the old player layout. + Hide Like and Dislike buttons + Hides the Radio button. + Hide Radio button + Hides the Share button. + Hide Share button + Hides the Audio / Video toggle in the player. + Hide Audio / Video toggle + Hides the channel guidelines at the top of the comments section. + Hide channel guidelines + Hides the timestamp and emoji buttons when typing comments. + Hide timestamp and emoji buttons + Hides dark overlay that appears when double-tapping to seek. + Hide double-tap overlay filter + Hides the floating button in the Library tab. + Hide floating button + Hide 3-column component + Hide Add to queue menu + Hide Captions menu + Hide Delete playlist menu + Hide Dismiss queue menu + Hide Download menu + Hide Edit playlist menu + Hide Go to album menu + Hide Go to artist menu + Hide Go to episode menu + Hide Go to podcast menu + Hide Help & feedback menu + Hide Like and Dislike buttons + Hide Pin to Speed dial menu + Hide Play next menu + Hide Quality menu + Hide Remove from library menu + Hide Remove from playlist menu + Hide Report menu + Hide Save episode for later menu + Hide Save to library menu + Hide Save to playlist menu + Hide Share menu + Hide Shuffle play menu + Hide Sleep timer menu + Hide Start radio menu + Hide Stats for nerds menu + Hide Subscribe / Unsubscribe menu + Hide Unpin from Speed dial menu + Hide View song credits menu + "Hides fullscreen ads. + +Limitations: +• Sometimes you may see a blank black screen instead of the home feed." + Hide fullscreen ads + Hides the Share button in the fullscreen player. + Hide fullscreen Share button + Hides general ads. + Hide general ads + Hides the handle in the account menu. + Hide handle + Hides the History button in the toolbar. + Hide History button + Hides the navigation bar. + Hide navigation bar + Hides the Explore button. + Hide Explore button + Hides the Home button. + Hide Home button + Hides the Library button. + Hide Library button + Hides the Samples button. + Hide Samples button + Hides the Upgrade button. + Hide Upgrade button + Hides the Notifications button in the toolbar. + Hide Notifications button + Hides the paid promotion label. + Hide paid promotion label + Hides the playlist card shelf in the feed. + Hide playlist card shelf + Hides premium promotion popups. + Hide premium promotion popups + Hides the premium renewal banner. + Hide premium renewal banner + Hides the promotion alert banner. + Hide promotion alert banner + Hides the Samples shelf in the feed. + Hide Samples shelf + Hide About menu + Hide Data saving menu + Hide Downloads & storage menu + Hide General menu + Hide Notifications menu + Hide Get Music premium menu + Hide Family Center menu + Hide Playback menu + Hide Privacy & data menu + Hide Recommendations menu + "Hide elements of the settings menu. +This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." + Hide settings menu + Hides the sound search button in the search bar. + Hide sound search button + Hides the Tap to update button. + Hide Tap to update button + Hides the Terms of Service container. + Hide terms container + Hides the voice search button in the search bar. + Hide voice search button + Action Bar + Ads + Flyout Menu + General + Miscellaneous + Navigation Bar + Player + Return YouTube Username + Return YouTube Dislike + SponsorBlock + Settings menu + Video + Remembers the last playback speed selected. + Remember playback speed changes + Show a toast when changing the default playback speed. + Show a toast + Changing default speed to %s. + Remembers the state of the repeat toggle. + Remember repeat state + Remembers the state of the shuffle toggle. + Remember shuffle state + Remembers the last video quality selected. + Remember video quality changes + Show a toast when changing the default video quality. + Show a toast + Changing default mobile data quality to %s. + Failed to set quality. + Changing default Wi-Fi quality to %s. + "Removes the viewer discretion dialog. +This does not bypass the age restriction. It just accepts it automatically." + Remove viewer discretion dialog + Continues the video from the current time when switching to YouTube. + Continue watching + Replaces the Dismiss queue menu with the Watch on YouTube menu. + Replace Dismiss queue menu + Watch on YouTube + Invalid video url. + Keeps the Report menu in the comments section intact. + Keep Report in comments + Replaces the Report menu with the Playback speed menu. + Replace Report menu + Returns the comments popup panels to the old style. + Restore old comments popup panels + Returns the player background to the old style. + Restore old player background + "Returns the player layout to the old style. +Some features may not work properly in the old player layout." + Restore old player layout + Returns the Library tab to the old style. (Experimental) + Restore old style library shelf + @handle (Username) + Select the username display format. + Display format + Username (@handle) + Username + Replaces handles with usernames in comments. + Enable Return YouTube Username + "A YouTube Data API v3 Developer Key is required to replace handles with usernames. + +The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. + +Click to see how to issue a API key." + About YouTube Data API key + The developer key for using the YouTube Data API v3. + YouTube Data API key + 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. + Issue YouTube Data API v3 developer key + About + Data is provided by the Return YouTube Dislike API. Tap here to learn more. + ReturnYouTubeDislike.com + Hides the separator of the like button. + Compact like button + Displays the percentage of dislikes instead of the dislike count. + Dislikes as percentage + Shows the dislike count of videos. + Enable Return YouTube Dislike + Shows the estimated like count of videos. + Show estimated likes + Dislikes are unavailable (client API limit reached). + Dislikes are unavailable (status %d). + Dislikes are temporarily unavailable (API timed out). + Dislikes are unavailable (%s). + Shows a toast if the Return YouTube Dislike API is unavailable. + Show a toast if API is unavailable + Hidden + Removes tracking query parameters from URLs when sharing links. + Sanitize sharing links + About + sponsor.ajay.app + Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. + Change API URL + API URL changed. + API URL is invalid. + API URL reset. + The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. + Color changed. + Color: + Invalid color code. + Color reset. + Change segment behavior + Enable SponsorBlock + SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. + Reset color + Filler Tangent / Jokes + Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. + Interaction Reminder (Subscribe) + A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. + Intermission / Intro Animation + An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. + Music: Non-Music Section + Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. + Endcards / Credits + Credits or when the YouTube endcards appear. Not for conclusions with information. + Preview / Recap / Hook + Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. + Unpaid / Self Promotion + Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. + Sponsor + Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. + Skip automatically + Disable + Skipped filler. + Skipped annoying reminder. + Skipped intro. + Skipped intermission. + Skipped intermission. + Skipped multiple segments. + Skipped a non-music section. + Skipped outro. + Skipped preview. + Skipped recap. + Skipped preview. + Skipped self promotion. + Skipped sponsor. + SponsorBlock is temporarily unavailable. + SponsorBlock is temporarily unavailable (status %d). + SponsorBlock is temporarily unavailable (API timed out). + Show a toast if API is unavailable + Shows a toast if the SponsorBlock API is unavailable. + Show a toast when skipping automatically + Shows a toast when a segment is automatically skipped. + Settings copied to clipboard. + "Spoofs the client version to an older version. + +• This will change the appearance of the app, but unknown side effects may occur. +• If later disabled, the old UI may remain until the app data is cleared." + 4.27.53 - Disable Radio mode in Canadian regions + 6.11.52 - Disable real-time lyrics + 7.16.53 - Restore old action bar + Select the spoof app version target. + Spoof app version target + "Spoof the client to prevent playback issues. + +※ When used with 'Spoofing streaming data', playback issues may occur." + Spoof client + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + Android TV + Android VR + iOS + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/cs-rCZ/strings.xml b/patches/src/main/resources/music/translations/cs-rCZ/strings.xml new file mode 100644 index 000000000..ca5d15cdb --- /dev/null +++ b/patches/src/main/resources/music/translations/cs-rCZ/strings.xml @@ -0,0 +1,47 @@ + + + Upravit vlastní filtr + Povolit vlastní filtry + Povolit vlastní filtr + Zakáže vynucené automatické titulky. + Zakázat vynucené automatické titulky + Nastaví barvu navigačního panelu na černou. + Povolit černou navigační lištu + Odpovídá barvě mini přehrávače a režimu celé obrazovky. + Povolit barevně shodný přehrávač + Povolit kompaktní dialogové okno + Vypíše protokol ladění + Povolit režim ladění + Zachovat přehrávač trvale minimalizovaný, i když je přehrávána jiná skladba. + Povolit vynucenou minimalizaci přehrávače + Umožňuje vstup do režimu na šířku pomocí otočení obrazovky telefonu. + Povolit režim na šířku + "Povolit 250/251 opus kodek při přehrávání audia." + Povolit opus kodek + Přidá šedý odstín do přehrávače videa ke snížení namáhání očí. + Povolit zen mod + Obnovit na výchozí hodnoty. + + Obnovit a restartovat + ReVanced Extended + %s není instalován. Prosím, nainstalujte jej. + Název balíčku externí nainstalované aplikace na stahování, jako jsou např. NewPipe nebo Seal + Název balíčku pro externí stahování + Filtr nabídky účtu + Skryje prvky nabídky účtu pomocí vlastního filtru. + Skrýt nabídku účtu + Skryje pole s tlačítky z domovské stránky a prohlížeče. + Skrýt pole s tlačítky + Skryje točivé pole z domovské stránky a prohlížeče. + Skrýt točivé pole + Skryje tlačítko pro vysílání obrazu z horní části domovské obrazovky a horní části přehrávače. + Skrýt tlačítko pro vysílání + Skryje lištu s hudebními kategoriemi z horní části domovské obrazovky. + Skrýt lištu s kategoriemi + Skryje reklamy před přehráváním hudby. + Skrýt hudební reklamy + Skrýt popisky v navigačním panelu. + Skrýt popisky navigačního panelu + + Zfalšovat verzi aplikace + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/el-rGR/missing_strings.xml b/patches/src/main/resources/music/translations/el-rGR/missing_strings.xml new file mode 100644 index 000000000..973422826 --- /dev/null +++ b/patches/src/main/resources/music/translations/el-rGR/missing_strings.xml @@ -0,0 +1,6 @@ + + + Don\'t show again + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/el-rGR/strings.xml b/patches/src/main/resources/music/translations/el-rGR/strings.xml new file mode 100644 index 000000000..460a064fe --- /dev/null +++ b/patches/src/main/resources/music/translations/el-rGR/strings.xml @@ -0,0 +1,430 @@ + + + Συνέχεια + "Το MicroG GmsCore δεν έχει άδεια να τρέχει στο παρασκήνιο. + +Ακολουθήστε τον οδηγό \"Don't kill my app!\" για το τηλέφωνό σας και εφαρμόστε τις οδηγίες στο MicroG. + +Αυτό απαιτείται για να λειτουργήσει η εφαρμογή." + "Οι βελτιστοποιήσεις μπαταρίας πρέπει να απενεργοποιηθούν για το MicroG GmsCore ώστε να αποφευχθούν προβλήματα. + +Η απενεργοποίηση των βελτιστοποιήσεων μπαταρίας για το MicroG δεν θα επηρεάσει αρνητικά την κατανάλωση ενέργειας. + +Πατήστε το κουμπί «Συνέχεια» και επιτρέψτε τις αλλαγές βελτιστοποίησης." + Άνοιγμα ιστοσελίδας + Απαιτείται ενέργεια + Ενεργοποιήστε τις ρυθμίσεις cloud messaging για να λαμβάνετε ειδοποιήσεις. + Άνοιγμα του MicroG GmsCore + Το MicroG GmsCore δεν είναι εγκατεστημένο. Εγκαταστήστε το. + Αντικατάσταση του domain για την φόρτωση εικόνων όπου είναι μπλοκαρισμένες σε ορισμένες περιοχές ώστε να μπορούν να ληφθούν μικρογραφίες βίντεο, εικόνες δημοσιεύσεων, κλπ. + Παράκαμψη μπλοκαρίσματος φόρτωσης εικόνων + Αλλαγή του μενού κοινοποίησης σε αυτό του συστήματος σας αντί του YouTube Music. + Αλλαγή μενού κοινοποίησης + Διαγράμματα + Εξερεύνηση + Αρχική + Βιβλιοθήκη + Εγγραφές + Ορισμός της αρχικής σελίδας ανοίγματος της εφαρμογής. + Αλλαγή αρχικής σελίδας + Λίστα από συμβολοσειρές στοιχείων που θα φιλτραριστούν, διαχωρισμένες με νέες γραμμές. + Επεξεργασία προσαρμοσμένου φίλτρου + Χρήση προσαρμοσμένου φίλτρου για απόκρυψη στοιχείων διεπαφής. + Προσαρμοσμένο φίλτρο + Μη έγκυρο φίλτρο: %s. + Οι ταχύτητες πρέπει να είναι μικρότερες από %sx. + Μη έγκυρες ταχύτητες αναπαραγωγής. + Ρύθμιση των διαθέσιμων ταχυτήτων αναπαραγωγής. + Επεξεργασία ταχυτήτων αναπαραγωγής + Για να ανοίγουν οι συνδέσμοι YouTube Music στο RVX Music, ενεργοποιήστε το «Άνοιγμα υποστηριζόμενων συνδέσμων» και τις υποστηριζόμενες διευθύνσεις ιστού. + Άνοιγμα ρυθμίσεων προεπιλεγμένων εφαρμογών + Απενεργοποίηση της αυτόματης ενεργοποίησης υπότιτλων. + Απενεργοποίηση αυτόματων υπότιτλων + Απενεργοποίηση των εφέ θέματος Cairo κατά την εκκίνηση της εφαρμογής. + Απενεργοποίηση εφέ εκκίνησης θέματος Cairo + Απενεργοποίηση της ανακατεύθυνσης στο επόμενο κομμάτι όταν πατάτε το κουμπί «Δεν μου αρέσει». + Απενεργοποίηση ανακατεύθυνσης dislike + Απενεργοποίηση του DRC (Συμπίεση Δυναμικού Εύρους) που εφαρμόζεται στον ήχο. + Απενεργοποίηση ήχου DRC + Απενεργοποίηση της χειρονομίας σάρωσης για αλλαγή κομματιού στην ελαχιστοποιημένη οθόνη αναπαραγωγής. + Απενεργοποίηση χειρονομίας ελαχιστοποιημένης οθόνης αναπαραγωγής + Απενεργοποίηση της χειρονομίας σάρωσης για αλλαγή κομματιού στην οθόνη αναπαραγωγής. + Απενεργοποίηση χειρονομίας οθόνης αναπαραγωγής + Ορισμός του χρώματος της γραμμής πλοήγησης σε μαύρο. + Μαύρη γραμμής πλοήγησης + Αλλαγή χρώματος της οθόνης αναπαραγωγής σε μαύρο. + Μαύρο φόντο οθόνης αναπαραγωγής + Να ταιριάζει το χρώμα της ελαχιστοποιημένης οθόνης αναπαραγωγής με αυτό της οθόνης αναπαραγωγής πλήρους οθόνης. + Ταίριασμα χρωμάτων οθόνων αναπαραγωγής + "Χρήση μικρότερου στυλ για το αναδυόμενο μενού. + +Περιορισμοί: +• Τα εξώφυλλα άλμπουμ στην καρτέλα βιβλιοθήκης γίνονται μικρότερα επίσης. +• Η διεπαφή του χρονομέτρου ύπνου ενδέχεται να φαίνεται ασυνήθιστη." + Αναδυόμενο μενού μικρότερου στυλ + Η καταγραφή εντοπισμού σφαλμάτων θα περιλαμβάνει το proto buffer. + Συμπερίληψη του buffer στην καταγραφή + Εκτύπωση του αρχείου καταγραφής σφαλμάτων. + Ενεργοποίηση καταγραφής σφαλμάτων + Να διατηρείται μόνιμα ελαχιστοποιημένο το πρόγραμμα αναπαραγωγής ακόμη και όταν αναπαράγεται άλλο κομμάτι. + Εξαναγκαστική ελαχιστοποίηση οθόνης αναπαραγωγής + Ενεργοποίηση της οριζόντιας λειτουργίας με την περιστροφή της οθόνης. + Ενεργοποίηση οριζόντιας λειτουργίας + Ενεργοποίηση του κουμπιού επόμενου βίντεο στην ελαχιστοποιημένη οθόνη αναπαραγωγής. + Κουμπί επόμενου βίντεο στον miniplayer + Ενεργοποίηση του κουμπιού προηγούμενου βίντεο στην ελαχιστοποιημένη οθόνη αναπαραγωγής. + Κουμπί προηγούμενου βίντεο στον miniplayer + "Ενεργοποίηση του κωδικοποιητή OPUS αν η ανταπόκριση του προγράμματος αναπαραγωγής τον περιλαμβάνει. + +Πληροφορία: Οι τελευταίες εκδόσεις Android χρησιμοποιούν τον κωδικοποιητή opus από προεπιλογή, οπότε αυτή η ρύθμιση ισχύει μόνο για χρήστες που χρησιμοποιούν τη λειτουργία παραποίησης έκδοσης εφαρμογής, σε πολύ παλιές εκδόσεις." + Ενεργοποίηση κωδικοποιητή opus + Ενεργοποίηση χειρονομίας σάρωσης προς τα κάτω για απόρριψη της ελαχιστοποιημένης οθόνης αναπαραγωγής. + Χειρονομία απόρριψης ελαχιστοποιημένης οθόνης αναπαραγωγής + "Ενεργοποίηση της λειτουργίας «Περικοπή σίγασης» στο αναδυόμενο μενού αλλαγής ταχύτητας αναπαραγωγής. + +Πληροφορίες: +• Αυτή η λειτουργία είναι για ηχητικές εκπομπές. +• Αυτή η λειτουργία είναι ακόμη υπό ανάπτυξη, οπότε ενδέχεται να είναι ασταθής." + Ενεργοποίηση περικοπής σίγασης + Η λειτουργία Zen εφαρμόζεται σε ηχητικές εκπομπές επίσης. + Λειτουργία zen σε ηχητικές εκπομπές + Προσθήκη μιας γκρι απόχρωσης στο παρασκήνιο της οθόνης αναπαραγωγής για να μειωθεί η καταπόνηση των ματιών. + Ενεργοποίηση λειτουργίας zen + Επαναφέρθηκε στην προεπιλεγμένη τιμή. + Επανεκκίνηση ώστε να φορτωθεί σωστά η διάταξη + Ανανέωση και επανεκκίνηση + Εξαγωγή ρυθμίσεων σε αρχείο + Αποτυχία εξαγωγής ρυθμίσεων. + Οι ρυθμίσεις εξήχθησαν με επιτυχία. + Εισαγωγή + Εισαγωγή ρυθμίσεων από αρχείο + Αντιγραφή + Εισαγωγή / Εξαγωγή ρυθμίσεων ως κείμενο + Εισαγωγή ή εξαγωγή των ρυθμίσεών σας. + Εισαγωγή / Εξαγωγή + Η εισαγωγή απέτυχε: %s. + Οι ρυθμίσεις επαναφέρθηκαν στις προεπιλογές. + Έγινε εισαγωγή %d ρυθμίσεων. + Επαναφορά + ReVanced Extended + "Το κουμπί «Λήψη» ανοίγει το εξωτερικό πρόγραμμα λήψης σας. + +• Η μετατροπή αφορά μόνο το κουμπί ενέργειας στην οθόνη αναπαραγωγής. +• Δεν αφορά το κουμπί «Λήψη» στο αναδυόμενο μενού ή στη βιβλιοθήκη." + Μετατροπή κουμπιού ενέργειας «Λήψη» + Εξωτερικό πρόγραμμα λήψης + "Το %1$s δεν είναι εγκατεστημένο. +Παρακαλούμε εγκαταστήστε το %2$s από την ιστοσελίδα." + Προειδοποίηση + %s δεν έχει εγκατασταθεί. Παρακαλούμε εγκαταστήστε το. + Όνομα πακέτου της εγκατεστημένης εξωτερικής εφαρμογής λήψης (π.χ NewPipe, Seal). + Όνομα πακέτου εξωτερικού προγράμματος λήψης + Απόκρυψη κενών στοιχείων στο μενού λογαριασμού. + Απόκρυψη κενών στοιχείων + Λίστα ονομάτων των επιλογών του μενού λογαριασμού για φιλτράρισμα, διαχωρισμένα με νέες γραμμές. + Επεξεργασία φίλτρου μενού λογαριασμού + Απόκρυψη στοιχείων του μενού λογαριασμού χρησιμοποιώντας προσαρμοσμένο φίλτρο. + Φιλτράρισμα του μενού λογαριασμού + Απόκρυψη του κουμπιού αποθήκευσης σε λίστα αναπαραγωγής. + Απόκρυψη κουμπιού «Αποθήκευση» + Απόκρυψη του κουμπιού σχολίων. + Απόκρυψη κουμπιού σχολίων + Απόκρυψη του κουμπιού λήψης. + Απόκρυψη κουμπιού «Λήψη» + Απόκρυψη των ονομασιών των κουμπιών ενεργειών. + Απόκρυψη ονομασιών κουμπιών ενέργειας + Απόκρυψη των κουμπιών «Μου αρέσει» και «Δεν μου αρέσει». Δεν λειτουργεί στην παλιά εμφάνιση της οθόνης αναπαραγωγής. + Απόκρυψη κουμπιών «Μου αρέσει» και «Δεν μου αρέσει» + Απόκρυψη του κουμπιού έναρξης ραδιοφώνου. + Απόκρυψη κουμπιού «Ραδιόφωνο» + Απόκρυψη του κουμπιού κοινοποίησης. + Απόκρυψη κουμπιού «Κοινοποίηση» + Απόκρυψη της εναλλαγής ήχου-βίντεο στην οθόνη αναπαραγωγής. + Απόκρυψη εναλλαγής ήχου-βίντεο + Απόκρυψη της ενότητας κουμπιών στη ροή. + Απόκρυψη ενότητας κουμπιών + Απόκρυψη ενότητας καρουζέλ στη ροή. + Απόκρυψη ενότητας καρουζέλ + Απόκρυψη του κουμπιού μετάδοσης. + Απόκρυψη κουμπιού μετάδοσης + Απόκρυψη της γραμμής κατηγοριών. + Απόκρυψη γραμμής κατηγοριών + Απόκρυψη των οδηγιών κοινότητας στην κορυφή της ενότητας σχολίων. + Απόκρυψη οδηγιών κοινότητας + Απόκρυψη των κουμπιών χρονοσήμανσης και επιλογής emoji κατά την πληκτρολόγηση σχολίου. + Απόκρυψη κουμπιών χρονοσήμανσης & emoji + Απόκρυψη του σκοτεινού φόντου που εμφανίζεται στην οθόνη αναπαραγωγής όταν γίνεται διπλό πάτημα για αναζήτηση. + Απόκρυψη φόντου διπλού πατήματος της οθόνης αναπαραγωγής + Απόκρυψη του αιωρούμενου κουμπιού στην καρτέλα βιβλιοθήκης. + Απόκρυψη αιωρούμενου κουμπιού + Απόκρυψη στοιχείου 3 στηλών + Απόκρυψη μενού «Προσθήκη στην ουρά» + Απόκρυψη μενού «Υπότιτλοι» + Απόκρυψη μενού «Διαγραφή λίστας αναπαραγωγής» + Απόκρυψη μενού «Παράβλεψη ουράς» + Απόκρυψη μενού «Λήψη» + Απόκρυψη μενού «Επεξεργασία λίστας αναπαραγωγής» + Απόκρυψη μενού «Μετάβαση στο άλμπουμ» + Απόκρυψη μενού «Μετάβαση στον καλλιτέχνη» + Απόκρυψη μενού «Μετάβαση στο επεισόδιο» + Απόκρυψη μενού «Μετάβαση στο podcast» + Απόκρυψη μενού «Βοήθεια & σχόλια» + Απόκρυψη κουμπιών «Μου αρέσει» και «Δεν μου αρέσει» + Απόκρυψη μενού «Καρφίτσωμα στην ταχεία κλήση» + Απόκρυψη μενού «Αναπαραγωγή μετά» + Απόκρυψη μενού «Ποιότητα» + Απόκρυψη μενού «Κατάργηση λίστας αναπαραγωγής από τη βιβλιοθήκη» + Απόκρυψη μενού «Κατάργηση από λίστα αναπαραγωγής» + Απόκρυψη μενού «Αναφορά» + Απόκρυψη μενού «Αποθήκευση επεισοδίου για αργότερα» + Απόκρυψη μενού «Αποθήκευση λίστας αναπαραγωγής στη Βιβλιοθήκη» + Απόκρυψη μενού «Αποθήκευση στη λίστα αναπαραγωγής» + Απόκρυψη μενού «Κοινοποίηση» + Απόκρυψη μενού «Τυχαία αναπαραγωγή» + Απόκρυψη μενού «Χρονόμετρο ύπνου» + Απόκρυψη μενού «Έναρξη ραδιοφώνου» + Απόκρυψη μενού «Στατιστικά για σπασίκλες» + Απόκρυψη των «Εγγραφή» / «Απεγγραφή» + Απόκρυψη μενού «Ξεκαρφίτσωμα από την ταχεία κλήση» + Απόκρυψη μενού «Προβολή συντελεστών τραγουδιού» + "Απόκρυψη των ενδιάμεσων διαφημίσεων πλήρους οθόνης. + +Περιορισμοί: +• Ενδέχεται μερικές φορές να φαίνεται μια κενή μαύρη οθόνη αντί για την αρχική ροή." + Απόκρυψη διαφημίσεων πλήρους οθόνης + Απόκρυψη του κουμπιού κοινοποίησης στην οθόνη αναπαραγωγής πλήρους οθόνης. + Απόκρυψη κουμπιού κοινοποίησης στη λειτουργία πλήρους οθόνης + Απόκρυψη των γενικών διαφημίσεων. + Απόκρυψη γενικών διαφημίσεων + Απόκρυψη του ψευδώνυμου στην εναλλαγή λογαριασμού. + Απόκρυψη ψευδώνυμου + Απόκρυψη του κουμπιού ιστορικού στη γραμμή εργαλείων. + Απόκρυψη κουμπιού ιστορικού + Απόκρυψη διαφημίσεων πριν την αναπαραγωγή κομματιού. + Απόκρυψη διαφημίσεων μουσικής + Απόκρυψη της γραμμής πλοήγησης. + Απόκρυψη γραμμής πλοήγησης + Απόκρυψη της καρτέλας «Εξερεύνηση». + Απόκρυψη κουμπιού «Εξερεύνηση» + Απόκρυψη της καρτέλας «Αρχική». + Απόκρυψη κουμπιού «Αρχική» + Απόκρυψη ονομασιών των κουμπιών στη γραμμή πλοήγησης. + Απόκρυψη ονομασιών γραμμής πλοήγησης + Απόκρυψη της καρτέλας «Βιβλιοθήκη». + Απόκρυψη κουμπιού «Βιβλιοθήκη» + Απόκρυψη της καρτέλας «Δείγματα». + Απόκρυψη κουμπιού «Δείγματα» + Απόκρυψη της καρτέλας «Αναβάθμιση». + Απόκρυψη κουμπιού «Αναβάθμιση» + Απόκρυψη του κουμπιού ειδοποιήσεων στη γραμμή εργαλείων. + Απόκρυψη κουμπιού ειδοποιήσεων + Απόκρυψη της ετικέτας προώθησης επί πληρωμή. + Απόκρυψη ετικέτας προώθησης επί πληρωμή + Απόκρυψη της ενότητας καρτών λίστας αναπαραγωγής στη ροή. + Απόκρυψη καρτών λίστας αναπαραγωγής + Απόκρυψη των αναδυόμενων παραθύρων προώθησης Premium. + Απόκρυψη παραθύρων προώθησης Premium + Απόκρυψη του διαφημιστικού ανανέωσης YT Premium. + Απόκρυψη διαφημιστικού ανανέωσης Premium + Απόκρυψη των ετικετών προειδοποίησης προώθησης. + Απόκρυψη ετικετών προειδοποίησης προώθησης + Απόκρυψη της ενότητας «Δείγματα» στη ροή. + Απόκρυψη ενότητας «Δείγματα» + Απόκρυψη μενού «Πληροφορίες για το YouTube Music» + Απόκρυψη μενού «Εξοικονόμηση δεδομένων» + Απόκρυψη μενού «Λήψεις & αποθηκευτικός χώρος» + Απόκρυψη μενού «Γενικά» + Απόκρυψη μενού «Ειδοποιήσεις» + Απόκρυψη μενού «Αποκτήστε το Music Premium» + Απόκρυψη μενού «Κέντρο οικογένειας» + Απόκρυψη μενού «Αναπαραγωγή» + Απόκρυψη μενού «Απόρρητο & δεδομένα» + Απόκρυψη μενού «Προτάσεις» + "Απόκρυψη στοιχείων του μενού ρυθμίσεων YouTube. +Αυτή η λειτουργία γίνεται να κρύψει και το μενού ρυθμίσεων ReVanced Extended." + Φιλτράρισμα του μενού ρυθμίσεων + Απόκρυψη του κουμπιού ηχητικής αναζήτησης στην γραμμή αναζήτησης. + Απόκρυψη κουμπιού ηχητικής αναζήτησης + Απόκρυψη του κουμπιού «Πατήστε για ενημέρωση». + Απόκρυψη κουμπιού «Πατήστε για ενημέρωση» + Απόκρυψη των στοιχείων απορρήτου / όρων και προϋποθέσεων. + Απόκρυψη στοιχείων απορρήτου & όρων + Απόκρυψη του κουμπιού φωνητικής αναζήτησης στην γραμμή αναζήτησης. + Απόκρυψη κουμπιού φωνητικής αναζήτησης + Λογαριασμός + Γραμμή ενεργειών + Διαφημίσεις + Αναδυόμενο μενού ρυθμίσεων + Γενικά + Διάφορα + Γραμμή πλοήγησης + Οθόνη αναπαραγωγής + Return YouTube Username + Return YouTube Dislike + SponsorBlock + Μενού ρυθμίσεων + Βίντεο + Απομνημόνευση της τελευταίας ταχύτητας αναπαραγωγής που επιλέχθηκε. + Απομνημόνευση αλλαγών ταχύτητας αναπαραγωγής + Εμφάνιση μηνύματος στο κάτω μέρος της οθόνης κατά την αλλαγή προεπιλεγμένης ταχύτητας αναπαραγωγής. + Εμφάνιση μηνύματος + Η προεπιλεγμένη ταχύτητα άλλαξε σε %s. + Απομνημόνευση της κατάστασης του κουμπιού επανάληψης. + Απομνημόνευση κατάστασης επανάληψης + Απομνημόνευση της κατάστασης του ανακατέματος τραγουδιών. + Απομνημόνευση κατάστασης ανακατέματος + Απομνημόνευση της τελευταίας ποιότητας βίντεο που επιλέχθηκε. + Απομνημόνευση αλλαγών ποιότητας βίντεο + Εμφάνιση μηνύματος στο κάτω μέρος της οθόνης κατά την αλλαγή προεπιλεγμένης ποιότητας βίντεο. + Εμφάνιση μηνύματος + Η προεπιλεγμένη ποιότητα δεδομένων άλλαξε σε %s. + Αποτυχία ρύθμισης της ποιότητας. + Η προεπιλεγμένη ποιότητα με Wi-Fi άλλαξε σε %s. + "Αφαίρεση του παραθύρου προειδοποίησης ηλικιακού περιορισμού. +Αυτό δεν παρακάμπτει τον ηλικιακό περιορισμό, απλά τον αποδέχεται αυτόματα." + Αφαίρεση παραθύρου ηλικιακού περιορισμού + Αν πατήσετε το «Παρακολούθηση στο YouTube», η αναπαραγωγή θα συνεχιστεί από την τρέχουσα ώρα προβολής. + Συνέχιση παρακολούθησης + Αντικατάσταση του μενού «Παράβλεψη ουράς» με το μενού «Παρακολούθηση στο YouTube». + Αντικατάσταση μενού «Παράβλεψη ουράς» + Παρακολούθηση στο YouTube + Μη έγκυρη διεύθυνση URL βίντεο. + Το μενού αναφοράς στα σχόλια δε θα επηρεαστεί. + Διατήρηση του μενού «Αναφορά» στα σχόλια + Αντικατάσταση του μενού «Αναφορά» με το μενού «Ταχύτητα αναπαραγωγής». + Αντικατάσταση μενού «Αναφορά» + Επιστροφή του πίνακα αναδυόμενων σχολίων στο παλιό του στυλ. + Αναδυόμενα σχόλια παλιού στυλ + Επιστροφή του φόντου της οθόνης αναπαραγωγής στο παλιό στυλ. + Φόντο οθόνης αναπαραγωγής παλιού στυλ + "Επιστροφή της εμφάνισης της οθόνης αναπαραγωγής στο παλιό στυλ. +Κάποιες λειτουργίες ενδέχεται να μη λειτουργούν σωστά στην παλιά εμφάνιση της οθόνης αναπαραγωγής." + Οθόνη αναπαραγωγής παλιού στυλ + Επιστροφή της ενότητας βιβλιοθήκης στο παλιό στυλ. (Πειραματικό) + Ενότητα βιβλιοθήκης παλιού στυλ + \@ψευδώνυμο (Όνομα χρήστη) + Επιλογή της μορφής εμφάνισης ονόματος χρήστη. + Μορφή εμφάνισης + Όνομα χρήστη (@ψευδώνυμο) + Όνομα χρήστη + Εμφάνιση του ονόματος χρήστη αντί για το ψευδώνυμο στα σχόλια. + Επαναφορά ονομάτων χρήστη + "Για να γίνει αντικατάσταση του ψευδωνύμου με όνομα χρήστη, απαιτείται κλειδί προγραμματιστή YouTube Data API v3. + +Η ημερήσια ποσόστωση για τα κλειδιά API στο δωρεάν πακέτο είναι 10,000, και χρησιμοποιείται 1 ποσόστωση για την αντικατάσταση ψευδωνύμου με όνομα χρήστη για 1 σχόλιο. + +Πατήστε για να δείτε πώς να εκδώσετε ένα κλειδί API." + Σχετικά με το κλειδί YouTube Data API + Το κλειδί προγραμματιστή για τη χρήση του YouTube Data API v3. + Κλειδί YouTube Data API + 1. Μεταβείτε στη <a href=%1$s>δημιουργία νέου project</a>.<br>2. Πατήστε το κουμπί <b>CREATE</b>. <br>3. Μεταβείτε στην επιλογή <a href=%2$s>YouTube Data API v3</a>.<br>4. Πατήστε το κουμπί <b>ENABLE</b>.<br>5. Πατήστε το κουμπί <b>CREATE CREDENTIALS</b>.<br>6. Επιλέξτε την επιλογή <b>Public data</b>.<br>7. Πατήστε το κουμπί <b>NEXT</b>.<br>8. Αντιγράψτε το κλειδί API.<br><br>※ Το κλειδί API δεν πρέπει να το μοιράζεστε ποτέ με άλλους, οπότε δεν περιλαμβάνεται κατά την Εισαγωγή / Εξαγωγή ρυθμίσεων. + Έκδοση κλειδιού προγραμματιστή YouTube Data API v3 + Σχετικά με + Τα δεδομένα Dislike παρέχονται από το Return YouTube Dislike API. Πατήστε για να μάθετε περισσότερα. + ReturnYouTubeDislike.com + Απόκρυψη του διαχωριστικού του κουμπιού «Μου αρέσει». + Κουμπί «Μου αρέσει» μικρότερου στυλ + Αντί για τον αριθμό των dislike, θα εμφανίζεται το ποσοστό τους. + Εμφάνιση ως ποσοστό + Εμφάνιση της ποσότητας των «Δεν μου αρέσει» των βίντεο. + Επιστρέψτε το «Δεν μου αρέσει» στο YouTube + Εμφάνιση της εκτιμώμενης ποσότητας των «Μου αρέσει» των βίντεο. + Εμφάνιση εκτιμώμενων likes + Δεδομένα dislike μη διαθέσιμα (το όριο API έχει επιτευχθεί). + Δεδομένα dislike μη διαθέσιμα (κατάσταση %d). + Δεδομένα dislike προσωρινά μη διαθέσιμα (καθυστέρηση API). + Δεδομένα dislike μη διαθέσιμα (%s). + Να εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης αν το Return YouTube Dislike API δεν είναι διαθέσιμο. + Μήνυμα αν το API δεν είναι διαθέσιμο + Κρυμμένο + Αφαίρεση των παραμέτρων παρακολούθησης από τις διευθύνσεις URL κατά την κοινοποίηση συνδέσμων. + Καθαρισμός συνδέσμων κοινοποίησης + Σχετικά με + sponsor.ajay.app + Τα δεδομένα παρέχονται από το SponsorBlock API. Πατήστε για να μάθετε περισσότερα και να δείτε λήψεις για άλλες πλατφόρμες. + Αλλαγή διεύθυνσης API + Η διεύθυνση URL του API άλλαξε. + Η διεύθυνση URL του API δεν είναι έγκυρη. + Η διεύθυνση URL του API επαναφέρθηκε. + Η διεύθυνση που χρησιμοποιείται για επικοινωνία με τον διακομιστή του SponsorBlock. Μη το αλλάξετε αν δεν ξέρετε τι κάνετε. + Το χρώμα άλλαξε. + Χρώμα: + Μη έγκυρος κωδικός χρώματος. + Το χρώμα επαναφέρθηκε. + Αλλαγή συμπεριφοράς τμημάτων + Ενεργοποίηση του SponsorBlock + Το SponsorBlock είναι ένα σύστημα που προέρχεται από το κοινό για παράβλεψη ενοχλητικών τμημάτων σε βίντεο YouTube. + Επαναφορά χρώματος + Εφαπτομενικές Σκηνές / Αστεία + Παρεμβατικές σκηνές που προστίθενται μόνο για γέμισμα ή χιούμορ και δεν είναι απαραίτητες για την κατανόηση του κύριου περιεχομένου του βίντεο. Δεν περιλαμβάνει τμήματα που παρέχουν πλαίσιο ή λεπτομέρειες υποβάθρου. + Υπενθύμιση αλληλεπίδρασης (Εγγραφή) + Όταν υπάρχει μια σύντομη υπενθύμιση για να προσθέσετε το βίντεο στα βίντεο που σας αρέσουν, να εγγραφείτε ή να τους ακολουθήσετε στην μέση του περιεχομένου. Αν είναι μεγάλο ή αφορά κάτι συγκεκριμένο, θα πρέπει να είναι στην κατηγορία αυτοπροώθησης. + Διάλειμμα / Εισαγωγή + Χρονικό διάστημα χωρίς πραγματικό περιεχόμενο. Θα μπορούσε να είναι μια παύση, ένα στατικό καρέ ή μια επαναλαμβανόμενη κίνηση. Δεν περιλαμβάνει μεταβάσεις που περιέχουν πληροφορίες. + Μουσική: Τμήμα χωρίς μουσική + Μόνο για χρήση σε βίντεο μουσικής. Τμήματα χωρίς μουσική σε βίντεο μουσικής, που δεν καλύπτονται ήδη από άλλη κατηγορία. + Τελική Οθόνη / Συντελεστές + Όταν εμφανίζονται οι συντελεστές ή τα προτεινόμενα βίντεο των καναλιών. Όχι για επίλογους που περιέχουν πληροφορίες. + Προεπισκόπηση / Περίληψη + Συλλογή από κλιπ που δείχνουν τι έρχεται ή τι συνέβη στο βίντεο ή σε άλλα βίντεο μιας σειράς, όπου όλες οι πληροφορίες επαναλαμβάνονται αλλού. + Αφιλοκέρδεια / Αυτοπροώθηση + Παρόμοιο με το «Χορηγός» αλλά για μη κερδοσκοπικό σκοπό ή για προσωπική προώθηση. Περιλαμβάνει τμήματα σχετικά με εμπορεύματα, δωρεές ή πληροφορίες για το με ποιους συνεργάστηκαν. + Χορηγός + Προώθηση επί πληρωμή, παραπομπές επί πληρωμή και άμεσες διαφημίσεις. Όχι για αυτοπροώθηση ή δωρεάν αναφορές σε σκοπούς / δημιουργούς / ιστοσελίδες / προϊόντα που τους αρέσουν. + Αυτόματη παράλειψη + Απενεργοποίηση + Παραλείφθηκε η σπατάλη χρόνου. + Παραλείφθηκε η ενοχλητική υπενθύμιση. + Παραλείφθηκε η εισαγωγή. + Παραλείφθηκε η διακοπή. + Παραλείφθηκε η διακοπή. + Παραλείφθηκαν πολλαπλά τμήματα. + Παραλείφθηκε τμήμα χωρίς μουσική. + Παραλείφθηκε ο επίλογος. + Παραλείφθηκε η προεπισκόπηση. + Παραλείφθηκε η ανακεφαλαίωση. + Παραλείφθηκε η προεπισκόπηση. + Παραλείφθηκε η αυτοπροώθηση. + Παραλείφθηκε ο χορηγός. + SponsorBlock προσωρινά μη διαθέσιμο. + SponsorBlock προσωρινά μη διαθέσιμο (κατάσταση %d). + SponsorBlock προσωρινά μη διαθέσιμο (καθυστέρηση API). + Μήνυμα αν το API δεν είναι διαθέσιμο + Να εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης αν το SponsorBlock API δεν είναι διαθέσιμο. + Εμφάνιση μηνύματος κατά την αυτόματη παράλειψη + Να εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης όταν ένα τμήμα παραλείπεται αυτόματα. + Οι ρυθμίσεις αντιγράφηκαν στο πρόχειρο. + "Παραποίηση έκδοσης εφαρμογής σε παλιότερη έκδοση. + +• Αυτό θα αλλάξει την εμφάνιση της εφαρμογής, αλλά πιθανότατα να προκύψουν άγνωστες παρενέργειες. +• Αν αργότερα γίνει απενεργοποίηση, η παλιά εμφάνιση μπορεί να παραμείνει μέχρι να διαγραφούν τα δεδομένα της εφαρμογής." + 4.27.53 - Απενεργοποίηση λειτουργίας ραδιοφώνου σε περιοχές του Καναδά + 6.11.52 - Απενεργοποίηση στίχων σε πραγματικό χρόνο + 7.16.53 - Επαναφορά παλιάς γραμμής ενεργειών + Επιλέξτε την έκδοση εφαρμογής που θα χρησιμοποιηθεί. + Έκδοση της εφαρμογής που θα χρησιμοποιηθεί + Παραποίηση έκδοσης εφαρμογής + "Παραποίηση του προγράμματος πελάτη για την αποφυγή προβλημάτων αναπαραγωγής. + +※ Αν ενεργοποιηθεί παράλληλα με τη λειτουργία «Παραποίηση δεδομένων ροής», ενδέχεται να εμφανιστούν προβλήματα αναπαραγωγής." + Παραποίηση προγράμματος πελάτη + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Καθορισμός ενός προεπιλεγμένου πρόγραμμα πελάτη για παραποίηση. + +※ Όταν ορίζεται πρόγραμμα πελάτη τύπου Android, συνιστάται να το χρησιμοποιήσετε μαζί με την λειτουργία «Παραποίηση έκδοσης εφαρμογής»." + Προεπιλεγμένο πρόγραμμα πελάτη + Εμφάνιση του προγράμματος πελάτη που χρησιμοποιείται για τη λήψη δεδομένων ροής στο μενού «Στατιστικά για σπασίκλες». + Εμφάνιση στο «Στατιστικά για σπασίκλες» + "Παραποίηση των δεδομένων ροής για την αποφυγή προβλημάτων αναπαραγωγής. + +※ Αν ενεργοποιηθεί παράλληλα με τη λειτουργία «Παραποίηση προγράμματος πελάτη», ενδέχεται να εμφανιστούν προβλήματα αναπαραγωγής." + Παραποίηση δεδομένων ροής + Android TV + Android VR + iOS + iOS Music + Καθορισμός ενός προεπιλεγμένου προγράμματος-πελάτη για την λήψη δεδομένων ροής. + Προεπιλεγμένο πρόγραμμα πελάτη + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/es-rES/missing_strings.xml b/patches/src/main/resources/music/translations/es-rES/missing_strings.xml new file mode 100644 index 000000000..6a2f801d8 --- /dev/null +++ b/patches/src/main/resources/music/translations/es-rES/missing_strings.xml @@ -0,0 +1,29 @@ + + + Don\'t show again + Disables DRC (Dynamic Range Compression) applied to audio. + Disable DRC audio + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + Hide Pin to Speed dial menu + Hide Unpin from Speed dial menu + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + Android TV + Android VR + iOS + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/es-rES/strings.xml b/patches/src/main/resources/music/translations/es-rES/strings.xml new file mode 100644 index 000000000..86a91fa37 --- /dev/null +++ b/patches/src/main/resources/music/translations/es-rES/strings.xml @@ -0,0 +1,403 @@ + + + Continuar + "GmsCore no tiene permiso para ejecutarse en segundo plano. + +Sigue la guía \"Don't kill my app!\" para tu dispositivo y aplica las instrucciones a tu instalación de GmsCore. + +Esto es necesario para que la aplicación funcione." + "Las optimizaciones de la batería para GmsCore deben estar desactivadas para evitar problemas. + +Pulsa el botón de continuar y desactiva las optimizaciones de la batería." + Abrir página Web + Acción necesaria + Activa los ajustes de mensajería en la nube para recibir notificaciones. + Abrir GmsCore + GmsCore no está instalado. Instálalo. + Reemplaza el dominio que está bloqueado en algunas regiones para que las miniaturas de la lista de reproducción, avatares de canales, etc. puedan ser recibidas. + Eludir las restricciones regionales de imágenes + Cambia la hoja de compartir en la app a la hoja de compartir del sistema. + Cambiar la hoja de compartir + Ranking + Explorar + Inicio + Biblioteca + Suscripciones + Seleccione en qué página se abre la aplicación. + Cambiar página de inicio + Filtra los nombres de los componentes, separados por líneas. + Editar filtro personalizado + Habilita el filtro personalizado para ocultar los componentes de diseño. + Activar filtro personalizado + Filtro personalizado no válido: %s. + Las velocidades de reproducción personalizadas no son válidas. Restablezca a los valores predeterminados. + Velocidades de reproducción personalizadas no válidas. Utilizando valores predeterminados. + Agregar o cambiar las velocidades de reproducción disponibles. + Editar velocidades de reproducción personalizadas + Para abrir los enlaces de YouTube Music en RVX Music, activa \'Abrir enlaces soportados\' y activa las direcciones web soportadas. + Abrir ajustes predeterminados de la app + Desactiva la activación automática de los subtítulos forzados en el reproductor de vídeo. + Desactivar subtítulos automáticos + Deshabilita la animación de bienvenida \"Cairo\" cuando se inicia la aplicación. + Desactiva la animación Cairo + Deshabilita la redirección a la siguiente pista al hacer clic en el botón No me Gusta. + Desactivar redirección de No me Gusta + Desactivar el gesto de deslizar para cambiar de pista en el minireproductor. + Desactivar gesto de minireproductor + Desactivar el gesto de deslizar para cambiar de pista en el reproductor. + Desactivar gesto del reproductor + Establece el color de la barra de navegación en negro. + Activar barra de navegación negra + Cambia el color de fondo del reproductor a negro. + Activar fondo de reproductor negro + Hace coincidir el color del reproductor a pantalla completa con el de minimizado. + Activar coincidencia de color de reproductores + "Activa el diálogo compacto en el teléfono. + +Problemas conocidos: +- La carátula del álbum en la estantería de la biblioteca también se hace más pequeña. +- El diseño del temporizador puede parecer inusual." + Activar diálogo compacto + Incluye el búfer en el registro de depuración. + Incluir búfer en registro de depuración + Imprime el registro de depuración. + Activar registro de depuración + Mantiene el reproductor permanentemente minimizado incluso si se reproduce otra pista. + Activar reproductor minimizado forzado + Permite entrar en modo horizontal mediante la rotación de la pantalla del teléfono. + Activar modo horizontal + Añadir botón siguiente pista al minireproductor. + Añadir botón siguiente al minireproductor + Añadir botón pista anterior al minireproductor. + Añadir botón anterior al minireproductor + "Activa el códec Opus 250/251 al reproducir audio." + Activar códec opus + Permite deslizar hacia abajo para descartar el minireproductor. + Activar deslizar para descartar el minireproductor + "Añade un interruptor para recortar silencios en el menú desplegable de velocidad de reproducción. + +Información: +Esta función es para podcasts. +Esta función aún está en desarrollo, por lo que puede ser inestable." + Añadir interruptor para recortar silencios + También activa el modo Zen para podcasts. + Activar el modo Zen en podcasts + Añade un tinte gris al reproductor de vídeo para reducir la fatiga visual. + Activar modo zen + Restablecer a valores por defecto. + Reiniciar para cargar el diseño normalmente + Actualizar y reiniciar + Exportar ajustes a archivo + Error al exportar los ajustes. + Los ajustes se han exportado correctamente. + Importar + Importar ajustes desde archivo + Copiar + Importar o exportar ajustes como texto + Importar o exportar ajustes como texto. + Importar / Exportar + Error de importación: %s + La configuración se restableció a los valores predeterminados. + Configuración importada de %d. + Restablecer + ReVanced Extended + "El botón Descargar abre su descargador externo. + + • Solo anula el botón de acción Descargar en el reproductor. + • No anula el botón Descargar en el menú desplegable o en la pestaña Biblioteca." + Reemplazar botón de acción de Descarga + Descargador externo + "%1$s no está instalado. +Descarga %2$s desde el sitio web." + Advertencia + %s no está instalado. Por favor, instálelo. + Nombre del paquete de su aplicación de descargas externas instalada, como NewPipe o YTDLnis. + Nombre del paquete del descargador externo + Oculta componentes vacíos en el menú de la cuenta. + Ocultar componente vacío + Lista de nombres del menú de la cuenta a filtrar separados por una nueva línea. + Filtro de menú de cuenta + Oculta los elementos del menú de la cuenta usando el filtro personalizado. + Ocultar menú de cuenta + Oculta botón Guardar. + Ocultar botón Guardar + Oculta botón de comentarios. + Ocultar botón de comentarios + Oculta el botón Descargar. + Ocultar botón Descargar + Oculta las etiquetas de los botones de acción. + Ocultar etiquetas de botón de acción + Oculta los botones \"Me gusta\" y \"no me gusta\". No funciona en el diseño del reproductor antiguo. + Ocultar botones Me gusta y No me gusta + Oculta el botón Radio. + Ocultar botón de emisoras de radio + Oculta el botón Compartir. + Ocultar botón de compartir + Oculta el interruptor de Audio / Video en el reproductor. + Ocultar Interruptor de Audio / Video + Oculta el estante de botones de la página de inicio y del explorador. + Ocultar estante de botones + Oculta el estante de carrusel de la página de inicio y del explorador. + Ocultar estante de carrusel + Oculta el botón de trasmisión en la parte superior de la página de inicio y en la parte superior del reproductor. + Ocultar botón de transmisión + Oculta la barra de categorías musicales de la parte superior de la página de inicio. + Ocultar barra de categorías + Oculta las normas del canal en la parte superior de la sección de comentarios. + Ocultar normas del canal + Oculta los botones marca de tiempo y emoji al escribir comentarios. + Ocultar botones de marca de tiempo y emoji + Oculta la superposición oscura que aparece al tocar dos veces para buscar. + Oculta la capa que aparece al tocar dos veces + Oculta el botón flotante en la pestaña Biblioteca. + Ocultar botón flotante + Ocultar componente de 3 columnas + Ocultar menú de Añadir a la cola + Ocultar menú de Subtítulos + Ocultar menú Borrar lista de reproducción + Ocultar menú de Descartar cola + Ocultar menú de Descarga + Ocultar menú Editar lista de reproducción + Ocultar menú de ir al álbum + Ocultar menú de ir al artista + Ocultar menú de ir a episodios + Ocultar menú de ir al podcast + Ocultar menú Ayuda & Comentarios + Ocultar botones Me gusta y No me gusta + Ocultar menú de reproducción siguiente + Ocultar menú de calidad + Ocultar menú de eliminar de la biblioteca + Ocultar menú de quitar de la lista de reproducción + Ocultar menú Denunciar + Ocultar menú de Guardar episodio para más tarde + Ocultar menú de Guardar en biblioteca + Ocultar menú de Guardar en lista de reproducción + Ocultar menú de Compartir + Ocultar menú de Reproducción aleatoria + Ocultar menú de Temporizador de sueño + Ocultar menú de Iniciar radio + Ocultar menú Estadísticas para Nerds + Ocultar menú Suscribirse / Desuscribirse + Ocultar menú de vista de créditos de canción + "Oculta anuncios en pantalla completa." + Ocultar anuncios en pantalla completa + Oculta el botón Compartir en el reproductor de pantalla completa. + Ocultar el botón Compartir en pantalla completa + Oculta anuncios generales. + Ocultar anuncios generales + Oculta el asa en el conmutador de cuenta. + Ocultar asa + Oculta el botón de historial en la barra de herramientas. + Ocultar botón de historial + Oculta los anuncios antes de reproducir una pista. + Ocultar anuncios de música + Oculta barra de navegación. + Ocultar barra de navegación + Oculta el botón Explorar. + Ocultar botón de Explorar + Oculta el botón de Inicio. + Ocultar botón de Inicio + Oculta las etiquetas en la barra de navegación. + Ocultar etiquetas en barra de navegación + Oculta el botón de la biblioteca. + Ocultar botón de Biblioteca + Oculta el botón de Samples. + Ocultar botón de Samples + Oculta el botón de Actualización. + Ocultar botón de Actualización + Oculta el botón de notificaciones en la barra de herramientas. + Ocultar botón de Notificaciones + Oculta etiqueta de promoción pagada. + Ocultar etiqueta de promoción pagada + Oculta la tarjeta de lista de reproducción del feed. + Ocultar tarjeta de lista de reproducción + Oculta popups de promoción premium. + Ocultar popups de promoción premium + Oculta banner de renovación premium. + Ocultar banner de renovación premium + Oculta el banner de alerta de promoción. + Ocultar banner de alerta de promoción + Oculta estante de Samples en el feed. + Ocultar estante de Samples + Ocultar menú Acerca de + Ocultar menú de ahorro de datos + Ocultar Descargas & menú de almacenamiento + Ocultar menú general + Ocultar menú de notificaciones + Ocultar menú premium de Get Music + Ocultar menú de Centro Familiar + Ocultar menú de reproducción + Ocultar menú privacidad de & datos + Ocultar menú de recomendaciones + "Oculta elementos del menú de configuración. +Esto oculta no solo el menú de ajustes de YT Music, sino también el menú de ajustes de ReVanced Extended." + Ocultar menú de configuración + Oculta el botón de búsqueda de sonido en la barra de búsqueda. + Ocultar botón de búsqueda de sonido + Oculta el botón Toque para actualizar. + Ocultar el botón Toque para actualizar + Oculta los términos del contenedor de servicio. + Ocultar contenedor de términos + Oculta el botón de búsqueda por voz en la barra de búsqueda. + Ocultar botón de búsqueda por voz + Cuenta + Barra de Acción + Anuncios + Menú desplegable + General + Otros + Barra de navegación + Reproductor + Devolver usuario de YouTube + Return YouTube Dislike + SponsorBlock + Menú de ajustes + Video + Recuerda la última velocidad de reproducción seleccionada. + Recordar cambios de velocidad de reproducción + Mostrar un mensaje al cambiar la velocidad de reproducción predeterminada. + Mostrar un mensaje + Cambiando la velocidad predeterminada a %s. + Recuerda el estado de la repetición. + Recordar estado de repetición + Recuerda el estado del aleatorio (shuffle). + Recordar estado aleatorio + Recuerda la última calidad de vídeo seleccionada. + Recordar cambios de calidad de vídeo + Mostrar un mensaje al cambiar la calidad de vídeo por defecto. + Mostrar un mensaje + Cambiando la calidad predeterminada con datos móviles a %s. + Error al establecer calidad. + Cambiando la calidad predeterminada con Wi-Fi a %s. + "Elimina el diálogo de discreción del espectador. +Esto no evita la restricción de edad. Solo la acepta automáticamente." + Eliminar diálogo de discreción del espectador + Continúa el vídeo desde el tiempo actual cuando se cambia a YouTube. + Continuar viendo + Reemplaza el menú de descartar cola por el de ver en YouTube. + Reemplazar el menú descartar cola + Ver en YouTube + Url del video no válida. + Mantiene intacto el menú Denunciar en la sección de comentarios. + Mantener Denunciar en comentarios + Reemplaza el menú Denunciar con el menú Velocidad de reproducción. + Reemplazar menú Denunciar + Devuelve los paneles emergentes de comentarios al estilo antiguo. + Restaurar paneles emergentes de comentarios antiguos + Devuelve el fondo del reproductor al estilo antiguo. + Restaurar el fondo del reproductor antiguo + "Devuelve el diseño del reproductor al estilo antiguo. +Algunas características pueden no funcionar correctamente en la disposición del reproductor antiguo." + Activar diseño antiguo del reproductor + Devuelve la pestaña Biblioteca al estilo antiguo. (Experimental) + Restaurar el estante de la biblioteca de estilo antiguo + \@identificador (Nombre de usuario) + Seleccione el formato para mostrar el nombre de usuario. + Formato de visualización + Nombre de usuario (@identificador) + Nombre de usuario + Reemplaza identificadores con nombres de usuario en los comentarios. + Activa devolver nombre de usuario de YouTube + "Se requiere la clave de desarrollador de la API v3 de datos de YouTube para reemplazar el identificador con el nombre de usuario. + +La cuota diaria para las claves API en el plan gratuito es de 10,000, y se utiliza 1 cuota para reemplazar el identificador con el nombre de usuario en 1 comentario. + +Toca para ver cómo crear una clave de API." + Acerca de la clave API de datos de YouTube + La clave de desarrollador para utilizar la API v3 de datos de YouTube. + Clave API de datos de YouTube + 1. Ve a <a href=%1$s>Crear un nuevo proyecto</a>.<br>2. Pulsa en el botón <b>CREAR</b>.<br>3. 3. Ve a <a href=%2$s>API v3 de datos de YouTube</a>.<br>4. Pulsa en el botón <b>HABILITAR</b>.<br>5. Pulsa en <b>CREAR</b>. Pulsa en el botón <b>CREAR CREDENCIALES</b>.<br>6. Selecciona la opción <b>Datos públicos</b>.<br>7. Pulsa en el botón <b> SIGUIENTE</b>.<br>8. Copia la clave API.<br><br>※ La clave API nunca debe ser compartida con otros, por lo que no se incluye en los ajustes de Importar / Exportar. + Crear clave de desarrollador API v3 de datos de YouTube + Acerca de + Los datos son proporcionados por la API Return YouTube Dislike. Pulse aquí para obtener más información. + ReturnYouTubeDislike.com + Oculta el separador del botón Me gusta. + Botón Me Gusta compacto + En lugar del número de no me gusta, se muestra el porcentaje de no me gusta. + Porcentaje de No Me Gusta + Muestra el número de vídeos que no te gustan. + Activar Return YouTube Dislike + Muestra el recuento estimado de \"me gusta\" de los vídeos. + Mostrar \"Me gusta\" estimados + Los No Me Gusta no están disponibles (se alcanzó el límite de la API del cliente). + Los no me gusta no están disponibles (estado %d). + Los no me gusta están temporalmente no disponibles (la API no responde). + Los no me gusta no están disponibles (%s). + Se muestra el mensaje si la API de ReturnYouTubeDislike no está disponible. + Mostrar mensaje si la API no está disponible + Oculto + Elimina los parámetros de consulta de seguimiento de las URL al compartir enlaces. + Desinfectar enlaces compartidos + Acerca de + sponsor.ajay.app + Los datos son proporcionados por la API de SponsorBlock. Pulsa aquí para aprender más y ver las descargas para otras plataformas. + Cambiar URL de la API + URL de API cambiada. + La URL de la API no es válida. + Restablecer la URL de la API. + Dirección que el SponsorBlock utiliza para hacer llamadas al servidor. No cambie esto a menos que sepa qué está haciendo. + Color cambiado. + Color: + Código de color inválido. Restablecimiento de color predeterminado. + Restablecer color. + Cambiar el comportamiento del segmento + Activar SponsorBlock + SponsorBlock es un sistema colaborativo para omitir partes molestas en vídeos de YouTube. + Restablecer color + Tangente de relleno / Chistes + Escenas tangenciales añadidas solo para relleno o humor que no son necesarias para entender el contenido principal del vídeo. No incluye segmentos que proporcionen contexto o detalles de fondo. + Recordatorio de interacción (Suscribirse) + Un breve recordatorio para dar me gusta, suscribirse o seguirlos en medio del contenido. Si es largo o sobre algo específico, debe estar en la sección de autopromoción. + Intermedio / Animación de introducción + Un intervalo sin contenido real. Puede ser una pausa, un fotograma estático o una animación que se repite. No incluye transiciones que contengan información. + Música: Sección sin música + Solo para usar en vídeos musicales. Secciones de vídeos musicales sin música, que no estén ya cubiertas por otra categoría. + Tarjetas finales / Créditos + Créditos o cuando aparecen las tarjetas finales de YouTube. No para conclusiones con información. + Vista previa / Resumen / Gancho + Colección de clips que muestran lo que está por venir o lo que sucedió en el vídeo o en otros vídeos de una serie, donde toda la información se repite en otra parte. + Promoción no remunerada/autopromoción + Cuando hay una autopromoción o no remunerada. Esto incluye secciones específicas sobre mercancía, donaciones o información sobre con quién colaboraron. + Patrocinador + Promoción pagada, referencias pagadas y anuncios directos. No es para promoción propia ni para menciones gratuitas a causas, creadores, sitios web o productos que les gusten. + Omitir automáticamente + Deshabilitar + Relleno omitido. + Recordatorio molesto omitido. + Introducción omitida. + Intermisión omitida. + Intermisión omitida. + Varios segmentos omitidos. + Se omitió una sección sin música. + Créditos omitidos. + Vista previa omitida. + Resumen omitido. + Vista previa omitida. + Autopromoción omitida. + Patrocinador omitido. + SponsorBlock no está disponible temporalmente. + SponsorBlock no está disponible temporalmente. (estado %d). + SponsorBlock no está disponible temporalmente. (la API no responde). + Mostrar mensaje si la API no está disponible + Muestra un mensaje si la API de SponsorBlock no está disponible. + Mostrar mensaje al omitir segmento automáticamente + Mensaje emergente que se muestra cuando se salta un segmento automáticamente. + Ajustes copiados en el portapapeles. + "Modificación de la versión del cliente a la versión antigua. + +- Esto cambiará la apariencia de la aplicación, pero pueden producirse efectos secundarios desconocidos. +- Si más tarde se desactiva, la antigua interfaz de usuario puede permanecer hasta que se borren los datos de la aplicación." + 4.27.53 - Desactivar el modo radio en las regiones canadienses + 6.11.52 - Desactivar letras en tiempo real + 7.16.53 - Restaurar la antigua barra de acción + Seleccione el objetivo de la versión de la app a modificar. + Objetivo de la versión de la app a modificar + Modificar versión de aplicación + "\"falsifica al cliente para evitar problemas de reproducción. + +Limitaciones: +• Código de audio OPUS puede no ser compatible. +• La miniatura de la barra de Seekbar puede no estar presente. +• El historial de la vista no funciona con una cuenta de marca." + Falsificar cliente + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/fr-rFR/missing_strings.xml b/patches/src/main/resources/music/translations/fr-rFR/missing_strings.xml new file mode 100644 index 000000000..f20d393ac --- /dev/null +++ b/patches/src/main/resources/music/translations/fr-rFR/missing_strings.xml @@ -0,0 +1,33 @@ + + + Don\'t show again + Disables DRC (Dynamic Range Compression) applied to audio. + Disable DRC audio + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + Hide Pin to Speed dial menu + Hide Unpin from Speed dial menu + "Spoof the client to prevent playback issues. + +※ When used with 'Spoofing streaming data', playback issues may occur." + Spoof client + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + Android TV + Android VR + iOS + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/fr-rFR/strings.xml b/patches/src/main/resources/music/translations/fr-rFR/strings.xml new file mode 100644 index 000000000..111126993 --- /dev/null +++ b/patches/src/main/resources/music/translations/fr-rFR/strings.xml @@ -0,0 +1,400 @@ + + + Continuer + "GmsCore n'a pas les permissions pour fonctionner en arrière-plan. + +Suivez le guide \"Don't kill my app!\" pour votre appareil, et appliquez les instructions sur GmsCore. + +Requis pour que l'application fonctionne." + "L'optimisation de la batterie de GmsCore doit être désactivé pour éviter tout problème. + +Cliquez sur le bouton Continuer et désactivez les optimisations de la batterie." + Ouvrir le site web + Action requise + Activez la messagerie cloud pour recevoir les notifications. + Ouvrir GmsCore + GmsCore n\'est pas installé. Veuillez l\'installer. + Remplace le domaine qui est bloqué dans certaines régions afin que les miniatures des listes de lecture, les avatars des chaînes, etc. puissent être reçus. + Contourner les restrictions d\'image selon les régions + Remplace la fiche de partage de l\'appli par celui du système. + Modifier la fiche de partage + Charts + Explorer + Accueil + Bibliothèque + Abonnements + Sélectionnez la page de démarrage de l\'appli. + Modifier la page de démarrage + Filtrer la liste des noms du composant séparés par un saut de ligne. + Filtre personnalisé + Active les filtres personnalisés pour masquer des éléments de l’interface. + Activer le filtre personnalisé + Filtre personnalisé invalide : %s. + Les vitesses personnalisées doivent être inférieures à %sx. + Vitesses de lecture invalides. + Ajoute ou modifie les vitesses de lecture disponibles. + Éditez les vitesses de lecture personnalisées + Pour ouvrir les liens YouTube Music dans RVX Music, activez \'Ouvrir les liens compatibles\' et activez les adresses web prises en charge. + Ouvrir les paramètres par défaut de l\'application + Désactive les sous-titres automatiquement activés. + Désactiver les sous-titres forcés + Désactive l\'animation Cairo lors du démarrage de l\'application. + Désactiver l\'animation Cairo au démarrage + Désactive le passage à la piste suivante lorsque vous cliquez sur le bouton \"Je n\'aime pas\". + Désactiver la redirection du bouton \"Je n\'aime pas\" + Désactive les gestes pour changer de musique dans le minilecteur. + Désactiver les gestes du minilecteur + Désactive les gestes pour changer de musique dans le lecteur. + Désactiver les gestes du lecteur + Modifie la couleur de la barre de navigation en noir. + Activer la barre de navigation en noir + Change la couleur de l\'interface du lecteur en noir. + Activer l\'interface du lecteur en noir + Harmonise les couleurs du minilecteur à celle du lecteur en plein écran. + Activer l\'harmonisation des couleurs du lecteur + "Active le menu déroulant compact sur téléphones. + +Limitations : +• Les pochettes d'albums de la bibliothèque deviennent petites si organisées en mode grille. +• La mise en page du délai de mise en veille peut être inhabituelle." + Activer la boîte de dialogue compacte + Ajoute les informations sur la mémoire tampon dans le journal de débogage. + Activer les informations sur la mémoire tampon dans le journal de débogage + Enregistrer le journal de débogage. + Activer le journal de débogage + Maintient le lecteur minimisé même si une autre piste est lue. + Activer la minimisation forcée du lecteur + Active le mode paysage lors de la rotation du téléphone. + Activer le mode paysage + Ajoute le bouton \"Suivant\" sur le minilecteur. + Ajouter le bouton \"Suivant\" sur le minilecteur + Ajoute le bouton \"Précédent\" sur le minilecteur. + Ajouter le bouton \"Précédent\" sur le minilecteur + "Active le codec OPUS si la réponse du lecteur inclut le codec OPUS. + +Info : +• Les dernières versions de YouTube Music utilisent par défaut le codec audio Opus. +• Disponible uniquement pour les utilisateurs qui falsifient une très ancienne version du client." + Activer le Codec OPUS + Active le geste vers le bas pour fermer le minilecteur. + Activer le geste pour fermer le minilecteur + "Ajoute \"Masquer les silences\" dans le menu \"Vitesse de lecture\" du menu déroulant. + +Info : +• Cette fonctionnalité est destinée aux podcasts. +• Cette fonctionnalité est encore en développement, elle peut donc être instable." + Ajouter une option \"Masquer les silences\" + Active également le mode \"Zen\" pour les Podcasts. + Activer le mode \"Zen\" sur les Podcasts + Change la couleur du lecteur par un voile gris pour réduire la fatigue oculaire. + Activer le mode zen + Réinitialiser les valeurs par défaut. + Redémarrer pour charger l\'interface correctement + Appliquer et redémarrer ? + Exporter les paramètres vers un fichier + Échec de l\'exportation des paramètres. + Les paramètres ont été exportés avec succès. + Importer + Importer les paramètres depuis un fichier + Copier + Importer / Exporter les paramètres sous forme de texte + Importe ou exporte les paramètres. + Importer / Exporter les paramètres + Importation échouée : %s. + Les paramètres ont étés réinitialisés. + %d paramètres ont étés importés. + Réinitialiser + ReVanced Extended + "Le bouton \"Télécharger\" ouvre votre téléchargeur externe. + +• Remplace uniquement l’action du bouton \"Télécharger\" du lecteur. +• Ne remplace pas le bouton de téléchargement dans le menu déroulant ou la bibliothèque." + Remplacer l\'action du bouton \"Télécharger\" + Téléchargeur externe + "%1$s n'est pas installé. +Veuillez télécharger %2$s à partir du site web." + Attention + %s n\'est pas installé. Veuillez l’installer. + Nom de package du téléchargeur externe installé, telle que NewPipe ou YTDLnis. + Nom du paquet du téléchargeur externe + Masque les catégories vides dans le menu du compte. + Masquer les catégories vides + Liste de noms du menu de compte à filtrer, séparés par un saut de ligne. + Filtre du menu du compte + Masque les éléments dans le menu du compte à l\'aide du filtre personnalisé. + Masquer le menu du compte + Masque le bouton \"Enregistrer\". + Masquer le bouton \"Enregistrer\" + Masque le bouton \"Commentaires\". + Masquer le bouton \"Commentaires\" + Masque le bouton \"Télécharger\". + Masquer le bouton \"Télécharger\" + Masque les noms de la barre d’action. + Masquer les noms de la barre d’action + Masque les boutons \"J\'aime\" et \"Je n\'aime pas\". Ne fonctionne pas sur l\'ancienne interface du lecteur. + Masquer les boutons \"J\'aime\" et \"Je n\'aime pas\" + Masque le bouton \"Démarrer la radio\". + Masquer le bouton \"Radio\" + Masque le bouton \"Partager\". + Masquer le bouton \"Partager\" + Masque le sélecteur Audio/Vidéo en haut du lecteur. + Masquer le sélecteur Audio/Vidéo + Masque les étagères à boutons dans les flux. + Masquer les étagères de boutons + Masque les étagères à suggestions dans les flux. + Masquer les étagères à suggestions + Masque le Bouton \"Caster\". + Masquer le bouton \"Caster\" + Masque la barre de catégorie. + Masquer la barre de catégories + Masque les règles de la chaîne en haut de la section des commentaires. + Masquer les règles de la chaîne + Masque les boutons \"émoji\" et \"horodatage\" lors de la rédaction d\'un commentaire. + Masquer les boutons émoji et horodatage + Masque le voile sombre qui apparaît lors du double appui pour avancer. + Masquer le voile sombre lors du double appuie + Masque les boutons flottants dans la bibliothèque. + Masquer les boutons flottants + Masquer le composant à 3 colonnes + Masquer le bouton \"Ajouter à la file d\'attente\" + Masquer le menu \"Sous-titres\" + Masquer le menu \"Supprimer la playlist\" + Masquer le menu \"Supprimer la file d\'attente\" + Masquer le menu \"Télécharger\" + Masquer le menu \"Modifier la playlist\" + Masquer le menu \"Accéder à l\'album\" + Masquer le menu \"Accéder à la page de l\'artiste\" + Masquer le menu \"Accéder à l\'épisode\" + Masquer le menu \"Accéder au podcast\" + Masquer le menu \"Aide et commentaires\" + Masquer les boutons \"J\'aime\" et \"Je n\'aime pas\" + Masquer le menu \"Lire ensuite\" + Masquer le menu \"Qualité\" + Masquer le menu \"Retirer de la Bibliothèque\" + Masquer le menu \"Supprimer la playlist\" + Masquer le menu \"Signaler\" + Masquer le menu \"Enregistrer l\'épisode pour plus tard\" + Masquer le menu \"Enregistrer dans la bibliothèque\" + Masquer le menu \"Enregistrer dans une playlist\" + Masquer le menu \"Partager\" + Masquer le bouton \"Lecture aléatoire\" + Masquer le menu \"Délai de mise en veille\" + Masquer le menu \"Lancer la radio\" + Masquer le menu \"Statistiques avancées\" + Masquer le menu \"S\'abonner\" / \"Se désabonner\" + Masquer le menu \"Afficher les crédits du titre\" + "Masque les publicités en plein écran." + Masquer les publicités en plein écran + Masque le bouton \"Partager\" sur le lecteur en plein écran. + Masquer le bouton \"Partager\" en plein écran + Masque les publicités générales. + Masquer les publicités générales + Masque l\'identifiant dans le menu \"compte\". + Masquer l\'identifiant + Masque le bouton \"Historique\" de la barre d\'outils. + Masquer le bouton \"Historique\" + Masque les publicités avant la lecture d\'une musique. + Masquer les publicités musicales + Masque la barre de navigation. + Masquer la barre de navigation + Masque le bouton \"Explorer\". + Masquer le bouton \"Explorer\" + Masque le bouton \"Accueil\". + Masquer le bouton \"Accueil\" + Masque le nom sous les boutons de la barre de navigation. + Masquer les noms dans barre de navigation + Masque le bouton \"Bibliothèque\". + Masquer le bouton \"Bibliothèque\" + Masque le bouton \"Samples\". + Masquer le bouton \"Samples\" + Masque le bouton \"S\'abonner\" de la barre de navigation. + Masquer le bouton \"S\'abonner\" + Masque le bouton \"Notification\" de la barre d\'outils. + Masquer les boutons \"Notification\" + Masque la bannière \"Inclut une communication commerciale\". + Masquer la bannière \"Communication commerciale\" + Masque les étagères de cartes \"Playlists\" dans les flux. + Masquer les étagères de cartes \"Playlists\" + Masque les publicités pour YouTube Premium. + Masquer les publicités pour YouTube Premium + Masque la bannière \"Renouveler votre abonnement Premium\". + Masquer la bannière \"Renouveler votre abonnement Premium\" + Masque la bannière d\'alerte de promotion. + Masquer la bannière d\'alerte de promotion + Masque l’étagère \"Samples\" dans les flux. + Masquer l’étagère \"Samples\" + Masquer le menu \'À propos\' + Masquer le menu \'Économie de données\' + Masquer le menu \'Téléchargements et stockage\' + Masquer le menu \'Paramètres généraux\' + Masquer le menu \'Notifications\' + Masquer le menu \'Obtenir Music Premium\' + Masquer le menu \'Centre pour la famille\' + Masquer le menu \"Lecture\" + Masquer le menu \'Confidentialité et données\' + Masquer le menu \'Recommandations\' + "Masquer les éléments du menu Paramètre. +Cela masque non seulement le menu paramètre de YT Music, mais également le menu paramètre de ReVanced Extended." + Masquer le menu \'Paramètres\' + Masque le bouton \"Rechercher une musique\" de la barre de recherche. + Masquer le bouton \"Rechercher une musique\" + Masque le bouton \"Appuyer pour mettre à jour\". + Masquer le bouton \"Appuyer pour mettre à jour\" + Masque le conteneur des conditions d\'utilisation. + Masquer le conteneur de termes + Masque le bouton \"Recherche vocale\" de la barre de recherche. + Masquer le bouton \"Recherche vocale\" + Compte + Barre d\'action + Publicités + Menu déroulant + Interface + Paramètres avancés + Barre de navigation + Lecteur + Return YouTube Username + Return YouTube Dislike + SponsorBlock + Menu Paramètres + Qualité et vitesse vidéo + Enregistre la dernière vitesse de lecture sélectionnée. + Enregistrer la modification de la vitesse de lecture + Afficher un message lorsque vous modifiez la vitesse de lecture par défaut. + Afficher un message + Vitesse de lecture modifiée par %s. + Enregistre l\'état du mode répétition. + Enregistrer l\'état du mode répétition + Enregistre l\'état du mode aléatoire. + Enregistrer l\'état du mode aléatoire + Enregistre la dernière qualité vidéo sélectionnée. + Enregistrer la modification de la résolution + Afficher un message lorsque vous modifiez la qualité vidéo par défaut. + Afficher un message + La résolution sur les données mobiles a été modifiée par %s. + Impossible de définir la qualité. + La résolution sur le Wi-Fi a été modifiée par %s. + "Supprime le message \"Confirmer votre âge\". +Cela ne contourne pas la restriction d'âge, mais le confirme automatiquement." + Supprimer le message \"Confirmer votre âge\" + Continuer la lecture avec l\'horodatage en cours sur YouTube. + Continuer la lecture + Remplace le menu \"Supprimer de la file d\'attente\" par le menu \"Regarder sur YouTube\". + Remplacer le menu \"Supprimer de la file d\'attente\" + Regarder sur YouTube + Url de la vidéo invalide. + Conserve le menu \"Signaler\" dans la section des commentaires. + Conserver le menu \"Signaler\" dans les commentaires + Remplace le menu \"Signaler\" par le menu \"Vitesse de lecture\". + Remplacer le menu \"Signaler\" + Restaure l\'ancien style de la section commentaire. + Restaurer l\'ancienne interface des commentaires + Restaure l\'ancien style de l\'arrière-plan du lecteur. + Restaurer l\'ancien arrière-plan du lecteur + "Restaure l'ancien style de la mise en page du lecteur. +Certaines fonctions peuvent ne pas fonctionner sur l'ancienne mise en page." + Restaurer l\'ancienne mise en page du lecteur + Restaure l\'ancien style de l’étagère \"Bibliothèque\". (Expérimental) + Restaurer l\'ancien style de l’étagère \"Bibliothèque\" + \@identifiant (Nom d\'utilisateur) + Sélectionnez le format d\'affichage des noms d\'utilisateurs. + Format d\'affichage + Nom d\'utilisateur (@identifiant) + Nom d\'utilisateur + Remplace l\'identifiant par les noms d\'utilisateurs dans les commentaires. + Activer Return YouTube Username + "La clé YouTube Data API v3 est nécessaire pour remplacer les identifiants par des noms d'utilisateurs. + +Le quota journalier pour les clés API sur le plan gratuit est de 10 000, 1 quota est utilisé pour remplacer l'identifiant par un nom d'utilisateur pour 1 commentaire. + +Cliquez ici pour découvrir comment créer une clé API." + À propos de la clé YouTube Data API + La clé de développeur pour utiliser YouTube Data API v3. + Clé API des données YouTube + 1. Allez sur <a href=%1$s>Nouveau projet</a>.<br>2. Cliquez sur le bouton <b> Créer</b>.<br>3. Allez sur <a href=%2$s>YouTube Data API v3</a>.<br>4. Cliquez sur le bouton <b>ACTIVER</b>.<br>5. Cliquez sur le bouton <b>CRÉER DES IDENTIFIANTS</b>.<br>6. Sélectionnez l\'option <b>Données Publiques</b>.<br>7. Cliquez sur le bouton <b>SUIVANT</b>.<br>8. Copiez la clé API.<br><br>※ La clé API ne doit jamais être partagée avec d\'autres personnes, par conséquent, elle n\'est pas incluse dans les paramètres Importer / Exporter. + Obtenir une clé développeur pour YouTube Data API v3 + À propos + Les données des \"Je n\'aime pas\" sont fournies par l\'API de Return YouTube Dislike. Appuyez ici pour en savoir plus. + ReturnYouTubeDislike.com + Masque les séparateurs sur le bouton \"J\'aime\". + Bouton \"J\'aime\" compact + Les \"Je n\'aime pas\" sont affichés en pourcentage plutôt qu\'en nombre. + \"Je n\'aime pas\" en pourcentage + Affiche le compteur des \"Je n\'aime pas\" sur les vidéos. + Activer Return YouTube Dislike + Affiche le nombre de \"J\'aime\" estimés sur les vidéos. + Afficher les \"J\'aime\" estimés + Les \"Je n\'aime pas\" sont indisponibles (le client a atteint la limite de l\'API). + Les \"Je n\'aime pas\" sont indisponible (status %d). + Les \"Je n\'aime pas\" sont temporairement indisponible (API obsolète). + Les \"Je n\'aime pas\" sont indisponible (%s). + Affiche un message si l\'API de Return YouTube Dislike n\'est pas disponible. + Afficher un message si l\'API est indisponible + Masqué + Supprime les paramètres de suivi (tracking) des URL lors du partage de liens. + Nettoyer les liens partagés + À propos + sponsor.ajay.app + Les données sont fournies par l\'API SponsorBlock. Cliquez ici pour en savoir plus et voir les téléchargements pour d\'autres plateformes. + Modifier l\'URL de l\'API + L\'URL de l\'API a été modifiée. + L\'URL de l\'API est invalide. + L\'URL de l\'API a été réinitialisé. + L\'adresse qu\'utilise SponsorBlock pour contacter le serveur. Ne le modifiez que si vous savez ce que vous faites. + Couleur modifiée. + Couleur (hex) : + Code couleur invalide. + Couleur réinitialisée. + Modifier le comportement des segments + Activer Sponsorblock + SponsorBlock est un service d\'entraide permettant de passer des parties gênantes des vidéos YouTube. + Réinitialiser la couleur + Remplissage / Blagues + Scènes secondaires ajoutées uniquement à des fins de remplissage ou d\'humour qui ne sont pas nécessaires à la compréhension de la vidéo. N\'inclus pas les segments fournissant un contexte ou des détails utiles. + Rappel d\'interaction (S\'abonner) + Un bref rappel pour aimer, s\'abonner ou pour les suivre au milieu du contenu. S\'il est long ou s\'il traite d\'un sujet spécifique, il devrait être placé dans la section \"auto-promotion\". + Intro / Entracte + Un intervalle sans contenu réel. Il peut s\'agir d\'une pause, d\'une image fixe ou d\'une animation répétitive. Ne comprends pas des passages contenant des informations. + Musique : Section non musicale + Uniquement destiné pour les vidéos musicales. Sections de vidéos musicales sans musique, qui ne sont pas déjà couvertes par une autre catégorie. + Outro / Crédits + Crédits ou lorsque les cartes de fin de vidéo YouTube apparaissent. Ne pas utiliser pour des conclusions avec des informations. + Aperçu / Récapitulatif + Un récapitulatif montrant ce qu\'il va se passer ou qui s\'est passé dans la vidéo ou dans d\'autres vidéos de la série, lorsque des informations se répètes. + Auto-promotion / Non Rémunérée + Similaire à \'Sponsor\', à l\'exception de la promotion non rémunérée ou l\'autopromotion. Comprend les sections produits, les dons, et des informations sur les partenaires avec lesquels ils ont collaboré. + Sponsor + Partenariat rémunéré, parrainage rémunérés et publicités directes. Ne concerne pas l\'autopromotion ou les mentions gratuites pour des causes / créateurs / sites web / produits qu\'ils apprécient. + Passer automatiquement + Désactiver + Remplissages passés. + Passage ennuyeux passé. + Intro passée. + Entracte passé. + Entracte passé. + Plusieurs segments passés. + Section non musicale passée. + Outro passée. + Aperçu passé. + Résumé passé. + Aperçu passé. + Autopromotion passée. + Sponsor passé. + SponsorBlock est temporairement indisponible. + SponsorBlock est temporairement indisponible (status %d). + SponsorBlock est temporairement indisponible (API obsolète). + Afficher un message si l\'API est indisponible + Affiche un message si l\'API de SponsorBlock est indisponible. + Afficher un message lors du passage auto des segments + Affiche un message lorsqu\'un segment est automatiquement passé. + Paramètres copiés dans le presse-papier. + "Falsifie la version du client par une ancienne version. + +• Cela change l'apparence de l'application, mais des effets secondaires inconnus peuvent se produire. +• Si désactivée ultérieurement, l'ancienne interface peut subsister jusqu'à la suppression des données de l'application." + 4.27.53 - Désactive le mode radio dans les régions canadiennes + 6.11.52 - Désactive les paroles en temps réel + 7.16.53 - Restaurer l\'ancienne barre d\'action + Sélectionner la version de l\'application à falsifier. + Choisir la version à falsifier + Falsifier la version de l\'app + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/hu-rHU/missing_strings.xml b/patches/src/main/resources/music/translations/hu-rHU/missing_strings.xml new file mode 100644 index 000000000..973422826 --- /dev/null +++ b/patches/src/main/resources/music/translations/hu-rHU/missing_strings.xml @@ -0,0 +1,6 @@ + + + Don\'t show again + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/hu-rHU/strings.xml b/patches/src/main/resources/music/translations/hu-rHU/strings.xml new file mode 100644 index 000000000..12be9c2e6 --- /dev/null +++ b/patches/src/main/resources/music/translations/hu-rHU/strings.xml @@ -0,0 +1,429 @@ + + + Folytatás + "A GmsCore-nak nincs engedélye a háttérben történő futtatásra. + +Kövesd a telefonodra vonatkozó 'Don't kill my app!' útmutatót és alkalmazd az utasításokat a MicroG telepítésére. + +Ez szükséges az app működéséhez." + "A GmsCore akkumulátor-optimalizálásokat le kell tiltani a problémák megelőzése érdekében. + +A GmsCore akkumulátor-optimalizálás letiltása nem fogyasztja jobban az akkumulátort. + +Nyomj a folytatás gombra, és engedélyezd az optimalizálási módosításokat." + Weboldal megnyitása + Művelet szükséges + Értesítések fogadásához engedélyezd a felhő alapú üzenetküldést. + GmsCore megnyitása + A GmsCore nincs telepítve. Telepítsd. + Helyettesíti az egyes régiókban blokkolt tartományt, így a lejátszási lista miniatűrjei, csatorna avatarok stb. fogadhatóak. + Területi kép-korlátozások megkerülése + Váltás az alkalmazáson belüli megosztási lapról a rendszer megosztási lapjára. + Megosztási lap megváltoztatása + Diagramok + Felfedezés + Kezdőlap + Könyvtár + Feliratkozások + Kiválaszthatod, hogy milyen oldalon nyíljon meg az alkalmazás. + Kezdőlap megváltoztatása + A szűrendő elemek útvonal-építő stringjeinek listája, új sorokkal elválasztva. + Egyéni szűrő + Engedélyezi, hogy saját menüket is elrejts. + Egyéni szűrők engedélyezése + Érvénytelen egyéni szűrő: %s. + Az egyéni sebességnek kisebbnek kell lennie, mint %sx. + Érvénytelen egyedi lejátszási sebesség. + Az elérhető lejátszási sebességek módosítása vagy hozzáadása. + Egyéni lejátszási sebességek szerkesztése + A YouTube Music linkek megnyitásához az RVX Musicban engedélyezze a \'Támogatott linkek megnyitása\' opciót, és engedélyezze a támogatott webcímeket. + Alapértelmezett program beállítások megnyitása + Letiltja a feliratokat, hogy ne jelenjenek meg automatikusan. + Kényszerített automatikus feliratok letiltása + Letiltja a betöltési animációt amikor az app indul. + Betöltési animáció kikapcsolása + Letiltja az átirányítást a következő számra, amikor rányomsz a nem tetszik gombra. + Nem tetszik átirányítás letiltása + Letiltja a hangra alkalmazott DRC-t (dinamikatartomány-kompresszió). + DRC hang letiltása + Kikapcsolja a zeneszámok váltását a minialejátszóban. + Minilejátszó gesztus letiltása + Kikapcsolja a zeneszámok váltását a lejátszóban. + Lejtászó gesztus letiltása + A navigációs sor színét átállítja feketére. + Fekete navigációs sor engedélyezése + Megváltoztatja a lejátszó háttér színét feketére. + Fekete hátterű lejátszó engedélyezése + Egyező színe lesz a kis lejátszónak, mint a teljes képernyősnek. + Megegyező színű lejátszó bekapcsolása + "Engedélyezi a telefonokon a kompakt felugró menüt. + +Korlátozások: +• A könyvtár lapon lévő albumok képei kisebbek lesznek, ha rácsba vannak rendezve. +• Az alvásidőzítő elrendezése szokatlannak tűnhet." + Kompakt menü engedélyezése + Beleírja a puffert a hibakeresési naplóba. + Hibakeresési puffer naplózásának engedélyezése + Kiírja a hibanaplót. + Hibanaplók engedélyezése + A lejátszó akkor is minimalizálva marad, amikor egy másik zeneszámot játszanak le. + Mini lejátszó kényszerítése + Engedélyezi a fekvő módot, amikor elforgatod a telefonodat. + Fekvő mód engedélyezése + Engedélyezi a következő szám gombot a minilejátszónál. + Minilejátszó következő gomb engedélyezése + Engedélyezi a előző szám gombot a minilejátszónál. + Minilejátszó előző gomb engedélyezése + "Az opus audio codec engedélyezése az mp4a audio codec helyett. + +Info: +• A legújabb Android-kliensek alapértelmezés szerint az opus audio codec-et használják. +• Ez csak a nagyon régi klienseket használó felhasználókra érvényes." + Opus codec engedélyezése + Lehetővé teszi a minialejátszó elhagyását lefelé húzással. + Minilejátszó elhagyása egy húzással + "A 'Csend kivágás' kapcsoló hozzáadása a lejátszási sebesség felugró menühöz. + +Információ: +• Ez a funkció podcastek számára készült. +• Ez a funkció még fejlesztés alatt áll, ezért instabil lehet." + Csend kivágás kapcsoló hozzáadása + A zen mód a podcast-ekben is működni fog. + Zen mód engedélyezése podcastekben + Megváltoztatja a lejátszó hátterét világos szürkére a szem megóvására. + Zen mód bekapcsolása + Visszaállítás az alapértelmezett értékekre. + Indítsd újra az elrendezés normál betöltéséhez + Frissítés és újraindítás + Beállítások exportálása egy fájlba + A beállítások exportálása sikertelen. + A beállítások sikeresen exportálva. + Importálás + Beállítások importálása fájlból + Másolás + Beállítások import- / exportálása szövegként + Beállítások importálása vagy exportálása. + Beállítások Importálása / Exportálása + Sikertelen importálás: %s. + Beállítások visszaállítása alapra. + %d beállítás importálva. + Visszaállítás + ReVanced Extended + "A Letöltés gomb megnyitja a külső letöltőt. + +• Csak a lejátszóban lévő letöltési művelet gombot írja felül. +• Nem írja felül a letöltés gombot a felugró menüben vagy a könyvtárban." + Letöltés gomb felülírása + Külső letöltéskezelő + "A(z) %1$s nincs telepítve. +Töltsd le a(z) %2$s weboldalról." + Figyelmeztetés + %s nincs telepítve. Kérlek, telepítsd. + A telepített külső letöltő alkalmazás csomagneve, például NewPipe vagy YTDLnis. + Külső letöltő csomagneve + Elrejti az üres részeket a fiók menüben. + Üres részek elrejtése + A fiókmenüben szűrendő nevek listája, új sorokkal elválasztva. + Fiókmenü szűrő + Elrejti a fiókmenü elemeit az egyéni szűrőben. + Fiókmenü elrejtése + Elrejti a mentés gombot. + Mentés gomb elrejtése + Elrejti a megjegyzés gombot. + Megjegyzés gomb elrejtése + Elrejti a letöltés gombot. + Letöltés gomb elrejtése + Elrejti a címkéket az műveleti gombokon. + Navigációs gombok címkéinek elrejtése + Elrejti a tetszik és nem tetszik gombokat. Nem működik a régi lejátszóval. + A tetszik és nem tetszik gombok elrejtése + Elrejti a rádió gombot. + Rádió gomb elrejtése + Elrejti a Megosztás gombot. + Megosztás gomb elrejtése + Elrejti a hang/videó gombot a lejátszóban. + Hang/Videó gomb elrejtése + Elrejti a gomb polcot a főoldalon. + Gomb polc elrejtése + Elrejti a forduló polcot a főoldalon. + Forduló polc elrejtése + Elrejti az átküldés gombot. + Átküldés gomb elrejtése + Elrejti a kategória sávot. + Kategória sáv elrejtése + Elrejti a csatorna irányelveit a komment szekció tetején. + Csatorna irányelveinek elrejtése + Elrejti az időbélyeget és az emoji gombokat komment gépelés közben. + Elrejti az edőbélyeg és az emoji gombokat + Elrejti dupla koppintáskor megjelenő sötét átfedést. + Dupla koppintás átfedés elrejtése + Elrejti a lebegő gombot a könyvtárban. + Lebegő gomb elrejtése + 3 oszlopos komponens elrejtése + Hozzáadás a várólistához menü elrejtése + Feliratok menü elrejtése + Lejátszási lista törlés menü elrejtése + Várólista menü elrejtése + Letöltés menü elrejtése + Lejátszási lista szerkesztés menü elrejtése + Album menü elrejtése + Előadó menü elrejtése + Rész menü elrejtése + Podcast menü elrejtése + Súgó & visszajelzés menü elrejtése + A tetszik és nem tetszik gombok elrejtése + Fül elrejtése a Gyors elérés menüben + Következő lejátszása menü elrejtése + Minőség menü elrejtése + Eltávolítás a könyvtárból menü elrejtése + Eltávolítás a lejátszási listáról menü elrejtése + Jelentés menü elrejtése + Rész elmentése későbbre menü elrejtése + Mentés a könyvtárba menü elrejtése + Mentés a lejátszási listába menü elrejtése + Megosztás menü elrejtése + Kevert lejátszás menü elrejtése + Elalvási időzítő elrejtése + Rádió indítás menü elrejtése + Statisztikák kockáknak menü elrejtése + Feliratkozás / Leiratkozás menü elrejtése + Kitűzés a Gyorshívóba menü elrejtése + Dalkredit menü elrejtése + "Teljes képernyős hirdetések elrejtése." + Teljes képernyős hirdetések elrejtése + Elrejti a megosztás gombot a teljes képernyős lejátszóban. + Teljes képernyős megosztás gomb elrejtése + Elrejti az általános hirdetéseket. + Általános hirdetések elrejtése + Elrejti a felhasználónevedet a fiók menüben. + Felhaszálónév elrejtése + Elrejti az előzmények gombot az eszköztáron. + Előzmények gomb elrejtése + Elrejti a hirdetéseket a zene lejátszása előtt. + Zenei hirdetések elrejtése + Elrejti a navigációs sávot. + Navigációs sáv elrejtése + Elrejti a felfedezés gombot. + Felfedezés gomb elrejtése + Elrejti a kezdőlap gombot. + Kezdőlap gomb elrejtése + Elrejti a szöveget a navigációs gombok alatt. + Navigációs címkék elrejtése + Elrejti a könyvtár gombot. + Könyvtár gomb elrejtése + Elrejti a minták gombot. + Minták gomb elrejtése + Elrejti az előfizetés gombot. + Előfizetés gomb elrejtése + Elrejti az értesítés gombot az eszköztáron. + Értesítés gomb elrejtése + Elrejti a promóció címkét. + Fizetett promóció címke elrejtése + Elrejti a lejátszási lista kártya polcot a főoldalon. + Lejátszási lista kártya polc elrejtése + Elrejti a felugró prémium hírdetéseket. + Felugró prémium hírdetések elrejtése + Elrejti a prémium megújítás szalaghírdetést. + Prémium megújítás szalaghírdetés elrejtése + Promóciós figyelmeztető banner elrejtése. + Promóciós figyelmeztető banner elrejtése + Elrejti a minták polcot a főoldalon. + Minták polc elrejtése + A Youtube Music névjegye menü elrejtése + Adatmegtakarító menü elejtése + Letöltések és tárhely menü elrejtése + Általános menü elrejtése + Értesítések menü elrejtése + Music Premium-tagság vásárlása menü elrejtése + Családi központ menü elrejtése + Következő lejátszása menü elrejtése + Adatvédelem és adatok elrejtése + Ajánlott elrejtése + "A beállítások menü elemeinek elrejtése. +Ez nemcsak az YT Music beállítások menüjét, hanem a ReVanced Extended beállítások menüjét is elrejti." + Beállítások menü elrejtése + Elrejti a zene keresés gombot a kereső sávban. + Zenekeresés gomb elrejtése + Elrejti a Kattints a frissítéshez gombot. + Kattints a frissítéshez gomb elrejtése + Elrejti a Szolgáltatási feltételeket a fiókmenüben. + Feltételek rész elrejtése + Elrejti a hang keresés gombot a kereső sávban. + Hangkeresés gomb elrejtése + Fiók + Műveletsáv + Hirdetések + Felugró menü + Általános + Egyéb + Navigációs sor + Lejátszó + Youtube felhasználónév visszaállítása + YouTube nem tetszések visszaállítása + Szponzor Blokk + Beállítások menü + Videó + Megjegyzi az utoljára kiválasztott lejátszási sebességet. + Lejátszási sebesség módosításainak megjegyzése + Értesíts, ha változik a lejátszás sebessége. + Mutass egy felugró értesítést + Megváltoztatva az alap sebességet %s-re. + Emlékezik az ismétlés állapotára. + Isméltés állapotának megjegyzése + Emlékezik az keverés állapotára. + Keverés állapotának megjegyzése + Megjegyezi a legutolsó videó minőséget, amit kiválasztottál. + Videó minőség megjegyzése + Értesíts ha meg változik a lejátszás minőségének beállítása. + Mutass egy felugró értesítést + Az alapértelmezett mobiladat minőség módosítása a következőre %s. + Nem sikerült beállítani a minőséget. + Az alapértelmezett Wi-Fi-minőség módosítása a következőre: %s. + "Eltávolítja a nézői döntés menüt. +Ez nem kerüli meg a korhatárkorlátozást. Csak automatikusan elfogadja azt." + Nézői döntés menü eltávolítása + Folytatja a videót attól az időponttól, amikor átváltasz a Youtube-ra. + Megtekintés folytatása + Kicseréli a \"Várólistát\" a \"Megtekintés a Youtube-on\"-nal. + Várólista cserélése + Megnézés YouTube-on + Érvénytelen videó url. + Megtartja a jelentés menüt a komment szekcióban. + Jelentés meghagyása a kommenteknél + Kicseréli a \"Jelentés\"-t a \"Lejátszási sebesség\"-gel. + Jelentés cserélése + Visszaállítja a régi felugró menü a régi kinézetére. + Régi komment felugró menü visszaállítása + Visszaállítja a lejátszó hátterét a régi kinézetére. + Régi lejátszó kinézet visszaállítása + "Visszaállítja a lejátszó elrendezését a régi stílusra. +Előfordulhat, hogy egyes funkciók nem működnek megfelelően a régi lejátszó elrendezésben." + Régi lejátszó kinézet visszaállítása + Visszaállítja a régi megjelenését a könyvtár oldalnak. (Kísérleti) + Visszaállítja a régi stílusú könyvtár polcot + \@azonosító (felhasználónév) + Válaszd ki a felhasználónév megjelenítési formátumát. + Megjelenítési formátum + Felhasználónév (@azonosító) + Felhasználónév + A hozzászólásokban az azonosítót lecseréli a felhasználónevekre. + Youtube felhasználónév visszaállítása + "YouTube Data API v3 fejlesztői kulcsra van szükség az azonosítók felhasználónevekre való cseréjéhez. + +Az ingyenes csomagban az API-kulcsok napi kvótája 10 000, és 1 kvótával 1 megjegyzéshez egy azonosító felhasználónévre cserélhető. + +Kattints ide az API-kulcs megszerzéséhez." + A YouTube Data API-kulcsról + A fejlesztői kulcs a YouTube Data API v3 használatához. + YouTube Data API kulcs + 1. Nyisd meg az <a href=%1$s>Új projekt létrehozását</a>.<br>2. Kattints a <b>LÉTREHOZÁS</b> gombra.<br>3. Lépj a <a href=%2$s>YouTube Data API v3</a> oldalára.<br>4. Kattints az <b>Engedélyezés</b> gombra.<br>5. Kattints a <b>HITELEZÉSI ADATOK LÉTREHOZÁSA</b> gombot.<br>6. Válaszd ki a <b>Nyilvános adatok</b> lehetőség.<br>7. Kattints a <b>KÖVETKEZŐ</b> gombra.<br>8. Másold ki az API-kulcsot.<br><br>※ Az API-kulcsot soha nem szabad megosztani másokkal, így az nem szerepel az importálási/exportálási beállításokban. + YouTube Data API v3 fejlesztői kulcs megszerzése + Névjegy + Az adatokat a YouTube nem tetszések visszaállítása API biztosítja. További információért nyomj ide. + ReturnYouTubeDislike.com + Elrejti a kedvelés gomb elválasztóját. + Kompakt kedvelés gomb + A nem tetszések százalékos arányát jeleníti meg a nem tetszések száma helyett. + Nem tetszések megjelenítése százalékban + Megjeleníti a videók nem tetszésének számát. + YouTube nem tetszések visszaállításának engedélyezése + Megjeleníti a becsült kedvelések számát a videóknál. + Mutassa a becsült kedveléseket + Nem tetszések nem érhetőek el (kliens API limit elérve). + A nem tetszik funkció nem elérhető (állapot: %d). + Nem tetszések átmenetileg nem érhetőek el (API időkorlát lejárt). + A nem tetszik funkció nem elérhető (%s). + Megjelenik egy üzenet, ha a YouTube nem tetszések visszaállítása API nem érhető el. + Köszöntő megjelenítése, ha az API nem elérhető + Rejtett + Linkek megosztásakor eltávolítja a nyomkövetési paramétereket az URL-ekből. + Megosztási linkek tisztítása + Névjegy + sponsor.ajay.app + Az adatokat a SponsorBlock API biztosítja. Nyomj ide, ha többet szeretnél megtudni és megtekintenéd a letöltéseket más platformokra. + API URL módosítása + API URL megváltoztatva. + API URL érvénytelen. + Az API URL visszaállítása. + A cím, amelyet a SponsorBlock a szerverhez történő kommunikációhoz használ. Ne változtasso ezen, ha nem tudod, hogy mit csinálsz. + Szín módosítva. + Szín: + Érvénytelen színkód. + Szín visszaállítva. + Szakasz viselkedésének megváltoztatása + SzponsorBlokk engedélyezése + A SzponsorBlokk egy közösségi rendszer a YouTube videók zavaró részeinek átugrására. + Szín visszaállítása + Kitöltések / Viccek + Csak töltelék vagy humornak hozzáadott részek, amik nem szükségesek a videó fő tartalmának megértéséhez. Ne tartalmazzon olyan részeket, amik összefüggést, vagy háttérinformációt szolgáltatnak. + Emlékeztető (Feliratkozás) + Rövid emlékeztető a kedvelésre, feliratkozásra vagy követésre a tartalom közepette. Ha hosszú vagy valami specifikusról szól, akkor azt önpromóció alatt kell feltüntetni. + Szünet/Intro animáció + Egy részlet tartalom nélkül. Lehet szünet, álló képkocka, vagy ismétlődő animáció. Nem használandó információt tartalmazó átmeneteknél. + Zene: nem-zene szegmens + Csak zenei videókhoz használható. Zenei videók zene nélküli részei, amelyek még nem tartoznak más kategóriába. + Zárókártyák / Stáblista + Stáblista, vagy amikor megjelennek a YouTube zárókártyák. Nem tartozik bele az információt tartalmazó összegzés. + Előzetes / Összefoglaló / Hook + Olyan klipek gyűjteménye, amelyek megmutatják, hogy mi következik vagy mi történt a videóban vagy egy sorozat más videóiban, ahol minden információ máshol ismétlődik. + Nem fizetett / Önpromóció + Hasonló a szponzorhoz, kivéve a nem fizetett vagy önpromóciót. Tartalmazza az árucikkekre, adományokra vonatkozó részeket, vagy információkat arról, hogy kivel működtek együtt. + Szponzor + Fizetett promóciók, fizetett hivatkozások és közvetlen reklámok. Nem önreklámozásra vagy ingyenes kiemelésre vonatkozik okok / alkotók / weboldalak / termékek esetében, amelyeket szeretnek. + Átugrás automatikusan + Letiltás + Töltelék átugorva. + Idegesítő emlékeztető átugorva. + Bevezető kihagyva. + Szünet átugorva. + Szünet átugorva. + Több szakasz átugorva. + Nem zenei rész átugorva. + Befejezés átugorva. + Bevezető átugorva. + Összefoglaló átugorva. + Bevezető átugorva. + Önpromóció átugorva. + Szponzor átugorva. + A SponsorBlock átmenetileg nem elérhető. + A SponsorBlock jelenleg nem elérhető (állapot %d). + A SponsorBlock átmenetileg nem elérhető (API időtúllépés). + Üzenet megjelenítése, ha az API nem elérhető + Üzenetet ír ki, ha a SponsorBlock API nem érhető el. + Üzenet megjelenítése automatikus ugráskor + Üzenetet jelenít meg, amikor egy szegmens automatikusan kihagyásra kerül. + A beállítások vágólapra másolva. + "Hamisítja a kliens verziót egy régi verzióra. + +• Ez megváltoztatja az alkalmazás megjelenését, de ismeretlen mellékhatások előfordulhatnak. +• Ha később kikapcsolod, a régi felhasználói felület megmaradhat, amíg az alkalmazás adatait nem törlöd." + 4.27.53 - Letiltja a rádió módot Kanada területén + 6.11.52 - Letiltja a valós idejű dalszövegeket + 7.16.53 - Régi menüsor visszaállítása + Válaszd ki, hogy melyik alkalmazásverziót akarod használni. + Cél alkalmazásverzió + Alkalmazásverzió hamisítása + "A kliens hamisítása a lejátszási problémák elkerülése érdekében. + +※ Az 'Adatfolyam meghamisítása' használata esetén lejátszási problémák léphetnek fel." + Kliens hamisítása + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Meghatározza az alapértelmezett klienst a hamisításhoz. + +※ Az Android kliens használata esetén ajánlott a 'Alkalmazás verziójának meghamisítása' használni." + Alapértelmezett kliens + Megmutatja a streaming adatok lekérdezésére használt klienst a Statisztikák kockáknakban. + Megjelenik a statisztikák kockáknakban + "A lejátszási problémák megelőzése érdekében hamisítja a streaming-adatokat. + +※ A 'Kliens hamisítása' használata esetén lejátszási problémák léphetnek fel." + Adatfolyam meghamisítása + Android TV + Android VR + iOS + iOS Music + Meghatároz egy alapértelmezett klienst, amely streaming adatokat hív le. + Alapértelmezett kliens + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/id-rID/missing_strings.xml b/patches/src/main/resources/music/translations/id-rID/missing_strings.xml new file mode 100644 index 000000000..7e6233426 --- /dev/null +++ b/patches/src/main/resources/music/translations/id-rID/missing_strings.xml @@ -0,0 +1,73 @@ + + + Don\'t show again + To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. + Open default app settings + Disables Cairo splash animation when the app starts up. + Disable Cairo splash animation + Disables DRC (Dynamic Range Compression) applied to audio. + Disable DRC audio + Reset to default values. + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + Hide Pin to Speed dial menu + Hide Unpin from Speed dial menu + Hides the promotion alert banner. + Hide promotion alert banner + Hide About menu + Hide Data saving menu + Hide Downloads & storage menu + Hide General menu + Hide Notifications menu + Hide Get Music premium menu + Hide Family Center menu + Hide Playback menu + Hide Privacy & data menu + Hide Recommendations menu + Return YouTube Username + Settings menu + @handle (Username) + Select the username display format. + Display format + Username (@handle) + Username + Replaces handles with usernames in comments. + Enable Return YouTube Username + "A YouTube Data API v3 Developer Key is required to replace handles with usernames. + +The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. + +Click to see how to issue a API key." + About YouTube Data API key + The developer key for using the YouTube Data API v3. + YouTube Data API key + 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. + Issue YouTube Data API v3 developer key + Shows the estimated like count of videos. + Show estimated likes + Hidden + 7.16.53 - Restore old action bar + "Spoof the client to prevent playback issues. + +※ When used with 'Spoofing streaming data', playback issues may occur." + Spoof client + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + Android TV + Android VR + iOS + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/id-rID/strings.xml b/patches/src/main/resources/music/translations/id-rID/strings.xml new file mode 100644 index 000000000..3e9c64d8b --- /dev/null +++ b/patches/src/main/resources/music/translations/id-rID/strings.xml @@ -0,0 +1,356 @@ + + + Continue + "GmsCore does not have permission to run in the background. + +Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. + +This is required for the app to work." + "GmsCore battery optimizations must be disabled to prevent issues. + +Tap on the continue button and disable battery optimizations." + Open website + Action needed + Enable cloud messaging to receive notifications. + Open GmsCore + GmsCore is not installed. Install it. + Mengganti domain yang ke blokir di negara tertentu sehingga playlist thumbnail, channel avatar, dll bisa di terima. + Bypass gambar larangan wilayah + Mengubah dari lembar berbagi dalam aplikasi ke lembar berbagi sistem. + Ubah lembar berbagi + Charts + Jelajahi + Beranda + Koleksi + Berlangganan + Select which page the app opens in. + Ganti Halaman Awal + Memfilter nama komponen dengan baris yang dipisahkan. + Edit filter kustom + Mengaktifkan filter kustom untuk menyembunyikan komponen tata letak. + Aktifkan filter kustom + Invalid custom filter: %s. + Kecepatan pemutaran kustom tidak valid. Atur ulang ke nilai default. + Invalid custom playback speeds. Using default values. + Menambah atau mengubah kecepatan pemutaran yang tersedia. + Edit kecepatan pemutaran kustom + Teks otomatis paksa yang dinonaktifkan. + Nonaktifkan teks otomatis paksa + Disables redirection to the next track when clicking the Dislike button. + Disable dislike redirection + Nonaktifkan gesekan untuk mengubah trek di miniplayer. + Nonaktifkan gerakan miniplayer + Nonaktifkan usap untuk mengubah trek di pemutar. + Menonaktifkan gerakan pemutar + Mengatur warna bilah navigasi menjadi hitam. + Aktifkan bilah navigasi hitam + Changes the player background color to black. + Enable black player background + Mencocokkan warna pemutar layar penuh dengan yang diperkecil. + Aktifkan pencocokan warna pemutar + "Aktifkan dialog ringkas di ponsel. + +Masalah yang diketahui: +• Gambar album di Tab library juga menjadi lebih kecil. +• Tata letak pengatur waktu tidur mungkin terlihat tidak biasa." + Aktifkan dialog ringkas + Includes the buffer in the debug log. + Enable debug buffer logging + Mencetak catatan debug. + Aktifkan pencatatan debug + Mempertahankan pemutar agar tetap diminimalkan secara permanen meskipun trek lain diputar. + Aktifkan pemutar yang diminimalkan paksa + Mengaktifkan masuk ke mode lanskap dengan rotasi layar di ponsel. + Aktifkan mode lanskap + Adds a next track button to the miniplayer. + Add miniplayer next button + Adds a previous track button to the miniplayer. + Add miniplayer previous button + "Mengaktifkan codec audio opus alih-alih codec audio mp4a." + Aktifkan codec opus + Enables swipe down to dismiss miniplayer. + Enable swipe to dismiss miniplayer + "Menambahkan tombol Trim silence ke menu flyout playback speed. + +Info: +• Fitur ini hanya untuk podcast. +• Fitur ini masih dalam pengembangan, jadi ini tidak akan stabil." + Tambah switch Trim silence + Also enables Zen mode for podcasts. + Enable Zen mode in podcasts + Menambahkan rona abu-abu ke pemutar video untuk mengurangi ketegangan mata. + Aktifkan mode zen + Mulai ulang untuk memuat layout secara normal + Refresh dan mulai ulang + Export settings to file + Failed to export settings. + Settings were successfully exported. + Impor + Import settings from file + Salin + Import / Export settings as text + Impor atau ekspor setelan sebagai teks. + Ekspor / Impor + Import failed: %s. + Reset setelan ke default. + Setelan %d diimpor. + Reset + ReVanced Extended + "Tombol Unduh membuka Downloader eksternal kamu. + +• Hanya menggantikan tombol Unduh di player. +• Tidak bisa menggantikan tombol Unduh di menu flyout atau tab Library." + Ganti tombol tindakan Unduh + Downloader eksternal + "%1$s belum terinstall. +Download %2$s dari website." + Peringatan + %s tidak diinstal. Silakan instal. + Nama paket aplikasi downloader eksternal yang terinstal, seperti NewPipe atau YTDLnis. + Nama paket downloader eksternal + Menyembunyikan komponen kosong di menu akun. + Sembunyikan komponen kosong + Daftar dari nama-nama menu akun ke filter, terpisah oleh garis baru. + Filter menu Akun + Menyembunyikan elemen menu akun menggunakan filter custom. + Sembunyikan menu akun + Menyembunyikan tombol Simpan. + Sembunyikan tombol Save + Menyembunyikan tombol Komentar. + Sembunyikan tombol Komentar + Menyembunyikan tombol Unduh. + Sembunyikan tombol Unduh + Menyembunyikan bilah dari tombol tindakan. + Sembunyikan tombol bilah tindakan + Menyembunyikan tombol Like dan Dislike. Itu tidak akan bekerja di layout player lama. + Sembunyikan tombol Like dan Dislike + Menyembunyikan tombol Radio. + Sembunyikan tombol Radio + Menyembunyikan tombol Bagikan. + Sembunyikan tombol Bagikan + Hides the Audio / Video toggle in the player. + Hide Audio / Video toggle + Menyembunyikan rak tombol dari beranda dan eksplorasi. + Sembunyikan rak tombol + Menyembunyikan rak korsel dari beranda dan eksplorasi. + Sembunyikan rak korsel + Menyembunyikan tombol cast. + Sembunyikan tombol cast + Menyembunyikan bilah kategori musik di bagian atas beranda. + Sembunyikan bilah kategori + Hides the channel guidelines at the top of the comments section. + Hide channel guidelines + Hides the timestamp and emoji buttons when typing comments. + Hide timestamp and emoji buttons + Menyembunyikan overlay gelap yang muncul ketika double-tap to seek. + Sembunyikan filter overlay double-tap + Hides the floating button in the Library tab. + Hide floating button + Sembunyikan komponen 3-kolom + Sembunyikan menu tambahkan ke antrean + Sembunyikan menu teks + Sembunyikan menu hapus playlist + Sembunyikan menu abaikan antrean + Sembunyikan menu Unduh + Sembunyikan menu edit playlist + Sembunyikan menu Pergi ke album + Sembunyikan menu Pergi ke artis + Sembunyikan menu Pergi ke episode + Sembunyikan menu Pergi ke podcast + Sembunyikan menu bantuan & saran + Sembunyikan tombol Like dan Dislike + Sembunyikan menu putar berikutnya + Hide menu Kualitas + Sembunyikan menu hapus dari koleksi + Sembunyikan hapus dari menu playlist + Sembunyikan menu laporkan + Sembunyikan menu simpan episode untuk ditonton nanti + Sembunyikan menu simpan ke koleksi + Sembunyikan menu simpan ke playlist + Sembunyikan menu bagikan + Sembunyikan menu putar acak + Sembunyikan menu waktu tidur + Sembunyikan menu mulai radio + Sembunyikan menu statistik untuk nerds + Sembunyikan menu Subscribe / Unsubscribe + Sembunyikan menu kredit lagu + "Menyembunyikan iklan fullscreen." + Sembunyikan iklan fullscreen + Hides the Share button in the fullscreen player. + Hide fullscreen Share button + Menyembunyikan Iklan Umum. + Sembunyikan Iklan Umum + Menyembunyikan handle di menu akun. + Sembunyikan handle + Menyembunyikan tombol riwayat di toolbar. + Sembunyikan tombol riwayat + Menyembunyikan iklan sebelum memutar musik. + Sembunyikan iklan musik + Sembunyikan bilah navigasi. + Menyembunyikan bilah navigasi + Hides the Explore button. + Hide Explore button + Hides the Home button. + Hide Home button + Menyembunyikan label di bilah navigasi. + Sembunyikan label navigasi + Hides the Library button. + Hide Library button + Hides the Samples button. + Hide Samples button + Hides the Upgrade button. + Hide Upgrade button + Hides the Notifications button in the toolbar. + Hide Notifications button + Menyembunyikan label promosi berbayar. + Sembunyikan label promosi berbayar + Hides the playlist card shelf in the feed. + Hide playlist card shelf + Menyembunyikan popup promosi premium. + Sembunyikan popup promosi premium + Menyembunyikan banner pembaruan premium. + Sembunyikan banner pembaruan premium + Hides the Samples shelf in the feed. + Hide Samples shelf + "Sembunyikan elemen menu pengaturan. +Ini tidak hanya menyembunyikan menu pengaturan YT Music, tetapi juga menu pengaturan ReVanced Extended." + Sembunyikan menu pengaturan + Hides the sound search button in the search bar. + Hide sound search button + Hides the \'Tap to update\' button. + Hide \'Tap to update\' button + Menyembunyikan kontainer ketentuan layanan. + Sembunyikan kontainer ketentuan + Hides the voice search button in the search bar. + Hide voice search button + Akun + Bilah Tindakan + Iklan + Menu flyout + Umum + Miscellaneous + Bilah Navigasi + Player + Return YouTube Dislike + SponsorBlock + Video + Remembers the last playback speed selected. + Remember playback speed changes + Menunjukkan toast ketika mengubah playback speed semula. + Tampilkan toast + Changing default speed to %s. + Mengingat keadaan pengulangan. + Ingat keadaan pengulangan + Mengingat keadaan pengacakan. + Ingat keadaan pengacakan + Remembers the last video quality selected. + Remember video quality changes + Menunjukkan toast ketika mengubah playback speed semula. + Tampilkan toast + Changing default mobile data quality to %s. + Failed to set quality. + Changing default Wi-Fi quality to %s. + "Removes the viewer discretion dialog. +This does not bypass the age restriction. It just accepts it automatically." + Remove viewer discretion dialog + Melanjutkan video dari waktu saat ini ketika berlaih ke YouTube. + Lanjutkan menonton + Menggantikan menu hapus antrean menjadi tonton di YouTube. + Ganti menu hapus antrean + Tonton di YouTube + Url video tidak valid. + Mempertahankan menu laporkan di bagian komentar. + Simpan laporkan di komentar + Menggantikan menu laporkan dengan menu Kecepatan pemutaran. + Ganti menu laporkan + Returns the comments popup panels to the old style. + Restore old comments popup panels + Returns the player background to the old style. + Restore old player background + "Returns the player layout to the old style. +Some features may not work properly in the old player layout." + Restore old player layout + Returns the Library tab to the old style. (Experimental) + Restore old style library shelf + Tentang + Data disediakan oleh API Return YouTube Dislike. Tekan di sini untuk mempelajari lebih lanjut. + ReturnYouTubeDislike.com + Menyembunyikan pemisah tombol like. + Tombol like ringkas + Alih-alih jumlah dislike, yang ditampilkan adalah persentase dislike. + Dislike sebagai persentase + Menunjukkan jumlah dislike pada video. + Enable Return YouTube Dislike + Dislike tidak tersedia (batas API client tercapai). + Dislikes are unavailable (status %d). + Dislikes are temporarily unavailable (API timed out). + Dislikes are unavailable (%s). + Shows a toast if the Return YouTube Dislike API is unavailable. + Show a toast if API is unavailable + Menghapus parameter kueri pelacakan dari URL saat membagikan tautan. + Sanitasi tautan berbagi + About + sponsor.ajay.app + Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. + Change API URL + API URL changed. + API URL is invalid. + API URL reset. + The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. + Color changed. + Color: + Invalid color code. Color reset to default. + Color reset. + Change segment behavior + Enable SponsorBlock + SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. + Reset color + Filler Tangent / Jokes + Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. + Interaction Reminder (Subscribe) + A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. + Intermission / Intro Animation + An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. + Music: Non-Music Section + Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. + Endcards / Credits + Credits or when the YouTube endcards appear. Not for conclusions with information. + Preview / Recap / Hook + Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. + Unpaid / Self Promotion + Similar to \'Sponsor\' except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. + Sponsor + Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. + Skip automatically + Disable + Skipped filler. + Skipped annoying reminder. + Skipped intro. + Skipped intermission. + Skipped intermission. + Skipped multiple segments. + Skipped a non-music section. + Skipped outro. + Skipped preview. + Skipped recap. + Skipped preview. + Skipped self promotion. + Skipped sponsor. + SponsorBlock is temporarily unavailable. + SponsorBlock is temporarily unavailable (status %d). + SponsorBlock is temporarily unavailable (API timed out). + Show a toast if API is unavailable + Shows a toast if the SponsorBlock API is unavailable. + Show a toast when skipping automatically + Shows a toast when a segment is automatically skipped. + Setelan disalin ke papan klip. + "Memalsukan versi klien ke versi lama + +• Ini akan mengubah tampilan aplikasi, namun efek samping yang tidak diketahui mungkin terjadi. +• Jika nanti dinonaktifkan, UI lama mungkin tetap ada hingga aplikasi dihapus data." + 4.27.53 - Nonaktifkan mode radio di wilayah Kanada + 6.11.52 - Matikan Lirik real-time + Pilih target pemalsuan versi aplikasi. + Target pemalsuan versi aplikasi + Palsukan versi aplikasi + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/in/missing_strings.xml b/patches/src/main/resources/music/translations/in/missing_strings.xml new file mode 100644 index 000000000..7e6233426 --- /dev/null +++ b/patches/src/main/resources/music/translations/in/missing_strings.xml @@ -0,0 +1,73 @@ + + + Don\'t show again + To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. + Open default app settings + Disables Cairo splash animation when the app starts up. + Disable Cairo splash animation + Disables DRC (Dynamic Range Compression) applied to audio. + Disable DRC audio + Reset to default values. + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + Hide Pin to Speed dial menu + Hide Unpin from Speed dial menu + Hides the promotion alert banner. + Hide promotion alert banner + Hide About menu + Hide Data saving menu + Hide Downloads & storage menu + Hide General menu + Hide Notifications menu + Hide Get Music premium menu + Hide Family Center menu + Hide Playback menu + Hide Privacy & data menu + Hide Recommendations menu + Return YouTube Username + Settings menu + @handle (Username) + Select the username display format. + Display format + Username (@handle) + Username + Replaces handles with usernames in comments. + Enable Return YouTube Username + "A YouTube Data API v3 Developer Key is required to replace handles with usernames. + +The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. + +Click to see how to issue a API key." + About YouTube Data API key + The developer key for using the YouTube Data API v3. + YouTube Data API key + 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. + Issue YouTube Data API v3 developer key + Shows the estimated like count of videos. + Show estimated likes + Hidden + 7.16.53 - Restore old action bar + "Spoof the client to prevent playback issues. + +※ When used with 'Spoofing streaming data', playback issues may occur." + Spoof client + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + Android TV + Android VR + iOS + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/in/strings.xml b/patches/src/main/resources/music/translations/in/strings.xml new file mode 100644 index 000000000..3e9c64d8b --- /dev/null +++ b/patches/src/main/resources/music/translations/in/strings.xml @@ -0,0 +1,356 @@ + + + Continue + "GmsCore does not have permission to run in the background. + +Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. + +This is required for the app to work." + "GmsCore battery optimizations must be disabled to prevent issues. + +Tap on the continue button and disable battery optimizations." + Open website + Action needed + Enable cloud messaging to receive notifications. + Open GmsCore + GmsCore is not installed. Install it. + Mengganti domain yang ke blokir di negara tertentu sehingga playlist thumbnail, channel avatar, dll bisa di terima. + Bypass gambar larangan wilayah + Mengubah dari lembar berbagi dalam aplikasi ke lembar berbagi sistem. + Ubah lembar berbagi + Charts + Jelajahi + Beranda + Koleksi + Berlangganan + Select which page the app opens in. + Ganti Halaman Awal + Memfilter nama komponen dengan baris yang dipisahkan. + Edit filter kustom + Mengaktifkan filter kustom untuk menyembunyikan komponen tata letak. + Aktifkan filter kustom + Invalid custom filter: %s. + Kecepatan pemutaran kustom tidak valid. Atur ulang ke nilai default. + Invalid custom playback speeds. Using default values. + Menambah atau mengubah kecepatan pemutaran yang tersedia. + Edit kecepatan pemutaran kustom + Teks otomatis paksa yang dinonaktifkan. + Nonaktifkan teks otomatis paksa + Disables redirection to the next track when clicking the Dislike button. + Disable dislike redirection + Nonaktifkan gesekan untuk mengubah trek di miniplayer. + Nonaktifkan gerakan miniplayer + Nonaktifkan usap untuk mengubah trek di pemutar. + Menonaktifkan gerakan pemutar + Mengatur warna bilah navigasi menjadi hitam. + Aktifkan bilah navigasi hitam + Changes the player background color to black. + Enable black player background + Mencocokkan warna pemutar layar penuh dengan yang diperkecil. + Aktifkan pencocokan warna pemutar + "Aktifkan dialog ringkas di ponsel. + +Masalah yang diketahui: +• Gambar album di Tab library juga menjadi lebih kecil. +• Tata letak pengatur waktu tidur mungkin terlihat tidak biasa." + Aktifkan dialog ringkas + Includes the buffer in the debug log. + Enable debug buffer logging + Mencetak catatan debug. + Aktifkan pencatatan debug + Mempertahankan pemutar agar tetap diminimalkan secara permanen meskipun trek lain diputar. + Aktifkan pemutar yang diminimalkan paksa + Mengaktifkan masuk ke mode lanskap dengan rotasi layar di ponsel. + Aktifkan mode lanskap + Adds a next track button to the miniplayer. + Add miniplayer next button + Adds a previous track button to the miniplayer. + Add miniplayer previous button + "Mengaktifkan codec audio opus alih-alih codec audio mp4a." + Aktifkan codec opus + Enables swipe down to dismiss miniplayer. + Enable swipe to dismiss miniplayer + "Menambahkan tombol Trim silence ke menu flyout playback speed. + +Info: +• Fitur ini hanya untuk podcast. +• Fitur ini masih dalam pengembangan, jadi ini tidak akan stabil." + Tambah switch Trim silence + Also enables Zen mode for podcasts. + Enable Zen mode in podcasts + Menambahkan rona abu-abu ke pemutar video untuk mengurangi ketegangan mata. + Aktifkan mode zen + Mulai ulang untuk memuat layout secara normal + Refresh dan mulai ulang + Export settings to file + Failed to export settings. + Settings were successfully exported. + Impor + Import settings from file + Salin + Import / Export settings as text + Impor atau ekspor setelan sebagai teks. + Ekspor / Impor + Import failed: %s. + Reset setelan ke default. + Setelan %d diimpor. + Reset + ReVanced Extended + "Tombol Unduh membuka Downloader eksternal kamu. + +• Hanya menggantikan tombol Unduh di player. +• Tidak bisa menggantikan tombol Unduh di menu flyout atau tab Library." + Ganti tombol tindakan Unduh + Downloader eksternal + "%1$s belum terinstall. +Download %2$s dari website." + Peringatan + %s tidak diinstal. Silakan instal. + Nama paket aplikasi downloader eksternal yang terinstal, seperti NewPipe atau YTDLnis. + Nama paket downloader eksternal + Menyembunyikan komponen kosong di menu akun. + Sembunyikan komponen kosong + Daftar dari nama-nama menu akun ke filter, terpisah oleh garis baru. + Filter menu Akun + Menyembunyikan elemen menu akun menggunakan filter custom. + Sembunyikan menu akun + Menyembunyikan tombol Simpan. + Sembunyikan tombol Save + Menyembunyikan tombol Komentar. + Sembunyikan tombol Komentar + Menyembunyikan tombol Unduh. + Sembunyikan tombol Unduh + Menyembunyikan bilah dari tombol tindakan. + Sembunyikan tombol bilah tindakan + Menyembunyikan tombol Like dan Dislike. Itu tidak akan bekerja di layout player lama. + Sembunyikan tombol Like dan Dislike + Menyembunyikan tombol Radio. + Sembunyikan tombol Radio + Menyembunyikan tombol Bagikan. + Sembunyikan tombol Bagikan + Hides the Audio / Video toggle in the player. + Hide Audio / Video toggle + Menyembunyikan rak tombol dari beranda dan eksplorasi. + Sembunyikan rak tombol + Menyembunyikan rak korsel dari beranda dan eksplorasi. + Sembunyikan rak korsel + Menyembunyikan tombol cast. + Sembunyikan tombol cast + Menyembunyikan bilah kategori musik di bagian atas beranda. + Sembunyikan bilah kategori + Hides the channel guidelines at the top of the comments section. + Hide channel guidelines + Hides the timestamp and emoji buttons when typing comments. + Hide timestamp and emoji buttons + Menyembunyikan overlay gelap yang muncul ketika double-tap to seek. + Sembunyikan filter overlay double-tap + Hides the floating button in the Library tab. + Hide floating button + Sembunyikan komponen 3-kolom + Sembunyikan menu tambahkan ke antrean + Sembunyikan menu teks + Sembunyikan menu hapus playlist + Sembunyikan menu abaikan antrean + Sembunyikan menu Unduh + Sembunyikan menu edit playlist + Sembunyikan menu Pergi ke album + Sembunyikan menu Pergi ke artis + Sembunyikan menu Pergi ke episode + Sembunyikan menu Pergi ke podcast + Sembunyikan menu bantuan & saran + Sembunyikan tombol Like dan Dislike + Sembunyikan menu putar berikutnya + Hide menu Kualitas + Sembunyikan menu hapus dari koleksi + Sembunyikan hapus dari menu playlist + Sembunyikan menu laporkan + Sembunyikan menu simpan episode untuk ditonton nanti + Sembunyikan menu simpan ke koleksi + Sembunyikan menu simpan ke playlist + Sembunyikan menu bagikan + Sembunyikan menu putar acak + Sembunyikan menu waktu tidur + Sembunyikan menu mulai radio + Sembunyikan menu statistik untuk nerds + Sembunyikan menu Subscribe / Unsubscribe + Sembunyikan menu kredit lagu + "Menyembunyikan iklan fullscreen." + Sembunyikan iklan fullscreen + Hides the Share button in the fullscreen player. + Hide fullscreen Share button + Menyembunyikan Iklan Umum. + Sembunyikan Iklan Umum + Menyembunyikan handle di menu akun. + Sembunyikan handle + Menyembunyikan tombol riwayat di toolbar. + Sembunyikan tombol riwayat + Menyembunyikan iklan sebelum memutar musik. + Sembunyikan iklan musik + Sembunyikan bilah navigasi. + Menyembunyikan bilah navigasi + Hides the Explore button. + Hide Explore button + Hides the Home button. + Hide Home button + Menyembunyikan label di bilah navigasi. + Sembunyikan label navigasi + Hides the Library button. + Hide Library button + Hides the Samples button. + Hide Samples button + Hides the Upgrade button. + Hide Upgrade button + Hides the Notifications button in the toolbar. + Hide Notifications button + Menyembunyikan label promosi berbayar. + Sembunyikan label promosi berbayar + Hides the playlist card shelf in the feed. + Hide playlist card shelf + Menyembunyikan popup promosi premium. + Sembunyikan popup promosi premium + Menyembunyikan banner pembaruan premium. + Sembunyikan banner pembaruan premium + Hides the Samples shelf in the feed. + Hide Samples shelf + "Sembunyikan elemen menu pengaturan. +Ini tidak hanya menyembunyikan menu pengaturan YT Music, tetapi juga menu pengaturan ReVanced Extended." + Sembunyikan menu pengaturan + Hides the sound search button in the search bar. + Hide sound search button + Hides the \'Tap to update\' button. + Hide \'Tap to update\' button + Menyembunyikan kontainer ketentuan layanan. + Sembunyikan kontainer ketentuan + Hides the voice search button in the search bar. + Hide voice search button + Akun + Bilah Tindakan + Iklan + Menu flyout + Umum + Miscellaneous + Bilah Navigasi + Player + Return YouTube Dislike + SponsorBlock + Video + Remembers the last playback speed selected. + Remember playback speed changes + Menunjukkan toast ketika mengubah playback speed semula. + Tampilkan toast + Changing default speed to %s. + Mengingat keadaan pengulangan. + Ingat keadaan pengulangan + Mengingat keadaan pengacakan. + Ingat keadaan pengacakan + Remembers the last video quality selected. + Remember video quality changes + Menunjukkan toast ketika mengubah playback speed semula. + Tampilkan toast + Changing default mobile data quality to %s. + Failed to set quality. + Changing default Wi-Fi quality to %s. + "Removes the viewer discretion dialog. +This does not bypass the age restriction. It just accepts it automatically." + Remove viewer discretion dialog + Melanjutkan video dari waktu saat ini ketika berlaih ke YouTube. + Lanjutkan menonton + Menggantikan menu hapus antrean menjadi tonton di YouTube. + Ganti menu hapus antrean + Tonton di YouTube + Url video tidak valid. + Mempertahankan menu laporkan di bagian komentar. + Simpan laporkan di komentar + Menggantikan menu laporkan dengan menu Kecepatan pemutaran. + Ganti menu laporkan + Returns the comments popup panels to the old style. + Restore old comments popup panels + Returns the player background to the old style. + Restore old player background + "Returns the player layout to the old style. +Some features may not work properly in the old player layout." + Restore old player layout + Returns the Library tab to the old style. (Experimental) + Restore old style library shelf + Tentang + Data disediakan oleh API Return YouTube Dislike. Tekan di sini untuk mempelajari lebih lanjut. + ReturnYouTubeDislike.com + Menyembunyikan pemisah tombol like. + Tombol like ringkas + Alih-alih jumlah dislike, yang ditampilkan adalah persentase dislike. + Dislike sebagai persentase + Menunjukkan jumlah dislike pada video. + Enable Return YouTube Dislike + Dislike tidak tersedia (batas API client tercapai). + Dislikes are unavailable (status %d). + Dislikes are temporarily unavailable (API timed out). + Dislikes are unavailable (%s). + Shows a toast if the Return YouTube Dislike API is unavailable. + Show a toast if API is unavailable + Menghapus parameter kueri pelacakan dari URL saat membagikan tautan. + Sanitasi tautan berbagi + About + sponsor.ajay.app + Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. + Change API URL + API URL changed. + API URL is invalid. + API URL reset. + The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. + Color changed. + Color: + Invalid color code. Color reset to default. + Color reset. + Change segment behavior + Enable SponsorBlock + SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. + Reset color + Filler Tangent / Jokes + Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. + Interaction Reminder (Subscribe) + A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. + Intermission / Intro Animation + An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. + Music: Non-Music Section + Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. + Endcards / Credits + Credits or when the YouTube endcards appear. Not for conclusions with information. + Preview / Recap / Hook + Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. + Unpaid / Self Promotion + Similar to \'Sponsor\' except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. + Sponsor + Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. + Skip automatically + Disable + Skipped filler. + Skipped annoying reminder. + Skipped intro. + Skipped intermission. + Skipped intermission. + Skipped multiple segments. + Skipped a non-music section. + Skipped outro. + Skipped preview. + Skipped recap. + Skipped preview. + Skipped self promotion. + Skipped sponsor. + SponsorBlock is temporarily unavailable. + SponsorBlock is temporarily unavailable (status %d). + SponsorBlock is temporarily unavailable (API timed out). + Show a toast if API is unavailable + Shows a toast if the SponsorBlock API is unavailable. + Show a toast when skipping automatically + Shows a toast when a segment is automatically skipped. + Setelan disalin ke papan klip. + "Memalsukan versi klien ke versi lama + +• Ini akan mengubah tampilan aplikasi, namun efek samping yang tidak diketahui mungkin terjadi. +• Jika nanti dinonaktifkan, UI lama mungkin tetap ada hingga aplikasi dihapus data." + 4.27.53 - Nonaktifkan mode radio di wilayah Kanada + 6.11.52 - Matikan Lirik real-time + Pilih target pemalsuan versi aplikasi. + Target pemalsuan versi aplikasi + Palsukan versi aplikasi + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/it-rIT/missing_strings.xml b/patches/src/main/resources/music/translations/it-rIT/missing_strings.xml new file mode 100644 index 000000000..51963ea66 --- /dev/null +++ b/patches/src/main/resources/music/translations/it-rIT/missing_strings.xml @@ -0,0 +1,372 @@ + + + Continue + Don\'t show again + "GmsCore does not have permission to run in the background. + +Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. + +This is required for the app to work." + "GmsCore battery optimizations must be disabled to prevent issues. + +Disabling battery optimizations for GmsCore will not negatively affect battery usage. + +Tap the continue button and allow optimization changes." + Open website + Action needed + Enable cloud messaging to receive notifications. + Open GmsCore + GmsCore is not installed. Install it. + Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. + Bypass image region restrictions + Change from in-app share sheet to system share sheet. + Change share sheet + Charts + Explore + Home + Library + Subscriptions + Select which page the app opens in. + Change start page + Invalid custom filter: %s. + Invalid custom playback speeds. + To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. + Open default app settings + Disables Cairo splash animation when the app starts up. + Disable Cairo splash animation + Disables redirection to the next track when clicking the Dislike button. + Disable dislike redirection + Disables DRC (Dynamic Range Compression) applied to audio. + Disable DRC audio + Disable swipe to change tracks in the miniplayer. + Disable miniplayer gesture + Disable swipe to change tracks in the player. + Disable player gesture + Changes the player background color to black. + Enable black player background + "Enables the compact flyout menu on phones. + +Limitations: +• Album art in the Library tab becomes smaller when organized in a grid. +• Sleep timer layout may appear unusual." + Includes the buffer in the debug log. + Enable debug buffer logging + Adds a next track button to the miniplayer. + Add miniplayer next button + Adds a previous track button to the miniplayer. + Add miniplayer previous button + Enables swipe down to dismiss miniplayer. + Enable swipe to dismiss miniplayer + "Adds a Trim silence switch to the playback speed flyout menu. + +Info: +• This feature is for podcasts. +• This feature is still in development, so it may be unstable." + Add Trim silence switch + Also enables Zen mode for podcasts. + Enable Zen mode in podcasts + Reset to default values. + Restart to load the layout normally + Refresh and restart + Export settings to file + Failed to export settings. + Settings were successfully exported. + Import settings from file + Import / Export settings as text + Import failed: %s. + Reset + ReVanced Extended + "Download button opens your external downloader. + +• Only overrides the Download action button in the player. +• Does not override the Download button in the flyout menu or Library tab." + Override Download action button + External downloader + "%1$s is not installed. +Please download %2$s from the website." + Warning + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + List of account menu names to filter, separated by new lines. + Account menu filter + Hides the Save button. + Hide Save button + Hides the Comments button. + Hide Comments button + Hides the Download button. + Hide Download button + Hides the labels of the action buttons. + Hide action button labels + Hides the Like and Dislike buttons. It does not work in the old player layout. + Hide Like and Dislike buttons + Hides the Radio button. + Hide Radio button + Hides the Share button. + Hide Share button + Hides the Audio / Video toggle in the player. + Hide Audio / Video toggle + Hide carousel shelf + Hides the category bar. + Hide category bar + Hides the channel guidelines at the top of the comments section. + Hide channel guidelines + Hides the timestamp and emoji buttons when typing comments. + Hide timestamp and emoji buttons + Hides dark overlay that appears when double-tapping to seek. + Hide double-tap overlay filter + Hides the floating button in the Library tab. + Hide floating button + Hide 3-column component + Hide Add to queue menu + Hide Captions menu + Hide Delete playlist menu + Hide Dismiss queue menu + Hide Download menu + Hide Edit playlist menu + Hide Go to album menu + Hide Go to artist menu + Hide Go to episode menu + Hide Go to podcast menu + Hide Help & feedback menu + Hide Like and Dislike buttons + Hide Pin to Speed dial menu + Hide Play next menu + Hide Quality menu + Hide Remove from library menu + Hide Remove from playlist menu + Hide Report menu + Hide Save episode for later menu + Hide Save to library menu + Hide Save to playlist menu + Hide Share menu + Hide Shuffle play menu + Hide Sleep timer menu + Hide Start radio menu + Hide Stats for nerds menu + Hide Subscribe / Unsubscribe menu + Hide Unpin from Speed dial menu + Hide View song credits menu + "Hides fullscreen ads. + +Limitations: +• Sometimes you may see a blank black screen instead of the home feed." + Hide fullscreen ads + Hides the Share button in the fullscreen player. + Hide fullscreen Share button + Hides general ads. + Hide general ads + Hides the handle in the account menu. + Hide handle + Hides the History button in the toolbar. + Hide History button + Hides ads before playing media. + Hides the navigation bar. + Hide navigation bar + Hides the Explore button. + Hide Explore button + Hides the Home button. + Hide Home button + Hides labels below the navigation buttons. + Hides the Library button. + Hide Library button + Hides the Samples button. + Hide Samples button + Hides the Upgrade button. + Hide Upgrade button + Hides the Notifications button in the toolbar. + Hide Notifications button + Hides the paid promotion label. + Hide paid promotion label + Hides the playlist card shelf in the feed. + Hide playlist card shelf + Hides premium promotion popups. + Hide premium promotion popups + Hides the premium renewal banner. + Hide premium renewal banner + Hides the promotion alert banner. + Hide promotion alert banner + Hides the Samples shelf in the feed. + Hide Samples shelf + Hide About menu + Hide Data saving menu + Hide Downloads & storage menu + Hide General menu + Hide Notifications menu + Hide Get Music premium menu + Hide Family Center menu + Hide Playback menu + Hide Privacy & data menu + Hide Recommendations menu + "Hide elements of the settings menu. +This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." + Hide settings menu + Hides the sound search button in the search bar. + Hide sound search button + Hides the Tap to update button. + Hide Tap to update button + Hides the voice search button in the search bar. + Hide voice search button + Account + Action Bar + Ads + Flyout Menu + General + Miscellaneous + Navigation Bar + Player + Return YouTube Username + Return YouTube Dislike + SponsorBlock + Settings menu + Video + Remembers the last playback speed selected. + Remember playback speed changes + Show a toast when changing the default playback speed. + Show a toast + Changing default speed to %s. + Remembers the last video quality selected. + Remember video quality changes + Show a toast when changing the default video quality. + Show a toast + Changing default mobile data quality to %s. + Failed to set quality. + Changing default Wi-Fi quality to %s. + "Removes the viewer discretion dialog. +This does not bypass the age restriction. It just accepts it automatically." + Remove viewer discretion dialog + Continues the video from the current time when switching to YouTube. + Continue watching + Replaces the Dismiss queue menu with the Watch on YouTube menu. + Replace Dismiss queue menu + Watch on YouTube + Invalid video url. + Keeps the Report menu in the comments section intact. + Keep Report in comments + Replaces the Report menu with the Playback speed menu. + Replace Report menu + Returns the comments popup panels to the old style. + Restore old comments popup panels + Returns the player background to the old style. + Restore old player background + "Returns the player layout to the old style. +Some features may not work properly in the old player layout." + Restore old player layout + Returns the Library tab to the old style. (Experimental) + Restore old style library shelf + @handle (Username) + Select the username display format. + Display format + Username (@handle) + Username + Replaces handles with usernames in comments. + Enable Return YouTube Username + "A YouTube Data API v3 Developer Key is required to replace handles with usernames. + +The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. + +Click to see how to issue a API key." + About YouTube Data API key + The developer key for using the YouTube Data API v3. + YouTube Data API key + 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. + Issue YouTube Data API v3 developer key + ReturnYouTubeDislike.com + Enable Return YouTube Dislike + Shows the estimated like count of videos. + Show estimated likes + Dislikes are unavailable (status %d). + Dislikes are temporarily unavailable (API timed out). + Dislikes are unavailable (%s). + Shows a toast if the Return YouTube Dislike API is unavailable. + Show a toast if API is unavailable + Hidden + Removes tracking query parameters from URLs when sharing links. + Sanitize sharing links + About + sponsor.ajay.app + Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. + Change API URL + API URL changed. + API URL is invalid. + API URL reset. + The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. + Color changed. + Color: + Invalid color code. + Color reset. + Change segment behavior + Enable SponsorBlock + SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. + Reset color + Filler Tangent / Jokes + Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. + Interaction Reminder (Subscribe) + A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. + Intermission / Intro Animation + An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. + Music: Non-Music Section + Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. + Endcards / Credits + Credits or when the YouTube endcards appear. Not for conclusions with information. + Preview / Recap / Hook + Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. + Unpaid / Self Promotion + Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. + Sponsor + Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. + Skip automatically + Disable + Skipped filler. + Skipped annoying reminder. + Skipped intro. + Skipped intermission. + Skipped intermission. + Skipped multiple segments. + Skipped a non-music section. + Skipped outro. + Skipped preview. + Skipped recap. + Skipped preview. + Skipped self promotion. + Skipped sponsor. + SponsorBlock is temporarily unavailable. + SponsorBlock is temporarily unavailable (status %d). + SponsorBlock is temporarily unavailable (API timed out). + Show a toast if API is unavailable + Shows a toast if the SponsorBlock API is unavailable. + Show a toast when skipping automatically + Shows a toast when a segment is automatically skipped. + Settings copied to clipboard. + "Spoofs the client version to an older version. + +• This will change the appearance of the app, but unknown side effects may occur. +• If later disabled, the old UI may remain until the app data is cleared." + 4.27.53 - Disable Radio mode in Canadian regions + 6.11.52 - Disable real-time lyrics + 7.16.53 - Restore old action bar + Select the spoof app version target. + Spoof app version target + "Spoof the client to prevent playback issues. + +※ When used with 'Spoofing streaming data', playback issues may occur." + Spoof client + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + Android TV + Android VR + iOS + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/it-rIT/strings.xml b/patches/src/main/resources/music/translations/it-rIT/strings.xml new file mode 100644 index 000000000..b61dc80be --- /dev/null +++ b/patches/src/main/resources/music/translations/it-rIT/strings.xml @@ -0,0 +1,62 @@ + + + Filtra i nomi dei componenti separati da righe. + Modifica i filtri personalizzati + Abilita il filtro personalizzato per nascondere i componenti del layout. + Abilita filtri personalizzati + Velocità di riproduzione personalizzate non valide. Ripristina i valori predefiniti. + Aggiungi o modifica le velocità di riproduzione disponibili + Modifica velocità di riproduzione personalizzate + Sottotitoli automatici forzati disabilitati. + Disabilita i sottotitoli automatici forzati + Imposta il colore della barra di navigazione su nero. + Abilita la barra di navigazione nera + Allinea il colore del lettore a schermo intero con quello in secondo piano. + Abilita l\'abbinamento di colore dei Riproduttori + Abilita dialogo compatto + Stampa il registro di debug. + Abilita la registrazione del debug + Mantieni il riproduttore in secondo piano anche se un\'altra traccia viene riprodotta. + Abilita il riproduttore in secondo piano forzato + Consente l\'accesso alla modalità orizzontale ruotando lo schermo del telefono. + Abilita la modalità orizzontale + "Abilita il codec Opus 250/251 durante la riproduzione dell'audio." + Abilita il codec opus + Aggiunge una sfumatura grigia al riproduttore video per ridurre l\'affaticamento degli occhi. + Abilita la modalità zen + Importa + Copia + Importa o esporta le impostazioni come testo. + Importa/Esporta + Ripristino impostazioni ai valori predefiniti + Impostazioni %d importate + %s non è installato. Installalo. + Nome del pacchetto dell\'app downloader esterna installata, come NewPipe o Seal. + Nome del pacchetto downloader esterno + Nasconde i componenti vuoti nel menu dell\'account + Nascondi componente vuoto + Nascondi gli elementi del menu dell\'account. + Nascondi il menu dell\'account + Nasconde lo scaffale dei pulsanti dalla home page e da Explorer. + Nasconde lo scaffale dei pulsanti + Nasconde lo scaffale del carosello dalla home page e da Explorer. + Nascondo il pulsante cast nella parte superiore della homepage e in cima al riproduttore. + Nascondi il bottone cast + Nascondi le pubblicità musicali + Nascondi etichetta di navigazione + Nasconde il contenitore dei termini di servizio. + Nascondi contenitore termini + Ricorda lo stato della ripetizione. + Ricorda lo stato di ripetizione + Ricorda lo stato della riproduzione casuale. + Ricorda lo stato della riproduzione casuale + Informazioni + I dati vengono forniti dall\'API Return YouTube Dislike. Tocca qui per saperne di più. + Nasconde il separatore del pulsante \"Mi piace\". + Pulsante \"Mi piace\" compatto + Al posto del numero di \"Non mi piace\", viene mostrata la percentuale dei Non mi piace. + \"Non mi piace\" in percentuale + Mostra il numero di \"Non mi piace\" dei video. + \"Non mi piace\" non disponibile (limite API client raggiunto) + Versione dell\'app falsificata + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/ja-rJP/missing_strings.xml b/patches/src/main/resources/music/translations/ja-rJP/missing_strings.xml new file mode 100644 index 000000000..3339728c3 --- /dev/null +++ b/patches/src/main/resources/music/translations/ja-rJP/missing_strings.xml @@ -0,0 +1,22 @@ + + + Disables DRC (Dynamic Range Compression) applied to audio. + Hide Pin to Speed dial menu + Hide Unpin from Speed dial menu + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/ja-rJP/strings.xml b/patches/src/main/resources/music/translations/ja-rJP/strings.xml new file mode 100644 index 000000000..935992d6f --- /dev/null +++ b/patches/src/main/resources/music/translations/ja-rJP/strings.xml @@ -0,0 +1,406 @@ + + + 続行 + 今後表示しない + "MicroG GmsCoreはバックグラウンドで実行する権限がありません。\n\nあなたの端末の \"Don't kill my app \"のガイドに従って、MicroGのインストールに適用してください。\n\nこれはアプリが動作するために必要です。" + "問題を防ぐためにGmsCoreのバッテリー最適化を無効にする必要があります。 + +「続行」をタップし、バッテリーの最適化を無効にします。" + ウェブサイトを開く + 操作が必要です + 通知を受け取るには、Cloud Messaging 設定を有効にしてください。 + GmsCoreを開く + GmsCoreがインストールされていません。インストールしてください。 + プレイリストのサムネイルやチャンネルアバターなどを受信できるように、一部の地域でブロックされているドメインを置き換えます。 + 画像表示の地域制限を回避 + アプリ内共有メニューからシステムの共有メニューに置き換えます。 + 共有メニューを変更 + チャート + 探索 + ホーム + ライブラリ + 定期購入 + アプリのスタートページを変更します。 + スタートページを変更 + コンポーネント名でフィルター (改行区切り) + カスタムフィルターを編集 + カスタムフィルターを有効にします。 + カスタムフィルター + 無効なカスタムフィルターです: %s。 + 無効なカスタム再生速度です。デフォルト値にリセットします。 + 無効なカスタム再生速度です。デフォルトの値を使用します。 + 利用可能な再生速度を編集します。 + カスタム再生速度の編集 + RVX Music でYouTube Music のURLを開くには、「対応リンクを開く」を有効にし、サポートされているURLを有効にします。 + 「デフォルトで開く」の設定 + 動画側で設定されている、字幕の強制は無効です。 + 字幕の強制を無効化 + アプリ起動時のCairo のスプラッシュアニメーションを無効にします。 + Cairo スプラッシュアニメーションを無効にする + 低評価ボタンを押したとき、次の曲へのリダイレクトするのを無効にする。 + 低評価リダイレクトを無効化 + DRCオーディオを無効にする + ミニプレーヤーでスワイプによる曲の変更を無効にします + ミニプレーヤージェスチャーを無効にする + プレイヤーでスワイプによる曲の変更を無効にします。 + プレイヤージェスチャーを無効にする + ナビゲーションバーの色を黒に設定します。 + 黒いナビゲーションバーを有効化 + プレイヤーの背景の色を黒に固定します。 + 黒のプレイヤー背景を有効化 + ミニプレーヤーと全画面プレーヤーの色を統一します。 + カラーマッチプレーヤーを有効化 + "コンパクトなダイアログを有効にします。 + +既知の問題: +• ライブラリのアルバムアートが小さくなります。 +• スリープタイマーのレイアウトが異常になる場合があります。" + コンパクトなダイアログ + デバッグログをバッファに含めて出力する。 + デバッグバッファログを有効化 + デバッグログを出力します。 + デバッグログ + 他のトラックが再生されていても、プレーヤーを常に最小化したままにします。 + 最小化されたプレーヤーを有効にする + 画面回転で横画面モードに入るようにします。 + 横画面モードを有効化 + ミニプレーヤーの「次の曲に進むボタン」を表示します。 + 「次の曲に進むボタン」を表示 + ミニプレーヤーで「前の曲に戻るボタン」を表示します。 + 「前の曲に戻るボタン」を表示 + "MP4A コーデックの代わりに、Opus コーデックを適用します。" + Opus コーデックを有効化 + 下にスワイプしてミニプレーヤーを閉じられるようにします。 + スワイプしてミニプレーヤーを閉じる + "再生スピードのフライアウトメニューで「無音トリム」スイッチを有効にする。 + +情報 +- この機能はポッドキャスト用です。 +- この機能はまだ開発中のため、不安定な場合があります。" + 「無音トリム」を有効化 + ポッドキャストにもZenモードを適用します。 + ポッドキャストでZenモードを有効化 + 動画プレーヤーに灰色の色合いを追加し、目の疲れを軽減します。 + Zen モードを有効化 + デフォルト値にリセット。 + 再起動してレイアウトを正常に読み込みます + 再起動して更新 + 設定をファイルにエクスポート + 設定のエクスポートに失敗しました。 + 設定は正常にエクスポートされました。 + インポート + ファイルから設定をインポート + コピー + テキストとしてインポート/エクスポート + 設定をテキストとしてインポート/エクスポートします。 + 設定のインポート/エクスポート + インポートに失敗: %s + 設定をデフォルトにリセットしました。 + %d の設定をインポートしました。 + リセット + ReVanced Extended + "ダウンロードボタンをでのダウンロードを外部のアプリで行います。 + +• プレーヤー内のダウンロードボタンのみを置き換えます。 +• フライアウトメニューまたはライブラリのダウンロードボタンは置き換えません。" + ダウンロードボタンを置き換える + 外部ダウンローダーを選択 + "%1$s はインストールされていません。 +ウェブサイトから %2$s をダウンロードしてください。" + 警告 + %s はインストールされていません。インストールしてください。 + NewPipe や YTDLnis などの、インストールされている外部ダウンローダーアプリのパッケージ名。 + 外部ダウンローダーのパッケージ名 + アプリの起動時に、GMSCore 最適化ダイアログを表示します。 + GMSCore の最適化ダイアログを表示 + アカウントメニューの空のコンポーネントを非表示にします。 + 空のコンポーネントを非表示 + フィルタリングするメニュー名(改行区切り) + アカウントメニューフィルター + アカウントメニューの要素を非表示にします。 + アカウントメニューを非表示 + プレイリストに追加ボタンを非表示にします。 + プレイリストに追加ボタンを非表示 + コメントボタンを非表示にします。 + コメントボタンを非表示 + ダウンロードボタンを非表示にします。 + ダウンロードボタンを非表示 + アクションボタンのラベルを非表示にします。 + アクションボタンのラベルを非表示 + 高評価ボタンや低評価ボタンを非表示にします。古いプレイヤーのレイアウトでは動作しません。 + 評価ボタンを非表示 + ラジオボタンを非表示にします。 + ラジオボタンを非表示 + 共有ボタンを非表示にします。 + 共有ボタンを非表示 + プレイヤーの曲と動画の切り替えスイッチを非表示にします。 + 曲と動画の切り替えスイッチを非表示 + ホームタブや探索タブのボタン欄を非表示にします。 + ボタン欄を非表示 + ホームタブや探索タブのカルーセル欄を非表示にします。 + カルーセル欄を非表示 + キャストボタンを非表示にします。 + キャストボタンを非表示 + ホームタブの上部にある音楽カテゴリーバーを非表示にします。 + カテゴリーバーを非表示 + コメント欄上部のコミュニティガイドラインを非表示にします。 + コミュニティガイドラインを非表示 + コメントを入力するときにタイムスタンプと絵文字ボタンを非表示にします。 + タイムスタンプと絵文字ボタンを非表示 + ダブルタップしてシークすると表示される暗いオーバーレイを非表示にします。 + ダブルタップオーバーレイフィルタを非表示 + ライブラリのフローティングボタンを非表示にします。 + フローティングボタンを非表示 + 3点メニューのコンポーネントを非表示 + 「キューに追加」を非表示 + 字幕メニューを非表示にする + プレイリスト削除メニューを非表示 + 「キューを閉じる」を非表示 + 「オフラインに一時保存」を非表示 + プレイリスト編集メニューを非表示 + 「アルバムに移動」を非表示 + 「アーティストに移動」を非表示 + 「エピソードに移動」を非表示 + 「ポッドキャストに移動」を非表示 + 「ヘルプとフィードバック」を非表示 + 高評価/低評価ボタンを非表示 + 「次に再生」を非表示 + 画質メニューを非表示 + 「ライブラリから削除」を非表示 + 「プレイリストから削除」を非表示 + 「報告」を非表示 + 「エピソードを保存」を非表示 + 「ライブラリに保存」を非表示 + 「再生リストに保存」を非表示 + 「共有」を非表示 + シャッフル再生メニューを非表示 + スリープタイマーメニューを非表示 + 「ラジオを聴く」を非表示 + 統計情報を非表示 + 登録/解除メニューを非表示 + 「曲のクレジットを表示」を非表示 + "全画面広告を非表示にします。" + 全画面広告を非表示 + 全画面表示のプレイヤーの共有ボタンを非表示にします。 + 全画面共有ボタンを非表示 + 一般広告を非表示にします。 + 一般広告を非表示 + アカウントスイッチャーでハンドルを非表示にします。 + ハンドルを非表示 + ツールバーの履歴ボタンを非表示にします。 + 履歴ボタンを非表示 + トラックを再生する前の広告を非表示にします。 + 音楽の広告を非表示 + ナビゲーションバーを非表示にします。 + ナビゲーションバーを非表示 + 探索ボタンを非表示にします。 + 探索ボタンを非表示 + ホームボタンを非表示にします。 + ホームボタンを非表示 + ナビゲーションバーのラベルを非表示にします。 + ナビゲーションバーのラベルを非表示 + ライブラリボタンを非表示にします。 + ライブラリボタンを非表示 + サンプルボタンを非表示にします。 + サンプルボタンを非表示 + アップグレードボタンを非表示にします。 + アップグレードボタンを非表示 + ツールバーの通知ボタンを非表示にします。 + 通知ボタンを非表示 + 有料プロモーションラベルを非表示にします。 + 有料プロモーションバナーを非表示 + プレイリストシェルフを非表示にします。 + プレイリストシェルフを非表示 + プレミアムプロモーションポップアップを非表示にします。 + プレミアムプロモーションポップアップを非表示 + プレミアム更新バナーを非表示にします。 + プレミアム更新バナーを非表示 + プロモーションバナーを非表示にします。 + プロモーションバナーを非表示 + フィードからサンプルシェルフを非表示にします。 + サンプルシェルフを非表示 + 「YouTube Music について」を非表示 + 「データの節約」を非表示 + 「一時保存とストレージ」を非表示 + 「全般」を非表示 + 「通知」を非表示 + 「Music Premium の購入」を非表示 + 「ファミリーセンター」を非表示 + 「再生」を非表示 + 「プライバシーとデータ」を非表示 + 「おすすめ」を非表示 + "設定の要素を非表示にします。 +YT Music の設定だけでなく、ReVanced Extended の設定も非表示にします。" + 設定メニューを非表示 + 検索バーのサウンドサーチボタンを非表示にします。 + サウンドサーチボタンを非表示 + 「タップして更新」ボタンを非表示にします。 + 「タップして更新」ボタンを非表示 + 利用規約コンテナーを非表示にします。 + 利用規約を非表示 + 検索バーのボイスサーチボタンを非表示にします。 + ボイスサーチボタンを非表示 + アカウント + アクションバー + 広告 + フライアウトメニュー + 全般 + その他 + ナビゲーションバー + プレーヤー + Return YouTube Username + Return YouTube Dislike + SponsorBlock + 設定メニュー + 動画 + 再生速度を変更するたびに、再生速度を保存します。 + 再生速度の変更を保存 + デフォルトの再生速度を変更するときにトーストを表示します。 + トーストを表示 + デフォルトの再生速度を %s に変更しました。 + リピートの状態を記憶します。 + リピートの状態を保存 + シャッフルの状態を記憶します。 + シャッフルの状態を保存 + 画質を変更するたびに、画質を保存します。 + ビデオ画質の変更を保存 + デフォルトの画質を変更するときにトーストを表示します。 + トーストを表示 + モバイルネットワーク使用時のデフォルト画質を %s に変更しました。 + 画質の設定に失敗しました。 + Wi-Fi 使用時のデフォルト画質を %s に変更しました。 + "視聴者の裁量ダイアログを削除します。 +これは年齢制限を回避するものではなく、自動的に受け入れられるだけです。" + ビューアの裁量ダイアログを削除 + YouTubeに切り替えたときに、現在の時間から再生します。 + 視聴を続ける + 「キューを閉じる」を「YouTube で視聴」に置き換えます。 + 「キューを閉じる」メニューを置き換え + YouTube で視聴 + 動画のURLが無効です。 + コメントのレポート メニューは置き換えられません。 + プレイヤーのフライアウトメニューにのみ適用 + 「報告」を「再生速度」に置き換えます。 + 「報告」を置き換え + コメントポップアップパネルを古いスタイルに戻します。 + 古いコメントポップアップパネルを有効化 + プレイヤーの背景を古いスタイルに戻します。 + 古いプレイヤーの背景を有効化 + "プレイヤーのレイアウトを古いスタイルに戻します。 +一部の機能は正しく動作しない可能性があります。" + 古いプレーヤーのレイアウト + ライブラリのUIを古いスタイルに戻します (実験的) + 古いスタイルのライブラリを有効化 + \@ハンドルネーム(ユーザーネーム) + ユーザーネームの表示形式を選択します。 + 表示形式 + ユーザーネーム (@ハンドルネーム) + ユーザーネーム + コメント中のハンドルネームをユーザー名に置き換えます。 + Return YouTube Username を有効化 + "ハンドルネームをユーザーネームに置き換えるには、YouTube Data API v3 の開発者キーが必要です。 + +無料プランの API キーの1日の割り当ては 10,000 で、コメント1件につき1つの割り当てが使用されます。 + +API キーの発行方法については、ここをタップしてください。" + YouTube Data API キーについて + YouTube Data API v3 を使用するための開発者キー。 + YouTube Data API キー + 1. 「<a href=%1$s>新しいプロジェクトの作成</a>」に移動します。<br>2. 「<b>作成</b>」をタップします。<br>3. 「<a href=%2$s>YouTube Data API v3</a>」に移動します。<br>4. 「<b>有効にする</b>」をタップします。<br>5. 「<b>認証情報を作成</b>」をタップします。<br>6. 「<b>一般公開データ</b>」オプションを選択します。<br>7. 「<b>次へ</b>」をタップします。<br>8. API キーをコピーします。<br><br>※API キーは他人と共有してはならないため、インポート/エクスポート設定には含まれません。 + YouTube Data API v3 開発者キーの発行 + Return YouTube Dislike について + 低評価のデータは、Return YouTube Dislike API によって提供されています。詳細はここをタップしてください。 + ReturnYouTubeDislike.com + 高評価ボタンの区切りを非表示にします。 + コンパクトな高評価ボタン + 低評価はパーセンテージで表示されます。 + 低評価数の形式の切り替え + 低評価は数字で表示されます。 + Return YouTube Dislike を有効化 + 動画の推定高評価数を表示します。 + 推定の高評価数を表示 + 低評価数は利用できません (クライアント API 制限) + 低評価数は一時的に利用できません。(ステータス %d) + 低評価数は一時的に利用できません。(API タイムアウト) + 低評価数は一時的に利用できません。(%s) + RYDが利用できない場合、メッセージが表示されます。 + API が利用できない場合にメッセージを表示 + 非表示 + リンクを共有する際に、URL からトラッキングクエリパラメーターを削除します。 + 共有リンクのクリーンアップ + この機能について + sponsor.ajay.app + データは SponsorBlock API によって提供されています。他のプラットフォームのダウンロードや詳細については、ここをタップしてください。 + APIのURLを変更 + APIのURLを変更しました。 + APIのURLが無効です。 + APIのURLをリセットしました。 + SponsorBlock がサーバーとの通信で使うアドレスです。自分が何をしているのか理解していない場合は、変更しないでください。 + 色を変更しました。 + 色: + カラーコードが無効です。既定の値に戻します。 + 色をリセットしました。 + セグメントの設定 + Sponsor Block を有効化 + SponsorBlock は、YouTube の動画の迷惑な部分をスキップするためのクラウドソーシングシステムです。 + 色をリセット + 繋ぎの話 / 冗談 + 動画の本編を理解するのに必要のない繋ぎの話やユーモアなどの逸脱したシーン。コンテクストや背景情報の詳細は含まれません。 + リマインダー(チャンネル登録などの催促) + 動画の途中に挿入される高評価、チャンネル登録、フォローなどを促す短いリマインダーは、長いものや何か具体的なものは「セルフプロモーション」に分類するべきです。 + 休憩 / イントロアニメーション + 本編ではない部分。一時停止、静止画面、アニメーションの繰り返しが含まれます。情報を含んだ転換画面は含まれません。 + MV: 音楽ではない区間 + ミュージックビデオでのみ使用できます。他のカテゴリーに含まれていない、ミュージックビデオの音楽のない区間。 + エンドカード / クレジット + クレジットや動画のエンドカードが表示されている場面。情報を含む結論は含まれません。 + 予告 / 要約 / フック + この動画やシリーズの他の動画で起きた、または今後起きる内容などをまとめたクリップのコレクション。すべての情報は、別の場所で繰り返し表示されます。 + 無報酬 / セルフプロモーション + 無報酬のプロモーションあるいはセルフプロモーションであるという点を除いては「スポンサー」と同様です。商品、寄付、コラボ情報に関する内容を含みます。 + スポンサー + 有料プロモーション、有料紹介、直接広告が含まれます。セルフプロモーションや、個人の好きなクリエイター/ウェブサイト/商品に対する無償の活動は含まれません。 + 自動的にスキップ + 無効 + 繋ぎの話をスキップしました。 + リマインダーをスキップしました。 + イントロをスキップしました。 + 休憩をスキップしました。 + 休憩をスキップしました。 + 複数のセグメントをスキップしました + 音楽ではない区間をスキップしました。 + アウトロをスキップしました。 + 予告をスキップしました。 + 要約をスキップしました。 + 予告をスキップしました。 + セルフプロモーションをスキップしました。 + スポンサーをスキップしました。 + SponsorBlock は一時的に利用できません。 + SponsorBlock は一時的に利用できません。(ステータス %d) + SponsorBlock は一時的に利用できません。(API タイムアウト) + API が利用できない場合にメッセージを表示 + SponsorBlock API が利用できない場合、メッセージが表示されます。 + 自動的にスキップする時にトーストを表示する + セグメントが自動的にスキップされたときにトーストを表示します。 + 設定をクリップボードにコピーしました。 + "YT Music のバージョンを古いバージョンに偽装します。 + +• これによりアプリの外観が変わりますが、未知の問題が発生する場合があります。 +• 後からこの機能を無効にしても、データを消去するまで古い UI のままになる場合があります。" + 4.27.53 - カナダの地域でラジオモードを無効化 + 6.11.52 - リアルタイムの歌詞を無効化 + 7.16.53 - 古いアクションバーを復元 + 偽装するバージョンを選択してください。 + 偽装するバージョン + アプリのバージョンを偽装 + "再生の問題を防ぐためにクライアントを偽装します + +制限事項 +• OPUSオーディオコーデックはサポートされていない可能性があります。 +• シークバーのサムネイルが表示されない場合があります。 +• 再生履歴はブランドアカウントでは動作しません。" + クライアントを偽装 + Android TV + Android VR + iOS + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/ko-rKR/strings.xml b/patches/src/main/resources/music/translations/ko-rKR/strings.xml new file mode 100644 index 000000000..6a745525f --- /dev/null +++ b/patches/src/main/resources/music/translations/ko-rKR/strings.xml @@ -0,0 +1,434 @@ + + + 계속하기 + 다시 보지 않기 + "GmsCore에 백그라운드에서 실행할 수 있는 권한이 없습니다. + +이 기기에 대한 \"Don't kill my app\" 가이드를 읽어보고, GmsCore 설치 지침을 적용하세요. + +앱을 실행하려면 이 과정이 필요합니다." + "GmsCore를 배터리 최적화 목록에서 제외하여 앱 문제를 방지할 수 있습니다. + +배터리 최적화 목록에서 제외하려면 '계속하기' 버튼을 누르세요." + 웹사이트 열기 + 필수 조치 + 알림 수신을 위한 클라우드 메시징 설정을 할 수 있습니다. + GmsCore 열기 + GmsCore가 설치되어 있지 않습니다. 설치하세요. + 이미지 도메인을 변경하여 일부 국가에서 차단된 재생목록 썸네일, 채널 프로필 사진, 커뮤니티 게시물 이미지 등을 수신할 수 있습니다. + 이미지 표시 제한 국가 우회 + YT Music 기본 공유 시트에서 Android 기본 공유 시트로 변경합니다.\n\n• 공유 버튼으로 바로 Android 기본 공유 메뉴를 실행할 수 있습니다. + 공유 시트 변경 + 차트 + 둘러보기 + + 보관함 + 구독 + 앱 시작 페이지를 변경합니다. + 앱 시작 페이지 변경 + 필터링할 구성요소를 줄바꿈으로 구분하여 설정합니다. + 사용자 정의 필터 + 사용자 정의 필터를 활성화하여 레이아웃 구성요소를 숨깁니다. + 사용자 정의 필터 활성화 + 잘못된 필터 값입니다: %s + 사용자 정의 재생 속도는 %s배속보다 작아야 합니다. + 잘못된 재생 속도 값입니다. + 사용하고 싶은 재생 속도 값을 추가하거나 변경할 수 있습니다. + 사용자 정의 재생 속도 편집 + YT Music 링크를 RVX Music으로 열려면 \'지원되는 링크 열기\'를 활성화하고 지원되는 링크를 추가하세요. 링크 추가가 잠겨있다면 순정 YT Music 앱 정보 → \'기본적으로 열기\'에서 \'지원되는 링크 열기\'를 비활성화한 후에 추가할 수 있습니다. + 기본 앱 설정 열기 + 자막이 자동으로 활성화되지 않도록 설정합니다. + 자동 자막 비활성화 + 앱을 시작할 때, Cairo 스플래시 애니메이션을 비활성화합니다. + Cairo 스플래시 애니메이션 비활성화 + \'싫어요 버튼을 누르면 다음 트랙으로 리다이렉션\'을 비활성화합니다. + 싫어요 리다이렉션 비활성화 + 오디오에 적용된 DRC (Dynamic Range Compression)를 비활성화합니다. + DRC 오디오 비활성화 + 미니 플레이어에서 \'스와이프 제스처로 트랙 변경\'을 비활성화합니다. + 미니 플레이어 제스처 비활성화 + 플레이어에서 \'스와이프 제스처로 트랙 변경\'을 비활성화합니다. + 플레이어 제스처 비활성화 + 하단바 색상을 검정으로 설정합니다. + 검정 하단바 활성화 + 플레이어 배경 색상을 검정으로 설정합니다. + 검정 플레이어 배경 활성화 + 최소화 상태의 플레이어와 전체 화면 플레이어의 색상을 통일시킵니다. + 색상 일치 플레이어 활성화 + "휴대폰에서 소형 메뉴 구성요소를 활성화합니다. + +알려진 문제점: +• 보관함 탭에서 앨범 아트가 그리드로 구성될 때 작아집니다. +• 취침 타이머 레이아웃이 비정상적으로 보일 수 있습니다." + 소형 다이얼로그 활성화 + 디버그 로그에 버퍼를 포함하여 출력합니다. + 디버그 버퍼 로깅 활성화 + 디버그 로그를 출력합니다. + 디버그 로깅 활성화 + 다른 트랙이 재생되더라도 플레이어를 항상 최소화 상태로 유지합니다. + 플레이어를 항상 최소화 상태로 유지 + 앱을 가로로 회전할 수 있도록 합니다. + 가로 모드 활성화 + 미니 플레이어에서 다음 버튼을 활성화합니다. + 미니 플레이어에서 다음 버튼 활성화 + 미니 플레이어에서 이전 버튼을 활성화합니다. + 미니 플레이어에서 이전 버튼 활성화 + "플레이어 응답에 OPUS 코덱이 포함된 경우에는 OPUS 코덱을 활성화합니다. + +알림: +• 최신 YT Music 클라이언트는 기본적으로 OPUS 코덱을 사용합니다. +• 이 설정은 아주 오래된 클라이언트 사용자에게만 유효합니다." + OPUS 코덱 활성화 + 아래로 스와이프하여 미니 플레이어 닫기를 활성화합니다. + 스와이프하여 미니 플레이어 닫기 활성화 + "재생 속도 메뉴 구성요소에 '무음 건너뛰기' 스위치를 추가합니다. + +알림: +• 팟캐스트 기능입니다. +• 이 기능은 아직 개발 중이므로 불안정할 수 있습니다." + 무음 건너뛰기 스위치 추가 + 팟캐스트에서 집중 모드를 활성화합니다. + 팟캐스트에서 집중 모드 활성화 + 동영상 플레이어의 색상을 회색조로 설정해 눈의 피로를 줄입니다. + 집중 모드 활성화 + 기본값으로 초기화합니다. + 레이아웃을 정상적으로 불러오기 위해 다시 시작합니다. + 새로고침 및 다시 시작 + 파일로 설정 내보내기 + 설정을 내보내는 데 실패하였습니다. + 설정을 성공적으로 내보냈습니다. + 가져오기 + 파일에서 설정 가져오기 + 복사하기 + 텍스트로 설정 가져오기 / 내보내기 + 설정을 가져오거나 내보낼 수 있습니다. + 설정 가져오기 / 내보내기 + 가져오기를 실패하였습니다: %s + 설정을 기본값으로 초기화합니다. + %d 설정을 가져왔습니다. + 초기화 + ReVanced Extended 설정 + "오프라인 저장 버튼으로 외부 다운로더 앱을 실행할 수 있습니다. + +• 플레이어 하단에 있는 오프라인 저장 버튼만 재정의할 수 있습니다. +• 메뉴 구성요소 또는 보관함에서는 오프라인 저장 버튼을 재정의할 수 없습니다." + 오프라인 저장 버튼 재정의 + 외부 다운로더 앱 + "%1$s 가 설치되어 있지 않습니다. +웹사이트에서 %2$s 를 다운로드하세요." + 경고 + %s가 설치되지 않았습니다. 설치해주세요. + NewPipe 또는 YTDLnis와 같은 설치된 외부 다운로더 앱 패키지명입니다. + 외부 다운로더 앱 패키지명 + 앱을 시작할 때마다 GmsCore에 대한 배터리 최적화 다이얼로그를 표시합니다. + GmsCore 배터리 최적화 다이얼로그 표시 + 계정 메뉴에서 비어있는 구성요소를 숨깁니다. + 비어있는 구성요소 제거 + 필터링할 계정 메뉴 이름 목록을 줄바꿈으로 구분하여 설정합니다. + 계정 메뉴 필터 + 사용자 정의 필터를 사용하여 계정 메뉴 구성요소를 숨깁니다. + 계정 메뉴 제거 + (재생목록에) 저장 버튼을 숨깁니다. + (재생목록에) 저장 버튼 제거 + 댓글 버튼을 숨깁니다. + 댓글 버튼 제거 + 오프라인 저장 버튼을 숨깁니다. + 오프라인 저장 버튼 제거 + 액션 버튼에서 라벨을 숨깁니다. + 액션 버튼 라벨 제거 + 좋아요 & 싫어요 버튼을 숨깁니다. \n이전 플레이어 레이아웃에서는 작동하지 않습니다. + 좋아요 & 싫어요 버튼 제거 + 뮤직 스테이션 버튼을 숨깁니다. + 뮤직 스테이션 버튼 제거 + 공유 버튼을 숨깁니다. + 공유 버튼 제거 + 플레이어에서 \'노래↔동영상\' 전환 토글을 숨깁니다. + \'노래↔동영상\' 전환 토글 제거 + 피드에서 버튼형 선반을 숨깁니다. + 버튼형 선반 제거 + 피드에서 좌우 슬라이드형 선반을 숨깁니다. + 좌우 슬라이드형 선반 제거 + 크롬캐스트 버튼을 숨깁니다. + 크롬캐스트 버튼 제거 + 카테고리 바를 숨깁니다. + 카테고리 바 제거 + 댓글 섹션 상단에서 커뮤니티 가이드라인을 숨깁니다. + 커뮤니티 가이드라인 제거 + 댓글을 입력할 때, 타임스탬프 및 이모지 버튼을 숨깁니다. + 타임스탬프, 이모지 버튼 제거 + 두 번 눌러서 탐색할 때 표시되는 어두운 오버레이를 숨깁니다. + 두 번 누르기 오버레이 필터 + 보관함에서 플로팅 버튼을 숨깁니다. + 플로팅 버튼 제거 + 3-열 구성요소 제거 + 현재 재생목록에 추가 메뉴 제거 + 자막 메뉴 제거 + 재생목록 삭제 메뉴 제거 + 현재 재생목록 닫기 메뉴 제거 + 오프라인 저장 메뉴 제거 + 재생목록 수정 메뉴 제거 + 앨범으로 이동 메뉴 제거 + 아티스트 페이지로 이동 메뉴 제거 + 에피소드로 이동 메뉴 제거 + 팟캐스트로 이동 메뉴 제거 + 고객센터 메뉴 제거 + 좋아요 & 싫어요 버튼 제거 + 빠른 선곡에 고정 제거 + 다음에 재생 메뉴 제거 + 품질 메뉴 제거 + 보관함에서 삭제 메뉴 제거 + 재생목록에서 삭제 메뉴 제거 + 신고 메뉴 제거 + 나중에 볼 에피소드 저장 메뉴 제거 + 보관함에 저장 메뉴 제거 + 재생목록에 저장 메뉴 제거 + 공유 메뉴 제거 + 셔플 재생 메뉴 제거 + 취침 타이머 메뉴 제거 + 뮤직 스테이션 시작 메뉴 제거 + 동영상 통계 메뉴 제거 + 구독 / 구독 취소 메뉴 제거 + 빠른 선곡에서 고정 해제 제거 + 노래 크레딧 보기 메뉴 제거 + "전체 화면 광고를 숨깁니다. + +알려진 문제점: +• 가끔씩 홈 피드 대신 검정 공백 화면이 표시될 수 있습니다." + 전체 화면 광고 제거 + 전체 화면에서 공유 버튼을 숨깁니다. + 전체 화면에서 공유 버튼 제거 + 일반 레이아웃 광고를 숨깁니다. + 일반 레이아웃 광고 제거 + 계정 메뉴에서 핸들(@사용자 아이디)을 숨깁니다. + 핸들(@사용자 아이디) 제거 + 툴바에서 최근 감상 기록 버튼을 숨깁니다. + 최근 감상 기록 버튼 제거 + 음악을 재생하기 전 광고를 숨깁니다. + 음악 광고 제거 + 하단바를 숨깁니다. + 하단바 제거 + 둘러보기 버튼을 숨깁니다. + 둘러보기 버튼 제거 + 홈 버튼을 숨깁니다. + 홈 버튼 제거 + 하단바에서 버튼 라벨을 숨깁니다. + 하단바 버튼 라벨 제거 + 보관함 버튼을 숨깁니다. + 보관함 버튼 제거 + 샘플 버튼을 숨깁니다. + 샘플 버튼 제거 + 업그레이드 버튼을 숨깁니다. + 업그레이드 버튼 제거 + 툴바에서 알림 버튼을 숨깁니다. + 알림 버튼 제거 + 유료 광고 포함 라벨을 숨깁니다. + 유료 광고 포함 라벨 제거 + 피드에서 재생목록 카드 선반을 숨깁니다. + 재생목록 카드 선반 제거 + YouTube Premium 팝업 광고를 숨깁니다. + YouTube Premium 팝업 광고 제거 + YouTube Premium 갱신 배너를 숨깁니다. + YouTube Premium 갱신 배너 제거 + 프로모션 알림 배너를 숨깁니다. + 프로모션 알림 배너 제거 + 피드에서 샘플 선반을 숨깁니다. + 샘플 선반 제거 + YouTube Music 정보 메뉴 숨기기 + 데이터 절약 메뉴 숨기기 + 오프라인 저장 및 저장용량 메뉴 숨기기 + 일반 메뉴 숨기기 + 알림 메뉴 숨기기 + Music Premium 가입 메뉴 숨기기 + 가족 센터 메뉴 숨기기 + 재생 메뉴 숨기기 + 개인 정보 보호 및 데이터 메뉴 숨기기 + 맞춤 콘텐츠 메뉴 숨기기 + "설정 메뉴 구성요소를 숨깁니다. +YT Music 설정 메뉴뿐만 아니라 ReVanced Extended 설정 메뉴도 숨겨집니다." + 설정 메뉴 제거 + 툴바에서 노래 검색 버튼을 숨깁니다. + 노래 검색 버튼 제거 + \'탭하여 업데이트\' 버튼을 숨깁니다. + \'탭하여 업데이트\' 버튼 제거 + 서비스 약관 컨테이너를 숨깁니다. + 서비스 약관 컨테이너 제거 + 툴바에서 음성 검색 버튼을 숨깁니다. + 음성 검색 버튼 제거 + 계정 + 액션바 + 광고 + 메뉴 구성요소 + 일반 + 기타 + 하단바 + 플레이어 + Return YouTube Username + Return YouTube Dislike + SponsorBlock + 설정 메뉴 + 동영상 + 재생 속도 값을 변경할 때마다 저장합니다. + 재생 속도 저장 활성화 + 기본 동영상 재생 속도 값으로 변경되었을 때, 팝업 메시지를 표시합니다. + 팝업 메시지 표시 + 기본 재생 속도 값을 %s으로 변경합니다. + 반복 재생 토글 상태를 저장합니다. + 반복 상태 저장 + 셔플 재생 토글 상태를 저장합니다. + 셔플 상태 저장 + 동영상 품질 값을 변경할 때마다 저장합니다. + 동영상 품질 저장 활성화 + 기본 동영상 화질 값으로 변경되었을 때, 팝업 메시지를 표시합니다. + 팝업 메시지 표시 + 모바일 네트워크 이용 시 기본 동영상 품질 값을 %s로 변경합니다. + 동영상 품질을 설정할 수 없습니다. + Wi-Fi 이용 시 기본 동영상 품질 값을 %s로 변경합니다. + "시청 경고 다이얼로그를 제거합니다. + +이 설정은 다이얼로그를 자동으로 허용하기만 하며, 연령 제한(성인인증 절차)을 우회할 수 없습니다." + 시청 경고 다이얼로그 제거 + \'YouTube로 시청\' 메뉴를 누르면 YouTube로 변경하여 동영상을 현재 재생 시간부터 이어서 시청합니다. + 이어서 시청 + \'현재 재생목록 닫기\' 메뉴를 \'YouTube로 시청\' 메뉴로 변경합니다. + 현재 재생목록 닫기 메뉴 변경 + YouTube로 시청 + 잘못된 동영상 URL입니다. + 댓글 섹션에서 \'신고\' 메뉴를 그대로 유지합니다. + 댓글에서 신고 메뉴 유지 + \'신고\' 메뉴를 \'재생 속도\' 메뉴로 변경합니다. + 신고 메뉴 변경 + 이전 댓글 팝업 패널으로 복원합니다. + 이전 댓글 팝업 패널으로 복원 + 이전 플레이어 배경으로 복원합니다. + 이전 플레이어 배경으로 복원 + "이전 플레이어 레이아웃으로 복원합니다. +이전 플레이어 레이아웃에서 일부 기능이 제대로 작동하지 않을 수 있습니다." + 이전 플레이어 레이아웃으로 복원 + 이전 보관함 탭으로 복원합니다. (실험 기능) + 이전 보관함 선반으로 복원 + \@핸들 (사용자 이름) + 사용자 이름 표시 형식을 선택하세요. + 표시 형식 + 사용자 이름 (@핸들) + 사용자 이름 + 댓글에서 핸들(@사용자 아이디)이 아닌 사용자 이름을 표시합니다. + Return YouTube Username 활성화 + "핸들을 사용자 이름으로 변경하려면 YouTube Data API v3 Developer Key가 필요합니다. + +무료 요금제에서 API Key의 일일 할당량은 10,000개이며, 1개의 할당량은 댓글 1개에 대해 핸들을 사용자 이름으로 변경하는 데 사용됩니다. + +API Key를 발급받는 방법을 보려면 여기를 누르세요." + YouTube Data API Key에 대한 정보 + YouTube Data API v3를 사용하기 위한 Developer Key입니다. + YouTube Data API Key + 1. <a href=%1$s>새 프로젝트 만들기</a> 로 이동합니다.<br>2. <b>만들기</b> 버튼을 터치합니다.<br>3. <a href=%2$s>YouTube Data API v3</a> 로 이동합니다.<br>4. <b>사용</b> 버튼을 터치합니다.<br>5. <b>사용자 인증 정보 만들기</b> 버튼을 터치합니다.<br>6. <b>공개 데이터</b> 옵션을 선택합니다.<br>7. <b>다음</b> 버튼을 터치합니다.<br>8. API Key를 복사합니다.<br><br>※ API Key는 다른 사람과 공유해서는 안 되므로 가져오기 / 내보내기 설정에 포함되지 않습니다. + YouTube Data API v3 Developer Key 발급 + 정보 + 싫어요 수의 데이터는 Return YouTube Dislike API에 의해 제공됩니다. 자세한 내용을 보려면 여기를 누르세요. + ReturnYouTubeDislike.com + 좋아요 버튼에서 구분선을 숨깁니다. + 좋아요 버튼에서 구분선 제거 + 싫어요 수를 숫자가 아닌 퍼센트로 표시합니다. + 싫어요 수를 퍼센트로 표시 + 싫어요 수를 표시합니다. + Return YouTube Dislike 활성화 + 좋아요 수가 숨겨진 음악(동영상)에서 추정되는 좋아요 수를 표시합니다. + 추정되는 좋아요 수 표시 + 싫어요 수를 표시할 수 없습니다. (클라이언트 API 제한 도달) + 싫어요 수를 표시할 수 없습니다 (상태 코드: %d). + 싫어요 수를 일시적으로 표시할 수 없습니다 (응답 시간 초과). + 싫어요 수를 표시할 수 없습니다 (%s). + ReturnYouTubeDislike를 사용할 수 없을 때, 팝업 메시지를 표시합니다. + API를 사용할 수 없을 때 팝업 메시지 표시 + 숨겨짐 + 링크를 공유할 때, URL에서 추적 쿼리 매개변수를 제거합니다. + 추적 쿼리를 제거한 링크 공유 + 정보 + sponsor.ajay.app + 건너뛸 구간의 데이터는 SponsorBlock API에 의해 제공됩니다. 자세한 내용을 보려면 여기를 누르세요. + API URL 변경 + API URL을 변경하였습니다. + 잘못된 API URL입니다. + API URL을 초기화하였습니다. + SponsorBlock이 요청을 보낼 서버 URL입니다. 이것이 무슨 역할을 하는지 모르는 경우에는 이 URL을 변경하지 마세요. + 설정한 색상을 적용하였습니다. + 색상: + 잘못된 헥스 코드입니다. + 색상을 초기화하였습니다. + 각 구간에 설정할 동작 + SponsorBlock 활성화 + SponsorBlock은 YouTube 동영상 내 성가신 구간을 건너뛰게 해주는 크라우드소싱 시스템입니다. + 색상 초기화 + 주제와 관련 없는 구간 + 전반적인 동영상의 주제를 이해하는 데 필요 없는 내용을 포함하고 있습니다. + 상호 작용 요청 + 좋아요, 구독, 알림 설정을 요청하는 내용에 관한 구간입니다. + 무음 구간 / 인트로 + 아무 내용도 없는 구간입니다. 애니메이션이나 정적 프레임과 같은 내용을 포함하고 있습니다. + 음악이 아닌 구간 + 정식 음원이 아닌 동영상 음원에서 음악이 아닌 구간이 해당됩니다. + 최종 화면 / 크레딧 + 엔딩 크레딧이나 최종 화면이 나타나는 구간입니다. + 미리 보기 / 요약 / 흥미 유발 + 이전 에피소드를 간략히 요약하거나 현재 동영상의 하이라이트를 미리 보여줍니다. + 자체 홍보 구간 + \'스폰서 광고\' 구간과 비슷하지만, 자발적으로 홍보하는 내용을 포함하는 구간입니다. 채널 굿즈 광고, 기부 광고와 동영상에 참여한 사람들을 홍보하는 광고가 해당됩니다. + 스폰서 광고 + 유료 광고, 협찬과 같은 직/간접적인 광고 구간입니다. + 자동으로 건너뛰기 + 아무것도 하지 않기 + 주제와 관련 없는 구간을 건너뛰었습니다. + 상호 작용 요청을 건너뛰었습니다. + 인트로를 건너뛰었습니다. + 무음 구간을 건너뛰었습니다. + 무음 구간을 건너뛰었습니다. + 여러 구간을 건너뛰었습니다. + 음악이 아닌 구간을 건너뛰었습니다. + 최종 화면을 건너뛰었습니다. + 미리 보기를 건너뛰었습니다. + 요약을 건너뛰었습니다. + 미리 보기를 건너뛰었습니다. + 자체 홍보 구간을 건너뛰었습니다. + 스폰서 광고를 건너뛰었습니다. + SponsorBlock을 일시적으로 사용할 수 없습니다. + SponsorBlock을 일시적으로 사용할 수 없습니다 (상태 코드: %d). + SponsorBlock을 일시적으로 사용할 수 없습니다 (응답 시간 초과). + API를 사용할 수 없을 때, 팝업 메시지 표시 + SponsorBlock를 사용할 수 없을 때, 팝업 메시지를 표시합니다. + 자동으로 구간을 건너뛸 때, 팝업 메시지 표시 + 자동으로 구간을 건너뛸 때, 팝업 메시지를 표시합니다. + 설정을 클립보드에 복사하였습니다. + "이전 앱 버전으로 변경합니다. + +• 이 기능을 활성화하면 앱 레이아웃이 변경되지만 알려지지 않은 문제점이 발생할 수 있습니다. +• 나중에 이 기능을 비활성화하면 앱 데이터를 지우기 전까지 이전 레이아웃이 유지될 수 있습니다." + 4.27.53 - 캐나다 지역에서 뮤직 스테이션 모드를 비활성화합니다. + 6.11.52 - 실시간 가사를 비활성화합니다. + 7.16.53 - 이전 액션바로 복원합니다. + 변경할 앱 버전을 선택하세요. + 변경할 앱 버전 + 앱 버전 변경 + "클라이언트를 변경하여 재생 문제를 방지할 수 있습니다. + +※ '스트리밍 데이터 변경'과 함께 사용할 경우에 재생 문제가 발생할 수 있습니다." + 클라이언트 변경 + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "변경할 기본 클라이언트를 정의합니다. + +※ Android 클라이언트를 사용할 경우에 '앱 버전 변경'과 함께 사용하는 것을 권장합니다." + 기본 클라이언트 + \'스트리밍 데이터를 가져오는 데 사용되는 클라이언트\'가 동영상 통계에서 표시됩니다. + 동영상 통계에서 표시 + "스트리밍 데이터를 변경하여 재생 문제를 방지합니다. + +※ '클라이언트 변경'과 함께 사용할 경우에 재생 문제가 발생할 수 있습니다." + 스트리밍 데이터 변경 + Android TV + Android VR + iOS + iOS Music + 스트리밍 데이터를 가져오는 데 사용되는 기본 클라이언트를 정의할 수 있습니다. + 기본 클라이언트 + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/nl-rNL/missing_strings.xml b/patches/src/main/resources/music/translations/nl-rNL/missing_strings.xml new file mode 100644 index 000000000..a2b00b638 --- /dev/null +++ b/patches/src/main/resources/music/translations/nl-rNL/missing_strings.xml @@ -0,0 +1,239 @@ + + + Continue + Don\'t show again + "GmsCore does not have permission to run in the background. + +Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. + +This is required for the app to work." + "GmsCore battery optimizations must be disabled to prevent issues. + +Disabling battery optimizations for GmsCore will not negatively affect battery usage. + +Tap the continue button and allow optimization changes." + Open website + Action needed + Enable cloud messaging to receive notifications. + Open GmsCore + GmsCore is not installed. Install it. + Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. + Bypass image region restrictions + Change from in-app share sheet to system share sheet. + Change share sheet + Charts + Explore + Home + Library + Subscriptions + Select which page the app opens in. + Change start page + Invalid custom filter: %s. + Invalid custom playback speeds. + To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. + Open default app settings + Disables Cairo splash animation when the app starts up. + Disable Cairo splash animation + Disables redirection to the next track when clicking the Dislike button. + Disable dislike redirection + Disables DRC (Dynamic Range Compression) applied to audio. + Disable DRC audio + Disable swipe to change tracks in the miniplayer. + Disable miniplayer gesture + Disable swipe to change tracks in the player. + Disable player gesture + Changes the player background color to black. + Enable black player background + Includes the buffer in the debug log. + Enable debug buffer logging + Reset to default values. + Reset + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + Hides dark overlay that appears when double-tapping to seek. + Hide double-tap overlay filter + Hides the floating button in the Library tab. + Hide floating button + Hide Go to episode menu + Hide Go to podcast menu + Hide Help & feedback menu + Hide Pin to Speed dial menu + Hide Play next menu + Hide Quality menu + Hide Remove from library menu + Hide Remove from playlist menu + Hide Report menu + Hide Save episode for later menu + Hide Save to library menu + Hide Save to playlist menu + Hide Share menu + Hide Shuffle play menu + Hide Sleep timer menu + Hide Start radio menu + Hide Stats for nerds menu + Hide Subscribe / Unsubscribe menu + Hide Unpin from Speed dial menu + Hide View song credits menu + Hides the Notifications button in the toolbar. + Hide Notifications button + Hides the playlist card shelf in the feed. + Hide playlist card shelf + Hides the promotion alert banner. + Hide promotion alert banner + Hides the Samples shelf in the feed. + Hide Samples shelf + Hide About menu + Hide Data saving menu + Hide Downloads & storage menu + Hide General menu + Hide Notifications menu + Hide Get Music premium menu + Hide Family Center menu + Hide Playback menu + Hide Privacy & data menu + Hide Recommendations menu + "Hide elements of the settings menu. +This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." + Hide settings menu + Hide sound search button + Hides the Tap to update button. + Hide Tap to update button + General + Miscellaneous + Return YouTube Username + Return YouTube Dislike + SponsorBlock + Settings menu + Video + Remembers the last playback speed selected. + Remember playback speed changes + Show a toast when changing the default playback speed. + Show a toast + Changing default speed to %s. + Remembers the last video quality selected. + Remember video quality changes + Show a toast when changing the default video quality. + Show a toast + Changing default mobile data quality to %s. + Failed to set quality. + Changing default Wi-Fi quality to %s. + Continues the video from the current time when switching to YouTube. + Continue watching + Replaces the Dismiss queue menu with the Watch on YouTube menu. + Replace Dismiss queue menu + Watch on YouTube + Invalid video url. + Keeps the Report menu in the comments section intact. + Keep Report in comments + Replaces the Report menu with the Playback speed menu. + Replace Report menu + Returns the comments popup panels to the old style. + Restore old comments popup panels + Returns the player background to the old style. + Restore old player background + "Returns the player layout to the old style. +Some features may not work properly in the old player layout." + Restore old player layout + @handle (Username) + Select the username display format. + Display format + Username (@handle) + Username + Replaces handles with usernames in comments. + Enable Return YouTube Username + "A YouTube Data API v3 Developer Key is required to replace handles with usernames. + +The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. + +Click to see how to issue a API key." + About YouTube Data API key + The developer key for using the YouTube Data API v3. + YouTube Data API key + 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. + Issue YouTube Data API v3 developer key + ReturnYouTubeDislike.com + Enable Return YouTube Dislike + Shows the estimated like count of videos. + Show estimated likes + Shows a toast if the Return YouTube Dislike API is unavailable. + Show a toast if API is unavailable + Hidden + About + sponsor.ajay.app + Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. + Change API URL + API URL changed. + API URL is invalid. + API URL reset. + The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. + Color changed. + Color: + Invalid color code. + Color reset. + Change segment behavior + Enable SponsorBlock + SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. + Reset color + Filler Tangent / Jokes + Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. + Interaction Reminder (Subscribe) + A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. + Intermission / Intro Animation + An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. + Music: Non-Music Section + Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. + Endcards / Credits + Credits or when the YouTube endcards appear. Not for conclusions with information. + Preview / Recap / Hook + Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. + Unpaid / Self Promotion + Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. + Sponsor + Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. + Skip automatically + Disable + Skipped filler. + Skipped annoying reminder. + Skipped intro. + Skipped intermission. + Skipped intermission. + Skipped multiple segments. + Skipped a non-music section. + Skipped outro. + Skipped preview. + Skipped recap. + Skipped preview. + Skipped self promotion. + Skipped sponsor. + SponsorBlock is temporarily unavailable. + SponsorBlock is temporarily unavailable (status %d). + SponsorBlock is temporarily unavailable (API timed out). + Show a toast if API is unavailable + Shows a toast if the SponsorBlock API is unavailable. + Show a toast when skipping automatically + Shows a toast when a segment is automatically skipped. + 7.16.53 - Restore old action bar + "Spoof the client to prevent playback issues. + +※ When used with 'Spoofing streaming data', playback issues may occur." + Spoof client + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + Android TV + Android VR + iOS + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/nl-rNL/strings.xml b/patches/src/main/resources/music/translations/nl-rNL/strings.xml new file mode 100644 index 000000000..1190e8654 --- /dev/null +++ b/patches/src/main/resources/music/translations/nl-rNL/strings.xml @@ -0,0 +1,192 @@ + + + Componentnamen filteren op lijn spatie + Wijzig aangepaste filter + Aangepast filter inschakelen + Aangepast filter inschakelen + Ongeldige aangepaste afspeelsnelheden. Herstel naar standaardwaarden. + Voeg toe of verander de beschikbare afspeelsnelheden + Bewerk aangepaste afspeelsnelheden + Geforceerde automatische ondertitels uitschakelen. + Geforceerde automatische ondertitels uitschakelen + Zet de navigatie balk kleur naar zwart. + Activeer zwarte navigatie balk + Komt overeen met de kleur van de mini speler en de volschermspeler. + Kleuren overeenkomst van de speler inschakelen + "Zet compact dialoogvenster aan op telefoon. + +Bekende problemen: +• Albumhoezen op de bibliotheekschaal worden ook kleiner. +• Slaap timer lay-out kan ongebruikelijk verschijnen." + Compacte dialoog inschakelen op telefoon + Laat het debug logboek zien. + Debug logging aanzetten + Houd de speler permanent geminimaliseerd, zelfs als er een ander nummer wordt afgespeeld. + Forceer geminimaliseerde speler + Schakelt scherm rotatie in door je scherm te draaien. + Landschap modus inschakelen + Schakelt de volgende knop in de minispeler in. + Schakel de volgende knop voor de minispeler in + Schakelt de vorige knop in de minispeler in. + Knop Vorige minispeler inschakelen + "Zet 250/251 opus codec aan tijdens het afspelen van audio." + Opus codec inschakelen + Hiermee kun je naar beneden vegen om de minispeler te sluiten. + Schakel vegen in om de minispeler te sluiten + "Voegt de schakelaar 'Trim stilte' toe aan het vervolgmenu voor afspeelsnelheid. + + Info: + • Deze functie is voor podcasts. + • Deze functie is nog in ontwikkeling en kan dus instabiel zijn." + Voeg een trimstilteschakelaar toe + De Zen-modus wordt ook toegepast op podcasts. + Schakel de zen-modus in podcasts in + Een grijze tint toevoegen aan de videospeler om vermoeidheid van de ogen te verminderen. + Zen-modus inschakelen + Start opnieuw op om de lay-out normaal te laden + Vernieuwen en opnieuw opstarten + Instellingen exporteren naar bestand + Instellingen exporteren mislukt. + Instellingen zijn succesvol geëxporteerd. + Importeer + Instellingen importeren uit bestand + Kopiëer + Importeer / exporteer instellingen als tekst + Importeer / Exporteer instellingen + Importeren / exporteren + Importeren mislukt: %s. + Instellingen teruggezet naar standaard + Geïmporteerde %d instellingen + ReVanced ExtExtended + "Downloadknop opent uw externe downloader. + + • Overschrijft alleen de downloadactieknop in de speler. + • Heeft geen voorrang op de downloadknop in het vervolgmenu of de bibliotheek." + Downloadactieknop negeren + Externe downloader + "%1$s is niet geïnstalleerd. + Download %2$s van de website." + Waarschuwing + %s is niet geïnstalleerd. Installeer het alstublieft. + Pakketnaam van uw geïnstalleerde externe downloader-app, zoals NewPipe of Seal + Externe downloader pakketnaam + Verbergt lege componenten in het accountmenu + Leeg onderdeel verbergen + Lijst met accountmenunamen die moeten worden gefilterd, gescheiden door nieuwe regels. + Accountmenufilter + Verberg account menu elementen. + Accountmenu verbergen + Verbergt de knop Toevoegen aan afspeellijst. + Knop Toevoegen aan afspeellijst verbergen + Verbergt de commentaarknop. + Knop voor commentaar verbergen + Verbergt de downloadknop. + Downloadknop verbergen + Verbergt labels in actieknoppen. + Actieknoplabels verbergen + Verbergt de knoppen \'Vind ik leuk\' en \'Niet leuk\'. Het werkt niet in de oude spelerindeling. + Verberg de like- en dislike-knoppen + Verbergt het startkeuzerondje. + Keuzerondje verbergen + Verbergt de deelknop. + Deelknop verbergen + Verbergt de audio-videoschakelaar in de speler. + Verberg audio-videoschakelaar + Verbergt de knop plank van het thuisscherm en verkenner. + Verberg knop plank + Verbergt de carrousel plank van thuisscherm en verkenner. + Carrousel plank verbergen + Verbergt de cast knop bovenaan de thuispagina en bovenaan de speler. + Verberg cast knop + Verbergt de muziekcategoriebalk bovenaan de thuispagina. + Verberg categorie balk + Verbergt kanaalrichtlijnen bovenaan het opmerkingengedeelte. + Kanaalrichtlijnen verbergen + Verbergt tijdstempel- en emoji-knoppen tijdens het typen van opmerkingen. + Verberg tijdstempel en emoji-knoppen + Component met 3 kolommen verbergen + Verberg toevoegen aan wachtrijmenu + Ondertitelingsmenu verbergen + Verberg het menu voor het verwijderen van afspeellijsten + Wachtrijmenu voor negeren verbergen + Downloadmenu verbergen + Verberg het menu voor het bewerken van afspeellijsten + Verbergen ga naar albummenu + Verbergen ga naar artiestenmenu + Knop Vind ik leuk en niet leuk verbergen + "Verbergt advertenties op volledig scherm." + Advertenties op volledig scherm verbergen + Verbergt de deelknop in de speler op volledig scherm. + Knop voor delen op volledig scherm verbergen + Verbergt algemene advertenties. + Algemene advertenties verbergen + Verbergt de handgreep in de account wijziger. + Verberg handvat + Verbergt de geschiedenis knop in de werkbalk. + Verberg geschiedenisknop + Verbergt advertenties voordat je muziek afspeelt. + Verberg muziek advertenties + Navigatiebalk verbergen. + Navigatiebalk verbergen + Verbergt de verkenningsknop. + Knop Verkennen verbergen + Verbergt de homeknop. + Home-knop verbergen + Verberg labels in de navigatie balk. + Verberg e navigatie balk labels + Verbergt de bibliotheekknop. + Bibliotheekknop verbergen + Verbergt de voorbeeldknop. + Knop Monsters verbergen + Verbergt de upgradeknop. + Upgradeknop verbergen + Verbergt het betaalde promotielabel. + Verberg het betaalde promotielabel + Verbergt pop-ups van premiumpromoties. + Pop-ups van premiumpromoties verbergen + Verbergt de banner voor premiumverlenging. + Banner voor premiumverlenging verbergen + Verbergt de geluidszoekknop in de zoekbalk. + Verbergt de gebruikersvoorwaarden in het accountmenu. + Container van termen verbergen + Verbergt de stemzoekknop in de zoekbalk. + Knop voor gesproken zoekopdrachten verbergen + Rekening + Actie bar + Advertenties + Flyout-menu + Navigatiebalk + Speler + Onthoudt de status van de herhaling. + Herinner me de herhalingsstatus + Onthoudt de status van het shuffle. + Onthoud shuffle status + "Verwijdert het dialoogvenster voor discretie van de kijker. + Hiermee wordt de leeftijdsbeperking niet omzeild. Het accepteert het gewoon automatisch." + Dialoogvenster voor discretie van kijkers verwijderen + Brengt het bibliotheektabblad terug naar de oude stijl. (Experimenteel) + Herstel bibliotheekplank in oude stijl + Over + Data is gegeven door de Return YouTube Dislike API. Tik hier voor meer informatie. + Verbergt de spatie van de \"vind ik leuk\" knop. + Compacte \"vind ik leuk\" knop + In plaats van het aantal \"hout niet van\" te laten zien, wordt het percentage getoond. + Hout niet van als percentage + Laat de \"vind ik niet leuk\"s zien van video\'s. + Vind ik niet leuk is niet beschikbaar (client API limiet bereikt) + Dislikes niet beschikbaar (status %d). + Dislikes tijdelijk niet beschikbaar (API time-out). + Dislikes niet beschikbaar (%s). + Verwijdert tracking query parameters uit de URL\'s bij het delen van links. + Koppelingen delen + Instellingen naar het klembord gekopieerd. + "Spoofing van de clientversie naar de oude versie + +• Dit zal het uiterlijk van de app veranderen. maar onbekende neveneffecten kunnen zich voordoen +• Als later uitgeschakeld kan de oude UI blijven totdat de appgegevens gewist worden" + 4.27.53 - Radio modus uitschakelen in Canadese regio\'s + 6.11.52 - Realtime songteksten uitschakelen + Selecteer het doel van de spoof app versie + Spoof app versie doel + Spoof app versie + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/pl-rPL/missing_strings.xml b/patches/src/main/resources/music/translations/pl-rPL/missing_strings.xml new file mode 100644 index 000000000..973422826 --- /dev/null +++ b/patches/src/main/resources/music/translations/pl-rPL/missing_strings.xml @@ -0,0 +1,6 @@ + + + Don\'t show again + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/pl-rPL/strings.xml b/patches/src/main/resources/music/translations/pl-rPL/strings.xml new file mode 100644 index 000000000..4b35a3833 --- /dev/null +++ b/patches/src/main/resources/music/translations/pl-rPL/strings.xml @@ -0,0 +1,433 @@ + + + Kontynuuj + "GmsCore nie ma uprawnień do działania w tle. + +Postępuj zgodnie z przewodnikiem 'Don't kill my app!' dla twojego urządzenia i zastosuj instrukcje dla swojej instalacji GmsCore. + +Jest to wymagane do działania aplikacji." + "Optymalizacja baterii GmsCore musi być wyłączona, aby zapobiec problemom. + +Wyłączenie optymalizacji baterii dla GmsCore nie wpłynie negatywnie na zużycie baterii. + +Stuknij przycisk kontynuacji i zezwól na zmiany w optymalizacji." + Otwórz stronę + Wymagane działanie + Włącz cloud messaging, by otrzymywać powiadomienia. + Otwórz GmsCore + GmsCore nie jest zainstalowany. Zainstaluj go. + Zastępuje domenę, która jest blokowana w niektórych regionach, aby można było otrzymywać miniaturki playlist, awatary kanałów itp. + Pomiń ograniczenia regionu dla obrazów + Zmienia wygląd panelu udostępniania z natywnego aplikacji na systemowy. + Zmień wygląd panelu udostępniania + Listy przebojów + Odkrywaj + Strona główna + Biblioteka + Subskrypcje + Wybierz, na której stronie ma otwierać się aplikacja. + Zmień stronę startową + Lista tekstów tworzących ścieżkę komponentów do filtrowania, które muszą być oddzielone nowymi liniami. + Edytuj własny filtr + Włącza własny filtr do ukrywania komponentów układu aplikacji. + Włącz własny filtr + Nieprawidłowy własny filtr: %s. + Niestandardowe prędkości muszą być mniejsze niż %sx. + Nieprawidłowe niestandardowe prędkości odtwarzania. + Skonfiguruj dostępne prędkości odtwarzania. + Edytuj niestandardowe prędkości odtwarzania + Aby otwierać linki YouTube Music w RVX Music, przejdź do opcji obsługiwanych linków w ustawieniach i włącz obsługiwane adresy internetowe dla RVX. + Otwórz systemowe ustawienia aplikacji + Wyłącza automatycznie włączane napisy w odtwarzaczu filmów. + Wyłącz automatyczne napisy + Wyłącza animację ładowania aplikacji związaną z motywem Cairo podczas otwierania aplikacji. + Wyłącz animację uruchamiania aplikacji + Wyłącza przenoszenie do następnego utworu po kliknięciu łapki w dół. + Wyłącz pomijanie nielubianych piosenek + Wyłącza DRC (Dynamic Range Compression) stosowane w utworach. + Wyłącz audio DRC + Wyłącza gest przesuwania, aby zmienić utwór w miniodtwarzaczu. + Wyłącz gest w miniodtwarzaczu + Wyłącza gest przesuwania, aby zmienić utwór w odtwarzaczu. + Wyłącz gest w odtwarzaczu + Ustawia kolor paska nawigacji na czarny. + Włącz czarny pasek nawigacji + Zmienia tło odtwarzacza na czarne. + Włącz czarne tło odtwarzacza + Dopasowuje kolor miniodtwarzacza do otwarzacza pełnoekranowego. + Włącz pasujące kolory odtwarzaczy + "Włącza kompaktowe menu ustawień utworu na telefonie. + +Ograniczenia: +• Okładka albumu w zakładce biblioteki staje się mniejsza, gdy jest ustawiona siatka +• Układ wyłącznika czasowego może wyglądać nietypowo" + Włącz kompaktowe menu + Zawiera buforowanie w logach debugowania. + Logi do debugowania buforu + Wyświetla log od debugowania. + Włącz logowanie debugowania + Zostawia odtwarzacz zminimalizowany, nawet jeśli zostanie odtworzony inny utwór. + Włącz wymuszenie zminimalizowanego odtwarzacza + Pozwala wejść w tryb pełnoekranowy poprzez obrót ekranu telefonu. + Włącz tryb pełnoekranowy + Dodaje przycisk do następnej piosenki w miniodtwarzaczu. + Dodaj przycisk do następnej piosenki w miniodtwarzaczu + Dodaje przycisk do poprzedniej piosenki w miniodtwarzaczu. + Dodaj przycisk do poprzedniej piosenki w miniodtwarzaczu + "Włącza kodek OPUS, jeśli odpowiedź odtwarzacza zawiera ten kodek. + +Informacje: +• Najnowsze klienty YouTube Music domyślnie używają kodeka OPUS +• Działa jedynie dla użytkowników używających oszukiwania aplikacji na bardzo starych klientach" + Włącz kodek OPUS + Włącza przesuwanie w dół do zamykania miniodtwarzacza. + Włącz przesuwanie do zamykania miniodtwarzacza + "Dodaje przycisk pomijania ciszy do menu od prędkości odtwarzania. + +Informacje: +• Ta funkcja jest dostępna tylko dla podcastów. +• Ta funkcja jest nadal w fazie rozwoju, więc może być niestabilna." + Włącz przełącznik do pomijania ciszy + Tryb zen jest stosowany również do podcastów. + Włącz tryb zen w podcastach + Zmienia kolor tła odtwarzacza na jasnoszary, aby zmniejszyć zmęczenie oczu. + Włącz tryb zen + Przywrócono domyślne wartości. + Uruchom ponownie, aby wczytać układ poprawnie + Odśwież i uruchom ponownie + Wyeksportuj ustawienia do pliku + Nie udało się wyeksportować ustawień. + Ustawienia zostały pomyślnie wyeksportowane. + Import + Zaimportuj ustawienia z pliku + Kopiuj + Zaimportuj/Wyeksportuj ustawienia jako tekst + Zaimportuj lub wyeksportuj ustawienia + Importuj/Eksportuj ustawienia + Nie udało się zaimportować: %s. + Ustawienia zostały zresetowane do domyślnych. + Zaimportowano ustawienia %d. + Zresetuj + ReVanced Extended + "Przycisk od pobierania otwiera zewnętrzną aplikację do pobierania. + +• Jedynie zmienia działanie przycisku w odtwarzaczu. +• Nie zmienia działania przycisków w menu ustawień utworu i bibliotece." + Zastąp przycisk od pobierania + Aplikacja od pobierania + "%1$s nie jest zainstalowany. +Pobierz %2$s ze strony." + Ostrzeżenie + %s nie jest zainstalowany. Zainstaluj go. + Nazwa pakietu zainstalowanej aplikacji od pobierania, takiej jak NewPipe lub YTDLnis. + Nazwa pakietu aplikacji od pobierania + Ukrywa puste komponenty w menu konta. + Ukryj puste komponenty + Lista nazw w menu konta do filtrowania, która musi być oddzielona nowymi liniami. + Filtr menu konta + Ukrywa elementy menu konta za pomocą własnego filtra. + Ukryj menu konta + Ukrywa przycisk dodawania do playlisty. + Ukryj przycisk dodawania do playlisty + Ukrywa przycisk komentarzy. + Ukryj przycisk komentarzy + Ukrywa przycisk pobierania. + Ukryj przycisk pobierania + Ukrywa nazwy przycisków akcji. + Ukryj nazwy przycisków akcji + Ukrywa przyciski łapki w górę i dół. Nie będzie działać na starym układzie odtwarzacza. + Ukryj przyciski łapki w górę i dół + Ukrywa przycisk radia. + Ukryj przycisk radia + Ukrywa przycisk udostępniania. + Ukryj przycisk udostępniania + Ukrywa przełącznik utwór-teledysk w odtwarzaczu. + Ukryj przełącznik utwór-teledysk + Ukrywa półkę z przyciskami na stronie głównej. + Ukryj półki z przyciskami + Ukrywa półkę z karuzelami na stronie głównej. + Ukryj półki z karuzelami + Ukrywa przyciski castowania. + Ukryj przycisk do castowania + Ukrywa panel kategorii. + Ukryj panel kategorii + Ukrywa wytyczne kanału na górze sekcji komentarzy. + Ukryj wytyczne + Ukrywa czas i przyciski od emotikon podczas pisania komentarzy. + Ukryj czas i przyciski od emotikon + Ukrywa poświatę pojawiającą się, gdy przewijamy za pomocą podwójnego kliknięcia. + Ukryj poświatę po dwukrotnym kliknięciu + Ukrywa pływający przycisk w zakładce biblioteki. + Ukryj pływający przycisk + Ukryj 3-kolumnowy komponent + Ukryj menu od dodawania do kolejki + Ukryj menu od napisów + Ukryj menu od usuwania playlisty + Ukryj menu od odrzucania kolejki + Ukryj menu od pobierania + Ukryj menu od edytowania playlisty + Ukryj menu od pokazywania albumu + Ukryj menu od pokazywania wykonawcy + Ukryj menu od przechodzenia do odcinka + Ukryj menu od przechodzenia do podcastu + Ukryj menu do pomocy i opinii + Ukryj przyciski łapki w górę i dół + Ukryj przypinanie do menu szybkiego wybierania + Ukryj menu od odtwarzania jako następny + Ukryj menu od jakości + Ukryj menu od usuwania z biblioteki + Ukryj menu od usuwania z playlist + Ukryj menu od zgłaszania + Ukryj menu od zapisywania odcinka na później + Ukryj menu od dodawania do biblioteki + Ukryj menu od dodawania do playlisty + Ukryj menu od udostępniania + Ukryj menu od odtwarzania losowo + Ukryj menu od wyłącznika czasowego + Ukryj menu do radia + Ukryj menu od statystyk dla nerdów + Ukryj menu od subskrybowania / odsubskrybowywania + Ukryj odpinanie z menu szybkiego wybierania + Ukryj menu do autorów utworu + "Ukrywa reklamy pełnoekranowe. + +Ograniczenie: +• Czasem może pojawić się pusty, czarny ekran zamiast strony głównej" + Ukryj reklamy pełnoekranowe + Ukrywa przycisk udostępniania w trybie pełnoekranowym. + Ukryj przycisk udostępniania w trybie pełnoekranowym + Ukrywa ogólne reklamy. + Ukryj ogólne reklamy + Ukrywa nicki w przełączniku kont. + Ukryj nicki + Ukrywa przycisk historii z paska narzędzi. + Ukryj przycisk historii + Ukrywa reklamy przed odtworzeniem multimediów. + Ukryj reklamy multimedialne + Ukrywa pasek nawigacji. + Ukryj pasek nawigacji + Ukrywa przycisk od strony odkrywania. + Ukryj przycisk od strony odkrywania + Ukrywa przycisk do strony głównej. + Ukryj przycisk do strony głównej + Ukrywa nazwy w pasku nawigacji. + Ukryj nazwy w pasku nawigacji + Ukrywa przycisk biblioteki. + Ukryj przycisk do biblioteki + Ukrywa przycisk od sampli. + Ukryj przycisk od sampli + Ukrywa przycisk do YouTube Premium. + Ukryj przycisk do YouTube Premium + Ukrywa przycisk do powiadomień z paska narzędzi. + Ukryj przycisk do powiadomień + Ukrywa etykiety oznaczające płatne promocje. + Ukryj etykiety oznaczające płatne promocje + Ukrywa półkę z rekomendowanymi playlistami na stronie głównej. + Ukryj półki z rekomendowanymi playlistami + Ukrywa wyskakujące okienka promocyjne Premium. + Ukryj wyskakujące okienka promocyjne Premium + Ukrywa baner odnawiania Premium. + Ukryj baner odnawiania Premium + Ukrywa banery z alertami promocyjnymi. + Ukryj banery z alertami promocyjnymi + Ukrywa półke z samplami na stronie głównej. + Ukryj półkę z samplami + Ukryj menu informacji o YouTube Music + Ukryj menu oszczędzania danych + Ukryj menu pobranych i miejsca na dane + Ukryj menu ustawień ogólnych + Ukryj menu powiadomień + Ukryj menu zasubskrybowania Music Premium + Ukryj menu centrum rodziny + Ukryj menu odtwarzania + Ukryj menu prywatności i danych + Ukryj menu rekomendacji + "Ukrywa elementy menu ustawień. +Działa nie tylko na elementy menu ustawień YT Music, lecz także ReVanced Extended." + Ukryj menu ustawień + Ukrywa przycisk od rozpoznawania piosenek w pasku wyszukiwania. + Ukryj przycisk od rozpoznawania piosenek + Ukrywa przycisk \'Stuknij, aby zaktualizować\'. + Ukryj przycisk \'Stuknij, aby zaktualizować\' + Ukrywa kontener warunków usług z menu konta. + Ukryj kontener warunków usług + Ukrywa przycisk od wyszukiwania głosowego w pasku wyszukiwania. + Ukryj przycisk od wyszukiwania głosowego + Konto + Pasek akcji + Reklamy + Menu ustawień utworu + Ogólne + Pozostałe + Pasek nawigacji + Odtwarzacz + Return YouTube Username + Return YouTube Dislike + SponsorBlock + Menu ustawień YouTube Music + Teledyski + Zapisuje ostatnią wybraną prędkość odtwarzania. + Zapamiętuj zmiany prędkości odtwarzania + Komunikaty będą wyświetlane po zmianie domyślnej prędkości odtwarzania. + Komunikaty o zmianie domyślnej prędkości odtwarzania + Zmieniono domyślną prędkość odtwarzania na %s. + Zapisuje stan pętli. + Zapamiętaj stan pętli + Zapisuje stan odtwarzania losowego. + Zapamiętaj stan odtwarzania losowego + Zapisuje ostatnią wybraną jakość teledysku. + Zapamiętuj zmiany jakości teledysku + Komunikaty będą wyświetlane po zmianie domyślnej jakości teledysków. + Komunikaty o zmianie domyślnej jakości teledysków + Zmieniono domyślną jakość podczas używania sieci mobilnej na %s. + Jakość nie została ustawiona. + Zmieniono domyślną jakość podczas używania Wi-Fi na %s. + "Usuwa okno dialogowe treści ograniczonej do oglądania. +Nie pomija to ograniczeń wiekowych, lecz akceptuje je automatycznie." + Usuń okno dialogowe treści ograniczonej do oglądania + Podczas oglądania na YouTube kontynuuj oglądanie od aktualnego czasu. + Kontynuuj oglądanie + Zamienia menu od czyszczenia kolejek z menu od oglądania na YouTube. + Zmodyfikuj czyszczenie kolejek + Oglądaj na YouTube + Niepoprawne URL filmu. + Zachowuje menu zgłaszania w sekcji komentarzy. + Zachowaj zgłaszanie w komentarzach + Zamienia przycisk od zgłaszania z przyciskiem od prędkości odtwarzania. + Zamień przycisk od zgłaszania + Przywraca stary wygląd wyskakującym panelom z komentarzami. + Przywróć stare wyskakujące panele z komentarzami + Przywraca tło odtwarzacza do starego stylu. + Przywróć stare tło odtwarzacza + "Przywraca układ odtwarzacza do starego wyglądu. +Niektóre ustawienia mogą nie działać poprawnie ze starym układem odtwarzacza." + Przywróć stary układ odtwarzacza + Przywraca zakładkę biblioteki do starego stylu. (Eksperymentalne) + Włącz stary styl półek biblioteki + \@nick (Nazwa użytkownika) + Wybierz format wyświetlania nazwy użytkownika. + Format wyświetlania + Nazwa użytkownika (@nick) + Nazwa użytkownika + Zastępuje nicki nazwami użytkowników w komentarzach. + Włącz Return YouTube Username + "Klucz deweloperski YouTube Data API v3 jest wymagany do zastępowania nicków nazwami użytkownika. + +Dzienny limit kluczy API w planie darmowym wynosi 10 000, a 1 limit służy do zastąpienia nicku nazwą użytkownika dla 1 komentarza. + +Kliknij, by zobaczyć, jak zgłosić klucz API." + O kluczu YouTube Data API + Klucz deweloperski używany do korzystania z API YouTube Data V3. + Klucz YouTube Data API + 1. Przejdź do <a href=%1$s>Utwórz nowy projekt</a>.<br>2. Kliknij przycisk <b>UTWÓRZ</b><br>3. Przejdź do <a href=%2$s>YouTube Data API v3</a>.<br>4. Kliknij przycisk <b>WŁĄCZ</b><br>5. Kliknij przycisk <b>UTWÓRZ DANE LOGOWANIA</b><br>6. Wybierz opcję <b>Dane publiczne</b><br>7. Kliknij przycisk <b>DALEJ</b><br>8. Skopiuj klucz API<br><br>※ Klucz API nie powinien być współdzielony z innymi, dlatego nie jest zawarty w ustawieniach importu/eksportu + Zgłoś klucz deweloperski YouTube Data API + O integracji + Dane są dostarczane dzięki API Return YouTube Dislike. Kliknij tutaj, aby dowiedzieć się więcej. + ReturnYouTubeDislike.com + Ukrywa linię w przycisku od łapkowania. + Kompaktowy przycisk od łapkowania + Zamiast ilości łapek w dół, jest wyświetlany ich procent. + Łapki w dół wyświetlane jako procent + Pokazuje ilość łapek w dół filmów. + Włącz Return YouTube Dislike + Pokazuje szacowaną ilość polubień filmów. + Pokaż szacowaną ilość polubień + Łapki w dół nie są dostępne (limit API użytkownika został osiągnięty). + Liczba łapek w dół nie jest dostępna (status %d). + Łapki w dół są tymczasowo niedostępne (API nie reaguje). + Liczba łapek w dół nie jest dostępna (%s). + Komunikat wyświetlany w momencie, gdy API ReturnYouTubeDislike jest niedostępne. + Pokaż komunikat o niedostępności API + Ukryte + Usuwa parametry śledzących zapytań z adresów URL podczas udostępniania linków. + Oczyść udostępniane linki + O integracji + sponsor.ajay.app + Dane są dostarczane przez API SponsorBlock. Stuknij tutaj, aby dowiedzieć się więcej i pobrać na inne platformy. + Zmień adres API + Adres API został zmieniony. + Adres API jest nieprawidłowy. + Adres API został zresetowany. + Adres SponsorBlock jest używany do wykonywania połączeń z serwerem. Nie zmieniaj tego, chyba że wiesz, co robisz. + Kolor został zmieniony. + Kolor: + Nieprawidłowy kod koloru. + Kolor został zresetowany. + Zmień sposoby pomijania segmentów + Włącz SponsorBlock + SponsorBlock to system pomijania denerwujących fragmentów w filmach na YouTube. + Zresetuj kolor + Nietematyczny Wypełniacz / Żarty + Segmenty nietematyczne dodawane tylko dla wypełnienia lub humor, który nie jest wymagany do zrozumienia głównej treści filmu. Nie dotyczy segmentów zawierających informacje kontekstowe lub szczegółowe. + Przypomnienie O Interakcji (Zasubskrybuj) + Krótkie przypomnienie o łapce w górę, subskrypcji lub obserwowaniu w środku kontentu. Jeśli trwa długo lub dotyczy czegoś konkretnego, powinno być oznaczone jako autopromocja. + Przerywnik / Animowane Intro + Fragment bez faktycznej treści. Może to być pauza, statyczna klatka lub powtarzająca się animacja. Nie dotyczy przejść zawierających informacje. + Muzyka: Sekcja Bez Muzyki + Do użytku jedynie w teledyskach. Sekcje teledysków, które nie są uwzględnione w innej kategorii. + Karty / Napisy Końcowe + Napisy końcowe lub gdy pojawia się ekran końcowy. Nie dotyczy zakończeń zawierających informacje. + Zapowiedź / Podsumowanie / Haczyk + Zbiór klipów pokazujących to, co pojawi się lub co pojawiło się w tym filmie, oraz innych fiilmach z tej serii, w którym wszystkie informacje są gdzieś powtarzane. + Nieopłacona / Auto Reklama + Podobne do treści sponsorowanych, z wyjątkiem nieopłaconych lub autoreklam. Obejmuje to sekcje o własnych towarach, darowiznach czy informacjach o tym, z kim współpracowali. + Treści Sponsorowane + Płatna promocja, płatne rekomendacje oraz bezpośrednie reklamy. Nie do autopromocji ani darmowych wyrazów uznania dla kwestii / twórców / stron / produktów, które im się podobają. + Pomiń automatycznie + Wyłącz + Pominięto wypełniacz. + Pominięto irytujące przypomnienie. + Pominięto wstęp. + Pominięto przerywnik. + Pominięto przerywnik. + Pominięto wiele segmentów. + Pominięto fragment bez muzyki. + Pominięto zakończenie. + Pominięto zapowiedź. + Pominięto podsumowanie. + Pominięto zapowiedź. + Pominięto autoreklamę. + Pominięto treści sponsorowane. + SponsorBlock jest tymczasowo niedostępny. + SponsorBlock jest tymczasowo niedostępny (status %d). + SponsorBlock jest tymczasowo niedostępny (API nie reaguje). + Pokaż komunikat, jeśli API jest niedostępne + Wyświetla komunikat w momencie, gdy API SponsorBlock jest niedostępne. + Pokazuj komunikaty podczas automatycznego pomijania + Komunikaty są pokazywane, gdy segmenty są automatycznie pomijane. + Skopiowano ustawienia do schowka. + "Oszukuje wersję klienta do starszej wersji. + +• Zmieni to wygląd aplikacji, lecz mogą pojawić się nieznane efekty uboczne. +• Jeśli potem opcja zostanie wyłączona, stary interfejs użytkownika może pozostać do momentu wyczyszczenia danych aplikacji." + 4.27.53 - Wyłącza tryb radia w rejonach kanadyjskich + 6.11.52 - Wyłącza teksty w czasie rzeczywistym + 7.16.53 - Przywraca stary pasek akcji + Wybierz wersję, którą chcesz oszukiwać. + Docelowa wersja aplikacji + Oszukaj wersję aplikacji + "Oszukuj klienta, by zapobiec problemom z odtwarzaniem. + +※ Używane równolegle z opcją 'Oszukuj strumień danych', może powodować problemy z odtwarzaniem +" + Oszukuj klienta + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Definiuje domyślnego klienta do oszukiwania. + +※ Jeśli używasz klienta Androida, zaleca się korzystanie razem z opcją 'Oszukuj wersję aplikacji'." + Domyślny klient + Pokazuje używanego klienta do przechwytywania strumienia danych w statystykach dla nerdów. + Informacja w statystykach dla nerdów + "Oszukuj strumień danych, by zapobiec problemom z odtwarzaniem. + +※ Używane równolegle z opcją 'Oszukuj klienta', może powodować problemy z odtwarzaniem" + Oszukuj strumień danych + Android TV + Android VR + iOS + iOS Music + Denifiuje domyślnego klienta, z którego będzie przechwytywany strumień danych. + Domyślny klient + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/pt-rBR/missing_strings.xml b/patches/src/main/resources/music/translations/pt-rBR/missing_strings.xml new file mode 100644 index 000000000..7a33026c6 --- /dev/null +++ b/patches/src/main/resources/music/translations/pt-rBR/missing_strings.xml @@ -0,0 +1,9 @@ + + + Don\'t show again + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/pt-rBR/strings.xml b/patches/src/main/resources/music/translations/pt-rBR/strings.xml new file mode 100644 index 000000000..7c6a89042 --- /dev/null +++ b/patches/src/main/resources/music/translations/pt-rBR/strings.xml @@ -0,0 +1,427 @@ + + + Continuar + "O GmsCore não tem permissão para executar em segundo plano. + +Siga o guia Não mate o meu aplicativo para o seu telefone e aplique as instruções para a sua instalação do MicroG. + +Isto é necessário para o aplicativo funcionar." + "As otimizações de bateria do GmsCore devem ser desativadas para evitar problemas. + +Desativar as otimizações de bateria do GmsCore não afetará negativamente o uso da bateria. + +Toque no botão continuar e permita as alterações de otimização." + Abrir site + Ação necessária + Ative as mensagens na nuvem para receber notificações. + Abrir GmsCore + O GmsCore não está instalado. Instale-o. + Substitui o domínio que está bloqueado em algumas regiões para que miniaturas para playlists, avatares de canais, etc. possam ser recebidos. + Ignorar restrições de imagem por região + Alterar o menu de compartilhamento do app para o meno de compartilhamento do sistema. + Alterar menu de compartilhamento + Paradas + Explorar + Início + Biblioteca + Inscrições + Selecione em qual página o aplicativo será aberto. + Alterar a página inicial + Lista de componentes a serem filtradas separadas por uma nova linha. + Filtro personalizado + Ativa o filtro personalizado para ocultar componentes do layout. + Ativar filtro personalizado + Filtro personalizado inválido: %s. + Velocidades personalizadas devem ser menores que %sx. + Velocidades de reprodução personalizadas inválidas. + Adicionar ou alterar as velocidades de reprodução disponíveis. + Editar velocidades de reprodução personalizadas + Para abrir os links de música do YouTube no RVX Music, ative \'Abrir links suportados\' e ative os endereços web suportados. + Abrir configurações padrão do aplicativo + Desativa as legendas de serem ativadas automaticamente. + Desativar legendas automáticas + Desabilita a animação inicial do Cairo quando o aplicativo é iniciado. + Desativar a animação inicial do Cairo + Desativa o redirecionamento para a próxima faixa ao clicar no botão de Dislike. + Desativar redirecionamento de dislike + Desativa o DRC (Compressão de faixa dinâmica) aplicada ao áudio. + Desativar áudio DRC + Desativar deslize para alterar faixas no mini reprodutor. + Desativar gesto do mini reprodutor + Desativar deslize para alterar faixas no reprodutor. + Desativar gesto do reprodutor + Define a cor da barra de navegação para preto. + Ativar barra de navegação preta + Altera a cor de fundo do reprodutor para preto. + Ativar fundo do reprodutor preto + Corresponde à cor do mini reprodutor para o reprodutor em tela cheia. + Ativar combinação de cores do reprodutor + "Ativa o menu flutuante compacto em telefones. + +Limitações: +• A arte do álbum na guia da Biblioteca fica menor quando organizada em uma grade. +• O layout do temporizador pode parecer incomum." + Ativar diálogo compacto + Inclui o buffer no log de depuração. + Ativar o registro de depuração do buffer + Imprime o relatório de depuração + Ativar o relatório de depuração + Mantém o reprodutor minimizado mesmo quando outra faixa é reproduzida. + Ativar reprodutor minimizado forçado + Ativa o modo paisagem ao girar a tela nos telefones. + Ativar modo paisagem + Adiciona o botão próximo no mini reprodutor. + Adicionar o botão próximo no mini reprodutor + Adiciona o botão anterior no mini reprodutor. + Adicionar o botão anterior no mini reprodutor + "Ative o codec OPUS se a resposta do reprodutor incluir o codec OPUS. + +Informações: +• Os clientes mais recentes do YouTube Music usam o codec de áudio OPUS por padrão. +• Isto só é válido para usuários que falsificam com clientes muito antigos." + Ativar codec OPUS + Permite deslizar para baixo para dispensar o mini reprodutor. + Ativar deslizar para dispensar o mini reprodutor + "Adiciona a opção Cortar silêncio ao menu flutuante de velocidade de reprodução. + +Informações: +• Este recurso é para podcasts. +• Este recurso ainda está em desenvolvimento, portanto pode ser instável." + Adicionar alternador para Cortar silêncio + Também ativa o modo Calmo para podcasts. + Ativar o modo Calmo em podcasts + Altera a cor de fundo do reprodutor para cinza claro para reduzir o cansaço visual. + Ativar modo Calmo + Redefinir para os valores padrão. + Reinicie para carregar o layout normalmente + Atualizar e reiniciar + Exportar configurações para um arquivo + Falha ao exportar configurações. + As configurações foram exportadas com sucesso. + Importar + Importar configurações de um arquivo + Copiar + Importar / Exportar as configurações como texto + Importar ou exportar as configurações como texto. + Importar / Exportar configurações + A importação falhou: %s. + Configurações redefinidas para o padrão + Configurações %d importadas + Reiniciar + ReVanced Extended + "O botão de Download abre seu aplicativo de download externo. + +• Substitui apenas a ação do botão de download no reprodutor. +• Não substitui o botão de Download no menu flutuante ou na biblioteca." + Substituir ação do botão de Download + Aplicativo de download externo + "%1$s não está instalado. +Por favor, baixe %2$s do site." + Aviso + %s não está instalado. Por favor, instale-o. + Nome do pacote do seu aplicativo de download externo instalado, como NewPipe ou YTDLnis. + Nome do pacote do aplicativo de download externo + Oculta componentes vazios no menu de contas + Ocultar componentes vazios + Lista de nomes do menu de contas a serem filtrados, separados por novas linhas. + Filtro do menu de conta + Oculta os elementos do menu da conta usando o filtro personalizado. + Ocultar menu de contas + Oculta o botão Salvar. + Ocultar botão Salvar + Oculta o botão de Comentários. + Ocultar botão Comentários + Oculta o botão de Download. + Ocultar botão Download + Oculta os rótulos dos botões de ação. + Ocultar rótulo do botão de ação + Oculta os botões de Like e Deslike. Não funciona no antigo layout do reprodutor. + Ocultar botões de Like e Deslike + Oculta o botão de Rádio. + Ocultar botão Rádio + Oculta botão de Compartilhar. + Ocultar botão Compartilhar + Oculta o alternador de Áudio / Vídeo no reprodutor. + Ocultar alternador de Áudio / Vídeo + Oculta o painel de botões no feed. + Ocultar painel de botões + Oculta o painel de carrossel no feed. + Ocultar painel de carrossel + Oculta o botão Transmissão. + Ocultar botão Transmissão + Oculta a barra de categorias. + Ocultar barra de categoria + Oculta as diretrizes do canal na parte superior da seção de comentários. + Ocultar diretrizes do canal + Oculta os botões de marcação de tempo e emoji ao escrever comentários. + Ocultar botões de marcação de tempo e emoji + Oculta a sobreposição escura que aparece quando um duplo toque para procurar. + Ocultar filtro de sobreposição de toque duplo + Oculta o botão flutuante na aba Biblioteca. + Ocultar botão flutuante + Ocultar componente de 3 colunas + Ocultar menu Adicionar à fila + Ocultar menu Legendas + Ocultar menu Excuir playlist + Ocultar menu Remover fila + Ocultar menu Fazer o download + Ocultar menu Editar playlist + Ocultar menu Ir para o álbum + Ocultar menu Ir para a página do artista + Ocultar menu Acessar episódio + Ocultar menu Acessar podcast + Ocultar menu Ajuda & feedback + Ocultar botões de Like e Deslike + Ocultar Fixar no menu de discagem rápida + Ocultar menu Tocar a seguir + Ocultar menu Qualidade + Ocultar menu Remover da biblioteca + Ocultar menu Remover da playlist + Ocultar menu Denunciar + Ocultar menu Salvar episódio para mais tarde + Ocultar menu Salvar na biblioteca + Ocultar menu Salvar na playlist + Ocultar menu Compartilhar + Ocultar menu Reprodução aleatória + Ocultar menu de Timer de suspensão + Ocultar menu Iniciar rádio + Ocultar menu Estatísticas para nerds + Ocultar menu de Inscrição / Cancelar inscrição + Ocultar Desafixar do menu de discagem rápida + Ocultar menu Mostrar créditos da música + "Oculta anúncios de tela cheia. + +Limitações: +• Às vezes você pode ver uma tela preta em branco ao invés da fonte inicial." + Ocultar anúncios em tela cheia + Oculta o botão Compartilhar no reprodutor de tela cheia. + Ocultar botão Compartilhar em tela cheia + Oculta anúncios gerais. + Ocultar anúncios gerais + Oculta o identificador no menu da conta. + Ocultar identificador + Oculta o botão Histórico na barra de ferramentas. + Ocultar botão Histórico + Oculta os anúncios antes de reproduzir mídia. + Ocultar anúncios de mídia + Oculta a barra de navegação. + Ocultar barra de navegação + Oculta o botão Explorar. + Ocultar botão Explorar + Oculta o botão de Início. + Ocultar botão Início + Oculta rótulos abaixo dos botões de navegação. + Ocultar rótulos de navegação + Oculta o botão Biblioteca. + Ocultar botão Biblioteca + Oculta o botão Descobertas. + Ocultar botão Descobertas + Oculta o botão Upgrade. + Ocultar botão Upgrade + Oculta o botão Notificações na barra de ferramentas. + Ocultar botão Notificações + Oculta o rótulo de promoção paga. + Ocultar rótulo de promoção paga + Oculta o painel de cartão de lista de reprodução no feed. + Ocultar painel de cartão de lista de reprodução + Oculta pop-ups de promoção premium. + Ocultar pop-ups de promoção premium + Oculta o banner de renovação premium. + Ocultar banner de renovação premium + Oculta o banner de alerta de promoção. + Ocultar banner de alerta de promoção + Oculta o painel Descobertas no feed. + Ocultar painel Descobertas + Ocultar menu Sobre o YouTube Music + Ocultar menu Economia de dados + Ocultar menu Downloads e armazenamento + Ocultar menu Geral + Ocultar Menu Notificações + Ocultar menu Conheça o Music Premium + Ocultar menu Central da família + Ocultar menu Reprodução + Ocultar menu Privacidade e dados + Ocultar menu Recomendações + "Oculte elementos do menu de configurações. +Isso oculta não apenas o menu de configurações do YT Music, mas também o menu de configurações do ReVanced Extended." + Ocultar menu de configurações + Oculta o botão de pesquisa de som na barra de pesquisa. + Ocultar botão de pesquisa de som + Oculta o botão Toque para atualizar. + Ocultar botão Toque para atualizar + Oculta o contêiner dos Termos de Serviço. + Ocultar contêiner de termos + Oculta o botão de pesquisa por voz na barra de pesquisa. + Ocultar botão de pesquisa por voz + Conta + Barra de ação + Anúncios + Menu flutuante + Geral + Diversos + Barra de Navegação + Reprodutor + Return YouTube Username + Return YouTube Dislike + SponsorBlock + Menu de configurações + Vídeo + Lembra a última velocidade de reprodução selecionada. + Lembrar mudança na velocidade de reprodução + Exibir uma notificação flutuante quando alterar a velocidade padrão de reprodução. + Exibir uma notificação flutuante + Alterando a velocidade padrão para %s. + Lembra o estado da alternância de repetição. + Lembrar estado de repetição + Lembre o estado da alternância do modo aleatório. + Lembrar estado do modo aleatório + Lembra a última qualidade de vídeo selecionada. + Lembrar mudança na qualidade do vídeo + Exibir uma notificação flutuante quando alterar a qualidade padrão do vídeo. + Exibir uma notificação flutuante + Alterando a qualidade padrão de dados móveis para %s. + Falha ao definir qualidade. + Alterando a qualidade padrão do Wi-Fi para %s. + "Remover o diálogo discricionário de visualização. +Isso não ignora a restrição de idade, apenas aceita isso automaticamente." + Remover o diálogo discricionário do visualizador + Continua o vídeo a partir do tempo atual ao mudar para o YouTube. + Continuar assistindo + Substitui o menu \'Remover fila\' pelo menu \'Assistir no YouTube\'. + Substituir Remover fila + Assistir no YouTube + URL de vídeo inválida. + Mantém intacto o menu Denunciar na seção de comentários. + Manter Denunciar nos comentários + Substitui o menu Denunciar pelo menu Velocidade de Reprodução. + Substituir menu Denunciar + Retorna os painéis popup de comentários ao estilo antigo. + Restaurar antigo painel popup de comentários + Retorna o fundo do reprodutor para o estilo antigo. + Restaurar antigo fundo do reprodutor + "Retorna o layout do reprodutor ao estilo antigo. +Alguns recursos podem não funcionar corretamente no layout antigo do reprodutor." + Restaurar antigo layout do reprodutor + Retorna a aba da Biblioteca para o estilo antigo. (Experimental) + Restaurar antigo estilo do painel da biblioteca + \@identificador (Nome de usuário) + Selecione o formato de exibição do nome de usuário. + Formato de exibição + Nome de usuário (@identificador) + Nome de usuário + Substitui identificadores por nomes de usuários em comentários. + Ativar Return YouTube Username + "Uma Chave de desenvolvedor da API de Dados do YouTube v3 é necessária para substituir identificadores por nomes de usuários. + +A cota diária para chaves de API no plano gratuito é de 10.000, e 1 cota é usada para substituir um identificador por um nome de usuário para 1 comentário. + +Clique para ver como emitir uma chave de API." + Sobre a chave API de dados do YouTube + A chave de desenvolvedor para usar a API de Dados do YouTube v3. + Chave API dos Dados do YouTube + 1. Vá para <a href=%1$s>Criar um novo projeto</a>.<br>2. Clique no botão <b>CRIAR</b>.<br>3. Vá para <a href=%2$s>API de dados do YouTube v3</a>.<br>4. Clique no botão <b>ATIVAR</b>.<br>5. Clique no botão <b>CRIAR CREDENCIAIS</b>.<br>6. Selecione a opção <b>Dados públicos</b>.<br>7. Clique no botão <b>PRÓXIMO</b>.<br>8. Copie a chave da API.<br><br>※ A chave da API nunca deve ser compartilhada com outras pessoas, portanto, ela não é incluída nas configurações de Importação/Exportação. + Emitir chave de desenvolvedor da API de dados do YouTube v3 + Sobre + Os dados são fornecidos pela API do Return YouTube Dislike. Toque aqui para saber mais. + ReturnYouTubeDislike.com + Oculta o separador do botão curtir. + Botão de curtir compacto + Exibe a porcentagem de deslikes em vez da contagem de deslikes. + Dislikes como porcentagem + Mostra a contagem de deslike dos vídeos. + Ativar Return YouTube Dislike + Mostra a contagem estimada de curtidas dos vídeos. + Exibir curtidas estimadas + Dislikes indisponível (limite de API do cliente atingido). + Deslikes indisponível (status %d). + Dislikes temporariamente indisponível (API expirou). + Deslikes indisponível (%s). + Notificação flutuante exibida se o Return YouTube Dislike não está disponível. + Exibir uma notificação flutuante se a API não estiver disponível + Oculto + Remove os parâmetros de consulta de rastreamento das URLs ao compartilhar os links. + Limpar links compartilhados + Sobre + sponsor.ajay.app + Os dados são fornecidos pela API do SponsorBlock. Toque aqui para aprender mais e ver downloads para outras plataformas. + Alterar URL da API + URL da API alterada. + URL da API é inválida. + Redefinir URL da API. + O endereço que o SponsorBlock usa para fazer chamadas para o servidor. Não mude isso a menos que você saiba o que está fazendo. + Cor alterada. + Cor: + Código de cor inválido. + Redefinir cor. + Alterar comportamento do segmento + Ativar SponsorBlock + SponsorBlock é um sistema coletivo para pular partes irritantes de vídeos do YouTube. + Redefinir cor + Enrolação / Piadas + Cenas tangenciais inseridas apenas por enrolação ou humor que não são necessárias para compreender o tópico principal do vídeo. Isto não deve incluir segmentos que fornecem contexto ou detalhes de segundo plano. + Lembrete de Interação (Inscreva-se) + Um breve lembrete para curtir, se inscrever ou segui-los no meio do conteúdo. Se for longo ou sobre algo específico, deve estar sob autopromoção. + Intervalo / Introdução Animada + Um intervalo sem conteúdo real. Pode ser uma pausa, um quadro estático ou uma animação repetida. Não inclui transições contendo informações. + Música: Seção sem música + Somente para uso em vídeos de música. Seções de vídeos de música sem música, que já não estão cobertas por outra categoria. + Cartões finais / Créditos + Créditos ou quando os cartões finais do YouTube aparecem. Não para conclusões com informações. + Pré-visualização / Recapitulação / Hook + Coleção de clipes que mostram o que está por vir ou o que aconteceu no vídeo ou em outros vídeos de uma série, onde todas as informações são repetidas em outro lugar. + Não pago / Autopromoção + Semelhante ao Patrocinador exceto pela promoção não paga ou autopromoção. Inclui seções sobre mercadorias, doações ou informações sobre com quem eles colaboraram. + Patrocinador + Promoção paga, referências pagas e anúncios diretos. Não deve ser usado para auto-promoção ou mensagens grátis para causas / criadores/sites / produtos que eles gostam. + Pular automaticamente + Desativar + Enrolação pulada. + Lembrete irritante pulado. + Introdução pulada. + Intervalo pulado. + Intervalo pulado. + Pulou vários segmentos. + Segmento sem música pulado. + Outro pulado. + Pré-visualização pulada. + Recapitulação pulada. + Pré-visualização pulada. + Autopromoção pulada. + Patrocinador pulado. + O SponsorBlock está temporariamente indisponível. + O SponsorBlock está temporariamente indisponível (status %d). + O SponsorBlock está temporariamente indisponível (API expirou). + Exibir uma notificação flutuante se a API não estiver disponível + Exibe uma notificação flutuante se a API SponsorBlock não estiver disponível. + Exibir uma notificação flutuante quando pular automaticamente + Uma notificação flutuante é exibida quando um segmento é ignorado automaticamente. + Configurações copiadas para área de transferência. + "Falsifica a versão do cliente para uma versão mais antiga. + +• Isto alterará a aparência do aplicativo, mas poderão ocorrer efeitos colaterais desconhecidos. +• Se for desativada posteriormente, a interface do usuário antiga poderá permanecer até que os dados do aplicativo sejam apagados." + 4.27.53 - Desativar modo de Rádio em regiões Canadenses + 6.11.52 - Desativar letras em tempo real + 7.16.53 - Restaurar barra de ação antiga + Selecione a versão do app para falsificação. + Versão da falsificação do aplicativo + Falsificar a versão do aplicativo + "Falsificar o cliente para evitar problemas de reprodução. + +※ Quando usado com 'Dados de streaming falsos', podem ocorrer problemas de reprodução." + Falsificar cliente + "Define um cliente padrão para falsificar." + Cliente padrão + Exibir o cliente usado para buscar dados de streaming em estatísticas para nerds. + Exibir em Estatísticas para nerds + "Falsifique os dados de streaming para evitar problemas de reprodução. + +※ Quando usado com 'Falsificar cliente', podem ocorrer problemas de reprodução." + Dados de streaming falsos + Android TV + Android VR + iOS + iOS Music + Define um cliente padrão que busca dados de streaming. + Cliente padrão + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/ro-rRO/missing_strings.xml b/patches/src/main/resources/music/translations/ro-rRO/missing_strings.xml new file mode 100644 index 000000000..79eb86d0a --- /dev/null +++ b/patches/src/main/resources/music/translations/ro-rRO/missing_strings.xml @@ -0,0 +1,356 @@ + + + Continue + Don\'t show again + "GmsCore does not have permission to run in the background. + +Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. + +This is required for the app to work." + "GmsCore battery optimizations must be disabled to prevent issues. + +Disabling battery optimizations for GmsCore will not negatively affect battery usage. + +Tap the continue button and allow optimization changes." + Open website + Action needed + Enable cloud messaging to receive notifications. + Open GmsCore + GmsCore is not installed. Install it. + Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. + Bypass image region restrictions + Change from in-app share sheet to system share sheet. + Change share sheet + Charts + Explore + Home + Library + Subscriptions + Select which page the app opens in. + Change start page + Invalid custom filter: %s. + Invalid custom playback speeds. + To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. + Open default app settings + Disables Cairo splash animation when the app starts up. + Disable Cairo splash animation + Disables redirection to the next track when clicking the Dislike button. + Disable dislike redirection + Disables DRC (Dynamic Range Compression) applied to audio. + Disable DRC audio + Disable swipe to change tracks in the miniplayer. + Disable miniplayer gesture + Disable swipe to change tracks in the player. + Disable player gesture + Changes the player background color to black. + Enable black player background + Includes the buffer in the debug log. + Enable debug buffer logging + Adds a next track button to the miniplayer. + Add miniplayer next button + Adds a previous track button to the miniplayer. + Add miniplayer previous button + Enables swipe down to dismiss miniplayer. + Enable swipe to dismiss miniplayer + "Adds a Trim silence switch to the playback speed flyout menu. + +Info: +• This feature is for podcasts. +• This feature is still in development, so it may be unstable." + Add Trim silence switch + Also enables Zen mode for podcasts. + Enable Zen mode in podcasts + Reset to default values. + Restart to load the layout normally + Refresh and restart + Export settings to file + Failed to export settings. + Settings were successfully exported. + Import settings from file + Import / Export settings as text + Import failed: %s. + Reset + ReVanced Extended + "Download button opens your external downloader. + +• Only overrides the Download action button in the player. +• Does not override the Download button in the flyout menu or Library tab." + Override Download action button + External downloader + "%1$s is not installed. +Please download %2$s from the website." + Warning + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + List of account menu names to filter, separated by new lines. + Account menu filter + Hides the Save button. + Hide Save button + Hides the Comments button. + Hide Comments button + Hides the Download button. + Hide Download button + Hides the labels of the action buttons. + Hide action button labels + Hides the Like and Dislike buttons. It does not work in the old player layout. + Hide Like and Dislike buttons + Hides the Radio button. + Hide Radio button + Hides the Share button. + Hide Share button + Hides the Audio / Video toggle in the player. + Hide Audio / Video toggle + Hides the channel guidelines at the top of the comments section. + Hide channel guidelines + Hides the timestamp and emoji buttons when typing comments. + Hide timestamp and emoji buttons + Hides dark overlay that appears when double-tapping to seek. + Hide double-tap overlay filter + Hides the floating button in the Library tab. + Hide floating button + Hide 3-column component + Hide Add to queue menu + Hide Captions menu + Hide Delete playlist menu + Hide Dismiss queue menu + Hide Download menu + Hide Edit playlist menu + Hide Go to album menu + Hide Go to artist menu + Hide Go to episode menu + Hide Go to podcast menu + Hide Help & feedback menu + Hide Like and Dislike buttons + Hide Pin to Speed dial menu + Hide Play next menu + Hide Quality menu + Hide Remove from library menu + Hide Remove from playlist menu + Hide Report menu + Hide Save episode for later menu + Hide Save to library menu + Hide Save to playlist menu + Hide Share menu + Hide Shuffle play menu + Hide Sleep timer menu + Hide Start radio menu + Hide Stats for nerds menu + Hide Subscribe / Unsubscribe menu + Hide Unpin from Speed dial menu + Hide View song credits menu + "Hides fullscreen ads. + +Limitations: +• Sometimes you may see a blank black screen instead of the home feed." + Hide fullscreen ads + Hides the Share button in the fullscreen player. + Hide fullscreen Share button + Hides general ads. + Hide general ads + Hides the Explore button. + Hide Explore button + Hides the Home button. + Hide Home button + Hides the Library button. + Hide Library button + Hides the Samples button. + Hide Samples button + Hides the Upgrade button. + Hide Upgrade button + Hides the Notifications button in the toolbar. + Hide Notifications button + Hides the paid promotion label. + Hide paid promotion label + Hides the playlist card shelf in the feed. + Hide playlist card shelf + Hides premium promotion popups. + Hide premium promotion popups + Hides the premium renewal banner. + Hide premium renewal banner + Hides the promotion alert banner. + Hide promotion alert banner + Hides the Samples shelf in the feed. + Hide Samples shelf + Hide About menu + Hide Data saving menu + Hide Downloads & storage menu + Hide General menu + Hide Notifications menu + Hide Get Music premium menu + Hide Family Center menu + Hide Playback menu + Hide Privacy & data menu + Hide Recommendations menu + "Hide elements of the settings menu. +This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." + Hide settings menu + Hides the sound search button in the search bar. + Hide sound search button + Hides the Tap to update button. + Hide Tap to update button + Hides the voice search button in the search bar. + Hide voice search button + Account + Action Bar + Ads + Flyout Menu + General + Miscellaneous + Navigation Bar + Player + Return YouTube Username + Return YouTube Dislike + SponsorBlock + Settings menu + Video + Remembers the last playback speed selected. + Remember playback speed changes + Show a toast when changing the default playback speed. + Show a toast + Changing default speed to %s. + Remembers the last video quality selected. + Remember video quality changes + Show a toast when changing the default video quality. + Show a toast + Changing default mobile data quality to %s. + Failed to set quality. + Changing default Wi-Fi quality to %s. + "Removes the viewer discretion dialog. +This does not bypass the age restriction. It just accepts it automatically." + Remove viewer discretion dialog + Continues the video from the current time when switching to YouTube. + Continue watching + Replaces the Dismiss queue menu with the Watch on YouTube menu. + Replace Dismiss queue menu + Watch on YouTube + Invalid video url. + Keeps the Report menu in the comments section intact. + Keep Report in comments + Replaces the Report menu with the Playback speed menu. + Replace Report menu + Returns the comments popup panels to the old style. + Restore old comments popup panels + Returns the player background to the old style. + Restore old player background + "Returns the player layout to the old style. +Some features may not work properly in the old player layout." + Restore old player layout + Returns the Library tab to the old style. (Experimental) + Restore old style library shelf + @handle (Username) + Select the username display format. + Display format + Username (@handle) + Username + Replaces handles with usernames in comments. + Enable Return YouTube Username + "A YouTube Data API v3 Developer Key is required to replace handles with usernames. + +The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. + +Click to see how to issue a API key." + About YouTube Data API key + The developer key for using the YouTube Data API v3. + YouTube Data API key + 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. + Issue YouTube Data API v3 developer key + ReturnYouTubeDislike.com + Shows the dislike count of videos. + Enable Return YouTube Dislike + Shows the estimated like count of videos. + Show estimated likes + Dislikes are unavailable (client API limit reached). + Dislikes are unavailable (status %d). + Dislikes are temporarily unavailable (API timed out). + Dislikes are unavailable (%s). + Shows a toast if the Return YouTube Dislike API is unavailable. + Show a toast if API is unavailable + Hidden + Removes tracking query parameters from URLs when sharing links. + About + sponsor.ajay.app + Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. + Change API URL + API URL changed. + API URL is invalid. + API URL reset. + The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. + Color changed. + Color: + Invalid color code. + Color reset. + Change segment behavior + Enable SponsorBlock + SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. + Reset color + Filler Tangent / Jokes + Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. + Interaction Reminder (Subscribe) + A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. + Intermission / Intro Animation + An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. + Music: Non-Music Section + Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. + Endcards / Credits + Credits or when the YouTube endcards appear. Not for conclusions with information. + Preview / Recap / Hook + Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. + Unpaid / Self Promotion + Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. + Sponsor + Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. + Skip automatically + Disable + Skipped filler. + Skipped annoying reminder. + Skipped intro. + Skipped intermission. + Skipped intermission. + Skipped multiple segments. + Skipped a non-music section. + Skipped outro. + Skipped preview. + Skipped recap. + Skipped preview. + Skipped self promotion. + Skipped sponsor. + SponsorBlock is temporarily unavailable. + SponsorBlock is temporarily unavailable (status %d). + SponsorBlock is temporarily unavailable (API timed out). + Show a toast if API is unavailable + Shows a toast if the SponsorBlock API is unavailable. + Show a toast when skipping automatically + Shows a toast when a segment is automatically skipped. + "Spoofs the client version to an older version. + +• This will change the appearance of the app, but unknown side effects may occur. +• If later disabled, the old UI may remain until the app data is cleared." + 6.11.52 - Disable real-time lyrics + 7.16.53 - Restore old action bar + Select the spoof app version target. + Spoof app version target + Spoof app version + "Spoof the client to prevent playback issues. + +※ When used with 'Spoofing streaming data', playback issues may occur." + Spoof client + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + Android TV + Android VR + iOS + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/ro-rRO/strings.xml b/patches/src/main/resources/music/translations/ro-rRO/strings.xml new file mode 100644 index 000000000..2bf3556b5 --- /dev/null +++ b/patches/src/main/resources/music/translations/ro-rRO/strings.xml @@ -0,0 +1,78 @@ + + + Filtrează numele componentelor după linie separat. + Editați filtrul personalizat + Activează filtru personalizat pentru a ascunde componentele aspectului. + Activați filtrul personalizat + Viteze de redare personalizate invalide! Resetare la valorile implicite. + Adaugă sau modifică vitezele de redare disponibile. + Modifică vitezele de redare personalizate + Dezactivează subtitrările automate forțate. + Dezactivează subtitrările automate forțate + Setează culoarea barei de navigare la negru. + Activare bară neagră de navigare + Potrivește culoarea mini player-ului și a player-ului pe tot ecranul. + Activare culori potrivire player + "Activați dialogul compact pe telefon. + +Probleme cunoscute: +• Arta albumului pe raftul bibliotecii devine de asemenea mai mică. +• Aspect cronometru somn poate părea neobișnuit." + Activare dialog compact + Afișează jurnalul de depanare. + Activează jurnalul de depanare + Menține player-ul minimizat permanent chiar dacă o altă piesă este redată. + Activare player minimizat forțat + Activează intrarea în modul peisaj după rotirea ecranului pe telefon. + Activare mod peisaj + "Activează codec-ul opus 250/251 atunci când redați audio." + Activează codec opus + Adaugă o nuanță gri la player-ul video pentru a reduce oboseala ochilor. + Activare mod zen + Importă + Copiere + Importă sau exportă setările ca text. + Importă / Exportă + Setări resetate la valorile implicite. + Setări %d importate. + %s nu este instalat. Vă rugăm să-l instalaţi. + Numele pachetului al aplicației externe de descărcare instalate, cum ar fi NewPipe sau YTDLnis. + Numele pachetului de descărcare extern + Ascunde componentele goale în meniul contului. + Ascunde componenta goală + Ascunde elementele meniului contului. + Ascunde meniul contului + Ascunde raftul butonului de pe pagina principală și explorare. + Ascunde buton raft + Ascunde raftul carusel de pe pagina principală și explorare. + Ascunde raft carusel + Ascunde butonul de difuzare. + Ascunde butonul de difuzare + Ascunde bara de categorii de muzică din partea de sus a paginii principale. + Ascunde bara de categorie + Ascunde mânerul în comutatorul de conturi. + Ascunde etichetele + Ascunde butonul de istoric în bara de instrumente. + Ascunde butonul de istoric + Ascunde reclamele înainte de a reda o piesă. + Ascunde reclamele muzicale + Ascunde bara de navigare. + Ascunde bara de navigare + Ascunde etichetele în bara de navigare. + Ascunde eticheta de navigare + Ascunde containerul termenilor și condițiilor de utilizare. + Ascunde containerul termenilor + Reține starea repetării. + Memorează starea repetării + Reține starea redării aleatorii. + Reține starea redării aleatorii + Despre + Datele sunt furnizate de API-ul Returnare YouTube Dislike. Atinge aici pentru a afla mai multe. + Ascunde separatorul butonului apreciez. + Buton compact apreciez + În loc de numărul de dezaprobări, se afișează procentul de dezaprobări. + Dislike-uri ca procentaj + Curăță link-urile de partajare + Setări copiate în clipboard. + 4.27.53 - Dezactivare mod radio în regiunile canadiene + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/ru-rRU/missing_strings.xml b/patches/src/main/resources/music/translations/ru-rRU/missing_strings.xml new file mode 100644 index 000000000..0f4771465 --- /dev/null +++ b/patches/src/main/resources/music/translations/ru-rRU/missing_strings.xml @@ -0,0 +1,13 @@ + + + Don\'t show again + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/ru-rRU/strings.xml b/patches/src/main/resources/music/translations/ru-rRU/strings.xml new file mode 100644 index 000000000..498a82d80 --- /dev/null +++ b/patches/src/main/resources/music/translations/ru-rRU/strings.xml @@ -0,0 +1,423 @@ + + + Продолжить + "MicroG GmsCore не имеет разрешения на запуск в фоновом режиме. + +Следуйте инструкции \"Don't kill my app\" для Вашего телефона и установите MicroG согласно ее. + +Это необходимо для работы приложения." + "Во избежание проблем необходимо отключить оптимизацию батареи для MicroG GmsCore. + +Нажмите кнопку \"Продолжить\" и отключите оптимизацию батареи." + Открыть сайт + Требуется действие + Включите \"Облачные уведомления\" для получения уведомлений. + Открыть GmsCore + GmsCore не установлен. Установите его. + Заменяет заблокированный в некоторых регионах домен, чтобы можно было получать миниатюры плейлистов, аватары каналов и т. д. + Обойти ограничения изображений по региону + Меняет тип диалогового окна \"Поделиться\" из встроенного на системное. + Изменить окно \"Поделиться\" + Хит-парады + Навигатор + Главная + Библиотека + Подписки + Выберите, на какой странице открывается приложение. + Изменить начальную страницу + Настройте, какие компоненты фильтровать, обозначая конец каждого из них новой строкой. + Изменить пользовательский фильтр + Включает пользовательский фильтр для скрытия компонентов интерфейса. + Пользовательский фильтр + Недопустимый пользовательский фильтр: %s. + Недопустимые скорости. Значения сброшены к начальным. + Недопустимые пользовательские скорости воспроизведения. Используются значения по умолчанию. + Настройте доступные скорости воспроизведения. + Изменить скорости + Чтобы открыть ссылку на YouTube Music в RVX Music, включите \"Открывать поддерживаемые ссылки\" и включите поддерживаемые веб-адреса. + Открыть настройки по умолчанию + Отключает автоматическое включение субтитров. + Отключить автоматические субтитры + Отключает анимацию Кайро при запуске приложения. + Отключить анимацию Кайро + Отключает перенаправление на следующий трек при нажатии на кнопку \"Не нравится\". + Отключить переключение при \"Не нравится\" + Выключает DRC (сжатие динамического диапазона), применяемого к аудио. + Отключить DRC аудио + Отключает свайп для переключения треков в миниплеере. + Отключить жест мини плеера + Отключает свайп для переключения треков в плеере. + Отключить жест плеера + Устанавливает чёрный цвет панели навигации. + Чёрная панель навигации + Меняет адаптивный цвет фона плеера на черный. + Включить черный фон плеера + Цвет мини-проигрывателя соответствует цвету полноэкранного проигрывателя. + Цветовое соответствие проигрывателей + "Включает компактное всплывающее меню на телефонах. + +Известные проблемы: +• Заставки альбомов во вкладке \"Библиотека\" становятся меньше в виде сетки. +• Интерфейс \"Автовыключение\" может необычно появляться." + Компактный вид окна + Вывод журнала отладки включает буфер. + Ведение журналов отладки буфера + Выводит журнал отладки. + Ведение журнала отладки + Удерживает проигрыватель свёрнутым, даже если играет другой трек. + Удерживать проигрыватель свёрнутым + Включает альбомный режим при повороте экрана на телефонах. + Альбомный режим + Включает кнопку следующего трека в миниплеере. + Включить кнопку следующего в миниплеере + Включает кнопку предыдущего трека в миниплеере. + Включить кнопку предыдущего в миниплеере + "Включает аудио кодек opus вместо аудио кодека mp4a. + +Информация: +• Последние Android клиенты используют аудио кодек opus по умолчанию. +• Эта функция подходит только для очень старых клиентов." + Кодек Opus + Включает жест вниз для скрытия миниплеера. + Включить жест скрытия миниплеера + "Включает переключатель \"Обрезать тишину\" во всплывающем меню скорости воспроизведения. + +Информация: +• Эта функция предназначена для подкастов. +• Эта функция все еще находится в разработке, поэтому может работать нестабильно." + Включить обрезание тишины + Режим \"Дзен\" также применяется к подкастам. + Включить режим \"Дзен\" в подкастах + Меняет оттенок фона проигрывателя видео на светло-серый, чтобы уменьшить нагрузку на глаза. + Режим \"Дзен\" + Сброшены до значений по умолчанию. + Перезапустите для правильной загрузки интерфейса + Обновите и перезапустите + Извлечь настройки в файл + Не удалось извлечь настройки. + Настройки успешно извлечены. + Восстановить + Восстановить настройки из файла + Копировать + Восстановить / Извлечь настройки в виде текста + Восстановить или извлечь настройки. + Восстановить / Извлечь настройки + Не удалось восстановить: %s. + Настройки сброшены до начальных. + Восстановлено %d настройки(ек). + Сбросить + Настройки ReVanced Extended + "Кнопка \"Скачать\" открывает внешний загрузчик. + +• Переопределяет только кнопку загрузки в плеере. +• Не переопределяет кнопку загрузки во всплывающем меню или библиотеке." + Подменить кнопку \"Скачать\" + Внешний загрузчик + "%1$s не установлен. +Пожалуйста, скачайте %2$s с сайта." + Внимание + %s не установлен. Пожалуйста, установите его. + Имя пакета вашего установленного внешнего загрузчика. Например, NewPipe или YTDLnis. + Имя пакета внешнего загрузчика + Скрывает пустой пункт в меню аккаунта. + Скрыть пустые пункты + Список имен меню аккаунта для фильтрации, разделяемых новой строкой. + Фильтр меню аккаунта + Скрывает элементы меню аккаунта в пользовательском фильтре. + Скрыть меню аккаунта + Скрывает кнопку \"Добавить в плейлист\". + Скрыть кнопку \"Добавить в плейлист\" + Скрывает кнопку \"Комментарии\". + Скрыть кнопку \"Комментарии\" + Скрывает кнопку \"Скачать\". + Скрыть кнопку \"Скачать\" + Скрывает подписи на кнопках действий. + Скрыть подписи кнопок действий + Скрывает кнопки \"Нравится\" и \"Не нравится\". Не работает в старом интерфейсе проигрывателя. + Скрыть кнопки \"Нравится\" и \"Не нравится\" + Скрывает кнопку \"Включить радиостанцию\". + Скрыть кнопку \"Включить радиостанцию\" + Скрывает кнопку \"Поделиться\". + Скрыть кнопку \"Поделиться\" + Скрывает переключатель \"аудио/видео\" в плеере. + Скрыть переключатель \"аудио/видео\" + Скрывает ряд кнопок с главной страницы и со вкладки \"Навигатор\". + Скрыть ряд кнопок + Скрывает карусель треков с главной страницы и со вкладки \"Навигатор\". + Скрыть карусель треков + Скрывает кнопку \"Трансляция\". + Скрыть кнопку \"Трансляция\" + Скрывает панель категорий. + Скрыть панель категорий + Скрывает правила канала в верхней части комментариев. + Скрыть правила канала + Скрывает кнопки метки времени и эмодзи при вводе комментариев. + Метка времени и кнопки эмодзи + Скрывает затемнение, которое появляется при двойном нажатии при перемотке. + Скрыть фильтр двойного нажатия + Скрывает всплывающую кнопку в библиотеке. + Скрыть всплывающую кнопку + Скрыть компонент из 3 столбцов + Скрыть пункт \"Добавить в очередь\" + Скрыть пункт \"Субтитры\" + Скрыть пункт \"Удалить плейлист\" + Скрыть пункт \"Очистить очередь\" + Скрыть пункт \"Скачать\" + Скрыть пункт \"Изменить плейлист\" + Скрыть пункт \"Открыть альбом\" + Скрыть пункт \"Перейти на страницу исполнителя\" + Скрыть пункт \"Перейти к выпуску\" + Скрыть пункт \"Перейти к подкасту\" + Скрыть пункт \"Справка и отзывы\" + Скрыть кнопки \"Нравится\" и \"Не нравится\" + Скрыть \"Закрепить в быстром выборе\" + Скрыть пункт \"Включить следующим\" + Скрыть пункт \"Качество\" + Скрыть пункт \"Удалить из библиотеки\" + Скрыть пункт \"Удалить из плейлиста\" + Скрыть пункт \"Пожаловаться\" + Скрыть пункт \"Слушать позже\" + Скрыть пункт \"Сохранить в библиотеке\" + Скрыть пункт \"Добавить в плейлист\" + Скрыть пункт \"Поделиться\" + Скрыть пункт \"Перемешать\" + Скрыть пункт \"Автовыключение\" + Скрыть пункт \"Включить радиостанцию\" + Скрыть пункт \"Статистика для сисадминов\" + Скрыть пункт \"Подписаться / Отменить подписку\" + Скрыть \"Открепить из быстрого выбора\" + Скрыть пункт \"Участники и создатели\" + "Скрывает полноэкранную рекламу." + Полноэкранная реклама + Скрывает кнопку \"Поделиться\" в полноэкранном проигрывателе. + Скрыть кнопку \"Поделиться\" в полноэкранном режиме + Скрывает рекламу общего формата. + Скрыть рекламу общего формата + Скрывает электронную почту / @ник в меню смены аккаунтов. + Скрыть электронную почту / @ник + Скрывает кнопку \"История\" на панели инструментов. + Скрыть кнопку \"История\" + Скрывает рекламу перед воспроизведением музыки. + Скрыть музыкальную рекламу + Скрывает панель навигации. + Скрыть панель навигации + Скрывает кнопку \"Навигатор\". + Скрыть кнопку \"Навигатор\" + Скрывает кнопку \"Главная\". + Скрыть кнопку \"Главная\" + Скрывает подписи под кнопками навигации. + Скрыть подписи кнопок навигации + Скрывает кнопку \"Библиотека\". + Скрыть кнопку \"Библиотека\" + Скрывает кнопку \"Семплы\". + Скрыть кнопку \"Семплы\" + Скрывает кнопку \"Платные подписки\". + Скрыть кнопку \"Платные подписки\" + Скрывает кнопку \"Уведомления\" на панели инструментов. + Скрыть кнопку \"Уведомления\" + Скрывает метку \"Содержит прямую рекламу\". + Скрыть \"Содержит прямую рекламу\" + Скрывает полку с заставкой плейлиста в ленте. + Скрыть полку с заставкой плейлиста + Скрывает всплывающую рекламу Premium. + Скрыть всплывающую рекламу Premium + Скрывает баннер продления Premium. + Скрыть баннер продления Premium + Скрывает баннер с уведомлением о промо акции. + Скрыть баннер с уведомлением о промо акции + Скрывает полку \"Семплы\" в ленте. + Скрыть полку \"Семплы\" + Скрыть \"О YouTube Music\" + Скрыть \"Экономия трафика\" + Скрыть \"Скачивание и хранение\" + Скрыть \"Общие\" + Скрыть \"Уведомления\" + Скрыть \"Оформить подписку Music Premium\" + Скрыть \"Семейный центр\" + Скрыть \"Воспроизведение\" + Скрыть \"Конфиденциальность и данные\" + Скрыть \"Рекомендации\" + "Скрывает элементы меню настроек. +При этом скрывается не только меню настроек YT Music, но и меню настроек ReVanced Extended." + Скрыть меню настроек + Скрывает кнопку поиска звука в строке поиска. + Скрыть кнопку поиска звука + Скрывает кнопку \"Обновить\". + Скрыть кнопку \"Обновить\" + Скрывает пункт \"Конфиденциальность • Условия\". + Скрыть \"Конфиденциальность • Условия\" + Скрывает кнопку голосового поиска в строке поиска. + Скрыть кнопку голосового поиска + Аккаунт + Панель действий + Реклама + Выдвижное меню + Основные + Разное + Панель навигации + Плеер + Вернуть имя пользователя YouTube + Вернуть YouTube Dislike + SponsorBlock + Меню настроек + Видео + Запоминает последнюю выбранную скорость воспроизведения. + Запоминать изменения скорости + Показывать всплывающее уведомление при смене скорости воспроизведения по умолчанию. + Показывать всплывающее уведомление + Скорость по умолчанию изменена на %s. + Запоминает состояние переключателя \"Повтор воспроизведения\". + Запоминать состояние повтора + Запоминает состояние переключателя \"Перемешать\". + Запоминать состояние перемешивания + Запоминает последнее выбранное качество видео. + Запоминать изменения качества видео + Показывать всплывающее уведомление при смене качества видео по умолчанию. + Показывать всплывающее уведомление + Качество по умолчанию при моб. сети изменено на %s. + Не удалось установить качество. + Качество по умолчанию при Wi-Fi изменено на %s. + "Убирает окно о нежелательном контенте. +Не обходит возрастное ограничение, просто принимает его автоматически." + Убрать окно о нежелательном контенте + Видео продолжается с текущего времени просмотра при переходе на YouTube. + Продолжить просмотр + Заменяет \"Очистить очередь\" на \"Открыть в YouTube\'. + Замена пункта \"Очистить очередь\" + Смотреть на YouTube + Нерабочая ссылка на видео. + Сохраняет пункт меню \"Пожаловаться\" в комментариях не тронутым. + Сохранить пункт \"Пожаловаться\" + Заменяет \"Пожаловаться\" на \"Скорость воспроизведения\". + Заменить \"Пожаловаться\" + Возвращает всплывающие панели комментариев к старому стилю. + Восстановить старые всплывающие панели комментариев + Возвращает фон проигрывателя к старому стилю. + Восстановить старый фон плеера + "Возвращает интерфейс проигрывателя к старому стилю. +Некоторые вещи могут работать неправильно в старом интерфейсе проигрывателя." + Восстановить старый интерфейс проигрывателя + Возвращает вкладку \"Библиотека\" к старому стилю. (Экспериментальная опция) + Восстановить старый стиль вкладки \"Библиотека\" + \@псевдоним (Имя пользователя) + Выбор формата отображения имени пользователя. + Формат отображения + Имя пользователя (@псевдоним) + Имя пользователя + Заменяет псевдонимы имена пользователей в комментариях. + Включить возврат имени пользователя YouTube + "Чтобы заменить псевдонимы на имена пользователей, необходим ключ разработчика YouTube Data API v3. + +Ежедневная квота для ключей API в бесплатном тарифе составляет 10 000 и 1 квота используется для замены псевдонима на имя пользователя для 1 комментария. + +Нажмите, чтобы узнать, как создать ключ API." + О ключе YouTube Data API + Ключ разработчика для использования API YouTube Data v3. + Ключ YouTube Data API + 1. Перейдите в раздел <a href=%1$s>Создать New Project</a>.<br>2. Нажмите кнопку <b>CREATE</b>.<br>3. Перейдите в <a href=%2$s>YouTube Data API v3</a>.<br>4. Нажмите кнопку <b>ENABLE</b>.<br>5. Нажмите кнопку <b>CREATE CREDENTIALS</b>.<br>6. Выберите <b>Public data</b>.<br>7. Нажмите кнопку <b>NEXT</b>.<br>8. Скопируйте ключ API. <br><br>※ Ключ API нельзя предоставлять другим, поэтому он не включен в Импорт/Экспорт настроек. + Создание ключа разработчика YouTube Data API v3 + Об интеграции + Данные предоставлены Return YouTube Dislike API. Нажмите здесь, чтобы узнать больше. + ReturnYouTubeDislike.com + Скрывает линию после кнопки \"Нравится\". + Компактная кнопка \"Нравится\" + Вместо числа отметок \"Не нравится\", они отображаются как процент. + Кол-во отметок \"Не нравится\" в процентах + Отображает количество отметок \"Не нравится\" в видео. + Включить Return YouTube Dislike + Показывает примерное количество лайков видео. + Показать приблизительное количество лайков + Отметки \"Не нравится\" недоступны (достигнут лимит клиентов сервера API). + Отметки \"Не нравится\" недоступны (состояние %d). + Отметки \"Не нравится\" недоступны (время API истекло). + Отметки \"Не нравится\" недоступны (%s). + Отображает всплывающее уведомление, когда API Return YouTube Dislike недоступен. + Уведомлять, когда API недоступен + Скрыто + Убирает параметры отслеживания запросов из адресов при отправке ссылки. + Подчищать ссылки + Об интеграции + sponsor.ajay.app + Данные предоставлены SponsorBlock API. Нажмите здесь, чтобы узнать больше и увидеть опцию загрузки для других платформ. + Изменить адрес сервера API + Адрес сервера API изменён. + Адрес сервера API недействителен. + Адрес сервера API сброшен. + Адрес, используемый для связи с сервером SponsorBlock. Не изменяйте его, если не знаете, что делаете. + Цвет изменён. + Цвет: + Неверный код цвета. Значения сброшены к начальным. + Цвет сброшен. + Изменить поведение сегмента + Включить SponsorBlock + SponsorBlock - это реализованная с помощью участия сообщества система для пропуска раздражающих частей видео в YouTube. + Сбросить цвет + Отвлечённые темы / Шутки + Сегменты, которые увеличивают длительность видео за счёт отвлечённых тем или шуток, но не требуются для понимания основного содержания. Не включает сегменты, объясняющие контекст или предысторию. + Напоминание о взаимодействии (подписка) + Короткое напоминание поставить отметку \"Нравится\", подписаться на канал или соцсети посреди ролика. Если эта вставка длительная или о чём-то конкретном, она должна классифицироваться как самореклама. + Пауза / Интро + Интервал без фактического содержания. Это может быть пауза, статический кадр или повторяющаяся анимация. Не включает переходы, содержащие информацию. + Музыка: Сегмент без музыки + Только для использования в музыкальных роликах. Разделы музыкальных видео без музыки, которые ещё не охвачены другой категорией. + Конечная заставка / Титры + Титры или время появления конечных заставок YouTube. Не для подведения итогов сказанного в видео. + Предпросмотр / Краткое содержание / Завязка + Краткое содержание предыдущих эпизодов или предварительный просмотр того, что будет в данном видео. + Безвозмездная реклама / Самореклама + Похоже на спонсорскую рекламу, за исключением безвозмездной рекламы или саморекламы. Включает разделы о товарах, пожертвованиях или информации о том, с кем они сотрудничали. + Спонсорская реклама + Платные промоакции, платные рефералы и прямая реклама. Не для саморекламы или бесплатных рекомендаций о делах / создателях / веб-сайтах / продуктах, которые им нравятся. + Автоматически пропускать + Отключить + Пропущен наполнитель. + Пропущено назойливое напоминание. + Пропущено интро. + Пропущена пауза. + Пропущена пауза. + Пропущено несколько сегментов. + Пропущен сегмент без музыки. + Пропущена концовка. + Пропущен предпросмотр. + Пропущено краткое повторение. + Пропущен предпросмотр. + Пропущена самореклама. + Пропущена спонсорская реклама. + SponsorBlock временно недоступен. + SponsorBlock временно недоступен (состояние %d). + SponsorBlock временно недоступен (время API истекло). + Показывать тост, если API недоступен + Отображает всплывающее уведомление, когда SponsorBlock недоступен. + Показать тост когда пропущен сегмент автоматически + Отображает всплывающее уведомление при автоматическом пропуске сегмента. + Настройки скопированы в буфер. + "Подменяет версию клиента на старую. + +• Это изменит внешний вид приложения, но могут возникнуть неизвестные проблемы. +• Если отключить данную опцию после её активации, старый интерфейс может оставаться до тех пор, пока данные приложения не будут очищены." + 4.27.53 - Отключить режим радиостанции в канадских регионах + 6.11.52 - Отключить динамические текста + 7.16.53 - Восстановить старую панель действий + Выберите целевую версию приложения для подмены. + Целевая версия приложения при подмене + Подмена версии приложения + "Подмена клиента для предотвращения проблем с воспроизведением. + +Ограничения: +• Кодек OPUS может не работать. +• Превью во время перемотки может отсутствовать. +• История просмотра не работает при использовании аккаунта компании." + Подмена клиента + Показывает клиент, используемый для получения потоковых данных в Статистике для сисадминов. + Показывать в \"Статистике для сисадминов\" + "Подделать потоковые данные, чтобы предотвратить проблемы с воспроизведением. + +※ При использовании вместе с \"Подменой клиента\" могут возникнуть проблемы с воспроизведением." + Подделка потоковых данных + Android TV + Android VR + iOS + iOS Music + Определяет клиент по умолчанию, получающий потоковые данные. + Клиент по умолчанию + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/tr-rTR/missing_strings.xml b/patches/src/main/resources/music/translations/tr-rTR/missing_strings.xml new file mode 100644 index 000000000..6a2f801d8 --- /dev/null +++ b/patches/src/main/resources/music/translations/tr-rTR/missing_strings.xml @@ -0,0 +1,29 @@ + + + Don\'t show again + Disables DRC (Dynamic Range Compression) applied to audio. + Disable DRC audio + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + Hide Pin to Speed dial menu + Hide Unpin from Speed dial menu + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + Android TV + Android VR + iOS + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/tr-rTR/strings.xml b/patches/src/main/resources/music/translations/tr-rTR/strings.xml new file mode 100644 index 000000000..9b8fdd002 --- /dev/null +++ b/patches/src/main/resources/music/translations/tr-rTR/strings.xml @@ -0,0 +1,417 @@ + + + Devam Et + "GmsCore'un arka planda çalışma izni yoktur. + +'Uygulamamı öldürmeyin!' cihazınız için kılavuzu kullanın ve talimatları GmsCore kurulumunuza uygulayın. + +Uygulamanın çalışması için bu gereklidir." + "Sorunları önlemek için GmsCore pil optimizasyonları devre dışı bırakılmalıdır. + +Devam düğmesine dokunun ve pil optimizasyonlarını devre dışı bırakın." + Web sitesini aç + Eylem gerekiyor + Bildirimleri alabilmek için bulut mesajlaşmayı etkinleştirin. + GmsCore\'yi aç + GmsCore yüklü değil. Yükleyin. + Bazı bölgelerde engellenen alan adını değiştirerek oynatma listesi küçük resimlerinin, kanal avatarlarının vb. alınabilmesini sağlar. + Resimlerin bölge kısıtlamalarını atla + Uygulama içi paylaşım sayfasını sistem paylaşım sayfasıyla değiştirin. + Paylaşım sayfasını değiştir + Listeler + Keşfet + Ana Sayfa + Kitaplık + Abonelikler + Uygulamanın başlangıç sayfasını değiştirin. + Başlangıç ​​sayfasını değiştir + Yeni satırlarla ayrılmış olarak hangi bileşenlerin filtreleneceğini yapılandırın. + Özel filtreyi düzenle + Düzen bileşenlerini gizlemek için özel filtreyi etkinleştirir. + Özel filtreyi etkinleştir + Geçersiz özel filtre: %s. + Özel hızlar %sx\'den küçük olmalıdır. + Geçersiz özel oynatma hızları. + Mevcut oynatma hızlarını değiştirin. + Özel oynatma hızlarını düzenle + YouTube Müzik bağlantılarını RVX Music\'te açmak için \'Desteklenen bağlantıları aç\'ı etkinleştirin ve desteklenen web adreslerini etkinleştirin. + Varsayılan uygulama ayarlarını aç + Altyazıların kendiliğinden açılmasını devre dışı bırakır. + Altyazıların kendiliğinden açılmasını kapat + Uygulama açılışındaki Kahire açılış animasyonunu devre dışı bırakır. + Kahire açılış animasyonunu devre dışı bırak + Beğenmedim düğmesine tıklandığında sonraki parçaya yönlendirmeyi devre dışı bırakır. + Beğenmeme yönlendirmesini devre dışı bırak + Mini oynatıcıdaki parçaları değiştirmek için kaydırmayı devre dışı bırakın. + Mini oynatıcı hareketini devre dışı bırak + Oynatıcıdaki parçaları değiştirmek için kaydırmayı devre dışı bırakın. + Oynatıcı hareketini devre dışı bırak + Gezinme çubuğunun rengini siyah yapar. + Siyah gezinme çubuğunu etkinleştir + Oynatıcının arka plan rengini siyaha değiştirir. + Siyah oynatıcı arka planını etkinleştir + Küçültülmüş oynatıcının rengi ile tam ekran oynatıcının rengini eşler. + Oynatıcı renk eşlemesini etkinleştir + "Telefonda kompakt iletişim kutusunu etkinleştirin. + +Bilinen sorunlar: +• Kitaplık sekmesindeki albüm resmi, ızgara halinde düzenlendiğinde küçülür. +• Uyku zamanlayıcısı düzeni olağandışı görünebilir." + Kompakt diyaloğu etkinleştir + Hata ayıklama günlüklerini arabellek dahiline yazdırır. + Hata ayıklama günlüklerine arabelleği kaydet + Hata ayıklama günlüğünü yazdırır. + Hata ayıklama günlüğünü etkinleştir + Başka bir kayıt oynatılıyor ise oynatıcıyı tamamen küçült. + Zorla küçültülmüş pencereyi aktifleştir + Telefonlarda ekran döndürme ile manzara moduna geçiş sağlar. + Yatay Modu Etkinleştir + Mini oynatıcıda sonraki düğmesini etkinleştirir. + Mini oynatıcıyıdaki sonraki düğmesini etkinleştir + Mini oynatıcıda önceki düğmesini etkinleştirir. + Mini oynatıcıdaki önceki düğmesini etkinleştir + "Mp4a ses codec bileşeni yerine opus ses codec bileşenini etkinleştirir. + +Bilgi: +• En yeni Android istemcileri varsayılan olarak opus ses codec bileşenini kullanır. +• Bu yalnızca çok eski istemcilerle sahtecilik yapan kullanıcılar için geçerlidir." + Opus kodeğini etkinleştir + Mini oynatıcıyı kapatmak için aşağı kaydırmayı etkinleştirir. + Mini oynatıcıyı kapatmak için kaydırmayı etkinleştirin + "Oynatma hızı açılır menüsüne 'Sessizliği kırp' geçişini etkinleştirir. + +Bilgi: +• Bu özellik podcast'ler içindir. +• Bu özellik henüz geliştirme aşamasında olduğundan kararsız olabilir." + Kırpma sessizliğini etkinleştir + Zen modu podcast\'lere de uygulanır. + Podcastlerde zen modunu aç/kapa + Video oynatıcıya açık gri bir ton ekleyerek göz yorgunluğunu azaltır. + Zen modunu aç/kapa + Varsayılan değerlere sıfırlayın. + Düzeni normal şekilde yüklemek için yeniden başlatın + Yenile ve yeniden başlat + Ayarları bir dosyaya dışa aktar + Ayarlar dışa aktarılamadı. + Ayarlar başarıyla dışa aktarıldı. + İçe aktar + Ayarları dosyadan içe aktar + Kopyala + Ayarları yazı olarak içe / dışa aktar + Ayarları içe veya dışa aktarın. + Ayarları içe / dışa aktar + İçe aktarma başarısız oldu: %s. + Ayarlar varsayılana sıfırlandı. + %d ayar içe aktarıldı. + Sıfırla + ReVanced Extended + "İndir düğmesi harici indiricinizi açar. + +• Yalnızca oynatıcıdaki indirme işlemi düğmesini geçersiz kılar. +• Açılır menü veya kitaplıktaki indirme düğmesini geçersiz kılmaz." + İndirme eylem düğmesini kullan + Harici indirici + "%1$s yüklü değil. +Lütfen web sitesinden %2$s dosyasını indirin." + Uyarı + %s kurulmamış. Lütfen önce indiriniz. + Yüklü olan harici indirme uygulamanızın paket adı, örneğin NewPipe veya YTDLnis gibi. + Harici indirici paket adı + Hesap menüsündeki boş bileşenleri gizler. + Boş bileşenleri gizle + Filtrelenecek hesap menüsü adlarının yeni satırlarla ayrılmış listesi. + Hesap menüsü filtresini düzenle + Özel filtreyi kullanarak hesap menüsü öğelerini gizler. + Hesap menüsünü gizle + Kaydet butonunu gizler. + Kaydet butonunu gizle + \"Yorumlar\" butonunu gizler. + \"Yorumlar\" butonunu gizle + İndir butonunu gizler. + İndir butonunu gizle + Aksiyon butonlarındaki etiketleri gizle. + Aksiyon butonu etiketlerini gizle + Beğenme ve beğenmeme düğmelerini gizler. Eski oynatıcı arayüzünde çalışmayabilir. + \"Beğen\" ve \"Beğenme\" butonlarını gizle + Radyo düğmesini gizler. + Radyo düğmesini gizle + Paylaş butonunu gizler. + Paylaş butonunu gizle + Oynatıcıdaki ses video geçiş anahtarını gizler. + Ses video geçiş anahtarını gizle + Düğme rafını ana sayfadan ve keşfet sekmesinden gizler. + Tuş rafını gizle + Döner rafı ana sayfa ve keşfet sekmesinden gizler. + Atlıkarınca rafını gizle + Yayınlama düğmesini gizler. + \"Yayınla\" butonunu gizle + Kategori çubuğunu gizler. + Kategor barını Gizle + Yorumları bölümünün üst kısmında kanal kurallarını gizler. + Kanal yönergelerini gizle + Yorum yazarken zaman damgası ve emoji düğmelerini gizle. + Zaman damgası ve emoji düğmelerini gizle + Çift tıklayarak sürükleme etkinken kara arayüzü gizler. + Çift tıklama arayüz filtresini gizle + Kitaplıktaki Yüzen Butonu Gizle. + Yüzen Butonu Gizle + 3-sütunlu bileşenleri gizle + Kuyruğa ekle menüsünü gizle + \"Altyazılar\" menüsü + Oynatma listesini sil menüsünü gizle + Kuyruğu kapat menüsünü gizle + İndirme menüsünü gizle + Oynatma listesini düzenle menüsünü gizle + Albüm menüsüne gitme menüsünü gizle + Artist menüsüne gitme menüsünü gizle + Bölüm menüsüne gitmeyi gizle + Podcast menüsüne gitmeyi gizle + Yardım & geri bildirim menüsünü gizle + Beğen ve beğenme butonunu gizle + Sonraki menüyü oynat\'ı gizle + Kalite menüsünü gizle + Kitaplıktan kaldır menüsünü gizle + \"Oynatma listesinden kaldır\" menüsünü gizle + Rapor menüsünü gizle + Bölümü daha sonra için kaydet menüsünü gizle + Kitaplığa kaydet menüsünü gizle + Oynatma listesine kaydet menüsünü gizle + Paylaş menüsünü gizle + Karışık çal menüsünü gizle + Uyku zamanlayıcısı menüsünü gizle + Radyoyu başlat menüsünü gizle + \"Meraklısı için istatikler\" menüsü + Abone ol / Abonelikten çık menüsünü gizle + Şarkı kredileri menüsünü görüntüle + "Tam ekran reklamları gizle. + +Sınırlamalar: +• Bazen ana akış yerine boş siyah bir ekran görebilirsiniz." + Tam ekran reklamlarını gizle + Tam ekran oynatıcısındaki paylaş butonunu gizler. + Tam ekran\'daki paylaş butonunu gizle + Genel reklamları gizler. + Genel reklamları gizle + Hesap menüsündeki etiketi gizler. + Etiketi gizle + Araç çubuğundaki geçmiş düğmesini gizler. + Geçmiş düğmesini gizle + Bir parça çalınmadan önce reklamları gizler. + Müzik reklamlarını gizle + Gezinme çubuğunu gizler. + Gezinme çubuğunu gizle + Keşfet düğmesini gizler. + Keşfet düğmesini gizle + Ev düğmesini gizler. + Ana sayfa düğmesini gizle + Gezinme çubuğunun altındaki etiketleri gizleyin. + Navigasyon paneli altyazılarını gizle + Kitaplık düğmesini gizler. + Kitaplık düğmesini gizle + Örnek düğmesini gizler. + Örnekler düğmesini gizle + Gūncelle düğmesini gizler. + Gūncelle düğmesini gizle + Araç çubuğundaki bildirim düğmesini gizler. + Bildirim butonunu gizle + Ücretli tanıtım yazısını gizler. + Ücretli tanıtım yazısını gizle + Oynatma listesi kartı rafını ana sayfadan gizler. + Çalma listesi kartı rafını gizle + Premium promosyonu açılır penceresini gizler. + Premium promosyonu açılır penceresini gizler + Premium yenileme başlığını gizler. + Premium yenileme başlığını gizle + Promosyon uyarı başlığını gizler. + Promosyon uyarı başlığını gizle + Feed\'deki örnek rafını gizler. + Örnek rafını gizle + Hakkında menüsünü gizle + Veri tasarrufu menüsünü gizle + İndirmeleri gizle & depolama menüsü + Genel menüyü gizle + Bildirimler menüsünü gizle + YT Müzik Premium Edinin menüsünü gizle + Aile Merkezi menüsünü gizle + Oynatma menüsünü gizle + Gizliliği gizle & veri menüsü + Öneriler menüsünü gizle + "Ayarlar menüsünün öğelerini gizleyin. +Bu, yalnızca YT Music ayarlar menüsünü değil aynı zamanda ReVanced Extended ayarlar menüsünü de gizler." + Ayarlar menüsünü gizle + Arama çubuğundaki ses arama düğmesini gizler. + Sesli arama düğmesini gizle + Güncellemek için tıkla butonunu gizler. + Güncellemek için tıkla butonunu gizle + Hizmet Şartları kapsayıcısını gizler. + Terimler kapsayıcısını gizle + Arama çubuğundaki sesli arama düğmesini gizler. + Sesli arama düğmesini gizle + Hesap + Görev Çubuğu + Reklamlar + Açılır menü + Genel + Diğer ayarlar + Gezinti çubuğu + Oynatıcı + YouTube Kullanıcı Adına geri dönüş + Return YouTube Dislike + SponsorBlock + Ayarlar menüsü + Video + Oynatma hızını değiştirdiğinizde en son seçili oynatma hızı değerini hatırlar. + Oynatma hızı değişimlerini hatırla + Varsayılan oynatma hızını değiştirirken bir tost bildirimi göster. + Tost göster + Varsayılan hız %s olarak değiştiriliyor. + Tekrarın durumunu hatırlar. + Tekrarın durumunu hatırlar + Karıştır durumunu hatırlar. + Karıştır durumunu hatırlar + Seçili video kalitesini hatırlar. + Video kalitesi değişimlerini hatırla + Varsayılan video kalitesini değiştirirken bir tost bildirimi göster. + Bir Tost bildirimi göster + Mobil ağda varsayılan video kalitesi %s olarak değiştiriliyor. + Kalite ayarlanamadı. + Wi-Fi\'da varsayılan video kalitesi %s olarak değiştiriliyor. + "Görüntüleyicinin takdirine ilişkin iletişim kutusunu kaldırır. Bu yaş sınırlamasını atlamaz. Sadece otomatik olarak kabul ediyor." + İzleyicinin takdirine bağlı iletişim kutusunu kaldır + YouTube\'da izlerken geçerli saatten itibaren izlemeye devam ettirir. + İzlemeye devam et + \'Sırayı kapat\' seçeneğini \'YouTube\'da İzle\' ile değiştirir. + Kuyruğu görmezden gelmeyi değiştir + YouTube\'da izle + Geçersiz video url\'si. + Yorumlardaki rapor menüsü değiştirilmeyecektir. + Yalnızca oynatıcı açılır menüsü için geçerlidir + \'Rapor\'u \'Oynatma hızı\' ile değiştirir. + Rapor menüsünü değiştir + Yorum açılır pencerelerini eski stile döndürür. + Eski yorum açılır panellerini geri getir + Oynatıcı arkaplanını eski stile döndürür. + Eski oynatıcı arka planını geri getir + "Oynatıcı düzenini eski stile döndürün. +Eski oynatıcı düzeninde bazı ayarlar düzgün çalışmayabilir." + Eski oynatıcı düzenini geri getir + Kütüphane rafını eski stile döndürün. +(Deneysel) + Eski stil kitaplık rafını geri getir + \@HerkeseAçıkKullanıcıAdı (Kullanıcı Adı) + Kullanıcı adı görüntüleme biçimini seçin. + Ekran formatı + Kullanıcı Adı (@HerkeseAçıkKullanıcıAdı) + Kullanıcı Adı + Yorumlardaki kullanıcı adlarını herkese açık kullanıcı adlarıyla (YT Handles ile) değiştirir. + YouTube Kullanıcı Adına dönmeyi etkinleştir + "Herkese açık kullanıcı adlarını (YouTube Handles) kullanıcı adlarıyla değiştirmek için bir YouTube Data API v3 Geliştirici Anahtarı gereklidir. + +Ücretsiz planda APl anahtarları için günlük kota 10.000'dir ve 1 yorum için bir tanıtıcıyı bir kullanıcı adıyla değiştirmek için 1 kota kullanılır. + +APl anahtarının nasıl verileceğini görmek için tıklayın." + YouTube Verileri API anahtarı hakkında + YouTube Data API v3\'ü kullanmak için geliştirici anahtarı. + YouTube Verileri API anahtarı + 1. <a href=%1$s>Yeni proje oluştur</a>.<br> \'a git. +2. <b>OLUŞTUR</b> butonuna tıklayın.<br> +3. <a href=%2$s>YouTube Verileri API v3</a>.<br> \'e gidin. +4. <b>ETKİNLEŞTİR</b> butonuna tıklayın.<br> +5. <b>KİMLİK BİLGİLERİ OLUŞTUR</b> butonuna tıklayın.<br> +6. <b>Herkese açık veriler</b> seçeneğini seçin.<br> +7. <b>SONRAKİ</b> butonuna tıklayın.<br> +8. API anahtarını kopyalayın.<br><br>※ API anahtarı asla başkalarıyla paylaşılmamalıdır, bu nedenle İçe/Dışa Aktarma ayarlarına dahil değildir. + YouTube Data API v3 geliştirici anahtarı sorunu + Hakkında + Veriler True RYD Worker API tarafından sağlanır. Daha fazlasını öğrenmek için buraya dokunun. + ReturnYouTubeDislike.com + Beğen butonunun ayırıcısını gizler. + Kompakt beğenme düğmesi + Beğenmeme sayısı yerine beğenmeme yüzdesi gösterilir. + Yüzde olarak beğenmemeler + Videoların beğenmeme sayısını gösterir. + Return YouTube Dislike\'ı etkinleştir + Videoların tahmini beğeni sayısını gösterir. + Tahmini beğenileri göster + Beğenmeme sayısı mevcut değil (istemci API sınırına ulaşıldı). + Beğenmemeler mevcut değil (durum %d). + Beğenmemeler geçici olarak kullanılamıyor (API zaman aşımına uğradı). + Beğenmemeler mevcut değil (%s). + Return YouTube Dislike API mevcut değilse uyarı gösterir. + API mevcut değilse bir uyarı göster + Gizlendi + Bağlantıları paylaşırken, tracking query parametrelerini URL\'lerden kaldırır. + Paylaşılan bağlantıları sterilize edin + Hakkında + sponsor.ajay.app + Veriler, SponsorBlock API tarafından sağlanmaktadır. Daha fazla bilgi edinmek ve diğer platformlar için indirmeleri görmek için buraya dokunun. + API URL\'sini değiştir + API URL\'si değiştirildi. + API bağlantısı geçersiz. + API URL\'si sıfırlandı. + SponsorBlock\'un sunucuya çağrı yapmak için kullandığı adres. Ne yaptığınızı bilmiyorsanız bunu değiştirmeyin. + Renk değiştirildi. + Renk: + Renk kodu geçersiz. + Renk sıfırlandı. + Segment davranışını değiştir + SponsorBlock\'u etkinleştir + SponsorBlock, YouTube videolarının sinir bozucu kısımlarını atlamak için kitle kaynaklı bir sistemdir. + Rengi sıfırla + Dolgu Tanjantı / Şakalar + Sadece videoyu doldurmak ya da mizah için eklenmiş, videonun ana içeriğini anlamak için gerekli olmayan alakasız sahneler. Bu, içerik veya arka plan detayları hakkında bilgi veren kısımları içermemelidir. + Etkileşim Hatırlatıcısı (Abone Ol) + İçeriğin ortasında onları beğenmeniz, abone olmanız veya takip etmeniz için kısa bir hatırlatma. Uzunsa veya belirli bir şeyle ilgiliyse, bunun yerine kendini tanıtma altında olmalıdır. + Ara / Giriş Animasyonu + Gerçek içeriği olmayan bir aralık. Bir duraklama, statik çerçeve veya yinelenen animasyon olabilir. Bilgi içeren geçişleri içermez. + Müzik: Müzik Dışı Bölüm + Yalnızca müzik videolarında kullanım içindir. Henüz başka bir kategoride yer almayan müzik videolarının müziksiz bölümleri. + Bitiş Kartları / Hakkında + Videoda emeği geçenlerin veya video sonunda çıkan kartların gösterildiği kısımlar. Bilgilendirici sona sahip videolar için değil. + Önizleme / Özet / Hook + Videoda veya bir dizinin diğer videolarında neler olduğunu veya neler olduğunu gösteren, tüm bilgilerin başka bir yerde tekrarlandığı klip koleksiyonu. + Karşılıksız/Kişisel Promosyon + Ücretsiz veya kendi kendine tanıtım haricinde Sponsora benzer. Ürünler, bağışlar veya kiminle işbirliği yaptıklarına ilişkin bilgilerle ilgili bölümler içerir. + Sponsor + Ücretli tanıtım, ücretli yönlendirmeler ve doğrudan reklamlar. Kendi tanıtımını yapmak veya beğendikleri olaylara / içerik üreticilerine / web sitelerine / ürünlere ücretsiz olarak atıfta bulunanlar için değil. + Otomatik olarak atla + Devre dışı bırak + Dolgu atlandı. + Rahatsız edici hatırlatıcı atlandı. + Giriş ekranı atlandı. + Ara atlandı. + Ara atlandı. + Birden çok segment atlandı. + Sessiz kısım atlandı. + Kapanış ekranı atlandı. + Ön izleme atlandı. + Seans atlandı. + Ön izleme atlandı. + Kişisel promosyon atlandı. + Sponsor atlandı. + SponsorBlock geçici olarak kullanılamıyor. + SponsorBlock geçici olarak kullanılamıyor (durum %d). + SponsorBlock geçici olarak kullanılamıyor (API zaman aşımına uğradı). + API mevcut olmadığında bir uyarı göster + SponsorBlock API\'nin mevcut olmaması durumunda uyarı gösterilir. + Otomatik olarak atlarken bir uyarı göster + Bölüm otomatik olarak atandığında uyarı gösterilir. + Ayarlar panoya kopyalandı + "İstemciyi sürümünün eski sürümle sahteleştir + +• Bu, uygulamanın görünümünü değiştirir ancak bilinmeyen yan etkiler ortaya çıkabilir. +• Daha sonra kapatılırsa, uygulama verileri temizlenene kadar eski kullanıcı arayüzü kalabilir." + 4.27.53 - Kanada bölgelerinde radyo modunu devre dışı bırakın + 6.11.52 - Gerçek zamanlı şarkı sözlerini devre dışı bırak + 7.16.53 - Eski eylem çubuğunu geri yükle + Sahte uygulama sürümü hedefini seçin. + Uygulama hedef sürümünü kandırma hedefi + Uygulama Versiyonunu taklit et + "\"Oynatma sorunlarını önlemek için istemciyi taklit edin. + +Sınırlamalar: +• OPUS ses kodeği desteklenmiyor olabilir. +• Arama çubuğu küçük resmi mevcut olmayabilir. +• İzleme geçmişi marka hesabında çalışmaz." + Sahte istemci + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/uk-rUA/missing_strings.xml b/patches/src/main/resources/music/translations/uk-rUA/missing_strings.xml new file mode 100644 index 000000000..0f4771465 --- /dev/null +++ b/patches/src/main/resources/music/translations/uk-rUA/missing_strings.xml @@ -0,0 +1,13 @@ + + + Don\'t show again + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/uk-rUA/strings.xml b/patches/src/main/resources/music/translations/uk-rUA/strings.xml new file mode 100644 index 000000000..8e46fc0b3 --- /dev/null +++ b/patches/src/main/resources/music/translations/uk-rUA/strings.xml @@ -0,0 +1,430 @@ + + + Продовжити + "GmsCore не дозволено працювати у фоні. + +Дотримуйтесь посібника \"Don't kill my app\" для вашого пристрою і застосуйте інструкції для встановлення GmsCore. + +Це необхідно для того, щоб програма працювала." + "Необхідно вимкнути оптимізацію енергії для MicroG GmsCore, щоб запобігти проблемам. + +Вимкнення оптимізації енергії для MicroG не вплине негативно на час автономної роботи. + +Натисніть кнопку \"Продовжити\" та вимкніть оптимізацію." + Відкрити сайт + Потрібна дія + Увімкніть \"Хмарні повідомлення\", щоб отримувати сповіщення. + Відкрити GmsCore + GmsCore не встановлено. Встановіть. + Замінює домен для зображень, заблокований у деяких регіонах, що дозволить отримувати мініатюри списків відтворення, аватари каналів тощо. + Змінити домен зображень + Змінює тип вікна діалогу поширення з вбудованого на системний. + Змінити діалог поширення + Хіт-паради + Навігація + Головна + Бібліотека + Підписка + Виберіть сторінку з якої буде стартувати додаток. + Змінити початкову сторінку + Список рядків конструктора шляхів компонентів для фільтрування, розділених новими рядками. + Редагувати користувацький фільтр + Вмикає користувацькі фільтри для приховування компонентів інтерфейсу. + Увімкнути користувацький фільтр + Недопустимий користувацький фільтр: %s. + Користувацькі швидкості мають бути меншими за %sx. + Неправильні користувацькі швидкості відтворення. + Налаштувати доступні швидкості відтворення. + Редагувати користувацькі швидкості відтворення + Щоб відкривати посилання на YouTube Music у RVX Music, увімкніть \"Відкривати підтримувані посилання\" та активуйте підтримувані веб-адреси. + Відкрити налаштування за замовчуванням + Вимикає автоматичне ввімкнення субтитрів. + Вимкнути примусові авто субтитри + Вимикає сплеш анімацію Каїр під час запуску застосунку. + Вимкнути сплеш анімацію Каїр + Вимикає перенаправлення на наступний трек при натисканні на кнопку \"Не подобається\". + Вимкнути перенаправлення при \"Не подобається\" + Вимикає DRC (стиснення динамічного діапазону), застосованого до аудіо. + Вимкнути DRC аудіо + Вимикає жести перемикання треків у мініплеєрі. + Вимкнути жести мініплеєра + Вимикає жести перемикання треків у плеєрі. + Вимкнути жести плеєра + Встановлює чорний колір для панелі навігації. + Увімкнути чорну панель навігації + Змінює адаптивний колір фона плеєра на чорний. + Увімкнути чорний фон плеєра + Колір мініплеєра повторює колір повноекранного плеєра. + Увімкнути колірну відповідність плеєрів + "Вмикає компактне спливаюче вікно на телефонах. + +Відомі проблеми: +• Обкладинка альбому на вкладці \"Бібліотека\" стає меншою, якщо вона впорядкована сіткою. +• Вікно \"Таймер сну\" може з'являтися незвично." + Увімкнути компактний вигляд меню + Включає буфер у журнал налагодження. + Увімкнути ведення журналу буфера налагодження + Виводить протокол налагодження. + Увімкнути протоколи налагодження + Тримає плеєр згорнутим, навіть коли відтворюється інший трек. + Увімкнути мініплеєр на постійній основі + Вмикає ландшафтний режим під час повороту екрана на телефонах. + Увімкнути ландшафтний режим + Додає кнопку наступного треку у мініплеєр. + Додати кнопку наступне у мініплеєр + Додає кнопку попереднього треку у мініплеєр. + Додати кнопку попереднє у мініплеєр + "Вмикає кодек OPUS, якщо відповідь від сервера містить кодек OPUS. + +Інформація: +• Останні YouTube Music клієнти за умовчанням використовують аудіокодек OPUS. +• Це буде корисно лише для користувачів, які користуються дуже старими клієнтами." + Увімкнути кодек OPUS + Вмикає жест вниз для закриття мініплеєру. + Увімкнути жест закриття мініплеєру + "Додає перемикач \"Пропуск тиші\" у спливаючому меню швидкості відео. + +Інформація: +• Ця функція призначена для подкастів. +• Ця функція все ще у розробці, тому може бути нестабільною." + Додати перемикач \"Пропуск тиші\" + Режим \"Дзен\" також застосовується до подкастів. + Увімкнути режим \"Дзен\" у подкастах + Змінює колір фона плеєра на світло-сірий, щоб зменшити навантаження на очі. + Увімкнути режим \"Дзен\" + Скинуто до значень за замовчуванням. + Перезапустіть, щоб нормально завантажився макет + Оновити та перезавантажити? + Експорт налаштувань у файл + Не вдалося експортувати налаштування. + Налаштування було вдало експортовано. + Імпортувати + Імпорт налаштувань із файлу + Копіювати + Імпорт або експорт налаштувань у вигляді тексту + Імпортує або експортує налаштування. + Імпорт / Експорт налаштувань + Не вдалося імпортувати налаштування: %s. + Налаштування скинуто до стандартних. + Налаштування в кількості: %d успішно відновлено + Скинути + ReVanced Extended + "Кнопка \"Завантажити\" відкриває ваш зовнішній завантажувач. + +• Підміняє лише кнопку \"Завантажити\" на панелі дій в плеєрі. +• Не підміняє кнопку \"Завантажити\" у спливаючому меню чи бібліотеці." + Підмінити \"Завантажити\" + Зовнішній завантажувач + "%1$s не встановлено. +Будь ласка, завантажте %2$s з сайту." + Увага + %s не встановлено. Будь ласка, встановіть його. + Ім\'я пакета встановленого зовнішнього завантажувача, наприклад NewPipe або YTDLnis. + Ім\'я пакета зовнішнього завантажувача + Приховує порожній простір у меню облікового запису + Приховати порожні компоненти + Список назв меню облікового запису для фільтрування, розділених новими рядками. + Фільтр меню облікового запису + Приховує елементи меню облікового запису використовуючи користувацький фільтр. + Приховати меню облікового запису + Приховує кнопку \"Зберегти\" (в список відтворення) на панелі дій плеєра. + Приховати \"Зберегти\" + Приховує кнопку \"Коментарі\" на панелі дій плеєра. + Приховати \"Коментарі\" + Приховує кнопку \"Завантажити\" на панелі дій плеєра. + Приховати \"Завантажити\" + Приховує підписи кнопок на панелі дій плеєра. + Приховати підписи панелі дій + Приховує кнопки \"Подобається\" та \"Не подобається\". Це не працює в старому інтерфейсі плеєра. + Приховати \"Подобається\" і \"Не подобається\" + Приховує кнопку \"Радіо\" на панелі дій плеєра. + Приховати \"Радіо\" + Приховує кнопку \"Поділитися\" на панелі дій плеєра. + Приховати \"Поділитися\" + Приховує перемикач \"пісня | відео\" у плеєрі. + Приховати перемикач \"пісня | відео\" + Приховує кнопки \"Новинки\", \"Хіт-паради\", \"Настрій і жанри\" на вкладці \"Навігація\". + Приховати категорії в Навігації + Приховує карусель треків на вкладках \"Головна\" та \"Навігація\". + Приховати карусель треків + Приховує кнопку \"Трансляція\" в плеєрі та мініплеєрі. + Приховати кнопку \"Трансляція\" + Приховує панель категорій. + Приховати панель категорій + Приховує правила каналу у верхній частині секції коментарів. + Приховати правила каналу + Приховує кнопки мітки часу та емодзі під час введення коментарів. + Приховати мітку часу та емодзі + Приховує затемнення, яке з’являється під час подвійного натискання для перемотування. + Приховати фільтр подвійного натискання + Приховує плаваючу кнопку у вкладці \"Бібліотека\". + Приховати плаваючу кнопку + Приховати 3-стовпцевий компонент + Приховати \"Додати в чергу\" + Приховати \"Субтитри\" + Приховати \"Видалити список відтворення\" + Приховати \"Відхилити чергу\" + Приховати \"Завантажити\" + Приховати \"Редагувати список відтворення\" + Приховати \"Перейти до альбому\" + Приховати \"Перейти на сторінку виконавця\" + Приховати \"Перейти до випуску\" + Приховати \"Перейти до подкасту\" + Приховати \"Довідка й відгуки\" + Приховати \"Подобається\" і \"Не подобається\" + Приховати \"Закріпити у швидкому виборі\" + Приховати \"Відтворити наступним\" + Приховати \"Якість\" + Приховати \"Вилучити з бібліотеки\" + Приховати \"Вилучити зі списку\" + Приховати \"Поскаржитись\" + Приховати \"Слухати згодом\" + Приховати \"Зберегти в бібліотеці\" + Приховати \"Зберегти в списку відтворення\" + Приховати \"Поділитися\" + Приховати \"Перемішати\" + Приховати \"Таймер сну\" + Приховати \"Увімкнути Радіо\" + Приховати \"Статистика для досвідчених користувачів\" + Приховати \"Підписатися / Скасувати підписку\" + Приховати \"Відкріпити зі швидкого вибору\" + Приховати \"Переглянути авторів пісні\" + "Приховує повноекранну рекламу. + +Обмеження: +• Іноді замість головної стрічки ви можете побачити порожній чорний екран." + Приховати повноекранну рекламу + Приховує кнопку \"Поділитися\" в повноекранному плеєрі. + Приховати \"Поділитися\" повноекранного режиму + Приховує загальну рекламу. + Приховати загальну рекламу + Приховує електронну пошту / @нік у меню облікових записів. + Приховати електронну пошту / @нік + Приховує кнопку історії на панелі інструментів вкладки \"Бібліотека\". + Приховати кнопку історії + Приховує рекламу перед відтворенням медіа. + Приховати медіарекламу + Приховує панель навігації. + Приховати панель навігації + Приховує кнопку \"Навігація\" на панелі навігації. + Приховати \"Навігація\" + Приховує кнопку \"Головна\" на панелі навігації. + Приховати \"Головна\" + Приховує підписи кнопок на панелі навігації. + Приховати підписи кнопок навігації + Приховує кнопку \"Бібліотека\" на панелі навігації. + Приховати \"Бібліотека\" + Приховує кнопку \"Семпли\" на панелі навігації. + Приховати \"Семпли\" + Приховує кнопку \"Підписка\" на панелі навігації. + Приховати \"Підписка\" + Приховує кнопку сповіщень на панелі інструментів. + Приховати кнопку сповіщень + Приховує мітку \"Містить пряму рекламу\". + Приховати \"Містить пряму рекламу\" + Приховує полицю карток списку відтворення в стрічці. + Приховати полицю карток списку відтворення + Приховує спливаючі вікна реклами підписки Music Premium. + Приховати спливаючу рекламу Premium + Приховує банер поновлення підписки Music Premium. + Приховати банер поновлення Premium + Приховує банер рекламних сповіщень. + Приховати рекламні сповіщення + Приховує полицю \"Семпли для вас\" у стрічці. + Приховати полицю \"Семпли\" + Приховати \"Про YouTube Music\" + Приховати \"Заощадження трафіку\" + Приховати \"Завантаження й зберігання\" + Приховати \"Загальні\" + Приховати \"Сповіщення\" + Приховати \"Підписатися на Music Premium\" + Приховати \"Сімейний Центр\" + Приховати \"Відтворення\" + Приховати \"Дані й конфіденційність\" + Приховати \"Рекомендації\" + "Приховує елементи меню налаштувань. +Це приховує не лише меню налаштувань YT Music, а й меню налаштувань ReVanced Extended." + Приховати меню налаштувань + Приховує кнопку пошуку музики у панелі пошуку. + Приховати кнопку пошуку музики + Приховує кнопку оновлення. + Приховати кнопку оновлення + Приховує контейнер \"Конфіденційність • Умови використання\". + Приховати \"Конфіденційність • Умови використання\" + Приховує кнопку голосового пошуку у панелі пошуку. + Приховати кнопку голосового пошуку + Обліковий запис + Панель дій + Реклама + Спливаюче меню + Загальні + Різне + Панель навігації + Плеєр + Повернути ім\'я користувача YouTube + Return YouTube Dislike + SponsorBlock + Меню налаштувань + Відео + Запам\'ятовує останню вибрану швидкість відтворення. + Запам\'ятовувати зміни швидкості відтворення + Показує тост під час зміни стандартної швидкості відтворення. + Показувати тост + Зміна типової швидкості на %s. + Запам\'ятовує стан кнопки \"Повтор відтворення\". + Запам\'ятовувати стан повтору + Запам\'ятовує стан кнопки \"Перемішати\". + Запам\'ятовувати стан перемішування + Запам\'ятовує останню вибрану якість відео. + Запам\'ятовувати зміни якості відео + Показує тост під час зміни стандартної якості відео. + Показувати тост + Зміна типової якості відео в мобільній мережі на %s. + Не вдалося встановити обрану якість. + Зміна типової якості відео для Wi-Fi мережі на %s. + "Вилучає діалог про небажаний контент. +Це не обходить вікові обмеження. Просто приймає їх автоматично." + Вилучити діалог про небажаний контент + Продовжує відео з поточного моменту під час переходу на YouTube. + Продовжити перегляд + Замінює пункт \"Відхилити чергу\" на пункт \"Дивитись на YouTube\". + Заміна \"Відхилити чергу\" + Дивитись на YouTube + Недійсна url-адреса відео. + Залишає пункт меню \"Поскаржитися\" в коментарях недоторканим. + Залишити \"Поскаржитися\" + Замінює пункт \"Поскаржитися\" на пункт \"Швидкість відео\". + Заміна \"Поскаржитися\" + Повертає старий стиль спливаючих панелей коментарів. + Відновити старі спливаючі панелі коментарів + Повертає фон плеєра до старого стилю. + Відновити старий фон плеєра + "Повертає інтерфейс плеєра до старого стилю. +Деякі функції можуть працювати не належним чином у старому інтерфейсі плеєра." + Відновити старий інтерфейс плеєра + Повертає старий стиль вкладки \"Бібліотека\". (Експериментальна опція) + Відновити старий стиль вкладки \"Бібліотека\" + \@псевдонім (Ім\'я користувача) + Вибрати формат відображення імені користувача. + Формат відображення + Ім\'я користувача (@псевдонім) + Ім\'я користувача + Замінює псевдоніми на імена користувачів у коментарях. + Увімкнути повернення імені користувача YouTube + "Щоб замінити псевдоніми на імена користувачів, потрібен ключ розробника YouTube Data API v3. + +Щоденна квота для ключів API у безкоштовному тарифі становить 10 000, і 1 квота використовується для заміни псевдоніма на ім’я користувача для 1 коментаря. + +Натисніть, щоб дізнатися, як створити ключ API." + Про ключ YouTube Data API + Ключ розробника для використання API YouTube Data v3. + Ключ YouTube Data API + 1. Перейдіть до <a href=%1$s>Створити New Project</a>.<br>2. Натисніть кнопку <b>CREATE</b>.<br>3. Перейдіть до <a href=%2$s>YouTube Data API v3</a>.<br>4. Натисніть кнопку <b>ENABLE</b>.<br>5. Натисніть кнопку <b>CREATE CREDENTIALS</b>.<br>6. Виберіть <b>Public data</b>.<br>7. Натисніть кнопку <b>NEXT</b>.<br>8. Скопіюйте ключ API.<br><br>※ Ключ API не можна надавати іншим, тому його не включено в Імпорт / Експорт налаштувань. + Створення ключа розробника YouTube Data API v3 + Про інтеграцію + Дані дизлайків надаються за допомогою Return YouTube Dislike API. Натисніть тут, щоб дізнатися більше. + ReturnYouTubeDislike.com + Приховує лінію між кнопкою \"Подобається\" та кількістю лайків. + Компактна кнопка \"Подобається\" + Відобажає відсоток замість кількості дизлайків. + Кількість дизлайків у відсотках + Показує кількість дизлайків у треках. + Увімкнути Return YouTube Dislike + Показує приблизну кількість лайків відео. + Показати приблизну кількість лайків + Дизлайки недоступні (досягнуто ліміт клієнтів сервера API). + Дизлайки недоступні (статус %d). + Дизлайки тимчасово недоступні (закінчився час API). + Дизлайки недоступні (%s). + Показує тост, якщо API ReturnYouTubeDislike не доступний. + Показувати тост, якщо API не доступний + Приховано + Видаляє параметри запиту відстеження з URL-адрес під час обміну посиланнями. + Обробляти поширення посилань + Про інтеграцію + sponsor.ajay.app + Дані надаються SponsorBlock API. Натисніть тут, щоб дізнатися більше та побачити завантаження для інших платформ. + Змінити URL-адресу API + Адресу сервера API змінено. + Недійсна адреса сервера API. + Адресу сервера API скинуто. + Адреса, яку SponsorBlock використовує для звернень до сервера. Не змінюйте це, якщо не знаєте, що робите. + Колір змінено. + Колір: + Недійсний код кольору. + Колір скинуто. + Змінити поведінку сегмента + Увімкнути SponsorBlock + SponsorBlock - це краудсорсингова система для пропускання дратівливих частин відео на YouTube. + Скинути колір + Дотичне наповнення / Жарти + Дотичні сцени, додані лише для наповнення або гумору, які не є необхідними для розуміння основного змісту відео. Не включає сегменти, що надають контекст або фонові деталі. + Нагадування про взаємодію (Підписка) + Коротке нагадування про вподобання, підписку або підписку посеред контенту. Якщо воно довге або про щось конкретне, його слід розмістити в розділі самореклами. + Пауза / Вступна Анімація + Інтервал без фактичного контенту. Може бути паузою, статичним кадром або повторюваною анімацією. Не включає переходи, що містять інформацію. + Музика: Немузична секція + Тільки для використання в музичних відео. Секції музичних відео без музики, які не підпадають під іншу категорію. + Кінцеві картки / Титри + Титри або коли з\'являються кінцеві картки YouTube. Не для підсумків з інформацією. + Прев\'ю / Коментарі / Підсумок + Колекція кліпів, які показують, що відбувається або що сталося у відео чи в інших відео серій, де вся інформація повторюється в іншому місці. + Неоплачувана / Самореклама + Подібно до \"Спонсор\", за винятком неоплачуваної або самореклами. Включає секції про товари, пожертви або інформацію про те, з ким вони співпрацювали. + Спонсор + Рекламні інтеграції, реферальні посилання і пряма реклама. Не для самореклами або рекомендацій різних подій / творців / сайтів / продуктів, які подобаються автору відео. + Пропустити автоматично + Вимкнути + Пропущено наповнювач. + Пропущено дратівливе нагадування. + Пропущено вступ. + Пропущено паузу. + Пропущено паузу. + Пропущено декілька сегментів. + Пропущено секцію без музики. + Пропущено закінчення. + Пропущено попередній перегляд. + Пропущено підсумок. + Пропущено попередній перегляд. + Пропущено саморекламу. + Пропущено спонсорську вставку. + SponsorBlock тимчасово недоступний. + SponsorBlock тимчасово недоступний (статус %d). + SponsorBlock тимчасово недоступний (закінчився час APІ). + Показувати тост, якщо API недоступний + Показує тост, якщо API SponsorBlock не доступний. + Показати тост, коли сегмент пропущено автоматично + Показує тост, коли сегмент автоматично пропущено. + Налаштування скопійовано до буфера обміну. + "Підміна версії клієнта на старішу версію. + +• Це змінить зовнішній вигляд програми, але можуть виникнути невідомі побічні ефекти. +• Якщо пізніше вимкнути, старий інтерфейс може залишитися, доки не буде очищено дані програми." + 4.27.53 - Вимкнення режиму радіо в канадських регіонах + 6.11.52 - Вимкнення динамічних текстів (караоке) + 7.16.53 - Відновлення старої панелі дій + Виберіть зі списку цільову версію підробки програми. + Підробити цільову версію програми + Підробити версію програми + "Підробити клієнт, щоб запобігти проблемам із відтворенням. + +Обмеження: +• Аудіокодек OPUS може не підтримуватися. +• Мініатюри перемотки можуть бути відсутніми. +• Історія переглядів не працює з обліковим записом бренду. + +※ Під час використання разом з \"Підміною клієнта\" можуть виникнути проблеми з відтворенням." + Підміна клієнта + Показує клієнт, який використовується для отримання потокових даних у \"Статистиці для сисадмінів\". + Показувати у \"Статистиці для сисадмінів\" + "Підробити потокові дані, щоб запобігти проблемам із відтворенням. + +※ Під час використання разом з \"Підміною клієнта\" можуть виникнути проблеми з відтворенням." + Підробка потокових даних + Android TV + Android VR + iOS + iOS Music + Визначає клієнт за замовчуванням, який отримує потокові дані. + Клієнт за замовчуванням + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/vi-rVN/missing_strings.xml b/patches/src/main/resources/music/translations/vi-rVN/missing_strings.xml new file mode 100644 index 000000000..973422826 --- /dev/null +++ b/patches/src/main/resources/music/translations/vi-rVN/missing_strings.xml @@ -0,0 +1,6 @@ + + + Don\'t show again + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/vi-rVN/strings.xml b/patches/src/main/resources/music/translations/vi-rVN/strings.xml new file mode 100644 index 000000000..878b08802 --- /dev/null +++ b/patches/src/main/resources/music/translations/vi-rVN/strings.xml @@ -0,0 +1,432 @@ + + + Tiếp tục + "Hiện GmsCore không có quyền chạy nền. + +Hãy làm theo hướng dẫn của 'Don't kill my app!' và tiến hành cài đặt GmsCore đúng cách. + +Để ứng dụng hoạt động hiệu quả nhất." + "Vui lòng tắt tối ưu hoá pin cho GmsCore để tránh phát sinh lỗi. + +Tắt tối ưu hoá pin cho GmsCore sẽ không làm ảnh hưởng đáng kể tới thời lượng sử dụng pin. + +Nhấn vào Tiếp tục và cho phép thay đổi lựa chọn tối ưu hoá pin." + Mở trang web + Hành động cần thiết + Chuyển hướng tới cài đặt GmsCore và kích hoạt Cloud Messaging để nhận thông báo đẩy. + GmsCore + GmsCore chưa được cài đặt. Hãy cài đặt nó đi nào. + Thay thế miền bị chặn ở một số khu vực để có thể thu được được hình thu nhỏ video của danh sách phát, ảnh đại diện kênh, v. v. + Bỏ qua hạn chế khu vực cho hình ảnh + Chuyển giao diện chia sẻ trong ứng dụng sang của hệ thống. + Thay đổi giao diện chia sẻ + Bảng xếp hạng + Khám phá + Trang chủ + Thư viện + Kênh đăng ký + Chọn trang sẽ hiển thị khi bạn khởi động ứng dụng. + Thay đổi trang khởi động + Nhập tên các mục mà bạn muốn lọc được phân cách bằng dòng. + Chỉnh sửa bộ lọc + Ẩn các thành phần không mong muốn bằng bộ lọc tuỳ chỉnh. + Bộ lọc tuỳ chỉnh + Tên mục đã nhập không hợp lệ: %s. + Tốc độ phát tuỳ chỉnh phải nhỏ hơn %sx. + Tốc độ phát tùy chỉnh không hợp lệ. + Thêm giá trị tốc độ phát mà bạn muốn thay đổi hoặc chỉnh sửa các giá trị tốc độ phát hiện có. + Chỉnh sửa tốc độ phát + Để mở liên kết YouTube Music trong RVX Music, hãy kích hoạt \"Mở các đường liên kết được hỗ trợ\" và thêm các đường liên kết được hỗ trợ. + Mở theo mặc định + Tắt tự động hiển thị phụ đề khi phát video nhạc có phụ đề. + Tắt tự động hiển thị phụ đề + Vô hiệu hóa hoạt ảnh kiểu Cairo khi ứng dụng khởi chạy. + Vô hiệu hóa hoạt ảnh kiểu Cairo + Không chuyển đến bài hát tiếp theo khi nhấn vào nút Không thích. + Tắt chuyển hướng khi nhấn nút Không thích + Vô hiệu hoá tính năng nén dải âm thanh động. + Vô hiệu hoá audio DRC + Tắt vuốt để chuyển bài hát trong trình phát thu nhỏ. + Tắt cử chỉ trình phát thu nhỏ + Tắt vuốt để chuyển bài hát trong trình phát. + Tắt cử chỉ trình phát + Đặt màu thanh điều hướng phía dưới cùng thành màu đen. + Thanh điều hướng màu đen + Thay đổi màu nền trình phát thành màu đen. + Nền trình phát màu đen + Đồng bộ màu của trình phát thu nhỏ với màu của trình phát. + Trình phát thu nhỏ khớp màu + "Bật trình đơn tuỳ chọn dạng hộp thoại. + +Hạn chế: +• Ảnh bìa Album trong thẻ Thư viện (Danh sách phát, Podcast, Bài hát, Đĩa nhạc, Nghệ sĩ,...) cũng thu gọn theo. +• Bố cục Hẹn giờ ngủ có thể xuất hiện bất thường." + Trình đơn tuỳ chọn thu gọn + Bao gồm bộ đệm trong nhật ký gỡ lỗi. + Bật nhật ký bộ đệm gỡ lỗi + Bật ghi nhật ký gỡ lỗi. + Nhật ký gỡ lỗi + Luôn phát nhạc trong trình phát thu nhỏ bất cứ khi nào bạn nghe một bài hát nằm ngoài trình phát hoặc bắt đầu đài phát. + Luôn phát trong trình phát thu nhỏ + Cho phép ứng dụng tự động xoay theo hướng màn hình mà thiết bị được giữ. + Tự động xoay màn hình + Thêm nút bài hát tiếp theo vào trình phát thu nhỏ. + Thêm nút tiếp theo vào trình phát thu nhỏ + Thêm nút bài hát trước đó vào trình phát thu nhỏ. + Thêm nút trước đó vào trình phát thu nhỏ + "Áp dụng codec OPUS nếu phản hồi của trình phát bao gồm nó. + +Cụ thể: +• Các phiên bản YouTube Music mới nhất sử dụng codec OPUS như mặc định. +• Điều này chỉ áp dụng cho người dùng giả mạo với các phiên bản ứng dụng rất cũ." + Codec OPUS + Vuốt xuống để đóng trình phát thu nhỏ. + Vuốt để đóng trình phát thu nhỏ + "Thêm tính năng Cắt bỏ khoảng lặng vào mục tuỳ chọn tốc độ phát. + + Cụ thể: + • Tính năng này dành cho podcast. + • Tính năng này vẫn đang được phát triển nên có thể chưa ổn định." + Cắt bỏ khoảng lặng + Đồng thời bật chế độ tập trung cho podcast. + Chế độ tập trung cho podcast + Thay đổi nền của trình phát thành màu xám nhạt để giúp bạn giảm mỏi mắt và tập trung hơn. + Chế độ tập trung + Đặt lại về giá trị mặc định. + Vui lòng khởi động lại ứng dụng để các tính năng hoạt động bình thường + Làm mới và khởi động lại + Xuất cài đặt dưới dạng tệp + Xuất cài đặt thất bại. + Cài đặt đã được xuất thành công. + Nhập + Nhập cài đặt từ tệp + Sao chép + Nhập/Xuất cài đặt dưới dạng văn bản + Nhập hoặc xuất các tuỳ chọn cài đặt của bạn. + Nhập/Xuất cài đặt + Nhập cài đặt thất bại: %s. + Đã đặt lại cài đặt về mặc định. + Đã nhập %d cài đặt. + Đặt lại + ReVanced Extended + "Nút tải xuống sẽ mở trình tải xuống bên ngoài của bạn. + +• Chỉ ghi đè lên nút Tải xuống trong trình phát. +• Không ghi đè lên nút Tải xuống trong Trình đơn tuỳ chọn hoặc trong thẻ Thư viện." + Ghi đè nút tải xuống + Trình tải xuống bên ngoài + "Có vẻ như %1$s chưa được cài đặt. + Vui lòng tải xuống %2$s từ trang web." + Chú ý + %s chưa được cài đặt. Hãy cài đặt và thử lại. + Nhập tên gói ứng dụng trình tải xuống đã cài đặt trên thiết bị của bạn, chẳng hạn như NewPipe hoặc YTDLnis. + Tên gói ứng dụng trình tải xuống + Ẩn các mục trống khỏi trình đơn Tài khoản. + Ẩn mục trống + Nhập tên các mục thành phần của trình đơn Tài khoản mà bạn muốn lọc được phân cách bằng dòng. + Bộ lọc trình đơn Tài khoản + Ẩn các thành phần của trình đơn Tài khoản bằng bộ lọc tuỳ chỉnh. + Ẩn trình đơn Tài khoản + Ẩn nút Lưu trong thanh thao tác. + Ẩn nút Lưu + Ẩn nút Bình luận trong thanh thao tác. + Ẩn nút Bình luận + Ẩn nút Tải xuống trong thanh thao tác. + Ẩn nút Tải xuống + Ẩn tên nút trong thanh thao tác. + Ẩn tên nút + Ẩn nút Thích và nút Không thích.\n\nLưu ý: Tuỳ chọn này không hoạt động trong bố cục trình phát kiểu cũ. + Ẩn các nút Thích/Không thích + Ẩn nút Đài phát trong thanh thao tác. + Ẩn nút Đài phát + Ẩn nút Chia sẻ trong thanh thao tác. + Ẩn nút Chia sẻ + Ẩn nút chuyển đổi Bài hát/Video trong trình phát. + Ẩn nút chuyển đổi Bài hát/Video + Ẩn khối danh mục ở cuối thẻ Trang chủ và đầu thẻ Khám phá. + Ẩn khối danh mục + Ẩn các kệ được cá nhân hoá dựa trên sở thích của bạn khỏi thẻ Trang chủ và thẻ Khám phá. + Ẩn các kệ được cá nhân hoá + Ẩn nút Truyền ở đầu trình phát. + Ẩn nút Truyền + Ẩn thanh danh mục. + Ẩn thanh danh mục + Ẩn các nhãn nguyên tắc (Nguyên tắc cộng đồng, Nguyên tắc của kênh,...) trong phần Bình luận. + Ẩn các nhãn nguyên tắc + Ẩn nút dấu thời gian và các biểu tượng cảm xúc khi đang nhập bình luận. + Ẩn nút dấu thời gian và các biểu tượng cảm xúc + Ẩn lớp phủ tối xuất hiện khi nhấn đúp để tua. + Ẩn lớp phủ khi nhấn đúp để tua + Ẩn nút nổi trong thẻ Thư viện. + Ẩn nút nổi + Ẩn 3 ô thao tác nhanh + Ẩn mục Thêm vào danh sách chờ + Ẩn mục Phụ đề + Ẩn mục Xoá danh sách phát + Ẩn mục Loại bỏ danh sách chờ + Ẩn mục Tải xuống + Ẩn mục Chỉnh sửa danh sách phát + Ẩn mục Chuyển đến đĩa nhạc + Ẩn mục Chuyển đến trang của nghệ sĩ + Ẩn mục Chuyển đến tập podcast + Ẩn mục Chuyển đến podcast + Ẩn mục Trợ giúp & phản hồi + Ẩn các nút Thích và Không thích + Ẩn mục Ghim vào kệ Phát nhanh + Ẩn mục Phát video tiếp theo + Ẩn mục Chất lượng + Ẩn mục Xoá khỏi thư viện + Ẩn mục Xóa khỏi danh sách phát + Ẩn mục Báo vi phạm + Ẩn mục Lưu tập này để thưởng thức sau + Ẩn mục Lưu vào thư viện + Ẩn mục Lưu vào danh sách phát + Ẩn mục Chia sẻ + Ẩn mục Phát ngẫu nhiên + Ẩn mục Hẹn giờ ngủ + Ẩn mục Bắt đầu đài phát + Ẩn mục Thống kê chi tiết + Ẩn mục Đăng ký/Huỷ đăng ký + Ẩn mục Bỏ ghim khỏi kệ Phát nhanh + Ẩn mục Xem thông tin ghi công của bài hát + "Ẩn quảng cáo toàn màn hình. + +Hạn chế: +• Đôi khi mở thẻ Trang chủ, bạn chỉ thấy một màn hình màu đen." + Ẩn quảng cáo toàn màn hình + Ẩn nút Chia sẻ trong trình phát toàn màn hình. + Ẩn nút Chia sẻ trong trình phát toàn màn hình + Ẩn quảng cáo xuất hiện trước khi phát. + Ẩn quảng cáo chung + Ẩn tên người dùng khỏi trình đơn Tài khoản. + Ẩn tên người dùng + Ẩn nút Video đã xem khỏi thanh công cụ. + Ẩn nút Video đã xem + Ẩn quảng cáo xuất hiện trước khi phát nhạc. + Ẩn quảng cáo + Ẩn thanh điều hướng. + Ẩn thanh điều hướng + Ẩn thẻ Khám phá khỏi thanh điều hướng. + Ẩn thẻ Khám phá + Ẩn thẻ Trang chủ khỏi thanh điều hướng. + Ẩn nút Trang chủ + Ẩn tên thẻ trên thanh điều hướng. + Ẩn tên thẻ + Ẩn thẻ Thư viện khỏi thanh điều hướng. + Ẩn thẻ Thư viện + Ẩn thẻ Đoạn nhạc khỏi thanh điều hướng. + Ẩn thẻ Đoạn nhạc + Ẩn thẻ Nâng cấp khỏi thanh điều hướng. + Ẩn thẻ Nâng cấp + Ẩn nút Thông báo khỏi thanh công cụ. + Ẩn nút Thông báo + Ẩn nhãn quảng cáo được tài trợ. + Ẩn nhãn quảng cáo được tài trợ + Ẩn kệ thẻ danh sách phát ở thẻ Trang chủ. + Ẩn kệ thẻ danh sách phát + Ẩn quảng cáo mua Music Premium bật lên. + Ẩn quảng cáo bật lên + Ẩn quảng cáo biểu ngữ mua Music Premium. + Ẩn quảng cáo biểu ngữ + Ẩn biểu ngữ thông báo khuyến mãi. + Ẩn biểu ngữ thông báo khuyến mãi + Ẩn kệ Đoạn nhạc ở thẻ Trang chủ. + Ẩn thẻ Đoạn nhạc + Ẩn mục Giới thiệu về Youtube Music + Ẩn mục Chế độ tiết kiệm dữ liệu + Ẩn mục Nhạc tải xuống & bộ nhớ + Ẩn mục Chung + Ẩn mục Thông báo + Ẩn mục Mua Music Premium + Ẩn mục Trung tâm dành cho gia đình + Ẩn mục Tính năng phát + Ẩn mục Quyền riêng tư & dữ liệu + Ẩn mục Đề xuất + "Ẩn các thành phần của mục Cài đặt. +Khi bật không những ẩn mục Cài đặt YT Music, mà còn ẩn mục Cài đặt ReVanced Extended." + Ẩn mục cài đặt + Ẩn nút Tìm kiếm bằng âm thanh kế bên thanh tìm kiếm. + Ẩn nút Tìm kiếm bằng âm thanh + Ẩn nút Chạm để nâng cấp. + Ẩn nút Chạm để nâng cấp + Ẩn các mục Chính sách quyền riêng tư và Điều khoản dịch vụ khỏi trình đơn Tài khoản. + Ẩn mục Bảo mật và Điều khoản + Ẩn nút Tìm kiếm bằng giọng nói kế bên thanh tìm kiếm. + Ẩn nút Tìm kiếm bằng giọng nói + Tài khoản + Thanh thao tác + Quảng cáo + Trình đơn tuỳ chọn + Tổng quan + Cài đặt khác + Thanh điều hướng + Trình phát + Return YouTube Username + Return YouTube Dislike + SponsorBlock + Trình đơn cài đặt + Video + Ghi nhớ lựa chọn tốc độ phát đã chọn gần nhất. + Lưu lựa chọn tốc độ phát + Hiển thị một thông báo ngắn khi thay đổi tốc độ phát mặc định. + Thông báo ngắn + Đã lưu tốc độ phát mặc định thành %s. + Ghi nhớ trạng thái phát lặp lại một danh sách phát hoặc phát lặp lại một bài hát. + Lưu trạng thái phát lặp lại + Ghi nhớ trạng thái phát ngẫu nhiên các bài hát. + Lưu trạng thái phát ngẫu nhiên + Ghi nhớ lựa chọn chất lượng video đã chọn gần nhất. + Lưu lựa chọn chất lượng video + Hiển thị một thông báo ngắn khi thay đổi chất lượng video mặc định. + Thông báo ngắn + Đã lưu chất lượng video mặc định khi sử dụng dữ liệu di động thành %s. + Đặt chất lượng video thất bại. + Đã lưu chất lượng video mặc định khi sử dụng Wi-Fi thành %s. + "Xoá hộp thoại cảnh báo nội dung cần cân nhắc trước khi xem. +\nLưu ý: Tuỳ chọn này chỉ tự động chấp nhận hộp thoại cảnh báo, không thể bỏ qua giới hạn về độ tuổi." + Xoá hộp thoại cảnh báo trước khi xem + Tiếp tục phát video từ thời điểm đã dừng lại khi chuyển sang YouTube. + Tiếp tục phát + Thay thế mục \"Loại bỏ danh sách chờ\" bằng mục \"Xem trên YouTube\". + Thay thế mục Loại bỏ danh sách chờ + Xem trên YouTube + URL của video không hợp lệ. + Giữ nguyên mục Báo vi phạm trong phần bình luận. + Báo vi phạm trong phần bình luận + Thay thế mục Báo vi phạm bằng mục Tốc độ phát. + Thay thế mục Báo vi phạm + Khôi phục bảng bình luận kiểu cũ. + Bảng bình luận kiểu cũ + Khôi phục nền trình phát về kiểu cũ. + Nền trình phát kiểu cũ + "Khôi phục bố cục trình phát về kiểu cũ. +\nLưu ý: Một số tính năng có thể không hoạt động bình thường trong bố cục trình phát kiểu cũ." + Bố cục trình phát kiểu cũ + Khôi phục lại thẻ Thư viện về kiểu cũ. (Thử nghiệm) + Thẻ Thư viện kiểu cũ + \@handle (Tên người dùng) + Chọn định dạng hiển thị tên người dùng. + Định dạng hiển thị + Tên người dùng (@handle) + Tên người dùng + Hiển thị tên người dùng thay vì tên hiển thị trong phần bình luận. + Kích hoạt Return YouTube Username + "Khoá nhà phát triển YouTube Data API v3 là một mã khoá cho phép các nhà phát triển truy cập lấy dữ liệu từ Youtube, và chúng cũng cần thiết để thay thế @tên hiển thị thành tên người dùng. + +Giới hạn truy cập hàng ngày cho các khoá API trên gói miễn phí là 10000 lần, với mỗi lượt truy cập chỉ áp dụng cho 1 bình luận. + +Nhấp vào đây để xem các bước phát hành khóa API." + Giới thiệu về khoá YouTube Data API + Khoá nhà phát triển để sử dụng YouTube Data API v3. + Khoá Youtube Data API + 1. <a href=%1$s>Tạo dự án mới</a>.<br>2. Ấn vào <b>CREATE</b>.<br>3. Đi tới <a href=%2$s>YouTube Data API v3</a>.<br>4. Ấn vào <b>ENABLE</b>.<br>5. Ấn vào <b>CREATE CREDENTIALS</b>.<br>6. Chọn <b>Public data</b>.<br>7. Ấn vào <b>NEXT</b>.<br>8. Sao chép mã khoá API.<br><br>※ Không nên chia sẻ mã khoá API với người khác, vì vậy chúng cũng không có mặt trong mục Nhập/Xuất cài đặt. + Phát hành mã khoá + Giới thiệu + Dữ liệu về số lượt không thích được cung cấp bởi API Return YouTube Dislike. Nhấn vào đây để tìm hiểu thêm. + ReturnYouTubeDislike.com + Ẩn dấu phân cách giữa nút Thích và số lượt thích. + Nút Thích thu gọn + Hiển thị số lượt không thích dưới dạng tỉ lệ phần trăm. + Số lượt không thích theo phần trăm + Hiển thị số lượt không thích của bài hát và video nhạc. + Kích hoạt Return YouTube Dislike + Hiển thị số lượt thích được ước tính của video. + Số lượt thích ước tính + Số lượt không thích không khả dụng (đã đạt đến giới hạn API máy khách). + Số lượt không thích không khả dụng (trạng thái %d). + Số lượt không thích tạm thời không khả dụng (API đã hết thời gian chờ). + Số lượt không thích không khả dụng (%s). + Hiển thị thông báo ngắn nếu API ReturnYouTubeDislike không khả dụng. + Thông báo ngắn nếu API không khả dụng + Ẩn + Loại bỏ các tham số truy vấn theo dõi khỏi URL khi chia sẻ liên kết. + Liên kết sạch khi chia sẻ + Giới thiệu + sponsor.ajay.app + Dữ liệu được cung cấp bởi API SponsorBlock. Nhấn vào đây để tìm hiểu thêm và xem các bản tải xuống cho các nền tảng khác. + Thay đổi địa chỉ URL của API + Đã thay đổi địa chỉ URL của API SponsorBlock. + Địa chỉ URL của API SponsorBlock không hợp lệ. + Đã đặt lại địa chỉ URL của API SponsorBlock. + Địa chỉ URL của API SponsorBlock được dùng để thực hiện các kết nối đến máy chủ. Không thay đổi địa chỉ này trừ khi bạn biết mình đang làm gì. + Đã thay đổi màu phân đoạn. + Màu: + Mã màu không hợp lệ. + Đã đặt lại màu phân đoạn về mặc định. + Cài đặt phân đoạn + Kích hoạt SponsorBlock + SponsorBlock là một tiện tích được đóng góp bởi cộng đồng nhằm bỏ qua các phân đoạn gây khó chịu trong video YouTube. + Đặt lại màu + Cảnh phụ/Lạc đề/Hài hước + Phân cảnh được thêm vào chỉ để câu giờ hoặc gây cười nhưng không cần thiết cho nội dung chính của video. Không bao gồm phân đoạn cung cấp bối cảnh hoặc chi tiết nền. + Nhắc nhở tương tác (Đăng ký) + Một lời nhắc ngắn rằng bạn hãy nhấn vào nút thích, đăng ký hoặc theo dõi họ ở giữa nội dung. Nếu chúng dài hoặc về một cái gì đó cụ thể, thay vào đó chúng sẽ được xếp vào phân đoạn Tự quảng cáo. + Đoạn tạm dừng/Phần Intro + Một khoảng thời gian không chứa nội dung thực tế nào. Có thể chỉ là tạm dừng, khung hình tĩnh hoặc hoạt ảnh lặp lại. Không bao gồm các phần chuyển cảnh chứa thông tin. + Âm nhạc: Phần không phải nhạc + Phần của video âm nhạc nhưng không có âm nhạc, cũng không thuộc danh mục nào. + Đoạn kết thúc/Phần Credit + Phần danh đề hoặc đoạn Youtube chèn các thẻ liên kết video khác ở cuối video. Không chứa thông tin quan trọng. + Đoạn xem trước/Phần tóm tắt/Gây chú ý + Đoạn cắt thể hiện những gì đã xảy ra hoặc sắp xảy ra trong video này hoặc trong loạt video khác cùng bộ. + Không được trả tiền/Tự quảng cáo + Tương tự như Nhà tài trợ, ngoại trừ việc không được trả phí hoặc tự quảng bá. Thì chúng bao gồm các phần về sản phẩm, quyên góp hoặc thông tin về người mà họ hợp tác. + Nhà tài trợ + Dạng quảng cáo, giới thiệu được trả phí và quảng cáo trực tiếp. Không nhằm mục đích tự quảng bá hoặc giới thiệu với người xem về các hoạt động từ thiện, nhà sáng tạo khác, trang web hay các sản phẩm mà họ yêu thích. + Tự động bỏ qua + Vô hiệu hoá + Đã bỏ qua Cảnh phụ - lạc đề. + Đã bỏ qua Nhắc nhở tương tác. + Đã bỏ qua Phần Intro. + Đã bỏ qua Đoạn tạm dừng. + Đã bỏ qua Đoạn tạm dừng. + Đã bỏ qua nhiều phân đoạn. + Đã bỏ qua Phần không phải nhạc. + Đã bỏ qua Phần Outro. + Đã bỏ qua Đoạn xem trước. + Đã bỏ qua Phần tóm tắt. + Đã bỏ qua Đoạn xem trước. + Đã bỏ qua Tự quảng cáo. + Đã bỏ qua Nhà tài trợ. + SponsorBlock tạm thời không khả dụng. + SponsorBlock tạm thời không khả dụng (trạng thái %d). + SponsorBlock tạm thời không khả dụng (API đã hết thời gian chờ). + Thông báo ngắn nếu API không khả dụng + Hiển thị một thông báo ngắn nếu API của SponsorBlock không khả dụng. + Thông báo ngắn khi tự động bỏ qua + Hiển thị một thông báo ngắn mỗi khi tự động bỏ qua phân đoạn. + Đã sao chép cài đặt sang bảng nhớ tạm. + "Giả mạo phiên bản YouTube Music hiện tại thành phiên bản cũ. + +Lưu ý:\n- Tuỳ chọn này sẽ thay đổi giao diện ứng dụng, tuy nhiên có thể xảy ra các sự cố chưa xác định khác. +- Nếu tắt tuỳ chọn này sau đó, giao diện cũ có thể vẫn tồn tại cho đến khi bạn xoá dữ liệu ứng dụng." + 4.27.53 - Tắt chế độ Đài phát ở một số vùng của Canada + 6.11.52 - Tắt lời bài hát theo thời gian thực + 7.16.53 - Khôi phục thanh thao tác kiểu cũ + Chọn phiên bản YouTube Music mà bạn muốn giả mạo. + Phiên bản giả mạo + Giả mạo phiên bản ứng dụng + "Giả mạo ứng dụng khách nhằm khắc phục sự cố phát. + +※ Khi được sử dụng đồng thời với \"Giả mạo luồng dữ liệu trực tuyến\", có thể gây ra sự cố phát." + Giả mạo ứng dụng khách + Music Android 4.27.53 + Music Android 5.29.53 + Music iOS 6.21 + "Xác định ứng dụng khách mặc định để giả mạo. + +※ Khi sử dụng ứng dụng khách Android, tính năng này nên được sử dụng đồng thời với \"Giả mạo phiên bản ứng dụng\"." + Ứng dụng khách mặc định + Hiển thị ứng dụng khách được sử dụng để nạp luồng dữ liệu trực tuyến trong Thống kê chi tiết. + Hiển thị trong Thống kê chi tiết + "Giả mạo luồng dữ liệu trực tuyến nhằm khắc phục sự cố phát. + +※ Khi được sử dụng đồng thời với \"Giả mạo ứng dụng khách\", có thể gây ra sự cố phát." + Giả mạo luồng dữ liệu trực tuyến + Android TV + Android VR + iOS + Music iOS + Xác định một ứng dụng khách mặc định để nạp luồng dữ liệu trực tuyến. + Ứng dụng khách mặc định + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/zh-rCN/missing_strings.xml b/patches/src/main/resources/music/translations/zh-rCN/missing_strings.xml new file mode 100644 index 000000000..4caf53e5f --- /dev/null +++ b/patches/src/main/resources/music/translations/zh-rCN/missing_strings.xml @@ -0,0 +1,184 @@ + + + Continue + Don\'t show again + "GmsCore does not have permission to run in the background. + +Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. + +This is required for the app to work." + "GmsCore battery optimizations must be disabled to prevent issues. + +Disabling battery optimizations for GmsCore will not negatively affect battery usage. + +Tap the continue button and allow optimization changes." + Open website + Action needed + Enable cloud messaging to receive notifications. + Open GmsCore + GmsCore is not installed. Install it. + Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. + Bypass image region restrictions + Change from in-app share sheet to system share sheet. + Change share sheet + Invalid custom playback speeds. + To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. + Open default app settings + Disables Cairo splash animation when the app starts up. + Disable Cairo splash animation + Disables DRC (Dynamic Range Compression) applied to audio. + Disable DRC audio + Disable swipe to change tracks in the miniplayer. + Disable miniplayer gesture + Disable swipe to change tracks in the player. + Disable player gesture + Includes the buffer in the debug log. + Enable debug buffer logging + Reset to default values. + Reset + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + Hides dark overlay that appears when double-tapping to seek. + Hide Pin to Speed dial menu + Hide Unpin from Speed dial menu + Hides the promotion alert banner. + Hide promotion alert banner + Hide About menu + Hide Data saving menu + Hide Downloads & storage menu + Hide General menu + Hide Notifications menu + Hide Get Music premium menu + Hide Family Center menu + Hide Playback menu + Hide Privacy & data menu + Hide Recommendations menu + "Hide elements of the settings menu. +This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." + Hide settings menu + Miscellaneous + Return YouTube Username + Return YouTube Dislike + SponsorBlock + Settings menu + Video + Remembers the last playback speed selected. + Remember playback speed changes + Show a toast when changing the default playback speed. + Show a toast + Changing default speed to %s. + Remembers the last video quality selected. + Remember video quality changes + Show a toast when changing the default video quality. + Show a toast + Changing default mobile data quality to %s. + Failed to set quality. + Changing default Wi-Fi quality to %s. + Returns the comments popup panels to the old style. + Restore old comments popup panels + Returns the player background to the old style. + Restore old player background + "Returns the player layout to the old style. +Some features may not work properly in the old player layout." + Restore old player layout + @handle (Username) + Select the username display format. + Display format + Username (@handle) + Username + Replaces handles with usernames in comments. + Enable Return YouTube Username + "A YouTube Data API v3 Developer Key is required to replace handles with usernames. + +The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. + +Click to see how to issue a API key." + About YouTube Data API key + The developer key for using the YouTube Data API v3. + YouTube Data API key + 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. + Issue YouTube Data API v3 developer key + ReturnYouTubeDislike.com + Enable Return YouTube Dislike + Shows the estimated like count of videos. + Show estimated likes + Hidden + About + sponsor.ajay.app + Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. + Change API URL + API URL changed. + API URL is invalid. + API URL reset. + The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. + Color changed. + Color: + Invalid color code. + Color reset. + Change segment behavior + Enable SponsorBlock + SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. + Reset color + Filler Tangent / Jokes + Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. + Interaction Reminder (Subscribe) + A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. + Intermission / Intro Animation + An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. + Music: Non-Music Section + Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. + Endcards / Credits + Credits or when the YouTube endcards appear. Not for conclusions with information. + Preview / Recap / Hook + Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. + Unpaid / Self Promotion + Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. + Sponsor + Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. + Skip automatically + Disable + Skipped filler. + Skipped annoying reminder. + Skipped intro. + Skipped intermission. + Skipped intermission. + Skipped multiple segments. + Skipped a non-music section. + Skipped outro. + Skipped preview. + Skipped recap. + Skipped preview. + Skipped self promotion. + Skipped sponsor. + SponsorBlock is temporarily unavailable. + SponsorBlock is temporarily unavailable (status %d). + SponsorBlock is temporarily unavailable (API timed out). + Show a toast if API is unavailable + Shows a toast if the SponsorBlock API is unavailable. + Show a toast when skipping automatically + Shows a toast when a segment is automatically skipped. + 7.16.53 - Restore old action bar + "Spoof the client to prevent playback issues. + +※ When used with 'Spoofing streaming data', playback issues may occur." + Spoof client + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + Android TV + Android VR + iOS + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/zh-rCN/strings.xml b/patches/src/main/resources/music/translations/zh-rCN/strings.xml new file mode 100644 index 000000000..f7a47df8d --- /dev/null +++ b/patches/src/main/resources/music/translations/zh-rCN/strings.xml @@ -0,0 +1,247 @@ + + + 图表 + 探索 + 首页 + 媒体库 + 订阅 + 选择打开应用显示的页面 + 更改起始页面 + 按行分割过滤组件名称 + 编辑自定义过滤隐藏 + 启用自定义过滤 + 自定义过滤隐藏 + 自定义过滤器无效: %s + 无效的自定义播放速度,已重置为默认值 + 添加或更改可用的播放速度 + 编辑自定义播放速度 + 禁止视频播放器自动启用的强制字幕 + 禁用自动字幕 + 点击不喜欢按钮时禁用重定向到下一曲目 + 禁用不喜欢重定向 + 将导航栏设为黑色 + 黑色导航栏 + 将播放器背景颜色更改为黑色 + 启用黑色播放器背景 + 使导航栏播放器与全屏播放器颜色一致. + 匹配播放器颜色 + "在手机上启用紧凑对话框。 + +已知问题: +• 媒体库上的专辑封面也变得更小。 +• 睡眠定时器布局可能出现异常。" + 启用紧凑对话框 + 打印 Debug 日志 + Debug 日志 + 保持播放器最小化,即使播放另一首曲目 + 强制最小化播放器 + 允许通过手机屏幕旋转进入横屏模式 + 横屏模式 + 启用迷你播放器的下一首按钮 + 启用迷你播放器的下一首按钮 + 启用迷你播放器的上一首按钮 + 启用迷你播放器的上一首按钮 + "播放音频使用 250/251 opus 编码" + OPUS 编解码器 + 启用向下滑动以关闭迷你播放器 + 启用滑动以关闭迷你播放器 + "将修改静音开关新增至播放速度弹出式选单 + + 信息: + • 此功能适用于播客 + • 此功能仍在开发中,因此可能不稳定" + 新增修改静音开关 + 同时允许播客的 Zen 模式 + 在播客中启用 Zen 模式 + 在视频播放器上添加灰色阴影以减少眼睛疲劳 + 禅定模式 + 重启应用以正常加载界面布局 + 刷新并重启 + 导出配置到文件 + 导出配置失败 + 导出配置成功 + 导入 + 从文件导入配置 + 复制 + 导入 / 导出配置文本 + 导入或导出设置为文本。 + 导入/导出 + 导入失败:%s + 设置已被重置为默认值。 + 已导入 %d 设置。 + ReVanced Extended + "下载按钮开启您的外部下载器 + + • 仅覆写播放器中的下载动作按钮 + • 不会覆写弹出式功能表或媒体库的下载按钮" + 覆盖下载操作按钮 + 外部下载器 + "%1$s 未安装 +请从网站下载 %2$s" + 警告 + %s未安装,请先安装 + 已安装的外部下载器应用的包名,例如 NewPipe 或 Seal + 外部下载器应用包名 + 隐藏在账户菜单中的空组件。 + 隐藏空组件 + 要过滤的帐户菜单名称列表,每行一个名称 + 账户菜单过滤器 + 隐藏账户菜单元素。 + 隐藏账户菜单 + 隐藏添加到播放列表按钮 + 隐藏添加到播放列表按钮 + 隐藏评论按钮 + 隐藏评论按钮 + 隐藏下载按钮 + 隐藏下载按钮 + 隐藏操作按钮中的标签 + 隐藏操作按钮标签 + 隐藏点赞和点踩按钮(在旧的播放器布局中不生效) + 隐藏点赞和点踩按钮 + 隐藏开启电台按钮 + 隐藏电台按钮 + 隐藏分享按钮 + 隐藏分享按钮 + 隐藏播放器中的音频/视频开关 + 隐藏音频/视频开关 + 隐藏主页和探索中的按钮栏 + 隐藏按钮栏 + 隐藏主页和探索中的播放列表 + 播放列表栏 + 隐藏首页顶部和播放器顶部的投屏按钮 + 投屏按钮 + 隐藏主页顶部的音乐分类 + 隐藏分类 + 隐藏评论顶部的频道指南 + 隐藏频道指南 + 输入评论时隐藏时间戳和表情符号按钮 + 隐藏时间戳和表情按钮 + 隐藏双击叠加层过滤器 + 隐藏媒体库中的悬浮按钮 + 隐蔽悬浮按钮 + 隐藏三列组件 + 隐藏添加到队列选项 + 隐藏字幕菜单选项 + 隐藏删除播放列表选项 + 隐藏清除队列选项 + 隐藏下载选项 + 隐藏编辑播放列表 + 隐藏转到专辑 + 隐藏转到艺术家 + 隐藏跳转到选集菜单 + 隐藏跳转到播客菜单 + 隐藏帮助 & 反馈菜单 + 隐藏点赞和点踩按钮 + 隐藏播放下一首 + 隐藏画质菜单 + 隐藏从媒体库中移除 + 隐藏从播放列表移除菜单 + 隐藏举报菜单 + 隐藏保存剧集到稍后再看菜单 + 隐藏保存到媒体库 + 隐藏保存到播放列表 + 隐藏分享菜单 + 隐藏随机播放菜单 + 隐藏睡眠计时器菜单 + 隐藏开启电台 + 隐藏详细统计信息菜单 + 隐藏订阅 / 退订菜单 + 隐藏歌曲详细信息 + "隐藏全屏广告" + 隐藏全屏广告 + 隐藏全屏播放器中的分享按钮 + 隐藏全屏分享按钮 + 隐藏一般广告 + 隐藏一般广告 + 隐藏账号菜单中的句柄 + 隐藏控制列 + 隐藏工具栏中的历史按钮 + 隐藏历史按钮 + 隐藏播放曲目前的广告 + 音乐广告 + 隐藏导航栏 + 隐藏导航栏 + 隐藏在导航栏中的探索按钮 + 隐藏探索按钮 + 隐藏首页按钮 + 隐藏首页按钮 + 隐藏导航栏标签 + 隐藏导航栏标签 + 隐藏媒体库按钮 + 隐藏媒体库按钮 + 隐藏样品按钮 + 隐藏样品按钮 + 隐藏更新按钮 + 隐藏更新按钮 + 隐藏工具栏中的通知按钮 + 隐藏通知按钮 + 隐藏付费推广标签 + 隐藏付费推广标签 + 隐藏订阅中的播放列表卡片 + 隐藏播放列表卡片 + 隐藏 Premium 推广弹出窗口 + 隐藏 Premium 推广弹出窗口 + 隐藏 Premium 续订横幅 + 隐藏 Premium 续订横幅 + 隐藏订阅中的样品栏 + 隐藏样品栏 + 隐藏搜索栏中的音频搜索按钮 + 隐藏音频搜索按钮 + 隐藏点击以更新按钮 + 隐藏点击以更新按钮 + 隐藏服务条款栏 + 隐藏服务条款栏 + 隐藏搜索栏中的语音搜索按钮 + 隐藏语音搜索按钮 + 账号 + 快捷操作栏 + 广告 + 弹出菜单 + 常规设置 + 导航栏 + 播放器 + 记住重复播放状态 + 记住重复播放状态 + 记住随机播放状态 + 记住随机播放状态 + "移除查看器的自由裁量对话框。 +这不会绕过年龄限制。它只会自动同意。" + 移除查看器的自由裁量对话框 + 切换到 YouTube 时从当前时间起继续视频 + 继续观看 + 替换清空队列菜单为在 YouTube上观看 + 替换清空队列菜单 + 在 Youtube上观看 + 视频链接无效 + 保持评论部分中的举报菜单不变 + 在评论中保留举报 + 用播放速度菜单替换举报菜单 + 替换举报菜单 + 将媒体库栏恢复为旧版 (实验性) + 还原旧版媒体库栏 + 关于 + 数据由 Return YouTube Dislike API 提供。点击了解更多信息。 + 隐藏点赞按钮的分隔符 + 紧凑点赞按钮 + 用百分比替换点踩数量 + 点踩百分比 + 显示视频点踩数 + 点踩数不可用(已达到客户端 API 限制) + 点踩数不可用(状态 %d) + 点踩数暂时不可用(API 连接超时) + 点踩数不可用(%s) + 当 Return YouTube Dislike API 不可用时显示提示 + 当 API 不可用时显示提示 + 分享链接时删除跟踪查询参数 + 清理分享链接 + 配置已复制到剪贴板 + "伪装应用版本为旧版本 + +・这将会改变应用的界面,但也可能出现未知的问题 +・如果关掉该项,可能仍然保留旧版界面,清除应用数据以解决该问题" + 4.27.53 - 在加拿大地区禁用收音机模式 + 6.11.52 - 禁用实时歌词 + 选择伪装的应用版本 + 伪装应用版本 + 伪装应用版本 + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/zh-rTW/missing_strings.xml b/patches/src/main/resources/music/translations/zh-rTW/missing_strings.xml new file mode 100644 index 000000000..4580b02ce --- /dev/null +++ b/patches/src/main/resources/music/translations/zh-rTW/missing_strings.xml @@ -0,0 +1,134 @@ + + + Continue + Don\'t show again + "GmsCore does not have permission to run in the background. + +Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. + +This is required for the app to work." + "GmsCore battery optimizations must be disabled to prevent issues. + +Disabling battery optimizations for GmsCore will not negatively affect battery usage. + +Tap the continue button and allow optimization changes." + Open website + Action needed + Enable cloud messaging to receive notifications. + Open GmsCore + GmsCore is not installed. Install it. + Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. + Bypass image region restrictions + Change from in-app share sheet to system share sheet. + Change share sheet + To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. + Open default app settings + Disables Cairo splash animation when the app starts up. + Disable Cairo splash animation + Disables DRC (Dynamic Range Compression) applied to audio. + Disable DRC audio + Disable swipe to change tracks in the miniplayer. + Disable miniplayer gesture + Disable swipe to change tracks in the player. + Disable player gesture + Includes the buffer in the debug log. + Enable debug buffer logging + Reset to default values. + Reset + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + Hides dark overlay that appears when double-tapping to seek. + Hide double-tap overlay filter + Hide Pin to Speed dial menu + Hide Unpin from Speed dial menu + Hides the promotion alert banner. + Hide promotion alert banner + Hide About menu + Hide Data saving menu + Hide Downloads & storage menu + Hide General menu + Hide Notifications menu + Hide Get Music premium menu + Hide Family Center menu + Hide Playback menu + Hide Privacy & data menu + Hide Recommendations menu + "Hide elements of the settings menu. +This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." + Hide settings menu + Miscellaneous + Return YouTube Username + Settings menu + Show a toast when changing the default playback speed. + Show a toast + Show a toast when changing the default video quality. + Show a toast + @handle (Username) + Select the username display format. + Display format + Username (@handle) + Username + Replaces handles with usernames in comments. + Enable Return YouTube Username + "A YouTube Data API v3 Developer Key is required to replace handles with usernames. + +The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. + +Click to see how to issue a API key." + About YouTube Data API key + The developer key for using the YouTube Data API v3. + YouTube Data API key + 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. + Issue YouTube Data API v3 developer key + Shows the estimated like count of videos. + Show estimated likes + Hidden + About + sponsor.ajay.app + Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. + Color changed. + Color: + Invalid color code. + Color reset. + Reset color + Skip automatically + Disable + Skipped filler. + Skipped annoying reminder. + Skipped intro. + Skipped intermission. + Skipped intermission. + Skipped multiple segments. + Skipped a non-music section. + Skipped outro. + Skipped preview. + Skipped recap. + Skipped preview. + SponsorBlock is temporarily unavailable. + SponsorBlock is temporarily unavailable (status %d). + SponsorBlock is temporarily unavailable (API timed out). + 7.16.53 - Restore old action bar + "Spoof the client to prevent playback issues. + +※ When used with 'Spoofing streaming data', playback issues may occur." + Spoof client + Android Music 4.27.53 + Android Music 5.29.53 + iOS Music 6.21 + "Defines a default client to spoofing. + +※ When using the Android client, it is recommended to use it with 'Spoof app version'." + Default client + Shows the client used to fetch streaming data in Stats for nerds. + Show in Stats for nerds + "Spoof the streaming data to prevent playback issues. + +※ When used with 'Spoof client', playback issues may occur." + Spoof streaming data + Android TV + Android VR + iOS + iOS Music + Defines a default client that fetches streaming data. + Default client + \ No newline at end of file diff --git a/patches/src/main/resources/music/translations/zh-rTW/strings.xml b/patches/src/main/resources/music/translations/zh-rTW/strings.xml new file mode 100644 index 000000000..55743697a --- /dev/null +++ b/patches/src/main/resources/music/translations/zh-rTW/strings.xml @@ -0,0 +1,306 @@ + + + 圖表 + 探索 + 首頁 + 媒體庫 + 訂閱 + 更改應用程式開啟時的頁面 + 更改應用程式的起始頁面 + 按行分隔篩選元件名稱 + 編輯自訂篩選 + 啟用自訂篩選以隱藏介面元件 + 啟用自訂篩選 + 自訂過濾無效:%s + 自訂速度必須小於 %sx 使用預設值 + 自訂播放速度無效 使用預設值 + 新增或變更可以使用的播放速度 + 編輯自訂播放速度 + 停用在播放器中被強制啟用的字幕 + 停用強制自動字幕 + 點擊不喜歡按鈕時停用重定向到下一首歌曲 + 停用不喜歡重定向 + 將導覽列的顏色設成黑色 + 啟用黑色導覽列 + 將播放器背景顏色設成為黑色 + 啟用黑色播放器背景 + 讓播放列顏色和全螢幕播放器一致 + 啟用彩色播放列 + "在手機上啟用緊湊對話框 + +已知問題: +• 資料庫上的專輯封面會變得很小 +• 睡眠定時器的介面可能會出現錯誤" + 啟用精簡選單 + 列出除錯記錄檔 + 啟用除錯紀錄 + 切換歌曲時保持迷你播放器狀態 + 切換歌曲時保持迷你播放器 + 允許透過手機螢幕旋轉進入橫向模式 + 啟用橫向模式 + 啟用迷你播放器中的下一首按鈕 + 啟用迷你播放器的下一首按鈕 + 啟用迷你播放器中的上一首按鈕 + 啟用迷你播放器上一首按鈕 + "播放音樂時啟用 250/251 opus 解碼器。" + 啟用解碼器覆寫 + 允許向下滑動以關閉迷你播放器 + 啟用滑動來關閉迷你播放器 + "將修改靜音開關新增至播放速度彈出式選單 + + 資訊: + • 此功能適用於播客 + • 此功能仍在開發中,因此可能不穩定" + 新增修改靜音開關 + 播客也適用於護眼模式 + 在播客中啟用護眼模式 + 在影片播放器上增加灰色陰影以減少眼睛疲勞 + 護眼模式 + 重新啟動以套用更改後的介面 + 套用並重新啟動 + 匯出設定到文件 + 無法匯出設定 + 設定已成功匯出 + 匯入 + 從文件導入設定 + 複製 + 以文字形式匯入或匯出設定 + 匯入或匯出設定成文字檔 + 匯入/匯出設定 + 匯入失敗: %s + 設定已重設為預設值 + 已匯入設定: %d + ReVanced 擴充功能 + "下載按鈕開啟您的外部下載器 + + • 僅覆寫播放器中的下載動作按鈕 + • 不會覆寫彈出式功能表或媒體庫的下載按鈕" + 覆蓋下載動作按鈕 + 外部下載器 + "%1$s 未安裝 + 請到網站下載%2$s" + 警告 + %s 尚未安裝. 請先安裝該應用後重試. + 已安裝的外部下載程式的套件名稱,例如 NewPipe 或 Seal + 外部下載程式的套件名稱 + 隱藏帳戶選項中的空白處 + 隱藏帳戶空白處 + 要篩選的帳戶選單名稱列表,以換行符號分隔 + 帳號選單過濾 + 隱藏於自訂過濾器中的帳戶選項元素 + 隱藏帳戶選單 + 隱藏\"新增至播放列表\"按鈕 + 隱藏\"新增至播放列表\"按鈕 + 隱藏留言按鈕 + 隱藏留言按鈕 + 隱藏下載按鈕 + 隱藏下載按鈕 + 隱藏操作按鈕中的標籤 + 隱藏操作按鈕中的標籤 + 隱藏 \"讚\" 與 \"倒讚\" 按鈕 + 隱藏 \"讚\" 與 \"倒讚\" 按鈕 + 隱藏開啟電台按鈕 + 隱藏電台按鈕 + 隱藏分享按鈕 + 隱藏分享按鈕 + 在播放器中隱藏音訊影片切換開關 + 隱藏音訊影片切換開關 + 隱藏位於首頁與探索頁面的主題按鈕 + 隱藏主題按鈕 + 從首頁和探索頁面隱藏播放清單 + 隱藏播放清單 + 隱藏投放按鈕 + 隱藏投放按鈕 + 隱藏音樂分類列表 + 隱藏分類列表 + 隱藏留言頂部的頻道指南 + 隱藏頻道指南 + 輸入留言時隱藏時間戳記和表情符號按鈕 + 隱藏時間戳與表情符號按鈕 + 在媒體庫中隱藏浮動按鈕 + 隱藏浮動按鈕 + 隱藏三列組件 + 隱藏加入到待播清單選項 + 隱藏字幕選項 + 隱藏刪除播放列表選項 + 隱藏從媒體庫刪除選項 + 隱藏下載選項 + 隱藏編輯播放列表選項 + 隱藏前往專輯頁面選項 + 隱藏前往藝人頁面選項 + 隱藏前往插曲選項 + 隱藏前往播客選項 + 隱藏說明& 回報問題選項 + 隱藏讚與倒讚按鈕 + 隱藏播放下一個選項 + 隱藏畫質選項 + 隱藏從媒體庫中移除選項 + 隱藏從播放列表中移除選項 + 隱藏檢舉選項 + 隱藏儲存專輯以供日後使用選項 + 隱藏新增至媒體庫中選項 + 隱藏儲存至播放清單選項 + 隱藏分享選項 + 隱藏隨機播放選項 + 隱藏睡眠計時器選項 + 隱藏開啟電台選項 + 隱藏資訊統計選項 + 隱藏訂閱/取消訂閱選項 + 隱藏查看歌曲製作人員選項 + "隱藏全螢幕廣告" + 隱藏全螢幕廣告 + 在全螢幕播放器中隱藏分享按鈕 + 隱藏全螢幕播放器中的分享按鈕 + 隱藏一般廣告 + 隱藏一般廣告 + 隱藏帳戶選單中的用戶名稱 + 隱藏用戶名稱 + 隱藏工具欄裡的歷史紀錄按鈕 + 隱藏歷史紀錄按鈕 + 隱藏播放歌曲之前的廣告 + 隱藏音樂廣告 + 隱藏導覽列 + 隱藏導覽列 + 隱藏探索按鈕 + 隱藏探索按鈕 + 隱藏首頁按鈕 + 隱藏首頁按鈕 + 隱藏位於導覽列的標籤 + 隱藏導覽列標籤 + 隱藏媒體庫按鈕 + 隱藏媒體庫按鈕 + 隱藏樣品按鈕 + 隱藏樣品按鈕 + 隱藏升級按鈕 + 隱藏升級按鈕 + 在工具欄中隱藏通知按鈕 + 隱藏通知按鈕 + 隱藏付費推廣標籤 + 隱藏付費推廣標籤 + 在探索中隱藏播放清單卡 + 隱藏播放清單卡 + 隱藏 Premium 升級彈出選單 + 隱藏 Premium 升級彈出選單 + 隱藏 Premium 續訂橫幅 + 隱藏 Premium 續訂橫幅 + 在探索中隱藏樣品架 + 隱藏樣品架 + 在搜尋欄裡隱藏聽聲辨曲按鈕 + 隱藏聽聲辨取按鈕 + 隱藏輕觸以重新整理按鈕 + 隱藏輕觸以重新載入按鈕 + 隱藏服務條款 + 隱藏術語 + 在搜尋欄隱藏語音搜尋按鈕 + 隱藏語音搜尋按鈕 + 帳號 + 導覽列 + 廣告 + 彈出式選單 + 一般設定 + 導覽列 + 播放器 + 恢復Youtube 倒讚 + 贊助區塊阻擋(SponsorBlock) + 影片 + 記住上次選擇的播放速度 + 記住播放速度的變更 + 將預設速度更改為 %s + 記住重複播放的狀態 + 記住重複播放狀態 + 記住隨機播放的狀態 + 記住隨機播放狀態 + 記住上次選擇的影片畫質 + 記住影片畫質變更 + 設定行動數據預設的畫質為%s + 畫質設定失敗 + 設定WiFi預設的畫質為%s + "隱藏觀看者判斷對話框 +這並不能繞過年齡限制 +它只是自動接受它" + 隱藏觀看者判斷對話框 + 在Youtube上觀看時,從上次時間繼續觀看 + 繼續觀看 + 替換取消序列選項以在Youtube上觀看 + 替換取消序列 + 在Youtube上觀看 + 未知的影片網址 + 保持留言部分中的報檢舉選項完好無缺 + 將檢舉保留在留言 + 將檢舉替換成播放速度 + 變更檢舉 + 將留言彈出介面恢復為舊樣式 + 恢復舊版留言彈出介面 + 將播放器背景恢復為舊樣式 + 恢復舊版播放器背景 + "將播放器介面恢復為舊樣式 +某些功能在舊的播放器介面中可能無法正常運作" + 恢復舊版播放器介面 + 將媒體庫選項恢復為舊樣式(實驗性功能) + 恢復舊版媒體庫樣式 + 關於 + 資料由 Return YouTube Dislike API 提供 +點擊了解更多資訊 + ReturnYouTubeDislike.com + 隱藏按讚按鈕中間的分隔線 + 緊湊型按讚按鈕 + 將倒讚數以百分比的形式顯示 + 倒讚百分比 + 顯示影片的不喜歡次數 + 啟用恢復Youtube倒讚 + 倒讚顯示不正常(已達到客戶端 API 限制) + 倒讚數無法使用 (狀態 %d) + 倒讚數暫時無法使用 (API 連線超時) + 倒讚數無法使用 (狀態 %s) + 當 Return YouTube Dislike API 無法使用時顯示提示訊息 + 當API無法使用時顯示提示訊息 + 分享連結時從 URL 中刪除追蹤參數。 + 清理分享連結 + 更改 API 網址 + API 網址已更改 + API 網址無效 + API 網址重設 + SponsorBlock 用於調用伺服器位置 +如果您知道自己在做什麼,否則請不要更改此設定 + 更改片段操作 + 啟用SponsorBlock + SponsorBlock 是透過使用者共同編輯新增片段來跳過 YouTube 的惱人片段 + 贅詞、玩笑 + 僅為影片中主要內容所不相關的贅字或幽默片段。這不應有內容或背景細節 + 互動提醒 (訂閱) + 影片中間簡短提醒觀眾來按讚、訂閱或追蹤 +如果片段較長,或是關於某個具體事物,則應分類為自我推廣 + 中場休息、介紹動畫 + 沒有實際內容的片段 +可以是靜止畫面,重複的動畫 +片段內未含有任何資訊 + 音樂:非音樂部分 + 此功能僅供音樂影片使用。本功能僅應該用於音樂錄影帶中並未包含其他類別的段落。 + 結束卡、片尾 + 鳴謝或當 YouTube 結尾資訊卡出現時 +不是含有資訊的總結 + 預覽、回顧、掛勾 + 顯示影片或其他系列影片中即將發生的情況或發生的情況的剪輯集合,其中所有資訊都會在其他地方重複 + 無償、自我推銷 + 類似贊助商廣告,但是非付費或自我推廣 +這包括有關商品、捐贈或與他人的合作資訊 + 贊助商廣告 + 付費推廣、付費推薦和直接廣告 +非自我推廣或免費提及、推薦他們喜歡的事物、創作者、網站、產品 + 已跳過自我宣傳 + 已跳過贊助 + 當API無法使用時顯示提示訊息 + 當 Sponsorblock 無法使用時顯示提示訊息 + 自動跳過片段時顯示提示訊息 + 自動跳過某個片段時顯示提示訊息 + 設定已複製到剪貼簿 + "將客戶端版本偽裝為舊版本 + + • 這將改變應用程式的外觀,但可能會出現未知的錯誤 + • 如果稍後停用,舊的 UI 可能會保留,直到應用程式資料被清除" + 4.27.53 - 在加拿大地區停用電台模式 + 6.11.52 - 停用即時歌詞 + 選擇欲偽裝的應用程式版本 + 偽裝應用程式版本 + 偽裝應用程式版本 + \ No newline at end of file diff --git a/src/main/resources/youtube/visual/icons/drawable-xxhdpi/empty_icon.png b/patches/src/main/resources/music/visual/icons/drawable-xxhdpi/empty_icon.png similarity index 100% rename from src/main/resources/youtube/visual/icons/drawable-xxhdpi/empty_icon.png rename to patches/src/main/resources/music/visual/icons/drawable-xxhdpi/empty_icon.png diff --git a/patches/src/main/resources/music/visual/icons/drawable/icon.xml b/patches/src/main/resources/music/visual/icons/drawable/icon.xml new file mode 100644 index 000000000..cdcba371c --- /dev/null +++ b/patches/src/main/resources/music/visual/icons/drawable/icon.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/patches/src/main/resources/music/visual/icons/extension/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/visual/icons/extension/drawable/revanced_extended_settings_key_icon.xml new file mode 100644 index 000000000..987e224f2 --- /dev/null +++ b/patches/src/main/resources/music/visual/icons/extension/drawable/revanced_extended_settings_key_icon.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/patches/src/main/resources/music/visual/icons/gear/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/visual/icons/gear/drawable/revanced_extended_settings_key_icon.xml new file mode 100644 index 000000000..984b1fc6d --- /dev/null +++ b/patches/src/main/resources/music/visual/icons/gear/drawable/revanced_extended_settings_key_icon.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/patches/src/main/resources/music/visual/icons/revanced/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/visual/icons/revanced/drawable/revanced_extended_settings_key_icon.xml new file mode 100644 index 000000000..cbf886b16 --- /dev/null +++ b/patches/src/main/resources/music/visual/icons/revanced/drawable/revanced_extended_settings_key_icon.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/src/main/resources/youtube/visual/icons/revanced_colored/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/music/visual/icons/revanced_colored/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/icons/revanced_colored/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/music/visual/icons/revanced_colored/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_blue/header/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/afn_blue/launcher/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/afn_blue/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml b/patches/src/main/resources/youtube/branding/afn_blue/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml rename to patches/src/main/resources/youtube/branding/afn_blue/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml diff --git a/src/main/resources/youtube/branding/afn_blue/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/afn_blue/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/afn_blue/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-hdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-mdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/afn_blue/splash/drawable/avd_anim.xml b/patches/src/main/resources/youtube/branding/afn_blue/splash/drawable/avd_anim.xml similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/drawable/avd_anim.xml rename to patches/src/main/resources/youtube/branding/afn_blue/splash/drawable/avd_anim.xml diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/afn_red/header/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/afn_red/launcher/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/afn_red/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml b/patches/src/main/resources/youtube/branding/afn_red/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml similarity index 100% rename from src/main/resources/youtube/branding/afn_red/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml rename to patches/src/main/resources/youtube/branding/afn_red/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml diff --git a/src/main/resources/youtube/branding/afn_red/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/afn_red/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/afn_red/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/afn_red/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-hdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-mdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/afn_red/splash/drawable/avd_anim.xml b/patches/src/main/resources/youtube/branding/afn_red/splash/drawable/avd_anim.xml similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/drawable/avd_anim.xml rename to patches/src/main/resources/youtube/branding/afn_red/splash/drawable/avd_anim.xml diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt/header/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt/launcher/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml b/patches/src/main/resources/youtube/branding/mmt/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml rename to patches/src/main/resources/youtube/branding/mmt/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml diff --git a/src/main/resources/youtube/branding/mmt/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/mmt/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/mmt/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-hdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-mdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-xhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__0.xml b/patches/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__0.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__0.xml rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__0.xml diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__1.xml b/patches/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__1.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__1.xml rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__1.xml diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__2.xml b/patches/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__2.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__2.xml rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__2.xml diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__3.xml b/patches/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__3.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__3.xml rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__3.xml diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__4.xml b/patches/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__4.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__4.xml rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable/$avd_anim__4.xml diff --git a/src/main/resources/youtube/branding/mmt/splash/drawable/avd_anim.xml b/patches/src/main/resources/youtube/branding/mmt/splash/drawable/avd_anim.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/drawable/avd_anim.xml rename to patches/src/main/resources/youtube/branding/mmt/splash/drawable/avd_anim.xml diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_blue/header/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_blue/launcher/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_blue/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml b/patches/src/main/resources/youtube/branding/mmt_blue/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml rename to patches/src/main/resources/youtube/branding/mmt_blue/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml diff --git a/src/main/resources/youtube/branding/mmt_blue/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/mmt_blue/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/mmt_blue/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-hdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-mdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__0.xml b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__0.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__0.xml rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__0.xml diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__1.xml b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__1.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__1.xml rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__1.xml diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__2.xml b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__2.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__2.xml rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__2.xml diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__3.xml b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__3.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__3.xml rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__3.xml diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__4.xml b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__4.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__4.xml rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable/$avd_anim__4.xml diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/drawable/avd_anim.xml b/patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable/avd_anim.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/drawable/avd_anim.xml rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/drawable/avd_anim.xml diff --git a/src/main/resources/youtube/branding/afn_blue/splash/values-v31/styles.xml b/patches/src/main/resources/youtube/branding/mmt_blue/splash/values-v31/styles.xml similarity index 100% rename from src/main/resources/youtube/branding/afn_blue/splash/values-v31/styles.xml rename to patches/src/main/resources/youtube/branding/mmt_blue/splash/values-v31/styles.xml diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_green/header/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_green/launcher/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_green/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml b/patches/src/main/resources/youtube/branding/mmt_green/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml rename to patches/src/main/resources/youtube/branding/mmt_green/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml diff --git a/src/main/resources/youtube/branding/mmt_green/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/mmt_green/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/mmt_green/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-hdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-mdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__0.xml b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__0.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__0.xml rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__0.xml diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__1.xml b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__1.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__1.xml rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__1.xml diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__2.xml b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__2.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__2.xml rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__2.xml diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__3.xml b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__3.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__3.xml rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__3.xml diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__4.xml b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__4.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__4.xml rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable/$avd_anim__4.xml diff --git a/src/main/resources/youtube/branding/mmt_green/splash/drawable/avd_anim.xml b/patches/src/main/resources/youtube/branding/mmt_green/splash/drawable/avd_anim.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/drawable/avd_anim.xml rename to patches/src/main/resources/youtube/branding/mmt_green/splash/drawable/avd_anim.xml diff --git a/src/main/resources/youtube/branding/afn_red/splash/values-v31/styles.xml b/patches/src/main/resources/youtube/branding/mmt_green/splash/values-v31/styles.xml similarity index 100% rename from src/main/resources/youtube/branding/afn_red/splash/values-v31/styles.xml rename to patches/src/main/resources/youtube/branding/mmt_green/splash/values-v31/styles.xml diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_orange/header/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_orange/launcher/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_orange/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml b/patches/src/main/resources/youtube/branding/mmt_orange/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml rename to patches/src/main/resources/youtube/branding/mmt_orange/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml diff --git a/src/main/resources/youtube/branding/mmt_orange/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/mmt_orange/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/mmt_orange/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-hdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-mdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__0.xml b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__0.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__0.xml rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__0.xml diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__1.xml b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__1.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__1.xml rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__1.xml diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__2.xml b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__2.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__2.xml rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__2.xml diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__3.xml b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__3.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__3.xml rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__3.xml diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__4.xml b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__4.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__4.xml rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable/$avd_anim__4.xml diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/drawable/avd_anim.xml b/patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable/avd_anim.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/drawable/avd_anim.xml rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/drawable/avd_anim.xml diff --git a/src/main/resources/youtube/branding/mmt/splash/values-v31/styles.xml b/patches/src/main/resources/youtube/branding/mmt_orange/splash/values-v31/styles.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt/splash/values-v31/styles.xml rename to patches/src/main/resources/youtube/branding/mmt_orange/splash/values-v31/styles.xml diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_pink/header/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_pink/launcher/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_pink/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml b/patches/src/main/resources/youtube/branding/mmt_pink/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml rename to patches/src/main/resources/youtube/branding/mmt_pink/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml diff --git a/src/main/resources/youtube/branding/mmt_pink/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/mmt_pink/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/mmt_pink/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-hdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-mdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__0.xml b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__0.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__0.xml rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__0.xml diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__1.xml b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__1.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__1.xml rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__1.xml diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__2.xml b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__2.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__2.xml rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__2.xml diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__3.xml b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__3.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__3.xml rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__3.xml diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__4.xml b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__4.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__4.xml rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable/$avd_anim__4.xml diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/drawable/avd_anim.xml b/patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable/avd_anim.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/drawable/avd_anim.xml rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/drawable/avd_anim.xml diff --git a/src/main/resources/youtube/branding/mmt_blue/splash/values-v31/styles.xml b/patches/src/main/resources/youtube/branding/mmt_pink/splash/values-v31/styles.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_blue/splash/values-v31/styles.xml rename to patches/src/main/resources/youtube/branding/mmt_pink/splash/values-v31/styles.xml diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/header/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/launcher/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml b/patches/src/main/resources/youtube/branding/mmt_turquoise/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml rename to patches/src/main/resources/youtube/branding/mmt_turquoise/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml diff --git a/src/main/resources/youtube/branding/mmt_turquoise/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/mmt_turquoise/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/mmt_turquoise/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-hdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-mdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__0.xml b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__0.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__0.xml rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__0.xml diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__1.xml b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__1.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__1.xml rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__1.xml diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__2.xml b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__2.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__2.xml rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__2.xml diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__3.xml b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__3.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__3.xml rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__3.xml diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__4.xml b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__4.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__4.xml rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/$avd_anim__4.xml diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/avd_anim.xml b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/avd_anim.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/avd_anim.xml rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/drawable/avd_anim.xml diff --git a/src/main/resources/youtube/branding/mmt_green/splash/values-v31/styles.xml b/patches/src/main/resources/youtube/branding/mmt_turquoise/splash/values-v31/styles.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_green/splash/values-v31/styles.xml rename to patches/src/main/resources/youtube/branding/mmt_turquoise/splash/values-v31/styles.xml diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/header/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/launcher/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml b/patches/src/main/resources/youtube/branding/mmt_yellow/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml rename to patches/src/main/resources/youtube/branding/mmt_yellow/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml diff --git a/src/main/resources/youtube/branding/mmt_yellow/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/mmt_yellow/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/mmt_yellow/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-hdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-mdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__0.xml b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__0.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__0.xml rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__0.xml diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__1.xml b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__1.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__1.xml rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__1.xml diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__2.xml b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__2.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__2.xml rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__2.xml diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__3.xml b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__3.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__3.xml rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__3.xml diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__4.xml b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__4.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__4.xml rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/$avd_anim__4.xml diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/avd_anim.xml b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/avd_anim.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_yellow/splash/drawable/avd_anim.xml rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/drawable/avd_anim.xml diff --git a/src/main/resources/youtube/branding/mmt_orange/splash/values-v31/styles.xml b/patches/src/main/resources/youtube/branding/mmt_yellow/splash/values-v31/styles.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_orange/splash/values-v31/styles.xml rename to patches/src/main/resources/youtube/branding/mmt_yellow/splash/values-v31/styles.xml diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_blue/header/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/revancify_blue/launcher/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/revancify_blue/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml b/patches/src/main/resources/youtube/branding/revancify_blue/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml rename to patches/src/main/resources/youtube/branding/revancify_blue/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml diff --git a/src/main/resources/youtube/branding/revancify_blue/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/revancify_blue/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/revancify_blue/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-hdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-mdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/drawable/avd_anim.xml b/patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable/avd_anim.xml similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/drawable/avd_anim.xml rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/drawable/avd_anim.xml diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/revancify_red/header/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/revancify_red/launcher/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/revancify_red/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml b/patches/src/main/resources/youtube/branding/revancify_red/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml rename to patches/src/main/resources/youtube/branding/revancify_red/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml diff --git a/src/main/resources/youtube/branding/revancify_red/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/revancify_red/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/revancify_red/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-hdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-mdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/revancify_red/splash/drawable/avd_anim.xml b/patches/src/main/resources/youtube/branding/revancify_red/splash/drawable/avd_anim.xml similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/drawable/avd_anim.xml rename to patches/src/main/resources/youtube/branding/revancify_red/splash/drawable/avd_anim.xml diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_black/header/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/vanced_black/launcher/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/vanced_black/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/vanced_black/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/vanced_black/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-hdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-mdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/vanced_black/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/vanced_light/header/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/vanced_light/launcher/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/vanced_light/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/vanced_light/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/vanced_light/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-hdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-mdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/vanced_light/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/header/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/launcher/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml b/patches/src/main/resources/youtube/branding/xisr_yellow/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml rename to patches/src/main/resources/youtube/branding/xisr_yellow/monochrome/drawable/adaptive_monochrome_ic_youtube_launcher.xml diff --git a/src/main/resources/youtube/branding/xisr_yellow/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/xisr_yellow/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/xisr_yellow/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-hdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-mdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__0.xml b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__0.xml similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__0.xml rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__0.xml diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__1.xml b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__1.xml similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__1.xml rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__1.xml diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__2.xml b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__2.xml similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__2.xml rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__2.xml diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__3.xml b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__3.xml similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__3.xml rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__3.xml diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__4.xml b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__4.xml similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__4.xml rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/$avd_anim__4.xml diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/avd_anim.xml b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/avd_anim.xml similarity index 100% rename from src/main/resources/youtube/branding/xisr_yellow/splash/drawable/avd_anim.xml rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/drawable/avd_anim.xml diff --git a/src/main/resources/youtube/branding/mmt_pink/splash/values-v31/styles.xml b/patches/src/main/resources/youtube/branding/xisr_yellow/splash/values-v31/styles.xml similarity index 100% rename from src/main/resources/youtube/branding/mmt_pink/splash/values-v31/styles.xml rename to patches/src/main/resources/youtube/branding/xisr_yellow/splash/values-v31/styles.xml diff --git a/src/main/resources/youtube/branding/youtube/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/youtube/settings/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/branding/youtube/settings/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/branding/youtube/settings/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__1__0.xml b/patches/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__1__0.xml similarity index 100% rename from src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__1__0.xml rename to patches/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__1__0.xml diff --git a/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__1__1.xml b/patches/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__1__1.xml similarity index 100% rename from src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__1__1.xml rename to patches/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__1__1.xml diff --git a/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__2__0.xml b/patches/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__2__0.xml similarity index 100% rename from src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__2__0.xml rename to patches/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__2__0.xml diff --git a/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__2__1.xml b/patches/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__2__1.xml similarity index 100% rename from src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__2__1.xml rename to patches/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__2__1.xml diff --git a/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__3__0.xml b/patches/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__3__0.xml similarity index 100% rename from src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__3__0.xml rename to patches/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__3__0.xml diff --git a/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__3__1.xml b/patches/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__3__1.xml similarity index 100% rename from src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__3__1.xml rename to patches/src/main/resources/youtube/branding/youtube/splash/drawable/$$avd_anim__3__1.xml diff --git a/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__0.xml b/patches/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__0.xml similarity index 100% rename from src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__0.xml rename to patches/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__0.xml diff --git a/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__1.xml b/patches/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__1.xml similarity index 100% rename from src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__1.xml rename to patches/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__1.xml diff --git a/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__2.xml b/patches/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__2.xml similarity index 100% rename from src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__2.xml rename to patches/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__2.xml diff --git a/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__3.xml b/patches/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__3.xml similarity index 100% rename from src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__3.xml rename to patches/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__3.xml diff --git a/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__4.xml b/patches/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__4.xml similarity index 100% rename from src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__4.xml rename to patches/src/main/resources/youtube/branding/youtube/splash/drawable/$avd_anim__4.xml diff --git a/src/main/resources/youtube/branding/youtube/splash/drawable/avd_anim.xml b/patches/src/main/resources/youtube/branding/youtube/splash/drawable/avd_anim.xml similarity index 100% rename from src/main/resources/youtube/branding/youtube/splash/drawable/avd_anim.xml rename to patches/src/main/resources/youtube/branding/youtube/splash/drawable/avd_anim.xml diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-hdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-hdpi/yt_premium_wordmark_header_dark.png new file mode 100644 index 000000000..a6ea15fa1 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-hdpi/yt_premium_wordmark_header_dark.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-hdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-hdpi/yt_premium_wordmark_header_light.png new file mode 100644 index 000000000..5ee6fa4d7 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-hdpi/yt_premium_wordmark_header_light.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-hdpi/yt_wordmark_header_dark.png new file mode 100644 index 000000000..c87c7b264 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-hdpi/yt_wordmark_header_dark.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-hdpi/yt_wordmark_header_light.png new file mode 100644 index 000000000..4502f82c7 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-hdpi/yt_wordmark_header_light.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-mdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-mdpi/yt_premium_wordmark_header_dark.png new file mode 100644 index 000000000..f6f7da00b Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-mdpi/yt_premium_wordmark_header_dark.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-mdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-mdpi/yt_premium_wordmark_header_light.png new file mode 100644 index 000000000..75fdc36f1 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-mdpi/yt_premium_wordmark_header_light.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-mdpi/yt_wordmark_header_dark.png new file mode 100644 index 000000000..ff840dc1c Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-mdpi/yt_wordmark_header_dark.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-mdpi/yt_wordmark_header_light.png new file mode 100644 index 000000000..205191fcf Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-mdpi/yt_wordmark_header_light.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png new file mode 100644 index 000000000..b78af20ff Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xhdpi/yt_premium_wordmark_header_dark.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xhdpi/yt_premium_wordmark_header_light.png new file mode 100644 index 000000000..682c4b620 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xhdpi/yt_premium_wordmark_header_light.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xhdpi/yt_wordmark_header_dark.png new file mode 100644 index 000000000..d349943de Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xhdpi/yt_wordmark_header_dark.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xhdpi/yt_wordmark_header_light.png new file mode 100644 index 000000000..9e45c67fa Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xhdpi/yt_wordmark_header_light.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png new file mode 100644 index 000000000..7499c02fd Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxhdpi/yt_premium_wordmark_header_dark.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png new file mode 100644 index 000000000..8bd7fc116 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxhdpi/yt_premium_wordmark_header_light.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxhdpi/yt_wordmark_header_dark.png new file mode 100644 index 000000000..648b73272 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxhdpi/yt_wordmark_header_dark.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxhdpi/yt_wordmark_header_light.png new file mode 100644 index 000000000..955f0ab9e Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxhdpi/yt_wordmark_header_light.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png new file mode 100644 index 000000000..6fba37205 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxxhdpi/yt_premium_wordmark_header_dark.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png new file mode 100644 index 000000000..e9a40656e Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxxhdpi/yt_premium_wordmark_header_light.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxxhdpi/yt_wordmark_header_dark.png new file mode 100644 index 000000000..e8b492c3c Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxxhdpi/yt_wordmark_header_dark.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxxhdpi/yt_wordmark_header_light.png new file mode 100644 index 000000000..8b354c8ad Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/header/drawable-xxxhdpi/yt_wordmark_header_light.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png new file mode 100644 index 000000000..6382483a2 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png new file mode 100644 index 000000000..81bb5af9b Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-hdpi/ic_launcher.png new file mode 100644 index 000000000..88bcbd7c7 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-hdpi/ic_launcher.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 000000000..88bcbd7c7 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-hdpi/ic_launcher_round.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png new file mode 100644 index 000000000..e04fa7f17 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png new file mode 100644 index 000000000..a9691c276 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-mdpi/ic_launcher.png new file mode 100644 index 000000000..3c304c057 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-mdpi/ic_launcher.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 000000000..3c304c057 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-mdpi/ic_launcher_round.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png new file mode 100644 index 000000000..7468c22b0 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png new file mode 100644 index 000000000..94cf69b53 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 000000000..7ddabe7e1 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xhdpi/ic_launcher.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 000000000..7ddabe7e1 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png new file mode 100644 index 000000000..58643a568 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png new file mode 100644 index 000000000..f37727a6e Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 000000000..d87ea0d97 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxhdpi/ic_launcher.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..d87ea0d97 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png new file mode 100644 index 000000000..43ab0cc48 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png new file mode 100644 index 000000000..05d740d74 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 000000000..b6a2e90fd Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 000000000..b6a2e90fd Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/launcher/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/settings/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/branding/youtube_black/settings/drawable/revanced_extended_settings_key_icon.xml new file mode 100755 index 000000000..d4363e9bc --- /dev/null +++ b/patches/src/main/resources/youtube/branding/youtube_black/settings/drawable/revanced_extended_settings_key_icon.xml @@ -0,0 +1,1034 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-hdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-hdpi/product_logo_youtube_color_144.png new file mode 100644 index 000000000..7ffefe3d0 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-hdpi/product_logo_youtube_color_144.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-hdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-hdpi/product_logo_youtube_color_192.png new file mode 100644 index 000000000..7e15883cb Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-hdpi/product_logo_youtube_color_192.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-hdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-hdpi/product_logo_youtube_color_24.png new file mode 100644 index 000000000..5c357c65b Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-hdpi/product_logo_youtube_color_24.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-hdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-hdpi/product_logo_youtube_color_36.png new file mode 100644 index 000000000..c37f6d19e Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-hdpi/product_logo_youtube_color_36.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-mdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-mdpi/product_logo_youtube_color_144.png new file mode 100644 index 000000000..7ffefe3d0 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-mdpi/product_logo_youtube_color_144.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-mdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-mdpi/product_logo_youtube_color_192.png new file mode 100644 index 000000000..7e15883cb Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-mdpi/product_logo_youtube_color_192.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-mdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-mdpi/product_logo_youtube_color_24.png new file mode 100644 index 000000000..5c357c65b Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-mdpi/product_logo_youtube_color_24.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-mdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-mdpi/product_logo_youtube_color_36.png new file mode 100644 index 000000000..c37f6d19e Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-mdpi/product_logo_youtube_color_36.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xhdpi/product_logo_youtube_color_144.png new file mode 100644 index 000000000..7ffefe3d0 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xhdpi/product_logo_youtube_color_144.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xhdpi/product_logo_youtube_color_192.png new file mode 100644 index 000000000..7e15883cb Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xhdpi/product_logo_youtube_color_192.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xhdpi/product_logo_youtube_color_24.png new file mode 100644 index 000000000..5c357c65b Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xhdpi/product_logo_youtube_color_24.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xhdpi/product_logo_youtube_color_36.png new file mode 100644 index 000000000..c37f6d19e Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xhdpi/product_logo_youtube_color_36.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxhdpi/product_logo_youtube_color_144.png new file mode 100644 index 000000000..7ffefe3d0 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxhdpi/product_logo_youtube_color_144.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxhdpi/product_logo_youtube_color_192.png new file mode 100644 index 000000000..7e15883cb Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxhdpi/product_logo_youtube_color_192.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxhdpi/product_logo_youtube_color_24.png new file mode 100644 index 000000000..5c357c65b Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxhdpi/product_logo_youtube_color_24.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxhdpi/product_logo_youtube_color_36.png new file mode 100644 index 000000000..c37f6d19e Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxhdpi/product_logo_youtube_color_36.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png new file mode 100644 index 000000000..7ffefe3d0 Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxxhdpi/product_logo_youtube_color_144.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png new file mode 100644 index 000000000..7e15883cb Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxxhdpi/product_logo_youtube_color_192.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png new file mode 100644 index 000000000..5c357c65b Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxxhdpi/product_logo_youtube_color_24.png differ diff --git a/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png new file mode 100644 index 000000000..c37f6d19e Binary files /dev/null and b/patches/src/main/resources/youtube/branding/youtube_black/splash/drawable-xxxhdpi/product_logo_youtube_color_36.png differ diff --git a/src/main/resources/youtube/doubletap/values-v21/arrays.xml b/patches/src/main/resources/youtube/doubletap/values-v21/arrays.xml similarity index 100% rename from src/main/resources/youtube/doubletap/values-v21/arrays.xml rename to patches/src/main/resources/youtube/doubletap/values-v21/arrays.xml diff --git a/src/main/resources/youtube/materialyou/host/values-v31/colors.xml b/patches/src/main/resources/youtube/materialyou/host/values-v31/colors.xml similarity index 100% rename from src/main/resources/youtube/materialyou/host/values-v31/colors.xml rename to patches/src/main/resources/youtube/materialyou/host/values-v31/colors.xml diff --git a/patches/src/main/resources/youtube/navigationbuttons/drawable-hdpi/yt_fill_bell_cairo_black_24.png b/patches/src/main/resources/youtube/navigationbuttons/drawable-hdpi/yt_fill_bell_cairo_black_24.png new file mode 100644 index 000000000..9c212f738 Binary files /dev/null and b/patches/src/main/resources/youtube/navigationbuttons/drawable-hdpi/yt_fill_bell_cairo_black_24.png differ diff --git a/patches/src/main/resources/youtube/navigationbuttons/drawable-mdpi/yt_fill_bell_cairo_black_24.png b/patches/src/main/resources/youtube/navigationbuttons/drawable-mdpi/yt_fill_bell_cairo_black_24.png new file mode 100644 index 000000000..3e6e40a57 Binary files /dev/null and b/patches/src/main/resources/youtube/navigationbuttons/drawable-mdpi/yt_fill_bell_cairo_black_24.png differ diff --git a/patches/src/main/resources/youtube/navigationbuttons/drawable-xhdpi/yt_fill_bell_cairo_black_24.png b/patches/src/main/resources/youtube/navigationbuttons/drawable-xhdpi/yt_fill_bell_cairo_black_24.png new file mode 100644 index 000000000..e8cb2f6f9 Binary files /dev/null and b/patches/src/main/resources/youtube/navigationbuttons/drawable-xhdpi/yt_fill_bell_cairo_black_24.png differ diff --git a/patches/src/main/resources/youtube/navigationbuttons/drawable-xxhdpi/yt_fill_bell_cairo_black_24.png b/patches/src/main/resources/youtube/navigationbuttons/drawable-xxhdpi/yt_fill_bell_cairo_black_24.png new file mode 100644 index 000000000..bb967e060 Binary files /dev/null and b/patches/src/main/resources/youtube/navigationbuttons/drawable-xxhdpi/yt_fill_bell_cairo_black_24.png differ diff --git a/patches/src/main/resources/youtube/navigationbuttons/drawable-xxxhdpi/yt_fill_bell_cairo_black_24.png b/patches/src/main/resources/youtube/navigationbuttons/drawable-xxxhdpi/yt_fill_bell_cairo_black_24.png new file mode 100644 index 000000000..5f110ae2d Binary files /dev/null and b/patches/src/main/resources/youtube/navigationbuttons/drawable-xxxhdpi/yt_fill_bell_cairo_black_24.png differ diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_chevron_down_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_chevron_down_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_chevron_down_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_chevron_down_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_gear_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_gear_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_gear_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_gear_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_vertical_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_vertical_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_vertical_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-hdpi/yt_outline_screen_vertical_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_chevron_down_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_chevron_down_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_chevron_down_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_chevron_down_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_gear_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_gear_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_gear_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_gear_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_vertical_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_vertical_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_vertical_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-mdpi/yt_outline_screen_vertical_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_chevron_down_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_chevron_down_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_chevron_down_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_chevron_down_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_gear_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_gear_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_gear_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_gear_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_vertical_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_vertical_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_vertical_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xhdpi/yt_outline_screen_vertical_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_chevron_down_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_chevron_down_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_chevron_down_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_chevron_down_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_gear_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_gear_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_gear_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_gear_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_vertical_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_vertical_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_vertical_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxhdpi/yt_outline_screen_vertical_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_chevron_down_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_chevron_down_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_chevron_down_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_chevron_down_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_gear_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_gear_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_gear_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_gear_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_vertical_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_vertical_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_vertical_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/bold/drawable-xxxhdpi/yt_outline_screen_vertical_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_chevron_down_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_chevron_down_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_chevron_down_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_chevron_down_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_gear_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_gear_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_gear_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_gear_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_vertical_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_vertical_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_vertical_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-hdpi/yt_outline_screen_vertical_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_chevron_down_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_chevron_down_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_chevron_down_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_chevron_down_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_gear_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_gear_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_gear_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_gear_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_vertical_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_vertical_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_vertical_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-mdpi/yt_outline_screen_vertical_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_chevron_down_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_chevron_down_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_chevron_down_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_chevron_down_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_gear_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_gear_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_gear_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_gear_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_vertical_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_vertical_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_vertical_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xhdpi/yt_outline_screen_vertical_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_chevron_down_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_chevron_down_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_chevron_down_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_chevron_down_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_gear_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_gear_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_gear_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_gear_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_vertical_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_vertical_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_vertical_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxhdpi/yt_outline_screen_vertical_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_chevron_down_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_chevron_down_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_chevron_down_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_chevron_down_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_gear_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_gear_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_gear_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_gear_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_vertical_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_vertical_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_vertical_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/rounded/drawable-xxxhdpi/yt_outline_screen_vertical_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/shared/drawable/playlist_repeat_button.xml b/patches/src/main/resources/youtube/overlaybuttons/shared/drawable/playlist_repeat_button.xml similarity index 100% rename from src/main/resources/youtube/overlaybuttons/shared/drawable/playlist_repeat_button.xml rename to patches/src/main/resources/youtube/overlaybuttons/shared/drawable/playlist_repeat_button.xml diff --git a/src/main/resources/youtube/overlaybuttons/shared/drawable/playlist_shuffle_button.xml b/patches/src/main/resources/youtube/overlaybuttons/shared/drawable/playlist_shuffle_button.xml similarity index 100% rename from src/main/resources/youtube/overlaybuttons/shared/drawable/playlist_shuffle_button.xml rename to patches/src/main/resources/youtube/overlaybuttons/shared/drawable/playlist_shuffle_button.xml diff --git a/src/main/resources/youtube/overlaybuttons/shared/drawable/revanced_mute_volume_button.xml b/patches/src/main/resources/youtube/overlaybuttons/shared/drawable/revanced_mute_volume_button.xml similarity index 100% rename from src/main/resources/youtube/overlaybuttons/shared/drawable/revanced_mute_volume_button.xml rename to patches/src/main/resources/youtube/overlaybuttons/shared/drawable/revanced_mute_volume_button.xml diff --git a/src/main/resources/youtube/overlaybuttons/shared/drawable/revanced_repeat_button.xml b/patches/src/main/resources/youtube/overlaybuttons/shared/drawable/revanced_repeat_button.xml similarity index 100% rename from src/main/resources/youtube/overlaybuttons/shared/drawable/revanced_repeat_button.xml rename to patches/src/main/resources/youtube/overlaybuttons/shared/drawable/revanced_repeat_button.xml diff --git a/src/main/resources/youtube/overlaybuttons/shared/host/layout/youtube_controls_bottom_ui_container.xml b/patches/src/main/resources/youtube/overlaybuttons/shared/host/layout/youtube_controls_bottom_ui_container.xml similarity index 87% rename from src/main/resources/youtube/overlaybuttons/shared/host/layout/youtube_controls_bottom_ui_container.xml rename to patches/src/main/resources/youtube/overlaybuttons/shared/host/layout/youtube_controls_bottom_ui_container.xml index 0953f47ea..8d155519a 100644 --- a/src/main/resources/youtube/overlaybuttons/shared/host/layout/youtube_controls_bottom_ui_container.xml +++ b/patches/src/main/resources/youtube/overlaybuttons/shared/host/layout/youtube_controls_bottom_ui_container.xml @@ -4,8 +4,8 @@ - - + + diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_closed_caption_off_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_closed_caption_off_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_closed_caption_off_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_closed_caption_off_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_closed_caption_off_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_closed_caption_off_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_closed_caption_off_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_closed_caption_off_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_closed_caption_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_closed_caption_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_closed_caption_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_closed_caption_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_gear_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_gear_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_gear_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_gear_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-hdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_closed_caption_off_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_closed_caption_off_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_closed_caption_off_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_closed_caption_off_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_closed_caption_off_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_closed_caption_off_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_closed_caption_off_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_closed_caption_off_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_closed_caption_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_closed_caption_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_closed_caption_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_closed_caption_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_gear_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_gear_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_gear_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_gear_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-mdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_closed_caption_off_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_closed_caption_off_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_closed_caption_off_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_closed_caption_off_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_closed_caption_off_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_closed_caption_off_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_closed_caption_off_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_closed_caption_off_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_closed_caption_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_closed_caption_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_closed_caption_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_closed_caption_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_gear_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_gear_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_gear_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_gear_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xhdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_closed_caption_off_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_closed_caption_off_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_closed_caption_off_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_closed_caption_off_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_closed_caption_off_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_closed_caption_off_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_closed_caption_off_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_closed_caption_off_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_closed_caption_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_closed_caption_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_closed_caption_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_closed_caption_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxhdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/ic_vr.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/ic_vr.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/ic_vr.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/ic_vr.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_closed_caption_off_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_closed_caption_off_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_closed_caption_off_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_closed_caption_off_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_closed_caption_off_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_closed_caption_off_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_closed_caption_off_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_closed_caption_off_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_closed_caption_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_closed_caption_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_closed_caption_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_closed_caption_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_exit_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_exit_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_exit_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_exit_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_grey600_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_grey600_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_grey600_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_grey600_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/quantum_ic_fullscreen_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_copy_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_copy_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_copy_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_copy_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_copy_timestamp_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_copy_timestamp_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_copy_timestamp_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_copy_timestamp_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_download_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_download_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_download_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_download_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_time_ordered_playlist_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_play_all_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_time_ordered_playlist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_play_all_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_speed_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_speed_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_speed_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_speed_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_volume_muted_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_volume_muted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_volume_muted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_volume_muted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_volume_unmuted_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_volume_unmuted_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_volume_unmuted_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_volume_unmuted_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_whitelist_button.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_whitelist_button.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_whitelist_button.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/revanced_whitelist_button.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_fill_arrow_repeat_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_fill_arrow_repeat_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_fill_arrow_repeat_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_fill_arrow_repeat_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_arrow_repeat_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_arrow_repeat_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_arrow_repeat_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_arrow_repeat_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_arrow_shuffle_1_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_arrow_shuffle_1_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_arrow_shuffle_1_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_arrow_shuffle_1_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_screen_full_exit_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_screen_full_exit_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_screen_full_exit_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_screen_full_exit_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_screen_full_vd_theme_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_screen_full_vd_theme_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_screen_full_vd_theme_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_screen_full_vd_theme_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_screen_full_white_24.png b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_screen_full_white_24.png similarity index 100% rename from src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_screen_full_white_24.png rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable-xxxhdpi/yt_outline_screen_full_white_24.png diff --git a/src/main/resources/youtube/overlaybuttons/thin/drawable/yt_outline_screen_vertical_vd_theme_24.xml b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable/yt_outline_screen_vertical_vd_theme_24.xml similarity index 99% rename from src/main/resources/youtube/overlaybuttons/thin/drawable/yt_outline_screen_vertical_vd_theme_24.xml rename to patches/src/main/resources/youtube/overlaybuttons/thin/drawable/yt_outline_screen_vertical_vd_theme_24.xml index 084732f34..a460f6a95 100644 --- a/src/main/resources/youtube/overlaybuttons/thin/drawable/yt_outline_screen_vertical_vd_theme_24.xml +++ b/patches/src/main/resources/youtube/overlaybuttons/thin/drawable/yt_outline_screen_vertical_vd_theme_24.xml @@ -1,5 +1,5 @@ - - - + + + \ No newline at end of file diff --git a/patches/src/main/resources/youtube/seekbar/values/attrs.xml b/patches/src/main/resources/youtube/seekbar/values/attrs.xml new file mode 100644 index 000000000..2bf349f0d --- /dev/null +++ b/patches/src/main/resources/youtube/seekbar/values/attrs.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/src/main/resources/youtube/settings/drawable/revanced_cursor.xml b/patches/src/main/resources/youtube/settings/drawable/revanced_cursor.xml similarity index 100% rename from src/main/resources/youtube/settings/drawable/revanced_cursor.xml rename to patches/src/main/resources/youtube/settings/drawable/revanced_cursor.xml diff --git a/patches/src/main/resources/youtube/settings/host/values/arrays.xml b/patches/src/main/resources/youtube/settings/host/values/arrays.xml new file mode 100644 index 000000000..5ae3b83f1 --- /dev/null +++ b/patches/src/main/resources/youtube/settings/host/values/arrays.xml @@ -0,0 +1,316 @@ + + + + @string/revanced_alt_thumbnail_options_entry_1 + @string/revanced_alt_thumbnail_options_entry_2 + @string/revanced_alt_thumbnail_options_entry_3 + @string/revanced_alt_thumbnail_options_entry_4 + + + ORIGINAL + DEARROW + DEARROW_STILL_IMAGES + STILL_IMAGES + + + @string/revanced_alt_thumbnail_stills_time_entry_1 + @string/revanced_alt_thumbnail_stills_time_entry_2 + @string/revanced_alt_thumbnail_stills_time_entry_3 + + + BEGINNING + MIDDLE + END + + + @string/revanced_change_layout_entry_1 + @string/revanced_change_layout_entry_2 + @string/revanced_change_layout_entry_3 + @string/revanced_change_layout_entry_4 + @string/revanced_change_layout_entry_5 + + + ORIGINAL + SMALL_FORM_FACTOR + SMALL_FORM_FACTOR_WIDTH_DP + LARGE_FORM_FACTOR + LARGE_FORM_FACTOR_WIDTH_DP + + + @string/revanced_change_start_page_entry_default + @string/revanced_change_start_page_entry_search + @string/revanced_change_start_page_entry_shorts + @string/revanced_change_start_page_entry_subscriptions + @string/revanced_change_start_page_entry_explore + @string/revanced_change_start_page_entry_library + @string/revanced_change_start_page_entry_liked_videos + @string/revanced_change_start_page_entry_watch_later + @string/revanced_change_start_page_entry_history + @string/revanced_change_start_page_entry_trending + @string/revanced_change_start_page_entry_gaming + @string/revanced_change_start_page_entry_live + @string/revanced_change_start_page_entry_music + @string/revanced_change_start_page_entry_movies + @string/revanced_change_start_page_entry_sports + @string/revanced_change_start_page_entry_browse + @string/revanced_change_start_page_entry_courses + + + ORIGINAL + + SEARCH + SHORTS + + SUBSCRIPTIONS + EXPLORE + LIBRARY + LIKED_VIDEO + WATCH_LATER + HISTORY + TRENDING + GAMING + LIVE + MUSIC + MOVIE + SPORTS + BROWSE + COURSES + + + @string/revanced_change_shorts_repeat_state_entry_default + @string/revanced_change_shorts_repeat_state_entry_repeat + @string/revanced_change_shorts_repeat_state_entry_auto_play + @string/revanced_change_shorts_repeat_state_entry_pause + + + UNKNOWN + REPEAT + SINGLE_PLAY + END_SCREEN + + + @string/quality_auto + 144p + 240p + 360p + 480p + 720p + 1080p + 1440p + 2160p + + + -2 + 144 + 240 + 360 + 480 + 720 + 1080 + 1440 + 2160 + + + NewPipe + Seal + Tubular + YTDLnis + + + org.schabi.newpipe + com.junkfood.seal + org.polymorphicshade.tubular + com.deniscerri.ytdl + + + https://github.com/TeamNewPipe/NewPipe/releases/latest + https://github.com/JunkFood02/Seal/releases/latest + https://github.com/polymorphicshade/Tubular/releases/latest + https://github.com/deniscerri/ytdlnis/releases/latest + + + NewPipe + Seal + Tubular + YTDLnis + + + org.schabi.newpipe + com.junkfood.seal + org.polymorphicshade.tubular + com.deniscerri.ytdl + + + https://github.com/TeamNewPipe/NewPipe/releases/latest + https://github.com/JunkFood02/Seal/releases/latest + https://github.com/polymorphicshade/Tubular/releases/latest + https://github.com/deniscerri/ytdlnis/releases/latest + + + YTDLnis + + + com.deniscerri.ytdl + + + https://github.com/deniscerri/ytdlnis/releases/latest + + + @string/revanced_overlay_button_play_all_type_entry_0 + @string/revanced_overlay_button_play_all_type_entry_1 + @string/revanced_overlay_button_play_all_type_entry_2 + @string/revanced_overlay_button_play_all_type_entry_3 + @string/revanced_overlay_button_play_all_type_entry_4 + @string/revanced_overlay_button_play_all_type_entry_5 + @string/revanced_overlay_button_play_all_type_entry_6 + @string/revanced_overlay_button_play_all_type_entry_7 + @string/revanced_overlay_button_play_all_type_entry_8 + @string/revanced_overlay_button_play_all_type_entry_9 + @string/revanced_overlay_button_play_all_type_entry_10 + @string/revanced_overlay_button_play_all_type_entry_11 + @string/revanced_overlay_button_play_all_type_entry_12 + + + ALL_CONTENTS_WITH_TIME_ASCENDING + ALL_CONTENTS_WITH_TIME_DESCENDING + ALL_CONTENTS_WITH_POPULAR_DESCENDING + VIDEOS_ONLY_WITH_TIME_DESCENDING + VIDEOS_ONLY_WITH_POPULAR_DESCENDING + SHORTS_ONLY_WITH_TIME_DESCENDING + SHORTS_ONLY_WITH_POPULAR_DESCENDING + LIVESTREAMS_ONLY_WITH_TIME_DESCENDING + LIVESTREAMS_ONLY_WITH_POPULAR_DESCENDING + ALL_MEMBERSHIPS_CONTENTS + MEMBERSHIPS_VIDEOS_ONLY + MEMBERSHIPS_SHORTS_ONLY + MEMBERSHIPS_LIVESTREAMS_ONLY + + + @string/revanced_miniplayer_type_entry_0 + @string/revanced_miniplayer_type_entry_1 + @string/revanced_miniplayer_type_entry_2 + @string/revanced_miniplayer_type_entry_3 + @string/revanced_miniplayer_type_entry_4 + @string/revanced_miniplayer_type_entry_5 + @string/revanced_miniplayer_type_entry_6 + + + DISABLED + ORIGINAL + MINIMAL + TABLET + MODERN_1 + MODERN_2 + MODERN_3 + + + @string/revanced_miniplayer_type_entry_1 + @string/revanced_miniplayer_type_entry_2 + @string/revanced_miniplayer_type_entry_3 + @string/revanced_miniplayer_type_entry_4 + @string/revanced_miniplayer_type_entry_5 + @string/revanced_miniplayer_type_entry_6 + + + ORIGINAL + MINIMAL + TABLET + MODERN_1 + MODERN_2 + MODERN_3 + + + @string/revanced_miniplayer_type_entry_1 + @string/revanced_miniplayer_type_entry_2 + @string/revanced_miniplayer_type_entry_3 + + + ORIGINAL + MINIMAL + TABLET + + + @string/revanced_return_youtube_username_display_format_username_only + @string/revanced_return_youtube_username_display_format_username_handle + @string/revanced_return_youtube_username_display_format_handle_username + + + USERNAME_ONLY + USERNAME_HANDLE + HANDLE_USERNAME + + + @string/revanced_shorts_double_tap_to_like_animation_entry_1 + @string/revanced_shorts_double_tap_to_like_animation_entry_2 + @string/revanced_shorts_double_tap_to_like_animation_entry_3 + @string/revanced_shorts_double_tap_to_like_animation_entry_4 + @string/revanced_shorts_double_tap_to_like_animation_entry_5 + @string/revanced_shorts_double_tap_to_like_animation_entry_6 + + + ORIGINAL + THUMBS_UP + THUMBS_UP_CAIRO + HEART + HEART_TINT + HIDDEN + + + @string/revanced_spoof_streaming_data_type_entry_android_unplugged + @string/revanced_spoof_streaming_data_type_entry_android_vr + + + ANDROID_UNPLUGGED + ANDROID_VR + + + @string/revanced_spoof_streaming_data_type_entry_ios + @string/revanced_spoof_streaming_data_type_entry_ios_unplugged + @string/revanced_spoof_streaming_data_type_entry_android_unplugged + @string/revanced_spoof_streaming_data_type_entry_android_vr + + + IOS + IOS_UNPLUGGED + ANDROID_UNPLUGGED + ANDROID_VR + + + + + + + YouTube Music + + + com.google.android.apps.youtube.music + + + -1 + 0 + +1 + +2 + +3 + +4 + +5 + + + -1 + 0 + 1 + 2 + 3 + 4 + 5 + + + @string/revanced_watch_history_type_entry_1 + @string/revanced_watch_history_type_entry_2 + @string/revanced_watch_history_type_entry_3 + + + ORIGINAL + REPLACE + BLOCK + + diff --git a/src/main/resources/youtube/settings/host/values/dimens.xml b/patches/src/main/resources/youtube/settings/host/values/dimens.xml similarity index 100% rename from src/main/resources/youtube/settings/host/values/dimens.xml rename to patches/src/main/resources/youtube/settings/host/values/dimens.xml diff --git a/patches/src/main/resources/youtube/settings/host/values/strings.xml b/patches/src/main/resources/youtube/settings/host/values/strings.xml new file mode 100644 index 000000000..1983cee50 --- /dev/null +++ b/patches/src/main/resources/youtube/settings/host/values/strings.xml @@ -0,0 +1,1843 @@ + + + Enable accessibility controls for the video player? + Your controls are modified because an accessibility service is on. + Continue + Don\'t show again + "GmsCore does not have permission to run in the background. + +Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. + +This is required for the app to work." + "GmsCore battery optimizations must be disabled to prevent issues. + +Disabling battery optimizations for GmsCore will not negatively affect battery usage. + +Tap the continue button and allow optimization changes." + Open website + Action needed + Enable cloud messaging to receive notifications. + Open GmsCore + GmsCore is not installed. Install it. + "DeArrow provides crowd-sourced thumbnails for YouTube videos. These thumbnails are often more relevant than those provided by YouTube. + +If enabled, video URLs will be sent to the API server and no other data is sent. If a video does not have DeArrow thumbnails, then the original or still captures are shown. + +Tap here to learn more about DeArrow." + DeArrow + Invalid DeArrow API URL. + The URL of the DeArrow thumbnail cache endpoint. + DeArrow API endpoint + Toast is not shown if DeArrow is unavailable. + Toast is shown if DeArrow is unavailable. + Show a toast if API is unavailable + DeArrow temporarily unavailable. (status code: %s) + DeArrow temporarily unavailable. + Home tab + You tab + Original thumbnails + DeArrow & original thumbnails + DeArrow & still captures + Still captures + Player playlists, recommendations + Search results + Still video captures + Still captures are taken from the beginning, middle, or end of each video. These images are built into YouTube and no external API is used. + Still video captures + Using high quality still captures. + Using medium quality still captures. Thumbnails will load faster, but live streams, unreleased, or very old videos may show blank thumbnails. + Use fast still captures + Beginning of video + Middle of video + End of video + Video time to take still captures from + Subscriptions tab + Information is not appended to the timestamp. + "Information is appended to the timestamp. + +Tap to configure the video quality or playback speed. +Tap and hold to toggle the appended information type." + Append timestamp information + Append playback speed. + Append video quality. + Append information type + Ambient mode is disabled in battery saver mode. + Ambient mode is enabled in battery saver mode. + Bypass Ambient mode restrictions + The domain to fetch images from.\nNote: Only enter the domain name, i.e., without the \"https\:\/\/\" prefix. + Alternative domain + Using original image host.\n\nEnabling this can fix missing images that are blocked in some regions. + Using image host yt4.ggpht.com. + Bypass image region restrictions + Original + Phone + Phone (Max 480 dp) + Tablet + Tablet (Min 600 dp) + Change layout + Switch toggles are used. + Text toggles are used. + Change toggle type + In-app share sheet is used. + System share sheet is used. + Change share sheet + Change Shorts background repeat state + Autoplay + Default + Pause + Repeat + Change Shorts repeat state + Browse channels + Courses / Learning + Default + Explore + Gaming + History + Library + Liked videos + Live + Movies + Music + Search + Shorts + Sports + Subscriptions + Trending + Watch later + Change start page + Start page changes only once. + "Start page always changes. + +Limitation: Back button on the toolbar may not work." + Change start page type + Generic header is enabled. + Premium header is enabled. + Change YouTube header + List of component path builder strings to filter, separated by new lines. + Custom filter + Custom filter is disabled. + Custom filter is enabled. + Enable custom filter + Invalid custom filter: %s. + Old style flyout menu is used. + Custom dialog is used. + Custom playback speed menu type + Custom speeds must be less than %sx. + Invalid custom playback speeds. + Add or change available playback speeds. + Edit custom playback speeds + Player overlay opacity must be between 0-100. + Opacity value between 0-100, where 0 is transparent. + Custom player overlay opacity + Invalid seekbar color value. + Type the hex code of the seekbar color. + Custom seekbar color value + To open YouTube links in RVX, enable \'Open supported links\' and enable the supported web addresses. + Open default app settings + Default playback speed + Default video quality on mobile network + Default video quality on Wi-Fi network + Disables ambient mode for fullscreen only. + Ambient mode is enabled in fullscreen. + Ambient mode is disabled in fullscreen. + Disable Ambient mode in fullscreen + Disables ambient mode. + Ambient mode is enabled. + Ambient mode is disabled. + Disable Ambient mode + Forced auto audio tracks are enabled. + Forced auto audio tracks are disabled. + Disable forced auto audio tracks + Forced auto captions are enabled. + Forced auto captions are disabled. + Disable forced auto captions + Auto player popup panels are enabled. + Auto player popup panels are disabled. + Disable player popup panels + "Auto switch mix playlists is enabled when autoplay is turned on. + +Autoplay can be changed in YouTube settings: +Settings → Autoplay → Autoplay next video" + Auto switch mix playlists is disabled. + Disable switch mix playlists + Enabling this feature will disable automatic switching to YouTube Mix when playing music while autoplay is turned on. + Default playback speed is enabled for live streams. + Default playback speed is disabled for live streams. + Disable playback speed for live streams + Default playback speed is enabled for music. + "Default playback speed is disabled for music. + +Limitation: This setting may not apply to videos that do not include the 'Listen on YouTube Music' banner." + Disable playback speed for music + Engagement panel is enabled. + Engagement panel is disabled. + Disable engagement panel + Haptic feedback is enabled. + Haptic feedback is disabled. + Disable chapters haptic feedback + Haptic feedback is enabled. + Haptic feedback is disabled. + Disable scrubbing haptic feedback + Haptic feedback is enabled. + Haptic feedback is disabled. + Disable seek haptic feedback + Haptic feedback is enabled. + Haptic feedback is disabled. + Disable seek undo haptic feedback + Haptic feedback is enabled. + Haptic feedback is disabled. + Disable zoom haptic feedback + Auto HDR brightness is enabled. + Auto HDR brightness is disabled. + Disable auto HDR brightness + HDR video is enabled. + HDR video is disabled. + Disable HDR video + Video orientation follows device settings in fullscreen. + Video orientation is portrait mode in fullscreen. + Disable landscape mode + Like and Dislike buttons will glow when mentioned. + Like and Dislike buttons will not glow when mentioned. + Disable Like and Dislike button glow + "Disable CronetEngine's QUIC protocol." + Disable QUIC protocol + Shorts player will resume on app startup. + Shorts player will not resume on app startup. + Disable resuming Shorts player + Rolling numbers are animated. + Rolling numbers are not animated. + Disable Rolling number animations + Chapters are enabled in the seekbar. + Chapters are disabled in the seekbar. + Disable seekbar chapters + Shorts background play is enabled. + Shorts background play is disabled. + Disable Shorts background play + Fountain animation is enabled above the Like button. + Fountain animation is disabled above the Like button. + Disable Like button animation + "Disable '2x>>' while holding down. + +Note: +• Disabling the speed overlay restores the Slide to seek behavior of the old layout. +• Disabling this setting does not forcefully enable the speed overlay." + Disable speed overlay + Splash animation is enabled. + Splash animation is disabled. + Disable splash animation + Swiping up / down will play the next / previous video. + Swiping up / down will not play the next / previous video. + Disable swipe to change video + Dark mode navigation bar is opaque or translucent. + Dark mode navigation bar is opaque. + Disable dark translucent bar + Light mode navigation bar is opaque or translucent. + Light mode navigation bar is opaque. + Disable light translucent bar + Status bar is opaque or translucent. + Status bar is opaque. + Disable translucent status bar + "Disables the following interactions when the video description is expanded: + +• Tap to scroll. +• Tap and hold to select text." + Disable video description interaction + VP9 codec is enabled. + "VP9 codec is disabled. + +• Maximum resolution is 1080p. +• Video playback will use more internet data than VP9. +• VP9 codec is still used for HDR video." + Disable VP9 codec + Entering fullscreen when swiping down below the video player is enabled. + Entering fullscreen when swiping down below the video player is disabled. + Disable watch panel gestures + Cairo seekbar is disabled. + "Cairo seekbar is enabled. + +Side effect: Cairo theme is also applied to notification dots." + Enable Cairo seekbar + Controls overlay fills the fullscreen. + Controls overlay does not fill the fullscreen. + Enable compact controls overlay + Custom playback speed is disabled. + Custom playback speed is enabled. + Enable custom playback speed + Custom seekbar color is disabled. + Custom seekbar color is enabled. + Enable custom seekbar color + Debug logs do not include the buffer. + Debug logs include the buffer. + Enable debug buffer logging + Debug logs are disabled. + Debug logs are enabled. + Enable debug logging + Default playback speed does not apply to Shorts. + Default playback speed applies to Shorts. + Enable Shorts default playback speed + External browser is disabled. + External browser is enabled. + Enable external browser + Gradient loading screen is disabled. + Gradient loading screen is enabled. + Enable gradient loading screen + Spacing between navigation buttons is normal. + Spacing between navigation buttons is narrow. + Enable narrow navigation buttons + Following default redirect policy. + Bypassing URL redirects. + Enable open links directly + Enable the OPUS codec if the player response includes the OPUS codec. + Enable OPUS codec + Do not save and restore brightness when exiting or entering fullscreen. + Save and restore brightness when exiting or entering fullscreen. + Enable save and restore brightness + Seekbar tapping is disabled. + Seekbar tapping is enabled. + Enable seekbar tapping + "This will restore thumbnails to livestreams that do not have seekbar thumbnails. + +Internet data usage may be higher, and seekbar thumbnails will have a slight delay before showing. + +This feature works best with a very fast internet connection." + Seekbar thumbnails are medium quality. + Seekbar thumbnails are high quality. + Enable high quality thumbnails + Custom actions are disabled in flyout menu. + "Custom actions are enabled in flyout menu. + +Limitations: +• Does not work if app version is spoofed to 18.49.37 or earlier. +• Does not work with livestream." + Enable custom actions in flyout menu + Custom actions are disabled in toolbar. + "Custom actions are enabled in toolbar. + +Press and hold the More button to show the Custom actions dialog." + Enable custom actions in toolbar + Timestamp is disabled. + "Timestamp is enabled. + +Limitations: +• This setting not only enables timestamps, but also allows users to hide the UI by clicking on the player background. +• As this is a feature in the development stage by Google, the layout may be broken." + Enable timestamps + Brightness swipe is disabled. + Brightness swipe is enabled. + Enable brightness gesture + Haptic feedback is disabled. + Haptic feedback is enabled. + Enable haptic feedback + Lowest value of the brightness gesture does not activate auto-brightness. + Lowest value of the brightness gesture activates auto-brightness. + Enable auto-brightness gesture + Touch to activate swipe gesture. + Touch and hold to activate swipe gesture. + Enable press-to-swipe gesture + Swiping up / down will not play the next / previous video. + Swiping up / down will play the next / previous video. + Enable swipe to change video + Volume swipe is disabled. + Volume swipe is enabled. + Enable volume gesture + Navigation bar is opaque. + Navigation bar is translucent. + Enable translucent navigation bar + Status bar is opaque. + Status bar is translucent. + Enable translucent status bar + Entering fullscreen when swiping down below the video player is disabled. + Entering fullscreen when swiping down below the video player is enabled. + Enable watch panel gestures + "Enabling this setting will disable the Settings button in the You tab. + +In this case, please use the following path to access the settings: +You tab → View channel → Menu → Settings" + Enable wide search bar in You tab + Wide search bar is disabled. + Wide search bar is enabled. + Enable wide search bar + Wide search bar hides the YouTube header. + Wide search bar does not hide the YouTube header. + Enable wide search bar with header + Description + "Enter the title of the video description panel in your language. +The Expand video description option may not work if the entered string does not match the video description panel title." + Title in video description panel + Video descriptions are not expanded automatically. + Video descriptions are expanded automatically. + Expand video descriptions + Do you wish to proceed? + Reset to default values. + Restart to load the layout normally + Refresh and restart + Failed to export settings. + Settings were successfully exported. + Export settings to a file. + Export settings + Import + Copy + Import or export settings as text. + Import / Export as text + Failed to import settings. + Settings reset to default. + Settings were successfully imported. + Import settings from a saved file. + Import settings + Reset + Search %s + ReVanced Extended + External downloader + Not installed + "%1$s is not installed. +Please download %2$s from the website." + Warning + %s is not installed. Please install it. + Package name of your installed external downloader app, such as YTDLnis. + Playlist downloader package name + Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. + Long press video downloader package name + Package name of your installed external downloader app, such as NewPipe or YTDLnis. + Video downloader package name + "Videos will be switched to fullscreen in the following situations: + +• When a video is started. +• When a timestamp in the comments is clicked on." + Force fullscreen + Displays the optimization dialog for GMSCore at each application startup. + Show optimization dialog for GMSCore + List of account menu names to filter, separated by new lines. + Account menu filter + "Hide elements of the account menu and You tab. +Some components may not be hidden." + Hide account menu + AI-generated video summary section is shown. + AI-generated video summary section is hidden. + Hide AI-generated video summary section + Album cards are shown. + Album cards are hidden. + Hide album cards + Featured places, Games, Music and People mentioned sections are shown. + Featured places, Games, Music and People mentioned sections are hidden. + Hide Attributes section + Autoplay preview container is shown. + Autoplay preview container is hidden. + Hide autoplay preview container + Visit store button is shown. + Visit store button is hidden. + Hide Visit store button + "Hides the following shelves: +• Breaking news +• Continue watching +• Explore more channels +• Listen again +• Shopping +• Watch it again" + Hide carousel shelf + Shown in feed. + Hidden in feed. + Hide in feed + Shown in related videos. + Hidden in related videos. + Hide in related videos + Shown in search results. + Hidden in search results. + Hide in search results + Channel guidelines are shown. + Channel guidelines are hidden. + Hide channel guidelines + Channel member shelf is shown. + Channel member shelf is hidden. + Hide channel member shelf + Links at the top of channel profiles are shown. + Links at the top of channel profiles are hidden. + Hide channel profile links + "Shorts +Playlists +Store" + List of channel tab names to filter, separated by new lines. + Channel tab filter + Channel tab filter is disabled. + Channel tab filter is enabled. + Enable channel tab filter + Channel watermark is shown. + Channel watermark is hidden. + Hide channel watermark + Chapters section is shown. + Chapters section is hidden. + Hide Chapters section + Chips shelf is shown. + Chips shelf is hidden. + Hide chips shelf + Clip button is shown. + Clip button is hidden. + Hide Clip button + Create a Short button is shown. + Create a Short button is hidden. + Hide Create a Short button + Highlighted search links are shown. + Highlighted search links are hidden. + Hide highlighted search links + Thanks button is shown. + Thanks button is hidden. + Hide Thanks button + Timestamp and emoji buttons are shown. + Timestamp and emoji buttons are hidden. + Hide timestamp and emoji buttons + Comments by members banner is shown. + Comments by members banner is hidden. + Hide Comments by members banner + Comments section is shown in home feed. + Comments section is hidden in home feed. + Hide Comments section in home feed + Comments section is shown. + Comments section is hidden. + Hide Comments section + Shown in channel. + Hidden in channel. + Hide in channel + Shown in home feed and related videos. + Hidden in home feed and related videos. + Hide in home feed and related videos + Shown in subscriptions feed. + Hidden in subscriptions feed. + Hide in subscriptions feed + How this content was made section is shown. + How this content was made section is hidden. + Hide Contents section + Crowdfunding box is shown. + Crowdfunding box is hidden. + Hide crowdfunding box + Double-tap overlay filter is shown. + Double-tap overlay filter is hidden. + Hide double-tap overlay filter + Download button is shown. + Download button is hidden. + Hide Download button + End screen cards are shown. + End screen cards are hidden. + Hide end screen cards + Expandable chips are shown. + Expandable chips are hidden. + Hide expandable chip under videos + Expandable shelves are shown. + Expandable shelves are hidden. + Hide expandable shelves + Captions button is shown. + Captions button is hidden. + Hide Captions button + List of flyout menu names to filter, separated by new lines. + Feed flyout menu filter + Feed flyout menu filter is disabled. + Feed flyout menu filter is enabled. + Enable feed flyout menu filter + Search bar is shown. + Search bar is hidden. + Hide search bar + Surveys are shown. + Surveys are hidden. + Hide surveys + Film strip overlay is shown. + Film strip overlay is hidden. + Hide film strip overlay + Floating button is shown. + Floating button is hidden. + Hide floating button + Floating microphone button is shown. + Floating microphone button is hidden. + Hide floating microphone button + For You shelf is shown. + For You shelf is hidden. + Hide For You shelf + Fullscreen ads are shown. + Fullscreen ads are hidden. + Hide fullscreen ads + General ads are shown. + General ads are hidden. + Hide general ads + YouTube Premium promotion is shown. + YouTube Premium promotion is hidden. + Hide YouTube Premium promotion + Gray separators are shown. + Gray separators are hidden. + Hide gray separators + Handle is shown. + Handle is hidden. + Hide handle + Image search button is shown. + Image search button is hidden. + Hide image search button + Image shelves are shown. + Image shelves are hidden. + Hide image shelves + Info cards section is shown. + Info cards section is hidden. + Hide Info cards section + Info cards are shown. + Info cards are hidden. + Hide info cards + Info panels are shown. + Info panels are hidden. + Hide info panels + Join button is shown. + Join button is hidden. + Hide Join button + Key concepts section is shown. + Key concepts section is hidden. + Hide Key concepts section + "Home / Subscription / Search results are filtered to hide content that matches keyword phrases. + +Limitations: +• Shorts cannot be hidden by channel name. +• Some UI components may not be hidden. +• Searching for a keyword may show no results." + About keyword filtering + Surrounding a keyword/phrase with double-quotes will prevent partial matches of video titles and channel names.<br><br>For example,<br><b>\"ai\"</b> will hide the video: <b>How does AI work?</b><br>but will not hide: <b>What does fair use mean?</b> + Match whole words + Comments are not filtered. + Comments are filtered. + Hide comments by keywords + Videos in home feed are not filtered. + Videos in home feed are filtered. + Hide home videos by keywords + "Keywords and phrases to hide, separated by new lines. + +Keywords can be channel names or any text shown in video titles. + +Words with uppercase letters in the middle must be entered with the casing (ie: iPhone, TikTok, LeBlanc)." + Keywords to hide + Search results are not filtered. + Search results are filtered. + Hide search results by keywords + Videos in subscriptions feed are not filtered. + Videos in subscriptions feed are filtered. + Hide subscription videos by keywords + Keyword will hide all videos: %s. + Cannot use keyword: %s. + Add quotes to use keyword: %s. + Keyword has conflicting declarations: %s. + Keyword is too short and requires quotes: %s. + Latest posts are shown. + Latest posts are hidden. + Hide latest posts + Latest videos button is shown. + Latest videos button is hidden. + Hide Latest videos button + Like and Dislike buttons are shown. + Like and Dislike buttons are hidden. + Hide Like and Dislike buttons + Live chat messages are shown.\n\nThis setting applies to Shorts live videos too. + Live chat messages are hidden.\n\nThis setting applies to Shorts live videos too. + Hide live chat messages + Live chat replay button is shown.\n\nIt appears in fullscreen when closing live chat. + Live chat replay button is hidden.\n\nIt appears in fullscreen when closing live chat. + Hide live chat replay button + Chat summary is shown. + Chat summary is hidden. + Hide Chat summary in live chat + Hide videos with less than 1,000 views from home feeds that have been uploaded from unsubscribed channels. + Hide low views video + Medical panels are shown. + Medical panels are hidden. + Hide medical panels + Merchandise shelves are shown. + Merchandise shelves are hidden. + Hide merchandise shelves + Mix playlists are shown. + Mix playlists are hidden. + Hide mix playlists + Movies shelves are shown. + Movies shelves are hidden. + Hide movies shelves + Navigation bar is shown. + Navigation bar is hidden. + Hide navigation bar + Create button is shown. + Create button is hidden. + Hide Create button + Home button is shown. + Home button is hidden. + Hide Home button + Navigation labels are shown. + Navigation labels are hidden. + Hide navigation labels + Library button is shown. + Library button is hidden. + Hide Library button + Notifications button is shown. + Notifications button is hidden. + Hide notifications button + Shorts button is shown. + Shorts button is hidden. + Hide Shorts button + Subscriptions button is shown. + Subscriptions button is hidden. + Hide Subscriptions button + Notify me button is shown. + Notify me button is hidden. + Hide Notify me button + Paid promotion label is shown. + Paid promotion label is hidden. + Hide paid promotion label + Playables are shown. + Playables are hidden. + Hide Playables + Autoplay button is shown. + Autoplay button is hidden. + Hide Autoplay button + Captions button is shown. + Captions button is hidden. + Hide Captions button + Cast button is shown. + Cast button is hidden. + Hide Cast button + Collapse button is shown. + Collapse button is hidden. + Hide collapse button + Ambient mode menu is shown. + Ambient mode menu is hidden. + Hide Ambient mode menu + Audio track menu is shown. + Audio track menu is hidden. + Hide Audio track menu + Captions menu footer is shown. + Captions menu footer is hidden. + Hide captions menu footer + Captions menu is shown. + Captions menu is hidden. + Hide Captions menu + 1080p Premium menu is shown. + 1080p Premium menu is hidden. + Hide 1080p Premium menu + Help & feedback menu is shown. + Help & feedback menu is hidden. + Hide Help & feedback menu + Listen with YouTube Music menu is shown. + Listen with YouTube Music menu is hidden. + Hide Listen with YouTube Music menu + Lock screen menu is shown. + Lock screen menu is hidden. + Hide Lock screen menu + Loop video menu is shown. + Loop video menu is hidden. + Hide Loop video menu + More information menu is shown. + More information menu is hidden. + Hide More information menu + Picture-in-picture menu is shown. + Picture-in-picture menu is hidden. + Hide Picture-in-picture menu + Playback speed menu is shown. + Playback speed menu is hidden. + Hide Playback speed menu + Premium controls menu is shown. + Premium controls menu is hidden. + Hide Premium controls menu + Quality menu footer is shown. + Quality menu footer is hidden. + Hide quality menu footer + Quality menu header is shown. + Quality menu header is hidden. + Hide quality menu header + Report menu is shown. + Report menu is hidden. + Hide Report menu + Sleep timer menu is shown. + Sleep timer menu is hidden. + Hide Sleep timer menu + Stable volume menu is shown. + Stable volume menu is hidden. + Hide Stable volume menu + Stats for nerds menu is shown. + Stats for nerds menu is hidden. + Hide Stats for nerds menu + Watch in VR menu is shown. + Watch in VR menu is hidden. + Hide Watch in VR menu + Fullscreen button is shown. + Fullscreen button is hidden. + Hide Fullscreen button + Buttons are shown. + Buttons are hidden. + Hide Previous & Next buttons + Shopping shelf is shown. + Shopping shelf is hidden. + Hide player shopping shelf + YouTube Music button is shown. + YouTube Music button is hidden. + Hide YouTube Music button + Save button is shown. + Save button is hidden. + Hide Save button + Explore the podcast section is shown. + Explore the podcast section is hidden. + Hide Explore the podcast section + Preview comment is shown. + Preview comment is hidden. + Hide preview comment + This changes the size of the comments section, so it is impossible to open a live chat replay in the comments section. + This does not change the size of the comments section, so it is possible to open the live chat replay in the comments section. + Hide preview comment type + Promotion alert banner is shown. + Promotion alert banner is hidden. + Hide promotion alert banner + Comments button is shown. + Comments button is hidden. + Hide Comments button + Dislike button is shown. + Dislike button is hidden. + Hide Dislike button + Like button is shown. + Like button is hidden. + Hide Like button + Live chat button is shown. + Live chat button is hidden. + Hide Live chat button + More button is shown. + More button is hidden. + Hide More button + Open mix playlist button is shown. + Open mix playlist button is hidden. + Hide Open mix playlist button + Open playlist button is shown. + Open playlist button is hidden. + Hide Open playlist button + Save button is shown. + Save button is hidden. + Hide Save button + Share button is shown. + Share button is hidden. + Hide Share button + Quick actions container is shown. + Quick actions container is hidden. + Hide quick actions container + "Hides the following recommended videos: + +• Videos with the Members only tag. +• Videos with phrases such as 'People also watched' underneath." + Hide recommended videos + More videos section in the quick actions container and the related video overlay are shown. + More videos section in the quick actions container and the related video overlay are hidden. + Hide related video overlay + Related videos are shown. + Related videos are hidden. + Hide related videos + "This setting limits the maximum number of layouts that can be loaded on the player screen. + +If the layout of the player screen changes due to server-side changes, unintended layouts may be hidden on the player screen." + Remix button is shown. + Remix button is hidden. + Hide Remix button + Report button is shown. + Report button is hidden. + Hide Report button + Rewards button is shown. + Rewards button is hidden. + Hide Rewards button + Thumbnails in the search term history are shown. + Thumbnails in the search term history are hidden. + Hide search term thumbnails + Seek message is shown. + Seek message is hidden. + Hide seek message + Seek undo message is shown. + Seek undo message is hidden. + Hide seek undo message + Chapter labels next to the timestamp are shown. + Chapter labels next to the timestamp are hidden. + Hide seekbar chapter labels + Video player seekbar is shown. + Video player seekbar is hidden. + Thumbnail seekbar is shown. + Thumbnail seekbar is hidden. + Hide seekbar in video thumbnails + Hide seekbar in video player + Self sponsored cards are shown. + Self sponsored cards are hidden. + Hide self sponsored cards + About menu is shown. + About menu is hidden. + Hide About menu + Accessibility menu is shown. + Accessibility menu is hidden. + Hide Accessibility menu + Account menu is shown. + Account menu is hidden. + Hide Account menu + Autoplay menu is shown. + Autoplay menu is hidden. + Hide Autoplay menu + Billing and payments menu is shown. + Billing and payments menu is hidden. + Hide Billing and payments menu + Captions menu is shown. + Captions menu is hidden. + Hide Captions menu + Connected apps menu is shown. + Connected apps menu is hidden. + Hide Connected apps menu + Data saving menu is shown. + Data saving menu is hidden. + Hide Data saving menu + General menu is shown. + General menu is hidden. + Hide General menu + Manage all history menu is shown. + Manage all history menu is hidden. + Hide Manage all history menu + Live chat menu is shown. + Live chat menu is hidden. + Hide Live chat menu + Notifications menu is shown. + Notifications menu is hidden. + Hide Notifications menu + Background menu is shown. + Background menu is hidden. + Hide Background menu + Watch on TV menu is shown. + Watch on TV menu is hidden. + Hide Watch on TV menu + Family Center menu is shown. + Family Center menu is hidden. + Hide Family Center menu + Try experimental new features menu is shown. + Try experimental new features menu is hidden. + Hide Try experimental new features menu + Privacy menu is shown. + Privacy menu is hidden. + Hide Privacy menu + Purchases and memberships menu is shown. + Purchases and memberships menu is hidden. + Hide Purchases and memberships menu + Hide elements of the YouTube settings menu. + Hide YouTube settings menu + Video quality preferences menu is shown. + Video quality preferences menu is hidden. + Hide Video quality preferences menu + Your data in YouTube menu is shown. + Your data in YouTube menu is hidden. + Hide Your data in YouTube menu + Share button is shown. + Share button is hidden. + Hide Share button + Shop button is shown. + Shop button is hidden. + Hide Shop button + Shopping links are shown. + Shopping links are hidden. + Hide Shopping links + Channel bar is shown. + Channel bar is hidden. + Hide channel bar + Comments button is shown. + Comments button is hidden. + Hide Comments button + Disabled comments button or with label \"0\" is shown. + Disabled comments button or with label \"0\" is hidden. + Hide disabled comments button + Dislike button is shown. + Dislike button is hidden. + Hide Dislike button + "Floating buttons like 'Use this sound' are shown in the Shorts channel tab." + "Floating buttons like 'Use this sound' are hidden in the Shorts channel tab." + Hide floating button + Video link label is shown. + Video link label is hidden. + Hide full video link label + Green screen button is shown. + Green screen button is hidden. + Hide Green screen button + Info panels are shown. + Info panels are hidden. + Hide info panels + Join button is shown. + Join button is hidden. + Hide Join button + Like button is shown. + Like button is hidden. + Hide Like button + Live chat header is shown.\n\nBack button in header will not be hidden. + Live chat header is hidden.\n\nBack button in header will not be hidden. + Hide live chat header + Location button is shown. + Location button is hidden. + Hide location button + Navigation bar is shown. + Navigation bar is hidden. + Hide navigation bar + Paid promotion label is shown. + Paid promotion label is hidden. + Hide paid promotion label + Paused header is shown. + Paused header is hidden. + Hide paused header + Paused overlay buttons are shown. + Paused overlay buttons are hidden. + Hide paused overlay buttons + Button background is shown. + Button background is hidden. + Hide Play & Pause button background + Remix button is shown. + Remix button is hidden. + Hide Remix button + Save music button is shown. + Save music button is hidden. + Hide Save music button + Search suggestions button is shown. + Search suggestions button is hidden. + Hide search suggestions button + Share button is shown. + Share button is hidden. + Hide Share button + Shown in channel. + "Hidden in channel. + +Info: +• Only shelves with the Shorts header on the home tab are hidden." + Hide in channel + Shown in watch history. + Hidden in watch history. + Hide in watch history + Shown in home feed and related videos. + Hidden in home feed and related videos. + Hide in home feed and related videos + Shown in search results. + Hidden in search results. + Hide in search results + Shown in subscriptions feed. + Hidden in subscriptions feed. + Hide in subscriptions feed + "Hides Shorts shelves. + +Side effect: Official headers in search results will be hidden." + Hide Shorts shelves + Shop button is shown. + Shop button is hidden. + Hide Shop button + Shopping button is shown. + Shopping button is hidden. + Hide Shopping button + Sound button is shown. + Sound button is hidden. + Hide sound button + Metadata label is shown. + Metadata label is hidden. + Hide sound metadata label + Stickers are shown. + Stickers are hidden. + Hide stickers + Subscribe button is shown. + Subscribe button is hidden. + Hide Subscribe button + Super Thanks button is shown. + Super Thanks button is hidden. + Hide Super Thanks button + Tagged products are shown. + Tagged products are hidden. + Hide tagged products + Toolbar is shown. + Toolbar is hidden. + Hide toolbar + Trends button is shown. + Trends button is hidden. + Hide Trends button + Use template button is shown. + Use template button is hidden. + Hide Use template button + Use this sound button is shown. + Use this sound button is hidden. + Hide Use this sound button + Title is shown. + Title is hidden. + Hide video title + Show more button is shown. + Show more button is hidden. + Hide Show more button + Snack bar is shown. + Snack bar is hidden. + Hide snack bar + Start trial button is shown. + Start trial button is hidden. + Hide Start trial button + Subscriptions carousel is shown. + Subscriptions carousel is hidden. + Hide subscriptions carousel + Suggested actions are shown. + Suggested actions are hidden. + Hide suggested actions + "This setting has been deprecated. + +Instead, use the 'Settings → Autoplay → Autoplay next video' setting. + +Note: +• If you have any issues with 'Suggested video end screen', try restarting the app." + Suggested video end screen is shown. + "Suggested video end screen is hidden when autoplay is turned off. + +Autoplay can be changed in YouTube settings: +Settings → Autoplay → Autoplay next video" + Hide suggested video end screen + Thanks button is shown. + Thanks button is hidden. + Hide Thanks button + Ticket shelves are shown. + Ticket shelves are hidden. + Hide ticket shelves + Timestamp is shown. + Timestamp is hidden. + Hide timestamp + Timed reactions are shown. + Timed reactions are hidden. + Hide timed reactions + Cast button is shown. + Cast button is hidden. + Hide Cast button + Create button is shown. + Create button is hidden. + Hide Create button + Notifications button is shown. + Notifications button is hidden. + Hide Notifications button + Transcript section is shown. + Transcript section is hidden. + Hide Transcript section + Video ads are shown. + Video ads are hidden. + Hide video ads + "Home / Subscription / Search results are filtered to hide videos with views less or greater than a specified number. + +Limitations: +• Shorts cannot be hidden. +• Videos with 0 views are not filtered." + About view count filtering + Videos in home feed are not filtered. + Videos in home feed are filtered. + Hide home videos by views + Search results are not filtered. + Search results are filtered. + Hide search results by views + Videos in subscriptions feed are not filtered. + Videos in subscriptions feed are filtered. + Hide subscription videos by views + Hide recommended videos with less than a specified number of views.\n\nKnown issue: Videos with 0 views are not filtered. + Hide recommended videos by views + Videos with views greater than this number will be hidden. + Greater than views + Videos with views less than this number will be hidden. + Less than views + K -> 1 000\nM -> 1 000 000\nB -> 1 000 000 000\nviews -> views + Specify your language template for the number of views shown under each video in the user interface. Each key (a letter/word in your language) -> value (meaning of the key) must be on a new line. Keys go before "->" sign. If you change the app or system language you need to reset this setting.\n\nExamples:\nEnglish: 10K views = K -> 1000, views -> views\nSpanish: 10 K vistas = K -> 1000, vistas -> views + View keys + View products banner is shown. + View products banner is hidden. + Hide view products banner + Voice search button is shown. + Voice search button is hidden. + Hide voice search button + Web search results are shown. + Web search results are hidden. + Hide web search results + YouTube Doodles are shown. + YouTube Doodles are hidden. + Hide YouTube Doodles + "YouTube Doodles show up a few days each year. + +If a YouTube Doodle is currently showing in your region and this setting is on, the filter bar below the search bar will also be hidden." + Zoom overlay is shown. + Zoom overlay is hidden. + Hide zoom overlay + Afn Blue + Afn Red + Custom + Stock + MMT + MMT Blue + MMT Green + MMT Orange + MMT Pink + MMT Turquoise + MMT Yellow + Revancify Blue + Revancify Red + Revancify Yellow + Vanced Black + Vanced Light + Xisr Yellow + YouTube + YouTube Black + Keeps landscape mode when turning the screen off and on in fullscreen. + The amount of milliseconds the landscape mode is forced after the screen in turned on. + Keep landscape mode timeout + Keep landscape mode + Stock + Double-tap action and pinch to resize is disabled. + "Double-tap action and pinch to resize is enabled. + +• Double tap to increase miniplayer size. +• Double tap again to restore original size." + Enable double-tap and pinch to resize + Drag and drop is disabled. + "Drag and drop is enabled. + +Miniplayer can be dragged to any corner of the screen." + Enable drag and drop + Expand and close buttons are shown. + "Buttons are hidden. + +Swipe to expand or close." + Hide expand and close buttons + Close button is shown. + Close button is hidden. + Hide close button + Skip forward and back are shown. + Skip forward and back are hidden. + Hide skip forward and back buttons + Subtexts are shown. + Subtexts are hidden. + Hide subtexts + Horizontal drag gesture disabled. + "Horizontal drag gesture enabled. + +Miniplayer can be dragged off screen to the left or right." + Enable horizontal drag gesture. + Miniplayer overlay opacity must be between 0-100. + Opacity value between 0-100, where 0 is transparent. + Overlay opacity + Corners are square. + Corners are rounded. + Enable rounded corners + Disabled + Original + Minimal + Tablet + Modern 1 + Modern 2 + Modern 3 + Miniplayer type + Pixel size must be between %1$s and %2$s. + Initial on screen size, in pixels. + Initial size + Overlay buttons + "Tap to toggle always repeat states. +Tap and hold to toggle pause after repeat states." + Show always repeat button + "Tap to copy video URL. +Tap and hold to copy video URL with timestamp." + "Tap to copy video URL with timestamp. +Tap and hold to copy video timestamp." + Show copy timestamp URL button + Show copy video URL button + Tap to launch external downloader. + Show external downloader button + Tap to mute volume of the current video. Tap again to unmute. + Show mute volume button + Tap and hold to change button state. + Unable to generate playlist due to channel id mismatch. + "Tap to generate a playlist of all videos from channel. +Tap and hold to undo. + +Info: +• May not work on livestreams." + Show play all button + All contents (Sort by time, Ascending) + All contents (Sort by time) + Members only videos + Members only shorts + Members only livestreams + All contents (Sort by popular) + Videos only (Sort by time) + Videos only (Sort by popular) + Shorts only (Sort by time) + Shorts only (Sort by popular) + Streamed videos only (Sort by time) + Streamed videos only (Sort by popular) + All Members only contents + Generate playlist mode + Playback speed reset: %sx. + "Tap to open speed dialog. +Tap and hold to reset playback speed to 1.0x. Tap and hold again to reset back to default speed." + Show speed dialog button + "Tap to open whitelist dialog. +Tap and hold to open whitelist setting dialog. + Show whitelist button + If shown, the native playlist download button opens the native in-app downloader. + Native playlist download button is always shown, and in public playlists, it opens your external downloader. + Override playlist download button + Native video download button opens the native in-app downloader. + Native video download button opens your external downloader. + Override video download button + YouTube Music is required to override button action. Tap here to download YouTube Music. + Prerequisite + YouTube Music button opens the native app. + YouTube Music button opens the RVX Music. + Override YouTube Music button + Excluded + Included + Normal + Action buttons + Additional settings + Animation / Feedback + Custom actions + Download button + Experimental Flags + Image region restrictions + Import / Export as file + Import / Export as text + Keyword filter + Others + Overlay buttons + Patch information + Quick actions + Recommended video + Shorts shelves + Suggested actions + Tool used + View count filter + Hide or show elements in account menu and You tab. + Account menu + Hide or show action buttons under videos. + Action buttons + Ads + Alternative thumbnails + Disable Ambient mode or bypass Ambient mode restrictions. + Ambient mode + Hide or show the category bar in the feed, search, and related videos. + Category bar + Hide or show components of the channel bar under videos. + Channel bar + Hide or show components in the channel profile. + Channel profile + Hide or show comments section components. + Comments + Hide or show community posts in the feed and channel. + Community posts + Hide components using custom filters. + Custom filter + Hide or show components of the flyout menu in the feed. + Flyout menu + Feed + Hide or change components related to fullscreen. + Fullscreen + General + Disable or enable haptic feedback. + Haptic feedback + Overrides the click action of in-app buttons. + Hook buttons + Import or export settings. + Import / Export settings + Change the style of the in app minimized player. + Miniplayer + Miscellaneous + Hide or show navigation bar section components. + Navigation bar + Information about applied patches. + Patch information + Hide or show buttons in the video player. + Player buttons + Hide or change components of the flyout menu in the video player. + Flyout menu + Player + Return YouTube Username + Return YouTube Dislike + SponsorBlock + Customize the seekbar components. + Seekbar + Hide elements of the YouTube settings menu. + Settings menu + Hide or show components in the Shorts player. + Shorts player + Shorts + Spoof the streaming data to prevent playback issues. + Spoof streaming data + Swipe controls + Hide or change components located on the toolbar, such as the search bar, toolbar buttons, and header. + Toolbar + Hide or show video description components. + Video description + Hide videos by keywords or views. + Video filter + Video + Change settings related with watch history. + Watch history + Quick actions top margin must be between 0-32. + Configure the spacing from the seekbar to the quick action container, between 0-32. + Quick actions top margin + "Forcefully rejects the software AV1 codec response. +A different codec will be applied after about 20 seconds of buffering." + Reject software AV1 codec response + Fallback process causes about 20 seconds of buffering. + Offset + Playback speed changes only apply to the current video. + Playback speed changes apply to all videos. + Remember playback speed changes + A toast will not be shown when changing the default playback speed. + A toast will be shown when changing the default playback speed. + Show a toast + Changing default speed to %s. + Quality changes only apply to the current video. + Quality changes apply to all videos. + Remember video quality changes + A toast will not be shown when changing the default video quality. + A toast will be shown when changing the default video quality. + Show a toast + Changing default mobile data quality to %s. + Failed to set video quality. + Changing default Wi-Fi quality to %s. + "Removes the viewer discretion dialog. +This does not bypass the age restriction. It just accepts it automatically." + Remove viewer discretion dialog + Replaces the software AV1 codec with the VP9 codec. + Replace software AV1 codec + Channel handle is used. + Channel name is used. + Replace channel handle + Tap to show the remaining time. + Tap to open playback speed or video quality flyout menu. + Replace timestamp action + Replaces the Create button with the Settings button. + Replace Create button + "Tap to open YouTube settings. +Tap and hold to open RVX settings." + "Tap to open RVX settings. +Tap and hold to open YouTube settings." + Action type to assign to button + Seekbar thumbnails will appear in fullscreen. + Seekbar thumbnails will appear above the seekbar. + Restore old seekbar thumbnails + Old video quality menu is not shown. + Old video quality menu is shown. + Restore old video quality menu + Old player layout is not used. + "Old player layout is used. +No margins on top and bottom of player." + Restore old player layout + @handle (Username) + Display format + Username (@handle) + Username + Handle is used. + Username is used. + Enable Return YouTube Username + "A YouTube Data API v3 Developer Key is required to replace handles with usernames. + +The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. + +Click to see how to issue a API key." + About YouTube Data API key + The developer key for using the YouTube Data API v3. + YouTube Data API key + 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. + Issue YouTube Data API v3 developer key + About + Dislike data is provided by the Return YouTube Dislike API. Tap here to learn more. + ReturnYouTubeDislike.com + Like button styled for best appearance. + Like button styled for minimum width. + Compact Like button + Dislikes shown as a number. + Dislikes shown as a percentage. + Dislikes as percentage + Dislikes are not shown. + Dislikes are shown. + Enable Return YouTube Dislike + Estimated likes are hidden. + Estimated likes are shown. + Show estimated likes + Dislikes unavailable (client API limit reached). + Dislikes unavailable (status %d). + Dislikes temporarily unavailable (API timed out). + Dislikes unavailable (%s). + Reload video to vote using Return YouTube Dislike + Dislikes hidden on Shorts. + Dislikes shown on Shorts. + "Dislikes shown on Shorts. + +Limitation: Dislikes may not appear if the user is not logged in or in incognito mode." + Show dislikes on Shorts + Toast is not shown if Return YouTube Dislike is unavailable. + Toast is shown if Return YouTube Dislike is unavailable. + Show a toast if API is unavailable + Hidden + Removes tracking query parameters from the URLs when sharing links. + Sanitize sharing links + "Phrases like '#', 'Fundraiser', 'Shop' and 'products' were shown from the video subtitles." + "Phrases like '#', 'Fundraiser', 'Shop' and 'products' were hidden from the video subtitles." + Sanitize video subtitle + About + sponsor.ajay.app + Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. + API URL changed. + API URL is invalid. + API URL reset. + Appearance + Color changed. + Color: + Invalid color code. + Color reset. + Creating new segments + Change segment behavior + Automatically hide skip button + Skip button displayed for entire segment. + Skip button hides after several seconds. + Use compact skip button + Skip button styled for best appearance. + Skip button styled for minimum width. + Show create new segment button + Create new segment button is not shown. + Create new segment button is shown. + Enable SponsorBlock + SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. + Show voting button + Segment voting button is not shown. + Segment voting button is shown. + General + Adjust new segment step + Value must be a positive number. + Number of milliseconds the time adjustment buttons move when creating new segments. + Change API URL + The address SponsorBlock uses to make calls to the server. + Minimum segment duration + Invalid time duration. + Segments shorter than this value (in seconds) will not be shown or skipped. + Enable skip count tracking + Skip count tracking is not enabled. + Lets the SponsorBlock leaderboard know how much time is saved. A message is sent to the leaderboard each time a segment is skipped. + Show a toast when skipping automatically + Toast is not shown. Tap here to see an example. + Toast is shown when a segment is automatically skipped. Tap here to see an example. + Show video length without segments + Full video length shown. + Video length minus the combined segment length is shown in parentheses next to the full video length. + Your private user id + Private user id must be at least 30 characters long. + This should be kept private. This is like a password and should not be shared with anyone. If someone has this, they can impersonate you. + Already read + Read the SponsorBlock guidelines before creating new segments. + Show me + Follow the guidelines + Guidelines contain rules and tips for creating new segments. + View guidelines + Adjust: Mark Start and End Time for segment + Choose the segment category + Verify the Segment + The segment lasts from %1$02d:%2$02d to %3$02d:%4$02d (%5$d minutes %6$02d seconds)\nIs it ready to submit? + The segment is from\n\n%1$s\nto\n%2$s\n\n(%3$s)\n\nReady to submit? + Are the times correct? + Category is disabled in settings. Enable category to submit. + Edit the Segment + Do you want to edit the timing for the start or end of the segment? + Invalid time given. + Edit timing of segment manually + Forward by Specified Time (Default: 150ms) + Set %s as the start or end of a new segment? + end + Mark two locations on the time bar first. + start + now + Preview the segment, and ensure it skips smoothly. + Publish Created Segment + Rewind by Specified Time (Default: 150ms) + Start must be before the end. + Time the segment ends at + Time the segment begins at + New SponsorBlock segment + Reset + Reset color + Filler Tangent / Jokes + Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. + Highlight + The part of the video that most people are looking for. + Interaction Reminder (Subscribe) + A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. + Intermission / Intro Animation + An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. + Music: Non-Music Section + Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. + Endcards / Credits + Credits or when the YouTube endcards appear. Not for conclusions with information. + Preview / Recap / Hook + Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. + Unpaid / Self Promotion + Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. + Sponsor + Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. + Copy + Failed to export: %s. + Import / Export settings + Your SponsorBlock JSON configuration that can be imported / exported to ReVanced Extended and other SponsorBlock platforms. + Your SponsorBlock JSON configuration that can be imported / exported to ReVanced Extended and other SponsorBlock platforms. This includes your private user id. Be sure to share this wisely. + Failed to import: %s. + Settings imported successfully. + Your settings contain a private SponsorBlock userid.\n\nYour user id is like a password and it should never be shared.\n + Do not show again + Settings copied to clipboard. + Skip automatically + Skip automatically once + Skip + Highlight + Skip filler + Skip to highlight + Skip interact + Skip intro + Skip intermission + Skip intermission + Skip non-music + Skip outro + Skip preview + Skip recap + Skip preview + Skip promo + Skip sponsor + Skip segment + Disable + Show in seek bar + Show a skip button + Skipped filler. + Skipped to highlight. + Skipped annoying reminder. + Skipped intro. + Skipped intermission. + Skipped intermission. + Skipped multiple segments. + Skipped a non-music section. + Skipped outro. + Skipped preview. + Skipped recap. + Skipped preview. + Skipped self promotion. + Skipped sponsor. + Skipped unsubmitted segment. + SponsorBlock temporarily unavailable. + SponsorBlock temporarily unavailable (status %d). + SponsorBlock temporarily unavailable (API timed out). + Stats + Stats temporarily unavailable (API is down). + Loading... + Your reputation is <b>%.2f</b> + You\'ve saved people from <b>%s</b> segments + %1$s hours %2$s minutes + %1$s minutes %2$s seconds + %s seconds + That\'s <b>%s</b> of their lives.<br>Tap here to see the leaderboard. + Tap here to see the global stats and top contributors. + SponsorBlock leaderboard + SponsorBlock is disabled. + You\'ve skipped <b>%s</b> segments + Reset skipped segments counter? + That\'s <b>%s</b>. + You\'ve created <b>%s</b> segments + Tap here to view your segments. + Your username: <b>%s</b> + Tap here to change your username + Unable to change username: Status: %1$d %2$s. + Username successfully changed. + Can\'t submit the segment.\nAlready exists. + Can\'t submit the segment: %s. + Unable to submit segment: %s. + Unable to submit segment.\nRate Limited (too many from the same user or IP). + SponsorBlock is temporarily down. + Unable to submit segment (status: %1$d %2$s). + Segment submitted successfully. + Toast is not shown if SponsorBlock is unavailable. + Toast is shown if SponsorBlock is unavailable. + Show a toast if API is unavailable + Change category + Downvote + Unable to vote for segment: %s. + Unable to vote for segment (API timed out). + Unable to vote for segment (status: %1$d %2$s). + There are no segments to vote for. + Upvote + Settings copied to clipboard. + Timestamp copied to clipboard. (%s) + URL copied to clipboard. + URL with timestamp copied to clipboard. + "This feature is still experimental, so there is no guarantee that it will work perfectly. + +Most bugs cannot be fixed due to client-side limitations, so use it only for testing purposes." + About Custom actions + Copy video URL + Copy video URL menu is hidden. + Copy video URL menu is shown. + Copy timestamp URL + Copy timestamp URL menu is hidden. + Copy timestamp URL menu is shown. + Show copy timestamp URL menu + Show copy video URL menu + External downloader + External downloader menu is hidden. + External downloader menu is shown. + Show external downloader menu + Open video + Open video menu is hidden. + Open video menu is shown. + Show open video menu + Repeat state + Repeat state menu is hidden. + Repeat state menu is shown. + Show repeat state menu + Custom actions + Original + Thumbs up + Thumbs up (Cairo) + Heart + Heart (Tint) + Hidden + Double-tap animation + Meta panel bottom margin must be between 0-64. + Configure the spacing from the seekbar to the meta panel, between 0-64. + Meta panel bottom margin + Height percentage must be between 0-100 (%). + Configure the height percentage of the empty space left when the navigation bar is hidden, between 0 and 100 (%). + Height percentage of empty space + Press and hold the timestamp to change the Shorts repeat status. + Timestamp long press action + "Shows the video title section in fullscreen. + +Limitation: Video title disappears when clicked." + Show video title section + If autoplay is enabled, the next video will play after the countdown ends. + If autoplay is enabled, the next video will play immediately. + Skip autoplay countdown + "Skips the preloaded buffer at the start of videos to immediately apply the default video quality. + +Info: +• When the video starts, there is a delay of approximately 0.3 seconds. +• Does not apply to HDR videos, live stream videos, or videos shorter than 15 seconds." + Skip preloaded buffer + Toast is not shown. + Toast is shown. + Show a toast when skipping + Turning on this setting may cause video playback issues. + Skipped preloaded buffer. + Speed overlay value must be between 0-8.0. + Speed overlay value between 0-8.0. + Speed overlay value + "Spoofing the client version to the old version. + +• This will change the appearance of the app, but unknown side effects may occur. +• If later turned off, the old UI may remain until clear the app data." + Version not spoofed + Version spoofed + 17.33.42 - Restore old UI layout + 17.41.37 - Restore old playlist shelf + 18.05.40 - Restore old comment input box + 18.17.43 - Restore old player flyout panel + 18.33.40 - Restore old Shorts action bar + 18.38.45 - Restore old default video quality behavior + 18.48.39 - Disable views and likes from being updated in real time + 19.13.37 - Restore old style Rolling number animations + 19.26.42 - Disable Cairo icon in navigation and toolbar + 19.33.37 - Restore old playback speed flyout panel + Spoof app version target + Type the spoof app version target. + Edit spoof app version + Spoof app version + "App version will be spoofed to an older version of YouTube. + +This will change the appearance and features of the app, but unknown side effects may occur. + +If later turned off, it is recommended to clear the app data to prevent UI bugs." + "Spoofs the device dimensions to the maximum value. +High quality may be unlocked on some videos that require high device dimensions, but not all videos." + Spoof device dimensions + Android and iOS clients are used to fetch streaming data. + Android clients are used to fetch streaming data. + Use Android clients only + Turning off this setting may cause video playback issues. + "• Audio track menu is missing. +• Stable volume is not available. +• Disable forced auto audio tracks is not available." + "• Audio track menu is missing. +• Stable volume is not available." + "• Audio track menu is missing. +• Stable volume is not available." + • Not yet found. + "• Videos may end 1 second early. +• OPUS audio codec may not be supported." + • Videos may end 1 second early. + • Movies or paid videos may not play. + Spoofing side effects + • Video may not play. + Client used to fetch streaming data is hidden in Stats for nerds. + Client used to fetch streaming data is shown in Stats for nerds. + Show in Stats for nerds + "Streaming data is not spoofed. Video playback may not work." + Streaming data is spoofed. + Spoof streaming data + Android + Android Creator + Android TV + Android VR + iOS + iOS Music + iOS TV + Default client + Turning off this setting may cause video playback issues. + Brightness swipe sensitivity must be between 1-1000 (%). + Configure the minimum distance for brightness swiping between 1 and 1000 (%).\nThe shorter the minimum distance, the faster the brightness level changes. + Brightness swipe sensitivity + Swipe gestures are disabled in Lock screen mode. + Swipe gestures are enabled in Lock screen mode. + Swipe gestures in Lock screen mode + Auto + The amount of threshold for swipe to occur. + Swipe magnitude threshold + The visibility of swipe overlay background. + Swipe background visibility + Swipeable area size cannot be more than 50. + Percentage of swipeable screen area.\n\nNote: This will also change the size of the screen area for the double-tap-to-seek gesture. + Swipe overlay screen size + The text size for swipe overlay. + Swipe overlay text size + The amount of milliseconds the overlay is visible. + Swipe overlay timeout + Volume swipe sensitivity must be between 1-1000 (%). + Configure the minimum distance for volume swiping between 1 and 1000 (%).\n\nThe shorter the minimum distance, the faster the volume level changes.\n\nRecommended volume swipe sensitivity is 100% at 15-volume steps and 10% at 150-volume steps. + Volume swipe sensitivity + "Swaps the positions of the Create button with the Notifications button by spoofing device information. + +• The device may need to be rebooted for a change of this setting to take effect. +• Disabling this setting loads more ads from the server side. +• You should disable this setting to make video ads visible." + Create button is not switched with Notifications button. + "Create button is switched with Notifications button. + +Note: Enabling this also forcibly hides video ads." + Swap Create and Notifications buttons + "Disabling this might load more ads from the server. + +Also, ads will no longer be blocked in Shorts. + +If this setting do not take effect, try switching to Incognito mode." + Stock + RVX Music + Warning + %s is not installed. Please install it. + Package name of installed RVX Music. + RVX Music package name + • Watch history is blocked. + "• Follows the watch history settings of Google account. +• Watch history may not work due to DNS or VPN." + • Follows the watch history settings of Google account. + Status of watch history + Click to open the YouTube watch history management. + Manage all history + Original + Replace domain + Block watch history + Watch history type + Failed to add channel \'%1$s\' to the %2$s whitelist. + Channel \'%1$s\' was added to the %2$s whitelist. + There are no whitelisted channels. + Not added to whitelist. + Failed to load channel information. + Added to whitelist. + Playback speed + Remove channel \'%1$s\' from %2$s whitelist? + Failed to remove channel \'%1$s\' from the %2$s whitelist. + Channel \'%1$s\' was removed from the %2$s whitelist. + Check or remove the list of channels added to the whitelist. + Channel whitelist + SponsorBlock + \ No newline at end of file diff --git a/src/main/resources/youtube/settings/host/values/styles.xml b/patches/src/main/resources/youtube/settings/host/values/styles.xml similarity index 100% rename from src/main/resources/youtube/settings/host/values/styles.xml rename to patches/src/main/resources/youtube/settings/host/values/styles.xml diff --git a/src/main/resources/youtube/settings/layout/revanced_settings_preferences_category.xml b/patches/src/main/resources/youtube/settings/layout/revanced_settings_preferences_category.xml similarity index 100% rename from src/main/resources/youtube/settings/layout/revanced_settings_preferences_category.xml rename to patches/src/main/resources/youtube/settings/layout/revanced_settings_preferences_category.xml diff --git a/src/main/resources/youtube/settings/layout/revanced_settings_with_toolbar.xml b/patches/src/main/resources/youtube/settings/layout/revanced_settings_with_toolbar.xml similarity index 100% rename from src/main/resources/youtube/settings/layout/revanced_settings_with_toolbar.xml rename to patches/src/main/resources/youtube/settings/layout/revanced_settings_with_toolbar.xml diff --git a/patches/src/main/resources/youtube/settings/values-v21/strings.xml b/patches/src/main/resources/youtube/settings/values-v21/strings.xml new file mode 100644 index 000000000..6fe87a4c3 --- /dev/null +++ b/patches/src/main/resources/youtube/settings/values-v21/strings.xml @@ -0,0 +1,7 @@ + + + + @string/revanced_spoof_streaming_data_side_effects_android + @string/revanced_spoof_streaming_data_side_effects_android + @string/revanced_spoof_streaming_data_side_effects_android + diff --git a/src/main/resources/youtube/settings/xml/revanced_prefs.xml b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml similarity index 79% rename from src/main/resources/youtube/settings/xml/revanced_prefs.xml rename to patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml index 1faf961c8..fe5434ff2 100644 --- a/src/main/resources/youtube/settings/xml/revanced_prefs.xml +++ b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml @@ -8,12 +8,12 @@ + + SETTINGS: HOOK_DOWNLOAD_ACTIONS --> @@ -38,20 +38,43 @@ SETTINGS: HOOK_BUTTONS --> + SETTINGS: MINIPLAYER_TYPE_MODERN --> + + + + + SETTINGS: MINIPLAYER_DOUBLE_TAP_ACTION --> + SETTINGS: MINIPLAYER_DRAG_AND_DROP --> + + + + + + + + + + + + + + + + + SETTINGS: TRANSLUCENT_NAVIGATION_BAR --> + + @@ -144,8 +173,8 @@ SETTINGS: HIDE_LAYOUT_COMPONENTS --> - + @@ -158,8 +187,8 @@ + + SETTINGS: SPOOF_APP_VERSION --> @@ -190,9 +219,9 @@ - + - + SETTINGS: ALTERNATIVE_THUMBNAILS --> @@ -200,7 +229,7 @@ + SETTINGS: BYPASS_IMAGE_REGION_RESTRICTIONS --> @@ -217,7 +246,7 @@ - + @@ -232,7 +261,7 @@ - - - + + + @@ -379,6 +409,9 @@ SETTINGS: HIDE_PLAYER_FLYOUT_MENU --> + + + SETTINGS: KEEP_LANDSCAPE_MODE --> @@ -444,7 +477,7 @@ - + @@ -482,18 +515,18 @@ - + SETTINGS: DESCRIPTION_INTERACTION --> + + + + + + + + + + SETTINGS: SHORTS_TIME_STAMP --> + + + + + + @@ -590,6 +651,9 @@ SETTINGS: SHORTS_COMPONENTS --> + + @@ -603,24 +667,24 @@ - - - - - + + + + + - - PREFERENCE_SCREEN: SWIPE_CONTROLS --> + + PREFERENCE_SCREEN: SWIPE_CONTROLS --> - + - + @@ -636,7 +700,7 @@ - + @@ -676,9 +740,9 @@ @@ -688,7 +752,7 @@ - + @@ -699,23 +763,23 @@ - - - - - - - - - + + + + + + + + + - - + + - + PREFERENCE_SCREEN: SPONSOR_BLOCK --> @@ -726,15 +790,15 @@ - + @@ -744,7 +808,7 @@ - + SETTINGS: WATCH_HISTORY --> - + ", "") - ) - } - } - - fun ResourceContext.updatePatchStatus(patchTitle: String) { - updatePatchStatusSettings(patchTitle, "@string/revanced_patches_included") - } - - fun ResourceContext.updatePatchStatusIcon(iconName: String) { - iconType = iconName - updatePatchStatusSettings("Icon", "@string/revanced_icon_$iconName") - } - - fun ResourceContext.updatePatchStatusLabel(appName: String) { - updatePatchStatusSettings("Label", appName) - } - - fun ResourceContext.updatePatchStatusTheme(themeName: String) { - updatePatchStatusSettings("Theme", themeName) - } - - fun ResourceContext.updatePatchStatusSettings( - patchTitle: String, - updateText: String - ) { - this.xmlEditor[TARGET_PREFERENCE_PATH].use { editor -> - editor.file.doRecursively loop@{ - if (it !is Element) return@loop - - it.getAttributeNode("android:title")?.let { attribute -> - if (attribute.textContent == patchTitle) { - it.getAttributeNode("android:summary").textContent = updateText - } - } - } - } - } - - fun ResourceContext.addPreferenceFragment(key: String, insertKey: String) { - val targetClass = - "com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity" - - this.xmlEditor[YOUTUBE_SETTINGS_PATH].use { editor -> - with(editor.file) { - val processedKeys = mutableSetOf() // To track processed keys - - doRecursively loop@{ node -> - if (node !is Element) return@loop // Skip if not an element - - val attributeNode = node.getAttributeNode("android:key") - ?: return@loop // Skip if no key attribute - val currentKey = attributeNode.textContent - - // Check if the current key has already been processed - if (processedKeys.contains(currentKey)) { - return@loop // Skip if already processed - } else { - processedKeys.add(currentKey) // Add the current key to processedKeys - } - - when (currentKey) { - insertKey -> { - node.insertNode("Preference", node) { - setAttribute("android:key", "${key}_key") - setAttribute("android:title", "@string/${key}_title") - this.appendChild( - ownerDocument.createElement("intent").also { intentNode -> - intentNode.setAttribute( - "android:targetPackage", - youtubePackageName - ) - intentNode.setAttribute("android:data", key + "_intent") - intentNode.setAttribute("android:targetClass", targetClass) - } - ) - } - node.setAttribute("app:iconSpaceReserved", "true") - } - - "true" -> { - attributeNode.textContent = "false" - } - } - } - } - } - } -} diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsBytecodePatch.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsBytecodePatch.kt deleted file mode 100644 index e46e4a123..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsBytecodePatch.kt +++ /dev/null @@ -1,75 +0,0 @@ -package app.revanced.patches.youtube.utils.settings - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patches.shared.integrations.Constants.INTEGRATIONS_UTILS_CLASS_DESCRIPTOR -import app.revanced.patches.shared.integrations.Constants.INTEGRATIONS_UTILS_PATH -import app.revanced.patches.shared.mapping.ResourceMappingPatch -import app.revanced.patches.youtube.utils.integrations.Constants.UTILS_PATH -import app.revanced.patches.youtube.utils.mainactivity.MainActivityResolvePatch -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch -import app.revanced.patches.youtube.utils.settings.fingerprints.ThemeSetterSystemFingerprint -import app.revanced.util.resultOrThrow -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction - -@Patch( - dependencies = [ - MainActivityResolvePatch::class, - ResourceMappingPatch::class, - SharedResourceIdPatch::class - ] -) -object SettingsBytecodePatch : BytecodePatch( - setOf(ThemeSetterSystemFingerprint) -) { - private const val INTEGRATIONS_INITIALIZATION_CLASS_DESCRIPTOR = - "$UTILS_PATH/InitializationPatch;" - - private const val INTEGRATIONS_THEME_METHOD_DESCRIPTOR = - "$INTEGRATIONS_UTILS_PATH/BaseThemeUtils;->setTheme(Ljava/lang/Enum;)V" - - internal lateinit var contexts: BytecodeContext - - override fun execute(context: BytecodeContext) { - contexts = context - - // apply the current theme of the settings page - ThemeSetterSystemFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - injectCall(implementation!!.instructions.size - 1) - injectCall(it.scanResult.patternScanResult!!.startIndex) - } - } - - MainActivityResolvePatch.injectOnCreateMethodCall( - INTEGRATIONS_INITIALIZATION_CLASS_DESCRIPTOR, - "setExtendedUtils" - ) - MainActivityResolvePatch.injectOnCreateMethodCall( - INTEGRATIONS_INITIALIZATION_CLASS_DESCRIPTOR, - "onCreate" - ) - MainActivityResolvePatch.injectConstructorMethodCall( - INTEGRATIONS_UTILS_CLASS_DESCRIPTOR, - "setActivity" - ) - - } - - private fun MutableMethod.injectCall(index: Int) { - val register = getInstruction(index).registerA - - addInstructions( - index + 1, """ - invoke-static {v$register}, $INTEGRATIONS_THEME_METHOD_DESCRIPTOR - return-object v$register - """ - ) - removeInstruction(index) - } -} diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt deleted file mode 100644 index dfa7cc981..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt +++ /dev/null @@ -1,332 +0,0 @@ -package app.revanced.patches.youtube.utils.settings - -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption -import app.revanced.patches.shared.elements.StringsElementsUtils.removeStringsElements -import app.revanced.patches.shared.mapping.ResourceMappingPatch -import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE -import app.revanced.patches.youtube.utils.fix.cairo.CairoSettingsPatch -import app.revanced.patches.youtube.utils.integrations.IntegrationsPatch -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch -import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference -import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreferenceFragment -import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatus -import app.revanced.patches.youtube.utils.settings.ResourceUtils.updatePatchStatusSettings -import app.revanced.util.* -import app.revanced.util.patch.BaseBytecodePatch -import app.revanced.util.patch.BaseResourcePatch -import org.w3c.dom.Element -import java.io.Closeable -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import java.util.jar.Manifest - -@Suppress("DEPRECATION", "unused") -object SettingsPatch : BaseResourcePatch( - name = "Settings for YouTube", - description = "Applies mandatory patches to implement ReVanced Extended settings into the application.", - dependencies = setOf( - IntegrationsPatch::class, - ResourceMappingPatch::class, - SharedResourceIdPatch::class, - SettingsBytecodePatch::class, - CairoSettingsPatch::class, - ), - compatiblePackages = COMPATIBLE_PACKAGE, - requiresIntegrations = true -), Closeable { - private const val DEFAULT_POSITION_KEY = "About" - private const val DEFAULT_NAME = "ReVanced Extended" - - private val SETTINGS_ELEMENTS_MAP = mapOf( - "Parent settings" to "@string/parent_tools_key", - "General" to "@string/general_key", - "Account" to "@string/account_switcher_key", - "Data saving" to "@string/data_saving_settings_key", - "Autoplay" to "@string/auto_play_key", - "Video quality preferences" to "@string/video_quality_settings_key", - "Background" to "@string/offline_key", - "Watch on TV" to "@string/pair_with_tv_key", - "Manage all history" to "@string/history_key", - "Your data in YouTube" to "@string/your_data_key", - "Privacy" to "@string/privacy_key", - "History & privacy" to "@string/privacy_key", - "Try experimental new features" to "@string/premium_early_access_browse_page_key", - "Purchases and memberships" to "@string/subscription_product_setting_key", - "Billing & payments" to "@string/billing_and_payment_key", - "Billing and payments" to "@string/billing_and_payment_key", - "Notifications" to "@string/notification_key", - "Connected apps" to "@string/connected_accounts_browse_page_key", - "Live chat" to "@string/live_chat_key", - "Captions" to "@string/captions_key", - "Accessibility" to "@string/accessibility_settings_key", - DEFAULT_POSITION_KEY to "@string/about_key", - ) - - private val InsertPosition = stringPatchOption( - key = "InsertPosition", - default = DEFAULT_POSITION_KEY, - values = SETTINGS_ELEMENTS_MAP, - title = "Insert position", - description = "The settings menu name that the RVX settings menu should be above.", - required = true - ) - - private val RVXSettingsMenuName = stringPatchOption( - key = "RVXSettingsMenuName", - default = DEFAULT_NAME, - title = "RVX settings menu name", - description = "The name of the RVX settings menu.", - required = true - ) - - private lateinit var customName: String - - internal lateinit var contexts: ResourceContext - internal var upward1831 = false - internal var upward1834 = false - internal var upward1839 = false - internal var upward1842 = false - internal var upward1849 = false - internal var upward1902 = false - internal var upward1915 = false - internal var upward1923 = false - internal var upward1925 = false - internal var upward1928 = false - - override fun execute(context: ResourceContext) { - - /** - * check patch options - */ - customName = RVXSettingsMenuName - .valueOrThrow() - - // can be a key (case-insensitive) or a value - val rawLowerInsertKey = InsertPosition.lowerCaseOrThrow() - - val lowerCaseSettingsMap = SETTINGS_ELEMENTS_MAP.mapKeys { it.key.lowercase() } - - val insertKey = lowerCaseSettingsMap[rawLowerInsertKey] - // If not found, look for a matching value in the lowercase settings map - ?: lowerCaseSettingsMap.values.find { it == rawLowerInsertKey } - // If still not found, use the default position key from the original map - ?: SETTINGS_ELEMENTS_MAP[DEFAULT_POSITION_KEY]!! - - /** - * set resource context - */ - contexts = context - - /** - * set version info - */ - setVersionInfo() - - /** - * remove strings duplicated with RVX resources - * - * YouTube does not provide translations for these strings. - * That's why it's been added to RVX resources. - * This string also exists in RVX resources, so it must be removed to avoid being duplicated. - */ - context.removeStringsElements( - arrayOf("values"), - arrayOf( - "accessibility_settings_edu_opt_in_text", - "accessibility_settings_edu_opt_out_text" - ) - ) - - /** - * copy arrays, strings and preference - */ - arrayOf( - "arrays.xml", - "dimens.xml", - "strings.xml", - "styles.xml" - ).forEach { xmlFile -> - context.copyXmlNode("youtube/settings/host", "values/$xmlFile", "resources") - } - - arrayOf( - ResourceGroup( - "drawable", - "revanced_cursor.xml", - ), - ResourceGroup( - "layout", - "revanced_settings_preferences_category.xml", - "revanced_settings_with_toolbar.xml", - ), - ResourceGroup( - "xml", - "revanced_prefs.xml", - ) - ).forEach { resourceGroup -> - context.copyResources("youtube/settings", resourceGroup) - } - - /** - * initialize ReVanced Extended Settings - */ - context.addPreferenceFragment( - "revanced_extended_settings", - insertKey - ) - - /** - * remove ReVanced Extended Settings divider - */ - arrayOf("Theme.YouTube.Settings", "Theme.YouTube.Settings.Dark").forEach { themeName -> - context.xmlEditor["res/values/styles.xml"].use { editor -> - with(editor.file) { - val resourcesNode = getElementsByTagName("resources").item(0) as Element - - val newElement: Element = createElement("item") - newElement.setAttribute("name", "android:listDivider") - - for (i in 0 until resourcesNode.childNodes.length) { - val node = resourcesNode.childNodes.item(i) as? Element ?: continue - - if (node.getAttribute("name") == themeName) { - newElement.appendChild(createTextNode("@null")) - - node.appendChild(newElement) - } - } - } - } - } - - /** - * set revanced-patches version - */ - val jarManifest = classLoader.getResources("META-INF/MANIFEST.MF") - while (jarManifest.hasMoreElements()) - contexts.updatePatchStatusSettings( - "ReVanced Patches", - Manifest(jarManifest.nextElement().openStream()) - .mainAttributes - .getValue("Version") + "" - ) - - /** - * set revanced-integrations version - */ - val versionName = SettingsBytecodePatch.contexts - .findClass { it.sourceFile == "BuildConfig.java" }!! - .mutableClass - .fields - .single { it.name == "VERSION_NAME" } - .initialValue - .toString() - .trim() - .replace("\"", "") - .replace(""", "") - - contexts.updatePatchStatusSettings( - "ReVanced Integrations", - versionName - ) - } - - override fun close() { - /** - * change RVX settings menu name - * since it must be invoked after the Translations patch, it must be the last in the order. - */ - if (customName != DEFAULT_NAME) { - contexts.removeStringsElements( - arrayOf("revanced_extended_settings_title") - ) - contexts.xmlEditor["res/values/strings.xml"].use { editor -> - val document = editor.file - - mapOf( - "revanced_extended_settings_title" to customName - ).forEach { (k, v) -> - val stringElement = document.createElement("string") - - stringElement.setAttribute("name", k) - stringElement.textContent = v - - document.getElementsByTagName("resources").item(0) - .appendChild(stringElement) - } - } - } - } - - private fun setVersionInfo() { - val threadCount = Runtime.getRuntime().availableProcessors() - val threadPoolExecutor = Executors.newFixedThreadPool(threadCount) - - val resourceXmlFile = contexts["res/values/integers.xml"].readBytes() - - for (threadIndex in 0 until threadCount) { - threadPoolExecutor.execute thread@{ - contexts.xmlEditor[resourceXmlFile.inputStream()].use { editor -> - val resources = editor.file.documentElement.childNodes - val resourcesLength = resources.length - val jobSize = resourcesLength / threadCount - - val batchStart = jobSize * threadIndex - val batchEnd = jobSize * (threadIndex + 1) - element@ for (i in batchStart until batchEnd) { - if (i >= resourcesLength) return@thread - - val node = resources.item(i) - if (node !is Element) continue - - if (node.nodeName != "integer" || !node.getAttribute("name") - .startsWith("google_play_services_version") - ) continue - - val playServicesVersion = node.textContent.toInt() - - upward1831 = 233200000 <= playServicesVersion - upward1834 = 233500000 <= playServicesVersion - upward1839 = 234000000 <= playServicesVersion - upward1842 = 234302000 <= playServicesVersion - upward1849 = 235000000 <= playServicesVersion - upward1902 = 240204000 < playServicesVersion - upward1915 = 241602000 <= playServicesVersion - upward1923 = 242402000 <= playServicesVersion - upward1925 = 242599000 <= playServicesVersion - upward1928 = 242905000 <= playServicesVersion - - break - } - } - } - } - - threadPoolExecutor - .also { it.shutdown() } - .awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS) - } - - internal fun addPreference(settingArray: Array) { - contexts.addPreference(settingArray) - } - - internal fun updatePatchStatus(patch: BaseResourcePatch) { - updatePatchStatus(patch.name!!) - } - - internal fun updatePatchStatus(patch: BaseBytecodePatch) { - updatePatchStatus(patch.name!!) - } - - private val patchList = ArrayList() - - internal fun updatePatchStatus(patchName: String) { - patchList.add(patchName) - contexts.updatePatchStatus(patchName) - } - - internal fun containsPatch(patchName: String) = - patchList.contains(patchName) -} diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/settings/fingerprints/ThemeSetterSystemFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/settings/fingerprints/ThemeSetterSystemFingerprint.kt deleted file mode 100644 index 3b5636d25..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/settings/fingerprints/ThemeSetterSystemFingerprint.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.revanced.patches.youtube.utils.settings.fingerprints - -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.Appearance -import app.revanced.util.fingerprint.LiteralValueFingerprint -import com.android.tools.smali.dexlib2.Opcode - -internal object ThemeSetterSystemFingerprint : LiteralValueFingerprint( - returnType = "L", - opcodes = listOf(Opcode.RETURN_OBJECT), - literalSupplier = { Appearance }, -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/SponsorBlockBytecodePatch.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/SponsorBlockBytecodePatch.kt deleted file mode 100644 index 9bda4f425..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/SponsorBlockBytecodePatch.kt +++ /dev/null @@ -1,194 +0,0 @@ -package app.revanced.patches.youtube.utils.sponsorblock - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.InstructionExtensions.addInstruction -import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patches.youtube.utils.fingerprints.SeekbarFingerprint -import app.revanced.patches.youtube.utils.fingerprints.SeekbarOnDrawFingerprint -import app.revanced.patches.youtube.utils.fingerprints.TotalTimeFingerprint -import app.revanced.patches.youtube.utils.fingerprints.YouTubeControlsOverlayFingerprint -import app.revanced.patches.youtube.utils.integrations.Constants.INTEGRATIONS_PATH -import app.revanced.patches.youtube.utils.integrations.Constants.PATCH_STATUS_CLASS_DESCRIPTOR -import app.revanced.patches.youtube.utils.playercontrols.PlayerControlsPatch -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.InsetOverlayViewLayout -import app.revanced.patches.youtube.utils.sponsorblock.fingerprints.RectangleFieldInvalidatorFingerprint -import app.revanced.patches.youtube.utils.sponsorblock.fingerprints.SegmentPlaybackControllerFingerprint -import app.revanced.patches.youtube.video.information.VideoInformationPatch -import app.revanced.util.alsoResolve -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstructionOrThrow -import app.revanced.util.indexOfFirstInstructionReversedOrThrow -import app.revanced.util.indexOfFirstWideLiteralInstructionValueOrThrow -import app.revanced.util.resultOrThrow -import app.revanced.util.updatePatchStatus -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -import com.android.tools.smali.dexlib2.iface.reference.FieldReference -import com.android.tools.smali.dexlib2.iface.reference.MethodReference - -@Patch( - dependencies = [ - PlayerControlsPatch::class, - SharedResourceIdPatch::class, - VideoInformationPatch::class - ] -) -object SponsorBlockBytecodePatch : BytecodePatch( - setOf( - SeekbarFingerprint, - SegmentPlaybackControllerFingerprint, - TotalTimeFingerprint, - YouTubeControlsOverlayFingerprint - ) -) { - private const val INTEGRATIONS_SPONSOR_BLOCK_PATH = - "$INTEGRATIONS_PATH/sponsorblock" - - private const val INTEGRATIONS_SPONSOR_BLOCK_UI_PATH = - "$INTEGRATIONS_SPONSOR_BLOCK_PATH/ui" - - private const val INTEGRATIONS_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR = - "$INTEGRATIONS_SPONSOR_BLOCK_PATH/SegmentPlaybackController;" - - private const val INTEGRATIONS_SPONSOR_BLOCK_VIEW_CONTROLLER_CLASS_DESCRIPTOR = - "$INTEGRATIONS_SPONSOR_BLOCK_UI_PATH/SponsorBlockViewController;" - - override fun execute(context: BytecodeContext) { - - VideoInformationPatch.apply { - // Hook the video time method - videoTimeHook( - INTEGRATIONS_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR, - "setVideoTime" - ) - // Initialize the player controller - onCreateHook( - INTEGRATIONS_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR, - "initialize" - ) - } - - SeekbarOnDrawFingerprint.alsoResolve( - context, SeekbarFingerprint - ).mutableMethod.apply { - // Get left and right of seekbar rectangle - val moveObjectIndex = indexOfFirstInstructionOrThrow(opcode = Opcode.MOVE_OBJECT_FROM16) - - addInstruction( - moveObjectIndex + 1, - "invoke-static/range {p0 .. p0}, " + - "$INTEGRATIONS_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->setSponsorBarRect(Ljava/lang/Object;)V" - ) - - // Set seekbar thickness - val roundIndex = indexOfFirstInstructionOrThrow { - getReference()?.name == "round" - } + 1 - val roundRegister = getInstruction(roundIndex).registerA - - addInstruction( - roundIndex + 1, - "invoke-static {v$roundRegister}, " + - "$INTEGRATIONS_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->setSponsorBarThickness(I)V" - ) - - // Draw segment - val drawCircleIndex = indexOfFirstInstructionReversedOrThrow { - getReference()?.name == "drawCircle" - } - val drawCircleInstruction = getInstruction(drawCircleIndex) - addInstruction( - drawCircleIndex, - "invoke-static {v${drawCircleInstruction.registerC}, v${drawCircleInstruction.registerE}}, " + - "$INTEGRATIONS_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->drawSponsorTimeBars(Landroid/graphics/Canvas;F)V" - ) - } - - // Voting & Shield button - arrayOf("CreateSegmentButtonController;", "VotingButtonController;").forEach { className -> - PlayerControlsPatch.hookTopControlButton("$INTEGRATIONS_SPONSOR_BLOCK_UI_PATH/$className") - } - - // Append timestamp - TotalTimeFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val targetIndex = indexOfFirstInstructionOrThrow { - getReference()?.name == "getString" - } + 1 - val targetRegister = getInstruction(targetIndex).registerA - - addInstructions( - targetIndex + 1, """ - invoke-static {v$targetRegister}, $INTEGRATIONS_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->appendTimeWithoutSegments(Ljava/lang/String;)Ljava/lang/String; - move-result-object v$targetRegister - """ - ) - } - } - - // Initialize the SponsorBlock view - YouTubeControlsOverlayFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val targetIndex = - indexOfFirstWideLiteralInstructionValueOrThrow(InsetOverlayViewLayout) - val checkCastIndex = indexOfFirstInstructionOrThrow(targetIndex, Opcode.CHECK_CAST) - val targetRegister = - getInstruction(checkCastIndex).registerA - - addInstruction( - checkCastIndex + 1, - "invoke-static {v$targetRegister}, $INTEGRATIONS_SPONSOR_BLOCK_VIEW_CONTROLLER_CLASS_DESCRIPTOR->initialize(Landroid/view/ViewGroup;)V" - ) - } - } - - // Replace strings - RectangleFieldInvalidatorFingerprint.alsoResolve( - context, SeekbarFingerprint - ).let { result -> - result.mutableMethod.apply { - val invalidateIndex = - RectangleFieldInvalidatorFingerprint.indexOfInvalidateInstruction(this) - val rectangleIndex = indexOfFirstInstructionReversedOrThrow(invalidateIndex + 1) { - getReference()?.type == "Landroid/graphics/Rect;" - } - val rectangleFieldName = - (getInstruction(rectangleIndex).reference as FieldReference).name - - SegmentPlaybackControllerFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val replaceIndex = it.scanResult.patternScanResult!!.startIndex - val replaceRegister = - getInstruction(replaceIndex).registerA - - replaceInstruction( - replaceIndex, - "const-string v$replaceRegister, \"$rectangleFieldName\"" - ) - } - } - } - } - - // The vote and create segment buttons automatically change their visibility when appropriate, - // but if buttons are showing when the end of the video is reached then they will not automatically hide. - // Add a hook to forcefully hide when the end of the video is reached. - VideoInformationPatch.videoEndMethod.addInstruction( - 0, - "invoke-static {}, $INTEGRATIONS_SPONSOR_BLOCK_VIEW_CONTROLLER_CLASS_DESCRIPTOR->endOfVideoReached()V" - ) - - // Set current video id - VideoInformationPatch.hook("$INTEGRATIONS_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->newVideoStarted(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") - - context.updatePatchStatus(PATCH_STATUS_CLASS_DESCRIPTOR, "SponsorBlock") - - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/SponsorBlockPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/SponsorBlockPatch.kt deleted file mode 100644 index 3b36d09c1..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/SponsorBlockPatch.kt +++ /dev/null @@ -1,171 +0,0 @@ -package app.revanced.patches.youtube.utils.sponsorblock - -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.booleanPatchOption -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption -import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE -import app.revanced.patches.youtube.utils.settings.SettingsPatch -import app.revanced.util.* -import app.revanced.util.patch.BaseResourcePatch -import org.w3c.dom.Element - -@Suppress("DEPRECATION", "unused") -object SponsorBlockPatch : BaseResourcePatch( - name = "SponsorBlock", - description = "Adds options to enable and configure SponsorBlock, which can skip undesired video segments, such as sponsored content.", - dependencies = setOf( - SettingsPatch::class, - SponsorBlockBytecodePatch::class - ), - compatiblePackages = COMPATIBLE_PACKAGE -) { - private const val RIGHT = "right" - - private val OutlineIcon by booleanPatchOption( - key = "OutlineIcon", - default = true, - title = "Outline icons", - description = "Apply the outline icon.", - required = true - ) - - private val NewSegmentAlignment by stringPatchOption( - key = "NewSegmentAlignment", - default = RIGHT, - values = mapOf( - "Right" to RIGHT, - "Left" to "left", - ), - title = "New segment alignment", - description = "Align new segment window.", - required = true - ) - - override fun execute(context: ResourceContext) { - /** - * merge SponsorBlock drawables to main drawables - */ - arrayOf( - ResourceGroup( - "layout", - "revanced_sb_inline_sponsor_overlay.xml", - "revanced_sb_skip_sponsor_button.xml" - ), - ResourceGroup( - "drawable", - "revanced_sb_drag_handle.xml", - "revanced_sb_new_segment_background.xml", - "revanced_sb_skip_sponsor_button_background.xml" - ) - ).forEach { resourceGroup -> - context.copyResources("youtube/sponsorblock/shared", resourceGroup) - } - - if (OutlineIcon == true) { - arrayOf( - ResourceGroup( - "layout", - "revanced_sb_new_segment.xml" - ), - ResourceGroup( - "drawable", - "revanced_sb_adjust.xml", - "revanced_sb_backward.xml", - "revanced_sb_compare.xml", - "revanced_sb_edit.xml", - "revanced_sb_forward.xml", - "revanced_sb_logo.xml", - "revanced_sb_publish.xml", - "revanced_sb_voting.xml" - ) - ).forEach { resourceGroup -> - context.copyResources("youtube/sponsorblock/outline", resourceGroup) - } - } else { - arrayOf( - ResourceGroup( - "layout", - "revanced_sb_new_segment.xml" - ), - ResourceGroup( - "drawable", - "revanced_sb_adjust.xml", - "revanced_sb_compare.xml", - "revanced_sb_edit.xml", - "revanced_sb_logo.xml", - "revanced_sb_publish.xml", - "revanced_sb_voting.xml" - ) - ).forEach { resourceGroup -> - context.copyResources("youtube/sponsorblock/default", resourceGroup) - } - } - - if (NewSegmentAlignment == "left") { - context.xmlEditor["res/layout/revanced_sb_inline_sponsor_overlay.xml"].use { editor -> - editor.file.doRecursively { node -> - if (node is Element && node.tagName == "app.revanced.integrations.youtube.sponsorblock.ui.NewSegmentLayout") { - node.setAttribute("android:layout_alignParentRight", "false") - node.setAttribute("android:layout_alignParentLeft", "true") - } - } - } - } - - /** - * merge xml nodes from the host to their real xml files - */ - // copy nodes from host resources to their real xml files - var modifiedControlsLayout = false - - inputStreamFromBundledResource( - "youtube/sponsorblock", - "shared/host/layout/youtube_controls_layout.xml", - )?.let { hostingResourceStream -> - val editor = context.xmlEditor["res/layout/youtube_controls_layout.xml"] - - // voting button id from the voting button view from the youtube_controls_layout.xml host file - val votingButtonId = "@+id/revanced_sb_voting_button" - - "RelativeLayout".copyXmlNode( - context.xmlEditor[hostingResourceStream], - editor - ).also { - val document = editor.file - val children = document.getElementsByTagName("RelativeLayout").item(0).childNodes - - // Replace the startOf with the voting button view so that the button does not overlap - for (i in 1 until children.length) { - val view = children.item(i) - - val playerVideoHeading = view.hasAttributes() && - view.attributes.getNamedItem("android:id").nodeValue.endsWith("player_video_heading") - - // Replace the attribute for a specific node only - if (!playerVideoHeading) continue - - view.attributes.getNamedItem("android:layout_toStartOf").nodeValue = - votingButtonId - - modifiedControlsLayout = true - break - } - }.close() - } - - if (!modifiedControlsLayout) throw PatchException("Could not modify controls layout") - - /** - * Add settings - */ - SettingsPatch.addPreference( - arrayOf( - "PREFERENCE_SCREEN: SPONSOR_BLOCK" - ) - ) - - SettingsPatch.updatePatchStatus(this) - - } -} diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/fingerprints/RectangleFieldInvalidatorFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/fingerprints/RectangleFieldInvalidatorFingerprint.kt deleted file mode 100644 index 89415c67a..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/fingerprints/RectangleFieldInvalidatorFingerprint.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.revanced.patches.youtube.utils.sponsorblock.fingerprints - -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patches.youtube.utils.sponsorblock.fingerprints.RectangleFieldInvalidatorFingerprint.indexOfInvalidateInstruction -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstructionReversed -import com.android.tools.smali.dexlib2.iface.Method -import com.android.tools.smali.dexlib2.iface.reference.MethodReference - -internal object RectangleFieldInvalidatorFingerprint : MethodFingerprint( - returnType = "V", - parameters = emptyList(), - customFingerprint = { methodDef, _ -> - indexOfInvalidateInstruction(methodDef) >= 0 - } -) { - fun indexOfInvalidateInstruction(methodDef: Method) = - methodDef.indexOfFirstInstructionReversed { - getReference()?.name == "invalidate" - } -} diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/fingerprints/SegmentPlaybackControllerFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/fingerprints/SegmentPlaybackControllerFingerprint.kt deleted file mode 100644 index fde182634..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/fingerprints/SegmentPlaybackControllerFingerprint.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.revanced.patches.youtube.utils.sponsorblock.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patches.youtube.utils.integrations.Constants.INTEGRATIONS_PATH -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode - -internal object SegmentPlaybackControllerFingerprint : MethodFingerprint( - returnType = "V", - accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, - parameters = listOf("Ljava/lang/Object;"), - opcodes = listOf(Opcode.CONST_STRING), - customFingerprint = { methodDef, _ -> - methodDef.definingClass == "$INTEGRATIONS_PATH/sponsorblock/SegmentPlaybackController;" - && methodDef.name == "setSponsorBarRect" - } -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/ToolBarHookPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/ToolBarHookPatch.kt deleted file mode 100644 index f4e7ef890..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/ToolBarHookPatch.kt +++ /dev/null @@ -1,70 +0,0 @@ -package app.revanced.patches.youtube.utils.toolbar - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patches.youtube.utils.integrations.Constants.UTILS_PATH -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch -import app.revanced.patches.youtube.utils.toolbar.fingerprints.ToolBarButtonFingerprint -import app.revanced.patches.youtube.utils.toolbar.fingerprints.ToolBarPatchFingerprint -import app.revanced.util.resultOrThrow -import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction - -@Patch(dependencies = [SharedResourceIdPatch::class]) -object ToolBarHookPatch : BytecodePatch( - setOf( - ToolBarButtonFingerprint, - ToolBarPatchFingerprint - ) -) { - private const val INTEGRATIONS_CLASS_DESCRIPTOR = - "$UTILS_PATH/ToolBarPatch;" - - private lateinit var toolbarMethod: MutableMethod - - override fun execute(context: BytecodeContext) { - - ToolBarButtonFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val replaceIndex = it.scanResult.patternScanResult!!.startIndex - val freeIndex = it.scanResult.patternScanResult!!.endIndex - 1 - - val replaceReference = getInstruction(replaceIndex).reference - val replaceRegister = - getInstruction(replaceIndex).registerC - val enumRegister = getInstruction(replaceIndex).registerD - val freeRegister = getInstruction(freeIndex).registerA - - val imageViewIndex = replaceIndex + 2 - val imageViewReference = - getInstruction(imageViewIndex).reference - - addInstructions( - replaceIndex + 1, """ - iget-object v$freeRegister, p0, $imageViewReference - invoke-static {v$enumRegister, v$freeRegister}, $INTEGRATIONS_CLASS_DESCRIPTOR->hookToolBar(Ljava/lang/Enum;Landroid/widget/ImageView;)V - invoke-interface {v$replaceRegister, v$enumRegister}, $replaceReference - """ - ) - removeInstruction(replaceIndex) - } - } - - toolbarMethod = ToolBarPatchFingerprint.resultOrThrow().mutableMethod - } - - internal fun hook( - descriptor: String - ) { - toolbarMethod.addInstructions( - 0, - "invoke-static {p0, p1}, $descriptor(Ljava/lang/String;Landroid/view/View;)V" - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/fingerprints/ToolBarButtonFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/fingerprints/ToolBarButtonFingerprint.kt deleted file mode 100644 index a053668c9..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/fingerprints/ToolBarButtonFingerprint.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.revanced.patches.youtube.utils.toolbar.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.MenuItemView -import app.revanced.util.fingerprint.LiteralValueFingerprint -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode - -internal object ToolBarButtonFingerprint : LiteralValueFingerprint( - returnType = "V", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("Landroid/view/MenuItem;"), - opcodes = listOf( - Opcode.INVOKE_INTERFACE, - Opcode.MOVE_RESULT, - Opcode.IGET_OBJECT, - Opcode.IGET_OBJECT, - Opcode.INVOKE_VIRTUAL - ), - literalSupplier = { MenuItemView }, -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/fingerprints/ToolBarPatchFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/fingerprints/ToolBarPatchFingerprint.kt deleted file mode 100644 index 9d0fa8d04..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/fingerprints/ToolBarPatchFingerprint.kt +++ /dev/null @@ -1,14 +0,0 @@ -package app.revanced.patches.youtube.utils.toolbar.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patches.youtube.utils.integrations.Constants.UTILS_PATH -import com.android.tools.smali.dexlib2.AccessFlags - -internal object ToolBarPatchFingerprint : MethodFingerprint( - accessFlags = AccessFlags.PRIVATE or AccessFlags.STATIC, - customFingerprint = { methodDef, _ -> - methodDef.definingClass == "$UTILS_PATH/ToolBarPatch;" - && methodDef.name == "hookToolBar" - } -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/trackingurlhook/TrackingUrlHookPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/trackingurlhook/TrackingUrlHookPatch.kt deleted file mode 100644 index 5012f0c09..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/trackingurlhook/TrackingUrlHookPatch.kt +++ /dev/null @@ -1,48 +0,0 @@ -package app.revanced.patches.youtube.utils.trackingurlhook - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patches.youtube.utils.trackingurlhook.fingerprints.TrackingUrlModelFingerprint -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstructionOrThrow -import app.revanced.util.resultOrThrow -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.reference.MethodReference - -object TrackingUrlHookPatch : BytecodePatch( - setOf(TrackingUrlModelFingerprint) -) { - private lateinit var trackingUrlMethod: MutableMethod - - override fun execute(context: BytecodeContext) { - trackingUrlMethod = TrackingUrlModelFingerprint.resultOrThrow().mutableMethod - } - - internal fun hookTrackingUrl( - descriptor: String - ) = trackingUrlMethod.apply { - val targetIndex = indexOfFirstInstructionOrThrow { - opcode == Opcode.INVOKE_STATIC && - getReference()?.name == "parse" - } + 1 - val targetRegister = getInstruction(targetIndex).registerA - - var smaliInstruction = "invoke-static {v$targetRegister}, $descriptor" - - if (!descriptor.endsWith("V")) { - smaliInstruction += """ - move-result-object v$targetRegister - - """.trimIndent() - } - - addInstructions( - targetIndex + 1, - smaliInstruction - ) - } -} diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/trackingurlhook/fingerprints/TrackingUrlModelFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/utils/trackingurlhook/fingerprints/TrackingUrlModelFingerprint.kt deleted file mode 100644 index 37a6897b7..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/trackingurlhook/fingerprints/TrackingUrlModelFingerprint.kt +++ /dev/null @@ -1,20 +0,0 @@ -package app.revanced.patches.youtube.utils.trackingurlhook.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode - -internal object TrackingUrlModelFingerprint : MethodFingerprint( - returnType = "Landroid/net/Uri;", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = emptyList(), - opcodes = listOf( - Opcode.IGET_OBJECT, - Opcode.INVOKE_STATIC, - Opcode.MOVE_RESULT_OBJECT, - ), - customFingerprint = { methodDef, _ -> - methodDef.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/player/TrackingUrlModel;" - } -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt deleted file mode 100644 index 8a5f61e0d..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt +++ /dev/null @@ -1,682 +0,0 @@ -package app.revanced.patches.youtube.video.information - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.InstructionExtensions.addInstruction -import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patcher.fingerprint.MethodFingerprintResult -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable -import app.revanced.patcher.util.smali.toInstructions -import app.revanced.patches.shared.fingerprints.MdxPlayerDirectorSetVideoStageFingerprint -import app.revanced.patches.shared.fingerprints.VideoLengthFingerprint -import app.revanced.patches.youtube.utils.PlayerResponseModelUtils.PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR -import app.revanced.patches.youtube.utils.PlayerResponseModelUtils.indexOfPlayerResponseModelInstruction -import app.revanced.patches.youtube.utils.fingerprints.VideoEndFingerprint -import app.revanced.patches.youtube.utils.integrations.Constants.SHARED_PATH -import app.revanced.patches.youtube.utils.playertype.PlayerTypeHookPatch -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch -import app.revanced.patches.youtube.video.information.fingerprints.ChannelIdFingerprint -import app.revanced.patches.youtube.video.information.fingerprints.ChannelNameFingerprint -import app.revanced.patches.youtube.video.information.fingerprints.OnPlaybackSpeedItemClickFingerprint -import app.revanced.patches.youtube.video.information.fingerprints.PlaybackInitializationFingerprint -import app.revanced.patches.youtube.video.information.fingerprints.PlaybackSpeedClassFingerprint -import app.revanced.patches.youtube.video.information.fingerprints.PlayerControllerSetTimeReferenceFingerprint -import app.revanced.patches.youtube.video.information.fingerprints.SeekRelativeFingerprint -import app.revanced.patches.youtube.video.information.fingerprints.VideoIdFingerprint -import app.revanced.patches.youtube.video.information.fingerprints.VideoIdFingerprintBackgroundPlay -import app.revanced.patches.youtube.video.information.fingerprints.VideoIdFingerprintShorts -import app.revanced.patches.youtube.video.information.fingerprints.VideoQualityListFingerprint -import app.revanced.patches.youtube.video.information.fingerprints.VideoQualityTextFingerprint -import app.revanced.patches.youtube.video.information.fingerprints.VideoTitleFingerprint -import app.revanced.patches.youtube.video.playerresponse.PlayerResponseMethodHookPatch -import app.revanced.patches.youtube.video.videoid.VideoIdPatch -import app.revanced.util.addStaticFieldToIntegration -import app.revanced.util.alsoResolve -import app.revanced.util.cloneMutable -import app.revanced.util.getReference -import app.revanced.util.getWalkerMethod -import app.revanced.util.indexOfFirstInstructionOrThrow -import app.revanced.util.indexOfFirstInstructionReversedOrThrow -import app.revanced.util.indexOfFirstWideLiteralInstructionValueOrThrow -import app.revanced.util.resultOrThrow -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction -import com.android.tools.smali.dexlib2.iface.reference.MethodReference -import com.android.tools.smali.dexlib2.immutable.ImmutableMethod -import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation -import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter -import com.android.tools.smali.dexlib2.util.MethodUtil - -@Patch( - description = "Hooks YouTube to get information about the current playing video.", - dependencies = [ - PlayerResponseMethodHookPatch::class, - PlayerTypeHookPatch::class, - SharedResourceIdPatch::class, - VideoIdPatch::class - ] -) -@Suppress("MemberVisibilityCanBePrivate") -object VideoInformationPatch : BytecodePatch( - setOf( - ChannelIdFingerprint, - ChannelNameFingerprint, - MdxPlayerDirectorSetVideoStageFingerprint, - OnPlaybackSpeedItemClickFingerprint, - PlaybackInitializationFingerprint, - PlaybackSpeedClassFingerprint, - PlayerControllerSetTimeReferenceFingerprint, - VideoEndFingerprint, - VideoIdFingerprint, - VideoIdFingerprintBackgroundPlay, - VideoIdFingerprintShorts, - VideoLengthFingerprint, - VideoQualityListFingerprint, - VideoQualityTextFingerprint, - VideoTitleFingerprint, - ) -) { - private const val INTEGRATIONS_CLASS_DESCRIPTOR = - "$SHARED_PATH/VideoInformation;" - - private const val REGISTER_PLAYER_RESPONSE_MODEL = 8 - - private const val REGISTER_CHANNEL_ID = 0 - private const val REGISTER_CHANNEL_NAME = 1 - private const val REGISTER_VIDEO_ID = 2 - private const val REGISTER_VIDEO_TITLE = 3 - private const val REGISTER_VIDEO_LENGTH = 4 - - @Suppress("unused") - private const val REGISTER_VIDEO_LENGTH_DUMMY = 5 - private const val REGISTER_VIDEO_IS_LIVE = 6 - - private lateinit var channelIdMethodCall: String - private lateinit var channelNameMethodCall: String - private lateinit var videoIdMethodCall: String - private lateinit var videoTitleMethodCall: String - private lateinit var videoLengthMethodCall: String - private lateinit var videoIsLiveMethodCall: String - - private lateinit var videoInformationMethod: MutableMethod - private lateinit var backgroundVideoInformationMethod: MutableMethod - private lateinit var shortsVideoInformationMethod: MutableMethod - - /** - * Used in [VideoEndFingerprint] and [MdxPlayerDirectorSetVideoStageFingerprint]. - * Since both classes are inherited from the same class, - * [VideoEndFingerprint] and [MdxPlayerDirectorSetVideoStageFingerprint] always have the same [seekSourceEnumType], [seekSourceMethodName] and [seekRelativeSourceMethodName]. - */ - private var seekSourceEnumType = "" - private var seekSourceMethodName = "" - private var seekRelativeSourceMethodName = "" - private var cloneSeekRelativeSourceMethod = false - - private lateinit var context: BytecodeContext - - private lateinit var playerConstructorMethod: MutableMethod - private var playerConstructorInsertIndex = -1 - - private lateinit var mdxConstructorMethod: MutableMethod - private var mdxConstructorInsertIndex = -1 - - private lateinit var videoTimeConstructorMethod: MutableMethod - private var videoTimeConstructorInsertIndex = 2 - - // Used by other patches. - internal lateinit var speedSelectionInsertMethod: MutableMethod - internal lateinit var videoEndMethod: MutableMethod - - private fun cloneSeekRelativeSourceMethod(fingerprintResult: MethodFingerprintResult) { - if (!cloneSeekRelativeSourceMethod) return - - val methods = fingerprintResult.mutableClass.methods - - methods.find { method -> - method.name == seekRelativeSourceMethodName - }?.apply { - methods.add( - cloneMutable( - returnType = "Z" - ).apply { - val lastIndex = implementation!!.instructions.lastIndex - - removeInstruction(lastIndex) - addInstructions( - lastIndex, """ - move-result p1 - return p1 - """ - ) - } - ) - } - } - - private fun addSeekInterfaceMethods( - result: MethodFingerprintResult, - seekMethodName: String, - methodName: String, - fieldMethodName: String, - fieldName: String - ) { - result.mutableMethod.apply { - result.mutableClass.methods.add( - ImmutableMethod( - definingClass, - fieldMethodName, - listOf(ImmutableMethodParameter("J", annotations, "time")), - "Z", - AccessFlags.PUBLIC or AccessFlags.FINAL, - annotations, - null, - ImmutableMethodImplementation( - 4, """ - # first enum (field a) is SEEK_SOURCE_UNKNOWN - sget-object v0, $seekSourceEnumType->a:$seekSourceEnumType - invoke-virtual {p0, p1, p2, v0}, $definingClass->$seekMethodName(J$seekSourceEnumType)Z - move-result p1 - return p1 - """.toInstructions(), - null, - null - ) - ).toMutable() - ) - - val smaliInstructions = - """ - if-eqz v0, :ignore - invoke-virtual {v0, p0, p1}, $definingClass->$fieldMethodName(J)Z - move-result v0 - return v0 - :ignore - const/4 v0, 0x0 - return v0 - """ - - context.addStaticFieldToIntegration( - INTEGRATIONS_CLASS_DESCRIPTOR, - methodName, - fieldName, - definingClass, - smaliInstructions - ) - } - } - - override fun execute(context: BytecodeContext) { - this.context = context - - VideoEndFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - playerConstructorMethod = - it.mutableClass.methods.first { method -> MethodUtil.isConstructor(method) } - - playerConstructorInsertIndex = - playerConstructorMethod.indexOfFirstInstructionOrThrow { - opcode == Opcode.INVOKE_DIRECT && getReference()?.name == "" - } + 1 - - // hook the player controller for use through integrations - onCreateHook(INTEGRATIONS_CLASS_DESCRIPTOR, "initialize") - - val seekRelativeMethod = SeekRelativeFingerprint.alsoResolve( - context, - VideoEndFingerprint - ).mutableMethod - - seekSourceEnumType = parameterTypes[1].toString() - seekSourceMethodName = name - seekRelativeSourceMethodName = seekRelativeMethod.name - cloneSeekRelativeSourceMethod = seekRelativeMethod.returnType == "V" - cloneSeekRelativeSourceMethod(it) - - // Create integrations interface methods. - addSeekInterfaceMethods( - it, - seekSourceMethodName, - "overrideVideoTime", - "seekTo", - "videoInformationClass" - ) - addSeekInterfaceMethods( - it, - seekRelativeSourceMethodName, - "overrideVideoTimeRelative", - "seekToRelative", - "videoInformationClass" - ) - - val literalIndex = indexOfFirstWideLiteralInstructionValueOrThrow(45368273) - val walkerIndex = - indexOfFirstInstructionReversedOrThrow( - literalIndex, - Opcode.INVOKE_VIRTUAL_RANGE - ) - - videoEndMethod = - getWalkerMethod(context, walkerIndex) - } - } - - MdxPlayerDirectorSetVideoStageFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - mdxConstructorMethod = - it.mutableClass.methods.first { method -> MethodUtil.isConstructor(method) } - - mdxConstructorInsertIndex = mdxConstructorMethod.indexOfFirstInstructionOrThrow { - opcode == Opcode.INVOKE_DIRECT && getReference()?.name == "" - } + 1 - - // hook the MDX director for use through integrations - onCreateHookMdx(INTEGRATIONS_CLASS_DESCRIPTOR, "initializeMdx") - - cloneSeekRelativeSourceMethod(it) - - // Create integrations interface methods. - addSeekInterfaceMethods( - it, - seekSourceMethodName, - "overrideMDXVideoTime", - "seekTo", - "videoInformationMDXClass" - ) - addSeekInterfaceMethods( - it, - seekRelativeSourceMethodName, - "overrideMDXVideoTimeRelative", - "seekToRelative", - "videoInformationMDXClass" - ) - } - } - - /** - * Set current video information - */ - channelIdMethodCall = - ChannelIdFingerprint.getPlayerResponseInstruction("Ljava/lang/String;") - channelNameMethodCall = - ChannelNameFingerprint.getPlayerResponseInstruction("Ljava/lang/String;") - videoIdMethodCall = VideoIdFingerprint.getPlayerResponseInstruction("Ljava/lang/String;") - videoTitleMethodCall = - VideoTitleFingerprint.getPlayerResponseInstruction("Ljava/lang/String;") - videoLengthMethodCall = VideoLengthFingerprint.getPlayerResponseInstruction("J") - videoIsLiveMethodCall = ChannelIdFingerprint.getPlayerResponseInstruction("Z") - - PlaybackInitializationFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val targetIndex = - PlaybackInitializationFingerprint.indexOfPlayerResponseModelInstruction(this) + 1 - val targetRegister = getInstruction(targetIndex).registerA - - addInstruction( - targetIndex + 1, - "invoke-direct {p0, v$targetRegister}, $definingClass->setVideoInformation($PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR)V" - ) - - videoInformationMethod = getVideoInformationMethod() - it.mutableClass.methods.add(videoInformationMethod) - - hook("$INTEGRATIONS_CLASS_DESCRIPTOR->setVideoInformation(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") - } - } - - VideoIdFingerprintBackgroundPlay.resultOrThrow().let { - it.mutableMethod.apply { - val targetIndex = indexOfPlayerResponseModelInstruction(this) - val targetRegister = getInstruction(targetIndex).registerC - - addInstruction( - targetIndex, - "invoke-direct {p0, v$targetRegister}, $definingClass->setVideoInformation($PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR)V" - ) - - backgroundVideoInformationMethod = getVideoInformationMethod() - it.mutableClass.methods.add(backgroundVideoInformationMethod) - } - } - - VideoIdFingerprintShorts.resultOrThrow().let { - it.mutableMethod.apply { - val targetIndex = indexOfPlayerResponseModelInstruction(this) - val targetRegister = getInstruction(targetIndex).registerC - - addInstruction( - targetIndex, - "invoke-direct {p0, v$targetRegister}, $definingClass->setVideoInformation($PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR)V" - ) - - shortsVideoInformationMethod = getVideoInformationMethod() - it.mutableClass.methods.add(shortsVideoInformationMethod) - } - } - - /** - * Set current video time method - */ - PlayerControllerSetTimeReferenceFingerprint.resultOrThrow().let { - videoTimeConstructorMethod = - it.getWalkerMethod(context, it.scanResult.patternScanResult!!.startIndex) - } - - /** - * Set current video time - */ - videoTimeHook(INTEGRATIONS_CLASS_DESCRIPTOR, "setVideoTime") - - /** - * Set current video id - */ - VideoIdPatch.hookVideoId("$INTEGRATIONS_CLASS_DESCRIPTOR->setVideoId(Ljava/lang/String;)V") - VideoIdPatch.hookPlayerResponseVideoId( - "$INTEGRATIONS_CLASS_DESCRIPTOR->setPlayerResponseVideoId(Ljava/lang/String;Z)V" - ) - // Call before any other video id hooks, - // so they can use VideoInformation and check if the video id is for a Short. - PlayerResponseMethodHookPatch += PlayerResponseMethodHookPatch.Hook.PlayerParameterBeforeVideoId( - "$INTEGRATIONS_CLASS_DESCRIPTOR->newPlayerResponseParameter(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)Ljava/lang/String;" - ) - - /** - * Hook current playback speed - */ - OnPlaybackSpeedItemClickFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - speedSelectionInsertMethod = this - val speedSelectionValueInstructionIndex = - indexOfFirstInstructionOrThrow(Opcode.IGET) - - val setPlaybackSpeedContainerClassFieldIndex = - indexOfFirstInstructionReversedOrThrow( - speedSelectionValueInstructionIndex, - Opcode.IGET_OBJECT - ) - val setPlaybackSpeedContainerClassFieldReference = - getInstruction(setPlaybackSpeedContainerClassFieldIndex).reference.toString() - - val setPlaybackSpeedClassFieldReference = - getInstruction(speedSelectionValueInstructionIndex + 1).reference.toString() - val setPlaybackSpeedMethodReference = - getInstruction(speedSelectionValueInstructionIndex + 2).reference.toString() - - // add override playback speed method - it.mutableClass.methods.add( - ImmutableMethod( - definingClass, - "overridePlaybackSpeed", - listOf(ImmutableMethodParameter("F", annotations, null)), - "V", - AccessFlags.PUBLIC or AccessFlags.PUBLIC, - annotations, - null, - ImmutableMethodImplementation( - 4, """ - const/4 v0, 0x0 - cmpg-float v0, v3, v0 - if-lez v0, :ignore - - # Get the container class field. - iget-object v0, v2, $setPlaybackSpeedContainerClassFieldReference - - # Get the field from its class. - iget-object v1, v0, $setPlaybackSpeedClassFieldReference - - # Invoke setPlaybackSpeed on that class. - invoke-virtual {v1, v3}, $setPlaybackSpeedMethodReference - - :ignore - return-void - """.toInstructions(), null, null - ) - ).toMutable() - ) - - // set current playback speed - val walkerMethod = getWalkerMethod(context, speedSelectionValueInstructionIndex + 2) - walkerMethod.apply { - addInstruction( - this.implementation!!.instructions.size - 1, - "invoke-static { p1 }, $INTEGRATIONS_CLASS_DESCRIPTOR->setPlaybackSpeed(F)V" - ) - } - } - } - - PlaybackSpeedClassFingerprint.resultOrThrow().let { result -> - result.mutableMethod.apply { - val index = result.scanResult.patternScanResult!!.endIndex - val register = getInstruction(index).registerA - val playbackSpeedClass = this.returnType - - // set playback speed class - replaceInstruction( - index, - "sput-object v$register, $INTEGRATIONS_CLASS_DESCRIPTOR->playbackSpeedClass:$playbackSpeedClass" - ) - addInstruction( - index + 1, - "return-object v$register" - ) - - val smaliInstructions = - """ - if-eqz v0, :ignore - invoke-virtual {v0, p0}, $playbackSpeedClass->overridePlaybackSpeed(F)V - :ignore - return-void - """ - - context.addStaticFieldToIntegration( - INTEGRATIONS_CLASS_DESCRIPTOR, - "overridePlaybackSpeed", - "playbackSpeedClass", - playbackSpeedClass, - smaliInstructions, - false - ) - } - } - - /** - * Hook current video quality - */ - VideoQualityListFingerprint.resultOrThrow().let { - val overrideMethod = - it.mutableClass.methods.find { method -> method.parameterTypes.first() == "I" } - - val videoQualityClass = it.method.definingClass - val videoQualityMethodName = overrideMethod?.name - ?: throw PatchException("Failed to find hook method") - - // set video quality array - it.mutableMethod.apply { - val listIndex = it.scanResult.patternScanResult!!.startIndex - val listRegister = getInstruction(listIndex).registerD - - addInstruction( - listIndex, - "invoke-static {v$listRegister}, $INTEGRATIONS_CLASS_DESCRIPTOR->setVideoQualityList([Ljava/lang/Object;)V" - ) - } - - val smaliInstructions = - """ - if-eqz v0, :ignore - invoke-virtual {v0, p0}, $videoQualityClass->$videoQualityMethodName(I)V - :ignore - return-void - """ - - context.addStaticFieldToIntegration( - INTEGRATIONS_CLASS_DESCRIPTOR, - "overrideVideoQuality", - "videoQualityClass", - videoQualityClass, - smaliInstructions - ) - } - - // set current video quality - VideoQualityTextFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val textIndex = it.scanResult.patternScanResult!!.endIndex - val textRegister = getInstruction(textIndex).registerA - - addInstruction( - textIndex + 1, - "invoke-static {v$textRegister}, $INTEGRATIONS_CLASS_DESCRIPTOR->setVideoQuality(Ljava/lang/String;)V" - ) - } - } - } - - /** - * Hook the player controller. Called when a video is opened or the current video is changed. - * - * Note: This hook is called very early and is called before the video id, video time, video length, - * and many other data fields are set. - * - * @param targetMethodClass The descriptor for the class to invoke when the player controller is created. - * @param targetMethodName The name of the static method to invoke when the player controller is created. - */ - internal fun onCreateHook(targetMethodClass: String, targetMethodName: String) = - playerConstructorMethod.addInstruction( - playerConstructorInsertIndex++, - "invoke-static { }, $targetMethodClass->$targetMethodName()V" - ) - - /** - * Hook the MDX player director. Called when playing videos while casting to a big screen device. - * - * @param targetMethodClass The descriptor for the class to invoke when the player controller is created. - * @param targetMethodName The name of the static method to invoke when the player controller is created. - */ - internal fun onCreateHookMdx(targetMethodClass: String, targetMethodName: String) = - mdxConstructorMethod.addInstruction( - mdxConstructorInsertIndex++, - "invoke-static { }, $targetMethodClass->$targetMethodName()V" - ) - - /** - * Hook the video time. - * The hook is usually called once per second. - * - * @param targetMethodClass The descriptor for the static method to invoke when the player controller is created. - * @param targetMethodName The name of the static method to invoke when the player controller is created. - */ - internal fun videoTimeHook(targetMethodClass: String, targetMethodName: String) = - videoTimeConstructorMethod.addInstruction( - videoTimeConstructorInsertIndex++, - "invoke-static { p1, p2 }, $targetMethodClass->$targetMethodName(J)V" - ) - - private fun MethodFingerprint.getPlayerResponseInstruction(returnType: String): String { - resultOrThrow().mutableMethod.apply { - val targetReference = getInstruction( - indexOfFirstInstructionOrThrow { - val reference = getReference() - opcode == Opcode.INVOKE_INTERFACE && - reference?.definingClass == PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR && - reference.returnType == returnType - } - ).reference - - return "invoke-interface {v$REGISTER_PLAYER_RESPONSE_MODEL}, $targetReference" - } - } - - private fun MutableMethod.getVideoInformationMethod(): MutableMethod = - ImmutableMethod( - definingClass, - "setVideoInformation", - listOf( - ImmutableMethodParameter( - PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR, - annotations, - null - ) - ), - "V", - AccessFlags.PRIVATE or AccessFlags.FINAL, - annotations, - null, - ImmutableMethodImplementation( - REGISTER_PLAYER_RESPONSE_MODEL + 1, """ - $channelIdMethodCall - move-result-object v$REGISTER_CHANNEL_ID - $channelNameMethodCall - move-result-object v$REGISTER_CHANNEL_NAME - $videoIdMethodCall - move-result-object v$REGISTER_VIDEO_ID - $videoTitleMethodCall - move-result-object v$REGISTER_VIDEO_TITLE - $videoLengthMethodCall - move-result-wide v$REGISTER_VIDEO_LENGTH - $videoIsLiveMethodCall - move-result v$REGISTER_VIDEO_IS_LIVE - return-void - """.toInstructions(), - null, - null - ) - ).toMutable() - - private fun MutableMethod.insert(insertIndex: Int, register: String, descriptor: String) = - addInstruction(insertIndex, "invoke-static/range { $register }, $descriptor") - - /** - * This method is invoked on both regular videos and Shorts. - */ - internal fun hook(descriptor: String) = - videoInformationMethod.apply { - val index = implementation!!.instructions.size - 1 - - insert( - index, - "v$REGISTER_CHANNEL_ID .. v$REGISTER_VIDEO_IS_LIVE", - descriptor - ) - } - - /** - * This method is invoked only in regular videos. - */ - internal fun hookBackgroundPlay(descriptor: String) = - backgroundVideoInformationMethod.apply { - val index = implementation!!.instructions.size - 1 - - insert( - index, - "v$REGISTER_CHANNEL_ID .. v$REGISTER_VIDEO_IS_LIVE", - descriptor - ) - } - - /** - * This method is invoked only in shorts videos. - */ - internal fun hookShorts(descriptor: String) = - shortsVideoInformationMethod.apply { - val index = implementation!!.instructions.size - 1 - - insert( - index, - "v$REGISTER_CHANNEL_ID .. v$REGISTER_VIDEO_IS_LIVE", - descriptor - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/ChannelIdFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/ChannelIdFingerprint.kt deleted file mode 100644 index 928aaf4b3..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/ChannelIdFingerprint.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.revanced.patches.youtube.video.information.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags - -internal object ChannelIdFingerprint : MethodFingerprint( - returnType = "V", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("Ljava/lang/Object;"), - strings = listOf("com.google.android.apps.youtube.mdx.watch.LAST_MEALBAR_PROMOTED_LIVE_FEED_CHANNELS") -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/ChannelNameFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/ChannelNameFingerprint.kt deleted file mode 100644 index 7f2f928cb..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/ChannelNameFingerprint.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.revanced.patches.youtube.video.information.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags - -internal object ChannelNameFingerprint : MethodFingerprint( - returnType = "V", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("L"), - strings = listOf( - "setMetadata may only be called once", - "Person", - ) -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/OnPlaybackSpeedItemClickFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/OnPlaybackSpeedItemClickFingerprint.kt deleted file mode 100644 index 3655b5282..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/OnPlaybackSpeedItemClickFingerprint.kt +++ /dev/null @@ -1,23 +0,0 @@ -package app.revanced.patches.youtube.video.information.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patches.youtube.utils.PlayerResponseModelUtils.PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstruction -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.reference.FieldReference - -internal object OnPlaybackSpeedItemClickFingerprint : MethodFingerprint( - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - returnType = "V", - parameters = listOf("Landroid/widget/AdapterView;", "Landroid/view/View;", "I", "J"), - customFingerprint = { methodDef, _ -> - methodDef.name == "onItemClick" && - methodDef.indexOfFirstInstruction { - opcode == Opcode.IGET_OBJECT && - getReference()?.type == PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR - } >= 0 - } -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/PlaybackInitializationFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/PlaybackInitializationFingerprint.kt deleted file mode 100644 index d5bbd3cd6..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/PlaybackInitializationFingerprint.kt +++ /dev/null @@ -1,28 +0,0 @@ -package app.revanced.patches.youtube.video.information.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patches.youtube.utils.PlayerResponseModelUtils.PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR -import app.revanced.patches.youtube.video.information.fingerprints.PlaybackInitializationFingerprint.indexOfPlayerResponseModelInstruction -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstruction -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.Method -import com.android.tools.smali.dexlib2.iface.reference.MethodReference - -internal object PlaybackInitializationFingerprint : MethodFingerprint( - returnType = "V", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = emptyList(), - strings = listOf("play() called when the player wasn\'t loaded."), - customFingerprint = { methodDef, _ -> - indexOfPlayerResponseModelInstruction(methodDef) >= 0 - } -) { - fun indexOfPlayerResponseModelInstruction(methodDef: Method) = - methodDef.indexOfFirstInstruction { - opcode == Opcode.INVOKE_DIRECT && - getReference()?.returnType == PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR - } -} diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/PlaybackSpeedClassFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/PlaybackSpeedClassFingerprint.kt deleted file mode 100644 index 2a72d9ebb..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/PlaybackSpeedClassFingerprint.kt +++ /dev/null @@ -1,14 +0,0 @@ -package app.revanced.patches.youtube.video.information.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode - -internal object PlaybackSpeedClassFingerprint : MethodFingerprint( - returnType = "L", - accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, - parameters = listOf("L"), - opcodes = listOf(Opcode.RETURN_OBJECT), - strings = listOf("PLAYBACK_RATE_MENU_BOTTOM_SHEET_FRAGMENT") -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/PlayerControllerSetTimeReferenceFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/PlayerControllerSetTimeReferenceFingerprint.kt deleted file mode 100644 index 48d56206c..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/PlayerControllerSetTimeReferenceFingerprint.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.revanced.patches.youtube.video.information.fingerprints - -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.Opcode - -object PlayerControllerSetTimeReferenceFingerprint : MethodFingerprint( - opcodes = listOf( - Opcode.INVOKE_DIRECT_RANGE, - Opcode.IGET_OBJECT - ), - strings = listOf("Media progress reported outside media playback: ") -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/SeekRelativeFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/SeekRelativeFingerprint.kt deleted file mode 100644 index 90146b3c7..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/SeekRelativeFingerprint.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.revanced.patches.youtube.video.information.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patches.youtube.utils.fingerprints.VideoEndFingerprint -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode - -/** - * Resolves using class found in [VideoEndFingerprint]. - */ -internal object SeekRelativeFingerprint : MethodFingerprint( - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - // returnType = "Z", ~ YouTube 19.39.39 - // returnType = "V", YouTube 19.40.xx ~ - parameters = listOf("J", "L"), - opcodes = listOf( - Opcode.ADD_LONG_2ADDR, - Opcode.INVOKE_VIRTUAL, - ) -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoIdFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoIdFingerprint.kt deleted file mode 100644 index c69648f99..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoIdFingerprint.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.revanced.patches.youtube.video.information.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags - -internal object VideoIdFingerprint : MethodFingerprint( - returnType = "V", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = emptyList(), - strings = listOf("Failed to download video (IllegalStateException): %s") -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoIdFingerprintBackgroundPlay.kt b/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoIdFingerprintBackgroundPlay.kt deleted file mode 100644 index 750929252..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoIdFingerprintBackgroundPlay.kt +++ /dev/null @@ -1,29 +0,0 @@ -package app.revanced.patches.youtube.video.information.fingerprints - -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patches.youtube.utils.PlayerResponseModelUtils.indexOfPlayerResponseModelInstruction -import com.android.tools.smali.dexlib2.Opcode - -/** - * Renamed from VideoIdWithoutShortsFingerprint - */ -internal object VideoIdFingerprintBackgroundPlay : MethodFingerprint( - returnType = "V", - parameters = listOf("L"), - opcodes = listOf( - Opcode.IF_EQZ, - Opcode.INVOKE_INTERFACE, - Opcode.MOVE_RESULT_OBJECT, - Opcode.IPUT_OBJECT, - Opcode.MONITOR_EXIT, - Opcode.RETURN_VOID, - Opcode.MONITOR_EXIT, - Opcode.RETURN_VOID - ), - customFingerprint = { methodDef, classDef -> - methodDef.name == "l" && - classDef.methods.count() == 17 && - methodDef.implementation != null && - indexOfPlayerResponseModelInstruction(methodDef) >= 0 - } -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoIdFingerprintShorts.kt b/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoIdFingerprintShorts.kt deleted file mode 100644 index 0e419ab53..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoIdFingerprintShorts.kt +++ /dev/null @@ -1,31 +0,0 @@ -package app.revanced.patches.youtube.video.information.fingerprints - -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patches.youtube.utils.PlayerResponseModelUtils.PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR -import app.revanced.util.containsWideLiteralInstructionValue -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstruction -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.reference.FieldReference - -/** - * This fingerprint is compatible with all versions of YouTube starting from v18.29.38 to supported versions. - * This method is invoked only in Shorts. - * Accurate video information is invoked even when the user moves Shorts upward or downward. - */ -internal object VideoIdFingerprintShorts : MethodFingerprint( - returnType = "V", - parameters = listOf(PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR), - opcodes = listOf( - Opcode.INVOKE_INTERFACE, - Opcode.MOVE_RESULT_OBJECT - ), - customFingerprint = custom@{ methodDef, _ -> - if (methodDef.containsWideLiteralInstructionValue(45365621)) - return@custom true - - methodDef.indexOfFirstInstruction { - getReference()?.name == "reelWatchEndpoint" - } >= 0 - } -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoQualityListFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoQualityListFingerprint.kt deleted file mode 100644 index 21fcd8e6d..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoQualityListFingerprint.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.revanced.patches.youtube.video.information.fingerprints - -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.QualityAuto -import app.revanced.util.fingerprint.LiteralValueFingerprint -import com.android.tools.smali.dexlib2.Opcode - -internal object VideoQualityListFingerprint : LiteralValueFingerprint( - returnType = "V", - parameters = listOf("L"), - opcodes = listOf( - Opcode.INVOKE_INTERFACE, - Opcode.RETURN_VOID - ), - literalSupplier = { QualityAuto }, -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoQualityTextFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoQualityTextFingerprint.kt deleted file mode 100644 index 12f65ccf2..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoQualityTextFingerprint.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.revanced.patches.youtube.video.information.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode - -internal object VideoQualityTextFingerprint : MethodFingerprint( - returnType = "V", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("[L", "I", "Z"), - opcodes = listOf( - Opcode.IF_GE, - Opcode.AGET_OBJECT, - Opcode.IGET_OBJECT - ), - strings = listOf("menu_item_video_quality") -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoTitleFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoTitleFingerprint.kt deleted file mode 100644 index ff8ec7ef4..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/information/fingerprints/VideoTitleFingerprint.kt +++ /dev/null @@ -1,13 +0,0 @@ -package app.revanced.patches.youtube.video.information.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.NotificationBigPictureIconWidth -import app.revanced.util.fingerprint.LiteralValueFingerprint -import com.android.tools.smali.dexlib2.AccessFlags - -internal object VideoTitleFingerprint : LiteralValueFingerprint( - returnType = "V", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = emptyList(), - literalSupplier = { NotificationBigPictureIconWidth }, -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/playback/CustomPlaybackSpeedPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/video/playback/CustomPlaybackSpeedPatch.kt deleted file mode 100644 index c99deb1ab..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playback/CustomPlaybackSpeedPatch.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.revanced.patches.youtube.video.playback - -import app.revanced.patches.shared.customspeed.BaseCustomPlaybackSpeedPatch -import app.revanced.patches.youtube.utils.integrations.Constants.VIDEO_PATH - -object CustomPlaybackSpeedPatch : BaseCustomPlaybackSpeedPatch( - "$VIDEO_PATH/CustomPlaybackSpeedPatch;", - 8.0f -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/playback/VideoPlaybackPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/video/playback/VideoPlaybackPatch.kt deleted file mode 100644 index 6a17f77c8..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playback/VideoPlaybackPatch.kt +++ /dev/null @@ -1,360 +0,0 @@ -package app.revanced.patches.youtube.video.playback - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.InstructionExtensions.addInstruction -import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.util.smali.ExternalLabel -import app.revanced.patches.shared.litho.LithoFilterPatch -import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE -import app.revanced.patches.youtube.utils.fingerprints.QualityMenuViewInflateFingerprint -import app.revanced.patches.youtube.utils.fingerprints.VideoEndFingerprint -import app.revanced.patches.youtube.utils.fix.shortsplayback.ShortsPlaybackPatch -import app.revanced.patches.youtube.utils.flyoutmenu.FlyoutMenuHookPatch -import app.revanced.patches.youtube.utils.integrations.Constants.COMPONENTS_PATH -import app.revanced.patches.youtube.utils.integrations.Constants.PATCH_STATUS_CLASS_DESCRIPTOR -import app.revanced.patches.youtube.utils.integrations.Constants.VIDEO_PATH -import app.revanced.patches.youtube.utils.playertype.PlayerTypeHookPatch -import app.revanced.patches.youtube.utils.recyclerview.BottomSheetRecyclerViewPatch -import app.revanced.patches.youtube.utils.settings.SettingsPatch -import app.revanced.patches.youtube.video.information.VideoInformationPatch -import app.revanced.patches.youtube.video.information.VideoInformationPatch.speedSelectionInsertMethod -import app.revanced.patches.youtube.video.playback.fingerprints.AV1CodecFingerprint -import app.revanced.patches.youtube.video.playback.fingerprints.ByteBufferArrayFingerprint -import app.revanced.patches.youtube.video.playback.fingerprints.ByteBufferArrayParentFingerprint -import app.revanced.patches.youtube.video.playback.fingerprints.DeviceDimensionsModelToStringFingerprint -import app.revanced.patches.youtube.video.playback.fingerprints.HDRCapabilityFingerprint -import app.revanced.patches.youtube.video.playback.fingerprints.PlaybackSpeedChangedFromRecyclerViewFingerprint -import app.revanced.patches.youtube.video.playback.fingerprints.PlaybackSpeedInitializeFingerprint -import app.revanced.patches.youtube.video.playback.fingerprints.QualityChangedFromRecyclerViewFingerprint -import app.revanced.patches.youtube.video.playback.fingerprints.QualitySetterFingerprint -import app.revanced.patches.youtube.video.playback.fingerprints.VP9CapabilityFingerprint -import app.revanced.patches.youtube.video.videoid.VideoIdPatch -import app.revanced.util.getReference -import app.revanced.util.getWalkerMethod -import app.revanced.util.indexOfFirstInstructionOrThrow -import app.revanced.util.indexOfFirstStringInstructionOrThrow -import app.revanced.util.patch.BaseBytecodePatch -import app.revanced.util.resultOrThrow -import app.revanced.util.updatePatchStatus -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction -import com.android.tools.smali.dexlib2.iface.reference.FieldReference -import com.android.tools.smali.dexlib2.iface.reference.MethodReference -import com.android.tools.smali.dexlib2.util.MethodUtil - -@Suppress("unused") -object VideoPlaybackPatch : BaseBytecodePatch( - name = "Video playback", - description = "Adds options to customize settings related to video playback, " + - "such as default video quality and playback speed.", - dependencies = setOf( - BottomSheetRecyclerViewPatch::class, - CustomPlaybackSpeedPatch::class, - FlyoutMenuHookPatch::class, - LithoFilterPatch::class, - PlayerTypeHookPatch::class, - SettingsPatch::class, - ShortsPlaybackPatch::class, - VideoIdPatch::class, - VideoInformationPatch::class - ), - compatiblePackages = COMPATIBLE_PACKAGE, - fingerprints = setOf( - AV1CodecFingerprint, - ByteBufferArrayParentFingerprint, - DeviceDimensionsModelToStringFingerprint, - HDRCapabilityFingerprint, - PlaybackSpeedChangedFromRecyclerViewFingerprint, - QualityChangedFromRecyclerViewFingerprint, - QualityMenuViewInflateFingerprint, - QualitySetterFingerprint, - VideoEndFingerprint, - VP9CapabilityFingerprint - ) -) { - private const val PLAYBACK_SPEED_MENU_FILTER_CLASS_DESCRIPTOR = - "$COMPONENTS_PATH/PlaybackSpeedMenuFilter;" - private const val VIDEO_QUALITY_MENU_FILTER_CLASS_DESCRIPTOR = - "$COMPONENTS_PATH/VideoQualityMenuFilter;" - private const val INTEGRATIONS_AV1_CODEC_CLASS_DESCRIPTOR = - "$VIDEO_PATH/AV1CodecPatch;" - private const val INTEGRATIONS_VP9_CODEC_CLASS_DESCRIPTOR = - "$VIDEO_PATH/VP9CodecPatch;" - private const val INTEGRATIONS_CUSTOM_PLAYBACK_SPEED_CLASS_DESCRIPTOR = - "$VIDEO_PATH/CustomPlaybackSpeedPatch;" - private const val INTEGRATIONS_HDR_VIDEO_CLASS_DESCRIPTOR = - "$VIDEO_PATH/HDRVideoPatch;" - private const val INTEGRATIONS_PLAYBACK_SPEED_CLASS_DESCRIPTOR = - "$VIDEO_PATH/PlaybackSpeedPatch;" - private const val INTEGRATIONS_RELOAD_VIDEO_CLASS_DESCRIPTOR = - "$VIDEO_PATH/ReloadVideoPatch;" - private const val INTEGRATIONS_RESTORE_OLD_VIDEO_QUALITY_MENU_CLASS_DESCRIPTOR = - "$VIDEO_PATH/RestoreOldVideoQualityMenuPatch;" - private const val INTEGRATIONS_SPOOF_DEVICE_DIMENSIONS_CLASS_DESCRIPTOR = - "$VIDEO_PATH/SpoofDeviceDimensionsPatch;" - private const val INTEGRATIONS_VIDEO_QUALITY_CLASS_DESCRIPTOR = - "$VIDEO_PATH/VideoQualityPatch;" - - override fun execute(context: BytecodeContext) { - - // region patch for custom playback speed - - BottomSheetRecyclerViewPatch.injectCall("$INTEGRATIONS_CUSTOM_PLAYBACK_SPEED_CLASS_DESCRIPTOR->onFlyoutMenuCreate(Landroid/support/v7/widget/RecyclerView;)V") - LithoFilterPatch.addFilter(PLAYBACK_SPEED_MENU_FILTER_CLASS_DESCRIPTOR) - - // endregion - - // region patch for disable HDR video - - HDRCapabilityFingerprint.resultOrThrow().mutableMethod.apply { - val stringIndex = - indexOfFirstStringInstructionOrThrow("av1_profile_main_10_hdr_10_plus_supported") - val walkerIndex = indexOfFirstInstructionOrThrow(stringIndex) { - val reference = getReference() - reference?.parameterTypes == listOf("I", "Landroid/view/Display;") - && reference.returnType == "Z" - } - - val walkerMethod = getWalkerMethod(context, walkerIndex) - walkerMethod.apply { - addInstructionsWithLabels( - 0, """ - invoke-static {}, $INTEGRATIONS_HDR_VIDEO_CLASS_DESCRIPTOR->disableHDRVideo()Z - move-result v0 - if-nez v0, :default - return v0 - """, ExternalLabel("default", getInstruction(0)) - ) - } - } - - // endregion - - // region patch for default playback speed - - PlaybackSpeedChangedFromRecyclerViewFingerprint.resolve( - context, - QualityChangedFromRecyclerViewFingerprint.resultOrThrow().classDef - ) - - val newMethod = - PlaybackSpeedChangedFromRecyclerViewFingerprint.resultOrThrow().mutableMethod - - arrayOf( - newMethod, - speedSelectionInsertMethod - ).forEach { - it.apply { - val speedSelectionValueInstructionIndex = - indexOfFirstInstructionOrThrow(Opcode.IGET) - val speedSelectionValueRegister = - getInstruction(speedSelectionValueInstructionIndex).registerA - - addInstruction( - speedSelectionValueInstructionIndex + 1, - "invoke-static {v$speedSelectionValueRegister}, " + - "$INTEGRATIONS_PLAYBACK_SPEED_CLASS_DESCRIPTOR->userSelectedPlaybackSpeed(F)V" - ) - } - } - - PlaybackSpeedInitializeFingerprint.resolve( - context, - VideoEndFingerprint.resultOrThrow().classDef - ) - PlaybackSpeedInitializeFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val insertIndex = it.scanResult.patternScanResult!!.endIndex - val insertRegister = getInstruction(insertIndex).registerA - - addInstructions( - insertIndex, """ - invoke-static {v$insertRegister}, $INTEGRATIONS_PLAYBACK_SPEED_CLASS_DESCRIPTOR->getPlaybackSpeedInShorts(F)F - move-result v$insertRegister - """ - ) - } - } - - VideoInformationPatch.hookBackgroundPlay("$INTEGRATIONS_PLAYBACK_SPEED_CLASS_DESCRIPTOR->newVideoStarted(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") - VideoIdPatch.hookPlayerResponseVideoId("$INTEGRATIONS_PLAYBACK_SPEED_CLASS_DESCRIPTOR->fetchPlaylistData(Ljava/lang/String;Z)V") - - context.updatePatchStatus(PATCH_STATUS_CLASS_DESCRIPTOR, "RememberPlaybackSpeed") - - // endregion - - // region patch for default video quality - - QualityChangedFromRecyclerViewFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val index = it.scanResult.patternScanResult!!.startIndex - - addInstruction( - index + 1, - "invoke-static {}, $INTEGRATIONS_VIDEO_QUALITY_CLASS_DESCRIPTOR->userSelectedVideoQuality()V" - ) - - } - } - - QualitySetterFingerprint.resultOrThrow().let { - val onItemClickMethod = - it.mutableClass.methods.find { method -> method.name == "onItemClick" } - - onItemClickMethod?.apply { - addInstruction( - 0, - "invoke-static {}, $INTEGRATIONS_VIDEO_QUALITY_CLASS_DESCRIPTOR->userSelectedVideoQuality()V" - ) - } ?: throw PatchException("Failed to find onItemClick method") - } - - VideoInformationPatch.hookBackgroundPlay("$INTEGRATIONS_RELOAD_VIDEO_CLASS_DESCRIPTOR->newVideoStarted(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") - VideoInformationPatch.hook("$INTEGRATIONS_VIDEO_QUALITY_CLASS_DESCRIPTOR->newVideoStarted(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JZ)V") - VideoInformationPatch.onCreateHook( - INTEGRATIONS_VIDEO_QUALITY_CLASS_DESCRIPTOR, - "newVideoStarted" - ) - - // endregion - - // region patch for restore old video quality menu - - val videoQualityClass = QualitySetterFingerprint.resultOrThrow().mutableMethod.definingClass - - QualityMenuViewInflateFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val insertIndex = indexOfFirstInstructionOrThrow(Opcode.CHECK_CAST) - val insertRegister = getInstruction(insertIndex).registerA - - addInstruction( - insertIndex + 1, - "invoke-static { v$insertRegister }, " + - "$INTEGRATIONS_RESTORE_OLD_VIDEO_QUALITY_MENU_CLASS_DESCRIPTOR->restoreOldVideoQualityMenu(Landroid/widget/ListView;)V" - ) - } - val onItemClickMethod = - it.mutableClass.methods.find { method -> method.name == "onItemClick" } - - onItemClickMethod?.apply { - val insertIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_OBJECT) - val insertRegister = getInstruction(insertIndex).registerA - - val jumpIndex = indexOfFirstInstructionOrThrow { - opcode == Opcode.IGET_OBJECT - && this.getReference()?.type == videoQualityClass - } - - addInstructionsWithLabels( - insertIndex, """ - invoke-static {}, $INTEGRATIONS_RESTORE_OLD_VIDEO_QUALITY_MENU_CLASS_DESCRIPTOR->restoreOldVideoQualityMenu()Z - move-result v$insertRegister - if-nez v$insertRegister, :show - """, ExternalLabel("show", getInstruction(jumpIndex)) - ) - } ?: throw PatchException("Failed to find onItemClick method") - } - - BottomSheetRecyclerViewPatch.injectCall("$INTEGRATIONS_RESTORE_OLD_VIDEO_QUALITY_MENU_CLASS_DESCRIPTOR->onFlyoutMenuCreate(Landroid/support/v7/widget/RecyclerView;)V") - LithoFilterPatch.addFilter(VIDEO_QUALITY_MENU_FILTER_CLASS_DESCRIPTOR) - - // endregion - - // region patch for spoof device dimensions - - DeviceDimensionsModelToStringFingerprint.resultOrThrow().let { result -> - result.mutableClass.methods.first { method -> MethodUtil.isConstructor(method) } - .addInstructions( - 1, // Add after super call. - mapOf( - 1 to "MinHeightOrWidth", // p1 = min height - 2 to "MaxHeightOrWidth", // p2 = max height - 3 to "MinHeightOrWidth", // p3 = min width - 4 to "MaxHeightOrWidth" // p4 = max width - ).map { (parameter, method) -> - """ - invoke-static { p$parameter }, $INTEGRATIONS_SPOOF_DEVICE_DIMENSIONS_CLASS_DESCRIPTOR->get$method(I)I - move-result p$parameter - """ - }.joinToString("\n") { it } - ) - } - - // endregion - - // region patch for disable AV1 codec - - // replace av1 codec - - AV1CodecFingerprint.result?.let { - it.mutableMethod.apply { - val insertIndex = indexOfFirstStringInstructionOrThrow("video/av01") - val insertRegister = getInstruction(insertIndex).registerA - - addInstructions( - insertIndex + 1, """ - invoke-static/range {v$insertRegister .. v$insertRegister}, $INTEGRATIONS_AV1_CODEC_CLASS_DESCRIPTOR->replaceCodec(Ljava/lang/String;)Ljava/lang/String; - move-result-object v$insertRegister - """ - ) - } - - SettingsPatch.addPreference( - arrayOf( - "SETTINGS: REPLACE_AV1_CODEC" - ) - ) - } // for compatibility with old versions, no exceptions are raised. - - // reject av1 codec response - - ByteBufferArrayParentFingerprint.resultOrThrow().classDef.let { classDef -> - ByteBufferArrayFingerprint.also { it.resolve(context, classDef) }.resultOrThrow().let { - it.mutableMethod.apply { - val insertIndex = it.scanResult.patternScanResult!!.endIndex - val insertRegister = - getInstruction(insertIndex).registerA - - addInstructions( - insertIndex, """ - invoke-static {v$insertRegister}, $INTEGRATIONS_AV1_CODEC_CLASS_DESCRIPTOR->rejectResponse(I)I - move-result v$insertRegister - """ - ) - } - } - } - - // endregion - - // region patch for disable VP9 codec - - VP9CapabilityFingerprint.resultOrThrow().mutableMethod.apply { - addInstructionsWithLabels( - 0, """ - invoke-static {}, $INTEGRATIONS_VP9_CODEC_CLASS_DESCRIPTOR->disableVP9Codec()Z - move-result v0 - if-nez v0, :default - return v0 - """, ExternalLabel("default", getInstruction(0)) - ) - } - - // endregion - - /** - * Add settings - */ - SettingsPatch.addPreference( - arrayOf( - "PREFERENCE_SCREEN: VIDEO" - ) - ) - - SettingsPatch.updatePatchStatus(this) - } -} diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/AV1CodecFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/AV1CodecFingerprint.kt deleted file mode 100644 index 88e28d7fa..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/AV1CodecFingerprint.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.revanced.patches.youtube.video.playback.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.util.containsWideLiteralInstructionValue -import com.android.tools.smali.dexlib2.AccessFlags - -internal object AV1CodecFingerprint : MethodFingerprint( - accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, - returnType = "L", - strings = listOf("AtomParsers", "video/av01"), - customFingerprint = handler@{ methodDef, _ -> - if (methodDef.returnType == "Ljava/util/List;") - return@handler false - - methodDef.containsWideLiteralInstructionValue(1987076931) - } -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/ByteBufferArrayFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/ByteBufferArrayFingerprint.kt deleted file mode 100644 index 8a7a46c88..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/ByteBufferArrayFingerprint.kt +++ /dev/null @@ -1,21 +0,0 @@ -package app.revanced.patches.youtube.video.playback.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode - -internal object ByteBufferArrayFingerprint : MethodFingerprint( - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - returnType = "I", - parameters = emptyList(), - opcodes = listOf( - Opcode.SHL_INT_LIT8, - Opcode.SHL_INT_LIT8, - Opcode.OR_INT_2ADDR, - Opcode.SHL_INT_LIT8, - Opcode.OR_INT_2ADDR, - Opcode.OR_INT_2ADDR, - Opcode.RETURN - ) -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/ByteBufferArrayParentFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/ByteBufferArrayParentFingerprint.kt deleted file mode 100644 index d9e7803af..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/ByteBufferArrayParentFingerprint.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.revanced.patches.youtube.video.playback.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags - -internal object ByteBufferArrayParentFingerprint : MethodFingerprint( - accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, - returnType = "C", - parameters = listOf("Ljava/nio/charset/Charset;", "[C") -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/DeviceDimensionsModelToStringFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/DeviceDimensionsModelToStringFingerprint.kt deleted file mode 100644 index 42270bb2d..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/DeviceDimensionsModelToStringFingerprint.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.revanced.patches.youtube.video.playback.fingerprints - -import app.revanced.patcher.fingerprint.MethodFingerprint - -internal object DeviceDimensionsModelToStringFingerprint : MethodFingerprint( - returnType = "L", - strings = listOf("minh.", ";maxh.") -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/HDRCapabilityFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/HDRCapabilityFingerprint.kt deleted file mode 100644 index 4b3bb4f75..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/HDRCapabilityFingerprint.kt +++ /dev/null @@ -1,13 +0,0 @@ -package app.revanced.patches.youtube.video.playback.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags - -internal object HDRCapabilityFingerprint : MethodFingerprint( - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - strings = listOf( - "av1_profile_main_10_hdr_10_plus_supported", - "video/av01" - ) -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/PlaybackSpeedChangedFromRecyclerViewFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/PlaybackSpeedChangedFromRecyclerViewFingerprint.kt deleted file mode 100644 index 4dec570e3..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/PlaybackSpeedChangedFromRecyclerViewFingerprint.kt +++ /dev/null @@ -1,23 +0,0 @@ -package app.revanced.patches.youtube.video.playback.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode - -internal object PlaybackSpeedChangedFromRecyclerViewFingerprint : MethodFingerprint( - returnType = "L", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("L"), - opcodes = listOf( - Opcode.IGET_OBJECT, - Opcode.INVOKE_INTERFACE, - Opcode.MOVE_RESULT_OBJECT, - Opcode.IF_EQZ, - Opcode.IGET_OBJECT, - Opcode.INVOKE_INTERFACE, - Opcode.MOVE_RESULT_OBJECT, - Opcode.IGET, - Opcode.INVOKE_VIRTUAL - ) -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/PlaybackSpeedInitializeFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/PlaybackSpeedInitializeFingerprint.kt deleted file mode 100644 index b750f4bc0..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/PlaybackSpeedInitializeFingerprint.kt +++ /dev/null @@ -1,16 +0,0 @@ -package app.revanced.patches.youtube.video.playback.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode - -internal object PlaybackSpeedInitializeFingerprint : MethodFingerprint( - returnType = "F", - accessFlags = AccessFlags.PRIVATE or AccessFlags.STATIC, - parameters = listOf("L"), - opcodes = listOf( - Opcode.IGET, - Opcode.RETURN - ) -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/QualityChangedFromRecyclerViewFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/QualityChangedFromRecyclerViewFingerprint.kt deleted file mode 100644 index a317ae503..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/QualityChangedFromRecyclerViewFingerprint.kt +++ /dev/null @@ -1,32 +0,0 @@ -package app.revanced.patches.youtube.video.playback.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode - -internal object QualityChangedFromRecyclerViewFingerprint : MethodFingerprint( - returnType = "L", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("L"), - opcodes = listOf( - Opcode.IGET, // Video resolution (human readable). - Opcode.IGET_OBJECT, - Opcode.IGET_BOOLEAN, - Opcode.IGET_OBJECT, - Opcode.INVOKE_STATIC, - Opcode.MOVE_RESULT_OBJECT, - Opcode.INVOKE_DIRECT, - Opcode.IGET_OBJECT, - Opcode.INVOKE_INTERFACE, - Opcode.MOVE_RESULT_OBJECT, - Opcode.INVOKE_VIRTUAL, - Opcode.GOTO, - Opcode.CONST_4, - Opcode.IF_NE, - Opcode.IGET_OBJECT, - Opcode.INVOKE_INTERFACE, - Opcode.MOVE_RESULT_OBJECT, - Opcode.IGET, - ) -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/QualitySetterFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/QualitySetterFingerprint.kt deleted file mode 100644 index f626f4bb1..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/QualitySetterFingerprint.kt +++ /dev/null @@ -1,12 +0,0 @@ -package app.revanced.patches.youtube.video.playback.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags - -internal object QualitySetterFingerprint : MethodFingerprint( - returnType = "V", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("L"), - strings = listOf("VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT") -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/VP9CapabilityFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/VP9CapabilityFingerprint.kt deleted file mode 100644 index e904b9312..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playback/fingerprints/VP9CapabilityFingerprint.kt +++ /dev/null @@ -1,14 +0,0 @@ -package app.revanced.patches.youtube.video.playback.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags - -internal object VP9CapabilityFingerprint : MethodFingerprint( - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - returnType = "Z", - strings = listOf( - "vp9_supported", - "video/x-vnd.on2.vp9" - ) -) diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt b/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt deleted file mode 100644 index 68beae36b..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt +++ /dev/null @@ -1,123 +0,0 @@ -package app.revanced.patches.youtube.video.playerresponse - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.InstructionExtensions.addInstruction -import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patches.youtube.video.playerresponse.fingerprint.PlayerParameterBuilderFingerprint -import app.revanced.util.resultOrThrow -import java.io.Closeable -import kotlin.properties.Delegates - -object PlayerResponseMethodHookPatch : - BytecodePatch(setOf(PlayerParameterBuilderFingerprint)), - Closeable, - MutableSet by mutableSetOf() { - - // Parameter numbers of the patched method. - private var PARAMETER_VIDEO_ID = 1 - private var PARAMETER_PLAYER_PARAMETER = 3 - private var PARAMETER_PLAYLIST_ID = 4 - private var PARAMETER_IS_SHORT_AND_OPENING_OR_PLAYING by Delegates.notNull() - - // Registers used to pass the parameters to integrations. - private var playerResponseMethodCopyRegisters = false - private lateinit var REGISTER_VIDEO_ID: String - private lateinit var REGISTER_PLAYER_PARAMETER: String - private lateinit var REGISTER_PLAYLIST_ID: String - private lateinit var REGISTER_IS_SHORT_AND_OPENING_OR_PLAYING: String - - private lateinit var playerResponseMethod: MutableMethod - private var numberOfInstructionsAdded = 0 - - override fun execute(context: BytecodeContext) { - playerResponseMethod = PlayerParameterBuilderFingerprint - .resultOrThrow() - .mutableMethod - - playerResponseMethod.apply { - PARAMETER_IS_SHORT_AND_OPENING_OR_PLAYING = parameters.size - 2 - - // On some app targets the method has too many registers pushing the parameters past v15. - // If needed, move the parameters to 4-bit registers so they can be passed to integrations. - playerResponseMethodCopyRegisters = implementation!!.registerCount - - parameterTypes.size + PARAMETER_IS_SHORT_AND_OPENING_OR_PLAYING > 15 - } - - if (playerResponseMethodCopyRegisters) { - REGISTER_VIDEO_ID = "v0" - REGISTER_PLAYER_PARAMETER = "v1" - REGISTER_PLAYLIST_ID = "v2" - REGISTER_IS_SHORT_AND_OPENING_OR_PLAYING = "v3" - } else { - REGISTER_VIDEO_ID = "p$PARAMETER_VIDEO_ID" - REGISTER_PLAYER_PARAMETER = "p$PARAMETER_PLAYER_PARAMETER" - REGISTER_PLAYLIST_ID = "p$PARAMETER_PLAYLIST_ID" - REGISTER_IS_SHORT_AND_OPENING_OR_PLAYING = "p$PARAMETER_IS_SHORT_AND_OPENING_OR_PLAYING" - } - } - - override fun close() { - fun hookVideoId(hook: Hook) { - playerResponseMethod.addInstruction( - 0, - "invoke-static {$REGISTER_VIDEO_ID, $REGISTER_IS_SHORT_AND_OPENING_OR_PLAYING}, $hook" - ) - numberOfInstructionsAdded++ - } - - fun hookPlayerParameter(hook: Hook) { - playerResponseMethod.addInstructions( - 0, """ - invoke-static {$REGISTER_VIDEO_ID, $REGISTER_PLAYER_PARAMETER, $REGISTER_PLAYLIST_ID, $REGISTER_IS_SHORT_AND_OPENING_OR_PLAYING}, $hook - move-result-object $REGISTER_PLAYER_PARAMETER - """ - ) - numberOfInstructionsAdded += 2 - } - - // Reverse the order in order to preserve insertion order of the hooks. - val beforeVideoIdHooks = filterIsInstance().asReversed() - val videoIdHooks = filterIsInstance().asReversed() - val afterVideoIdHooks = filterIsInstance().asReversed() - - // Add the hooks in this specific order as they insert instructions at the beginning of the method. - afterVideoIdHooks.forEach(::hookPlayerParameter) - videoIdHooks.forEach(::hookVideoId) - beforeVideoIdHooks.forEach(::hookPlayerParameter) - - if (playerResponseMethodCopyRegisters) { - playerResponseMethod.apply { - addInstructions( - 0, - """ - move-object/from16 $REGISTER_VIDEO_ID, p$PARAMETER_VIDEO_ID - move-object/from16 $REGISTER_PLAYER_PARAMETER, p$PARAMETER_PLAYER_PARAMETER - move-object/from16 $REGISTER_PLAYLIST_ID, p$PARAMETER_PLAYLIST_ID - move/from16 $REGISTER_IS_SHORT_AND_OPENING_OR_PLAYING, p$PARAMETER_IS_SHORT_AND_OPENING_OR_PLAYING - """, - ) - - numberOfInstructionsAdded += 4 - - // Move the modified register back. - addInstruction( - numberOfInstructionsAdded, - "move-object/from16 p$PARAMETER_PLAYER_PARAMETER, $REGISTER_PLAYER_PARAMETER" - ) - } - } - } - - internal abstract class Hook(private val methodDescriptor: String) { - internal class VideoId(methodDescriptor: String) : Hook(methodDescriptor) - - internal class PlayerParameter(methodDescriptor: String) : Hook(methodDescriptor) - internal class PlayerParameterBeforeVideoId(methodDescriptor: String) : - Hook(methodDescriptor) - - override fun toString() = methodDescriptor - } -} - diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/fingerprint/PlayerParameterBuilderFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/fingerprint/PlayerParameterBuilderFingerprint.kt deleted file mode 100644 index 6053c8064..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/fingerprint/PlayerParameterBuilderFingerprint.kt +++ /dev/null @@ -1,74 +0,0 @@ -package app.revanced.patches.youtube.video.playerresponse.fingerprint - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patches.youtube.video.playerresponse.fingerprint.PlayerParameterBuilderFingerprint.ENDS_WITH_PARAMETER_LIST -import app.revanced.patches.youtube.video.playerresponse.fingerprint.PlayerParameterBuilderFingerprint.STARTS_WITH_PARAMETER_LIST -import app.revanced.util.parametersEqual -import com.android.tools.smali.dexlib2.AccessFlags - -internal object PlayerParameterBuilderFingerprint : MethodFingerprint( - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - returnType = "L", - // 19.22 and earlier parameters are: - // "Ljava/lang/String;", // VideoId. - // "[B", - // "Ljava/lang/String;", // Player parameters proto buffer. - // "Ljava/lang/String;", // PlaylistId. - // "I", - // "I", - // "Ljava/util/Set;", - // "Ljava/lang/String;", - // "Ljava/lang/String;", - // "L", - // "Z", // Appears to indicate if the video id is being opened or is currently playing. - // "Z", - // "Z" - - // 19.23+ parameters are: - // "Ljava/lang/String;", // VideoId. - // "[B", - // "Ljava/lang/String;", // Player parameters proto buffer. - // "Ljava/lang/String;", // PlaylistId. - // "I", - // "I", - // "L", - // "Ljava/util/Set;", - // "Ljava/lang/String;", - // "Ljava/lang/String;", - // "L", - // "Z", // Appears to indicate if the video id is being opened or is currently playing. - // "Z", - // "Z" - customFingerprint = custom@{ methodDef, _ -> - val parameterTypes = methodDef.parameterTypes - val parameterSize = parameterTypes.size - if (parameterSize != 13 && parameterSize != 14) { - return@custom false - } - - val startsWithMethodParameterList = parameterTypes.slice(0..5) - val endsWithMethodParameterList = parameterTypes.slice(parameterSize - 7.. Unit) = - resultOrThrow().let { result -> - val videoIdRegisterIndex = result.scanResult.patternScanResult!!.endIndex - - result.mutableMethod.let { - val videoIdRegister = - it.getInstruction(videoIdRegisterIndex).registerA - val insertIndex = videoIdRegisterIndex + 1 - consumer(it, insertIndex, videoIdRegister) - } - } - - VideoIdFingerprint.setFields { method, index, register -> - videoIdMethod = method - videoIdInsertIndex = index - videoIdRegister = register - } - } - - /** - * Hooks the new video id when the video changes. - * - * Supports all videos (regular videos and Shorts). - * - * _Does not function if playing in the background with no video visible_. - * - * Be aware, this can be called multiple times for the same video id. - * - * @param methodDescriptor which method to call. Params have to be `Ljava/lang/String;` - */ - fun hookVideoId( - methodDescriptor: String - ) = videoIdMethod.addInstruction( - videoIdInsertIndex++, - "invoke-static {v$videoIdRegister}, $methodDescriptor" - ) - - /** - * Hooks the video id of every video when loaded. - * Supports all videos and functions in all situations. - * - * First parameter is the video id. - * Second parameter is if the video is a Short AND it is being opened or is currently playing. - * - * Hook is always called off the main thread. - * - * This hook is called as soon as the player response is parsed, - * and called before many other hooks are updated such as [PlayerTypeHookPatch]. - * - * Note: The video id returned here may not be the current video that's being played. - * It's common for multiple Shorts to load at once in preparation - * for the user swiping to the next Short. - * - * For most use cases, you probably want to use [hookVideoId] instead. - * - * Be aware, this can be called multiple times for the same video id. - * - * @param methodDescriptor which method to call. Params must be `Ljava/lang/String;Z` - */ - fun hookPlayerResponseVideoId(methodDescriptor: String) { - PlayerResponseMethodHookPatch += PlayerResponseMethodHookPatch.Hook.VideoId( - methodDescriptor - ) - } -} - diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/videoid/fingerprints/VideoIdFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/video/videoid/fingerprints/VideoIdFingerprint.kt deleted file mode 100644 index 04ed1527a..000000000 --- a/src/main/kotlin/app/revanced/patches/youtube/video/videoid/fingerprints/VideoIdFingerprint.kt +++ /dev/null @@ -1,49 +0,0 @@ -package app.revanced.patches.youtube.video.videoid.fingerprints - -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patches.youtube.utils.PlayerResponseModelUtils.PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR -import app.revanced.util.getReference -import app.revanced.util.indexOfFirstInstruction -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -import com.android.tools.smali.dexlib2.iface.reference.MethodReference - -internal object VideoIdFingerprint : MethodFingerprint( - returnType = "V", - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - parameters = listOf("L"), - opcodes = listOf( - Opcode.INVOKE_INTERFACE, - Opcode.MOVE_RESULT_OBJECT, - Opcode.INVOKE_INTERFACE, - Opcode.MOVE_RESULT_OBJECT - ), - customFingerprint = custom@{ methodDef, classDef -> - if (!classDef.fields.any { it.type == "Lcom/google/android/libraries/youtube/player/subtitles/model/SubtitleTrack;" }) { - return@custom false - } - val implementation = methodDef.implementation - ?: return@custom false - val instructions = implementation.instructions - val instructionCount = instructions.count() - if (instructionCount < 30) { - return@custom false - } - - val reference = - (instructions.elementAt(instructionCount - 2) as? ReferenceInstruction)?.reference.toString() - if (reference != "Ljava/util/Map;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;") { - return@custom false - } - - methodDef.indexOfFirstInstruction { - val methodReference = getReference() - opcode == Opcode.INVOKE_INTERFACE && - methodReference?.returnType == "Ljava/lang/String;" && - methodReference.parameterTypes.isEmpty() && - methodReference.definingClass == PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR - } >= 0 - }, -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/src/main/kotlin/app/revanced/util/BytecodeUtils.kt deleted file mode 100644 index 0f4918862..000000000 --- a/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ /dev/null @@ -1,671 +0,0 @@ -@file:Suppress("unused") - -package app.revanced.util - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.InstructionExtensions.addInstruction -import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels -import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patcher.fingerprint.MethodFingerprintResult -import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.util.proxy.mutableTypes.MutableClass -import app.revanced.patcher.util.proxy.mutableTypes.MutableField -import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable -import app.revanced.util.fingerprint.MultiMethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.Method -import com.android.tools.smali.dexlib2.iface.MethodParameter -import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.Instruction -import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction -import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction31i -import com.android.tools.smali.dexlib2.iface.reference.MethodReference -import com.android.tools.smali.dexlib2.iface.reference.Reference -import com.android.tools.smali.dexlib2.iface.reference.StringReference -import com.android.tools.smali.dexlib2.immutable.ImmutableField -import com.android.tools.smali.dexlib2.immutable.ImmutableMethod -import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation -import com.android.tools.smali.dexlib2.util.MethodUtil - -const val REGISTER_TEMPLATE_REPLACEMENT: String = "REGISTER_INDEX" - -fun MethodFingerprint.isDeprecated() = - javaClass.annotations[0].toString().contains("Deprecated") - -fun MethodFingerprint.resultOrThrow() = result ?: throw exception - -fun MultiMethodFingerprint.resultOrThrow() = result.ifEmpty { throw exception } - -fun parametersEqual( - parameters1: Iterable, - parameters2: Iterable -): Boolean { - if (parameters1.count() != parameters2.count()) return false - val iterator1 = parameters1.iterator() - parameters2.forEach { - if (!it.startsWith(iterator1.next())) return false - } - return true -} - -/** - * The [PatchException] of failing to resolve a [MethodFingerprint]. - * - * @return The [PatchException]. - */ -val MethodFingerprint.exception - get() = PatchException("Failed to resolve ${this.javaClass.simpleName}") - -val MultiMethodFingerprint.exception - get() = PatchException("Failed to resolve ${this.javaClass.simpleName}") - -fun MethodFingerprint.alsoResolve(context: BytecodeContext, fingerprint: MethodFingerprint) = - also { resolve(context, fingerprint.resultOrThrow().classDef) }.resultOrThrow() - -fun MethodFingerprint.getMethodCall() = - resultOrThrow().mutableMethod.getMethodCall() - -fun MutableMethod.getMethodCall(): String { - var methodCall = "$definingClass->$name(" - for (i in 0 until parameters.size) { - methodCall += parameterTypes[i] - } - methodCall += ")$returnType" - return methodCall -} - -/** - * Find the [MutableMethod] from a given [Method] in a [MutableClass]. - * - * @param method The [Method] to find. - * @return The [MutableMethod]. - */ -fun MutableClass.findMutableMethodOf(method: Method) = this.methods.first { - MethodUtil.methodSignaturesMatch(it, method) -} - -/** - * Apply a transform to all fields of the class. - * - * @param transform The transformation function. Accepts a [MutableField] and returns a transformed [MutableField]. - */ -fun MutableClass.transformFields(transform: MutableField.() -> MutableField) { - val transformedFields = fields.map { it.transform() } - fields.clear() - fields.addAll(transformedFields) -} - -/** - * Apply a transform to all methods of the class. - * - * @param transform The transformation function. Accepts a [MutableMethod] and returns a transformed [MutableMethod]. - */ -fun MutableClass.transformMethods(transform: MutableMethod.() -> MutableMethod) { - val transformedMethods = methods.map { it.transform() } - methods.removeIf { !MethodUtil.isConstructor(it) } - methods.addAll(transformedMethods) -} - -/** - * Inject a call to a method that hides a view. - * - * @param insertIndex The index to insert the call at. - * @param viewRegister The register of the view to hide. - * @param classDescriptor The descriptor of the class that contains the method. - * @param targetMethod The name of the method to call. - */ -fun MutableMethod.injectHideViewCall( - insertIndex: Int, - viewRegister: Int, - classDescriptor: String, - targetMethod: String -) = addInstruction( - insertIndex, - "invoke-static { v$viewRegister }, $classDescriptor->$targetMethod(Landroid/view/View;)V" -) - -fun MethodFingerprint.injectLiteralInstructionBooleanCall( - literal: Int, - descriptor: String -) = injectLiteralInstructionBooleanCall(literal.toLong(), descriptor) - -fun MethodFingerprint.injectLiteralInstructionBooleanCall( - literal: Long, - descriptor: String -) { - resultOrThrow().mutableMethod.apply { - val literalIndex = indexOfFirstWideLiteralInstructionValueOrThrow(literal) - val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT) - val targetRegister = getInstruction(targetIndex).registerA - - val smaliInstruction = - if (descriptor.startsWith("0x")) """ - const/16 v$targetRegister, $descriptor - """ - else if (descriptor.endsWith("(Z)Z")) """ - invoke-static {v$targetRegister}, $descriptor - move-result v$targetRegister - """ - else """ - invoke-static {}, $descriptor - move-result v$targetRegister - """ - - addInstructions( - targetIndex + 1, - smaliInstruction - ) - } -} - -fun MethodFingerprint.injectLiteralInstructionViewCall( - literal: Long, - smaliInstruction: String -) = resultOrThrow().mutableMethod.injectLiteralInstructionViewCall(literal, smaliInstruction) - -fun MutableMethod.injectLiteralInstructionViewCall( - literal: Long, - smaliInstruction: String -) { - val literalIndex = indexOfFirstWideLiteralInstructionValueOrThrow(literal) - val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT_OBJECT) - val targetRegister = getInstruction(targetIndex).registerA.toString() - - addInstructions( - targetIndex + 1, - smaliInstruction.replace(REGISTER_TEMPLATE_REPLACEMENT, targetRegister) - ) -} - -fun BytecodeContext.injectLiteralInstructionViewCall( - literal: Long, - smaliInstruction: String -) { - val context = this - context.classes.forEach { classDef -> - classDef.methods.forEach { method -> - method.implementation.apply { - this?.instructions?.forEachIndexed { _, instruction -> - if (instruction.opcode != Opcode.CONST) - return@forEachIndexed - if ((instruction as Instruction31i).wideLiteral != literal) - return@forEachIndexed - - context.proxy(classDef) - .mutableClass - .findMutableMethodOf(method) - .injectLiteralInstructionViewCall(literal, smaliInstruction) - } - } - } - } -} - -fun BytecodeContext.replaceLiteralInstructionCall( - literal: Long, - smaliInstruction: String -) { - val context = this - context.classes.forEach { classDef -> - classDef.methods.forEach { method -> - method.implementation.apply { - this?.instructions?.forEachIndexed { _, instruction -> - if (instruction.opcode != Opcode.CONST) - return@forEachIndexed - if ((instruction as Instruction31i).wideLiteral != literal) - return@forEachIndexed - - context.proxy(classDef) - .mutableClass - .findMutableMethodOf(method).apply { - val index = indexOfFirstWideLiteralInstructionValueOrThrow(literal) - val register = - (instruction as OneRegisterInstruction).registerA.toString() - - addInstructions( - index + 1, - smaliInstruction.replace(REGISTER_TEMPLATE_REPLACEMENT, register) - ) - } - } - } - } - } -} - -/** - * Get the index of the first [Instruction] that matches the predicate, starting from [startIndex]. - * - * @param startIndex Optional starting index to start searching from. - * @return -1 if the instruction is not found. - * @see indexOfFirstInstructionOrThrow - */ -fun Method.indexOfFirstInstruction(startIndex: Int = 0, opcode: Opcode): Int = - indexOfFirstInstruction(startIndex) { - this.opcode == opcode - } - -/** - * Get the index of the first [Instruction] that matches the predicate, starting from [startIndex]. - * - * @param startIndex Optional starting index to start searching from. - * @return -1 if the instruction is not found. - * @see indexOfFirstInstructionOrThrow - */ -fun Method.indexOfFirstInstruction(startIndex: Int = 0, predicate: Instruction.() -> Boolean): Int { - if (implementation == null) { - return -1 - } - var instructions = implementation!!.instructions - if (startIndex != 0) { - instructions = instructions.drop(startIndex) - } - val index = instructions.indexOfFirst(predicate) - - return if (index >= 0) { - startIndex + index - } else { - -1 - } -} - -fun Method.indexOfFirstInstructionOrThrow(opcode: Opcode): Int = - indexOfFirstInstructionOrThrow(0, opcode) - -/** - * Get the index of the first [Instruction] that matches the predicate, starting from [startIndex]. - * - * @return the index of the instruction - * @throws PatchException - * @see indexOfFirstInstruction - */ -fun Method.indexOfFirstInstructionOrThrow(startIndex: Int = 0, opcode: Opcode): Int = - indexOfFirstInstructionOrThrow(startIndex) { - this.opcode == opcode - } - -fun Method.indexOfFirstInstructionReversedOrThrow(opcode: Opcode): Int = - indexOfFirstInstructionReversedOrThrow(null, opcode) - -/** - * Get the index of the first [Instruction] that matches the predicate, starting from [startIndex]. - * - * @return the index of the instruction - * @throws PatchException - * @see indexOfFirstInstruction - */ -fun Method.indexOfFirstInstructionOrThrow( - startIndex: Int = 0, - predicate: Instruction.() -> Boolean -): Int { - val index = indexOfFirstInstruction(startIndex, predicate) - if (index < 0) { - throw PatchException("Could not find instruction index") - } - return index -} - -/** - * Get the index of matching instruction, - * starting from and [startIndex] and searching down. - * - * @param startIndex Optional starting index to search down from. Searching includes the start index. - * @return -1 if the instruction is not found. - * @see indexOfFirstInstructionReversedOrThrow - */ -fun Method.indexOfFirstInstructionReversed(startIndex: Int? = null, opcode: Opcode): Int = - indexOfFirstInstructionReversed(startIndex) { - this.opcode == opcode - } - -/** - * Get the index of matching instruction, - * starting from and [startIndex] and searching down. - * - * @param startIndex Optional starting index to search down from. Searching includes the start index. - * @return -1 if the instruction is not found. - * @see indexOfFirstInstructionReversedOrThrow - */ -fun Method.indexOfFirstInstructionReversed( - startIndex: Int? = null, - predicate: Instruction.() -> Boolean -): Int { - if (implementation == null) { - return -1 - } - var instructions = implementation!!.instructions - if (startIndex != null) { - instructions = instructions.take(startIndex + 1) - } - - return instructions.indexOfLast(predicate) -} - -/** - * Get the index of matching instruction, - * starting from and [startIndex] and searching down. - * - * @param startIndex Optional starting index to search down from. Searching includes the start index. - * @return -1 if the instruction is not found. - * @see indexOfFirstInstructionReversed - */ -fun Method.indexOfFirstInstructionReversedOrThrow( - startIndex: Int? = null, - opcode: Opcode -): Int = - indexOfFirstInstructionReversedOrThrow(startIndex) { - this.opcode == opcode - } - -/** - * Get the index of matching instruction, - * starting from and [startIndex] and searching down. - * - * @param startIndex Optional starting index to search down from. Searching includes the start index. - * @return -1 if the instruction is not found. - * @see indexOfFirstInstructionReversed - */ -fun Method.indexOfFirstInstructionReversedOrThrow( - startIndex: Int? = null, - predicate: Instruction.() -> Boolean -): Int { - val index = indexOfFirstInstructionReversed(startIndex, predicate) - - if (index < 0) { - throw PatchException("Could not find instruction index") - } - - return index -} - -/** - * @return The list of indices of the opcode in reverse order. - */ -fun Method.findOpcodeIndicesReversed(opcode: Opcode): List = - findOpcodeIndicesReversed { this.opcode == opcode } - -/** - * @return The list of indices of the opcode in reverse order. - */ -fun Method.findOpcodeIndicesReversed(filter: Instruction.() -> Boolean): List { - val indexes = implementation!!.instructions - .withIndex() - .filter { (_, instruction) -> filter(instruction) } - .map { (index, _) -> index } - .reversed() - - if (indexes.isEmpty()) throw PatchException("No matching instructions found in: $this") - - return indexes -} - -/** - * Find the index of the first wide literal instruction with the given value. - * - * @return the first literal instruction with the value, or -1 if not found. - * @see indexOfFirstWideLiteralInstructionValueOrThrow - */ -fun Method.indexOfFirstWideLiteralInstructionValue(literal: Long) = implementation?.let { - it.instructions.indexOfFirst { instruction -> - (instruction as? WideLiteralInstruction)?.wideLiteral == literal - } -} ?: -1 - - -/** - * Find the index of the first wide literal instruction with the given value, - * or throw an exception if not found. - * - * @return the first literal instruction with the value, or throws [PatchException] if not found. - */ -fun Method.indexOfFirstWideLiteralInstructionValueOrThrow(literal: Long): Int { - val index = indexOfFirstWideLiteralInstructionValue(literal) - if (index < 0) { - val value = - if (literal >= 2130706432) // 0x7f000000, general resource id - String.format("%#X", literal).lowercase() - else - literal.toString() - - throw PatchException("Found literal value: '$value' but method does not contain the id: $this") - } - - return index -} - -fun Method.indexOfFirstStringInstruction(str: String) = - indexOfFirstInstruction { - opcode == Opcode.CONST_STRING && - getReference()?.string == str - } - - -fun Method.indexOfFirstStringInstructionOrThrow(str: String): Int { - val index = indexOfFirstStringInstruction(str) - if (index < 0) { - throw PatchException("Found string value for: '$str' but method does not contain the id: $this") - } - - return index -} - -/** - * Check if the method contains a literal with the given value. - * - * @return if the method contains a literal with the given value. - */ -fun Method.containsWideLiteralInstructionValue(literal: Long) = - indexOfFirstWideLiteralInstructionValue(literal) >= 0 - -/** - * Traverse the class hierarchy starting from the given root class. - * - * @param targetClass the class to start traversing the class hierarchy from. - * @param callback function that is called for every class in the hierarchy. - */ -fun BytecodeContext.traverseClassHierarchy( - targetClass: MutableClass, - callback: MutableClass.() -> Unit -) { - callback(targetClass) - this.findClass(targetClass.superclass ?: return)?.mutableClass?.let { - traverseClassHierarchy(it, callback) - } -} - -/** - * Get the [Reference] of an [Instruction] as [T]. - * - * @param T The type of [Reference] to cast to. - * @return The [Reference] as [T] or null - * if the [Instruction] is not a [ReferenceInstruction] or the [Reference] is not of type [T]. - * @see ReferenceInstruction - */ -inline fun Instruction.getReference() = - (this as? ReferenceInstruction)?.reference as? T - -fun MethodFingerprintResult.getWalkerMethod(context: BytecodeContext, offset: Int) = - mutableMethod.getWalkerMethod(context, offset) - -/** - * MethodWalker can find the wrong class: - * https://github.com/ReVanced/revanced-patcher/issues/309 - * - * As a workaround, redefine MethodWalker here - */ -fun MutableMethod.getWalkerMethod(context: BytecodeContext, offset: Int): MutableMethod { - val newMethod = getInstruction(offset).reference as MethodReference - return context.findMethodOrThrow(newMethod.definingClass) { - MethodUtil.methodSignaturesMatch(this, newMethod) - } -} - -/** - * Taken from BiliRoamingX: - * https://github.com/BiliRoamingX/BiliRoamingX/blob/ae58109f3acdd53ec2d2b3fb439c2a2ef1886221/patches/src/main/kotlin/app/revanced/patches/bilibili/utils/Extenstions.kt#L151 - */ -fun MutableMethod.getFiveRegisters(index: Int) = - with(getInstruction(index)) { - arrayOf(registerC, registerD, registerE, registerF, registerG) - .take(registerCount).joinToString(",") { "v$it" } - } - -fun BytecodeContext.addStaticFieldToIntegration( - className: String, - methodName: String, - fieldName: String, - objectClass: String, - smaliInstructions: String, - shouldAddConstructor: Boolean = true -) { - val mutableClass = findClass { classDef -> classDef.type == className } - ?.mutableClass - ?: throw PatchException("No matching classes found: $className") - - val objectCall = "$mutableClass->$fieldName:$objectClass" - - mutableClass.apply { - methods.first { method -> method.name == methodName }.apply { - staticFields.add( - ImmutableField( - definingClass, - fieldName, - objectClass, - AccessFlags.PUBLIC or AccessFlags.STATIC, - null, - annotations, - null - ).toMutable() - ) - - addInstructionsWithLabels( - 0, - """ - sget-object v0, $objectCall - """ + smaliInstructions - ) - } - } - - if (!shouldAddConstructor) return - - findMethodsOrThrow(objectClass) - .filter { method -> MethodUtil.isConstructor(method) } - .forEach { mutableMethod -> - mutableMethod.apply { - val initializeIndex = indexOfFirstInstructionOrThrow { - opcode == Opcode.INVOKE_DIRECT && - getReference()?.name == "" - } - val insertIndex = if (initializeIndex == -1) - 1 - else - initializeIndex + 1 - - val initializeRegister = if (initializeIndex == -1) - "p0" - else - "v${getInstruction(initializeIndex).registerC}" - - addInstruction( - insertIndex, - "sput-object $initializeRegister, $objectCall" - ) - } - } -} - -fun BytecodeContext.findMethodOrThrow( - reference: String, - methodPredicate: Method.() -> Boolean = { MethodUtil.isConstructor(this) } -) = findMethodsOrThrow(reference).first(methodPredicate) - -fun BytecodeContext.findMethodsOrThrow(reference: String): MutableSet { - val methods = - findClass { classDef -> classDef.type == reference } - ?.mutableClass - ?.methods - - if (methods != null) { - return methods - } else { - throw PatchException("No matching methods found in: $reference") - } -} - -fun BytecodeContext.updatePatchStatus( - className: String, - methodName: String -) = findMethodOrThrow(className) { name == methodName } - .replaceInstruction( - 0, - "const/4 v0, 0x1" - ) - -/** - * Taken from BiliRoamingX: - * https://github.com/BiliRoamingX/BiliRoamingX/blob/ae58109f3acdd53ec2d2b3fb439c2a2ef1886221/patches/src/main/kotlin/app/revanced/patches/bilibili/utils/Extenstions.kt#L51 - */ -fun Method.cloneMutable( - registerCount: Int = implementation?.registerCount ?: 0, - clearImplementation: Boolean = false, - name: String = this.name, - accessFlags: Int = this.accessFlags, - parameters: List = this.parameters, - returnType: String = this.returnType -): MutableMethod { - val clonedImplementation = implementation?.let { - ImmutableMethodImplementation( - registerCount, - if (clearImplementation) emptyList() else it.instructions, - if (clearImplementation) emptyList() else it.tryBlocks, - if (clearImplementation) emptyList() else it.debugItems, - ) - } - return ImmutableMethod( - definingClass, - name, - parameters, - returnType, - accessFlags, - annotations, - hiddenApiRestrictions, - clonedImplementation - ).toMutable() -} - -/** - * Return the resolved methods of [MethodFingerprint]s early. - */ -fun List.returnEarly(bool: Boolean = false) { - val const = if (bool) "0x1" else "0x0" - this.forEach { fingerprint -> - fingerprint.resultOrThrow().let { result -> - val stringInstructions = when (result.method.returnType.first()) { - 'L' -> """ - const/4 v0, $const - return-object v0 - """ - - 'V' -> "return-void" - 'I', 'Z' -> """ - const/4 v0, $const - return v0 - """ - - else -> throw PatchException("This case should never happen: ${fingerprint.javaClass.simpleName}") - } - - result.mutableMethod.addInstructions(0, stringInstructions) - } - } -} diff --git a/src/main/kotlin/app/revanced/util/ResourceUtils.kt b/src/main/kotlin/app/revanced/util/ResourceUtils.kt deleted file mode 100644 index b1f43c18f..000000000 --- a/src/main/kotlin/app/revanced/util/ResourceUtils.kt +++ /dev/null @@ -1,313 +0,0 @@ -@file:Suppress("DEPRECATION", "MemberVisibilityCanBePrivate", "SpellCheckingInspection") - -package app.revanced.util - -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.patch.options.PatchOption -import app.revanced.patcher.util.DomFileEditor -import org.w3c.dom.Element -import org.w3c.dom.Node -import org.w3c.dom.NodeList -import java.io.File -import java.io.InputStream -import java.nio.file.Files -import java.nio.file.StandardCopyOption - -val classLoader: ClassLoader = object {}.javaClass.classLoader - -fun PatchOption.valueOrThrow() = value - ?: throw PatchException("Invalid patch option: $title.") - -fun PatchOption.valueOrThrow() = value - ?: throw PatchException("Invalid patch option: $title.") - -fun PatchOption.lowerCaseOrThrow() = valueOrThrow() - .lowercase() - -fun PatchOption.underBarOrThrow() = lowerCaseOrThrow() - .replace(" ", "_") - -fun Node.adoptChild(tagName: String, block: Element.() -> Unit) { - val child = ownerDocument.createElement(tagName) - child.block() - appendChild(child) -} - -fun Node.cloneNodes(parent: Node) { - val node = cloneNode(true) - parent.appendChild(node) - parent.removeChild(this) -} - -/** - * Recursively traverse the DOM tree starting from the given root node. - * - * @param action function that is called for every node in the tree. - */ -fun Node.doRecursively(action: (Node) -> Unit) { - action(this) - for (i in 0 until this.childNodes.length) this.childNodes.item(i).doRecursively(action) -} - -fun Node.insertNode(tagName: String, targetNode: Node, block: Element.() -> Unit) { - val child = ownerDocument.createElement(tagName) - child.block() - parentNode.insertBefore(child, targetNode) -} - -fun String.startsWithAny(vararg prefixes: String): Boolean { - for (prefix in prefixes) - if (this.startsWith(prefix)) - return true - - return false -} - -fun List.getResourceGroup(fileNames: Array) = map { directory -> - ResourceGroup( - directory, *fileNames - ) -} - -fun ResourceContext.appendAppVersion(appVersion: String) { - addEntryValues( - "revanced_spoof_app_version_target_entries", - "@string/revanced_spoof_app_version_target_entry_" + appVersion.replace(".", "_"), - prepend = false - ) - addEntryValues( - "revanced_spoof_app_version_target_entry_values", - appVersion, - prepend = false - ) -} - -fun ResourceContext.addEntryValues( - attributeName: String, - attributeValue: String, - path: String = "res/values/arrays.xml", - prepend: Boolean = true, -) { - xmlEditor[path].use { - with(it.file) { - val resourcesNode = getElementsByTagName("resources").item(0) as Element - - val newElement: Element = createElement("item") - for (i in 0 until resourcesNode.childNodes.length) { - val node = resourcesNode.childNodes.item(i) as? Element ?: continue - - if (node.getAttribute("name") == attributeName) { - newElement.appendChild(createTextNode(attributeValue)) - - if (prepend) { - node.appendChild(newElement) - } else { - node.insertBefore(newElement, node.firstChild) - } - } - } - } - } -} - -fun ResourceContext.copyFile( - resourceGroup: List, - path: String, - warning: String -): Boolean { - resourceGroup.let { resourceGroups -> - try { - val filePath = File(path) - val resourceDirectory = this["res"] - - resourceGroups.forEach { group -> - val fromDirectory = filePath.resolve(group.resourceDirectoryName) - val toDirectory = resourceDirectory.resolve(group.resourceDirectoryName) - - group.resources.forEach { iconFileName -> - Files.write( - toDirectory.resolve(iconFileName).toPath(), - fromDirectory.resolve(iconFileName).readBytes() - ) - } - } - - return true - } catch (_: Exception) { - println(warning) - } - } - return false -} - -/** - * Copy resources from the current class loader to the resource directory. - * - * @param sourceResourceDirectory The source resource directory name. - * @param resources The resources to copy. - * @param createDirectoryIfNotExist Whether to create a new directory if it does not exist. - */ -fun ResourceContext.copyResources( - sourceResourceDirectory: String, - vararg resources: ResourceGroup, - createDirectoryIfNotExist: Boolean = false, -) { - val targetResourceDirectory = this["res"] - - for (resourceGroup in resources) { - resourceGroup.resources.forEach { resource -> - val resourceDirectoryName = resourceGroup.resourceDirectoryName - - if (createDirectoryIfNotExist) { - val targetDirectory = targetResourceDirectory.resolve(resourceDirectoryName) - if (!targetDirectory.isDirectory) Files.createDirectories(targetDirectory.toPath()) - } - - val resourceFile = "$resourceDirectoryName/$resource" - - inputStreamFromBundledResource( - sourceResourceDirectory, - resourceFile - )?.let { inputStream -> - Files.copy( - inputStream, - targetResourceDirectory.resolve(resourceFile).toPath(), - StandardCopyOption.REPLACE_EXISTING, - ) - } - } - } -} - -/** - * Copy resources from the current class loader to the resource directory with the option to rename. - * - * @param sourceResourceDirectory The source resource directory name. - * @param resourceMap The map containing resource titles and their respective path data. - */ -fun ResourceContext.copyResourcesWithRename( - sourceResourceDirectory: String, - resourceMap: Map -) { - val targetResourceDirectory = this["res"] - - for ((title, pathData) in resourceMap) { - // Check if pathData is another title - if (resourceMap.containsKey(pathData)) { - continue // Skip copying if the pathData is another title - } - - val resourceFile = "drawable/icon.xml" - val inputStream = inputStreamFromBundledResource(sourceResourceDirectory, resourceFile)!! - val targetFile = targetResourceDirectory.resolve("drawable/$title.xml").toPath() - - Files.copy(inputStream, targetFile, StandardCopyOption.REPLACE_EXISTING) - - // Update the XML with the new path data - this.xmlEditor[targetFile.toString()].use { editor -> - updatePathData(editor.file, pathData) - } - } -} - -/** - * Update the `android:pathData` attribute in the XML document. - * - * @param document The XML document. - * @param pathData The new path data to set. - */ -fun updatePathData(document: org.w3c.dom.Document, pathData: String) { - val elements = document.getElementsByTagName("path") - for (i in 0 until elements.length) { - val pathElement = elements.item(i) as? Element - pathElement?.setAttribute("android:pathData", pathData) - } -} - -internal fun inputStreamFromBundledResource( - sourceResourceDirectory: String, - resourceFile: String, -): InputStream? = classLoader.getResourceAsStream("$sourceResourceDirectory/$resourceFile") - -/** - * Resource names mapped to their corresponding resource data. - * @param resourceDirectoryName The name of the directory of the resource. - * @param resources A list of resource names. - */ -class ResourceGroup(val resourceDirectoryName: String, vararg val resources: String) - -/** - * Copy resources from the current class loader to the resource directory. - * @param resourceDirectory The directory of the resource. - * @param targetResource The target resource. - * @param elementTag The element to copy. - */ -fun ResourceContext.copyXmlNode( - resourceDirectory: String, - targetResource: String, - elementTag: String -) = inputStreamFromBundledResource( - resourceDirectory, - targetResource -)?.let { inputStream -> - - // Copy nodes from the resources node to the real resource node - elementTag.copyXmlNode( - this.xmlEditor[inputStream], - this.xmlEditor["res/$targetResource"] - ).close() -} - -/** - * Copies the specified node of the source [DomFileEditor] to the target [DomFileEditor]. - * @param source the source [DomFileEditor]. - * @param target the target [DomFileEditor]- - * @return AutoCloseable that closes the target [DomFileEditor]s. - */ -fun String.copyXmlNode(source: DomFileEditor, target: DomFileEditor): AutoCloseable { - val hostNodes = source.file.getElementsByTagName(this).item(0).childNodes - - val destinationResourceFile = target.file - val destinationNode = destinationResourceFile.getElementsByTagName(this).item(0) - - for (index in 0 until hostNodes.length) { - val node = hostNodes.item(index).cloneNode(true) - destinationResourceFile.adoptNode(node) - destinationNode.appendChild(node) - } - - return AutoCloseable { - source.close() - target.close() - } -} - -internal fun NodeList.findElementByAttributeValue(attributeName: String, value: String): Element? { - for (i in 0 until length) { - val node = item(i) - if (node.nodeType == Node.ELEMENT_NODE) { - val element = node as Element - - if (element.getAttribute(attributeName) == value) { - return element - } - - // Recursively search. - val found = element.childNodes.findElementByAttributeValue(attributeName, value) - if (found != null) { - return found - } - } - } - - return null -} - -internal fun NodeList.findElementByAttributeValueOrThrow( - attributeName: String, - value: String -): Element { - return findElementByAttributeValue(attributeName, value) - ?: throw PatchException("Could not find: $attributeName $value") -} diff --git a/src/main/kotlin/app/revanced/util/Utils.kt b/src/main/kotlin/app/revanced/util/Utils.kt deleted file mode 100644 index 57f0edf03..000000000 --- a/src/main/kotlin/app/revanced/util/Utils.kt +++ /dev/null @@ -1,8 +0,0 @@ -package app.revanced.util - -internal object Utils { - internal fun String.trimIndentMultiline() = - this.split("\n") - .joinToString("\n") { it.trimIndent() } // Remove the leading whitespace from each line. - .trimIndent() // Remove the leading newline. -} diff --git a/src/main/kotlin/app/revanced/util/fingerprint/LiteralValueFingerprint.kt b/src/main/kotlin/app/revanced/util/fingerprint/LiteralValueFingerprint.kt deleted file mode 100644 index 729f7217f..000000000 --- a/src/main/kotlin/app/revanced/util/fingerprint/LiteralValueFingerprint.kt +++ /dev/null @@ -1,34 +0,0 @@ -package app.revanced.util.fingerprint - -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.util.containsWideLiteralInstructionValue -import com.android.tools.smali.dexlib2.Opcode - -/** - * A fingerprint to resolve methods that contain a specific literal value. - * - * @param returnType The method's return type compared using String.startsWith. - * @param accessFlags The method's exact access flags using values of AccessFlags. - * @param parameters The parameters of the method. Partial matches allowed and follow the same rules as returnType. - * @param opcodes An opcode pattern of the method's instructions. Wildcard or unknown opcodes can be specified by null. - * @param strings A list of the method's strings compared each using String.contains. - * @param literalSupplier A supplier for the literal value to check for. - */ -abstract class LiteralValueFingerprint( - returnType: String? = null, - accessFlags: Int? = null, - parameters: Iterable? = null, - opcodes: Iterable? = null, - strings: Iterable? = null, - // Has to be a supplier because the fingerprint is created before patches can set literals. - literalSupplier: () -> Long, -) : MethodFingerprint( - returnType = returnType, - accessFlags = accessFlags, - parameters = parameters, - opcodes = opcodes, - strings = strings, - customFingerprint = { methodDef, _ -> - methodDef.containsWideLiteralInstructionValue(literalSupplier()) - } -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/util/fingerprint/MultiMethodFingerprint.kt b/src/main/kotlin/app/revanced/util/fingerprint/MultiMethodFingerprint.kt deleted file mode 100644 index 54927e034..000000000 --- a/src/main/kotlin/app/revanced/util/fingerprint/MultiMethodFingerprint.kt +++ /dev/null @@ -1,211 +0,0 @@ -package app.revanced.util.fingerprint - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patcher.fingerprint.MethodFingerprintResult -import com.android.tools.smali.dexlib2.Opcode -import com.android.tools.smali.dexlib2.iface.ClassDef -import com.android.tools.smali.dexlib2.iface.Method -import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction -import com.android.tools.smali.dexlib2.iface.reference.StringReference - -private typealias StringMatch = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult.StringMatch -private typealias StringsScanResult = MethodFingerprintResult.MethodFingerprintScanResult.StringsScanResult - -/** - * Taken from BiliRoamingX: - * https://github.com/BiliRoamingX/BiliRoamingX/blob/ae58109f3acdd53ec2d2b3fb439c2a2ef1886221/patches/src/main/kotlin/app/revanced/patches/bilibili/patcher/fingerprint/MultiMethodFingerprint.kt - * - * Represents the [MethodFingerprint] for a method. - * @param returnType The return type of the method. - * @param accessFlags The access flags of the method. - * @param parameters The parameters of the method. - * @param opcodes The list of opcodes of the method. - * @param strings A list of strings which a method contains. - * @param customFingerprint A custom condition for this fingerprint. - * A `null` opcode is equals to an unknown opcode. - */ -abstract class MultiMethodFingerprint( - val returnType: String? = null, - val accessFlags: Int? = null, - val parameters: Iterable? = null, - val opcodes: Iterable? = null, - val strings: Iterable? = null, - val customFingerprint: ((methodDef: Method, classDef: ClassDef) -> Boolean)? = null -) { - /** - * The result of the [MethodFingerprint]. - */ - var result = mutableListOf() - private var resolved = false - - companion object { - /** - * Resolve a list of [MethodFingerprint] against a list of [ClassDef]. - * - * @param classes The classes on which to resolve the [MethodFingerprint] in. - * @param context The [BytecodeContext] to host proxies. - * @return True if the resolution was successful, false otherwise. - */ - fun Iterable.resolve( - context: BytecodeContext, - classes: Iterable - ) { - for (fingerprint in this) { // For each fingerprint - if (fingerprint.resolved) continue - for (classDef in classes) // search through all classes for the fingerprint - fingerprint.resolve(context, classDef) - fingerprint.resolved = true - } - } - - /** - * Resolve a [MethodFingerprint] against a [ClassDef]. - * - * @param forClass The class on which to resolve the [MethodFingerprint] in. - * @param context The [BytecodeContext] to host proxies. - * @return True if the resolution was successful, false otherwise. - */ - fun MultiMethodFingerprint.resolve(context: BytecodeContext, forClass: ClassDef): Boolean { - for (method in forClass.methods) - if (this.resolve(context, method, forClass)) - return true - return false - } - - /** - * Resolve a [MethodFingerprint] against a [Method]. - * - * @param method The class on which to resolve the [MethodFingerprint] in. - * @param forClass The class on which to resolve the [MethodFingerprint]. - * @param context The [BytecodeContext] to host proxies. - * @return True if the resolution was successful or if the fingerprint is already resolved, false otherwise. - */ - fun MultiMethodFingerprint.resolve( - context: BytecodeContext, - method: Method, - forClass: ClassDef - ): Boolean { - val methodFingerprint = this - - if (methodFingerprint.returnType != null && !method.returnType.startsWith( - methodFingerprint.returnType - ) - ) - return false - - if (methodFingerprint.accessFlags != null && methodFingerprint.accessFlags != method.accessFlags) - return false - - fun parametersEqual( - parameters1: Iterable, parameters2: Iterable - ): Boolean { - if (parameters1.count() != parameters2.count()) return false - val iterator1 = parameters1.iterator() - parameters2.forEach { - if (!it.startsWith(iterator1.next())) return false - } - return true - } - - if (methodFingerprint.parameters != null && !parametersEqual( - methodFingerprint.parameters, // TODO: parseParameters() - method.parameterTypes - ) - ) return false - - @Suppress("UNNECESSARY_NOT_NULL_ASSERTION") - if (methodFingerprint.customFingerprint != null && !methodFingerprint.customFingerprint!!( - method, - forClass - ) - ) - return false - - val stringsScanResult = if (methodFingerprint.strings != null) { - StringsScanResult( - buildList { - val implementation = method.implementation ?: return false - - val stringsList = methodFingerprint.strings.toMutableList() - - implementation.instructions.forEachIndexed { instructionIndex, instruction -> - if ( - instruction.opcode != Opcode.CONST_STRING && - instruction.opcode != Opcode.CONST_STRING_JUMBO - ) return@forEachIndexed - - val string = - ((instruction as ReferenceInstruction).reference as StringReference).string - val index = stringsList.indexOfFirst(string::contains) - if (index == -1) return@forEachIndexed - - add(StringMatch(string, instructionIndex)) - stringsList.removeAt(index) - } - - if (stringsList.isNotEmpty()) return false - } - ) - } else null - - val patternScanResult = if (methodFingerprint.opcodes != null) { - method.implementation?.instructions ?: return false - - method.patternScan(methodFingerprint) ?: return false - } else null - - methodFingerprint.result.add( - MethodFingerprintResult( - method, - forClass, - MethodFingerprintResult.MethodFingerprintScanResult( - patternScanResult, - stringsScanResult - ), - context - ) - ) - - return true - } - - private fun Method.patternScan( - fingerprint: MultiMethodFingerprint - ): MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult? { - val instructions = this.implementation!!.instructions - - val pattern = fingerprint.opcodes!! - val instructionLength = instructions.count() - val patternLength = pattern.count() - - for (index in 0 until instructionLength) { - var patternIndex = 0 - - while (index + patternIndex < instructionLength) { - val originalOpcode = instructions.elementAt(index + patternIndex).opcode - val patternOpcode = pattern.elementAt(patternIndex) - - if (patternOpcode != null && patternOpcode.ordinal != originalOpcode.ordinal) { - // reaching maximum threshold (0) means, - // the pattern does not match to the current instructions - break - } - - if (patternIndex < patternLength - 1) { - // if the entire pattern has not been scanned yet - // continue the scan - patternIndex++ - continue - } - return MethodFingerprintResult.MethodFingerprintScanResult.PatternScanResult( - index, - index + patternIndex - ) - } - } - - return null - } - } -} diff --git a/src/main/kotlin/app/revanced/util/patch/BaseBytecodePatch.kt b/src/main/kotlin/app/revanced/util/patch/BaseBytecodePatch.kt deleted file mode 100644 index 4a15c9de7..000000000 --- a/src/main/kotlin/app/revanced/util/patch/BaseBytecodePatch.kt +++ /dev/null @@ -1,23 +0,0 @@ -package app.revanced.util.patch - -import app.revanced.patcher.PatchClass -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patcher.patch.BytecodePatch - -abstract class BaseBytecodePatch( - name: String? = null, - description: String? = null, - dependencies: Set? = null, - compatiblePackages: Set? = null, - fingerprints: Set = emptySet(), - requiresIntegrations: Boolean = false, - use: Boolean = true, -) : BytecodePatch( - name = name, - description = description, - dependencies = dependencies, - compatiblePackages = compatiblePackages, - fingerprints = fingerprints, - requiresIntegrations = requiresIntegrations, - use = use -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/util/patch/BaseResourcePatch.kt b/src/main/kotlin/app/revanced/util/patch/BaseResourcePatch.kt deleted file mode 100644 index 2f61b3777..000000000 --- a/src/main/kotlin/app/revanced/util/patch/BaseResourcePatch.kt +++ /dev/null @@ -1,20 +0,0 @@ -package app.revanced.util.patch - -import app.revanced.patcher.PatchClass -import app.revanced.patcher.patch.ResourcePatch - -abstract class BaseResourcePatch( - name: String? = null, - description: String? = null, - dependencies: Set? = null, - compatiblePackages: Set? = null, - requiresIntegrations: Boolean = false, - use: Boolean = true -) : ResourcePatch( - name = name, - description = description, - dependencies = dependencies, - compatiblePackages = compatiblePackages, - requiresIntegrations = requiresIntegrations, - use = use -) \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/util/patch/MultiMethodBytecodePatch.kt b/src/main/kotlin/app/revanced/util/patch/MultiMethodBytecodePatch.kt deleted file mode 100644 index fc3e5d18d..000000000 --- a/src/main/kotlin/app/revanced/util/patch/MultiMethodBytecodePatch.kt +++ /dev/null @@ -1,20 +0,0 @@ -package app.revanced.util.patch - -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.util.fingerprint.MultiMethodFingerprint -import app.revanced.util.fingerprint.MultiMethodFingerprint.Companion.resolve - -/** - * Taken from BiliRoamingX: - * https://github.com/BiliRoamingX/BiliRoamingX/blob/ae58109f3acdd53ec2d2b3fb439c2a2ef1886221/patches/src/main/kotlin/app/revanced/patches/bilibili/patcher/patch/MultiMethodBytecodePatch.kt - */ -abstract class MultiMethodBytecodePatch( - val fingerprints: Set = setOf(), - val multiFingerprints: Set = setOf() -) : BytecodePatch(fingerprints) { - override fun execute(context: BytecodeContext) { - multiFingerprints.resolve(context, context.classes) - } -} diff --git a/src/main/resources/music/branding/afn_blue/settings/drawable/revanced_extended_settings_key_icon.xml b/src/main/resources/music/branding/afn_blue/settings/drawable/revanced_extended_settings_key_icon.xml deleted file mode 100644 index ae0d36a5d..000000000 --- a/src/main/resources/music/branding/afn_blue/settings/drawable/revanced_extended_settings_key_icon.xml +++ /dev/null @@ -1,2032 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/music/branding/afn_red/settings/drawable/revanced_extended_settings_key_icon.xml b/src/main/resources/music/branding/afn_red/settings/drawable/revanced_extended_settings_key_icon.xml deleted file mode 100644 index b880654e8..000000000 --- a/src/main/resources/music/branding/afn_red/settings/drawable/revanced_extended_settings_key_icon.xml +++ /dev/null @@ -1,2232 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/music/branding/mmt/settings/drawable/revanced_extended_settings_key_icon.xml b/src/main/resources/music/branding/mmt/settings/drawable/revanced_extended_settings_key_icon.xml deleted file mode 100644 index 55e191bf7..000000000 --- a/src/main/resources/music/branding/mmt/settings/drawable/revanced_extended_settings_key_icon.xml +++ /dev/null @@ -1,3288 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/main/resources/music/branding/revancify_blue/settings/drawable/revanced_extended_settings_key_icon.xml b/src/main/resources/music/branding/revancify_blue/settings/drawable/revanced_extended_settings_key_icon.xml deleted file mode 100644 index 7930a4d2f..000000000 --- a/src/main/resources/music/branding/revancify_blue/settings/drawable/revanced_extended_settings_key_icon.xml +++ /dev/null @@ -1,1034 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/music/branding/revancify_red/settings/drawable/revanced_extended_settings_key_icon.xml b/src/main/resources/music/branding/revancify_red/settings/drawable/revanced_extended_settings_key_icon.xml deleted file mode 100644 index 84b505199..000000000 --- a/src/main/resources/music/branding/revancify_red/settings/drawable/revanced_extended_settings_key_icon.xml +++ /dev/null @@ -1,1026 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-hdpi/action_bar_logo.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-hdpi/action_bar_logo.png deleted file mode 100644 index 107307bb5..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-hdpi/action_bar_logo.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-hdpi/logo_music.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-hdpi/logo_music.png deleted file mode 100644 index ead85fcbc..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-hdpi/logo_music.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-hdpi/ytm_logo.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-hdpi/ytm_logo.png deleted file mode 100644 index 4e1ed72c0..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-hdpi/ytm_logo.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-mdpi/action_bar_logo.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-mdpi/action_bar_logo.png deleted file mode 100644 index e05582988..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-mdpi/action_bar_logo.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-mdpi/logo_music.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-mdpi/logo_music.png deleted file mode 100644 index 7b77e96be..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-mdpi/logo_music.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-mdpi/ytm_logo.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-mdpi/ytm_logo.png deleted file mode 100644 index b4a6d97aa..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-mdpi/ytm_logo.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-xhdpi/action_bar_logo.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-xhdpi/action_bar_logo.png deleted file mode 100644 index eee2c5cc7..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-xhdpi/action_bar_logo.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-xhdpi/logo_music.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-xhdpi/logo_music.png deleted file mode 100644 index ae038f4bb..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-xhdpi/logo_music.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-xhdpi/ytm_logo.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-xhdpi/ytm_logo.png deleted file mode 100644 index ede6ab43b..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-xhdpi/ytm_logo.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-xxhdpi/action_bar_logo.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-xxhdpi/action_bar_logo.png deleted file mode 100644 index 61bb4612f..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-xxhdpi/action_bar_logo.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-xxhdpi/logo_music.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-xxhdpi/logo_music.png deleted file mode 100644 index ff2433458..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-xxhdpi/logo_music.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-xxhdpi/ytm_logo.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-xxhdpi/ytm_logo.png deleted file mode 100644 index 68016bd52..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-xxhdpi/ytm_logo.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-xxxhdpi/action_bar_logo.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-xxxhdpi/action_bar_logo.png deleted file mode 100644 index cbc22cab6..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-xxxhdpi/action_bar_logo.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-xxxhdpi/logo_music.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-xxxhdpi/logo_music.png deleted file mode 100644 index 2b7241bf9..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-xxxhdpi/logo_music.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/header/drawable-xxxhdpi/ytm_logo.png b/src/main/resources/music/branding/revancify_yellow/header/drawable-xxxhdpi/ytm_logo.png deleted file mode 100644 index bc451596c..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/header/drawable-xxxhdpi/ytm_logo.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png deleted file mode 100644 index 95a6845d1..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_background_color_108.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png deleted file mode 100644 index 77ac256b4..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-hdpi/adaptiveproduct_youtube_music_foreground_color_108.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-hdpi/ic_launcher_release.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-hdpi/ic_launcher_release.png deleted file mode 100644 index c58b6424f..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-hdpi/ic_launcher_release.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png deleted file mode 100644 index e35d36014..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_background_color_108.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png deleted file mode 100644 index 76fc84f8f..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-mdpi/adaptiveproduct_youtube_music_foreground_color_108.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-mdpi/ic_launcher_release.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-mdpi/ic_launcher_release.png deleted file mode 100644 index 038d3dd6e..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-mdpi/ic_launcher_release.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png deleted file mode 100644 index 8c9f66930..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_background_color_108.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png deleted file mode 100644 index 7971410ca..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xhdpi/adaptiveproduct_youtube_music_foreground_color_108.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xhdpi/ic_launcher_release.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xhdpi/ic_launcher_release.png deleted file mode 100644 index 2fa46a9b7..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xhdpi/ic_launcher_release.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png deleted file mode 100644 index 4687210f7..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_background_color_108.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png deleted file mode 100644 index d9b4d3c1c..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxhdpi/ic_launcher_release.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxhdpi/ic_launcher_release.png deleted file mode 100644 index 61dd8e000..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxhdpi/ic_launcher_release.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png deleted file mode 100644 index dfe12a3b2..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_background_color_108.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png deleted file mode 100644 index fdff54f63..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxxhdpi/adaptiveproduct_youtube_music_foreground_color_108.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxxhdpi/ic_launcher_release.png b/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxxhdpi/ic_launcher_release.png deleted file mode 100644 index 04f47630e..000000000 Binary files a/src/main/resources/music/branding/revancify_yellow/launcher/mipmap-xxxhdpi/ic_launcher_release.png and /dev/null differ diff --git a/src/main/resources/music/branding/revancify_yellow/settings/drawable/revanced_extended_settings_key_icon.xml b/src/main/resources/music/branding/revancify_yellow/settings/drawable/revanced_extended_settings_key_icon.xml deleted file mode 100755 index 85cbc17e9..000000000 --- a/src/main/resources/music/branding/revancify_yellow/settings/drawable/revanced_extended_settings_key_icon.xml +++ /dev/null @@ -1,1022 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/music/branding/xisr_yellow/monochrome/drawable/ic_app_icons_themed_youtube_music.xml b/src/main/resources/music/branding/xisr_yellow/monochrome/drawable/ic_app_icons_themed_youtube_music.xml deleted file mode 100644 index 88b865a1e..000000000 --- a/src/main/resources/music/branding/xisr_yellow/monochrome/drawable/ic_app_icons_themed_youtube_music.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/src/main/resources/music/branding/youtube_music/settings/drawable/revanced_extended_settings_key_icon.xml b/src/main/resources/music/branding/youtube_music/settings/drawable/revanced_extended_settings_key_icon.xml deleted file mode 100644 index a4b62d2bf..000000000 --- a/src/main/resources/music/branding/youtube_music/settings/drawable/revanced_extended_settings_key_icon.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - diff --git a/src/main/resources/music/settings/host/values/arrays.xml b/src/main/resources/music/settings/host/values/arrays.xml deleted file mode 100644 index aaaaac2e9..000000000 --- a/src/main/resources/music/settings/host/values/arrays.xml +++ /dev/null @@ -1,58 +0,0 @@ - - - - @string/revanced_change_start_page_entry_chart - @string/revanced_change_start_page_entry_explore - @string/revanced_change_start_page_entry_home - @string/revanced_change_start_page_entry_library - @string/revanced_change_start_page_entry_subscription - - - FEmusic_charts - FEmusic_explore - FEmusic_home - FEmusic_library_landing - FEmusic_library_corpus_artists - - - @string/revanced_extended_settings_export_as_file - @string/revanced_extended_settings_import_as_file - @string/revanced_extended_settings_import_export_as_text - - - NewPipe - Seal - Tubular - YTDLnis - - - org.schabi.newpipe - com.junkfood.seal - org.polymorphicshade.tubular - com.deniscerri.ytdl - - - https://github.com/TeamNewPipe/NewPipe/releases/latest - https://github.com/JunkFood02/Seal/releases/latest - https://github.com/polymorphicshade/Tubular/releases/latest - https://github.com/deniscerri/ytdlnis/releases/latest - - - @string/revanced_return_youtube_username_display_format_username_only - @string/revanced_return_youtube_username_display_format_username_handle - @string/revanced_return_youtube_username_display_format_handle_username - - - USERNAME_ONLY - USERNAME_HANDLE - HANDLE_USERNAME - - - @string/revanced_spoof_app_version_target_entry_6_11_52 - @string/revanced_spoof_app_version_target_entry_4_27_53 - - - 6.11.52 - 4.27.53 - - diff --git a/src/main/resources/music/settings/host/values/strings.xml b/src/main/resources/music/settings/host/values/strings.xml deleted file mode 100644 index ddc39ad93..000000000 --- a/src/main/resources/music/settings/host/values/strings.xml +++ /dev/null @@ -1,408 +0,0 @@ - - - Continue - Don\'t show again - "GmsCore does not have permission to run in the background. - -Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. - -This is required for the app to work." - "GmsCore battery optimizations must be disabled to prevent issues. - -Tap on the continue button and disable battery optimizations." - Open website - Action needed - Enable cloud messaging to receive notifications. - Open GmsCore - GmsCore is not installed. Install it. - Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. - Bypass image region restrictions - Change from in-app share sheet to system share sheet. - Change share sheet - Charts - Explore - Home - Library - Subscriptions - Select which page the app opens in. - Change start page - List of component path builder strings to filter, separated by new lines. - Custom filter - Enables the custom filter to hide layout components. - Enable custom filter - Invalid custom filter: %s. - Custom speeds must be less than %sx. - Invalid custom playback speeds. - Add or change available playback speeds. - Edit custom playback speeds - To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Disables captions from being automatically enabled. - Disable forced auto captions - Disables Cairo splash animation when the app starts up. - Disable Cairo splash animation - Disables redirection to the next track when clicking the Dislike button. - Disable dislike redirection - Disable swipe to change tracks in the miniplayer. - Disable miniplayer gesture - Disable swipe to change tracks in the player. - Disable player gesture - Sets the navigation bar color to black. - Enable black navigation bar - Changes the player background color to black. - Enable black player background - Matches the color of the miniplayer to the fullscreen player. - Enable color match player - "Enables the compact flyout menu on phones. - -Limitations: -• Album art in the Library tab becomes smaller when organized in a grid. -• Sleep timer layout may appear unusual." - Enable compact dialog - Includes the buffer in the debug log. - Enable debug buffer logging - Prints the debug log. - Enable debug logging - Keeps the player minimized even when another track is played. - Enable force minimized player - Enables landscape mode when rotating the screen on phones. - Enable landscape mode - Adds a next track button to the miniplayer. - Add miniplayer next button - Adds a previous track button to the miniplayer. - Add miniplayer previous button - "Enable the OPUS codec if the player response includes the OPUS codec. - -Info: -• Latest YouTube Music clients use the OPUS audio codec by default. -• This is only valid for users spoofing with very old clients." - Enable OPUS codec - Enables swipe down to dismiss miniplayer. - Enable swipe to dismiss miniplayer - "Adds a Trim silence switch to the playback speed flyout menu. - -Info: -• This feature is for podcasts. -• This feature is still in development, so it may be unstable." - Add Trim silence switch - Also enables Zen mode for podcasts. - Enable Zen mode in podcasts - Changes the player background color to light grey to reduce eye strain. - Enable Zen mode - Reset to default values. - Restart to load the layout normally - Refresh and restart - Export settings to file - Failed to export settings. - Settings were successfully exported. - Import - Import settings from file - Copy - Import / Export settings as text - Import or export settings. - Import / Export settings - Import failed: %s. - Settings reset to default. - Imported %d settings. - Reset - ReVanced Extended - "Download button opens your external downloader. - -• Only overrides the Download action button in the player. -• Does not override the Download button in the flyout menu or Library tab." - Override Download action button - External downloader - "%1$s is not installed. -Please download %2$s from the website." - Warning - %s is not installed. Please install it. - Package name of your installed external downloader app, such as NewPipe or YTDLnis. - External downloader package name - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - Hides empty components in the account menu. - Hide empty components - List of account menu names to filter, separated by new lines. - Account menu filter - Hides account menu elements using the custom filter. - Hide account menu - Hides the Save button. - Hide Save button - Hides the Comments button. - Hide Comments button - Hides the Download button. - Hide Download button - Hides the labels of the action buttons. - Hide action button labels - Hides the Like and Dislike buttons. It does not work in the old player layout. - Hide Like and Dislike buttons - Hides the Radio button. - Hide Radio button - Hides the Share button. - Hide Share button - Hides the Audio / Video toggle in the player. - Hide Audio / Video toggle - Hides the button shelf in the feed. - Hide button shelf - Hides the carousel shelf in the feed. - Hide carousel shelf - Hides the Cast button. - Hide Cast button - Hides the category bar. - Hide category bar - Hides the channel guidelines at the top of the comments section. - Hide channel guidelines - Hides the timestamp and emoji buttons when typing comments. - Hide timestamp and emoji buttons - Hides dark overlay that appears when double-tapping to seek. - Hide double-tap overlay filter - Hides the floating button in the Library tab. - Hide floating button - Hide 3-column component - Hide Add to queue menu - Hide Captions menu - Hide Delete playlist menu - Hide Dismiss queue menu - Hide Download menu - Hide Edit playlist menu - Hide Go to album menu - Hide Go to artist menu - Hide Go to episode menu - Hide Go to podcast menu - Hide Help & feedback menu - Hide Like and Dislike buttons - Hide Play next menu - Hide Quality menu - Hide Remove from library menu - Hide Remove from playlist menu - Hide Report menu - Hide Save episode for later menu - Hide Save to library menu - Hide Save to playlist menu - Hide Share menu - Hide Shuffle play menu - Hide Sleep timer menu - Hide Start radio menu - Hide Stats for nerds menu - Hide Subscribe / Unsubscribe menu - Hide View song credits menu - Hides fullscreen ads. - Hide fullscreen ads - "If it is enabled, fullscreen ads are closed through the Close button. -If it is disabled, fullscreen ads are blocked. (there may be side effects)" - Fullscreen ads are blocked. (there may be side effects) - Fullscreen ads are closed through the Close button. - Close fullscreen ads - Hides the Share button in the fullscreen player. - Hide fullscreen Share button - Hides general ads. - Hide general ads - Hides the handle in the account menu. - Hide handle - Hides the History button in the toolbar. - Hide History button - Hides ads before playing media. - Hide media ads - Hides the navigation bar. - Hide navigation bar - Hides the Explore button. - Hide Explore button - Hides the Home button. - Hide Home button - Hides labels below the navigation buttons. - Hide navigation labels - Hides the Library button. - Hide Library button - Hides the Samples button. - Hide Samples button - Hides the Upgrade button. - Hide Upgrade button - Hides the Notifications button in the toolbar. - Hide Notifications button - Hides the paid promotion label. - Hide paid promotion label - Hides the playlist card shelf in the feed. - Hide playlist card shelf - Hides premium promotion popups. - Hide premium promotion popups - Hides the premium renewal banner. - Hide premium renewal banner - Hides the promotion alert banner. - Hide promotion alert banner - Hides the Samples shelf in the feed. - Hide Samples shelf - Hide About menu - Hide Data saving menu - Hide Downloads & storage menu - Hide General menu - Hide Notifications menu - Hide Get Music premium menu - Hide Family Center menu - Hide Playback menu - Hide Privacy & data menu - Hide Recommendations menu - "Hide elements of the settings menu. -This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." - Hide settings menu - Hides the sound search button in the search bar. - Hide sound search button - Hides the Tap to update button. - Hide Tap to update button - Hides the Terms of Service container. - Hide terms container - Hides the voice search button in the search bar. - Hide voice search button - Account - Action Bar - Ads - Flyout Menu - General - Miscellaneous - Navigation Bar - Player - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Settings menu - Video - Remembers the last playback speed selected. - Remember playback speed changes - Show a toast when changing the default playback speed. - Show a toast - Changing default speed to %s. - Remembers the state of the repeat toggle. - Remember repeat state - Remembers the state of the shuffle toggle. - Remember shuffle state - Remembers the last video quality selected. - Remember video quality changes - Show a toast when changing the default video quality. - Show a toast - Changing default mobile data quality to %s. - Failed to set quality. - Changing default Wi-Fi quality to %s. - "Removes the viewer discretion dialog. -This does not bypass the age restriction. It just accepts it automatically." - Remove viewer discretion dialog - Continues the video from the current time when switching to YouTube. - Continue watching - Replaces the Dismiss queue menu with the Watch on YouTube menu. - Replace Dismiss queue menu - Watch on YouTube - Invalid video url. - Keeps the Report menu in the comments section intact. - Keep Report in comments - Replaces the Report menu with the Playback speed menu. - Replace Report menu - Returns the comments popup panels to the old style. - Restore old comments popup panels - Returns the player background to the old style. - Restore old player background - "Returns the player layout to the old style. -Some features may not work properly in the old player layout." - Restore old player layout - Returns the Library tab to the old style. (Experimental) - Restore old style library shelf - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Username - Replaces handles with usernames in comments. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - About - Data is provided by the Return YouTube Dislike API. Tap here to learn more. - ReturnYouTubeDislike.com - Hides the separator of the like button. - Compact like button - Displays the percentage of dislikes instead of the dislike count. - Dislikes as percentage - Shows the dislike count of videos. - Enable Return YouTube Dislike - Shows the estimated like count of videos. - Show estimated likes - Dislikes are unavailable (client API limit reached). - Dislikes are unavailable (status %d). - Dislikes are temporarily unavailable (API timed out). - Dislikes are unavailable (%s). - Shows a toast if the Return YouTube Dislike API is unavailable. - Show a toast if API is unavailable - Hidden - Removes tracking query parameters from URLs when sharing links. - Sanitize sharing links - About - sponsor.ajay.app - Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. - Change API URL - API URL changed. - API URL is invalid. - API URL reset. - The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. - Color changed. - Color: - Invalid color code. - Color reset. - Change segment behavior - Enable SponsorBlock - SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. - Reset color - Filler Tangent / Jokes - Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. - Interaction Reminder (Subscribe) - A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. - Intermission / Intro Animation - An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. - Music: Non-Music Section - Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. - Endcards / Credits - Credits or when the YouTube endcards appear. Not for conclusions with information. - Preview / Recap / Hook - Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. - Unpaid / Self Promotion - Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. - Sponsor - Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. - Skip automatically - Disable - Skipped filler. - Skipped annoying reminder. - Skipped intro. - Skipped intermission. - Skipped intermission. - Skipped multiple segments. - Skipped a non-music section. - Skipped outro. - Skipped preview. - Skipped recap. - Skipped preview. - Skipped self promotion. - Skipped sponsor. - SponsorBlock is temporarily unavailable. - SponsorBlock is temporarily unavailable (status %d). - SponsorBlock is temporarily unavailable (API timed out). - Show a toast if API is unavailable - Shows a toast if the SponsorBlock API is unavailable. - Show a toast when skipping automatically - Shows a toast when a segment is automatically skipped. - Settings copied to clipboard. - "Spoofs the client version to an older version. - -• This will change the appearance of the app, but unknown side effects may occur. -• If later disabled, the old UI may remain until the app data is cleared." - 4.27.53 - Disable Radio mode in Canadian regions - 6.11.52 - Disable real-time lyrics - 7.16.53 - Restore old action bar - Select the spoof app version target. - Spoof app version target - Spoof app version - diff --git a/src/main/resources/music/translations/bg-rBG/missing_strings.xml b/src/main/resources/music/translations/bg-rBG/missing_strings.xml deleted file mode 100644 index db42e40fa..000000000 --- a/src/main/resources/music/translations/bg-rBG/missing_strings.xml +++ /dev/null @@ -1,209 +0,0 @@ - - - Continue - Don\'t show again - "GmsCore does not have permission to run in the background. - -Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. - -This is required for the app to work." - "GmsCore battery optimizations must be disabled to prevent issues. - -Tap on the continue button and disable battery optimizations." - Open website - Action needed - Enable cloud messaging to receive notifications. - Open GmsCore - GmsCore is not installed. Install it. - Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. - Bypass image region restrictions - Change from in-app share sheet to system share sheet. - Change share sheet - Invalid custom playback speeds. - To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Disables Cairo splash animation when the app starts up. - Disable Cairo splash animation - Disable swipe to change tracks in the miniplayer. - Disable miniplayer gesture - Disable swipe to change tracks in the player. - Disable player gesture - Changes the player background color to black. - Enable black player background - Includes the buffer in the debug log. - Enable debug buffer logging - Prints the debug log. - Adds a next track button to the miniplayer. - Add miniplayer next button - Adds a previous track button to the miniplayer. - Add miniplayer previous button - "Enable the OPUS codec if the player response includes the OPUS codec. - -Info: -• Latest YouTube Music clients use the OPUS audio codec by default. -• This is only valid for users spoofing with very old clients." - Enables swipe down to dismiss miniplayer. - Enable swipe to dismiss miniplayer - Also enables Zen mode for podcasts. - Enable Zen mode in podcasts - Reset to default values. - Export settings to file - Failed to export settings. - Settings were successfully exported. - Import settings from file - Import / Export settings as text - Import / Export settings - Import failed: %s. - Settings reset to default. - Imported %d settings. - Reset - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - Hides the Audio / Video toggle in the player. - Hide Audio / Video toggle - Hides the channel guidelines at the top of the comments section. - Hide channel guidelines - Hides the timestamp and emoji buttons when typing comments. - Hide timestamp and emoji buttons - Hides dark overlay that appears when double-tapping to seek. - Hide double-tap overlay filter - Fullscreen ads are blocked. (there may be side effects) - Fullscreen ads are closed through the Close button. - Hides the Share button in the fullscreen player. - Hide fullscreen Share button - Hides labels below the navigation buttons. - Hides the Library button. - Hides the Samples button. - Hide Samples button - Hides the Upgrade button. - Hide Upgrade button - Hides the promotion alert banner. - Hide promotion alert banner - Hide About menu - Hide Data saving menu - Hide Downloads & storage menu - Hide General menu - Hide Notifications menu - Hide Get Music premium menu - Hide Family Center menu - Hide Playback menu - Hide Privacy & data menu - Hide Recommendations menu - Miscellaneous - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Settings menu - Video - Remembers the last playback speed selected. - Remember playback speed changes - Show a toast when changing the default playback speed. - Show a toast - Changing default speed to %s. - Remembers the state of the repeat toggle. - Remember repeat state - Remembers the state of the shuffle toggle. - Remember shuffle state - Remembers the last video quality selected. - Remember video quality changes - Show a toast when changing the default video quality. - Show a toast - Changing default mobile data quality to %s. - Failed to set quality. - Changing default Wi-Fi quality to %s. - Returns the comments popup panels to the old style. - Restore old comments popup panels - Returns the player background to the old style. - Restore old player background - "Returns the player layout to the old style. -Some features may not work properly in the old player layout." - Restore old player layout - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Username - Replaces handles with usernames in comments. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - ReturnYouTubeDislike.com - Hides the separator of the like button. - Displays the percentage of dislikes instead of the dislike count. - Shows the dislike count of videos. - Enable Return YouTube Dislike - Shows the estimated like count of videos. - Show estimated likes - Dislikes are unavailable (status %d). - Dislikes are temporarily unavailable (API timed out). - Dislikes are unavailable (%s). - Shows a toast if the Return YouTube Dislike API is unavailable. - Show a toast if API is unavailable - Hidden - Removes tracking query parameters from URLs when sharing links. - Sanitize sharing links - About - sponsor.ajay.app - Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. - Change API URL - API URL changed. - API URL is invalid. - API URL reset. - The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. - Color changed. - Color: - Invalid color code. - Color reset. - Change segment behavior - Enable SponsorBlock - SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. - Reset color - Filler Tangent / Jokes - Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. - Interaction Reminder (Subscribe) - A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. - Intermission / Intro Animation - An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. - Music: Non-Music Section - Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. - Endcards / Credits - Credits or when the YouTube endcards appear. Not for conclusions with information. - Preview / Recap / Hook - Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. - Unpaid / Self Promotion - Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. - Sponsor - Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. - Skip automatically - Disable - Skipped filler. - Skipped annoying reminder. - Skipped intro. - Skipped intermission. - Skipped intermission. - Skipped multiple segments. - Skipped a non-music section. - Skipped outro. - Skipped preview. - Skipped recap. - Skipped preview. - Skipped self promotion. - Skipped sponsor. - SponsorBlock is temporarily unavailable. - SponsorBlock is temporarily unavailable (status %d). - SponsorBlock is temporarily unavailable (API timed out). - Show a toast if API is unavailable - Shows a toast if the SponsorBlock API is unavailable. - Show a toast when skipping automatically - Shows a toast when a segment is automatically skipped. - Settings copied to clipboard. - 7.16.53 - Restore old action bar - diff --git a/src/main/resources/music/translations/bg-rBG/strings.xml b/src/main/resources/music/translations/bg-rBG/strings.xml deleted file mode 100644 index 778b1edde..000000000 --- a/src/main/resources/music/translations/bg-rBG/strings.xml +++ /dev/null @@ -1,201 +0,0 @@ - - - Хит-парад - Преглед - Начало - Библиотека - Абонаменти - Изберете на коя страница да се отвори приложението. - Промяна на началната страница - Филтриране на компонентите по имена. - Редактиране на потребителския филтъра - Активира персонализиран филтър за скриване на компонентите на оформлението. - Вкл. на филтър по избор - Невалиден потребителски филтър: %s. - Невалидна скорост на видеото. Връщане на стойности по подразбиране. - Добавяне или смяна на възможните скорости - Редактиране на скоростите по избор на видеото - Изкл. принудителни автоматични субтититри. - Изкл. принудителни автоматични субтититри - Деактивира пренасочването към следващата песен, когато щракнете върху бутона „Не харесвам“. - Désactiver la redirection du bouton \"Je n\'aime pas\" - Задава цвета на лентата за навигация на черен. - Включване на черна навигационна лента - Цветът на плейъра на цял екран съответства с цвета на минимизирания. - A játékos színmegfelelésének engedélyezése - "Активира компактно изскачащо меню на телефони. - -Известни проблеми: -• Скрийнсейвърите на албуми в раздела \"Библиотека\" стават по-малки в мрежа. -• Интерфейсът за автоматично изключване може да изглежда необичайно." - Компактен изглед на прозореца - Вкл. отчети за грешки - Tartsa a lejátszót mindig minimálisra, még akkor is, ha egy másik számot játszik le. - Kapcsolja be az állandó összeomlott lejátszót - Активира пейзажен режим при завъртане на телефона. - Позволи Пейзажен Режим - Включване на OPUS аудио кодек - "Добавя „Скриване на мълчанията“ към падащото меню „Скорост на възпроизвеждане“. - -Информация: -• Тази функция е предназначена за подкасти. -• Тази функция все още е в процес на разработка, така че може да е нестабилна." - Добавете опция „Скриване на мълчанията“ - Добавя сив оттенък към видеоплейъра, за да намали напрежението на очите. - Включване на zen режим - Рестартирайте, за да заредите оформлението нормално - Опреснете и рестартирайте - Внасяне - Копиране - Импортирайте или експортирайте настройки като текст. - ReVanced Extended - "Бутонът \"Изтегляне\" отваря външната програма за изтегляне. - -• Заменя само бутона за изтегляне в плейъра. -• Не отменя бутона за изтегляне в изскачащото меню или библиотеката." - Замяна на бутона за изтегляне - Външна програма за изтегляне - "%1$s не е инсталиран. -Моля, изтеглете %2$s от уебсайта." - Внимание - %s не е инсталирано. Моля инсталирайте го. - Име на пакета на приложението за изтегляне като NewPipe или YTDLnis. - Име на пакета на външно приложение за изтегляне - Скрива празните компоненти в менюто на акаунта. - Скриване на празни компоненти - Списък с имена на менюта на акаунти за филтриране, разделени с нови редове. - Промяна на филтъра на менюто на акаунта - Скрива елементи от менюто на акаунта в персонализиран филтър. - Скриване на менюто на акаунта - Скрива бутона \"Запазване\". - Бутон \"Запазване\" - Скрийте бутона „Коментари“. - Скриване на бутона за коментари - Скрива бутона „Изтегляне“. - Скриване на бутона за изтегляне - Скрива. етикетите на бутоните за действие. - Скриване на етикетите на бутоните за действие - Скрива бутоните „Харесвам“ и „Не харесвам“. Не работи в стария интерфейс на плейъра. - Скриване на бутоните за харесване и нехаресване - Скрива бутона \"Радио\". - Скрийте бутона \"Радио\" - Скрива бутона „Споделяне“. - Скриване на бутона за споделяне - Скриване на секцията с бутони в емисията. - Скриване на секцията с бутони - Скриване на рафтовете с предложения в емисиите. - Скриване на рафта с Препоръчани - Скрива бутона \"Излъчване\". - Скриване на бутона за предаване на Тв - Скриване на панела с категории. - Скриване на панела с Категории - Скрива плаващите бутони в библиотеката. - Скриване на изскачащ бутон - Скриване на компонента с 3 колони - Скрийте бутона „Добавяне към опашката“ - Скриване на менюто за субтитри - Скрийте менюто „Изтриване на плейлист“ - Скрийте менюто „Изтриване на опашката“ - Скрийте менюто „Изтегляне“ - Скрийте менюто „Редактиране на плейлист“ - Скрийте менюто „Отиди на албум“ - Скрийте менюто „Отидете на страницата на изпълнителя“ - Скрийте менюто „Отидете на епизод“ - Скрийте менюто „Отидете на подкаст“ - Скриване на менюто & за помощ - Скриване на бутоните за харесване и нехаресване - Скрийте менюто „Пусни следващия клип“ - Скрийте менюто „Качество“ - Скрийте менюто „Премахване от библиотеката“ - Скрийте менюто „Изтриване на плейлист“ - Меню за докладване - Скрийте менюто „Запазване за гледане по-късно“ - Скрийте менюто „Запазване в библиотеката“ - Скрийте менюто „Запазване в плейлист“ - Скрийте менюто „Споделяне“ - Скрийте бутона „Разбъркване“ - Скрийте менюто „Изчакване на заспиване“ - Скрийте менюто „Стартиране на радио“ - Меню \"Статистика за сис. администратори\" - Скрийте менюто „Абониране“ / „Отписване“ - Скрийте менюто „Подробности за заглавие“ - Скриване на рекламите в режим на цял екран. - Скриване на рекламите в режим на цял екран - "Ако е активирана, рекламата на цял екран се затваря чрез бутона Затвори. -Ако е деактивирано, рекламата на цял екран е блокирана. (могат да възникнат нежелани реакции)" - Как да затворите реклами на цял екран - Скриване на общите реклами. - Скриване на общите реклами - Скрива имейл/@ник в менюто за промяна на акаунта. - Скриване на връзки - Скрива бутона \"История\" от лентата с инструменти. - Скрийте бутона \"История\" - Скрива реклами преди възпроизвеждане на музика. - Скриване на музикални реклами - Скриване лентата за навигация. - Скриване лентата за навигация - Скрива. бутона за Преглед. - Скриване на бутона \"навигация\" - Скрива бутона \"Начало\". - Скриване на бутон за Начало - Скриване на навигационен панел - Бутона за Библиотека - Скрива бутона „Известие“ от лентата с инструменти. - Бутон за Известия - Скриване на платените промоции. - Скриване на платените промоции - Скрива рафтовете с карти „Списъци за изпълнение“ в емисии. - Скрийте рафтовете „Списъци за изпълнение“ - Скрива изскачащи реклами Premium. - Скриване на изскачащи реклами Premium - Скриване на банера за подновяване на Premium. - Скриване на банера за подновяване на Premium - Скриване на рафтовете с Семпли в емисиите. - Скрийте рафта „Семпли“ - "Скриване на елементи от менюто с настройки. -Това не само скрива менюто с настройки на YT Music, но и менюто с разширени настройки на ReVanced." - Скрийте менюто „Настройки“ - Скрива бутона „звуково търсене на музика“ от лентата за търсене. - Бутон за \"Звуково търсене\" - Скриване на бутона „Докоснете за актуализиране“. - Скрийте бутона „Докоснете за актуализиране“ - Скриване на подробностите за поверителност / правила и условия. - Скриване на информацията за поверителност - Скрива бутона „Гласово търсене“ от лентата за търсене. - Бутон за \"гласово търсене\" - Акаунт - Лента с действия - Реклами - Падащо меню - Главни - Лента за навигация - Плеър - "Премахва диалоговите прозорци. Това не заобикаля възрастовите ограничения, но ги приема автоматично." - Скриване на прозореца за възрастово ограничение - Видеото продължава от текущото време на гледане, когато отидете в YouTube. - Продължете да гледате - Заменя менюто „Премахване от опашката“ с менюто „Гледайте в YouTube“. - Заменете менюто „Премахване от опашката“ - Гледайте в YouTube - Невалиден Url адрес на видеото. - Запазва менюто Доклад в раздела за коментари непокътнато. - Запазете доклада в коментарите - Заменя менюто Доклад с менюто Скорост на възпроизвеждане. - Заменете менюто „Докладвай“ - Възстановява стария стил на рафт \"Библиотека\". (Експериментално) - Възстановете стария стил на рафта „Библиотека“ - Относно - Данните за нехаресване са от Return YouTube Dislike API. Докоснете за да научите повече. - Компактен бутон за харесване - Нехаресвания като процент - Нехаресванията не са достъпни (достигнат лимит на API). - "Заменя клиентската версия със старата. - -• Това ще промени външния вид на приложението, но може да възникнат неизвестни проблеми. -• Ако деактивирате тази опция, след като я активирате, старият интерфейс може да остане, докато данните на приложението не бъдат изчистени." - 4.27.53 - Деактивира радио режима в регионите на Канада - 6.11.52 -Изключва речта в реално време - Задайте желаната фалшива версия на приложението. - Подлъгване за версията на приложението - Подлъгване за версията на приложението - diff --git a/src/main/resources/music/translations/bn/missing_strings.xml b/src/main/resources/music/translations/bn/missing_strings.xml deleted file mode 100644 index bd5f4af77..000000000 --- a/src/main/resources/music/translations/bn/missing_strings.xml +++ /dev/null @@ -1,340 +0,0 @@ - - - Continue - Don\'t show again - "GmsCore does not have permission to run in the background. - -Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. - -This is required for the app to work." - "GmsCore battery optimizations must be disabled to prevent issues. - -Tap on the continue button and disable battery optimizations." - Open website - Action needed - Enable cloud messaging to receive notifications. - Open GmsCore - GmsCore is not installed. Install it. - Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. - Bypass image region restrictions - Change from in-app share sheet to system share sheet. - Change share sheet - Charts - Explore - Home - Library - Subscriptions - Select which page the app opens in. - Change start page - Invalid custom filter: %s. - Invalid custom playback speeds. - To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Disables Cairo splash animation when the app starts up. - Disable Cairo splash animation - Disables redirection to the next track when clicking the Dislike button. - Disable dislike redirection - Disable swipe to change tracks in the miniplayer. - Disable miniplayer gesture - Disable swipe to change tracks in the player. - Disable player gesture - Changes the player background color to black. - Enable black player background - "Enables the compact flyout menu on phones. - -Limitations: -• Album art in the Library tab becomes smaller when organized in a grid. -• Sleep timer layout may appear unusual." - Includes the buffer in the debug log. - Enable debug buffer logging - Adds a next track button to the miniplayer. - Add miniplayer next button - Adds a previous track button to the miniplayer. - Add miniplayer previous button - Enables swipe down to dismiss miniplayer. - Enable swipe to dismiss miniplayer - "Adds a Trim silence switch to the playback speed flyout menu. - -Info: -• This feature is for podcasts. -• This feature is still in development, so it may be unstable." - Add Trim silence switch - Also enables Zen mode for podcasts. - Enable Zen mode in podcasts - Reset to default values. - Restart to load the layout normally - Refresh and restart - Export settings to file - Failed to export settings. - Settings were successfully exported. - Import settings from file - Import / Export settings as text - Import failed: %s. - Reset - ReVanced Extended - "Download button opens your external downloader. - -• Only overrides the Download action button in the player. -• Does not override the Download button in the flyout menu or Library tab." - Override Download action button - External downloader - "%1$s is not installed. -Please download %2$s from the website." - Warning - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - List of account menu names to filter, separated by new lines. - Account menu filter - Hides the Save button. - Hide Save button - Hides the Comments button. - Hide Comments button - Hides the Download button. - Hide Download button - Hides the labels of the action buttons. - Hide action button labels - Hides the Like and Dislike buttons. It does not work in the old player layout. - Hide Like and Dislike buttons - Hides the Radio button. - Hide Radio button - Hides the Share button. - Hide Share button - Hides the Audio / Video toggle in the player. - Hide Audio / Video toggle - Hides the channel guidelines at the top of the comments section. - Hide channel guidelines - Hides the timestamp and emoji buttons when typing comments. - Hide timestamp and emoji buttons - Hides dark overlay that appears when double-tapping to seek. - Hide double-tap overlay filter - Hides the floating button in the Library tab. - Hide floating button - Hide 3-column component - Hide Add to queue menu - Hide Captions menu - Hide Delete playlist menu - Hide Dismiss queue menu - Hide Download menu - Hide Edit playlist menu - Hide Go to album menu - Hide Go to artist menu - Hide Go to episode menu - Hide Go to podcast menu - Hide Help & feedback menu - Hide Like and Dislike buttons - Hide Play next menu - Hide Quality menu - Hide Remove from library menu - Hide Remove from playlist menu - Hide Report menu - Hide Save episode for later menu - Hide Save to library menu - Hide Save to playlist menu - Hide Share menu - Hide Shuffle play menu - Hide Sleep timer menu - Hide Start radio menu - Hide Stats for nerds menu - Hide Subscribe / Unsubscribe menu - Hide View song credits menu - Hides fullscreen ads. - Hide fullscreen ads - "If it is enabled, fullscreen ads are closed through the Close button. -If it is disabled, fullscreen ads are blocked. (there may be side effects)" - Fullscreen ads are blocked. (there may be side effects) - Fullscreen ads are closed through the Close button. - Close fullscreen ads - Hides the Share button in the fullscreen player. - Hide fullscreen Share button - Hides general ads. - Hide general ads - Hides the History button in the toolbar. - Hide History button - Hides the Explore button. - Hide Explore button - Hides the Home button. - Hide Home button - Hides the Library button. - Hide Library button - Hides the Samples button. - Hide Samples button - Hides the Upgrade button. - Hide Upgrade button - Hides the Notifications button in the toolbar. - Hide Notifications button - Hides the paid promotion label. - Hide paid promotion label - Hides the playlist card shelf in the feed. - Hide playlist card shelf - Hides premium promotion popups. - Hide premium promotion popups - Hides the premium renewal banner. - Hide premium renewal banner - Hides the promotion alert banner. - Hide promotion alert banner - Hides the Samples shelf in the feed. - Hide Samples shelf - Hide About menu - Hide Data saving menu - Hide Downloads & storage menu - Hide General menu - Hide Notifications menu - Hide Get Music premium menu - Hide Family Center menu - Hide Playback menu - Hide Privacy & data menu - Hide Recommendations menu - "Hide elements of the settings menu. -This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." - Hide settings menu - Hides the sound search button in the search bar. - Hide sound search button - Hides the Tap to update button. - Hide Tap to update button - Hides the voice search button in the search bar. - Hide voice search button - Account - Action Bar - Ads - Flyout Menu - General - Miscellaneous - Navigation Bar - Player - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Settings menu - Video - Remembers the last playback speed selected. - Remember playback speed changes - Show a toast when changing the default playback speed. - Show a toast - Changing default speed to %s. - Remembers the state of the repeat toggle. - Remember repeat state - Remembers the state of the shuffle toggle. - Remember shuffle state - Remembers the last video quality selected. - Remember video quality changes - Show a toast when changing the default video quality. - Show a toast - Changing default mobile data quality to %s. - Failed to set quality. - Changing default Wi-Fi quality to %s. - "Removes the viewer discretion dialog. -This does not bypass the age restriction. It just accepts it automatically." - Remove viewer discretion dialog - Continues the video from the current time when switching to YouTube. - Continue watching - Replaces the Dismiss queue menu with the Watch on YouTube menu. - Replace Dismiss queue menu - Watch on YouTube - Invalid video url. - Keeps the Report menu in the comments section intact. - Keep Report in comments - Replaces the Report menu with the Playback speed menu. - Replace Report menu - Returns the comments popup panels to the old style. - Restore old comments popup panels - Returns the player background to the old style. - Restore old player background - "Returns the player layout to the old style. -Some features may not work properly in the old player layout." - Restore old player layout - Returns the Library tab to the old style. (Experimental) - Restore old style library shelf - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Username - Replaces handles with usernames in comments. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - ReturnYouTubeDislike.com - Enable Return YouTube Dislike - Shows the estimated like count of videos. - Show estimated likes - Dislikes are unavailable (status %d). - Dislikes are temporarily unavailable (API timed out). - Dislikes are unavailable (%s). - Shows a toast if the Return YouTube Dislike API is unavailable. - Show a toast if API is unavailable - Hidden - Removes tracking query parameters from URLs when sharing links. - Sanitize sharing links - About - sponsor.ajay.app - Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. - Change API URL - API URL changed. - API URL is invalid. - API URL reset. - The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. - Color changed. - Color: - Invalid color code. - Color reset. - Change segment behavior - Enable SponsorBlock - SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. - Reset color - Filler Tangent / Jokes - Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. - Interaction Reminder (Subscribe) - A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. - Intermission / Intro Animation - An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. - Music: Non-Music Section - Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. - Endcards / Credits - Credits or when the YouTube endcards appear. Not for conclusions with information. - Preview / Recap / Hook - Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. - Unpaid / Self Promotion - Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. - Sponsor - Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. - Skip automatically - Disable - Skipped filler. - Skipped annoying reminder. - Skipped intro. - Skipped intermission. - Skipped intermission. - Skipped multiple segments. - Skipped a non-music section. - Skipped outro. - Skipped preview. - Skipped recap. - Skipped preview. - Skipped self promotion. - Skipped sponsor. - SponsorBlock is temporarily unavailable. - SponsorBlock is temporarily unavailable (status %d). - SponsorBlock is temporarily unavailable (API timed out). - Show a toast if API is unavailable - Shows a toast if the SponsorBlock API is unavailable. - Show a toast when skipping automatically - Shows a toast when a segment is automatically skipped. - Settings copied to clipboard. - "Spoofs the client version to an older version. - -• This will change the appearance of the app, but unknown side effects may occur. -• If later disabled, the old UI may remain until the app data is cleared." - 4.27.53 - Disable Radio mode in Canadian regions - 6.11.52 - Disable real-time lyrics - 7.16.53 - Restore old action bar - Select the spoof app version target. - Spoof app version target - diff --git a/src/main/resources/music/translations/bn/strings.xml b/src/main/resources/music/translations/bn/strings.xml deleted file mode 100644 index a3dc0538b..000000000 --- a/src/main/resources/music/translations/bn/strings.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - ভিন্ন লাইনে ফিল্টারযোগ্য উপাদানের নাম লিখুন। - কাস্টম ফিল্টার সম্পাদনা করুন - কাস্টম ফিল্টার সক্রিয় করুন - কাস্টম ফিল্টার সক্রিয় করুন - ত্রুটিপূর্ণ কাস্টম প্লেব্যাক স্পিড! পুর্বনির্ধারিত ভ্যালুতে আবার সেট করুন। - পাওয়া যাচ্ছে এমন প্লেব্যাক স্পিড যুক্ত বা পরিবর্তন করুন - কাস্টম প্লেব্যাক স্পিড সম্পাদনা করুন - ভিডিও প্লেয়ারে স্বয়ংক্রিয়ভাবে চালু হওয়া বাধ্যতামূলক ক্যাপশনগুলি অকার্যকর করুন। - স্বয়ংক্রিয় ক্যাপশন বন্ধ করুন - নেভিগেমন বার এর রং কালো করে। - কালো নেভিগেশন বার সক্রিয় করুন - পূর্ণস্ক্রীন প্লেয়ারের রং মিনিমাইজ করা প্লেয়ারের রং এর সাথে মিলবে। - প্লেয়ারের রং মিলানো সক্রিয় করুন - কম্প্যাক্ট ডায়ালগ সক্রিয় করুন - ডিবাগ লগ প্রিন্ট করে - ডিবাগ লগ সক্রিয় করুন - প্লেয়ারকে স্থায়ীভাবে মিনিমাইজ করে রাখুন এমনকি যদি অন্য ট্র্যাক চালানো হয়। - জোরপূর্বক মিনিমাইজড প্লেয়ার সক্রিয় করুন - ফোনের স্ক্রিন ঘুরিয়ে আড়াআড়ি মোডে প্রবেশ সক্রিয় করে। - আড়াআড়ি মোড সক্রিয় করুন - "অডিও প্লে করার সময় ২৫০/২৫১ অপাস কোডেক সক্রিয় করুন।" - Opus কোডেক সক্রিয় করুন - চোখের চাপ কমাতে ভিডিও প্লেয়ারে ধূসর আভা যোগ করুন। - জেন মোড সক্রিয় করুন - আমদানি করুন - কপি করুন - টেক্সট আকারে সেটিং আমদানি বা রপ্তানি করুন। - আমদানি / রপ্তানি - সেটিং পূর্ব নির্ধারিততে ফিরে গিয়েছে - %d সেটিং আমদানি হয়েছে - %s ইনস্টল করা হয়নি। অনুগ্রপূর্বক এটি ইনস্টল করুন। - আপনার ইনস্টল করা বাইরের ডাউনলোডার অ্যাপের প্যাকেজ নাম, যেমন NewPipe বা Seal - বাহিরের ডাউনলোডারের প্যাকেজ নাম - অ্যাকাউন্ট মেনুতে ফাঁকা উপাদানগুলো লুকান - ফাঁকা উপাদান লুকান - অ্যাকাউন্ট মেনু উপাদানগুলো লুকান। - অ্যাকাউন্ট মেনু লুকান - প্রধান পাতা ও এক্সপ্লোরার থেকে বাটন শেলফ লুকান। - বাটন শেলফ লুকান - প্রধান পাতা ও এক্সপ্লোরার থেকে ক্যারোসেল শেলফ লুকান। - ক্যারোসেল শেলফ লুকান - মূল পাতা এবং প্লেয়ারের উপর থেকে কাস্ট বাটন লুকিয়ে রাখে। - কাস্ট বাটন লুকান - প্রধান পাতার উপর থেকে বিভাগ বার লুকান। - বিভাগ বার লুকান - অ্যাকাউন্ট পরিবর্তনের হ্যান্ডল লুকায়। - হ্যান্ডল লুকান - ট্র্যাক চালু হওয়ার আগে বিজ্ঞাপনগুলি লুকান। - সঙ্গীতের বিজ্ঞাপন লুকান - নেভিগেশন বার লুকায়। - নেভিগেশন বার লুকান - নেভিগেশন বার থেকে লেবেল হাইড করুন। - নেভিগেশন বারের লেবেল লুকান - সার্ভিস কন্টেইনারের নীতিমালা লুকায়। - নীতিমালা কন্টেইনার লুকান - সম্পর্কে - তথ্য প্রদান করা হয় Return YouTube Dislike API দ্বারা। আরও জানতে আলতো চাপুন। - পছন্দ বাটনের বিভাজক লুকায়। - কমপ্যাক্ট পছন্দ বাটন - অপছন্দ সংখ্যার পরিবর্তে, অপছন্দের শতাংশ দেখায়। - শতাংশ অনুযায়ী অপছন্দ - ভিডিওর অপছন্দ কাউন্ট দেখায়। - অপছন্দ পাওয়া যাচ্ছে না (ক্লায়েন্ট API সর্বোচ্চ সীমা পৌঁছেছে) - অ্যাপ সংস্করণ স্পুফ করুন - diff --git a/src/main/resources/music/translations/cs-rCZ/missing_strings.xml b/src/main/resources/music/translations/cs-rCZ/missing_strings.xml deleted file mode 100644 index 073e422c7..000000000 --- a/src/main/resources/music/translations/cs-rCZ/missing_strings.xml +++ /dev/null @@ -1,364 +0,0 @@ - - - Continue - Don\'t show again - "GmsCore does not have permission to run in the background. - -Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. - -This is required for the app to work." - "GmsCore battery optimizations must be disabled to prevent issues. - -Tap on the continue button and disable battery optimizations." - Open website - Action needed - Enable cloud messaging to receive notifications. - Open GmsCore - GmsCore is not installed. Install it. - Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. - Bypass image region restrictions - Change from in-app share sheet to system share sheet. - Change share sheet - Charts - Explore - Home - Library - Subscriptions - Select which page the app opens in. - Change start page - List of component path builder strings to filter, separated by new lines. - Invalid custom filter: %s. - Custom speeds must be less than %sx. - Invalid custom playback speeds. - Add or change available playback speeds. - Edit custom playback speeds - To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Disables Cairo splash animation when the app starts up. - Disable Cairo splash animation - Disables redirection to the next track when clicking the Dislike button. - Disable dislike redirection - Disable swipe to change tracks in the miniplayer. - Disable miniplayer gesture - Disable swipe to change tracks in the player. - Disable player gesture - Changes the player background color to black. - Enable black player background - "Enables the compact flyout menu on phones. - -Limitations: -• Album art in the Library tab becomes smaller when organized in a grid. -• Sleep timer layout may appear unusual." - Includes the buffer in the debug log. - Enable debug buffer logging - Adds a next track button to the miniplayer. - Add miniplayer next button - Adds a previous track button to the miniplayer. - Add miniplayer previous button - Enables swipe down to dismiss miniplayer. - Enable swipe to dismiss miniplayer - "Adds a Trim silence switch to the playback speed flyout menu. - -Info: -• This feature is for podcasts. -• This feature is still in development, so it may be unstable." - Add Trim silence switch - Also enables Zen mode for podcasts. - Enable Zen mode in podcasts - Reset to default values. - Export settings to file - Failed to export settings. - Settings were successfully exported. - Import - Import settings from file - Copy - Import / Export settings as text - Import or export settings. - Import / Export settings - Import failed: %s. - Settings reset to default. - Imported %d settings. - Reset - "Download button opens your external downloader. - -• Only overrides the Download action button in the player. -• Does not override the Download button in the flyout menu or Library tab." - Override Download action button - External downloader - "%1$s is not installed. -Please download %2$s from the website." - Warning - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - Hides empty components in the account menu. - Hide empty components - List of account menu names to filter, separated by new lines. - Account menu filter - Hides account menu elements using the custom filter. - Hide account menu - Hides the Save button. - Hide Save button - Hides the Comments button. - Hide Comments button - Hides the Download button. - Hide Download button - Hides the labels of the action buttons. - Hide action button labels - Hides the Like and Dislike buttons. It does not work in the old player layout. - Hide Like and Dislike buttons - Hides the Radio button. - Hide Radio button - Hides the Share button. - Hide Share button - Hides the Audio / Video toggle in the player. - Hide Audio / Video toggle - Hides the channel guidelines at the top of the comments section. - Hide channel guidelines - Hides the timestamp and emoji buttons when typing comments. - Hide timestamp and emoji buttons - Hides dark overlay that appears when double-tapping to seek. - Hide double-tap overlay filter - Hides the floating button in the Library tab. - Hide floating button - Hide 3-column component - Hide Add to queue menu - Hide Captions menu - Hide Delete playlist menu - Hide Dismiss queue menu - Hide Download menu - Hide Edit playlist menu - Hide Go to album menu - Hide Go to artist menu - Hide Go to episode menu - Hide Go to podcast menu - Hide Help & feedback menu - Hide Like and Dislike buttons - Hide Play next menu - Hide Quality menu - Hide Remove from library menu - Hide Remove from playlist menu - Hide Report menu - Hide Save episode for later menu - Hide Save to library menu - Hide Save to playlist menu - Hide Share menu - Hide Shuffle play menu - Hide Sleep timer menu - Hide Start radio menu - Hide Stats for nerds menu - Hide Subscribe / Unsubscribe menu - Hide View song credits menu - Hides fullscreen ads. - Hide fullscreen ads - "If it is enabled, fullscreen ads are closed through the Close button. -If it is disabled, fullscreen ads are blocked. (there may be side effects)" - Fullscreen ads are blocked. (there may be side effects) - Fullscreen ads are closed through the Close button. - Close fullscreen ads - Hides the Share button in the fullscreen player. - Hide fullscreen Share button - Hides general ads. - Hide general ads - Hides the handle in the account menu. - Hide handle - Hides the History button in the toolbar. - Hide History button - Hides the navigation bar. - Hide navigation bar - Hides the Explore button. - Hide Explore button - Hides the Home button. - Hide Home button - Hides the Library button. - Hide Library button - Hides the Samples button. - Hide Samples button - Hides the Upgrade button. - Hide Upgrade button - Hides the Notifications button in the toolbar. - Hide Notifications button - Hides the paid promotion label. - Hide paid promotion label - Hides the playlist card shelf in the feed. - Hide playlist card shelf - Hides premium promotion popups. - Hide premium promotion popups - Hides the premium renewal banner. - Hide premium renewal banner - Hides the promotion alert banner. - Hide promotion alert banner - Hides the Samples shelf in the feed. - Hide Samples shelf - Hide About menu - Hide Data saving menu - Hide Downloads & storage menu - Hide General menu - Hide Notifications menu - Hide Get Music premium menu - Hide Family Center menu - Hide Playback menu - Hide Privacy & data menu - Hide Recommendations menu - "Hide elements of the settings menu. -This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." - Hide settings menu - Hides the sound search button in the search bar. - Hide sound search button - Hides the Tap to update button. - Hide Tap to update button - Hides the Terms of Service container. - Hide terms container - Hides the voice search button in the search bar. - Hide voice search button - Action Bar - Ads - Flyout Menu - General - Miscellaneous - Navigation Bar - Player - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Settings menu - Video - Remembers the last playback speed selected. - Remember playback speed changes - Show a toast when changing the default playback speed. - Show a toast - Changing default speed to %s. - Remembers the state of the repeat toggle. - Remember repeat state - Remembers the state of the shuffle toggle. - Remember shuffle state - Remembers the last video quality selected. - Remember video quality changes - Show a toast when changing the default video quality. - Show a toast - Changing default mobile data quality to %s. - Failed to set quality. - Changing default Wi-Fi quality to %s. - "Removes the viewer discretion dialog. -This does not bypass the age restriction. It just accepts it automatically." - Remove viewer discretion dialog - Continues the video from the current time when switching to YouTube. - Continue watching - Replaces the Dismiss queue menu with the Watch on YouTube menu. - Replace Dismiss queue menu - Watch on YouTube - Invalid video url. - Keeps the Report menu in the comments section intact. - Keep Report in comments - Replaces the Report menu with the Playback speed menu. - Replace Report menu - Returns the comments popup panels to the old style. - Restore old comments popup panels - Returns the player background to the old style. - Restore old player background - "Returns the player layout to the old style. -Some features may not work properly in the old player layout." - Restore old player layout - Returns the Library tab to the old style. (Experimental) - Restore old style library shelf - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Username - Replaces handles with usernames in comments. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - About - Data is provided by the Return YouTube Dislike API. Tap here to learn more. - ReturnYouTubeDislike.com - Hides the separator of the like button. - Compact like button - Displays the percentage of dislikes instead of the dislike count. - Dislikes as percentage - Shows the dislike count of videos. - Enable Return YouTube Dislike - Shows the estimated like count of videos. - Show estimated likes - Dislikes are unavailable (client API limit reached). - Dislikes are unavailable (status %d). - Dislikes are temporarily unavailable (API timed out). - Dislikes are unavailable (%s). - Shows a toast if the Return YouTube Dislike API is unavailable. - Show a toast if API is unavailable - Hidden - Removes tracking query parameters from URLs when sharing links. - Sanitize sharing links - About - sponsor.ajay.app - Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. - Change API URL - API URL changed. - API URL is invalid. - API URL reset. - The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. - Color changed. - Color: - Invalid color code. - Color reset. - Change segment behavior - Enable SponsorBlock - SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. - Reset color - Filler Tangent / Jokes - Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. - Interaction Reminder (Subscribe) - A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. - Intermission / Intro Animation - An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. - Music: Non-Music Section - Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. - Endcards / Credits - Credits or when the YouTube endcards appear. Not for conclusions with information. - Preview / Recap / Hook - Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. - Unpaid / Self Promotion - Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. - Sponsor - Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. - Skip automatically - Disable - Skipped filler. - Skipped annoying reminder. - Skipped intro. - Skipped intermission. - Skipped intermission. - Skipped multiple segments. - Skipped a non-music section. - Skipped outro. - Skipped preview. - Skipped recap. - Skipped preview. - Skipped self promotion. - Skipped sponsor. - SponsorBlock is temporarily unavailable. - SponsorBlock is temporarily unavailable (status %d). - SponsorBlock is temporarily unavailable (API timed out). - Show a toast if API is unavailable - Shows a toast if the SponsorBlock API is unavailable. - Show a toast when skipping automatically - Shows a toast when a segment is automatically skipped. - Settings copied to clipboard. - "Spoofs the client version to an older version. - -• This will change the appearance of the app, but unknown side effects may occur. -• If later disabled, the old UI may remain until the app data is cleared." - 4.27.53 - Disable Radio mode in Canadian regions - 6.11.52 - Disable real-time lyrics - 7.16.53 - Restore old action bar - Select the spoof app version target. - Spoof app version target - diff --git a/src/main/resources/music/translations/cs-rCZ/strings.xml b/src/main/resources/music/translations/cs-rCZ/strings.xml deleted file mode 100644 index 3e937ae2b..000000000 --- a/src/main/resources/music/translations/cs-rCZ/strings.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - Upravit vlastní filtr - Povolit vlastní filtry - Povolit vlastní filtr - Zakáže vynucené automatické titulky. - Zakázat vynucené automatické titulky - Nastaví barvu navigačního panelu na černou. - Povolit černou navigační lištu - Odpovídá barvě mini přehrávače a režimu celé obrazovky. - Povolit barevně shodný přehrávač - Povolit kompaktní dialogové okno - Vypíše protokol ladění - Povolit režim ladění - Zachovat přehrávač trvale minimalizovaný, i když je přehrávána jiná skladba. - Povolit vynucenou minimalizaci přehrávače - Umožňuje vstup do režimu na šířku pomocí otočení obrazovky telefonu. - Povolit režim na šířku - "Povolit 250/251 opus kodek při přehrávání audia." - Povolit opus kodek - Přidá šedý odstín do přehrávače videa ke snížení namáhání očí. - Povolit zen mod - - Obnovit a restartovat - ReVanced Extended - %s není instalován. Prosím, nainstalujte jej. - Název balíčku externí nainstalované aplikace na stahování, jako jsou např. NewPipe nebo Seal - Název balíčku pro externí stahování - Skryje pole s tlačítky z domovské stránky a prohlížeče. - Skrýt pole s tlačítky - Skryje točivé pole z domovské stránky a prohlížeče. - Skrýt točivé pole - Skryje tlačítko pro vysílání obrazu z horní části domovské obrazovky a horní části přehrávače. - Skrýt tlačítko pro vysílání - Skryje lištu s hudebními kategoriemi z horní části domovské obrazovky. - Skrýt lištu s kategoriemi - Skryje reklamy před přehráváním hudby. - Skrýt hudební reklamy - Skrýt popisky v navigačním panelu. - Skrýt popisky navigačního panelu - - Zfalšovat verzi aplikace - diff --git a/src/main/resources/music/translations/el-rGR/missing_strings.xml b/src/main/resources/music/translations/el-rGR/missing_strings.xml deleted file mode 100644 index acebd7abf..000000000 --- a/src/main/resources/music/translations/el-rGR/missing_strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Don\'t show again - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - diff --git a/src/main/resources/music/translations/el-rGR/strings.xml b/src/main/resources/music/translations/el-rGR/strings.xml deleted file mode 100644 index e2f7f5cdf..000000000 --- a/src/main/resources/music/translations/el-rGR/strings.xml +++ /dev/null @@ -1,405 +0,0 @@ - - - Συνέχεια - "Το MicroG GmsCore δεν έχει άδεια να τρέχει στο παρασκήνιο. - -Ακολουθήστε τον οδηγό \"Don't kill my app!\" για το τηλέφωνό σας και εφαρμόστε τις οδηγίες στο MicroG. - -Αυτό απαιτείται για να λειτουργήσει η εφαρμογή." - "Οι βελτιστοποιήσεις μπαταρίας στο MicroG GmsCore πρέπει να απενεργοποιηθούν για την αποφυγή προβλημάτων. - -Πατήστε το κουμπί «Συνέχεια» και απενεργοποιήστε τις βελτιστοποιήσεις μπαταρίας για το MicroG." - Άνοιγμα ιστοσελίδας - Απαιτείται ενέργεια - Ενεργοποιήστε τις ρυθμίσεις cloud messaging για να λαμβάνετε ειδοποιήσεις. - Άνοιγμα του MicroG GmsCore - Το MicroG GmsCore δεν είναι εγκατεστημένο. Εγκαταστήστε το. - Αντικατάσταση του domain για την φόρτωση εικόνων όπου είναι μπλοκαρισμένες σε ορισμένες περιοχές ώστε να μπορούν να ληφθούν μικρογραφίες βίντεο, εικόνες δημοσιεύσεων, κλπ. - Παράκαμψη μπλοκαρίσματος φόρτωσης εικόνων - Αλλαγή του μενού κοινοποίησης σε αυτό του συστήματος σας αντί του YouTube Music. - Αλλαγή μενού κοινοποίησης - Διαγράμματα - Εξερεύνηση - Αρχική - Βιβλιοθήκη - Εγγραφές - Ορισμός της αρχικής σελίδας ανοίγματος της εφαρμογής. - Αλλαγή αρχικής σελίδας - Λίστα από συμβολοσειρές στοιχείων που θα φιλτραριστούν, διαχωρισμένες με νέες γραμμές. - Επεξεργασία προσαρμοσμένου φίλτρου - Χρήση προσαρμοσμένου φίλτρου για απόκρυψη στοιχείων διεπαφής. - Προσαρμοσμένο φίλτρο - Μη έγκυρο φίλτρο: %s. - Οι ταχύτητες πρέπει να είναι μικρότερες από %sx. - Μη έγκυρες ταχύτητες αναπαραγωγής. - Ρύθμιση των διαθέσιμων ταχυτήτων αναπαραγωγής. - Επεξεργασία ταχυτήτων αναπαραγωγής - Για να ανοίγουν οι συνδέσμοι YouTube Music στο RVX Music, ενεργοποιήστε το «Άνοιγμα υποστηριζόμενων συνδέσμων» και τις υποστηριζόμενες διευθύνσεις ιστού. - Άνοιγμα ρυθμίσεων προεπιλεγμένων εφαρμογών - Απενεργοποίηση της αυτόματης ενεργοποίησης υπότιτλων. - Απενεργοποίηση αυτόματων υπότιτλων - Απενεργοποίηση των εφέ θέματος Cairo κατά την εκκίνηση της εφαρμογής. - Απενεργοποίηση εφέ εκκίνησης θέματος Cairo - Απενεργοποίηση της ανακατεύθυνσης στο επόμενο κομμάτι όταν πατάτε το κουμπί «Δεν μου αρέσει». - Απενεργοποίηση ανακατεύθυνσης dislike - Απενεργοποίηση της χειρονομίας σάρωσης για αλλαγή κομματιού στην ελαχιστοποιημένη οθόνη αναπαραγωγής. - Απενεργοποίηση χειρονομίας ελαχιστοποιημένης οθόνης αναπαραγωγής - Απενεργοποίηση της χειρονομίας σάρωσης για αλλαγή κομματιού στην οθόνη αναπαραγωγής. - Απενεργοποίηση χειρονομίας οθόνης αναπαραγωγής - Ορισμός του χρώματος της γραμμής πλοήγησης σε μαύρο. - Μαύρη γραμμής πλοήγησης - Αλλαγή χρώματος της οθόνης αναπαραγωγής σε μαύρο. - Μαύρο φόντο οθόνης αναπαραγωγής - Να ταιριάζει το χρώμα της ελαχιστοποιημένης οθόνης αναπαραγωγής με αυτό της οθόνης αναπαραγωγής πλήρους οθόνης. - Ταίριασμα χρωμάτων οθόνων αναπαραγωγής - "Χρήση μικρότερου στυλ για το αναδυόμενο μενού. - -Περιορισμοί: -• Τα εξώφυλλα άλμπουμ στην καρτέλα βιβλιοθήκης γίνονται μικρότερα επίσης. -• Η διεπαφή του χρονομέτρου ύπνου ενδέχεται να φαίνεται ασυνήθιστη." - Αναδυόμενο μενού μικρότερου στυλ - Η καταγραφή εντοπισμού σφαλμάτων θα περιλαμβάνει το proto buffer. - Συμπερίληψη του buffer στην καταγραφή - Εκτύπωση του αρχείου καταγραφής σφαλμάτων. - Ενεργοποίηση καταγραφής σφαλμάτων - Να διατηρείται μόνιμα ελαχιστοποιημένο το πρόγραμμα αναπαραγωγής ακόμη και όταν αναπαράγεται άλλο κομμάτι. - Εξαναγκαστική ελαχιστοποίηση οθόνης αναπαραγωγής - Ενεργοποίηση της οριζόντιας λειτουργίας με την περιστροφή της οθόνης. - Ενεργοποίηση οριζόντιας λειτουργίας - Ενεργοποίηση του κουμπιού επόμενου βίντεο στην ελαχιστοποιημένη οθόνη αναπαραγωγής. - Κουμπί επόμενου βίντεο στον miniplayer - Ενεργοποίηση του κουμπιού προηγούμενου βίντεο στην ελαχιστοποιημένη οθόνη αναπαραγωγής. - Κουμπί προηγούμενου βίντεο στον miniplayer - "Ενεργοποίηση του κωδικοποιητή OPUS αν η ανταπόκριση του προγράμματος αναπαραγωγής τον περιλαμβάνει. - -Πληροφορία: Οι τελευταίες εκδόσεις Android χρησιμοποιούν τον κωδικοποιητή opus από προεπιλογή, οπότε αυτή η ρύθμιση ισχύει μόνο για χρήστες που χρησιμοποιούν τη λειτουργία παραποίησης έκδοσης εφαρμογής, σε πολύ παλιές εκδόσεις." - Ενεργοποίηση κωδικοποιητή opus - Ενεργοποίηση χειρονομίας σάρωσης προς τα κάτω για απόρριψη της ελαχιστοποιημένης οθόνης αναπαραγωγής. - Χειρονομία απόρριψης ελαχιστοποιημένης οθόνης αναπαραγωγής - "Ενεργοποίηση της λειτουργίας «Περικοπή σίγασης» στο αναδυόμενο μενού αλλαγής ταχύτητας αναπαραγωγής. - -Πληροφορίες: -• Αυτή η λειτουργία είναι για ηχητικές εκπομπές. -• Αυτή η λειτουργία είναι ακόμη υπό ανάπτυξη, οπότε ενδέχεται να είναι ασταθής." - Ενεργοποίηση περικοπής σίγασης - Η λειτουργία Zen εφαρμόζεται σε ηχητικές εκπομπές επίσης. - Λειτουργία zen σε ηχητικές εκπομπές - Προσθήκη μιας γκρι απόχρωσης στο παρασκήνιο της οθόνης αναπαραγωγής για να μειωθεί η καταπόνηση των ματιών. - Ενεργοποίηση λειτουργίας zen - Επαναφέρθηκε στην προεπιλεγμένη τιμή. - Επανεκκίνηση ώστε να φορτωθεί σωστά η διάταξη - Ανανέωση και επανεκκίνηση - Εξαγωγή ρυθμίσεων σε αρχείο - Αποτυχία εξαγωγής ρυθμίσεων. - Οι ρυθμίσεις εξήχθησαν με επιτυχία. - Εισαγωγή - Εισαγωγή ρυθμίσεων από αρχείο - Αντιγραφή - Εισαγωγή / Εξαγωγή ρυθμίσεων ως κείμενο - Εισαγωγή ή εξαγωγή των ρυθμίσεών σας. - Εισαγωγή / Εξαγωγή - Η εισαγωγή απέτυχε: %s. - Οι ρυθμίσεις επαναφέρθηκαν στις προεπιλογές. - Έγινε εισαγωγή %d ρυθμίσεων. - Επαναφορά - ReVanced Extended - "Το κουμπί «Λήψη» ανοίγει το εξωτερικό πρόγραμμα λήψης σας. - -• Η μετατροπή αφορά μόνο το κουμπί ενέργειας στην οθόνη αναπαραγωγής. -• Δεν αφορά το κουμπί «Λήψη» στο αναδυόμενο μενού ή στη βιβλιοθήκη." - Μετατροπή κουμπιού ενέργειας «Λήψη» - Εξωτερικό πρόγραμμα λήψης - "Το %1$s δεν είναι εγκατεστημένο. -Παρακαλούμε εγκαταστήστε το %2$s από την ιστοσελίδα." - Προειδοποίηση - %s δεν έχει εγκατασταθεί. Παρακαλούμε εγκαταστήστε το. - Όνομα πακέτου της εγκατεστημένης εξωτερικής εφαρμογής λήψης (π.χ NewPipe, Seal). - Όνομα πακέτου εξωτερικού προγράμματος λήψης - Απόκρυψη κενών στοιχείων στο μενού λογαριασμού. - Απόκρυψη κενών στοιχείων - Λίστα ονομάτων των επιλογών του μενού λογαριασμού για φιλτράρισμα, διαχωρισμένα με νέες γραμμές. - Επεξεργασία φίλτρου μενού λογαριασμού - Απόκρυψη στοιχείων του μενού λογαριασμού χρησιμοποιώντας προσαρμοσμένο φίλτρο. - Φιλτράρισμα του μενού λογαριασμού - Απόκρυψη του κουμπιού αποθήκευσης σε λίστα αναπαραγωγής. - Απόκρυψη κουμπιού «Αποθήκευση» - Απόκρυψη του κουμπιού σχολίων. - Απόκρυψη κουμπιού σχολίων - Απόκρυψη του κουμπιού λήψης. - Απόκρυψη κουμπιού «Λήψη» - Απόκρυψη των ονομασιών των κουμπιών ενεργειών. - Απόκρυψη ονομασιών κουμπιών ενέργειας - Απόκρυψη των κουμπιών «Μου αρέσει» και «Δεν μου αρέσει». Δεν λειτουργεί στην παλιά εμφάνιση της οθόνης αναπαραγωγής. - Απόκρυψη κουμπιών «Μου αρέσει» και «Δεν μου αρέσει» - Απόκρυψη του κουμπιού έναρξης ραδιοφώνου. - Απόκρυψη κουμπιού «Ραδιόφωνο» - Απόκρυψη του κουμπιού κοινοποίησης. - Απόκρυψη κουμπιού «Κοινοποίηση» - Απόκρυψη της εναλλαγής ήχου-βίντεο στην οθόνη αναπαραγωγής. - Απόκρυψη εναλλαγής ήχου-βίντεο - Απόκρυψη της ενότητας κουμπιών στη ροή. - Απόκρυψη ενότητας κουμπιών - Απόκρυψη ενότητας καρουζέλ στη ροή. - Απόκρυψη ενότητας καρουζέλ - Απόκρυψη του κουμπιού μετάδοσης. - Απόκρυψη κουμπιού μετάδοσης - Απόκρυψη της γραμμής κατηγοριών. - Απόκρυψη γραμμής κατηγοριών - Απόκρυψη των οδηγιών κοινότητας στην κορυφή της ενότητας σχολίων. - Απόκρυψη οδηγιών κοινότητας - Απόκρυψη των κουμπιών χρονοσήμανσης και επιλογής emoji κατά την πληκτρολόγηση σχολίου. - Απόκρυψη κουμπιών χρονοσήμανσης & emoji - Απόκρυψη του σκοτεινού φόντου που εμφανίζεται στην οθόνη αναπαραγωγής όταν γίνεται διπλό πάτημα για αναζήτηση. - Φόντο διπλού πατήματος της οθόνης αναπαραγωγής - Απόκρυψη του αιωρούμενου κουμπιού στην καρτέλα βιβλιοθήκης. - Απόκρυψη αιωρούμενου κουμπιού - Απόκρυψη στοιχείου 3 στηλών - Απόκρυψη μενού «Προσθήκη στην ουρά» - Απόκρυψη μενού «Υπότιτλοι» - Απόκρυψη μενού «Διαγραφή λίστας αναπαραγωγής» - Απόκρυψη μενού «Παράβλεψη ουράς» - Απόκρυψη μενού «Λήψη» - Απόκρυψη μενού «Επεξεργασία λίστας αναπαραγωγής» - Απόκρυψη μενού «Μετάβαση στο άλμπουμ» - Απόκρυψη μενού «Μετάβαση στον καλλιτέχνη» - Απόκρυψη μενού «Μετάβαση στο επεισόδιο» - Απόκρυψη μενού «Μετάβαση στο podcast» - Απόκρυψη μενού «Βοήθεια & σχόλια» - Απόκρυψη κουμπιών «Μου αρέσει» και «Δεν μου αρέσει» - Απόκρυψη μενού «Αναπαραγωγή μετά» - Απόκρυψη μενού «Ποιότητα» - Απόκρυψη μενού «Κατάργηση λίστας αναπαραγωγής από τη βιβλιοθήκη» - Απόκρυψη μενού «Κατάργηση από λίστα αναπαραγωγής» - Απόκρυψη μενού «Αναφορά» - Απόκρυψη μενού «Αποθήκευση επεισοδίου για αργότερα» - Απόκρυψη μενού «Αποθήκευση λίστας αναπαραγωγής στη Βιβλιοθήκη» - Απόκρυψη μενού «Αποθήκευση στη λίστα αναπαραγωγής» - Απόκρυψη μενού «Κοινοποίηση» - Απόκρυψη μενού «Τυχαία αναπαραγωγή» - Απόκρυψη μενού «Χρονόμετρο ύπνου» - Απόκρυψη μενού «Έναρξη ραδιοφώνου» - Απόκρυψη μενού «Στατιστικά για σπασίκλες» - Απόκρυψη των «Εγγραφή» / «Απεγγραφή» - Απόκρυψη μενού «Προβολή συντελεστών τραγουδιού» - Απόκρυψη των ενδιάμεσων διαφημίσεων πλήρους οθόνης. - Απόκρυψη διαφημίσεων πλήρους οθόνης - "Αν είναι ενεργοποιημένο, οι διαφημίσεις πλήρους οθόνης κλείνουν μέσω του κουμπιού κλεισίματος. -Αν είναι απενεργοποιημένο, οι διαφημίσεις πλήρους οθόνης είναι αυτόματα αποκλεισμένες. (μπορεί να υπάρχουν παρενέργειες)" - "Αν είναι ενεργοποιημένο, οι διαφημίσεις πλήρους οθόνης κλείνουν μέσω του κουμπιού κλεισίματος. -Αν είναι απενεργοποιημένο, οι διαφημίσεις πλήρους οθόνης είναι αυτόματα αποκλεισμένες. (μπορεί να υπάρχουν παρενέργειες)" - "Αν είναι ενεργοποιημένο, οι διαφημίσεις πλήρους οθόνης κλείνουν μέσω του κουμπιού κλεισίματος. -Αν είναι απενεργοποιημένο, οι διαφημίσεις πλήρους οθόνης είναι αυτόματα αποκλεισμένες. (μπορεί να υπάρχουν παρενέργειες)" - Κλείσιμο διαφημίσεων πλήρους οθόνης - Απόκρυψη του κουμπιού κοινοποίησης στην οθόνη αναπαραγωγής πλήρους οθόνης. - Απόκρυψη κουμπιού κοινοποίησης στη λειτουργία πλήρους οθόνης - Απόκρυψη των γενικών διαφημίσεων. - Απόκρυψη γενικών διαφημίσεων - Απόκρυψη του ψευδώνυμου στην εναλλαγή λογαριασμού. - Απόκρυψη ψευδώνυμου - Απόκρυψη του κουμπιού ιστορικού στη γραμμή εργαλείων. - Απόκρυψη κουμπιού ιστορικού - Απόκρυψη διαφημίσεων πριν την αναπαραγωγή κομματιού. - Απόκρυψη διαφημίσεων μουσικής - Απόκρυψη της γραμμής πλοήγησης. - Απόκρυψη γραμμής πλοήγησης - Απόκρυψη της καρτέλας «Εξερεύνηση». - Απόκρυψη κουμπιού «Εξερεύνηση» - Απόκρυψη της καρτέλας «Αρχική». - Απόκρυψη κουμπιού «Αρχική» - Απόκρυψη ονομασιών των κουμπιών στη γραμμή πλοήγησης. - Απόκρυψη ονομασιών γραμμής πλοήγησης - Απόκρυψη της καρτέλας «Βιβλιοθήκη». - Απόκρυψη κουμπιού «Βιβλιοθήκη» - Απόκρυψη της καρτέλας «Δείγματα». - Απόκρυψη κουμπιού «Δείγματα» - Απόκρυψη της καρτέλας «Αναβάθμιση». - Απόκρυψη κουμπιού «Αναβάθμιση» - Απόκρυψη του κουμπιού ειδοποιήσεων στη γραμμή εργαλείων. - Απόκρυψη κουμπιού ειδοποιήσεων - Απόκρυψη της ετικέτας προώθησης επί πληρωμή. - Απόκρυψη ετικέτας προώθησης επί πληρωμή - Απόκρυψη της ενότητας καρτών λίστας αναπαραγωγής στη ροή. - Απόκρυψη καρτών λίστας αναπαραγωγής - Απόκρυψη των αναδυόμενων παραθύρων προώθησης Premium. - Απόκρυψη παραθύρων προώθησης Premium - Απόκρυψη του διαφημιστικού ανανέωσης YT Premium. - Απόκρυψη διαφημιστικού ανανέωσης Premium - Απόκρυψη των ετικετών προειδοποίησης προώθησης. - Απόκρυψη ετικετών προειδοποίησης προώθησης - Απόκρυψη της ενότητας «Δείγματα» στη ροή. - Απόκρυψη ενότητας «Δείγματα» - Απόκρυψη μενού «Πληροφορίες για το YouTube Music» - Απόκρυψη μενού «Εξοικονόμηση δεδομένων» - Απόκρυψη μενού «Λήψεις & αποθηκευτικός χώρος» - Απόκρυψη μενού «Γενικά» - Απόκρυψη μενού «Ειδοποιήσεις» - Απόκρυψη μενού «Αποκτήστε το Music Premium» - Απόκρυψη μενού «Κέντρο οικογένειας» - Απόκρυψη μενού «Αναπαραγωγή» - Απόκρυψη μενού «Απόρρητο & δεδομένα» - Απόκρυψη μενού «Προτάσεις» - "Απόκρυψη στοιχείων του μενού ρυθμίσεων YouTube. -Αυτή η λειτουργία γίνεται να κρύψει και το μενού ρυθμίσεων ReVanced Extended." - Φιλτράρισμα του μενού ρυθμίσεων - Απόκρυψη του κουμπιού ηχητικής αναζήτησης στην γραμμή αναζήτησης. - Απόκρυψη κουμπιού ηχητικής αναζήτησης - Απόκρυψη του κουμπιού «Πατήστε για ενημέρωση». - Απόκρυψη κουμπιού «Πατήστε για ενημέρωση» - Απόκρυψη των στοιχείων απορρήτου / όρων και προϋποθέσεων. - Απόκρυψη στοιχείων απορρήτου & όρων - Απόκρυψη του κουμπιού φωνητικής αναζήτησης στην γραμμή αναζήτησης. - Απόκρυψη κουμπιού φωνητικής αναζήτησης - Λογαριασμός - Γραμμή ενεργειών - Διαφημίσεις - Αναδυόμενο μενού ρυθμίσεων - Γενικά - Διάφορα - Γραμμή πλοήγησης - Οθόνη αναπαραγωγής - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Μενού ρυθμίσεων - Βίντεο - Απομνημόνευση της τελευταίας ταχύτητας αναπαραγωγής που επιλέχθηκε. - Απομνημόνευση αλλαγών ταχύτητας αναπαραγωγής - Εμφάνιση μηνύματος στο κάτω μέρος της οθόνης κατά την αλλαγή προεπιλεγμένης ταχύτητας αναπαραγωγής. - Εμφάνιση μηνύματος - Η προεπιλεγμένη ταχύτητα άλλαξε σε %s. - Απομνημόνευση της κατάστασης του κουμπιού επανάληψης. - Απομνημόνευση κατάστασης επανάληψης - Απομνημόνευση της κατάστασης του ανακατέματος τραγουδιών. - Απομνημόνευση κατάστασης ανακατέματος - Απομνημόνευση της τελευταίας ποιότητας βίντεο που επιλέχθηκε. - Απομνημόνευση αλλαγών ποιότητας βίντεο - Εμφάνιση μηνύματος στο κάτω μέρος της οθόνης κατά την αλλαγή προεπιλεγμένης ποιότητας βίντεο. - Εμφάνιση μηνύματος - Η προεπιλεγμένη ποιότητα δεδομένων άλλαξε σε %s. - Αποτυχία ρύθμισης της ποιότητας. - Η προεπιλεγμένη ποιότητα με Wi-Fi άλλαξε σε %s. - "Αφαίρεση του παραθύρου προειδοποίησης ηλικιακού περιορισμού. -Αυτό δεν παρακάμπτει τον ηλικιακό περιορισμό, απλά τον αποδέχεται αυτόματα." - Αφαίρεση παραθύρου ηλικιακού περιορισμού - Αν πατήσετε το «Παρακολούθηση στο YouTube», η αναπαραγωγή θα συνεχιστεί από την τρέχουσα ώρα προβολής. - Συνέχιση παρακολούθησης - Αντικατάσταση του μενού «Παράβλεψη ουράς» με το μενού «Παρακολούθηση στο YouTube». - Αντικατάσταση μενού «Παράβλεψη ουράς» - Παρακολούθηση στο YouTube - Μη έγκυρη διεύθυνση URL βίντεο. - Το μενού αναφοράς στα σχόλια δε θα επηρεαστεί. - Διατήρηση του μενού «Αναφορά» στα σχόλια - Αντικατάσταση του μενού «Αναφορά» με το μενού «Ταχύτητα αναπαραγωγής». - Αντικατάσταση μενού «Αναφορά» - Επιστροφή του πίνακα αναδυόμενων σχολίων στο παλιό του στυλ. - Αναδυόμενα σχόλια παλιού στυλ - Επιστροφή του φόντου της οθόνης αναπαραγωγής στο παλιό στυλ. - Φόντο οθόνης αναπαραγωγής παλιού στυλ - "Επιστροφή της εμφάνισης της οθόνης αναπαραγωγής στο παλιό στυλ. -Κάποιες λειτουργίες ενδέχεται να μη λειτουργούν σωστά στην παλιά εμφάνιση της οθόνης αναπαραγωγής." - Οθόνη αναπαραγωγής παλιού στυλ - Επιστροφή της ενότητας βιβλιοθήκης στο παλιό στυλ. (Πειραματικό) - Ενότητα βιβλιοθήκης παλιού στυλ - \@ψευδώνυμο (Όνομα χρήστη) - Επιλογή της μορφής εμφάνισης ονόματος χρήστη. - Μορφή εμφάνισης - Όνομα χρήστη (@ψευδώνυμο) - Όνομα χρήστη - Εμφάνιση του ονόματος χρήστη αντί για το ψευδώνυμο στα σχόλια. - Επαναφορά ονομάτων χρήστη - "Για να γίνει αντικατάσταση του ψευδωνύμου με όνομα χρήστη, απαιτείται κλειδί προγραμματιστή YouTube Data API v3. - -Η ημερήσια ποσόστωση για τα κλειδιά API στο δωρεάν πακέτο είναι 10,000, και χρησιμοποιείται 1 ποσόστωση για την αντικατάσταση ψευδωνύμου με όνομα χρήστη για 1 σχόλιο. - -Πατήστε για να δείτε πώς να εκδώσετε ένα κλειδί API." - Σχετικά με το κλειδί YouTube Data API - Το κλειδί προγραμματιστή για τη χρήση του YouTube Data API v3. - Κλειδί YouTube Data API - 1. Μεταβείτε στη <a href=%1$s>δημιουργία νέου project</a>.<br>2. Πατήστε το κουμπί <b>CREATE</b>. <br>3. Μεταβείτε στην επιλογή <a href=%2$s>YouTube Data API v3</a>.<br>4. Πατήστε το κουμπί <b>ENABLE</b>.<br>5. Πατήστε το κουμπί <b>CREATE CREDENTIALS</b>.<br>6. Επιλέξτε την επιλογή <b>Public data</b>.<br>7. Πατήστε το κουμπί <b>NEXT</b>.<br>8. Αντιγράψτε το κλειδί API.<br><br>※ Το κλειδί API δεν πρέπει να το μοιράζεστε ποτέ με άλλους, οπότε δεν περιλαμβάνεται κατά την Εισαγωγή / Εξαγωγή ρυθμίσεων. - Έκδοση κλειδιού προγραμματιστή YouTube Data API v3 - Σχετικά με - Τα δεδομένα Dislike παρέχονται από το Return YouTube Dislike API. Πατήστε για να μάθετε περισσότερα. - ReturnYouTubeDislike.com - Απόκρυψη του διαχωριστικού του κουμπιού «Μου αρέσει». - Κουμπί «Μου αρέσει» μικρότερου στυλ - Αντί για τον αριθμό των dislike, θα εμφανίζεται το ποσοστό τους. - Εμφάνιση ως ποσοστό - Εμφάνιση της ποσότητας των «Δεν μου αρέσει» των βίντεο. - Επιστρέψτε το «Δεν μου αρέσει» στο YouTube - Εμφάνιση της εκτιμώμενης ποσότητας των «Μου αρέσει» των βίντεο. - Εμφάνιση εκτιμώμενων likes - Δεδομένα dislike μη διαθέσιμα (το όριο API έχει επιτευχθεί). - Δεδομένα dislike μη διαθέσιμα (κατάσταση %d). - Δεδομένα dislike προσωρινά μη διαθέσιμα (καθυστέρηση API). - Δεδομένα dislike μη διαθέσιμα (%s). - Να εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης αν το Return YouTube Dislike API δεν είναι διαθέσιμο. - Μήνυμα αν το API δεν είναι διαθέσιμο - Κρυμμένο - Αφαίρεση των παραμέτρων παρακολούθησης από τις διευθύνσεις URL κατά την κοινοποίηση συνδέσμων. - Καθαρισμός συνδέσμων κοινοποίησης - Σχετικά με - sponsor.ajay.app - Τα δεδομένα παρέχονται από το SponsorBlock API. Πατήστε για να μάθετε περισσότερα και να δείτε λήψεις για άλλες πλατφόρμες. - Αλλαγή διεύθυνσης API - Η διεύθυνση URL του API άλλαξε. - Η διεύθυνση URL του API δεν είναι έγκυρη. - Η διεύθυνση URL του API επαναφέρθηκε. - Η διεύθυνση που χρησιμοποιείται για επικοινωνία με τον διακομιστή του SponsorBlock. Μη το αλλάξετε αν δεν ξέρετε τι κάνετε. - Το χρώμα άλλαξε. - Χρώμα: - Μη έγκυρος κωδικός χρώματος. - Το χρώμα επαναφέρθηκε. - Αλλαγή συμπεριφοράς τμημάτων - Ενεργοποίηση του SponsorBlock - Το SponsorBlock είναι ένα σύστημα που προέρχεται από το κοινό για παράβλεψη ενοχλητικών τμημάτων σε βίντεο YouTube. - Επαναφορά χρώματος - Εφαπτομενικές Σκηνές / Αστεία - Παρεμβατικές σκηνές που προστίθενται μόνο για γέμισμα ή χιούμορ και δεν είναι απαραίτητες για την κατανόηση του κύριου περιεχομένου του βίντεο. Δεν περιλαμβάνει τμήματα που παρέχουν πλαίσιο ή λεπτομέρειες υποβάθρου. - Υπενθύμιση αλληλεπίδρασης (Εγγραφή) - Όταν υπάρχει μια σύντομη υπενθύμιση για να προσθέσετε το βίντεο στα βίντεο που σας αρέσουν, να εγγραφείτε ή να τους ακολουθήσετε στην μέση του περιεχομένου. Αν είναι μεγάλο ή αφορά κάτι συγκεκριμένο, θα πρέπει να είναι στην κατηγορία αυτοπροώθησης. - Διάλειμμα / Εισαγωγή - Χρονικό διάστημα χωρίς πραγματικό περιεχόμενο. Θα μπορούσε να είναι μια παύση, ένα στατικό καρέ ή μια επαναλαμβανόμενη κίνηση. Δεν περιλαμβάνει μεταβάσεις που περιέχουν πληροφορίες. - Μουσική: Τμήμα χωρίς μουσική - Μόνο για χρήση σε βίντεο μουσικής. Τμήματα χωρίς μουσική σε βίντεο μουσικής, που δεν καλύπτονται ήδη από άλλη κατηγορία. - Τελική Οθόνη / Συντελεστές - Όταν εμφανίζονται οι συντελεστές ή τα προτεινόμενα βίντεο των καναλιών. Όχι για επίλογους που περιέχουν πληροφορίες. - Προεπισκόπηση / Περίληψη - Συλλογή από κλιπ που δείχνουν τι έρχεται ή τι συνέβη στο βίντεο ή σε άλλα βίντεο μιας σειράς, όπου όλες οι πληροφορίες επαναλαμβάνονται αλλού. - Αφιλοκέρδεια / Αυτοπροώθηση - Παρόμοιο με το «Χορηγός» αλλά για μη κερδοσκοπικό σκοπό ή για προσωπική προώθηση. Περιλαμβάνει τμήματα σχετικά με εμπορεύματα, δωρεές ή πληροφορίες για το με ποιους συνεργάστηκαν. - Χορηγός - Προώθηση επί πληρωμή, παραπομπές επί πληρωμή και άμεσες διαφημίσεις. Όχι για αυτοπροώθηση ή δωρεάν αναφορές σε σκοπούς / δημιουργούς / ιστοσελίδες / προϊόντα που τους αρέσουν. - Αυτόματη παράλειψη - Απενεργοποίηση - Παραλείφθηκε η σπατάλη χρόνου. - Παραλείφθηκε η ενοχλητική υπενθύμιση. - Παραλείφθηκε η εισαγωγή. - Παραλείφθηκε η διακοπή. - Παραλείφθηκε η διακοπή. - Παραλείφθηκαν πολλαπλά τμήματα. - Παραλείφθηκε τμήμα χωρίς μουσική. - Παραλείφθηκε ο επίλογος. - Παραλείφθηκε η προεπισκόπηση. - Παραλείφθηκε η ανακεφαλαίωση. - Παραλείφθηκε η προεπισκόπηση. - Παραλείφθηκε η αυτοπροώθηση. - Παραλείφθηκε ο χορηγός. - SponsorBlock προσωρινά μη διαθέσιμο. - SponsorBlock προσωρινά μη διαθέσιμο (κατάσταση %d). - SponsorBlock προσωρινά μη διαθέσιμο (καθυστέρηση API). - Μήνυμα αν το API δεν είναι διαθέσιμο - Να εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης αν το SponsorBlock API δεν είναι διαθέσιμο. - Εμφάνιση μηνύματος κατά την αυτόματη παράλειψη - Να εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης όταν ένα τμήμα παραλείπεται αυτόματα. - Οι ρυθμίσεις αντιγράφηκαν στο πρόχειρο. - "Παραποίηση έκδοσης εφαρμογής σε παλιότερη έκδοση. - -• Αυτό θα αλλάξει την εμφάνιση της εφαρμογής, αλλά πιθανότατα να προκύψουν άγνωστα θέματα. -• Αν αργότερα γίνει απενεργοποίηση, η παλιά εμφάνιση μπορεί να παραμείνει μέχρι να διαγραφούν τα δεδομένα της εφαρμογής." - 4.27.53 - Απενεργοποίηση λειτουργίας ραδιοφώνου σε περιοχές του Καναδά - 6.11.52 - Απενεργοποίηση στίχων σε πραγματικό χρόνο - 7.16.53 - Επαναφορά παλιάς γραμμής ενεργειών - Επιλέξτε την έκδοση εφαρμογής που θα χρησιμοποιηθεί. - Έκδοση της εφαρμογής που θα χρησιμοποιηθεί - Παραποίηση έκδοσης εφαρμογής - diff --git a/src/main/resources/music/translations/es-rES/missing_strings.xml b/src/main/resources/music/translations/es-rES/missing_strings.xml deleted file mode 100644 index 391f0ac24..000000000 --- a/src/main/resources/music/translations/es-rES/missing_strings.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - Don\'t show again - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - Fullscreen ads are blocked. (there may be side effects) - Fullscreen ads are closed through the Close button. - diff --git a/src/main/resources/music/translations/es-rES/strings.xml b/src/main/resources/music/translations/es-rES/strings.xml deleted file mode 100644 index c4ef8537e..000000000 --- a/src/main/resources/music/translations/es-rES/strings.xml +++ /dev/null @@ -1,399 +0,0 @@ - - - Continuar - "GmsCore no tiene permiso para ejecutarse en segundo plano. - -Sigue la guía \"Don't kill my app!\" para tu dispositivo y aplica las instrucciones a tu instalación de GmsCore. - -Esto es necesario para que la aplicación funcione." - "Las optimizaciones de la batería para GmsCore deben estar desactivadas para evitar problemas. - -Pulsa el botón de continuar y desactiva las optimizaciones de la batería." - Abrir página Web - Acción necesaria - Activa los ajustes de mensajería en la nube para recibir notificaciones. - Abrir GmsCore - GmsCore no está instalado. Instálalo. - Reemplaza el dominio que está bloqueado en algunas regiones para que las miniaturas de la lista de reproducción, avatares de canales, etc. puedan ser recibidas. - Eludir las restricciones regionales de imágenes - Cambia la hoja de compartir en la app a la hoja de compartir del sistema. - Cambiar la hoja de compartir - Ranking - Explorar - Inicio - Biblioteca - Suscripciones - Seleccione en qué página se abre la aplicación. - Cambiar página de inicio - Filtra los nombres de los componentes, separados por líneas. - Editar filtro personalizado - Habilita el filtro personalizado para ocultar los componentes de diseño. - Activar filtro personalizado - Filtro personalizado no válido: %s. - Las velocidades de reproducción personalizadas no son válidas. Restablezca a los valores predeterminados. - Velocidades de reproducción personalizadas no válidas. Utilizando valores predeterminados. - Agregar o cambiar las velocidades de reproducción disponibles. - Editar velocidades de reproducción personalizadas - Para abrir los enlaces de YouTube Music en RVX Music, activa \'Abrir enlaces soportados\' y activa las direcciones web soportadas. - Abrir ajustes predeterminados de la app - Desactiva la activación automática de los subtítulos forzados en el reproductor de vídeo. - Desactivar subtítulos automáticos - Deshabilita la animación de bienvenida \"Cairo\" cuando se inicia la aplicación. - Desactiva la animación Cairo - Deshabilita la redirección a la siguiente pista al hacer clic en el botón No me Gusta. - Desactivar redirección de No me Gusta - Desactivar el gesto de deslizar para cambiar de pista en el minireproductor. - Desactivar gesto de minireproductor - Desactivar el gesto de deslizar para cambiar de pista en el reproductor. - Desactivar gesto del reproductor - Establece el color de la barra de navegación en negro. - Activar barra de navegación negra - Cambia el color de fondo del reproductor a negro. - Activar fondo de reproductor negro - Hace coincidir el color del reproductor a pantalla completa con el de minimizado. - Activar coincidencia de color de reproductores - "Activa el diálogo compacto en el teléfono. - -Problemas conocidos: -- La carátula del álbum en la estantería de la biblioteca también se hace más pequeña. -- El diseño del temporizador puede parecer inusual." - Activar diálogo compacto - Incluye el búfer en el registro de depuración. - Incluir búfer en registro de depuración - Imprime el registro de depuración. - Activar registro de depuración - Mantiene el reproductor permanentemente minimizado incluso si se reproduce otra pista. - Activar reproductor minimizado forzado - Permite entrar en modo horizontal mediante la rotación de la pantalla del teléfono. - Activar modo horizontal - Añadir botón siguiente pista al minireproductor. - Añadir botón siguiente al minireproductor - Añadir botón pista anterior al minireproductor. - Añadir botón anterior al minireproductor - "Activa el códec Opus 250/251 al reproducir audio." - Activar códec opus - Permite deslizar hacia abajo para descartar el minireproductor. - Activar deslizar para descartar el minireproductor - "Añade un interruptor para recortar silencios en el menú desplegable de velocidad de reproducción. - -Información: -Esta función es para podcasts. -Esta función aún está en desarrollo, por lo que puede ser inestable." - Añadir interruptor para recortar silencios - También activa el modo Zen para podcasts. - Activar el modo Zen en podcasts - Añade un tinte gris al reproductor de vídeo para reducir la fatiga visual. - Activar modo zen - Restablecer a valores por defecto. - Reiniciar para cargar el diseño normalmente - Actualizar y reiniciar - Exportar ajustes a archivo - Error al exportar los ajustes. - Los ajustes se han exportado correctamente. - Importar - Importar ajustes desde archivo - Copiar - Importar o exportar ajustes como texto - Importar o exportar ajustes como texto. - Importar / Exportar - Error de importación: %s - La configuración se restableció a los valores predeterminados. - Configuración importada de %d. - Restablecer - ReVanced Extended - "El botón Descargar abre su descargador externo. - - • Solo anula el botón de acción Descargar en el reproductor. - • No anula el botón Descargar en el menú desplegable o en la pestaña Biblioteca." - Reemplazar botón de acción de Descarga - Descargador externo - "%1$s no está instalado. -Descarga %2$s desde el sitio web." - Advertencia - %s no está instalado. Por favor, instálelo. - Nombre del paquete de su aplicación de descargas externas instalada, como NewPipe o YTDLnis. - Nombre del paquete del descargador externo - Oculta componentes vacíos en el menú de la cuenta. - Ocultar componente vacío - Lista de nombres del menú de la cuenta a filtrar separados por una nueva línea. - Filtro de menú de cuenta - Oculta los elementos del menú de la cuenta usando el filtro personalizado. - Ocultar menú de cuenta - Oculta botón Guardar. - Ocultar botón Guardar - Oculta botón de comentarios. - Ocultar botón de comentarios - Oculta el botón Descargar. - Ocultar botón Descargar - Oculta las etiquetas de los botones de acción. - Ocultar etiquetas de botón de acción - Oculta los botones \"Me gusta\" y \"no me gusta\". No funciona en el diseño del reproductor antiguo. - Ocultar botones Me gusta y No me gusta - Oculta el botón Radio. - Ocultar botón de emisoras de radio - Oculta el botón Compartir. - Ocultar botón de compartir - Oculta el interruptor de Audio / Video en el reproductor. - Ocultar Interruptor de Audio / Video - Oculta el estante de botones de la página de inicio y del explorador. - Ocultar estante de botones - Oculta el estante de carrusel de la página de inicio y del explorador. - Ocultar estante de carrusel - Oculta el botón de trasmisión en la parte superior de la página de inicio y en la parte superior del reproductor. - Ocultar botón de transmisión - Oculta la barra de categorías musicales de la parte superior de la página de inicio. - Ocultar barra de categorías - Oculta las normas del canal en la parte superior de la sección de comentarios. - Ocultar normas del canal - Oculta los botones marca de tiempo y emoji al escribir comentarios. - Ocultar botones de marca de tiempo y emoji - Oculta la superposición oscura que aparece al tocar dos veces para buscar. - Oculta la capa que aparece al tocar dos veces - Oculta el botón flotante en la pestaña Biblioteca. - Ocultar botón flotante - Ocultar componente de 3 columnas - Ocultar menú de Añadir a la cola - Ocultar menú de Subtítulos - Ocultar menú Borrar lista de reproducción - Ocultar menú de Descartar cola - Ocultar menú de Descarga - Ocultar menú Editar lista de reproducción - Ocultar menú de ir al álbum - Ocultar menú de ir al artista - Ocultar menú de ir a episodios - Ocultar menú de ir al podcast - Ocultar menú Ayuda & Comentarios - Ocultar botones Me gusta y No me gusta - Ocultar menú de reproducción siguiente - Ocultar menú de calidad - Ocultar menú de eliminar de la biblioteca - Ocultar menú de quitar de la lista de reproducción - Ocultar menú Denunciar - Ocultar menú de Guardar episodio para más tarde - Ocultar menú de Guardar en biblioteca - Ocultar menú de Guardar en lista de reproducción - Ocultar menú de Compartir - Ocultar menú de Reproducción aleatoria - Ocultar menú de Temporizador de sueño - Ocultar menú de Iniciar radio - Ocultar menú Estadísticas para Nerds - Ocultar menú Suscribirse / Desuscribirse - Ocultar menú de vista de créditos de canción - Oculta anuncios en pantalla completa. - Ocultar anuncios en pantalla completa - "Si está habilitado, los anuncios a pantalla completa se cierran mediante el botón Cerrar. -Si está deshabilitado, se bloquean los anuncios a pantalla completa. (puede haber efectos secundarios)" - Cerrar anuncios en pantalla completa - Oculta el botón Compartir en el reproductor de pantalla completa. - Ocultar el botón Compartir en pantalla completa - Oculta anuncios generales. - Ocultar anuncios generales - Oculta el asa en el conmutador de cuenta. - Ocultar asa - Oculta el botón de historial en la barra de herramientas. - Ocultar botón de historial - Oculta los anuncios antes de reproducir una pista. - Ocultar anuncios de música - Oculta barra de navegación. - Ocultar barra de navegación - Oculta el botón Explorar. - Ocultar botón de Explorar - Oculta el botón de Inicio. - Ocultar botón de Inicio - Oculta las etiquetas en la barra de navegación. - Ocultar etiquetas en barra de navegación - Oculta el botón de la biblioteca. - Ocultar botón de Biblioteca - Oculta el botón de Samples. - Ocultar botón de Samples - Oculta el botón de Actualización. - Ocultar botón de Actualización - Oculta el botón de notificaciones en la barra de herramientas. - Ocultar botón de Notificaciones - Oculta etiqueta de promoción pagada. - Ocultar etiqueta de promoción pagada - Oculta la tarjeta de lista de reproducción del feed. - Ocultar tarjeta de lista de reproducción - Oculta popups de promoción premium. - Ocultar popups de promoción premium - Oculta banner de renovación premium. - Ocultar banner de renovación premium - Oculta el banner de alerta de promoción. - Ocultar banner de alerta de promoción - Oculta estante de Samples en el feed. - Ocultar estante de Samples - Ocultar menú Acerca de - Ocultar menú de ahorro de datos - Ocultar Descargas & menú de almacenamiento - Ocultar menú general - Ocultar menú de notificaciones - Ocultar menú premium de Get Music - Ocultar menú de Centro Familiar - Ocultar menú de reproducción - Ocultar menú privacidad de & datos - Ocultar menú de recomendaciones - "Oculta elementos del menú de configuración. -Esto oculta no solo el menú de ajustes de YT Music, sino también el menú de ajustes de ReVanced Extended." - Ocultar menú de configuración - Oculta el botón de búsqueda de sonido en la barra de búsqueda. - Ocultar botón de búsqueda de sonido - Oculta el botón Toque para actualizar. - Ocultar el botón Toque para actualizar - Oculta los términos del contenedor de servicio. - Ocultar contenedor de términos - Oculta el botón de búsqueda por voz en la barra de búsqueda. - Ocultar botón de búsqueda por voz - Cuenta - Barra de Acción - Anuncios - Menú desplegable - General - Otros - Barra de navegación - Reproductor - Devolver usuario de YouTube - Return YouTube Dislike - SponsorBlock - Menú de ajustes - Video - Recuerda la última velocidad de reproducción seleccionada. - Recordar cambios de velocidad de reproducción - Mostrar un mensaje al cambiar la velocidad de reproducción predeterminada. - Mostrar un mensaje - Cambiando la velocidad predeterminada a %s. - Recuerda el estado de la repetición. - Recordar estado de repetición - Recuerda el estado del aleatorio (shuffle). - Recordar estado aleatorio - Recuerda la última calidad de vídeo seleccionada. - Recordar cambios de calidad de vídeo - Mostrar un mensaje al cambiar la calidad de vídeo por defecto. - Mostrar un mensaje - Cambiando la calidad predeterminada con datos móviles a %s. - Error al establecer calidad. - Cambiando la calidad predeterminada con Wi-Fi a %s. - "Elimina el diálogo de discreción del espectador. -Esto no evita la restricción de edad. Solo la acepta automáticamente." - Eliminar diálogo de discreción del espectador - Continúa el vídeo desde el tiempo actual cuando se cambia a YouTube. - Continuar viendo - Reemplaza el menú de descartar cola por el de ver en YouTube. - Reemplazar el menú descartar cola - Ver en YouTube - Url del video no válida. - Mantiene intacto el menú Denunciar en la sección de comentarios. - Mantener Denunciar en comentarios - Reemplaza el menú Denunciar con el menú Velocidad de reproducción. - Reemplazar menú Denunciar - Devuelve los paneles emergentes de comentarios al estilo antiguo. - Restaurar paneles emergentes de comentarios antiguos - Devuelve el fondo del reproductor al estilo antiguo. - Restaurar el fondo del reproductor antiguo - "Devuelve el diseño del reproductor al estilo antiguo. -Algunas características pueden no funcionar correctamente en la disposición del reproductor antiguo." - Activar diseño antiguo del reproductor - Devuelve la pestaña Biblioteca al estilo antiguo. (Experimental) - Restaurar el estante de la biblioteca de estilo antiguo - \@identificador (Nombre de usuario) - Seleccione el formato para mostrar el nombre de usuario. - Formato de visualización - Nombre de usuario (@identificador) - Nombre de usuario - Reemplaza identificadores con nombres de usuario en los comentarios. - Activa devolver nombre de usuario de YouTube - "Se requiere la clave de desarrollador de la API v3 de datos de YouTube para reemplazar el identificador con el nombre de usuario. - -La cuota diaria para las claves API en el plan gratuito es de 10,000, y se utiliza 1 cuota para reemplazar el identificador con el nombre de usuario en 1 comentario. - -Toca para ver cómo crear una clave de API." - Acerca de la clave API de datos de YouTube - La clave de desarrollador para utilizar la API v3 de datos de YouTube. - Clave API de datos de YouTube - 1. Ve a <a href=%1$s>Crear un nuevo proyecto</a>.<br>2. Pulsa en el botón <b>CREAR</b>.<br>3. 3. Ve a <a href=%2$s>API v3 de datos de YouTube</a>.<br>4. Pulsa en el botón <b>HABILITAR</b>.<br>5. Pulsa en <b>CREAR</b>. Pulsa en el botón <b>CREAR CREDENCIALES</b>.<br>6. Selecciona la opción <b>Datos públicos</b>.<br>7. Pulsa en el botón <b> SIGUIENTE</b>.<br>8. Copia la clave API.<br><br>※ La clave API nunca debe ser compartida con otros, por lo que no se incluye en los ajustes de Importar / Exportar. - Crear clave de desarrollador API v3 de datos de YouTube - Acerca de - Los datos son proporcionados por la API Return YouTube Dislike. Pulse aquí para obtener más información. - ReturnYouTubeDislike.com - Oculta el separador del botón Me gusta. - Botón Me Gusta compacto - En lugar del número de no me gusta, se muestra el porcentaje de no me gusta. - Porcentaje de No Me Gusta - Muestra el número de vídeos que no te gustan. - Activar Return YouTube Dislike - Muestra el recuento estimado de \"me gusta\" de los vídeos. - Mostrar \"Me gusta\" estimados - Los No Me Gusta no están disponibles (se alcanzó el límite de la API del cliente). - Los no me gusta no están disponibles (estado %d). - Los no me gusta están temporalmente no disponibles (la API no responde). - Los no me gusta no están disponibles (%s). - Se muestra el mensaje si la API de ReturnYouTubeDislike no está disponible. - Mostrar mensaje si la API no está disponible - Oculto - Elimina los parámetros de consulta de seguimiento de las URL al compartir enlaces. - Desinfectar enlaces compartidos - Acerca de - sponsor.ajay.app - Los datos son proporcionados por la API de SponsorBlock. Pulsa aquí para aprender más y ver las descargas para otras plataformas. - Cambiar URL de la API - URL de API cambiada. - La URL de la API no es válida. - Restablecer la URL de la API. - Dirección que el SponsorBlock utiliza para hacer llamadas al servidor. No cambie esto a menos que sepa qué está haciendo. - Color cambiado. - Color: - Código de color inválido. Restablecimiento de color predeterminado. - Restablecer color. - Cambiar el comportamiento del segmento - Activar SponsorBlock - SponsorBlock es un sistema colaborativo para omitir partes molestas en vídeos de YouTube. - Restablecer color - Tangente de relleno / Chistes - Escenas tangenciales añadidas solo para relleno o humor que no son necesarias para entender el contenido principal del vídeo. No incluye segmentos que proporcionen contexto o detalles de fondo. - Recordatorio de interacción (Suscribirse) - Un breve recordatorio para dar me gusta, suscribirse o seguirlos en medio del contenido. Si es largo o sobre algo específico, debe estar en la sección de autopromoción. - Intermedio / Animación de introducción - Un intervalo sin contenido real. Puede ser una pausa, un fotograma estático o una animación que se repite. No incluye transiciones que contengan información. - Música: Sección sin música - Solo para usar en vídeos musicales. Secciones de vídeos musicales sin música, que no estén ya cubiertas por otra categoría. - Tarjetas finales / Créditos - Créditos o cuando aparecen las tarjetas finales de YouTube. No para conclusiones con información. - Vista previa / Resumen / Gancho - Colección de clips que muestran lo que está por venir o lo que sucedió en el vídeo o en otros vídeos de una serie, donde toda la información se repite en otra parte. - Promoción no remunerada/autopromoción - Cuando hay una autopromoción o no remunerada. Esto incluye secciones específicas sobre mercancía, donaciones o información sobre con quién colaboraron. - Patrocinador - Promoción pagada, referencias pagadas y anuncios directos. No es para promoción propia ni para menciones gratuitas a causas, creadores, sitios web o productos que les gusten. - Omitir automáticamente - Deshabilitar - Relleno omitido. - Recordatorio molesto omitido. - Introducción omitida. - Intermisión omitida. - Intermisión omitida. - Varios segmentos omitidos. - Se omitió una sección sin música. - Créditos omitidos. - Vista previa omitida. - Resumen omitido. - Vista previa omitida. - Autopromoción omitida. - Patrocinador omitido. - SponsorBlock no está disponible temporalmente. - SponsorBlock no está disponible temporalmente. (estado %d). - SponsorBlock no está disponible temporalmente. (la API no responde). - Mostrar mensaje si la API no está disponible - Muestra un mensaje si la API de SponsorBlock no está disponible. - Mostrar mensaje al omitir segmento automáticamente - Mensaje emergente que se muestra cuando se salta un segmento automáticamente. - Ajustes copiados en el portapapeles. - "Modificación de la versión del cliente a la versión antigua. - -- Esto cambiará la apariencia de la aplicación, pero pueden producirse efectos secundarios desconocidos. -- Si más tarde se desactiva, la antigua interfaz de usuario puede permanecer hasta que se borren los datos de la aplicación." - 4.27.53 - Desactivar el modo radio en las regiones canadienses - 6.11.52 - Desactivar letras en tiempo real - 7.16.53 - Restaurar la antigua barra de acción - Seleccione el objetivo de la versión de la app a modificar. - Objetivo de la versión de la app a modificar - Modificar versión de aplicación - diff --git a/src/main/resources/music/translations/fr-rFR/missing_strings.xml b/src/main/resources/music/translations/fr-rFR/missing_strings.xml deleted file mode 100644 index b7e1e75f7..000000000 --- a/src/main/resources/music/translations/fr-rFR/missing_strings.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - Don\'t show again - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - Return YouTube Username - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Username - Replaces handles with usernames in comments. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - Shows the estimated like count of videos. - Show estimated likes - Hidden - diff --git a/src/main/resources/music/translations/fr-rFR/strings.xml b/src/main/resources/music/translations/fr-rFR/strings.xml deleted file mode 100644 index a9792e7a2..000000000 --- a/src/main/resources/music/translations/fr-rFR/strings.xml +++ /dev/null @@ -1,386 +0,0 @@ - - - Continuer - "GmsCore n'a pas les permissions pour fonctionner en arrière-plan. - -Suivez le guide \"Don't kill my app!\" pour votre appareil, et appliquez les instructions sur GmsCore. - -Requis pour que l'application fonctionne." - "L'optimisation de la batterie de GmsCore doit être désactivé pour éviter tout problème. - -Cliquez sur le bouton Continuer et désactivez les optimisations de la batterie." - Ouvrir le site web - Action requise - Activez la messagerie cloud pour recevoir les notifications. - Ouvrir GmsCore - GmsCore n\'est pas installé. Veuillez l\'installer. - Remplace le domaine qui est bloqué dans certaines régions afin que les miniatures des listes de lecture, les avatars des chaînes, etc. puissent être reçus. - Contourner les restrictions d\'image selon les régions - Remplace la fiche de partage de l\'appli par celui du système. - Modifier la fiche de partage - Charts - Explorer - Accueil - Bibliothèque - Abonnements - Sélectionnez la page de démarrage de l\'appli. - Modifier la page de démarrage - Filtrer la liste des noms du composant séparés par un saut de ligne. - Filtre personnalisé - Active les filtres personnalisés pour masquer des éléments de l’interface. - Activer le filtre personnalisé - Filtre personnalisé invalide : %s. - Les vitesses personnalisées doivent être inférieures à %sx. - Vitesses de lecture invalides. - Ajoute ou modifie les vitesses de lecture disponibles. - Éditez les vitesses de lecture personnalisées - Pour ouvrir les liens YouTube Music dans RVX Music, activez \'Ouvrir les liens compatibles\' et activez les adresses web prises en charge. - Ouvrir les paramètres par défaut de l\'application - Désactive les sous-titres automatiquement activés. - Désactiver les sous-titres forcés - Désactive l\'animation Cairo lors du démarrage de l\'application. - Désactiver l\'animation Cairo au démarrage - Désactive le passage à la piste suivante lorsque vous cliquez sur le bouton \"Je n\'aime pas\". - Désactiver la redirection du bouton \"Je n\'aime pas\" - Désactive les gestes pour changer de musique dans le minilecteur. - Désactiver les gestes du minilecteur - Désactive les gestes pour changer de musique dans le lecteur. - Désactiver les gestes du lecteur - Modifie la couleur de la barre de navigation en noir. - Activer la barre de navigation en noir - Change la couleur de l\'interface du lecteur en noir. - Activer l\'interface du lecteur en noir - Harmonise les couleurs du minilecteur à celle du lecteur en plein écran. - Activer l\'harmonisation des couleurs du lecteur - "Active le menu déroulant compact sur téléphones. - -Limitations : -• Les pochettes d'albums de la bibliothèque deviennent petites si organisées en mode grille. -• La mise en page du délai de mise en veille peut être inhabituelle." - Activer la boîte de dialogue compacte - Ajoute les informations sur la mémoire tampon dans le journal de débogage. - Activer les informations sur la mémoire tampon dans le journal de débogage - Enregistrer le journal de débogage. - Activer le journal de débogage - Maintient le lecteur minimisé même si une autre piste est lue. - Activer la minimisation forcée du lecteur - Active le mode paysage lors de la rotation du téléphone. - Activer le mode paysage - Ajoute le bouton \"Suivant\" sur le minilecteur. - Ajouter le bouton \"Suivant\" sur le minilecteur - Ajoute le bouton \"Précédent\" sur le minilecteur. - Ajouter le bouton \"Précédent\" sur le minilecteur - "Active le codec OPUS si la réponse du lecteur inclut le codec OPUS. - -Info : -• Les dernières versions de YouTube Music utilisent par défaut le codec audio Opus. -• Disponible uniquement pour les utilisateurs qui falsifient une très ancienne version du client." - Activer le Codec OPUS - Active le geste vers le bas pour fermer le minilecteur. - Activer le geste pour fermer le minilecteur - "Ajoute \"Masquer les silences\" dans le menu \"Vitesse de lecture\" du menu déroulant. - -Info : -• Cette fonctionnalité est destinée aux podcasts. -• Cette fonctionnalité est encore en développement, elle peut donc être instable." - Ajouter une option \"Masquer les silences\" - Active également le mode \"Zen\" pour les Podcasts. - Activer le mode \"Zen\" sur les Podcasts - Change la couleur du lecteur par un voile gris pour réduire la fatigue oculaire. - Activer le mode zen - Réinitialiser les valeurs par défaut. - Redémarrer pour charger l\'interface correctement - Appliquer et redémarrer ? - Exporter les paramètres vers un fichier - Échec de l\'exportation des paramètres. - Les paramètres ont été exportés avec succès. - Importer - Importer les paramètres depuis un fichier - Copier - Importer / Exporter les paramètres sous forme de texte - Importe ou exporte les paramètres. - Importer / Exporter les paramètres - Importation échouée : %s. - Les paramètres ont étés réinitialisés. - %d paramètres ont étés importés. - Réinitialiser - ReVanced Extended - "Le bouton \"Télécharger\" ouvre votre téléchargeur externe. - -• Remplace uniquement l’action du bouton \"Télécharger\" du lecteur. -• Ne remplace pas le bouton de téléchargement dans le menu déroulant ou la bibliothèque." - Remplacer l\'action du bouton \"Télécharger\" - Téléchargeur externe - "%1$s n'est pas installé. -Veuillez télécharger %2$s à partir du site web." - Attention - %s n\'est pas installé. Veuillez l’installer. - Nom de package du téléchargeur externe installé, telle que NewPipe ou YTDLnis. - Nom du paquet du téléchargeur externe - Masque les catégories vides dans le menu du compte. - Masquer les catégories vides - Liste de noms du menu de compte à filtrer, séparés par un saut de ligne. - Filtre du menu du compte - Masque les éléments dans le menu du compte à l\'aide du filtre personnalisé. - Masquer le menu du compte - Masque le bouton \"Enregistrer\". - Masquer le bouton \"Enregistrer\" - Masque le bouton \"Commentaires\". - Masquer le bouton \"Commentaires\" - Masque le bouton \"Télécharger\". - Masquer le bouton \"Télécharger\" - Masque les noms de la barre d’action. - Masquer les noms de la barre d’action - Masque les boutons \"J\'aime\" et \"Je n\'aime pas\". Ne fonctionne pas sur l\'ancienne interface du lecteur. - Masquer les boutons \"J\'aime\" et \"Je n\'aime pas\" - Masque le bouton \"Démarrer la radio\". - Masquer le bouton \"Radio\" - Masque le bouton \"Partager\". - Masquer le bouton \"Partager\" - Masque le sélecteur Audio/Vidéo en haut du lecteur. - Masquer le sélecteur Audio/Vidéo - Masque les étagères à boutons dans les flux. - Masquer les étagères de boutons - Masque les étagères à suggestions dans les flux. - Masquer les étagères à suggestions - Masque le Bouton \"Caster\". - Masquer le bouton \"Caster\" - Masque la barre de catégorie. - Masquer la barre de catégories - Masque les règles de la chaîne en haut de la section des commentaires. - Masquer les règles de la chaîne - Masque les boutons \"émoji\" et \"horodatage\" lors de la rédaction d\'un commentaire. - Masquer les boutons émoji et horodatage - Masque le voile sombre qui apparaît lors du double appui pour avancer. - Masquer le voile sombre lors du double appuie - Masque les boutons flottants dans la bibliothèque. - Masquer les boutons flottants - Masquer le composant à 3 colonnes - Masquer le bouton \"Ajouter à la file d\'attente\" - Masquer le menu \"Sous-titres\" - Masquer le menu \"Supprimer la playlist\" - Masquer le menu \"Supprimer la file d\'attente\" - Masquer le menu \"Télécharger\" - Masquer le menu \"Modifier la playlist\" - Masquer le menu \"Accéder à l\'album\" - Masquer le menu \"Accéder à la page de l\'artiste\" - Masquer le menu \"Accéder à l\'épisode\" - Masquer le menu \"Accéder au podcast\" - Masquer le menu \"Aide et commentaires\" - Masquer les boutons \"J\'aime\" et \"Je n\'aime pas\" - Masquer le menu \"Lire ensuite\" - Masquer le menu \"Qualité\" - Masquer le menu \"Retirer de la Bibliothèque\" - Masquer le menu \"Supprimer la playlist\" - Masquer le menu \"Signaler\" - Masquer le menu \"Enregistrer l\'épisode pour plus tard\" - Masquer le menu \"Enregistrer dans la bibliothèque\" - Masquer le menu \"Enregistrer dans une playlist\" - Masquer le menu \"Partager\" - Masquer le bouton \"Lecture aléatoire\" - Masquer le menu \"Délai de mise en veille\" - Masquer le menu \"Lancer la radio\" - Masquer le menu \"Statistiques avancées\" - Masquer le menu \"S\'abonner\" / \"Se désabonner\" - Masquer le menu \"Afficher les crédits du titre\" - Masque les publicités en plein écran. - Masquer les publicités en plein écran - "Si activé, les publicités en plein écran seront fermées grâce au bouton \"Fermer\". -Si désactivé, Les publicités en plein écran seront bloquées. (peut avoir des effets secondaires)" - "Si activé, les publicités en plein écran seront fermées grâce au bouton \"Fermer\". -Si désactivé, Les publicités en plein écran seront bloquées. (peut avoir des effets secondaires)" - "Si activé, les publicités en plein écran seront fermées grâce au bouton \"Fermer\". -Si désactivé, Les publicités en plein écran seront bloquées. (peut avoir des effets secondaires)" - Fermer les publicités en plein écran - Masque le bouton \"Partager\" sur le lecteur en plein écran. - Masquer le bouton \"Partager\" en plein écran - Masque les publicités générales. - Masquer les publicités générales - Masque l\'identifiant dans le menu \"compte\". - Masquer l\'identifiant - Masque le bouton \"Historique\" de la barre d\'outils. - Masquer le bouton \"Historique\" - Masque les publicités avant la lecture d\'une musique. - Masquer les publicités musicales - Masque la barre de navigation. - Masquer la barre de navigation - Masque le bouton \"Explorer\". - Masquer le bouton \"Explorer\" - Masque le bouton \"Accueil\". - Masquer le bouton \"Accueil\" - Masque le nom sous les boutons de la barre de navigation. - Masquer les noms dans barre de navigation - Masque le bouton \"Bibliothèque\". - Masquer le bouton \"Bibliothèque\" - Masque le bouton \"Samples\". - Masquer le bouton \"Samples\" - Masque le bouton \"S\'abonner\" de la barre de navigation. - Masquer le bouton \"S\'abonner\" - Masque le bouton \"Notification\" de la barre d\'outils. - Masquer les boutons \"Notification\" - Masque la bannière \"Inclut une communication commerciale\". - Masquer la bannière \"Communication commerciale\" - Masque les étagères de cartes \"Playlists\" dans les flux. - Masquer les étagères de cartes \"Playlists\" - Masque les publicités pour YouTube Premium. - Masquer les publicités pour YouTube Premium - Masque la bannière \"Renouveler votre abonnement Premium\". - Masquer la bannière \"Renouveler votre abonnement Premium\" - Masque la bannière d\'alerte de promotion. - Masquer la bannière d\'alerte de promotion - Masque l’étagère \"Samples\" dans les flux. - Masquer l’étagère \"Samples\" - Masquer le menu \'À propos\' - Masquer le menu \'Économie de données\' - Masquer le menu \'Téléchargements et stockage\' - Masquer le menu \'Paramètres généraux\' - Masquer le menu \'Notifications\' - Masquer le menu \'Obtenir Music Premium\' - Masquer le menu \'Centre pour la famille\' - Masquer le menu \"Lecture\" - Masquer le menu \'Confidentialité et données\' - Masquer le menu \'Recommandations\' - "Masquer les éléments du menu Paramètre. -Cela masque non seulement le menu paramètre de YT Music, mais également le menu paramètre de ReVanced Extended." - Masquer le menu \'Paramètres\' - Masque le bouton \"Rechercher une musique\" de la barre de recherche. - Masquer le bouton \"Rechercher une musique\" - Masque le bouton \"Appuyer pour mettre à jour\". - Masquer le bouton \"Appuyer pour mettre à jour\" - Masque le conteneur des conditions d\'utilisation. - Masquer le conteneur de termes - Masque le bouton \"Recherche vocale\" de la barre de recherche. - Masquer le bouton \"Recherche vocale\" - Compte - Barre d\'action - Publicités - Menu déroulant - Interface - Paramètres avancés - Barre de navigation - Lecteur - Return YouTube Dislike - SponsorBlock - Menu Paramètres - Qualité et vitesse vidéo - Enregistre la dernière vitesse de lecture sélectionnée. - Enregistrer la modification de la vitesse de lecture - Afficher un message lorsque vous modifiez la vitesse de lecture par défaut. - Afficher un message - Vitesse de lecture modifiée par %s. - Enregistre l\'état du mode répétition. - Enregistrer l\'état du mode répétition - Enregistre l\'état du mode aléatoire. - Enregistrer l\'état du mode aléatoire - Enregistre la dernière qualité vidéo sélectionnée. - Enregistrer la modification de la résolution - Afficher un message lorsque vous modifiez la qualité vidéo par défaut. - Afficher un message - La résolution sur les données mobiles a été modifiée par %s. - Impossible de définir la qualité. - La résolution sur le Wi-Fi a été modifiée par %s. - "Supprime le message \"Confirmer votre âge\". -Cela ne contourne pas la restriction d'âge, mais le confirme automatiquement." - Supprimer le message \"Confirmer votre âge\" - Continuer la lecture avec l\'horodatage en cours sur YouTube. - Continuer la lecture - Remplace le menu \"Supprimer de la file d\'attente\" par le menu \"Regarder sur YouTube\". - Remplacer le menu \"Supprimer de la file d\'attente\" - Regarder sur YouTube - Url de la vidéo invalide. - Conserve le menu \"Signaler\" dans la section des commentaires. - Conserver le menu \"Signaler\" dans les commentaires - Remplace le menu \"Signaler\" par le menu \"Vitesse de lecture\". - Remplacer le menu \"Signaler\" - Restaure l\'ancien style de la section commentaire. - Restaurer l\'ancienne interface des commentaires - Restaure l\'ancien style de l\'arrière-plan du lecteur. - Restaurer l\'ancien arrière-plan du lecteur - "Restaure l'ancien style de la mise en page du lecteur. -Certaines fonctions peuvent ne pas fonctionner sur l'ancienne mise en page." - Restaurer l\'ancienne mise en page du lecteur - Restaure l\'ancien style de l’étagère \"Bibliothèque\". (Expérimental) - Restaurer l\'ancien style de l’étagère \"Bibliothèque\" - À propos - Les données des \"Je n\'aime pas\" sont fournies par l\'API de Return YouTube Dislike. Appuyez ici pour en savoir plus. - ReturnYouTubeDislike.com - Masque les séparateurs sur le bouton \"J\'aime\". - Bouton \"J\'aime\" compact - Les \"Je n\'aime pas\" sont affichés en pourcentage plutôt qu\'en nombre. - \"Je n\'aime pas\" en pourcentage - Affiche le compteur des \"Je n\'aime pas\" sur les vidéos. - Activer Return YouTube Dislike - Les \"Je n\'aime pas\" sont indisponibles (le client a atteint la limite de l\'API). - Les \"Je n\'aime pas\" sont indisponible (status %d). - Les \"Je n\'aime pas\" sont temporairement indisponible (API obsolète). - Les \"Je n\'aime pas\" sont indisponible (%s). - Affiche un message si l\'API de Return YouTube Dislike n\'est pas disponible. - Afficher un message si l\'API est indisponible - Supprime les paramètres de suivi (tracking) des URL lors du partage de liens. - Nettoyer les liens partagés - À propos - sponsor.ajay.app - Les données sont fournies par l\'API SponsorBlock. Cliquez ici pour en savoir plus et voir les téléchargements pour d\'autres plateformes. - Modifier l\'URL de l\'API - L\'URL de l\'API a été modifiée. - L\'URL de l\'API est invalide. - L\'URL de l\'API a été réinitialisé. - L\'adresse qu\'utilise SponsorBlock pour contacter le serveur. Ne le modifiez que si vous savez ce que vous faites. - Couleur modifiée. - Couleur (hex) : - Code couleur invalide. - Couleur réinitialisée. - Modifier le comportement des segments - Activer Sponsorblock - SponsorBlock est un service d\'entraide permettant de passer des parties gênantes des vidéos YouTube. - Réinitialiser la couleur - Remplissage / Blagues - Scènes secondaires ajoutées uniquement à des fins de remplissage ou d\'humour qui ne sont pas nécessaires à la compréhension de la vidéo. N\'inclus pas les segments fournissant un contexte ou des détails utiles. - Rappel d\'interaction (S\'abonner) - Un bref rappel pour aimer, s\'abonner ou pour les suivre au milieu du contenu. S\'il est long ou s\'il traite d\'un sujet spécifique, il devrait être placé dans la section \"auto-promotion\". - Intro / Entracte - Un intervalle sans contenu réel. Il peut s\'agir d\'une pause, d\'une image fixe ou d\'une animation répétitive. Ne comprends pas des passages contenant des informations. - Musique : Section non musicale - Uniquement destiné pour les vidéos musicales. Sections de vidéos musicales sans musique, qui ne sont pas déjà couvertes par une autre catégorie. - Outro / Crédits - Crédits ou lorsque les cartes de fin de vidéo YouTube apparaissent. Ne pas utiliser pour des conclusions avec des informations. - Aperçu / Récapitulatif - Un récapitulatif montrant ce qu\'il va se passer ou qui s\'est passé dans la vidéo ou dans d\'autres vidéos de la série, lorsque des informations se répètes. - Auto-promotion / Non Rémunérée - Similaire à \'Sponsor\', à l\'exception de la promotion non rémunérée ou l\'autopromotion. Comprend les sections produits, les dons, et des informations sur les partenaires avec lesquels ils ont collaboré. - Sponsor - Partenariat rémunéré, parrainage rémunérés et publicités directes. Ne concerne pas l\'autopromotion ou les mentions gratuites pour des causes / créateurs / sites web / produits qu\'ils apprécient. - Passer automatiquement - Désactiver - Remplissages passés. - Passage ennuyeux passé. - Intro passée. - Entracte passé. - Entracte passé. - Plusieurs segments passés. - Section non musicale passée. - Outro passée. - Aperçu passé. - Résumé passé. - Aperçu passé. - Autopromotion passée. - Sponsor passé. - SponsorBlock est temporairement indisponible. - SponsorBlock est temporairement indisponible (status %d). - SponsorBlock est temporairement indisponible (API obsolète). - Afficher un message si l\'API est indisponible - Affiche un message si l\'API de SponsorBlock est indisponible. - Afficher un message lors du passage auto des segments - Affiche un message lorsqu\'un segment est automatiquement passé. - Paramètres copiés dans le presse-papier. - "Falsifie la version du client par une ancienne version. - -• Cela change l'apparence de l'application, mais des effets secondaires inconnus peuvent se produire. -• Si désactivée ultérieurement, l'ancienne interface peut subsister jusqu'à la suppression des données de l'application." - 4.27.53 - Désactive le mode radio dans les régions canadiennes - 6.11.52 - Désactive les paroles en temps réel - 7.16.53 - Restaurer l\'ancienne barre d\'action - Sélectionner la version de l\'application à falsifier. - Choisir la version à falsifier - Falsifier la version de l\'app - diff --git a/src/main/resources/music/translations/hu-rHU/missing_strings.xml b/src/main/resources/music/translations/hu-rHU/missing_strings.xml deleted file mode 100644 index 2880dda8b..000000000 --- a/src/main/resources/music/translations/hu-rHU/missing_strings.xml +++ /dev/null @@ -1,48 +0,0 @@ - - - Don\'t show again - To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Disables Cairo splash animation when the app starts up. - Disable Cairo splash animation - Reset to default values. - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - Hide About menu - Hide Data saving menu - Hide Downloads & storage menu - Hide General menu - Hide Notifications menu - Hide Get Music premium menu - Hide Family Center menu - Hide Playback menu - Hide Privacy & data menu - Hide Recommendations menu - Return YouTube Username - Settings menu - Show a toast when changing the default playback speed. - Show a toast - Show a toast when changing the default video quality. - Show a toast - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Username - Replaces handles with usernames in comments. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - Shows the estimated like count of videos. - Show estimated likes - Hidden - 7.16.53 - Restore old action bar - diff --git a/src/main/resources/music/translations/hu-rHU/strings.xml b/src/main/resources/music/translations/hu-rHU/strings.xml deleted file mode 100644 index 647ee35fd..000000000 --- a/src/main/resources/music/translations/hu-rHU/strings.xml +++ /dev/null @@ -1,365 +0,0 @@ - - - Folytatás - "A GmsCore-nak nincs engedélye a háttérben történő futtatásra. - -Kövesd a telefonodra vonatkozó 'Don't kill my app!' útmutatót és alkalmazd az utasításokat a MicroG telepítésére. - -Ez szükséges az app működéséhez." - "A GmsCore akkumulátor-optimalizálásokat le kell tiltani a problémák megelőzése érdekében. - -Nyomd meg a folytatás gombot, és tiltsd le az akkumulátor-optimalizálásokat." - Weboldal megnyitása - Művelet szükséges - Értesítések fogadásához engedélyezd a felhő alapú üzenetküldést. - GmsCore megnyitása - A GmsCore nincs telepítve. Telepítsd. - Helyettesíti az egyes régiókban blokkolt tartományt, így a lejátszási lista miniatűrjei, csatorna avatarok stb. fogadhatóak. - Területi kép-korlátozások megkerülése - Váltás az alkalmazáson belüli megosztási lapról a rendszer megosztási lapjára. - Megosztási lap megváltoztatása - Diagramok - Felfedezés - Kezdőlap - Könyvtár - Feliratkozások - Kiválaszthatod, hogy milyen oldalon nyíljon meg az alkalmazás. - Kezdőlap megváltoztatása - A szűrendő elemek útvonal-építő stringjeinek listája, új sorokkal elválasztva. - Egyéni szűrő - Engedélyezi, hogy saját menüket is elrejts. - Egyéni szűrők engedélyezése - Érvénytelen egyéni szűrő: %s. - Az egyéni sebességnek kisebbnek kell lennie, mint %sx. Alapértelmezett értékek használata. - Érvénytelen egyedi lejátszási sebesség. Használd az alap értékeket. - Az elérhető lejátszási sebességek módosítása vagy hozzáadása. - Egyéni lejátszási sebességek szerkesztése - Letiltja a feliratokat, hogy ne jelenjenek meg automatikusan. - Kényszerített automatikus feliratok letiltása - Letiltja az átirányítást a következő számra, amikor rányomsz a nem tetszik gombra. - Nem tetszik átirányítás letiltása - Kikapcsolja a zeneszámok váltását a minialejátszóban. - Minilejátszó gesztus letiltása - Kikapcsolja a zeneszámok váltását a lejátszóban. - Lejtászó gesztus letiltása - A navigációs sor színét átállítja feketére. - Fekete navigációs sor engedélyezése - Megváltoztatja a lejátszó háttér színét feketére. - Fekete hátterű lejátszó engedélyezése - Egyező színe lesz a kis lejátszónak, mint a teljes képernyősnek. - Megegyező színű lejátszó bekapcsolása - "Engedélyezi a telefonokon a kompakt felugró menüt. - -Korlátozások: -• A könyvtár lapon lévő albumok képei kisebbek lesznek, ha rácsba vannak rendezve. -• Az alvásidőzítő elrendezése szokatlannak tűnhet." - Kompakt menü engedélyezése - Beleírja a puffert a hibakeresési naplóba. - Hibakeresési puffer naplózásának engedélyezése - Kiírja a hibanaplót. - Hibanaplók engedélyezése - A lejátszó akkor is minimalizálva marad, amikor egy másik zeneszámot játszanak le. - Mini lejátszó kényszerítése - Engedélyezi a fekvő módot, amikor elforgatod a telefonodat. - Fekvő mód engedélyezése - Engedélyezi a következő szám gombot a minilejátszónál. - Minilejátszó következő gomb engedélyezése - Engedélyezi a előző szám gombot a minilejátszónál. - Minilejátszó előző gomb engedélyezése - "Az opus audio codec engedélyezése az mp4a audio codec helyett. - -Info: -• A legújabb Android-kliensek alapértelmezés szerint az opus audio codec-et használják. -• Ez csak a nagyon régi klienseket használó felhasználókra érvényes." - Opus codec engedélyezése - Lehetővé teszi a minialejátszó elhagyását lefelé húzással. - Minilejátszó elhagyása egy húzással - "A 'Csend kivágás' kapcsoló hozzáadása a lejátszási sebesség felugró menühöz. - -Információ: -• Ez a funkció podcastek számára készült. -• Ez a funkció még fejlesztés alatt áll, ezért instabil lehet." - Csend kivágás kapcsoló hozzáadása - A zen mód a podcast-ekben is működni fog. - Zen mód engedélyezése podcastekben - Megváltoztatja a lejátszó hátterét világos szürkére a szem megóvására. - Zen mód bekapcsolása - Indítsd újra az elrendezés normál betöltéséhez - Frissítés és újraindítás - Beállítások exportálása egy fájlba - A beállítások exportálása sikertelen. - A beállítások sikeresen exportálva. - Importálás - Beállítások importálása fájlból - Másolás - Beállítások import- / exportálása szövegként - Beállítások importálása vagy exportálása. - Beállítások Importálása / Exportálása - Sikertelen importálás: %s. - Beállítások visszaállítása alapra. - %d beállítás importálva. - Visszaállítás - ReVanced Extended - "A Letöltés gomb megnyitja a külső letöltőt. - -• Csak a lejátszóban lévő letöltési művelet gombot írja felül. -• Nem írja felül a letöltés gombot a felugró menüben vagy a könyvtárban." - Letöltés gomb felülírása - Külső letöltéskezelő - "A(z) %1$s nincs telepítve. -Töltsd le a(z) %2$s weboldalról." - Figyelmeztetés - %s nincs telepítve. Kérlek, telepítsd. - A telepített külső letöltő alkalmazás csomagneve, például NewPipe vagy YTDLnis. - Külső letöltő csomagneve - Elrejti az üres részeket a fiók menüben. - Üres részek elrejtése - A fiókmenüben szűrendő nevek listája, új sorokkal elválasztva. - Fiókmenü szűrő - Elrejti a fiókmenü elemeit az egyéni szűrőben. - Fiókmenü elrejtése - Elrejti a mentés gombot. - Mentés gomb elrejtése - Elrejti a megjegyzés gombot. - Megjegyzés gomb elrejtése - Elrejti a letöltés gombot. - Letöltés gomb elrejtése - Elrejti a címkéket az műveleti gombokon. - Navigációs gombok címkéinek elrejtése - Elrejti a tetszik és nem tetszik gombokat. Nem működik a régi lejátszóval. - A tetszik és nem tetszik gombok elrejtése - Elrejti a rádió gombot. - Rádió gomb elrejtése - Elrejti a Megosztás gombot. - Megosztás gomb elrejtése - Elrejti a hang/videó gombot a lejátszóban. - Hang/Videó gomb elrejtése - Elrejti a gomb polcot a főoldalon. - Gomb polc elrejtése - Elrejti a forduló polcot a főoldalon. - Forduló polc elrejtése - Elrejti az átküldés gombot. - Átküldés gomb elrejtése - Elrejti a kategória sávot. - Kategória sáv elrejtése - Elrejti a csatorna irányelveit a komment szekció tetején. - Csatorna irányelveinek elrejtése - Elrejti az időbélyeget és az emoji gombokat komment gépelés közben. - Elrejti az edőbélyeg és az emoji gombokat - Elrejti dupla koppintáskor megjelenő sötét átfedést. - Dupla koppintás átfedés elrejtése - Elrejti a lebegő gombot a könyvtárban. - Lebegő gomb elrejtése - 3 oszlopos komponens elrejtése - Hozzáadás a várólistához menü elrejtése - Feliratok menü elrejtése - Lejátszási lista törlés menü elrejtése - Várólista menü elrejtése - Letöltés menü elrejtése - Lejátszási lista szerkesztés menü elrejtése - Album menü elrejtése - Előadó menü elrejtése - Rész menü elrejtése - Podcast menü elrejtése - Súgó & visszajelzés menü elrejtése - A tetszik és nem tetszik gombok elrejtése - Következő lejátszása menü elrejtése - Minőség menü elrejtése - Eltávolítás a könyvtárból menü elrejtése - Eltávolítás a lejátszási listáról menü elrejtése - Jelentés menü elrejtése - Rész elmentése későbbre menü elrejtése - Mentés a könyvtárba menü elrejtése - Mentés a lejátszási listába menü elrejtése - Megosztás menü elrejtése - Kevert lejátszás menü elrejtése - Elalvási időzítő elrejtése - Rádió indítás menü elrejtése - Statisztikák kockáknak menü elrejtése - Feliratkozás / Leiratkozás menü elrejtése - Dalkredit menü elrejtése - Teljes képernyős hirdetések elrejtése. - Teljes képernyős hirdetések elrejtése - "Ha engedélyezve van, akkor a teljes képernyő hírdetéseket a bezárás gombbal lehet eltüntetni. -Ha tiltva van, akkor blokkolja a t. k. hírdetéseket. (lehetnek mellékhatások)" - "Ha engedélyezve van, akkor a teljes képernyő hírdetéseket a bezárás gombbal lehet eltüntetni. -Ha tiltva van, akkor blokkolja a t. k. hírdetéseket. (lehetnek mellékhatások)" - "Ha engedélyezve van, akkor a teljes képernyő hírdetéseket a bezárás gombbal lehet eltüntetni. -Ha tiltva van, akkor blokkolja a t. k. hírdetéseket. (lehetnek mellékhatások)" - Teljes képernyős hirdetések bezárása - Elrejti a megosztás gombot a teljes képernyős lejátszóban. - Teljes képernyős megosztás gomb elrejtése - Elrejti az általános hirdetéseket. - Általános hirdetések elrejtése - Elrejti a felhasználónevedet a fiók menüben. - Felhaszálónév elrejtése - Elrejti az előzmények gombot az eszköztáron. - Előzmények gomb elrejtése - Elrejti a hirdetéseket a zene lejátszása előtt. - Zenei hirdetések elrejtése - Elrejti a navigációs sávot. - Navigációs sáv elrejtése - Elrejti a felfedezés gombot. - Felfedezés gomb elrejtése - Elrejti a kezdőlap gombot. - Kezdőlap gomb elrejtése - Elrejti a szöveget a navigációs gombok alatt. - Navigációs címkék elrejtése - Elrejti a könyvtár gombot. - Könyvtár gomb elrejtése - Elrejti a minták gombot. - Minták gomb elrejtése - Elrejti az előfizetés gombot. - Előfizetés gomb elrejtése - Elrejti az értesítés gombot az eszköztáron. - Értesítés gomb elrejtése - Elrejti a promóció címkét. - Fizetett promóció címke elrejtése - Elrejti a lejátszási lista kártya polcot a főoldalon. - Lejátszási lista kártya polc elrejtése - Elrejti a felugró prémium hírdetéseket. - Felugró prémium hírdetések elrejtése - Elrejti a prémium megújítás szalaghírdetést. - Prémium megújítás szalaghírdetés elrejtése - Promóciós figyelmeztető banner elrejtése. - Promóciós figyelmeztető banner elrejtése - Elrejti a minták polcot a főoldalon. - Minták polc elrejtése - "A beállítások menü elemeinek elrejtése. -Ez nemcsak az YT Music beállítások menüjét, hanem a ReVanced Extended beállítások menüjét is elrejti." - Beállítások menü elrejtése - Elrejti a zene keresés gombot a kereső sávban. - Zenekeresés gomb elrejtése - Elrejti a Kattints a frissítéshez gombot. - Kattints a frissítéshez gomb elrejtése - Elrejti a Szolgáltatási feltételeket a fiókmenüben. - Feltételek rész elrejtése - Elrejti a hang keresés gombot a kereső sávban. - Hangkeresés gomb elrejtése - Fiók - Műveletsáv - Hirdetések - Felugró menü - Általános - Egyéb - Navigációs sor - Lejátszó - YouTube nem tetszések visszaállítása - Szponzor Blokk - Videó - Megjegyzi az utoljára kiválasztott lejátszási sebességet. - Lejátszási sebesség módosításainak megjegyzése - Megváltoztatva az alap sebességet %s-re. - Emlékezik az ismétlés állapotára. - Isméltés állapotának megjegyzése - Emlékezik az keverés állapotára. - Keverés állapotának megjegyzése - Megjegyezi a legutolsó videó minőséget, amit kiválasztottál. - Videó minőség megjegyzése - Az alapértelmezett mobiladat minőség módosítása a következőre %s. - Nem sikerült beállítani a minőséget. - Az alapértelmezett Wi-Fi-minőség módosítása a következőre: %s. - "Eltávolítja a nézői döntés menüt. -Ez nem kerüli meg a korhatárkorlátozást. Csak automatikusan elfogadja azt." - Nézői döntés menü eltávolítása - Folytatja a videót attól az időponttól, amikor átváltasz a Youtube-ra. - Megtekintés folytatása - Kicseréli a \"Várólistát\" a \"Megtekintés a Youtube-on\"-nal. - Várólista cserélése - Megnézés YouTube-on - Érvénytelen videó url. - Megtartja a jelentés menüt a komment szekcióban. - Jelentés meghagyása a kommenteknél - Kicseréli a \"Jelentés\"-t a \"Lejátszási sebesség\"-gel. - Jelentés cserélése - Visszaállítja a régi felugró menü a régi kinézetére. - Régi komment felugró menü visszaállítása - Visszaállítja a lejátszó hátterét a régi kinézetére. - Régi lejátszó kinézet visszaállítása - "Visszaállítja a lejátszó elrendezését a régi stílusra. -Előfordulhat, hogy egyes funkciók nem működnek megfelelően a régi lejátszó elrendezésben." - Régi lejátszó kinézet visszaállítása - Visszaállítja a régi megjelenését a könyvtár oldalnak. (Kísérleti) - Visszaállítja a régi stílusú könyvtár polcot - Névjegy - Az adatokat a YouTube nem tetszések visszaállítása API biztosítja. További információért nyomj ide. - ReturnYouTubeDislike.com - Elrejti a kedvelés gomb elválasztóját. - Kompakt kedvelés gomb - A nem tetszések százalékos arányát jeleníti meg a nem tetszések száma helyett. - Nem tetszések megjelenítése százalékban - Megjeleníti a videók nem tetszésének számát. - YouTube nem tetszések visszaállításának engedélyezése - Nem tetszések nem érhetőek el (kliens API limit elérve). - A nem tetszik funkció nem elérhető (állapot: %d). - Nem tetszések átmenetileg nem érhetőek el (API időkorlát lejárt). - A nem tetszik funkció nem elérhető (%s). - Megjelenik egy üzenet, ha a YouTube nem tetszések visszaállítása API nem érhető el. - Köszöntő megjelenítése, ha az API nem elérhető - Linkek megosztásakor eltávolítja a nyomkövetési paramétereket az URL-ekből. - Megosztási linkek tisztítása - Névjegy - sponsor.ajay.app - Az adatokat a SponsorBlock API biztosítja. Nyomj ide, ha többet szeretnél megtudni és megtekintenéd a letöltéseket más platformokra. - API URL módosítása - API URL megváltoztatva. - API URL érvénytelen. - Az API URL visszaállítása. - A cím, amelyet a SponsorBlock a szerverhez történő kommunikációhoz használ. Ne változtasso ezen, ha nem tudod, hogy mit csinálsz. - Szín módosítva. - Szín: - Érvénytelen színkód. Visszaállítva alapra. - Szín visszaállítva. - Szakasz viselkedésének megváltoztatása - SzponsorBlokk engedélyezése - A SzponsorBlokk egy közösségi rendszer a YouTube videók zavaró részeinek átugrására. - Szín visszaállítása - Kitöltések / Viccek - Csak töltelék vagy humornak hozzáadott részek, amik nem szükségesek a videó fő tartalmának megértéséhez. Ne tartalmazzon olyan részeket, amik összefüggést, vagy háttérinformációt szolgáltatnak. - Emlékeztető (Feliratkozás) - Rövid emlékeztető a kedvelésre, feliratkozásra vagy követésre a tartalom közepette. Ha hosszú vagy valami specifikusról szól, akkor azt önpromóció alatt kell feltüntetni. - Szünet/Intro animáció - Egy részlet tartalom nélkül. Lehet szünet, álló képkocka, vagy ismétlődő animáció. Nem használandó információt tartalmazó átmeneteknél. - Zene: nem-zene szegmens - Csak zenei videókhoz használható. Zenei videók zene nélküli részei, amelyek még nem tartoznak más kategóriába. - Zárókártyák / Stáblista - Stáblista, vagy amikor megjelennek a YouTube zárókártyák. Nem tartozik bele az információt tartalmazó összegzés. - Előzetes / Összefoglaló / Hook - Olyan klipek gyűjteménye, amelyek megmutatják, hogy mi következik vagy mi történt a videóban vagy egy sorozat más videóiban, ahol minden információ máshol ismétlődik. - Nem fizetett / Önpromóció - Hasonló a szponzorhoz, kivéve a nem fizetett vagy önpromóciót. Tartalmazza az árucikkekre, adományokra vonatkozó részeket, vagy információkat arról, hogy kivel működtek együtt. - Szponzor - Fizetett promóciók, fizetett hivatkozások és közvetlen reklámok. Nem önreklámozásra vagy ingyenes kiemelésre vonatkozik okok / alkotók / weboldalak / termékek esetében, amelyeket szeretnek. - Átugrás automatikusan - Letiltás - Töltelék átugorva. - Idegesítő emlékeztető átugorva. - Bevezető kihagyva. - Szünet átugorva. - Szünet átugorva. - Több szakasz átugorva. - Nem zenei rész átugorva. - Befejezés átugorva. - Bevezető átugorva. - Összefoglaló átugorva. - Bevezető átugorva. - Önpromóció átugorva. - Szponzor átugorva. - A SponsorBlock átmenetileg nem elérhető. - A SponsorBlock jelenleg nem elérhető (állapot %d). - A SponsorBlock átmenetileg nem elérhető (API időtúllépés). - Üzenet megjelenítése, ha az API nem elérhető - Üzenetet ír ki, ha a SponsorBlock API nem érhető el. - Üzenet megjelenítése automatikus ugráskor - Üzenetet jelenít meg, amikor egy szegmens automatikusan kihagyásra kerül. - A beállítások vágólapra másolva. - "Hamisítja a kliens verziót egy régi verzióra. - -• Ez megváltoztatja az alkalmazás megjelenését, de ismeretlen mellékhatások előfordulhatnak. -• Ha később kikapcsolod, a régi felhasználói felület megmaradhat, amíg az alkalmazás adatait nem törlöd." - 4.27.53 - Letiltja a rádió módot Kanada területén - 6.11.52 - Letiltja a valós idejű dalszövegeket - Válaszd ki, hogy melyik alkalmazásverziót akarod használni. - Cél alkalmazásverzió - Alkalmazásverzió hamisítása - diff --git a/src/main/resources/music/translations/id-rID/missing_strings.xml b/src/main/resources/music/translations/id-rID/missing_strings.xml deleted file mode 100644 index 6b0bc8019..000000000 --- a/src/main/resources/music/translations/id-rID/missing_strings.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - Don\'t show again - To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Disables Cairo splash animation when the app starts up. - Disable Cairo splash animation - Reset to default values. - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - Hides the promotion alert banner. - Hide promotion alert banner - Hide About menu - Hide Data saving menu - Hide Downloads & storage menu - Hide General menu - Hide Notifications menu - Hide Get Music premium menu - Hide Family Center menu - Hide Playback menu - Hide Privacy & data menu - Hide Recommendations menu - Return YouTube Username - Settings menu - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Username - Replaces handles with usernames in comments. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - Shows the estimated like count of videos. - Show estimated likes - Hidden - 7.16.53 - Restore old action bar - diff --git a/src/main/resources/music/translations/id-rID/strings.xml b/src/main/resources/music/translations/id-rID/strings.xml deleted file mode 100644 index 86653dd2a..000000000 --- a/src/main/resources/music/translations/id-rID/strings.xml +++ /dev/null @@ -1,363 +0,0 @@ - - - Continue - "GmsCore does not have permission to run in the background. - -Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. - -This is required for the app to work." - "GmsCore battery optimizations must be disabled to prevent issues. - -Tap on the continue button and disable battery optimizations." - Open website - Action needed - Enable cloud messaging to receive notifications. - Open GmsCore - GmsCore is not installed. Install it. - Mengganti domain yang ke blokir di negara tertentu sehingga playlist thumbnail, channel avatar, dll bisa di terima. - Bypass gambar larangan wilayah - Mengubah dari lembar berbagi dalam aplikasi ke lembar berbagi sistem. - Ubah lembar berbagi - Charts - Jelajahi - Beranda - Koleksi - Berlangganan - Select which page the app opens in. - Ganti Halaman Awal - Memfilter nama komponen dengan baris yang dipisahkan. - Edit filter kustom - Mengaktifkan filter kustom untuk menyembunyikan komponen tata letak. - Aktifkan filter kustom - Invalid custom filter: %s. - Kecepatan pemutaran kustom tidak valid. Atur ulang ke nilai default. - Invalid custom playback speeds. Using default values. - Menambah atau mengubah kecepatan pemutaran yang tersedia. - Edit kecepatan pemutaran kustom - Teks otomatis paksa yang dinonaktifkan. - Nonaktifkan teks otomatis paksa - Disables redirection to the next track when clicking the Dislike button. - Disable dislike redirection - Nonaktifkan gesekan untuk mengubah trek di miniplayer. - Nonaktifkan gerakan miniplayer - Nonaktifkan usap untuk mengubah trek di pemutar. - Menonaktifkan gerakan pemutar - Mengatur warna bilah navigasi menjadi hitam. - Aktifkan bilah navigasi hitam - Changes the player background color to black. - Enable black player background - Mencocokkan warna pemutar layar penuh dengan yang diperkecil. - Aktifkan pencocokan warna pemutar - "Aktifkan dialog ringkas di ponsel. - -Masalah yang diketahui: -• Gambar album di Tab library juga menjadi lebih kecil. -• Tata letak pengatur waktu tidur mungkin terlihat tidak biasa." - Aktifkan dialog ringkas - Includes the buffer in the debug log. - Enable debug buffer logging - Mencetak catatan debug. - Aktifkan pencatatan debug - Mempertahankan pemutar agar tetap diminimalkan secara permanen meskipun trek lain diputar. - Aktifkan pemutar yang diminimalkan paksa - Mengaktifkan masuk ke mode lanskap dengan rotasi layar di ponsel. - Aktifkan mode lanskap - Adds a next track button to the miniplayer. - Add miniplayer next button - Adds a previous track button to the miniplayer. - Add miniplayer previous button - "Mengaktifkan codec audio opus alih-alih codec audio mp4a." - Aktifkan codec opus - Enables swipe down to dismiss miniplayer. - Enable swipe to dismiss miniplayer - "Menambahkan tombol Trim silence ke menu flyout playback speed. - -Info: -• Fitur ini hanya untuk podcast. -• Fitur ini masih dalam pengembangan, jadi ini tidak akan stabil." - Tambah switch Trim silence - Also enables Zen mode for podcasts. - Enable Zen mode in podcasts - Menambahkan rona abu-abu ke pemutar video untuk mengurangi ketegangan mata. - Aktifkan mode zen - Mulai ulang untuk memuat layout secara normal - Refresh dan mulai ulang - Export settings to file - Failed to export settings. - Settings were successfully exported. - Impor - Import settings from file - Salin - Import / Export settings as text - Impor atau ekspor setelan sebagai teks. - Ekspor / Impor - Import failed: %s. - Reset setelan ke default. - Setelan %d diimpor. - Reset - ReVanced Extended - "Tombol Unduh membuka Downloader eksternal kamu. - -• Hanya menggantikan tombol Unduh di player. -• Tidak bisa menggantikan tombol Unduh di menu flyout atau tab Library." - Ganti tombol tindakan Unduh - Downloader eksternal - "%1$s belum terinstall. -Download %2$s dari website." - Peringatan - %s tidak diinstal. Silakan instal. - Nama paket aplikasi downloader eksternal yang terinstal, seperti NewPipe atau YTDLnis. - Nama paket downloader eksternal - Menyembunyikan komponen kosong di menu akun. - Sembunyikan komponen kosong - Daftar dari nama-nama menu akun ke filter, terpisah oleh garis baru. - Filter menu Akun - Menyembunyikan elemen menu akun menggunakan filter custom. - Sembunyikan menu akun - Menyembunyikan tombol Simpan. - Sembunyikan tombol Save - Menyembunyikan tombol Komentar. - Sembunyikan tombol Komentar - Menyembunyikan tombol Unduh. - Sembunyikan tombol Unduh - Menyembunyikan bilah dari tombol tindakan. - Sembunyikan tombol bilah tindakan - Menyembunyikan tombol Like dan Dislike. Itu tidak akan bekerja di layout player lama. - Sembunyikan tombol Like dan Dislike - Menyembunyikan tombol Radio. - Sembunyikan tombol Radio - Menyembunyikan tombol Bagikan. - Sembunyikan tombol Bagikan - Hides the Audio / Video toggle in the player. - Hide Audio / Video toggle - Menyembunyikan rak tombol dari beranda dan eksplorasi. - Sembunyikan rak tombol - Menyembunyikan rak korsel dari beranda dan eksplorasi. - Sembunyikan rak korsel - Menyembunyikan tombol cast. - Sembunyikan tombol cast - Menyembunyikan bilah kategori musik di bagian atas beranda. - Sembunyikan bilah kategori - Hides the channel guidelines at the top of the comments section. - Hide channel guidelines - Hides the timestamp and emoji buttons when typing comments. - Hide timestamp and emoji buttons - Menyembunyikan overlay gelap yang muncul ketika double-tap to seek. - Sembunyikan filter overlay double-tap - Hides the floating button in the Library tab. - Hide floating button - Sembunyikan komponen 3-kolom - Sembunyikan menu tambahkan ke antrean - Sembunyikan menu teks - Sembunyikan menu hapus playlist - Sembunyikan menu abaikan antrean - Sembunyikan menu Unduh - Sembunyikan menu edit playlist - Sembunyikan menu Pergi ke album - Sembunyikan menu Pergi ke artis - Sembunyikan menu Pergi ke episode - Sembunyikan menu Pergi ke podcast - Sembunyikan menu bantuan & saran - Sembunyikan tombol Like dan Dislike - Sembunyikan menu putar berikutnya - Hide menu Kualitas - Sembunyikan menu hapus dari koleksi - Sembunyikan hapus dari menu playlist - Sembunyikan menu laporkan - Sembunyikan menu simpan episode untuk ditonton nanti - Sembunyikan menu simpan ke koleksi - Sembunyikan menu simpan ke playlist - Sembunyikan menu bagikan - Sembunyikan menu putar acak - Sembunyikan menu waktu tidur - Sembunyikan menu mulai radio - Sembunyikan menu statistik untuk nerds - Sembunyikan menu Subscribe / Unsubscribe - Sembunyikan menu kredit lagu - Menyembunyikan iklan fullscreen. - Sembunyikan iklan fullscreen - "Jika diaktifkan, iklan fullscreen akan ditutup lewat tombol Close. -Jika dimatikan, iklan fullscreen akan di block. (kemungkinan akan ada side effect)" - "If it is enabled, fullscreen ads are closed through the Close button. -If it is disabled, fullscreen ads are blocked. (there may be side effects)" - "If it is enabled, fullscreen ads are closed through the Close button. -If it is disabled, fullscreen ads are blocked. (there may be side effects)" - Tutup iklan fullscreen - Hides the Share button in the fullscreen player. - Hide fullscreen Share button - Menyembunyikan Iklan Umum. - Sembunyikan Iklan Umum - Menyembunyikan handle di menu akun. - Sembunyikan handle - Menyembunyikan tombol riwayat di toolbar. - Sembunyikan tombol riwayat - Menyembunyikan iklan sebelum memutar musik. - Sembunyikan iklan musik - Sembunyikan bilah navigasi. - Menyembunyikan bilah navigasi - Hides the Explore button. - Hide Explore button - Hides the Home button. - Hide Home button - Menyembunyikan label di bilah navigasi. - Sembunyikan label navigasi - Hides the Library button. - Hide Library button - Hides the Samples button. - Hide Samples button - Hides the Upgrade button. - Hide Upgrade button - Hides the Notifications button in the toolbar. - Hide Notifications button - Menyembunyikan label promosi berbayar. - Sembunyikan label promosi berbayar - Hides the playlist card shelf in the feed. - Hide playlist card shelf - Menyembunyikan popup promosi premium. - Sembunyikan popup promosi premium - Menyembunyikan banner pembaruan premium. - Sembunyikan banner pembaruan premium - Hides the Samples shelf in the feed. - Hide Samples shelf - "Sembunyikan elemen menu pengaturan. -Ini tidak hanya menyembunyikan menu pengaturan YT Music, tetapi juga menu pengaturan ReVanced Extended." - Sembunyikan menu pengaturan - Hides the sound search button in the search bar. - Hide sound search button - Hides the \'Tap to update\' button. - Hide \'Tap to update\' button - Menyembunyikan kontainer ketentuan layanan. - Sembunyikan kontainer ketentuan - Hides the voice search button in the search bar. - Hide voice search button - Akun - Bilah Tindakan - Iklan - Menu flyout - Umum - Miscellaneous - Bilah Navigasi - Player - Return YouTube Dislike - SponsorBlock - Video - Remembers the last playback speed selected. - Remember playback speed changes - Menunjukkan toast ketika mengubah playback speed semula. - Tampilkan toast - Changing default speed to %s. - Mengingat keadaan pengulangan. - Ingat keadaan pengulangan - Mengingat keadaan pengacakan. - Ingat keadaan pengacakan - Remembers the last video quality selected. - Remember video quality changes - Menunjukkan toast ketika mengubah playback speed semula. - Tampilkan toast - Changing default mobile data quality to %s. - Failed to set quality. - Changing default Wi-Fi quality to %s. - "Removes the viewer discretion dialog. -This does not bypass the age restriction. It just accepts it automatically." - Remove viewer discretion dialog - Melanjutkan video dari waktu saat ini ketika berlaih ke YouTube. - Lanjutkan menonton - Menggantikan menu hapus antrean menjadi tonton di YouTube. - Ganti menu hapus antrean - Tonton di YouTube - Url video tidak valid. - Mempertahankan menu laporkan di bagian komentar. - Simpan laporkan di komentar - Menggantikan menu laporkan dengan menu Kecepatan pemutaran. - Ganti menu laporkan - Returns the comments popup panels to the old style. - Restore old comments popup panels - Returns the player background to the old style. - Restore old player background - "Returns the player layout to the old style. -Some features may not work properly in the old player layout." - Restore old player layout - Returns the Library tab to the old style. (Experimental) - Restore old style library shelf - Tentang - Data disediakan oleh API Return YouTube Dislike. Tekan di sini untuk mempelajari lebih lanjut. - ReturnYouTubeDislike.com - Menyembunyikan pemisah tombol like. - Tombol like ringkas - Alih-alih jumlah dislike, yang ditampilkan adalah persentase dislike. - Dislike sebagai persentase - Menunjukkan jumlah dislike pada video. - Enable Return YouTube Dislike - Dislike tidak tersedia (batas API client tercapai). - Dislikes are unavailable (status %d). - Dislikes are temporarily unavailable (API timed out). - Dislikes are unavailable (%s). - Shows a toast if the Return YouTube Dislike API is unavailable. - Show a toast if API is unavailable - Menghapus parameter kueri pelacakan dari URL saat membagikan tautan. - Sanitasi tautan berbagi - About - sponsor.ajay.app - Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. - Change API URL - API URL changed. - API URL is invalid. - API URL reset. - The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. - Color changed. - Color: - Invalid color code. Color reset to default. - Color reset. - Change segment behavior - Enable SponsorBlock - SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. - Reset color - Filler Tangent / Jokes - Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. - Interaction Reminder (Subscribe) - A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. - Intermission / Intro Animation - An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. - Music: Non-Music Section - Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. - Endcards / Credits - Credits or when the YouTube endcards appear. Not for conclusions with information. - Preview / Recap / Hook - Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. - Unpaid / Self Promotion - Similar to \'Sponsor\' except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. - Sponsor - Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. - Skip automatically - Disable - Skipped filler. - Skipped annoying reminder. - Skipped intro. - Skipped intermission. - Skipped intermission. - Skipped multiple segments. - Skipped a non-music section. - Skipped outro. - Skipped preview. - Skipped recap. - Skipped preview. - Skipped self promotion. - Skipped sponsor. - SponsorBlock is temporarily unavailable. - SponsorBlock is temporarily unavailable (status %d). - SponsorBlock is temporarily unavailable (API timed out). - Show a toast if API is unavailable - Shows a toast if the SponsorBlock API is unavailable. - Show a toast when skipping automatically - Shows a toast when a segment is automatically skipped. - Setelan disalin ke papan klip. - "Memalsukan versi klien ke versi lama - -• Ini akan mengubah tampilan aplikasi, namun efek samping yang tidak diketahui mungkin terjadi. -• Jika nanti dinonaktifkan, UI lama mungkin tetap ada hingga aplikasi dihapus data." - 4.27.53 - Nonaktifkan mode radio di wilayah Kanada - 6.11.52 - Matikan Lirik real-time - Pilih target pemalsuan versi aplikasi. - Target pemalsuan versi aplikasi - Palsukan versi aplikasi - diff --git a/src/main/resources/music/translations/in/missing_strings.xml b/src/main/resources/music/translations/in/missing_strings.xml deleted file mode 100644 index 6b0bc8019..000000000 --- a/src/main/resources/music/translations/in/missing_strings.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - Don\'t show again - To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Disables Cairo splash animation when the app starts up. - Disable Cairo splash animation - Reset to default values. - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - Hides the promotion alert banner. - Hide promotion alert banner - Hide About menu - Hide Data saving menu - Hide Downloads & storage menu - Hide General menu - Hide Notifications menu - Hide Get Music premium menu - Hide Family Center menu - Hide Playback menu - Hide Privacy & data menu - Hide Recommendations menu - Return YouTube Username - Settings menu - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Username - Replaces handles with usernames in comments. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - Shows the estimated like count of videos. - Show estimated likes - Hidden - 7.16.53 - Restore old action bar - diff --git a/src/main/resources/music/translations/in/strings.xml b/src/main/resources/music/translations/in/strings.xml deleted file mode 100644 index 86653dd2a..000000000 --- a/src/main/resources/music/translations/in/strings.xml +++ /dev/null @@ -1,363 +0,0 @@ - - - Continue - "GmsCore does not have permission to run in the background. - -Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. - -This is required for the app to work." - "GmsCore battery optimizations must be disabled to prevent issues. - -Tap on the continue button and disable battery optimizations." - Open website - Action needed - Enable cloud messaging to receive notifications. - Open GmsCore - GmsCore is not installed. Install it. - Mengganti domain yang ke blokir di negara tertentu sehingga playlist thumbnail, channel avatar, dll bisa di terima. - Bypass gambar larangan wilayah - Mengubah dari lembar berbagi dalam aplikasi ke lembar berbagi sistem. - Ubah lembar berbagi - Charts - Jelajahi - Beranda - Koleksi - Berlangganan - Select which page the app opens in. - Ganti Halaman Awal - Memfilter nama komponen dengan baris yang dipisahkan. - Edit filter kustom - Mengaktifkan filter kustom untuk menyembunyikan komponen tata letak. - Aktifkan filter kustom - Invalid custom filter: %s. - Kecepatan pemutaran kustom tidak valid. Atur ulang ke nilai default. - Invalid custom playback speeds. Using default values. - Menambah atau mengubah kecepatan pemutaran yang tersedia. - Edit kecepatan pemutaran kustom - Teks otomatis paksa yang dinonaktifkan. - Nonaktifkan teks otomatis paksa - Disables redirection to the next track when clicking the Dislike button. - Disable dislike redirection - Nonaktifkan gesekan untuk mengubah trek di miniplayer. - Nonaktifkan gerakan miniplayer - Nonaktifkan usap untuk mengubah trek di pemutar. - Menonaktifkan gerakan pemutar - Mengatur warna bilah navigasi menjadi hitam. - Aktifkan bilah navigasi hitam - Changes the player background color to black. - Enable black player background - Mencocokkan warna pemutar layar penuh dengan yang diperkecil. - Aktifkan pencocokan warna pemutar - "Aktifkan dialog ringkas di ponsel. - -Masalah yang diketahui: -• Gambar album di Tab library juga menjadi lebih kecil. -• Tata letak pengatur waktu tidur mungkin terlihat tidak biasa." - Aktifkan dialog ringkas - Includes the buffer in the debug log. - Enable debug buffer logging - Mencetak catatan debug. - Aktifkan pencatatan debug - Mempertahankan pemutar agar tetap diminimalkan secara permanen meskipun trek lain diputar. - Aktifkan pemutar yang diminimalkan paksa - Mengaktifkan masuk ke mode lanskap dengan rotasi layar di ponsel. - Aktifkan mode lanskap - Adds a next track button to the miniplayer. - Add miniplayer next button - Adds a previous track button to the miniplayer. - Add miniplayer previous button - "Mengaktifkan codec audio opus alih-alih codec audio mp4a." - Aktifkan codec opus - Enables swipe down to dismiss miniplayer. - Enable swipe to dismiss miniplayer - "Menambahkan tombol Trim silence ke menu flyout playback speed. - -Info: -• Fitur ini hanya untuk podcast. -• Fitur ini masih dalam pengembangan, jadi ini tidak akan stabil." - Tambah switch Trim silence - Also enables Zen mode for podcasts. - Enable Zen mode in podcasts - Menambahkan rona abu-abu ke pemutar video untuk mengurangi ketegangan mata. - Aktifkan mode zen - Mulai ulang untuk memuat layout secara normal - Refresh dan mulai ulang - Export settings to file - Failed to export settings. - Settings were successfully exported. - Impor - Import settings from file - Salin - Import / Export settings as text - Impor atau ekspor setelan sebagai teks. - Ekspor / Impor - Import failed: %s. - Reset setelan ke default. - Setelan %d diimpor. - Reset - ReVanced Extended - "Tombol Unduh membuka Downloader eksternal kamu. - -• Hanya menggantikan tombol Unduh di player. -• Tidak bisa menggantikan tombol Unduh di menu flyout atau tab Library." - Ganti tombol tindakan Unduh - Downloader eksternal - "%1$s belum terinstall. -Download %2$s dari website." - Peringatan - %s tidak diinstal. Silakan instal. - Nama paket aplikasi downloader eksternal yang terinstal, seperti NewPipe atau YTDLnis. - Nama paket downloader eksternal - Menyembunyikan komponen kosong di menu akun. - Sembunyikan komponen kosong - Daftar dari nama-nama menu akun ke filter, terpisah oleh garis baru. - Filter menu Akun - Menyembunyikan elemen menu akun menggunakan filter custom. - Sembunyikan menu akun - Menyembunyikan tombol Simpan. - Sembunyikan tombol Save - Menyembunyikan tombol Komentar. - Sembunyikan tombol Komentar - Menyembunyikan tombol Unduh. - Sembunyikan tombol Unduh - Menyembunyikan bilah dari tombol tindakan. - Sembunyikan tombol bilah tindakan - Menyembunyikan tombol Like dan Dislike. Itu tidak akan bekerja di layout player lama. - Sembunyikan tombol Like dan Dislike - Menyembunyikan tombol Radio. - Sembunyikan tombol Radio - Menyembunyikan tombol Bagikan. - Sembunyikan tombol Bagikan - Hides the Audio / Video toggle in the player. - Hide Audio / Video toggle - Menyembunyikan rak tombol dari beranda dan eksplorasi. - Sembunyikan rak tombol - Menyembunyikan rak korsel dari beranda dan eksplorasi. - Sembunyikan rak korsel - Menyembunyikan tombol cast. - Sembunyikan tombol cast - Menyembunyikan bilah kategori musik di bagian atas beranda. - Sembunyikan bilah kategori - Hides the channel guidelines at the top of the comments section. - Hide channel guidelines - Hides the timestamp and emoji buttons when typing comments. - Hide timestamp and emoji buttons - Menyembunyikan overlay gelap yang muncul ketika double-tap to seek. - Sembunyikan filter overlay double-tap - Hides the floating button in the Library tab. - Hide floating button - Sembunyikan komponen 3-kolom - Sembunyikan menu tambahkan ke antrean - Sembunyikan menu teks - Sembunyikan menu hapus playlist - Sembunyikan menu abaikan antrean - Sembunyikan menu Unduh - Sembunyikan menu edit playlist - Sembunyikan menu Pergi ke album - Sembunyikan menu Pergi ke artis - Sembunyikan menu Pergi ke episode - Sembunyikan menu Pergi ke podcast - Sembunyikan menu bantuan & saran - Sembunyikan tombol Like dan Dislike - Sembunyikan menu putar berikutnya - Hide menu Kualitas - Sembunyikan menu hapus dari koleksi - Sembunyikan hapus dari menu playlist - Sembunyikan menu laporkan - Sembunyikan menu simpan episode untuk ditonton nanti - Sembunyikan menu simpan ke koleksi - Sembunyikan menu simpan ke playlist - Sembunyikan menu bagikan - Sembunyikan menu putar acak - Sembunyikan menu waktu tidur - Sembunyikan menu mulai radio - Sembunyikan menu statistik untuk nerds - Sembunyikan menu Subscribe / Unsubscribe - Sembunyikan menu kredit lagu - Menyembunyikan iklan fullscreen. - Sembunyikan iklan fullscreen - "Jika diaktifkan, iklan fullscreen akan ditutup lewat tombol Close. -Jika dimatikan, iklan fullscreen akan di block. (kemungkinan akan ada side effect)" - "If it is enabled, fullscreen ads are closed through the Close button. -If it is disabled, fullscreen ads are blocked. (there may be side effects)" - "If it is enabled, fullscreen ads are closed through the Close button. -If it is disabled, fullscreen ads are blocked. (there may be side effects)" - Tutup iklan fullscreen - Hides the Share button in the fullscreen player. - Hide fullscreen Share button - Menyembunyikan Iklan Umum. - Sembunyikan Iklan Umum - Menyembunyikan handle di menu akun. - Sembunyikan handle - Menyembunyikan tombol riwayat di toolbar. - Sembunyikan tombol riwayat - Menyembunyikan iklan sebelum memutar musik. - Sembunyikan iklan musik - Sembunyikan bilah navigasi. - Menyembunyikan bilah navigasi - Hides the Explore button. - Hide Explore button - Hides the Home button. - Hide Home button - Menyembunyikan label di bilah navigasi. - Sembunyikan label navigasi - Hides the Library button. - Hide Library button - Hides the Samples button. - Hide Samples button - Hides the Upgrade button. - Hide Upgrade button - Hides the Notifications button in the toolbar. - Hide Notifications button - Menyembunyikan label promosi berbayar. - Sembunyikan label promosi berbayar - Hides the playlist card shelf in the feed. - Hide playlist card shelf - Menyembunyikan popup promosi premium. - Sembunyikan popup promosi premium - Menyembunyikan banner pembaruan premium. - Sembunyikan banner pembaruan premium - Hides the Samples shelf in the feed. - Hide Samples shelf - "Sembunyikan elemen menu pengaturan. -Ini tidak hanya menyembunyikan menu pengaturan YT Music, tetapi juga menu pengaturan ReVanced Extended." - Sembunyikan menu pengaturan - Hides the sound search button in the search bar. - Hide sound search button - Hides the \'Tap to update\' button. - Hide \'Tap to update\' button - Menyembunyikan kontainer ketentuan layanan. - Sembunyikan kontainer ketentuan - Hides the voice search button in the search bar. - Hide voice search button - Akun - Bilah Tindakan - Iklan - Menu flyout - Umum - Miscellaneous - Bilah Navigasi - Player - Return YouTube Dislike - SponsorBlock - Video - Remembers the last playback speed selected. - Remember playback speed changes - Menunjukkan toast ketika mengubah playback speed semula. - Tampilkan toast - Changing default speed to %s. - Mengingat keadaan pengulangan. - Ingat keadaan pengulangan - Mengingat keadaan pengacakan. - Ingat keadaan pengacakan - Remembers the last video quality selected. - Remember video quality changes - Menunjukkan toast ketika mengubah playback speed semula. - Tampilkan toast - Changing default mobile data quality to %s. - Failed to set quality. - Changing default Wi-Fi quality to %s. - "Removes the viewer discretion dialog. -This does not bypass the age restriction. It just accepts it automatically." - Remove viewer discretion dialog - Melanjutkan video dari waktu saat ini ketika berlaih ke YouTube. - Lanjutkan menonton - Menggantikan menu hapus antrean menjadi tonton di YouTube. - Ganti menu hapus antrean - Tonton di YouTube - Url video tidak valid. - Mempertahankan menu laporkan di bagian komentar. - Simpan laporkan di komentar - Menggantikan menu laporkan dengan menu Kecepatan pemutaran. - Ganti menu laporkan - Returns the comments popup panels to the old style. - Restore old comments popup panels - Returns the player background to the old style. - Restore old player background - "Returns the player layout to the old style. -Some features may not work properly in the old player layout." - Restore old player layout - Returns the Library tab to the old style. (Experimental) - Restore old style library shelf - Tentang - Data disediakan oleh API Return YouTube Dislike. Tekan di sini untuk mempelajari lebih lanjut. - ReturnYouTubeDislike.com - Menyembunyikan pemisah tombol like. - Tombol like ringkas - Alih-alih jumlah dislike, yang ditampilkan adalah persentase dislike. - Dislike sebagai persentase - Menunjukkan jumlah dislike pada video. - Enable Return YouTube Dislike - Dislike tidak tersedia (batas API client tercapai). - Dislikes are unavailable (status %d). - Dislikes are temporarily unavailable (API timed out). - Dislikes are unavailable (%s). - Shows a toast if the Return YouTube Dislike API is unavailable. - Show a toast if API is unavailable - Menghapus parameter kueri pelacakan dari URL saat membagikan tautan. - Sanitasi tautan berbagi - About - sponsor.ajay.app - Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. - Change API URL - API URL changed. - API URL is invalid. - API URL reset. - The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. - Color changed. - Color: - Invalid color code. Color reset to default. - Color reset. - Change segment behavior - Enable SponsorBlock - SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. - Reset color - Filler Tangent / Jokes - Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. - Interaction Reminder (Subscribe) - A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. - Intermission / Intro Animation - An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. - Music: Non-Music Section - Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. - Endcards / Credits - Credits or when the YouTube endcards appear. Not for conclusions with information. - Preview / Recap / Hook - Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. - Unpaid / Self Promotion - Similar to \'Sponsor\' except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. - Sponsor - Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. - Skip automatically - Disable - Skipped filler. - Skipped annoying reminder. - Skipped intro. - Skipped intermission. - Skipped intermission. - Skipped multiple segments. - Skipped a non-music section. - Skipped outro. - Skipped preview. - Skipped recap. - Skipped preview. - Skipped self promotion. - Skipped sponsor. - SponsorBlock is temporarily unavailable. - SponsorBlock is temporarily unavailable (status %d). - SponsorBlock is temporarily unavailable (API timed out). - Show a toast if API is unavailable - Shows a toast if the SponsorBlock API is unavailable. - Show a toast when skipping automatically - Shows a toast when a segment is automatically skipped. - Setelan disalin ke papan klip. - "Memalsukan versi klien ke versi lama - -• Ini akan mengubah tampilan aplikasi, namun efek samping yang tidak diketahui mungkin terjadi. -• Jika nanti dinonaktifkan, UI lama mungkin tetap ada hingga aplikasi dihapus data." - 4.27.53 - Nonaktifkan mode radio di wilayah Kanada - 6.11.52 - Matikan Lirik real-time - Pilih target pemalsuan versi aplikasi. - Target pemalsuan versi aplikasi - Palsukan versi aplikasi - diff --git a/src/main/resources/music/translations/it-rIT/missing_strings.xml b/src/main/resources/music/translations/it-rIT/missing_strings.xml deleted file mode 100644 index 860d4a0dc..000000000 --- a/src/main/resources/music/translations/it-rIT/missing_strings.xml +++ /dev/null @@ -1,345 +0,0 @@ - - - Continue - Don\'t show again - "GmsCore does not have permission to run in the background. - -Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. - -This is required for the app to work." - "GmsCore battery optimizations must be disabled to prevent issues. - -Tap on the continue button and disable battery optimizations." - Open website - Action needed - Enable cloud messaging to receive notifications. - Open GmsCore - GmsCore is not installed. Install it. - Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. - Bypass image region restrictions - Change from in-app share sheet to system share sheet. - Change share sheet - Charts - Explore - Home - Library - Subscriptions - Select which page the app opens in. - Change start page - Invalid custom filter: %s. - Invalid custom playback speeds. - To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Disables Cairo splash animation when the app starts up. - Disable Cairo splash animation - Disables redirection to the next track when clicking the Dislike button. - Disable dislike redirection - Disable swipe to change tracks in the miniplayer. - Disable miniplayer gesture - Disable swipe to change tracks in the player. - Disable player gesture - Changes the player background color to black. - Enable black player background - "Enables the compact flyout menu on phones. - -Limitations: -• Album art in the Library tab becomes smaller when organized in a grid. -• Sleep timer layout may appear unusual." - Includes the buffer in the debug log. - Enable debug buffer logging - Adds a next track button to the miniplayer. - Add miniplayer next button - Adds a previous track button to the miniplayer. - Add miniplayer previous button - Enables swipe down to dismiss miniplayer. - Enable swipe to dismiss miniplayer - "Adds a Trim silence switch to the playback speed flyout menu. - -Info: -• This feature is for podcasts. -• This feature is still in development, so it may be unstable." - Add Trim silence switch - Also enables Zen mode for podcasts. - Enable Zen mode in podcasts - Reset to default values. - Restart to load the layout normally - Refresh and restart - Export settings to file - Failed to export settings. - Settings were successfully exported. - Import settings from file - Import / Export settings as text - Import failed: %s. - Reset - ReVanced Extended - "Download button opens your external downloader. - -• Only overrides the Download action button in the player. -• Does not override the Download button in the flyout menu or Library tab." - Override Download action button - External downloader - "%1$s is not installed. -Please download %2$s from the website." - Warning - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - List of account menu names to filter, separated by new lines. - Account menu filter - Hides the Save button. - Hide Save button - Hides the Comments button. - Hide Comments button - Hides the Download button. - Hide Download button - Hides the labels of the action buttons. - Hide action button labels - Hides the Like and Dislike buttons. It does not work in the old player layout. - Hide Like and Dislike buttons - Hides the Radio button. - Hide Radio button - Hides the Share button. - Hide Share button - Hides the Audio / Video toggle in the player. - Hide Audio / Video toggle - Hide carousel shelf - Hides the category bar. - Hide category bar - Hides the channel guidelines at the top of the comments section. - Hide channel guidelines - Hides the timestamp and emoji buttons when typing comments. - Hide timestamp and emoji buttons - Hides dark overlay that appears when double-tapping to seek. - Hide double-tap overlay filter - Hides the floating button in the Library tab. - Hide floating button - Hide 3-column component - Hide Add to queue menu - Hide Captions menu - Hide Delete playlist menu - Hide Dismiss queue menu - Hide Download menu - Hide Edit playlist menu - Hide Go to album menu - Hide Go to artist menu - Hide Go to episode menu - Hide Go to podcast menu - Hide Help & feedback menu - Hide Like and Dislike buttons - Hide Play next menu - Hide Quality menu - Hide Remove from library menu - Hide Remove from playlist menu - Hide Report menu - Hide Save episode for later menu - Hide Save to library menu - Hide Save to playlist menu - Hide Share menu - Hide Shuffle play menu - Hide Sleep timer menu - Hide Start radio menu - Hide Stats for nerds menu - Hide Subscribe / Unsubscribe menu - Hide View song credits menu - Hides fullscreen ads. - Hide fullscreen ads - "If it is enabled, fullscreen ads are closed through the Close button. -If it is disabled, fullscreen ads are blocked. (there may be side effects)" - Fullscreen ads are blocked. (there may be side effects) - Fullscreen ads are closed through the Close button. - Close fullscreen ads - Hides the Share button in the fullscreen player. - Hide fullscreen Share button - Hides general ads. - Hide general ads - Hides the handle in the account menu. - Hide handle - Hides the History button in the toolbar. - Hide History button - Hides ads before playing media. - Hides the navigation bar. - Hide navigation bar - Hides the Explore button. - Hide Explore button - Hides the Home button. - Hide Home button - Hides labels below the navigation buttons. - Hides the Library button. - Hide Library button - Hides the Samples button. - Hide Samples button - Hides the Upgrade button. - Hide Upgrade button - Hides the Notifications button in the toolbar. - Hide Notifications button - Hides the paid promotion label. - Hide paid promotion label - Hides the playlist card shelf in the feed. - Hide playlist card shelf - Hides premium promotion popups. - Hide premium promotion popups - Hides the premium renewal banner. - Hide premium renewal banner - Hides the promotion alert banner. - Hide promotion alert banner - Hides the Samples shelf in the feed. - Hide Samples shelf - Hide About menu - Hide Data saving menu - Hide Downloads & storage menu - Hide General menu - Hide Notifications menu - Hide Get Music premium menu - Hide Family Center menu - Hide Playback menu - Hide Privacy & data menu - Hide Recommendations menu - "Hide elements of the settings menu. -This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." - Hide settings menu - Hides the sound search button in the search bar. - Hide sound search button - Hides the Tap to update button. - Hide Tap to update button - Hides the voice search button in the search bar. - Hide voice search button - Account - Action Bar - Ads - Flyout Menu - General - Miscellaneous - Navigation Bar - Player - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Settings menu - Video - Remembers the last playback speed selected. - Remember playback speed changes - Show a toast when changing the default playback speed. - Show a toast - Changing default speed to %s. - Remembers the last video quality selected. - Remember video quality changes - Show a toast when changing the default video quality. - Show a toast - Changing default mobile data quality to %s. - Failed to set quality. - Changing default Wi-Fi quality to %s. - "Removes the viewer discretion dialog. -This does not bypass the age restriction. It just accepts it automatically." - Remove viewer discretion dialog - Continues the video from the current time when switching to YouTube. - Continue watching - Replaces the Dismiss queue menu with the Watch on YouTube menu. - Replace Dismiss queue menu - Watch on YouTube - Invalid video url. - Keeps the Report menu in the comments section intact. - Keep Report in comments - Replaces the Report menu with the Playback speed menu. - Replace Report menu - Returns the comments popup panels to the old style. - Restore old comments popup panels - Returns the player background to the old style. - Restore old player background - "Returns the player layout to the old style. -Some features may not work properly in the old player layout." - Restore old player layout - Returns the Library tab to the old style. (Experimental) - Restore old style library shelf - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Username - Replaces handles with usernames in comments. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - ReturnYouTubeDislike.com - Enable Return YouTube Dislike - Shows the estimated like count of videos. - Show estimated likes - Dislikes are unavailable (status %d). - Dislikes are temporarily unavailable (API timed out). - Dislikes are unavailable (%s). - Shows a toast if the Return YouTube Dislike API is unavailable. - Show a toast if API is unavailable - Hidden - Removes tracking query parameters from URLs when sharing links. - Sanitize sharing links - About - sponsor.ajay.app - Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. - Change API URL - API URL changed. - API URL is invalid. - API URL reset. - The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. - Color changed. - Color: - Invalid color code. - Color reset. - Change segment behavior - Enable SponsorBlock - SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. - Reset color - Filler Tangent / Jokes - Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. - Interaction Reminder (Subscribe) - A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. - Intermission / Intro Animation - An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. - Music: Non-Music Section - Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. - Endcards / Credits - Credits or when the YouTube endcards appear. Not for conclusions with information. - Preview / Recap / Hook - Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. - Unpaid / Self Promotion - Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. - Sponsor - Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. - Skip automatically - Disable - Skipped filler. - Skipped annoying reminder. - Skipped intro. - Skipped intermission. - Skipped intermission. - Skipped multiple segments. - Skipped a non-music section. - Skipped outro. - Skipped preview. - Skipped recap. - Skipped preview. - Skipped self promotion. - Skipped sponsor. - SponsorBlock is temporarily unavailable. - SponsorBlock is temporarily unavailable (status %d). - SponsorBlock is temporarily unavailable (API timed out). - Show a toast if API is unavailable - Shows a toast if the SponsorBlock API is unavailable. - Show a toast when skipping automatically - Shows a toast when a segment is automatically skipped. - Settings copied to clipboard. - "Spoofs the client version to an older version. - -• This will change the appearance of the app, but unknown side effects may occur. -• If later disabled, the old UI may remain until the app data is cleared." - 4.27.53 - Disable Radio mode in Canadian regions - 6.11.52 - Disable real-time lyrics - 7.16.53 - Restore old action bar - Select the spoof app version target. - Spoof app version target - diff --git a/src/main/resources/music/translations/it-rIT/strings.xml b/src/main/resources/music/translations/it-rIT/strings.xml deleted file mode 100644 index 4d587a4d4..000000000 --- a/src/main/resources/music/translations/it-rIT/strings.xml +++ /dev/null @@ -1,62 +0,0 @@ - - - Filtra i nomi dei componenti separati da righe. - Modifica i filtri personalizzati - Abilita il filtro personalizzato per nascondere i componenti del layout. - Abilita filtri personalizzati - Velocità di riproduzione personalizzate non valide. Ripristina i valori predefiniti. - Aggiungi o modifica le velocità di riproduzione disponibili - Modifica velocità di riproduzione personalizzate - Sottotitoli automatici forzati disabilitati. - Disabilita i sottotitoli automatici forzati - Imposta il colore della barra di navigazione su nero. - Abilita la barra di navigazione nera - Allinea il colore del lettore a schermo intero con quello in secondo piano. - Abilita l\'abbinamento di colore dei Riproduttori - Abilita dialogo compatto - Stampa il registro di debug. - Abilita la registrazione del debug - Mantieni il riproduttore in secondo piano anche se un\'altra traccia viene riprodotta. - Abilita il riproduttore in secondo piano forzato - Consente l\'accesso alla modalità orizzontale ruotando lo schermo del telefono. - Abilita la modalità orizzontale - "Abilita il codec Opus 250/251 durante la riproduzione dell'audio." - Abilita il codec opus - Aggiunge una sfumatura grigia al riproduttore video per ridurre l\'affaticamento degli occhi. - Abilita la modalità zen - Importa - Copia - Importa o esporta le impostazioni come testo. - Importa/Esporta - Ripristino impostazioni ai valori predefiniti - Impostazioni %d importate - %s non è installato. Installalo. - Nome del pacchetto dell\'app downloader esterna installata, come NewPipe o Seal. - Nome del pacchetto downloader esterno - Nasconde i componenti vuoti nel menu dell\'account - Nascondi componente vuoto - Nascondi gli elementi del menu dell\'account. - Nascondi il menu dell\'account - Nasconde lo scaffale dei pulsanti dalla home page e da Explorer. - Nasconde lo scaffale dei pulsanti - Nasconde lo scaffale del carosello dalla home page e da Explorer. - Nascondo il pulsante cast nella parte superiore della homepage e in cima al riproduttore. - Nascondi il bottone cast - Nascondi le pubblicità musicali - Nascondi etichetta di navigazione - Nasconde il contenitore dei termini di servizio. - Nascondi contenitore termini - Ricorda lo stato della ripetizione. - Ricorda lo stato di ripetizione - Ricorda lo stato della riproduzione casuale. - Ricorda lo stato della riproduzione casuale - Informazioni - I dati vengono forniti dall\'API Return YouTube Dislike. Tocca qui per saperne di più. - Nasconde il separatore del pulsante \"Mi piace\". - Pulsante \"Mi piace\" compatto - Al posto del numero di \"Non mi piace\", viene mostrata la percentuale dei Non mi piace. - \"Non mi piace\" in percentuale - Mostra il numero di \"Non mi piace\" dei video. - \"Non mi piace\" non disponibile (limite API client raggiunto) - Versione dell\'app falsificata - diff --git a/src/main/resources/music/translations/ja-rJP/missing_strings.xml b/src/main/resources/music/translations/ja-rJP/missing_strings.xml deleted file mode 100644 index 11cf8ef8d..000000000 --- a/src/main/resources/music/translations/ja-rJP/missing_strings.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Reset to default values. - Hide About menu - Hide Get Music premium menu - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Replaces handles with usernames in comments. - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - Shows the estimated like count of videos. - Show estimated likes - Hidden - diff --git a/src/main/resources/music/translations/ja-rJP/strings.xml b/src/main/resources/music/translations/ja-rJP/strings.xml deleted file mode 100644 index 87c425d61..000000000 --- a/src/main/resources/music/translations/ja-rJP/strings.xml +++ /dev/null @@ -1,382 +0,0 @@ - - - 続行 - 今後表示しない - "MicroG GmsCoreはバックグラウンドで実行する権限がありません。\n\nあなたの端末の \"Don't kill my app \"のガイドに従って、MicroGのインストールに適用してください。\n\nこれはアプリが動作するために必要です。" - "問題を防ぐためにGmsCoreのバッテリー最適化を無効にする必要があります。 - -「続行」をタップし、バッテリーの最適化を無効にします。" - ウェブサイトを開く - 操作が必要です - 通知を受け取るには、Cloud Messaging 設定を有効にしてください。 - GmsCoreを開く - GmsCoreがインストールされていません。インストールしてください。 - プレイリストのサムネイルやチャンネルアバターなどを受信できるように、一部の地域でブロックされているドメインを置き換えます。 - 画像表示の地域制限を回避 - アプリ内共有メニューからシステムの共有メニューに置き換えます。 - 共有メニューを変更 - チャート - 探索 - ホーム - ライブラリ - 定期購入 - アプリのスタートページを変更します。 - スタートページを変更 - コンポーネント名でフィルター (改行区切り) - カスタムフィルターを編集 - カスタムフィルターを有効にします。 - カスタムフィルター - 無効なカスタムフィルターです: %s。 - 無効なカスタム再生速度です。デフォルト値にリセットします。 - 無効なカスタム再生速度です。デフォルトの値を使用します。 - 利用可能な再生速度を編集します。 - カスタム再生速度の編集 - 動画側で設定されている、字幕の強制は無効です。 - 字幕の強制を無効化 - アプリ起動時のCairo のスプラッシュアニメーションを無効にします。 - Cairo スプラッシュアニメーションを無効にする - 低評価ボタンを押したとき、次の曲へのリダイレクトするのを無効にする。 - 低評価リダイレクトを無効化 - ミニプレーヤーでスワイプによる曲の変更を無効にします - ミニプレーヤージェスチャーを無効にする - プレイヤーでスワイプによる曲の変更を無効にします。 - プレイヤージェスチャーを無効にする - ナビゲーションバーの色を黒に設定します。 - 黒いナビゲーションバーを有効化 - プレイヤーの背景の色を黒に固定します。 - 黒のプレイヤー背景を有効化 - ミニプレーヤーと全画面プレーヤーの色を統一します。 - カラーマッチプレーヤーを有効化 - "コンパクトなダイアログを有効にします。 - -既知の問題: -• ライブラリのアルバムアートが小さくなります。 -• スリープタイマーのレイアウトが異常になる場合があります。" - コンパクトなダイアログ - デバッグログをバッファに含めて出力する。 - デバッグバッファログを有効化 - デバッグログを出力します。 - デバッグログ - 他のトラックが再生されていても、プレーヤーを常に最小化したままにします。 - 最小化されたプレーヤーを有効にする - 画面回転で横画面モードに入るようにします。 - 横画面モードを有効化 - ミニプレーヤーの「次の曲に進むボタン」を表示します。 - 「次の曲に進むボタン」を表示 - ミニプレーヤーで「前の曲に戻るボタン」を表示します。 - 「前の曲に戻るボタン」を表示 - "MP4A コーデックの代わりに、Opus コーデックを適用します。" - Opus コーデックを有効化 - 下にスワイプしてミニプレーヤーを閉じられるようにします。 - スワイプしてミニプレーヤーを閉じる - "再生スピードのフライアウトメニューで「無音トリム」スイッチを有効にする。 - -情報 -- この機能はポッドキャスト用です。 -- この機能はまだ開発中のため、不安定な場合があります。" - 「無音トリム」を有効化 - ポッドキャストにもZenモードを適用します。 - ポッドキャストでZenモードを有効化 - 動画プレーヤーに灰色の色合いを追加し、目の疲れを軽減します。 - Zen モードを有効化 - 再起動してレイアウトを正常に読み込みます - 再起動して更新 - 設定をファイルにエクスポート - 設定のエクスポートに失敗しました。 - 設定は正常にエクスポートされました。 - インポート - ファイルから設定をインポート - コピー - テキストとしてインポート/エクスポート - 設定をテキストとしてインポート/エクスポートします。 - 設定のインポート/エクスポート - インポートに失敗: %s - 設定をデフォルトにリセットしました。 - %d の設定をインポートしました。 - リセット - ReVanced Extended - "ダウンロードボタンをでのダウンロードを外部のアプリで行います。 - -• プレーヤー内のダウンロードボタンのみを置き換えます。 -• フライアウトメニューまたはライブラリのダウンロードボタンは置き換えません。" - ダウンロードボタンを置き換える - 外部ダウンローダーを選択 - "%1$s はインストールされていません。 -ウェブサイトから %2$s をダウンロードしてください。" - 警告 - %s はインストールされていません。インストールしてください。 - NewPipe や YTDLnis などの、インストールされている外部ダウンローダーアプリのパッケージ名。 - 外部ダウンローダーのパッケージ名 - アプリの起動時に、GMSCore 最適化ダイアログを表示します。 - GMSCore の最適化ダイアログを表示 - アカウントメニューの空のコンポーネントを非表示にします。 - 空のコンポーネントを非表示 - フィルタリングするメニュー名(改行区切り) - アカウントメニューフィルター - アカウントメニューの要素を非表示にします。 - アカウントメニューを非表示 - プレイリストに追加ボタンを非表示にします。 - プレイリストに追加ボタンを非表示 - コメントボタンを非表示にします。 - コメントボタンを非表示 - ダウンロードボタンを非表示にします。 - ダウンロードボタンを非表示 - アクションボタンのラベルを非表示にします。 - アクションボタンのラベルを非表示 - 高評価ボタンや低評価ボタンを非表示にします。古いプレイヤーのレイアウトでは動作しません。 - 評価ボタンを非表示 - ラジオボタンを非表示にします。 - ラジオボタンを非表示 - 共有ボタンを非表示にします。 - 共有ボタンを非表示 - プレイヤーの曲と動画の切り替えスイッチを非表示にします。 - 曲と動画の切り替えスイッチを非表示 - ホームタブや探索タブのボタン欄を非表示にします。 - ボタン欄を非表示 - ホームタブや探索タブのカルーセル欄を非表示にします。 - カルーセル欄を非表示 - キャストボタンを非表示にします。 - キャストボタンを非表示 - ホームタブの上部にある音楽カテゴリーバーを非表示にします。 - カテゴリーバーを非表示 - コメント欄上部のコミュニティガイドラインを非表示にします。 - コミュニティガイドラインを非表示 - コメントを入力するときにタイムスタンプと絵文字ボタンを非表示にします。 - タイムスタンプと絵文字ボタンを非表示 - ダブルタップしてシークすると表示される暗いオーバーレイを非表示にします。 - ダブルタップオーバーレイフィルタを非表示 - ライブラリのフローティングボタンを非表示にします。 - フローティングボタンを非表示 - 3点メニューのコンポーネントを非表示 - 「キューに追加」を非表示 - 字幕メニューを非表示にする - プレイリスト削除メニューを非表示 - 「キューを閉じる」を非表示 - 「オフラインに一時保存」を非表示 - プレイリスト編集メニューを非表示 - 「アルバムに移動」を非表示 - 「アーティストに移動」を非表示 - 「エピソードに移動」を非表示 - 「ポッドキャストに移動」を非表示 - 「ヘルプとフィードバック」を非表示 - 高評価/低評価ボタンを非表示 - 「次に再生」を非表示 - 画質メニューを非表示 - 「ライブラリから削除」を非表示 - 「プレイリストから削除」を非表示 - 「報告」を非表示 - 「エピソードを保存」を非表示 - 「ライブラリに保存」を非表示 - 「再生リストに保存」を非表示 - 「共有」を非表示 - シャッフル再生メニューを非表示 - スリープタイマーメニューを非表示 - 「ラジオを聴く」を非表示 - 統計情報を非表示 - 登録/解除メニューを非表示 - 「曲のクレジットを表示」を非表示 - 全画面広告を非表示にします。 - 全画面広告を非表示 - "有効の場合、全画面広告は閉じるボタンで閉じられます。 -無効にすると、全画面広告はブロックされます(バグが発生するかもしれません)" - "有効の場合、全画面広告は閉じるボタンで閉じられます。 -無効にすると、全画面広告はブロックされます(バグが発生するかもしれません)" - "有効の場合、全画面広告は閉じるボタンで閉じられます。 -無効にすると、全画面広告はブロックされます(バグが発生するかもしれません)" - 全画面広告を閉じる - 全画面表示のプレイヤーの共有ボタンを非表示にします。 - 全画面共有ボタンを非表示 - 一般広告を非表示にします。 - 一般広告を非表示 - アカウントスイッチャーでハンドルを非表示にします。 - ハンドルを非表示 - ツールバーの履歴ボタンを非表示にします。 - 履歴ボタンを非表示 - トラックを再生する前の広告を非表示にします。 - 音楽の広告を非表示 - ナビゲーションバーを非表示にします。 - ナビゲーションバーを非表示 - 探索ボタンを非表示にします。 - 探索ボタンを非表示 - ホームボタンを非表示にします。 - ホームボタンを非表示 - ナビゲーションバーのラベルを非表示にします。 - ナビゲーションバーのラベルを非表示 - ライブラリボタンを非表示にします。 - ライブラリボタンを非表示 - サンプルボタンを非表示にします。 - サンプルボタンを非表示 - アップグレードボタンを非表示にします。 - アップグレードボタンを非表示 - ツールバーの通知ボタンを非表示にします。 - 通知ボタンを非表示 - 有料プロモーションラベルを非表示にします。 - 有料プロモーションバナーを非表示 - プレイリストシェルフを非表示にします。 - プレイリストシェルフを非表示 - プレミアムプロモーションポップアップを非表示にします。 - プレミアムプロモーションポップアップを非表示 - プレミアム更新バナーを非表示にします。 - プレミアム更新バナーを非表示 - プロモーションバナーを非表示にします。 - プロモーションバナーを非表示 - フィードからサンプルシェルフを非表示にします。 - サンプルシェルフを非表示 - 「データの節約」を非表示 - 「一時保存とストレージ」を非表示 - 「全般」を非表示 - 「通知」を非表示 - 「ファミリーセンター」を非表示 - 「再生」を非表示 - 「プライバシーとデータ」を非表示 - 「おすすめ」を非表示 - "設定の要素を非表示にします。 -YT Music の設定だけでなく、ReVanced Extended の設定も非表示にします。" - 設定メニューを非表示 - 検索バーのサウンドサーチボタンを非表示にします。 - サウンドサーチボタンを非表示 - 「タップして更新」ボタンを非表示にします。 - 「タップして更新」ボタンを非表示 - 利用規約コンテナーを非表示にします。 - 利用規約を非表示 - 検索バーのボイスサーチボタンを非表示にします。 - ボイスサーチボタンを非表示 - アカウント - アクションバー - 広告 - フライアウトメニュー - 全般 - その他 - ナビゲーションバー - プレーヤー - Return YouTube Username - Return YouTube Dislike - SponsorBlock - 設定メニュー - 動画 - 再生速度を変更するたびに、再生速度を保存します。 - 再生速度の変更を保存 - デフォルトの再生速度を変更するときにトーストを表示します。 - トーストを表示 - デフォルトの再生速度を %s に変更しました。 - リピートの状態を記憶します。 - リピートの状態を保存 - シャッフルの状態を記憶します。 - シャッフルの状態を保存 - 画質を変更するたびに、画質を保存します。 - ビデオ画質の変更を保存 - デフォルトの画質を変更するときにトーストを表示します。 - トーストを表示 - モバイルネットワーク使用時のデフォルト画質を %s に変更しました。 - 画質の設定に失敗しました。 - Wi-Fi 使用時のデフォルト画質を %s に変更しました。 - "視聴者の裁量ダイアログを削除します。 -これは年齢制限を回避するものではなく、自動的に受け入れられるだけです。" - ビューアの裁量ダイアログを削除 - YouTubeに切り替えたときに、現在の時間から再生します。 - 視聴を続ける - 「キューを閉じる」を「YouTube で視聴」に置き換えます。 - 「キューを閉じる」メニューを置き換え - YouTube で視聴 - 動画のURLが無効です。 - コメントのレポート メニューは置き換えられません。 - プレイヤーのフライアウトメニューにのみ適用 - 「報告」を「再生速度」に置き換えます。 - 「報告」を置き換え - コメントポップアップパネルを古いスタイルに戻します。 - 古いコメントポップアップパネルを有効化 - プレイヤーの背景を古いスタイルに戻します。 - 古いプレイヤーの背景を有効化 - "プレイヤーのレイアウトを古いスタイルに戻します。 -一部の機能は正しく動作しない可能性があります。" - 古いプレーヤーのレイアウト - ライブラリのUIを古いスタイルに戻します (実験的) - 古いスタイルのライブラリを有効化 - ユーザーネーム - Return YouTube Username を有効化 - YouTube Data API キーについて - YouTube Data API v3 を使用するための開発者キー。 - YouTube Data API キー - Return YouTube Dislike について - 低評価のデータは、Return YouTube Dislike API によって提供されています。詳細はここをタップしてください。 - ReturnYouTubeDislike.com - 高評価ボタンの区切りを非表示にします。 - コンパクトな高評価ボタン - 低評価はパーセンテージで表示されます。 - 低評価数の形式の切り替え - 低評価は数字で表示されます。 - Return YouTube Dislike を有効化 - 低評価数は利用できません (クライアント API 制限) - 低評価数は一時的に利用できません。(ステータス %d) - 低評価数は一時的に利用できません。(API タイムアウト) - 低評価数は一時的に利用できません。(%s) - RYDが利用できない場合、メッセージが表示されます。 - API が利用できない場合にメッセージを表示 - リンクを共有する際に、URL からトラッキングクエリパラメーターを削除します。 - 共有リンクのクリーンアップ - この機能について - sponsor.ajay.app - データは SponsorBlock API によって提供されています。他のプラットフォームのダウンロードや詳細については、ここをタップしてください。 - APIのURLを変更 - APIのURLを変更しました。 - APIのURLが無効です。 - APIのURLをリセットしました。 - SponsorBlock がサーバーとの通信で使うアドレスです。自分が何をしているのか理解していない場合は、変更しないでください。 - 色を変更しました。 - 色: - カラーコードが無効です。既定の値に戻します。 - 色をリセットしました。 - セグメントの設定 - Sponsor Block を有効化 - SponsorBlock は、YouTube の動画の迷惑な部分をスキップするためのクラウドソーシングシステムです。 - 色をリセット - 繋ぎの話 / 冗談 - 動画の本編を理解するのに必要のない繋ぎの話やユーモアなどの逸脱したシーン。コンテクストや背景情報の詳細は含まれません。 - リマインダー(チャンネル登録などの催促) - 動画の途中に挿入される高評価、チャンネル登録、フォローなどを促す短いリマインダーは、長いものや何か具体的なものは「セルフプロモーション」に分類するべきです。 - 休憩 / イントロアニメーション - 本編ではない部分。一時停止、静止画面、アニメーションの繰り返しが含まれます。情報を含んだ転換画面は含まれません。 - MV: 音楽ではない区間 - ミュージックビデオでのみ使用できます。他のカテゴリーに含まれていない、ミュージックビデオの音楽のない区間。 - エンドカード / クレジット - クレジットや動画のエンドカードが表示されている場面。情報を含む結論は含まれません。 - 予告 / 要約 / フック - この動画やシリーズの他の動画で起きた、または今後起きる内容などをまとめたクリップのコレクション。すべての情報は、別の場所で繰り返し表示されます。 - 無報酬 / セルフプロモーション - 無報酬のプロモーションあるいはセルフプロモーションであるという点を除いては「スポンサー」と同様です。商品、寄付、コラボ情報に関する内容を含みます。 - スポンサー - 有料プロモーション、有料紹介、直接広告が含まれます。セルフプロモーションや、個人の好きなクリエイター/ウェブサイト/商品に対する無償の活動は含まれません。 - 自動的にスキップ - 無効 - 繋ぎの話をスキップしました。 - リマインダーをスキップしました。 - イントロをスキップしました。 - 休憩をスキップしました。 - 休憩をスキップしました。 - 複数のセグメントをスキップしました - 音楽ではない区間をスキップしました。 - アウトロをスキップしました。 - 予告をスキップしました。 - 要約をスキップしました。 - 予告をスキップしました。 - セルフプロモーションをスキップしました。 - スポンサーをスキップしました。 - SponsorBlock は一時的に利用できません。 - SponsorBlock は一時的に利用できません。(ステータス %d) - SponsorBlock は一時的に利用できません。(API タイムアウト) - API が利用できない場合にメッセージを表示 - SponsorBlock API が利用できない場合、メッセージが表示されます。 - 自動的にスキップする時にトーストを表示する - セグメントが自動的にスキップされたときにトーストを表示します。 - 設定をクリップボードにコピーしました。 - "YT Music のバージョンを古いバージョンに偽装します。 - -• これによりアプリの外観が変わりますが、未知の問題が発生する場合があります。 -• 後からこの機能を無効にしても、データを消去するまで古い UI のままになる場合があります。" - 4.27.53 - カナダの地域でラジオモードを無効化 - 6.11.52 - リアルタイムの歌詞を無効化 - 7.16.53 - 古いアクションバーを復元 - 偽装するバージョンを選択してください。 - 偽装するバージョン - アプリのバージョンを偽装 - diff --git a/src/main/resources/music/translations/ko-rKR/strings.xml b/src/main/resources/music/translations/ko-rKR/strings.xml deleted file mode 100644 index 293ca358a..000000000 --- a/src/main/resources/music/translations/ko-rKR/strings.xml +++ /dev/null @@ -1,411 +0,0 @@ - - - 계속하기 - 다시 보지 않기 - "GmsCore에 백그라운드에서 실행할 수 있는 권한이 없습니다. - -이 기기에 대한 \"Don't kill my app\" 가이드를 읽어보고, GmsCore 설치 지침을 적용하세요. - -앱을 실행하려면 이 과정이 필요합니다." - "GmsCore를 배터리 최적화 목록에서 제외하여 앱 문제를 방지할 수 있습니다. - -배터리 최적화 목록에서 제외하려면 '계속하기' 버튼을 누르세요." - 웹사이트 열기 - 필수 조치 - 알림 수신을 위한 클라우드 메시징 설정을 할 수 있습니다. - GmsCore 열기 - GmsCore가 설치되어 있지 않습니다. 설치하세요. - 이미지 도메인을 변경하여 일부 국가에서 차단된 재생목록 썸네일, 채널 프로필 사진, 커뮤니티 게시물 이미지 등을 수신할 수 있습니다. - 이미지 표시 제한 국가 우회 - YT Music 기본 공유 시트에서 Android 기본 공유 시트로 변경합니다.\n\n• 공유 버튼으로 바로 Android 기본 공유 메뉴를 실행할 수 있습니다. - 공유 시트 변경 - 차트 - 둘러보기 - - 보관함 - 구독 - 앱 시작 페이지를 변경합니다. - 앱 시작 페이지 변경 - 필터링할 구성요소를 줄바꿈으로 구분하여 설정합니다. - 사용자 정의 필터 - 사용자 정의 필터를 활성화하여 레이아웃 구성요소를 숨깁니다. - 사용자 정의 필터 활성화 - 잘못된 필터 값입니다: %s - 사용자 정의 재생 속도는 %s배속보다 작아야 합니다. - 잘못된 재생 속도 값입니다. - 사용하고 싶은 재생 속도 값을 추가하거나 변경할 수 있습니다. - 사용자 정의 재생 속도 편집 - YT Music 링크를 RVX Music으로 열려면 \'지원되는 링크 열기\'를 활성화하고 지원되는 링크를 추가하세요. 링크 추가가 잠겨있다면 순정 YT Music 앱 정보 → \'기본적으로 열기\'에서 \'지원되는 링크 열기\'를 비활성화한 후에 추가할 수 있습니다. - 기본 앱 설정 열기 - 자막 사용이 강제된 동영상에서 자막을 비활성화합니다. - 자동 자막 비활성화 - 앱을 시작할 때, Cairo 스플래시 애니메이션을 비활성화합니다. - Cairo 스플래시 애니메이션 비활성화 - \'싫어요 버튼을 누르면 다음 트랙으로 리다이렉션\'을 비활성화합니다. - 싫어요 리다이렉션 비활성화 - 미니 플레이어에서 \'스와이프 제스처로 트랙 변경\'을 비활성화합니다. - 미니 플레이어 제스처 비활성화 - 플레이어에서 \'스와이프 제스처로 트랙 변경\'을 비활성화합니다. - 플레이어 제스처 비활성화 - 하단바 색상을 검정으로 설정합니다. - 검정 하단바 활성화 - 플레이어 배경 색상을 검정으로 설정합니다. - 검정 플레이어 배경 활성화 - 최소화 상태의 플레이어와 전체 화면 플레이어의 색상을 통일시킵니다. - 색상 일치 플레이어 활성화 - "휴대폰에서 소형 메뉴 구성요소를 활성화합니다. - -알려진 문제점: -• 보관함 탭에서 앨범 아트가 그리드로 구성될 때 작아집니다. -• 취침 타이머 레이아웃이 비정상적으로 보일 수 있습니다." - 소형 다이얼로그 활성화 - 디버그 로그에 버퍼를 포함하여 출력합니다. - 디버그 버퍼 로깅 활성화 - 디버그 로그를 출력합니다. - 디버그 로깅 활성화 - 다른 트랙이 재생되더라도 플레이어를 항상 최소화 상태로 유지합니다. - 플레이어를 항상 최소화 상태로 유지 - 앱을 가로로 회전할 수 있도록 합니다. - 가로 모드 활성화 - 미니 플레이어에서 다음 버튼을 활성화합니다. - 미니 플레이어에서 다음 버튼 활성화 - 미니 플레이어에서 이전 버튼을 활성화합니다. - 미니 플레이어에서 이전 버튼 활성화 - "플레이어 응답에 OPUS 코덱이 포함된 경우에는 OPUS 코덱을 활성화합니다. - -알림: -• 최신 YT Music 클라이언트는 기본적으로 OPUS 오디오 코덱을 사용합니다. -• 이 설정은 아주 오래된 클라이언트 사용자에게만 유효합니다." - OPUS 코덱 활성화 - 아래로 스와이프하여 미니 플레이어 닫기를 활성화합니다. - 스와이프하여 미니 플레이어 닫기 활성화 - "재생 속도 메뉴 구성요소에 '무음 건너뛰기' 스위치를 추가합니다. - -알림: -• 팟캐스트 기능입니다. -• 이 기능은 아직 개발 중이므로 불안정할 수 있습니다." - 무음 건너뛰기 스위치 추가 - 팟캐스트에서 집중 모드를 활성화합니다. - 팟캐스트에서 집중 모드 활성화 - 동영상 플레이어의 색상을 회색조로 설정해 눈의 피로를 줄입니다. - 집중 모드 활성화 - 기본값으로 초기화합니다. - 레이아웃을 정상적으로 불러오기 위해 다시 시작합니다. - 새로고침 및 다시 시작 - 파일로 설정 내보내기 - 설정을 내보내는 데 실패하였습니다. - 설정을 성공적으로 내보냈습니다. - 가져오기 - 파일에서 설정 가져오기 - 복사하기 - 텍스트로 설정 가져오기 / 내보내기 - 설정을 가져오거나 내보낼 수 있습니다. - 설정 가져오기 / 내보내기 - 가져오기를 실패하였습니다: %s - 설정을 기본값으로 초기화합니다. - %d 설정을 가져왔습니다. - 초기화 - ReVanced Extended 설정 - "오프라인 저장 버튼으로 외부 다운로더 앱을 실행할 수 있습니다. - -• 플레이어 하단에 있는 오프라인 저장 버튼만 재정의할 수 있습니다. -• 메뉴 구성요소 또는 보관함에서는 오프라인 저장 버튼을 재정의할 수 없습니다." - 오프라인 저장 버튼 재정의 - 외부 다운로더 앱 - "%1$s 가 설치되어 있지 않습니다. -웹사이트에서 %2$s 를 다운로드하세요." - 경고 - %s가 설치되지 않았습니다. 설치해주세요. - NewPipe 또는 YTDLnis와 같은 설치된 외부 다운로더 앱 패키지명입니다. - 외부 다운로더 앱 패키지명 - 앱을 시작할 때마다 GmsCore에 대한 배터리 최적화 다이얼로그를 표시합니다. - GmsCore 배터리 최적화 다이얼로그 표시 - 계정 메뉴에서 비어있는 구성요소를 숨깁니다. - 비어있는 구성요소 제거 - 필터링할 계정 메뉴 이름 목록을 줄바꿈으로 구분하여 설정합니다. - 계정 메뉴 필터 - 사용자 정의 필터를 사용하여 계정 메뉴 구성요소를 숨깁니다. - 계정 메뉴 제거 - (재생목록에) 저장 버튼을 숨깁니다. - (재생목록에) 저장 버튼 제거 - 댓글 버튼을 숨깁니다. - 댓글 버튼 제거 - 오프라인 저장 버튼을 숨깁니다. - 오프라인 저장 버튼 제거 - 액션 버튼에서 라벨을 숨깁니다. - 액션 버튼 라벨 제거 - 좋아요 & 싫어요 버튼을 숨깁니다. \n이전 플레이어 레이아웃에서는 작동하지 않습니다. - 좋아요 & 싫어요 버튼 제거 - 뮤직 스테이션 버튼을 숨깁니다. - 뮤직 스테이션 버튼 제거 - 공유 버튼을 숨깁니다. - 공유 버튼 제거 - 플레이어에서 \'노래↔동영상\' 전환 토글을 숨깁니다. - \'노래↔동영상\' 전환 토글 제거 - 피드에서 버튼형 선반을 숨깁니다. - 버튼형 선반 제거 - 피드에서 좌우 슬라이드형 선반을 숨깁니다. - 좌우 슬라이드형 선반 제거 - 크롬캐스트 버튼을 숨깁니다. - 크롬캐스트 버튼 제거 - 카테고리 바를 숨깁니다. - 카테고리 바 제거 - 댓글 섹션 상단에서 커뮤니티 가이드라인을 숨깁니다. - 커뮤니티 가이드라인 제거 - 댓글을 입력할 때, 타임스탬프 및 이모지 버튼을 숨깁니다. - 타임스탬프, 이모지 버튼 제거 - 두 번 눌러서 탐색할 때 표시되는 어두운 오버레이를 숨깁니다. - 두 번 누르기 오버레이 필터 - 보관함에서 플로팅 버튼을 숨깁니다. - 플로팅 버튼 제거 - 3-열 구성요소 제거 - 현재 재생목록에 추가 메뉴 제거 - 자막 메뉴 제거 - 재생목록 삭제 메뉴 제거 - 현재 재생목록 닫기 메뉴 제거 - 오프라인 저장 메뉴 제거 - 재생목록 수정 메뉴 제거 - 앨범으로 이동 메뉴 제거 - 아티스트 페이지로 이동 메뉴 제거 - 에피소드로 이동 메뉴 제거 - 팟캐스트로 이동 메뉴 제거 - 고객센터 메뉴 제거 - 좋아요 & 싫어요 버튼 제거 - 다음에 재생 메뉴 제거 - 품질 메뉴 제거 - 보관함에서 삭제 메뉴 제거 - 재생목록에서 삭제 메뉴 제거 - 신고 메뉴 제거 - 나중에 볼 에피소드 저장 메뉴 제거 - 보관함에 저장 메뉴 제거 - 재생목록에 저장 메뉴 제거 - 공유 메뉴 제거 - 셔플 재생 메뉴 제거 - 취침 타이머 메뉴 제거 - 뮤직 스테이션 시작 메뉴 제거 - 전문 통계 메뉴 제거 - 구독 / 구독 취소 메뉴 제거 - 노래 크레딧 보기 메뉴 제거 - 전체 화면 광고를 숨깁니다. - 전체 화면 광고 제거 - "활성화하면 닫기 버튼을 누르면 전체 화면 광고가 닫혀집니다. -비활성화하면 전체 화면 광고가 차단됩니다. (문제점이 발생할 수 있습니다.)" - "활성화하면 닫기 버튼을 누르면 전체 화면 광고가 닫혀집니다. -비활성화하면 전체 화면 광고가 차단됩니다. (문제점이 발생할 수 있습니다.)" - "활성화하면 닫기 버튼을 누르면 전체 화면 광고가 닫혀집니다. -비활성화하면 전체 화면 광고가 차단됩니다. (문제점이 발생할 수 있습니다.)" - 전체 화면 광고 닫기 - 전체 화면에서 공유 버튼을 숨깁니다. - 전체 화면에서 공유 버튼 제거 - 일반 레이아웃 광고를 숨깁니다. - 일반 레이아웃 광고 제거 - 계정 메뉴에서 핸들(@사용자 아이디)을 숨깁니다. - 핸들(@사용자 아이디) 제거 - 툴바에서 최근 감상 기록 버튼을 숨깁니다. - 최근 감상 기록 버튼 제거 - 음악을 재생하기 전 광고를 숨깁니다. - 음악 광고 제거 - 하단바를 숨깁니다. - 하단바 제거 - 둘러보기 버튼을 숨깁니다. - 둘러보기 버튼 제거 - 홈 버튼을 숨깁니다. - 홈 버튼 제거 - 하단바에서 버튼 라벨을 숨깁니다. - 하단바 버튼 라벨 제거 - 보관함 버튼을 숨깁니다. - 보관함 버튼 제거 - 샘플 버튼을 숨깁니다. - 샘플 버튼 제거 - 업그레이드 버튼을 숨깁니다. - 업그레이드 버튼 제거 - 툴바에서 알림 버튼을 숨깁니다. - 알림 버튼 제거 - 유료 광고 포함 라벨을 숨깁니다. - 유료 광고 포함 라벨 제거 - 피드에서 재생목록 카드 선반을 숨깁니다. - 재생목록 카드 선반 제거 - YouTube Premium 팝업 광고를 숨깁니다. - YouTube Premium 팝업 광고 제거 - YouTube Premium 갱신 배너를 숨깁니다. - YouTube Premium 갱신 배너 제거 - 프로모션 알림 배너를 숨깁니다. - 프로모션 알림 배너 제거 - 피드에서 샘플 선반을 숨깁니다. - 샘플 선반 제거 - YouTube Music 정보 메뉴 숨기기 - 데이터 절약 메뉴 숨기기 - 오프라인 저장 및 저장용량 메뉴 숨기기 - 일반 메뉴 숨기기 - 알림 메뉴 숨기기 - Music Premium 가입 메뉴 숨기기 - 가족 센터 메뉴 숨기기 - 재생 메뉴 숨기기 - 개인 정보 보호 및 데이터 메뉴 숨기기 - 맞춤 콘텐츠 메뉴 숨기기 - "설정 메뉴 구성요소를 숨깁니다. -YT Music 설정 메뉴뿐만 아니라 ReVanced Extended 설정 메뉴도 숨겨집니다." - 설정 메뉴 제거 - 툴바에서 노래 검색 버튼을 숨깁니다. - 노래 검색 버튼 제거 - \'탭하여 업데이트\' 버튼을 숨깁니다. - \'탭하여 업데이트\' 버튼 제거 - 서비스 약관 컨테이너를 숨깁니다. - 서비스 약관 컨테이너 제거 - 툴바에서 음성 검색 버튼을 숨깁니다. - 음성 검색 버튼 제거 - 계정 - 액션바 - 광고 - 메뉴 구성요소 - 일반 - 기타 - 하단바 - 플레이어 - Return YouTube Username - Return YouTube Dislike - SponsorBlock - 설정 메뉴 - 동영상 - 재생 속도 값을 변경할 때마다 저장합니다. - 재생 속도 저장 활성화 - 기본 동영상 재생 속도 값으로 변경되었을 때, 팝업 메시지를 표시합니다. - 팝업 메시지 표시 - 기본 재생 속도 값을 %s으로 변경합니다. - 반복 재생 토글 상태를 저장합니다. - 반복 상태 저장 - 셔플 재생 토글 상태를 저장합니다. - 셔플 상태 저장 - 동영상 품질 값을 변경할 때마다 저장합니다. - 동영상 품질 저장 활성화 - 기본 동영상 화질 값으로 변경되었을 때, 팝업 메시지를 표시합니다. - 팝업 메시지 표시 - 모바일 네트워크 이용 시 기본 동영상 품질 값을 %s로 변경합니다. - 동영상 품질을 설정할 수 없습니다. - Wi-Fi 이용 시 기본 동영상 품질 값을 %s로 변경합니다. - "시청 경고 다이얼로그를 제거합니다. - -이 설정은 다이얼로그를 자동으로 허용하기만 하며 연령 제한(성인인증 절차)을 우회할 수 없습니다." - 시청 경고 다이얼로그 제거 - \'YouTube로 시청\' 메뉴를 누르면 YouTube로 변경하여 동영상을 현재 재생 시간부터 이어서 시청합니다. - 이어서 시청 - \'현재 재생목록 닫기\' 메뉴를 \'YouTube로 시청\' 메뉴로 변경합니다. - 현재 재생목록 닫기 메뉴 변경 - YouTube로 시청 - 잘못된 동영상 URL입니다. - 댓글 섹션에서 \'신고\' 메뉴를 그대로 유지합니다. - 댓글에서 신고 메뉴 유지 - \'신고\' 메뉴를 \'재생 속도\' 메뉴로 변경합니다. - 신고 메뉴 변경 - 이전 댓글 팝업 패널으로 복원합니다. - 이전 댓글 팝업 패널으로 복원 - 이전 플레이어 배경으로 복원합니다. - 이전 플레이어 배경으로 복원 - "이전 플레이어 레이아웃으로 복원합니다. -이전 플레이어 레이아웃에서 일부 기능이 제대로 작동하지 않을 수 있습니다." - 이전 플레이어 레이아웃으로 복원 - 이전 보관함 탭으로 복원합니다. (실험 기능) - 이전 보관함 선반으로 복원 - \@핸들 (사용자 이름) - 사용자 이름 표시 형식을 선택하세요. - 표시 형식 - 사용자 이름 (@핸들) - 사용자 이름 - 댓글에서 핸들(@사용자 아이디)이 아닌 사용자 이름을 표시합니다. - Return YouTube Username 활성화 - "핸들을 사용자 이름으로 변경하려면 YouTube Data API v3 Developer Key가 필요합니다. - -무료 요금제에서 API Key의 일일 할당량은 10,000개이며, 1개의 할당량은 댓글 1개에 대해 핸들을 사용자 이름으로 변경하는 데 사용됩니다. - -API Key를 발급받는 방법을 보려면 여기를 누르세요." - YouTube Data API Key에 대한 정보 - YouTube Data API v3를 사용하기 위한 Developer Key입니다. - YouTube Data API Key - 1. <a href=%1$s>새 프로젝트 만들기</a> 로 이동합니다.<br>2. <b>만들기</b> 버튼을 터치합니다.<br>3. <a href=%2$s>YouTube Data API v3</a> 로 이동합니다.<br>4. <b>사용</b> 버튼을 터치합니다.<br>5. <b>사용자 인증 정보 만들기</b> 버튼을 터치합니다.<br>6. <b>공개 데이터</b> 옵션을 선택합니다.<br>7. <b>다음</b> 버튼을 터치합니다.<br>8. API Key를 복사합니다.<br><br>※ API Key는 다른 사람과 공유해서는 안 되므로 가져오기 / 내보내기 설정에 포함되지 않습니다. - YouTube Data API v3 Developer Key 발급 - 정보 - 싫어요 수의 데이터는 Return YouTube Dislike API에 의해 제공됩니다. 자세한 내용을 보려면 여기를 누르세요. - ReturnYouTubeDislike.com - 좋아요 버튼에서 구분선을 숨깁니다. - 좋아요 버튼에서 구분선 제거 - 싫어요 수를 숫자가 아닌 퍼센트로 표시합니다. - 싫어요 수를 퍼센트로 표시 - 싫어요 수를 표시합니다. - Return YouTube Dislike 활성화 - 좋아요 수가 숨겨진 음악(동영상)에서 추정되는 좋아요 수를 표시합니다. - 추정되는 좋아요 수 표시 - 싫어요 수를 표시할 수 없습니다. (클라이언트 API 제한 도달) - 싫어요 수를 표시할 수 없습니다 (상태 코드: %d). - 싫어요 수를 일시적으로 표시할 수 없습니다 (응답 시간 초과). - 싫어요 수를 표시할 수 없습니다 (%s). - ReturnYouTubeDislike를 사용할 수 없을 때, 팝업 메시지를 표시합니다. - API를 사용할 수 없을 때 팝업 메시지 표시 - 숨겨짐 - 링크를 공유할 때, URL에서 추적 쿼리 매개변수를 제거합니다. - 추적 쿼리를 제거한 링크 공유 - 정보 - sponsor.ajay.app - 건너뛸 구간의 데이터는 SponsorBlock API에 의해 제공됩니다. 자세한 내용을 보려면 여기를 누르세요. - API URL 변경 - API URL을 변경하였습니다. - 잘못된 API URL입니다. - API URL을 초기화하였습니다. - SponsorBlock이 요청을 보낼 서버 URL입니다. 이것이 무슨 역할을 하는지 모르는 경우에는 이 URL을 변경하지 마세요. - 설정한 색상을 적용하였습니다. - 색상: - 잘못된 헥스 코드입니다. - 색상을 초기화하였습니다. - 각 구간에 설정할 동작 - SponsorBlock 활성화 - SponsorBlock은 YouTube 동영상 내 성가신 구간을 건너뛰게 해주는 크라우드소싱 시스템입니다. - 색상 초기화 - 주제와 관련 없는 구간 - 전반적인 동영상의 주제를 이해하는 데 필요 없는 내용을 포함하고 있습니다. - 상호 작용 요청 - 좋아요, 구독, 알림 설정을 요청하는 내용에 관한 구간입니다. - 무음 구간 / 인트로 - 아무 내용도 없는 구간입니다. 애니메이션이나 정적 프레임과 같은 내용을 포함하고 있습니다. - 음악이 아닌 구간 - 정식 음원이 아닌 동영상 음원에서 음악이 아닌 구간이 해당됩니다. - 최종 화면 / 크레딧 - 엔딩 크레딧이나 최종 화면이 나타나는 구간입니다. - 미리 보기 / 요약 / 흥미 유발 - 이전 에피소드를 간략히 요약하거나 현재 동영상의 하이라이트를 미리 보여줍니다. - 자체 홍보 구간 - \'스폰서 광고\' 구간과 비슷하지만, 자발적으로 홍보하는 내용을 포함하는 구간입니다. 채널 굿즈 광고, 기부 광고와 동영상에 참여한 사람들을 홍보하는 광고가 해당됩니다. - 스폰서 광고 - 유료 광고, 협찬과 같은 직/간접적인 광고 구간입니다. - 자동으로 건너뛰기 - 아무것도 하지 않기 - 주제와 관련 없는 구간을 건너뛰었습니다. - 상호 작용 요청을 건너뛰었습니다. - 인트로를 건너뛰었습니다. - 무음 구간을 건너뛰었습니다. - 무음 구간을 건너뛰었습니다. - 여러 구간을 건너뛰었습니다. - 음악이 아닌 구간을 건너뛰었습니다. - 최종 화면을 건너뛰었습니다. - 미리 보기를 건너뛰었습니다. - 요약을 건너뛰었습니다. - 미리 보기를 건너뛰었습니다. - 자체 홍보 구간을 건너뛰었습니다. - 스폰서 광고를 건너뛰었습니다. - SponsorBlock을 일시적으로 사용할 수 없습니다. - SponsorBlock을 일시적으로 사용할 수 없습니다 (상태 코드: %d). - SponsorBlock을 일시적으로 사용할 수 없습니다 (응답 시간 초과). - API를 사용할 수 없을 때, 팝업 메시지 표시 - SponsorBlock를 사용할 수 없을 때, 팝업 메시지를 표시합니다. - 자동으로 구간을 건너뛸 때, 팝업 메시지 표시 - 자동으로 구간을 건너뛸 때, 팝업 메시지를 표시합니다. - 설정을 클립보드에 복사하였습니다. - "이전 앱 버전으로 변경합니다. - -• 이 기능을 활성화하면 앱 레이아웃이 변경되지만 알려지지 않은 문제점이 발생할 수 있습니다. -• 나중에 이 기능을 비활성화하면 앱 데이터를 지우기 전까지 이전 레이아웃이 유지될 수 있습니다." - 4.27.53 - 캐나다 지역에서 뮤직 스테이션 모드를 비활성화합니다. - 6.11.52 - 실시간 가사를 비활성화합니다. - 7.16.53 - 이전 액션바로 복원합니다. - 변경할 앱 버전을 선택하세요. - 변경할 앱 버전 설정 - 앱 버전 변경 - diff --git a/src/main/resources/music/translations/nl-rNL/missing_strings.xml b/src/main/resources/music/translations/nl-rNL/missing_strings.xml deleted file mode 100644 index dfa811b77..000000000 --- a/src/main/resources/music/translations/nl-rNL/missing_strings.xml +++ /dev/null @@ -1,215 +0,0 @@ - - - Continue - Don\'t show again - "GmsCore does not have permission to run in the background. - -Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. - -This is required for the app to work." - "GmsCore battery optimizations must be disabled to prevent issues. - -Tap on the continue button and disable battery optimizations." - Open website - Action needed - Enable cloud messaging to receive notifications. - Open GmsCore - GmsCore is not installed. Install it. - Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. - Bypass image region restrictions - Change from in-app share sheet to system share sheet. - Change share sheet - Charts - Explore - Home - Library - Subscriptions - Select which page the app opens in. - Change start page - Invalid custom filter: %s. - Invalid custom playback speeds. - To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Disables Cairo splash animation when the app starts up. - Disable Cairo splash animation - Disables redirection to the next track when clicking the Dislike button. - Disable dislike redirection - Disable swipe to change tracks in the miniplayer. - Disable miniplayer gesture - Disable swipe to change tracks in the player. - Disable player gesture - Changes the player background color to black. - Enable black player background - Includes the buffer in the debug log. - Enable debug buffer logging - Reset to default values. - Reset - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - Hides dark overlay that appears when double-tapping to seek. - Hide double-tap overlay filter - Hides the floating button in the Library tab. - Hide floating button - Hide Go to episode menu - Hide Go to podcast menu - Hide Help & feedback menu - Hide Play next menu - Hide Quality menu - Hide Remove from library menu - Hide Remove from playlist menu - Hide Report menu - Hide Save episode for later menu - Hide Save to library menu - Hide Save to playlist menu - Hide Share menu - Hide Shuffle play menu - Hide Sleep timer menu - Hide Start radio menu - Hide Stats for nerds menu - Hide Subscribe / Unsubscribe menu - Hide View song credits menu - "If it is enabled, fullscreen ads are closed through the Close button. -If it is disabled, fullscreen ads are blocked. (there may be side effects)" - Fullscreen ads are blocked. (there may be side effects) - Fullscreen ads are closed through the Close button. - Close fullscreen ads - Hides the Notifications button in the toolbar. - Hide Notifications button - Hides the playlist card shelf in the feed. - Hide playlist card shelf - Hides the promotion alert banner. - Hide promotion alert banner - Hides the Samples shelf in the feed. - Hide Samples shelf - Hide About menu - Hide Data saving menu - Hide Downloads & storage menu - Hide General menu - Hide Notifications menu - Hide Get Music premium menu - Hide Family Center menu - Hide Playback menu - Hide Privacy & data menu - Hide Recommendations menu - "Hide elements of the settings menu. -This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." - Hide settings menu - Hide sound search button - Hides the Tap to update button. - Hide Tap to update button - General - Miscellaneous - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Settings menu - Video - Remembers the last playback speed selected. - Remember playback speed changes - Show a toast when changing the default playback speed. - Show a toast - Changing default speed to %s. - Remembers the last video quality selected. - Remember video quality changes - Show a toast when changing the default video quality. - Show a toast - Changing default mobile data quality to %s. - Failed to set quality. - Changing default Wi-Fi quality to %s. - Continues the video from the current time when switching to YouTube. - Continue watching - Replaces the Dismiss queue menu with the Watch on YouTube menu. - Replace Dismiss queue menu - Watch on YouTube - Invalid video url. - Keeps the Report menu in the comments section intact. - Keep Report in comments - Replaces the Report menu with the Playback speed menu. - Replace Report menu - Returns the comments popup panels to the old style. - Restore old comments popup panels - Returns the player background to the old style. - Restore old player background - "Returns the player layout to the old style. -Some features may not work properly in the old player layout." - Restore old player layout - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Username - Replaces handles with usernames in comments. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - ReturnYouTubeDislike.com - Enable Return YouTube Dislike - Shows the estimated like count of videos. - Show estimated likes - Shows a toast if the Return YouTube Dislike API is unavailable. - Show a toast if API is unavailable - Hidden - About - sponsor.ajay.app - Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. - Change API URL - API URL changed. - API URL is invalid. - API URL reset. - The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. - Color changed. - Color: - Invalid color code. - Color reset. - Change segment behavior - Enable SponsorBlock - SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. - Reset color - Filler Tangent / Jokes - Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. - Interaction Reminder (Subscribe) - A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. - Intermission / Intro Animation - An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. - Music: Non-Music Section - Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. - Endcards / Credits - Credits or when the YouTube endcards appear. Not for conclusions with information. - Preview / Recap / Hook - Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. - Unpaid / Self Promotion - Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. - Sponsor - Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. - Skip automatically - Disable - Skipped filler. - Skipped annoying reminder. - Skipped intro. - Skipped intermission. - Skipped intermission. - Skipped multiple segments. - Skipped a non-music section. - Skipped outro. - Skipped preview. - Skipped recap. - Skipped preview. - Skipped self promotion. - Skipped sponsor. - SponsorBlock is temporarily unavailable. - SponsorBlock is temporarily unavailable (status %d). - SponsorBlock is temporarily unavailable (API timed out). - Show a toast if API is unavailable - Shows a toast if the SponsorBlock API is unavailable. - Show a toast when skipping automatically - Shows a toast when a segment is automatically skipped. - 7.16.53 - Restore old action bar - diff --git a/src/main/resources/music/translations/nl-rNL/strings.xml b/src/main/resources/music/translations/nl-rNL/strings.xml deleted file mode 100644 index efc733fe5..000000000 --- a/src/main/resources/music/translations/nl-rNL/strings.xml +++ /dev/null @@ -1,192 +0,0 @@ - - - Componentnamen filteren op lijn spatie - Wijzig aangepaste filter - Aangepast filter inschakelen - Aangepast filter inschakelen - Ongeldige aangepaste afspeelsnelheden. Herstel naar standaardwaarden. - Voeg toe of verander de beschikbare afspeelsnelheden - Bewerk aangepaste afspeelsnelheden - Geforceerde automatische ondertitels uitschakelen. - Geforceerde automatische ondertitels uitschakelen - Zet de navigatie balk kleur naar zwart. - Activeer zwarte navigatie balk - Komt overeen met de kleur van de mini speler en de volschermspeler. - Kleuren overeenkomst van de speler inschakelen - "Zet compact dialoogvenster aan op telefoon. - -Bekende problemen: -• Albumhoezen op de bibliotheekschaal worden ook kleiner. -• Slaap timer lay-out kan ongebruikelijk verschijnen." - Compacte dialoog inschakelen op telefoon - Laat het debug logboek zien. - Debug logging aanzetten - Houd de speler permanent geminimaliseerd, zelfs als er een ander nummer wordt afgespeeld. - Forceer geminimaliseerde speler - Schakelt scherm rotatie in door je scherm te draaien. - Landschap modus inschakelen - Schakelt de volgende knop in de minispeler in. - Schakel de volgende knop voor de minispeler in - Schakelt de vorige knop in de minispeler in. - Knop Vorige minispeler inschakelen - "Zet 250/251 opus codec aan tijdens het afspelen van audio." - Opus codec inschakelen - Hiermee kun je naar beneden vegen om de minispeler te sluiten. - Schakel vegen in om de minispeler te sluiten - "Voegt de schakelaar 'Trim stilte' toe aan het vervolgmenu voor afspeelsnelheid. - - Info: - • Deze functie is voor podcasts. - • Deze functie is nog in ontwikkeling en kan dus instabiel zijn." - Voeg een trimstilteschakelaar toe - De Zen-modus wordt ook toegepast op podcasts. - Schakel de zen-modus in podcasts in - Een grijze tint toevoegen aan de videospeler om vermoeidheid van de ogen te verminderen. - Zen-modus inschakelen - Start opnieuw op om de lay-out normaal te laden - Vernieuwen en opnieuw opstarten - Instellingen exporteren naar bestand - Instellingen exporteren mislukt. - Instellingen zijn succesvol geëxporteerd. - Importeer - Instellingen importeren uit bestand - Kopiëer - Importeer / exporteer instellingen als tekst - Importeer / Exporteer instellingen - Importeren / exporteren - Importeren mislukt: %s. - Instellingen teruggezet naar standaard - Geïmporteerde %d instellingen - ReVanced ExtExtended - "Downloadknop opent uw externe downloader. - - • Overschrijft alleen de downloadactieknop in de speler. - • Heeft geen voorrang op de downloadknop in het vervolgmenu of de bibliotheek." - Downloadactieknop negeren - Externe downloader - "%1$s is niet geïnstalleerd. - Download %2$s van de website." - Waarschuwing - %s is niet geïnstalleerd. Installeer het alstublieft. - Pakketnaam van uw geïnstalleerde externe downloader-app, zoals NewPipe of Seal - Externe downloader pakketnaam - Verbergt lege componenten in het accountmenu - Leeg onderdeel verbergen - Lijst met accountmenunamen die moeten worden gefilterd, gescheiden door nieuwe regels. - Accountmenufilter - Verberg account menu elementen. - Accountmenu verbergen - Verbergt de knop Toevoegen aan afspeellijst. - Knop Toevoegen aan afspeellijst verbergen - Verbergt de commentaarknop. - Knop voor commentaar verbergen - Verbergt de downloadknop. - Downloadknop verbergen - Verbergt labels in actieknoppen. - Actieknoplabels verbergen - Verbergt de knoppen \'Vind ik leuk\' en \'Niet leuk\'. Het werkt niet in de oude spelerindeling. - Verberg de like- en dislike-knoppen - Verbergt het startkeuzerondje. - Keuzerondje verbergen - Verbergt de deelknop. - Deelknop verbergen - Verbergt de audio-videoschakelaar in de speler. - Verberg audio-videoschakelaar - Verbergt de knop plank van het thuisscherm en verkenner. - Verberg knop plank - Verbergt de carrousel plank van thuisscherm en verkenner. - Carrousel plank verbergen - Verbergt de cast knop bovenaan de thuispagina en bovenaan de speler. - Verberg cast knop - Verbergt de muziekcategoriebalk bovenaan de thuispagina. - Verberg categorie balk - Verbergt kanaalrichtlijnen bovenaan het opmerkingengedeelte. - Kanaalrichtlijnen verbergen - Verbergt tijdstempel- en emoji-knoppen tijdens het typen van opmerkingen. - Verberg tijdstempel en emoji-knoppen - Component met 3 kolommen verbergen - Verberg toevoegen aan wachtrijmenu - Ondertitelingsmenu verbergen - Verberg het menu voor het verwijderen van afspeellijsten - Wachtrijmenu voor negeren verbergen - Downloadmenu verbergen - Verberg het menu voor het bewerken van afspeellijsten - Verbergen ga naar albummenu - Verbergen ga naar artiestenmenu - Knop Vind ik leuk en niet leuk verbergen - Verbergt advertenties op volledig scherm. - Advertenties op volledig scherm verbergen - Verbergt de deelknop in de speler op volledig scherm. - Knop voor delen op volledig scherm verbergen - Verbergt algemene advertenties. - Algemene advertenties verbergen - Verbergt de handgreep in de account wijziger. - Verberg handvat - Verbergt de geschiedenis knop in de werkbalk. - Verberg geschiedenisknop - Verbergt advertenties voordat je muziek afspeelt. - Verberg muziek advertenties - Navigatiebalk verbergen. - Navigatiebalk verbergen - Verbergt de verkenningsknop. - Knop Verkennen verbergen - Verbergt de homeknop. - Home-knop verbergen - Verberg labels in de navigatie balk. - Verberg e navigatie balk labels - Verbergt de bibliotheekknop. - Bibliotheekknop verbergen - Verbergt de voorbeeldknop. - Knop Monsters verbergen - Verbergt de upgradeknop. - Upgradeknop verbergen - Verbergt het betaalde promotielabel. - Verberg het betaalde promotielabel - Verbergt pop-ups van premiumpromoties. - Pop-ups van premiumpromoties verbergen - Verbergt de banner voor premiumverlenging. - Banner voor premiumverlenging verbergen - Verbergt de geluidszoekknop in de zoekbalk. - Verbergt de gebruikersvoorwaarden in het accountmenu. - Container van termen verbergen - Verbergt de stemzoekknop in de zoekbalk. - Knop voor gesproken zoekopdrachten verbergen - Rekening - Actie bar - Advertenties - Flyout-menu - Navigatiebalk - Speler - Onthoudt de status van de herhaling. - Herinner me de herhalingsstatus - Onthoudt de status van het shuffle. - Onthoud shuffle status - "Verwijdert het dialoogvenster voor discretie van de kijker. - Hiermee wordt de leeftijdsbeperking niet omzeild. Het accepteert het gewoon automatisch." - Dialoogvenster voor discretie van kijkers verwijderen - Brengt het bibliotheektabblad terug naar de oude stijl. (Experimenteel) - Herstel bibliotheekplank in oude stijl - Over - Data is gegeven door de Return YouTube Dislike API. Tik hier voor meer informatie. - Verbergt de spatie van de \"vind ik leuk\" knop. - Compacte \"vind ik leuk\" knop - In plaats van het aantal \"hout niet van\" te laten zien, wordt het percentage getoond. - Hout niet van als percentage - Laat de \"vind ik niet leuk\"s zien van video\'s. - Vind ik niet leuk is niet beschikbaar (client API limiet bereikt) - Dislikes niet beschikbaar (status %d). - Dislikes tijdelijk niet beschikbaar (API time-out). - Dislikes niet beschikbaar (%s). - Verwijdert tracking query parameters uit de URL\'s bij het delen van links. - Koppelingen delen - Instellingen naar het klembord gekopieerd. - "Spoofing van de clientversie naar de oude versie - -• Dit zal het uiterlijk van de app veranderen. maar onbekende neveneffecten kunnen zich voordoen -• Als later uitgeschakeld kan de oude UI blijven totdat de appgegevens gewist worden" - 4.27.53 - Radio modus uitschakelen in Canadese regio\'s - 6.11.52 - Realtime songteksten uitschakelen - Selecteer het doel van de spoof app versie - Spoof app versie doel - Spoof app versie - diff --git a/src/main/resources/music/translations/pl-rPL/missing_strings.xml b/src/main/resources/music/translations/pl-rPL/missing_strings.xml deleted file mode 100644 index acebd7abf..000000000 --- a/src/main/resources/music/translations/pl-rPL/missing_strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Don\'t show again - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - diff --git a/src/main/resources/music/translations/pl-rPL/strings.xml b/src/main/resources/music/translations/pl-rPL/strings.xml deleted file mode 100644 index 9c426a6fe..000000000 --- a/src/main/resources/music/translations/pl-rPL/strings.xml +++ /dev/null @@ -1,407 +0,0 @@ - - - Kontynuuj - "GmsCore nie ma uprawnień do działania w tle. - -Postępuj zgodnie z przewodnikiem 'Don't kill my app!' dla twojego urządzenia i zastosuj instrukcje dla swojej instalacji GmsCore. - -Jest to wymagane do działania aplikacji." - "Optymalizacja baterii GmsCore musi być wyłączona, aby zapobiec problemom. - -Kontynuuj i wyłącz optymalizację baterii." - Otwórz stronę - Wymagane działanie - Włącz cloud messaging, by otrzymywać powiadomienia. - Otwórz GmsCore - GmsCore nie jest zainstalowany. Zainstaluj go. - Zastępuje domenę, która jest blokowana w niektórych regionach, aby można było otrzymywać miniaturki playlist, awatary kanałów itp. - Pomiń ograniczenia regionu dla obrazów - Zmienia wygląd panelu udostępniania z natywnego aplikacji na systemowy. - Zmień wygląd panelu udostępniania - Listy przebojów - Odkrywaj - Strona główna - Biblioteka - Subskrypcje - Wybierz, na której stronie ma otwierać się aplikacja. - Zmień stronę startową - Lista tekstów tworzących ścieżkę komponentów do filtrowania, które muszą być oddzielone nowymi liniami. - Edytuj własny filtr - Włącza własny filtr do ukrywania komponentów układu aplikacji. - Włącz własny filtr - Nieprawidłowy własny filtr: %s. - Niestandardowe prędkości muszą być mniejsze niż %sx. - Nieprawidłowe niestandardowe prędkości odtwarzania. - Skonfiguruj dostępne prędkości odtwarzania. - Edytuj niestandardowe prędkości odtwarzania - Aby otwierać linki YouTube Music w RVX Music, przejdź do opcji obsługiwanych linków w ustawieniach i włącz obsługiwane adresy internetowe dla RVX. - Otwórz systemowe ustawienia aplikacji - Wyłącza automatycznie włączane napisy w odtwarzaczu filmów. - Wyłącz automatyczne napisy - Wyłącza animację ładowania aplikacji związaną z motywem Cairo podczas otwierania aplikacji. - Wyłącz animację uruchamiania aplikacji - Wyłącza przenoszenie do następnego utworu po kliknięciu łapki w dół. - Wyłącz pomijanie nielubianych piosenek - Wyłącza gest przesuwania, aby zmienić utwór w miniodtwarzaczu. - Wyłącz gest w miniodtwarzaczu - Wyłącza gest przesuwania, aby zmienić utwór w odtwarzaczu. - Wyłącz gest w odtwarzaczu - Ustawia kolor paska nawigacji na czarny. - Włącz czarny pasek nawigacji - Zmienia tło odtwarzacza na czarne. - Włącz czarne tło odtwarzacza - Dopasowuje kolor miniodtwarzacza do otwarzacza pełnoekranowego. - Włącz pasujące kolory odtwarzaczy - "Włącza kompaktowe menu ustawień utworu na telefonie. - -Znane problemy: -• Okładka albumu w zakładce biblioteki staje się mniejsza, gdy jest ustawiona siatka. -• Układ wyłącznika czasowego może wyglądać nietypowo." - Włącz kompaktowe dialogi - Zawiera buforowanie w logach debugowania. - Logi do debugowania buforu - Wyświetla log od debugowania. - Włącz logowanie debugowania - Zostawia odtwarzacz zminimalizowany, nawet jeśli zostanie odtworzony inny utwór. - Włącz wymuszenie zminimalizowanego odtwarzacza - Pozwala wejść w tryb pełnoekranowy poprzez obrót ekranu telefonu. - Włącz tryb pełnoekranowy - Dodaje przycisk do następnej piosenki w miniodtwarzaczu. - Dodaj przycisk do następnej piosenki w miniodtwarzaczu - Dodaje przycisk do poprzedniej piosenki w miniodtwarzaczu. - Dodaj przycisk do poprzedniej piosenki w miniodtwarzaczu - "Włącza kodek OPUS, jeśli odpowiedź odtwarzacza zawiera ten kodek. - -Informacje: -• Najnowsze klienty YouTube Music domyślnie używają kodeka OPUS -• Działa jedynie dla użytkowników używających oszukiwania aplikacji na bardzo starych klientach" - Włącz kodek OPUS - Włącza przesuwanie w dół do zamykania miniodtwarzacza. - Włącz przesuwanie do zamykania miniodtwarzacza - "Dodaje przycisk pomijania ciszy do menu od prędkości odtwarzania. - -Informacje: -• Ta funkcja jest dostępna tylko dla podcastów. -• Ta funkcja jest nadal w fazie rozwoju, więc może być niestabilna." - Włącz przełącznik do pomijania ciszy - Tryb zen jest stosowany również do podcastów. - Włącz tryb zen w podcastach - Zmienia kolor tła odtwarzacza na jasnoszary, aby zmniejszyć zmęczenie oczu. - Włącz tryb zen - Przywrócono domyślne wartości. - Uruchom ponownie, aby wczytać układ poprawnie - Odśwież i uruchom ponownie - Wyeksportuj ustawienia do pliku - Nie udało się wyeksportować ustawień. - Ustawienia zostały pomyślnie wyeksportowane. - Import - Zaimportuj ustawienia z pliku - Kopiuj - Zaimportuj/Wyeksportuj ustawienia jako tekst - Zaimportuj lub wyeksportuj ustawienia - Importuj/Eksportuj ustawienia - Nie udało się zaimportować: %s. - Ustawienia zostały zresetowane do domyślnych. - Zaimportowano ustawienia %d. - Zresetuj - ReVanced Extended - "Przycisk od pobierania otwiera zewnętrzną aplikację do pobierania. - -• Jedynie zmienia działanie przycisku w odtwarzaczu. -• Nie zmienia działania przycisków w menu ustawień utworu i bibliotece." - Zastąp przycisk od pobierania - Aplikacja od pobierania - "%1$s nie jest zainstalowany. -Pobierz %2$s ze strony." - Ostrzeżenie - %s nie jest zainstalowany. Zainstaluj go. - Nazwa pakietu zainstalowanej aplikacji od pobierania, takiej jak NewPipe lub YTDLnis. - Nazwa pakietu aplikacji od pobierania - Ukrywa puste komponenty w menu konta. - Ukryj puste komponenty - Lista nazw w menu konta do filtrowania, która musi być oddzielona nowymi liniami. - Filtr menu konta - Ukrywa elementy menu konta za pomocą własnego filtra. - Ukryj menu konta - Ukrywa przycisk dodawania do playlisty. - Ukryj przycisk dodawania do playlisty - Ukrywa przycisk komentarzy. - Ukryj przycisk komentarzy - Ukrywa przycisk pobierania. - Ukryj przycisk pobierania - Ukrywa nazwy przycisków akcji. - Ukryj nazwy przycisków akcji - Ukrywa przyciski łapki w górę i dół. Nie będzie działać na starym układzie odtwarzacza. - Ukryj przyciski łapki w górę i dół - Ukrywa przycisk radia. - Ukryj przycisk radia - Ukrywa przycisk udostępniania. - Ukryj przycisk udostępniania - Ukrywa przełącznik utwór-teledysk w odtwarzaczu. - Ukryj przełącznik utwór-teledysk - Ukrywa półkę z przyciskami na stronie głównej. - Ukryj półki z przyciskami - Ukrywa półkę z karuzelami na stronie głównej. - Ukryj półki z karuzelami - Ukrywa przyciski castowania. - Ukryj przycisk do castowania - Ukrywa panel kategorii. - Ukryj panel kategorii - Ukrywa wytyczne kanału na górze sekcji komentarzy. - Ukryj wytyczne - Ukrywa czas i przyciski od emotikon podczas pisania komentarzy. - Ukryj czas i przyciski od emotikon - Ukrywa poświatę pojawiającą się, gdy przewijamy za pomocą podwójnego kliknięcia. - Ukryj poświatę po dwukrotnym kliknięciu - Ukrywa pływający przycisk w zakładce biblioteki. - Ukryj pływający przycisk - Ukryj 3-kolumnowy komponent - Ukryj menu od dodawania do kolejki - Ukryj menu od napisów - Ukryj menu od usuwania playlisty - Ukryj menu od odrzucania kolejki - Ukryj menu od pobierania - Ukryj menu od edytowania playlisty - Ukryj menu od pokazywania albumu - Ukryj menu od pokazywania wykonawcy - Ukryj menu od przechodzenia do odcinka - Ukryj menu od przechodzenia do podcastu - Ukryj menu do pomocy i opinii - Ukryj przyciski łapki w górę i dół - Ukryj menu od odtwarzania jako następny - Ukryj menu od jakości - Ukryj menu od usuwania z biblioteki - Ukryj menu od usuwania z playlist - Ukryj menu od zgłaszania - Ukryj menu od zapisywania odcinka na później - Ukryj menu od dodawania do biblioteki - Ukryj menu od dodawania do playlisty - Ukryj menu od udostępniania - Ukryj menu od odtwarzania losowo - Ukryj menu od wyłącznika czasowego - Ukryj menu do radia - Ukryj menu od statystyk dla nerdów - Ukryj menu od subskrybowania / odsubskrybowywania - Ukryj menu do autorów utworu - Ukrywa reklamy pełnoekranowe - Ukryj reklamy pełnoekranowe - "Jeśli opcja jest włączona, pełnoekranowe reklamy są zamykane poprzez przycisk zamknięcia. -Jeśli opcja jest wyłączona, pełnoekranowe reklamy są blokowane (mogą wystąpić efekty uboczne)" - "Jeśli opcja jest włączona, pełnoekranowe reklamy są zamykane poprzez przycisk zamknięcia. -Jeśli opcja jest wyłączona, pełnoekranowe reklamy są blokowane (mogą wystąpić efekty uboczne)" - "Jeśli opcja jest włączona, pełnoekranowe reklamy są zamykane poprzez przycisk zamknięcia. -Jeśli opcja jest wyłączona, pełnoekranowe reklamy są blokowane (mogą wystąpić efekty uboczne)" - Zamknij pełnoekranowe reklamy - Ukrywa przycisk udostępniania w trybie pełnoekranowym. - Ukryj przycisk udostępniania w trybie pełnoekranowym - Ukrywa ogólne reklamy. - Ukryj ogólne reklamy - Ukrywa nicki w przełączniku kont. - Ukryj nicki - Ukrywa przycisk historii z paska narzędzi. - Ukryj przycisk historii - Ukrywa reklamy przed odtworzeniem multimediów. - Ukryj reklamy multimedialne - Ukrywa pasek nawigacji. - Ukryj pasek nawigacji - Ukrywa przycisk od strony odkrywania. - Ukryj przycisk od strony odkrywania - Ukrywa przycisk do strony głównej. - Ukryj przycisk do strony głównej - Ukrywa nazwy w pasku nawigacji. - Ukryj nazwy w pasku nawigacji - Ukrywa przycisk biblioteki. - Ukryj przycisk do biblioteki - Ukrywa przycisk od sampli. - Ukryj przycisk od sampli - Ukrywa przycisk do YouTube Premium. - Ukryj przycisk do YouTube Premium - Ukrywa przycisk do powiadomień z paska narzędzi. - Ukryj przycisk do powiadomień - Ukrywa etykiety oznaczające płatne promocje. - Ukryj etykiety oznaczające płatne promocje - Ukrywa półkę z rekomendowanymi playlistami na stronie głównej. - Ukryj półki z rekomendowanymi playlistami - Ukrywa wyskakujące okienka promocyjne Premium. - Ukryj wyskakujące okienka promocyjne Premium - Ukrywa baner odnawiania Premium. - Ukryj baner odnawiania Premium - Ukrywa banery z alertami promocyjnymi. - Ukryj banery z alertami promocyjnymi - Ukrywa półke z samplami na stronie głównej. - Ukryj półkę z samplami - Ukryj menu informacji o YouTube Music - Ukryj menu oszczędzania danych - Ukryj menu pobranych i miejsca na dane - Ukryj menu ustawień ogólnych - Ukryj menu powiadomień - Ukryj menu zasubskrybowania Music Premium - Ukryj menu centrum rodziny - Ukryj menu odtwarzania - Ukryj menu prywatności i danych - Ukryj menu rekomendacji - "Ukrywa elementy menu ustawień. -Działa nie tylko na elementy menu ustawień YT Music, lecz także ReVanced Extended." - Ukryj menu ustawień - Ukrywa przycisk od rozpoznawania piosenek w pasku wyszukiwania. - Ukryj przycisk od rozpoznawania piosenek - Ukrywa przycisk \'Stuknij, aby zaktualizować\'. - Ukryj przycisk \'Stuknij, aby zaktualizować\' - Ukrywa kontener warunków usług z menu konta. - Ukryj kontener warunków usług - Ukrywa przycisk od wyszukiwania głosowego w pasku wyszukiwania. - Ukryj przycisk od wyszukiwania głosowego - Konto - Pasek akcji - Reklamy - Menu ustawień utworu - Ogólne - Pozostałe - Pasek nawigacji - Odtwarzacz - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Menu ustawień YouTube Music - Teledyski - Zapisuje ostatnią wybraną prędkość odtwarzania. - Zapamiętuj zmiany prędkości odtwarzania - Komunikaty będą wyświetlane po zmianie domyślnej prędkości odtwarzania. - Komunikaty o zmianie domyślnej prędkości odtwarzania - Zmieniono domyślną prędkość odtwarzania na %s. - Zapisuje stan pętli. - Zapamiętaj stan pętli - Zapisuje stan odtwarzania losowego. - Zapamiętaj stan odtwarzania losowego - Zapisuje ostatnią wybraną jakość teledysku. - Zapamiętuj zmiany jakości teledysku - Komunikaty będą wyświetlane po zmianie domyślnej jakości teledysków. - Komunikaty o zmianie domyślnej jakości teledysków - Zmieniono domyślną jakość podczas używania sieci mobilnej na %s. - Jakość nie została ustawiona. - Zmieniono domyślną jakość podczas używania Wi-Fi na %s. - "Usuwa okno dialogowe treści ograniczonej do oglądania. -Nie pomija to ograniczeń wiekowych, lecz akceptuje je automatycznie." - Usuń okno dialogowe treści ograniczonej do oglądania - Podczas oglądania na YouTube kontynuuj oglądanie od aktualnego czasu. - Kontynuuj oglądanie - Zamienia menu od czyszczenia kolejek z menu od oglądania na YouTube. - Zmodyfikuj czyszczenie kolejek - Oglądaj na YouTube - Niepoprawne URL filmu. - Zachowuje menu zgłaszania w sekcji komentarzy. - Zachowaj zgłaszanie w komentarzach - Zamienia przycisk od zgłaszania z przyciskiem od prędkości odtwarzania. - Zamień przycisk od zgłaszania - Przywraca stary wygląd wyskakującym panelom z komentarzami. - Przywróć stare wyskakujące panele z komentarzami - Przywraca tło odtwarzacza do starego stylu. - Przywróć stare tło odtwarzacza - "Przywraca układ odtwarzacza do starego wyglądu. -Niektóre ustawienia mogą nie działać poprawnie ze starym układem odtwarzacza." - Przywróć stary układ odtwarzacza - Przywraca zakładkę biblioteki do starego stylu. (Eksperymentalne) - Włącz stary styl półek biblioteki - \@nick (Nazwa użytkownika) - Wybierz format wyświetlania nazwy użytkownika. - Format wyświetlania - Nazwa użytkownika (@nick) - Nazwa użytkownika - Zastępuje nicki nazwami użytkowników w komentarzach. - Włącz Return YouTube Username - "Klucz deweloperski YouTube Data API v3 jest wymagany do zastępowania nicków nazwami użytkownika. - -Dzienny limit kluczy API w planie darmowym wynosi 10 000, a 1 limit służy do zastąpienia nicku nazwą użytkownika dla 1 komentarza. - -Kliknij, by zobaczyć, jak zgłosić klucz API." - O kluczu YouTube Data API - Klucz deweloperski używany do korzystania z API YouTube Data V3. - Klucz YouTube Data API - 1. Przejdź do <a href=%1$s>Utwórz nowy projekt</a>.<br>2. Kliknij przycisk <b>UTWÓRZ</b><br>3. Przejdź do <a href=%2$s>YouTube Data API v3</a>.<br>4. Kliknij przycisk <b>WŁĄCZ</b><br>5. Kliknij przycisk <b>UTWÓRZ DANE LOGOWANIA</b><br>6. Wybierz opcję <b>Dane publiczne</b><br>7. Kliknij przycisk <b>DALEJ</b><br>8. Skopiuj klucz API<br><br>※ Klucz API nie powinien być współdzielony z innymi, dlatego nie jest zawarty w ustawieniach importu/eksportu - Zgłoś klucz deweloperski YouTube Data API - O integracji - Dane są dostarczane dzięki API Return YouTube Dislike. Kliknij tutaj, aby dowiedzieć się więcej. - ReturnYouTubeDislike.com - Ukrywa linię w przycisku od łapkowania. - Kompaktowy przycisk od łapkowania - Zamiast ilości łapek w dół, jest wyświetlany ich procent. - Łapki w dół wyświetlane jako procent - Pokazuje ilość łapek w dół filmów. - Włącz Return YouTube Dislike - Pokazuje szacowaną ilość polubień filmów. - Pokaż szacowaną ilość polubień - Łapki w dół nie są dostępne (limit API użytkownika został osiągnięty). - Liczba łapek w dół nie jest dostępna (status %d). - Łapki w dół są tymczasowo niedostępne (API nie reaguje). - Liczba łapek w dół nie jest dostępna (%s). - Komunikat wyświetlany w momencie, gdy API ReturnYouTubeDislike jest niedostępne. - Pokaż komunikat o niedostępności API - Ukryte - Usuwa parametry śledzących zapytań z adresów URL podczas udostępniania linków. - Oczyść udostępniane linki - O integracji - sponsor.ajay.app - Dane są dostarczane przez API SponsorBlock. Stuknij tutaj, aby dowiedzieć się więcej i pobrać na inne platformy. - Zmień adres API - Adres API został zmieniony. - Adres API jest nieprawidłowy. - Adres API został zresetowany. - Adres SponsorBlock jest używany do wykonywania połączeń z serwerem. Nie zmieniaj tego, chyba że wiesz, co robisz. - Kolor został zmieniony. - Kolor: - Nieprawidłowy kod koloru. - Kolor został zresetowany. - Zmień sposoby pomijania segmentów - Włącz SponsorBlock - SponsorBlock to system pomijania denerwujących fragmentów w filmach na YouTube. - Zresetuj kolor - Nietematyczny Wypełniacz / Żarty - Segmenty nietematyczne dodawane tylko dla wypełnienia lub humor, który nie jest wymagany do zrozumienia głównej treści filmu. Nie dotyczy segmentów zawierających informacje kontekstowe lub szczegółowe. - Przypomnienie O Interakcji (Zasubskrybuj) - Krótkie przypomnienie o łapce w górę, subskrypcji lub obserwowaniu w środku kontentu. Jeśli trwa długo lub dotyczy czegoś konkretnego, powinno być oznaczone jako autopromocja. - Przerywnik / Animowane Intro - Fragment bez faktycznej treści. Może to być pauza, statyczna klatka lub powtarzająca się animacja. Nie dotyczy przejść zawierających informacje. - Muzyka: Sekcja Bez Muzyki - Do użytku jedynie w teledyskach. Sekcje teledysków, które nie są uwzględnione w innej kategorii. - Karty / Napisy Końcowe - Napisy końcowe lub gdy pojawia się ekran końcowy. Nie dotyczy zakończeń zawierających informacje. - Zapowiedź / Podsumowanie / Haczyk - Zbiór klipów pokazujących to, co pojawi się lub co pojawiło się w tym filmie, oraz innych fiilmach z tej serii, w którym wszystkie informacje są gdzieś powtarzane. - Nieopłacona / Auto Reklama - Podobne do treści sponsorowanych, z wyjątkiem nieopłaconych lub autoreklam. Obejmuje to sekcje o własnych towarach, darowiznach czy informacjach o tym, z kim współpracowali. - Treści Sponsorowane - Płatna promocja, płatne rekomendacje oraz bezpośrednie reklamy. Nie do autopromocji ani darmowych wyrazów uznania dla kwestii / twórców / stron / produktów, które im się podobają. - Pomiń automatycznie - Wyłącz - Pominięto wypełniacz. - Pominięto irytujące przypomnienie. - Pominięto wstęp. - Pominięto przerywnik. - Pominięto przerywnik. - Pominięto wiele segmentów. - Pominięto fragment bez muzyki. - Pominięto zakończenie. - Pominięto zapowiedź. - Pominięto podsumowanie. - Pominięto zapowiedź. - Pominięto autoreklamę. - Pominięto treści sponsorowane. - SponsorBlock jest tymczasowo niedostępny. - SponsorBlock jest tymczasowo niedostępny (status %d). - SponsorBlock jest tymczasowo niedostępny (API nie reaguje). - Pokaż komunikat, jeśli API jest niedostępne - Wyświetla komunikat w momencie, gdy API SponsorBlock jest niedostępne. - Pokazuj komunikaty podczas automatycznego pomijania - Komunikaty są pokazywane, gdy segmenty są automatycznie pomijane. - Skopiowano ustawienia do schowka. - "Oszukuje wersję klienta do starszej wersji. - -• Zmieni to wygląd aplikacji, lecz mogą pojawić się nieznane efekty uboczne. -• Jeśli potem opcja zostanie wyłączona, stary interfejs użytkownika może pozostać do momentu wyczyszczenia danych aplikacji." - 4.27.53 - Wyłącza tryb radia w rejonach kanadyjskich - 6.11.52 - Wyłącza teksty w czasie rzeczywistym - 7.16.53 - Przywraca stary pasek akcji - Wybierz wersję, którą chcesz oszukiwać. - Docelowa wersja aplikacji - Oszukaj wersję aplikacji - diff --git a/src/main/resources/music/translations/pt-rBR/missing_strings.xml b/src/main/resources/music/translations/pt-rBR/missing_strings.xml deleted file mode 100644 index acebd7abf..000000000 --- a/src/main/resources/music/translations/pt-rBR/missing_strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Don\'t show again - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - diff --git a/src/main/resources/music/translations/pt-rBR/strings.xml b/src/main/resources/music/translations/pt-rBR/strings.xml deleted file mode 100644 index 1f85523a9..000000000 --- a/src/main/resources/music/translations/pt-rBR/strings.xml +++ /dev/null @@ -1,407 +0,0 @@ - - - Continuar - "O GmsCore não tem permissão para executar em segundo plano. - -Siga o guia Não mate o meu aplicativo para o seu telefone e aplique as instruções para a sua instalação do MicroG. - -Isto é necessário para o aplicativo funcionar." - "As otimizações da bateria GmsCore devem ser desativadas para evitar problemas. - -Toque no botão continuar e desative as otimizações da bateria." - Abrir site - Ação necessária - Ative as mensagens na nuvem para receber notificações. - Abrir GmsCore - O GmsCore não está instalado. Instale-o. - Substitui o domínio que está bloqueado em algumas regiões para que miniaturas para playlists, avatares de canais, etc. possam ser recebidos. - Ignorar restrições de imagem por região - Alterar o menu de compartilhamento do app para o meno de compartilhamento do sistema. - Alterar menu de compartilhamento - Paradas - Explorar - Início - Biblioteca - Inscrições - Selecione em qual página o aplicativo será aberto. - Alterar a página inicial - Lista de componentes a serem filtradas separadas por uma nova linha. - Filtro personalizado - Ativa o filtro personalizado para ocultar componentes do layout. - Ativar filtro personalizado - Filtro personalizado inválido: %s. - Velocidades personalizadas devem ser menores que %sx. - Velocidades de reprodução personalizadas inválidas. - Adicionar ou alterar as velocidades de reprodução disponíveis. - Editar velocidades de reprodução personalizadas - Para abrir os links de música do YouTube no RVX Music, ative \'Abrir links suportados\' e ative os endereços web suportados. - Abrir configurações padrão do aplicativo - Desativa as legendas de serem ativadas automaticamente. - Desativar legendas automáticas - Desabilita a animação inicial do Cairo quando o aplicativo é iniciado. - Desativar a animação inicial do Cairo - Desativa o redirecionamento para a próxima faixa ao clicar no botão de Dislike. - Desativar redirecionamento de dislike - Desativar deslize para alterar faixas no mini reprodutor. - Desativar gesto do mini reprodutor - Desativar deslize para alterar faixas no reprodutor. - Desativar gesto do reprodutor - Define a cor da barra de navegação para preto. - Ativar barra de navegação preta - Altera a cor de fundo do reprodutor para preto. - Ativar fundo do reprodutor preto - Corresponde à cor do mini reprodutor para o reprodutor em tela cheia. - Ativar combinação de cores do reprodutor - "Ativa o menu flutuante compacto em telefones. - -Limitações: -• A arte do álbum na guia da Biblioteca fica menor quando organizada em uma grade. -• O layout do temporizador pode parecer incomum." - Ativar diálogo compacto - Inclui o buffer no log de depuração. - Ativar o registro de depuração do buffer - Imprime o relatório de depuração - Ativar o relatório de depuração - Mantém o reprodutor minimizado mesmo quando outra faixa é reproduzida. - Ativar reprodutor minimizado forçado - Ativa o modo paisagem ao girar a tela nos telefones. - Ativar modo paisagem - Adiciona o botão próximo no mini reprodutor. - Adicionar o botão próximo no mini reprodutor - Adiciona o botão anterior no mini reprodutor. - Adicionar o botão anterior no mini reprodutor - "Ative o codec OPUS se a resposta do reprodutor incluir o codec OPUS. - -Informações: -• Os clientes mais recentes do YouTube Music usam o codec de áudio OPUS por padrão. -• Isto só é válido para usuários que falsificam com clientes muito antigos." - Ativar codec OPUS - Permite deslizar para baixo para dispensar o mini reprodutor. - Ativar deslizar para dispensar o mini reprodutor - "Adiciona a opção Cortar silêncio ao menu flutuante de velocidade de reprodução. - -Informações: -• Este recurso é para podcasts. -• Este recurso ainda está em desenvolvimento, portanto pode ser instável." - Adicionar alternador para Cortar silêncio - Também ativa o modo Calmo para podcasts. - Ativar o modo Calmo em podcasts - Altera a cor de fundo do reprodutor para cinza claro para reduzir o cansaço visual. - Ativar modo Calmo - Redefinir para os valores padrão. - Reinicie para carregar o layout normalmente - Atualizar e reiniciar - Exportar configurações para um arquivo - Falha ao exportar configurações. - As configurações foram exportadas com sucesso. - Importar - Importar configurações de um arquivo - Copiar - Importar / Exportar as configurações como texto - Importar ou exportar as configurações como texto. - Importar / Exportar configurações - A importação falhou: %s. - Configurações redefinidas para o padrão - Configurações %d importadas - Reiniciar - ReVanced Extended - "O botão de Download abre seu aplicativo de download externo. - -• Substitui apenas a ação do botão de download no reprodutor. -• Não substitui o botão de Download no menu flutuante ou na biblioteca." - Substituir ação do botão de Download - Aplicativo de download externo - "%1$s não está instalado. -Por favor, baixe %2$s do site." - Aviso - %s não está instalado. Por favor, instale-o. - Nome do pacote do seu aplicativo de download externo instalado, como NewPipe ou YTDLnis. - Nome do pacote do aplicativo de download externo - Oculta componentes vazios no menu de contas - Ocultar componentes vazios - Lista de nomes do menu de contas a serem filtrados, separados por novas linhas. - Filtro do menu de conta - Oculta os elementos do menu da conta usando o filtro personalizado. - Ocultar menu de contas - Oculta o botão Salvar. - Ocultar botão Salvar - Oculta o botão de Comentários. - Ocultar botão Comentários - Oculta o botão de Download. - Ocultar botão Download - Oculta os rótulos dos botões de ação. - Ocultar rótulo do botão de ação - Oculta os botões de Like e Deslike. Não funciona no antigo layout do reprodutor. - Ocultar botões de Like e Deslike - Oculta o botão de Rádio. - Ocultar botão Rádio - Oculta botão de Compartilhar. - Ocultar botão Compartilhar - Oculta o alternador de Áudio / Vídeo no reprodutor. - Ocultar alternador de Áudio / Vídeo - Oculta o painel de botões no feed. - Ocultar painel de botões - Oculta o painel de carrossel no feed. - Ocultar painel de carrossel - Oculta o botão Transmissão. - Ocultar botão Transmissão - Oculta a barra de categorias. - Ocultar barra de categoria - Oculta as diretrizes do canal na parte superior da seção de comentários. - Ocultar diretrizes do canal - Oculta os botões de marcação de tempo e emoji ao escrever comentários. - Ocultar botões de marcação de tempo e emoji - Oculta a sobreposição escura que aparece quando um duplo toque para procurar. - Ocultar filtro de sobreposição de toque duplo - Oculta o botão flutuante na aba Biblioteca. - Ocultar botão flutuante - Ocultar componente de 3 colunas - Ocultar menu Adicionar à fila - Ocultar menu Legendas - Ocultar menu Excuir playlist - Ocultar menu Remover fila - Ocultar menu Fazer o download - Ocultar menu Editar playlist - Ocultar menu Ir para o álbum - Ocultar menu Ir para a página do artista - Ocultar menu Acessar episódio - Ocultar menu Acessar podcast - Ocultar menu Ajuda & feedback - Ocultar botões de Like e Deslike - Ocultar menu Tocar a seguir - Ocultar menu Qualidade - Ocultar menu Remover da biblioteca - Ocultar menu Remover da playlist - Ocultar menu Denunciar - Ocultar menu Salvar episódio para mais tarde - Ocultar menu Salvar na biblioteca - Ocultar menu Salvar na playlist - Ocultar menu Compartilhar - Ocultar menu Reprodução aleatória - Ocultar menu de Timer de suspensão - Ocultar menu Iniciar rádio - Ocultar menu Estatísticas para nerds - Ocultar menu de Inscrição / Cancelar inscrição - Ocultar menu Mostrar créditos da música - Oculta anúncios em tela cheia. - Ocultar anúncios em tela cheia - "Se estiver ativado, os anúncios em tela cheia são fechados através do botão Fechar. -Se estiver desativado, os anúncios em tela cheia serão bloqueados. (pode haver efeitos colaterais)" - "Se estiver ativado, os anúncios em tela cheia são fechados através do botão Fechar. -Se estiver desativado, os anúncios em tela cheia serão bloqueados. (pode haver efeitos colaterais)" - "Se estiver ativado, os anúncios em tela cheia são fechados através do botão Fechar. -Se estiver desativado, os anúncios em tela cheia serão bloqueados. (pode haver efeitos colaterais)" - Fechar anúncios em tela cheia - Oculta o botão Compartilhar no reprodutor de tela cheia. - Ocultar botão Compartilhar em tela cheia - Oculta anúncios gerais. - Ocultar anúncios gerais - Oculta o identificador no menu da conta. - Ocultar identificador - Oculta o botão Histórico na barra de ferramentas. - Ocultar botão Histórico - Oculta os anúncios antes de reproduzir mídia. - Ocultar anúncios de mídia - Oculta a barra de navegação. - Ocultar barra de navegação - Oculta o botão Explorar. - Ocultar botão Explorar - Oculta o botão de Início. - Ocultar botão Início - Oculta rótulos abaixo dos botões de navegação. - Ocultar rótulos de navegação - Oculta o botão Biblioteca. - Ocultar botão Biblioteca - Oculta o botão Descobertas. - Ocultar botão Descobertas - Oculta o botão Upgrade. - Ocultar botão Upgrade - Oculta o botão Notificações na barra de ferramentas. - Ocultar botão Notificações - Oculta o rótulo de promoção paga. - Ocultar rótulo de promoção paga - Oculta o painel de cartão de lista de reprodução no feed. - Ocultar painel de cartão de lista de reprodução - Oculta pop-ups de promoção premium. - Ocultar pop-ups de promoção premium - Oculta o banner de renovação premium. - Ocultar banner de renovação premium - Oculta o banner de alerta de promoção. - Ocultar banner de alerta de promoção - Oculta o painel Descobertas no feed. - Ocultar painel Descobertas - Ocultar menu Sobre o YouTube Music - Ocultar menu Economia de dados - Ocultar menu Downloads e armazenamento - Ocultar menu Geral - Ocultar Menu Notificações - Ocultar menu Conheça o Music Premium - Ocultar menu Central da família - Ocultar menu Reprodução - Ocultar menu Privacidade e dados - Ocultar menu Recomendações - "Oculte elementos do menu de configurações. -Isso oculta não apenas o menu de configurações do YT Music, mas também o menu de configurações do ReVanced Extended." - Ocultar menu de configurações - Oculta o botão de pesquisa de som na barra de pesquisa. - Ocultar botão de pesquisa de som - Oculta o botão Toque para atualizar. - Ocultar botão Toque para atualizar - Oculta o contêiner dos Termos de Serviço. - Ocultar contêiner de termos - Oculta o botão de pesquisa por voz na barra de pesquisa. - Ocultar botão de pesquisa por voz - Conta - Barra de ação - Anúncios - Menu flutuante - Geral - Diversos - Barra de Navegação - Reprodutor - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Menu de configurações - Vídeo - Lembra a última velocidade de reprodução selecionada. - Lembrar mudança na velocidade de reprodução - Exibir uma notificação flutuante quando alterar a velocidade padrão de reprodução. - Exibir uma notificação flutuante - Alterando a velocidade padrão para %s. - Lembra o estado da alternância de repetição. - Lembrar estado de repetição - Lembre o estado da alternância do modo aleatório. - Lembrar estado do modo aleatório - Lembra a última qualidade de vídeo selecionada. - Lembrar mudança na qualidade do vídeo - Exibir uma notificação flutuante quando alterar a qualidade padrão do vídeo. - Exibir uma notificação flutuante - Alterando a qualidade padrão de dados móveis para %s. - Falha ao definir qualidade. - Alterando a qualidade padrão do Wi-Fi para %s. - "Remover o diálogo discricionário de visualização. -Isso não ignora a restrição de idade, apenas aceita isso automaticamente." - Remover o diálogo discricionário do visualizador - Continua o vídeo a partir do tempo atual ao mudar para o YouTube. - Continuar assistindo - Substitui o menu \'Remover fila\' pelo menu \'Assistir no YouTube\'. - Substituir Remover fila - Assistir no YouTube - URL de vídeo inválida. - Mantém intacto o menu Denunciar na seção de comentários. - Manter Denunciar nos comentários - Substitui o menu Denunciar pelo menu Velocidade de Reprodução. - Substituir menu Denunciar - Retorna os painéis popup de comentários ao estilo antigo. - Restaurar antigo painel popup de comentários - Retorna o fundo do reprodutor para o estilo antigo. - Restaurar antigo fundo do reprodutor - "Retorna o layout do reprodutor ao estilo antigo. -Alguns recursos podem não funcionar corretamente no layout antigo do reprodutor." - Restaurar antigo layout do reprodutor - Retorna a aba da Biblioteca para o estilo antigo. (Experimental) - Restaurar antigo estilo do painel da biblioteca - \@identificador (Nome de usuário) - Selecione o formato de exibição do nome de usuário. - Formato de exibição - Nome de usuário (@identificador) - Nome de usuário - Substitui identificadores por nomes de usuários em comentários. - Ativar Return YouTube Username - "Uma Chave de desenvolvedor da API de Dados do YouTube v3 é necessária para substituir identificadores por nomes de usuários. - -A cota diária para chaves de API no plano gratuito é de 10.000, e 1 cota é usada para substituir um identificador por um nome de usuário para 1 comentário. - -Clique para ver como emitir uma chave de API." - Sobre a chave API de dados do YouTube - A chave de desenvolvedor para usar a API de Dados do YouTube v3. - Chave API dos Dados do YouTube - 1. Vá para <a href=%1$s>Criar um novo projeto</a>.<br>2. Clique no botão <b>CRIAR</b>.<br>3. Vá para <a href=%2$s>API de dados do YouTube v3</a>.<br>4. Clique no botão <b>ATIVAR</b>.<br>5. Clique no botão <b>CRIAR CREDENCIAIS</b>.<br>6. Selecione a opção <b>Dados públicos</b>.<br>7. Clique no botão <b>PRÓXIMO</b>.<br>8. Copie a chave da API.<br><br>※ A chave da API nunca deve ser compartilhada com outras pessoas, portanto, ela não é incluída nas configurações de Importação/Exportação. - Emitir chave de desenvolvedor da API de dados do YouTube v3 - Sobre - Os dados são fornecidos pela API do Return YouTube Dislike. Toque aqui para saber mais. - ReturnYouTubeDislike.com - Oculta o separador do botão curtir. - Botão de curtir compacto - Exibe a porcentagem de deslikes em vez da contagem de deslikes. - Dislikes como porcentagem - Mostra a contagem de deslike dos vídeos. - Ativar Return YouTube Dislike - Mostra a contagem estimada de curtidas dos vídeos. - Exibir curtidas estimadas - Dislikes indisponível (limite de API do cliente atingido). - Deslikes indisponível (status %d). - Dislikes temporariamente indisponível (API expirou). - Deslikes indisponível (%s). - Notificação flutuante exibida se o Return YouTube Dislike não está disponível. - Exibir uma notificação flutuante se a API não estiver disponível - Oculto - Remove os parâmetros de consulta de rastreamento das URLs ao compartilhar os links. - Limpar links compartilhados - Sobre - sponsor.ajay.app - Os dados são fornecidos pela API do SponsorBlock. Toque aqui para aprender mais e ver downloads para outras plataformas. - Alterar URL da API - URL da API alterada. - URL da API é inválida. - Redefinir URL da API. - O endereço que o SponsorBlock usa para fazer chamadas para o servidor. Não mude isso a menos que você saiba o que está fazendo. - Cor alterada. - Cor: - Código de cor inválido. - Redefinir cor. - Alterar comportamento do segmento - Ativar SponsorBlock - SponsorBlock é um sistema coletivo para pular partes irritantes de vídeos do YouTube. - Redefinir cor - Enrolação / Piadas - Cenas tangenciais inseridas apenas por enrolação ou humor que não são necessárias para compreender o tópico principal do vídeo. Isto não deve incluir segmentos que fornecem contexto ou detalhes de segundo plano. - Lembrete de Interação (Inscreva-se) - Um breve lembrete para curtir, se inscrever ou segui-los no meio do conteúdo. Se for longo ou sobre algo específico, deve estar sob autopromoção. - Intervalo / Introdução Animada - Um intervalo sem conteúdo real. Pode ser uma pausa, um quadro estático ou uma animação repetida. Não inclui transições contendo informações. - Música: Seção sem música - Somente para uso em vídeos de música. Seções de vídeos de música sem música, que já não estão cobertas por outra categoria. - Cartões finais / Créditos - Créditos ou quando os cartões finais do YouTube aparecem. Não para conclusões com informações. - Pré-visualização / Recapitulação / Hook - Coleção de clipes que mostram o que está por vir ou o que aconteceu no vídeo ou em outros vídeos de uma série, onde todas as informações são repetidas em outro lugar. - Não pago / Autopromoção - Semelhante ao Patrocinador exceto pela promoção não paga ou autopromoção. Inclui seções sobre mercadorias, doações ou informações sobre com quem eles colaboraram. - Patrocinador - Promoção paga, referências pagas e anúncios diretos. Não deve ser usado para auto-promoção ou mensagens grátis para causas / criadores/sites / produtos que eles gostam. - Pular automaticamente - Desativar - Enrolação pulada. - Lembrete irritante pulado. - Introdução pulada. - Intervalo pulado. - Intervalo pulado. - Pulou vários segmentos. - Segmento sem música pulado. - Outro pulado. - Pré-visualização pulada. - Recapitulação pulada. - Pré-visualização pulada. - Autopromoção pulada. - Patrocinador pulado. - O SponsorBlock está temporariamente indisponível. - O SponsorBlock está temporariamente indisponível (status %d). - O SponsorBlock está temporariamente indisponível (API expirou). - Exibir uma notificação flutuante se a API não estiver disponível - Exibe uma notificação flutuante se a API SponsorBlock não estiver disponível. - Exibir uma notificação flutuante quando pular automaticamente - Uma notificação flutuante é exibida quando um segmento é ignorado automaticamente. - Configurações copiadas para área de transferência. - "Falsifica a versão do cliente para uma versão mais antiga. - -• Isto alterará a aparência do aplicativo, mas poderão ocorrer efeitos colaterais desconhecidos. -• Se for desativada posteriormente, a interface do usuário antiga poderá permanecer até que os dados do aplicativo sejam apagados." - 4.27.53 - Desativar modo de Rádio em regiões Canadenses - 6.11.52 - Desativar letras em tempo real - 7.16.53 - Restaurar barra de ação antiga - Selecione a versão do app para falsificação. - Versão da falsificação do aplicativo - Falsificar a versão do aplicativo - diff --git a/src/main/resources/music/translations/ro-rRO/missing_strings.xml b/src/main/resources/music/translations/ro-rRO/missing_strings.xml deleted file mode 100644 index 9be1519b5..000000000 --- a/src/main/resources/music/translations/ro-rRO/missing_strings.xml +++ /dev/null @@ -1,329 +0,0 @@ - - - Continue - Don\'t show again - "GmsCore does not have permission to run in the background. - -Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. - -This is required for the app to work." - "GmsCore battery optimizations must be disabled to prevent issues. - -Tap on the continue button and disable battery optimizations." - Open website - Action needed - Enable cloud messaging to receive notifications. - Open GmsCore - GmsCore is not installed. Install it. - Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. - Bypass image region restrictions - Change from in-app share sheet to system share sheet. - Change share sheet - Charts - Explore - Home - Library - Subscriptions - Select which page the app opens in. - Change start page - Invalid custom filter: %s. - Invalid custom playback speeds. - To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Disables Cairo splash animation when the app starts up. - Disable Cairo splash animation - Disables redirection to the next track when clicking the Dislike button. - Disable dislike redirection - Disable swipe to change tracks in the miniplayer. - Disable miniplayer gesture - Disable swipe to change tracks in the player. - Disable player gesture - Changes the player background color to black. - Enable black player background - Includes the buffer in the debug log. - Enable debug buffer logging - Adds a next track button to the miniplayer. - Add miniplayer next button - Adds a previous track button to the miniplayer. - Add miniplayer previous button - Enables swipe down to dismiss miniplayer. - Enable swipe to dismiss miniplayer - "Adds a Trim silence switch to the playback speed flyout menu. - -Info: -• This feature is for podcasts. -• This feature is still in development, so it may be unstable." - Add Trim silence switch - Also enables Zen mode for podcasts. - Enable Zen mode in podcasts - Reset to default values. - Restart to load the layout normally - Refresh and restart - Export settings to file - Failed to export settings. - Settings were successfully exported. - Import settings from file - Import / Export settings as text - Import failed: %s. - Reset - ReVanced Extended - "Download button opens your external downloader. - -• Only overrides the Download action button in the player. -• Does not override the Download button in the flyout menu or Library tab." - Override Download action button - External downloader - "%1$s is not installed. -Please download %2$s from the website." - Warning - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - List of account menu names to filter, separated by new lines. - Account menu filter - Hides the Save button. - Hide Save button - Hides the Comments button. - Hide Comments button - Hides the Download button. - Hide Download button - Hides the labels of the action buttons. - Hide action button labels - Hides the Like and Dislike buttons. It does not work in the old player layout. - Hide Like and Dislike buttons - Hides the Radio button. - Hide Radio button - Hides the Share button. - Hide Share button - Hides the Audio / Video toggle in the player. - Hide Audio / Video toggle - Hides the channel guidelines at the top of the comments section. - Hide channel guidelines - Hides the timestamp and emoji buttons when typing comments. - Hide timestamp and emoji buttons - Hides dark overlay that appears when double-tapping to seek. - Hide double-tap overlay filter - Hides the floating button in the Library tab. - Hide floating button - Hide 3-column component - Hide Add to queue menu - Hide Captions menu - Hide Delete playlist menu - Hide Dismiss queue menu - Hide Download menu - Hide Edit playlist menu - Hide Go to album menu - Hide Go to artist menu - Hide Go to episode menu - Hide Go to podcast menu - Hide Help & feedback menu - Hide Like and Dislike buttons - Hide Play next menu - Hide Quality menu - Hide Remove from library menu - Hide Remove from playlist menu - Hide Report menu - Hide Save episode for later menu - Hide Save to library menu - Hide Save to playlist menu - Hide Share menu - Hide Shuffle play menu - Hide Sleep timer menu - Hide Start radio menu - Hide Stats for nerds menu - Hide Subscribe / Unsubscribe menu - Hide View song credits menu - Hides fullscreen ads. - Hide fullscreen ads - "If it is enabled, fullscreen ads are closed through the Close button. -If it is disabled, fullscreen ads are blocked. (there may be side effects)" - Fullscreen ads are blocked. (there may be side effects) - Fullscreen ads are closed through the Close button. - Close fullscreen ads - Hides the Share button in the fullscreen player. - Hide fullscreen Share button - Hides general ads. - Hide general ads - Hides the Explore button. - Hide Explore button - Hides the Home button. - Hide Home button - Hides the Library button. - Hide Library button - Hides the Samples button. - Hide Samples button - Hides the Upgrade button. - Hide Upgrade button - Hides the Notifications button in the toolbar. - Hide Notifications button - Hides the paid promotion label. - Hide paid promotion label - Hides the playlist card shelf in the feed. - Hide playlist card shelf - Hides premium promotion popups. - Hide premium promotion popups - Hides the premium renewal banner. - Hide premium renewal banner - Hides the promotion alert banner. - Hide promotion alert banner - Hides the Samples shelf in the feed. - Hide Samples shelf - Hide About menu - Hide Data saving menu - Hide Downloads & storage menu - Hide General menu - Hide Notifications menu - Hide Get Music premium menu - Hide Family Center menu - Hide Playback menu - Hide Privacy & data menu - Hide Recommendations menu - "Hide elements of the settings menu. -This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." - Hide settings menu - Hides the sound search button in the search bar. - Hide sound search button - Hides the Tap to update button. - Hide Tap to update button - Hides the voice search button in the search bar. - Hide voice search button - Account - Action Bar - Ads - Flyout Menu - General - Miscellaneous - Navigation Bar - Player - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Settings menu - Video - Remembers the last playback speed selected. - Remember playback speed changes - Show a toast when changing the default playback speed. - Show a toast - Changing default speed to %s. - Remembers the last video quality selected. - Remember video quality changes - Show a toast when changing the default video quality. - Show a toast - Changing default mobile data quality to %s. - Failed to set quality. - Changing default Wi-Fi quality to %s. - "Removes the viewer discretion dialog. -This does not bypass the age restriction. It just accepts it automatically." - Remove viewer discretion dialog - Continues the video from the current time when switching to YouTube. - Continue watching - Replaces the Dismiss queue menu with the Watch on YouTube menu. - Replace Dismiss queue menu - Watch on YouTube - Invalid video url. - Keeps the Report menu in the comments section intact. - Keep Report in comments - Replaces the Report menu with the Playback speed menu. - Replace Report menu - Returns the comments popup panels to the old style. - Restore old comments popup panels - Returns the player background to the old style. - Restore old player background - "Returns the player layout to the old style. -Some features may not work properly in the old player layout." - Restore old player layout - Returns the Library tab to the old style. (Experimental) - Restore old style library shelf - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Username - Replaces handles with usernames in comments. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - ReturnYouTubeDislike.com - Shows the dislike count of videos. - Enable Return YouTube Dislike - Shows the estimated like count of videos. - Show estimated likes - Dislikes are unavailable (client API limit reached). - Dislikes are unavailable (status %d). - Dislikes are temporarily unavailable (API timed out). - Dislikes are unavailable (%s). - Shows a toast if the Return YouTube Dislike API is unavailable. - Show a toast if API is unavailable - Hidden - Removes tracking query parameters from URLs when sharing links. - About - sponsor.ajay.app - Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. - Change API URL - API URL changed. - API URL is invalid. - API URL reset. - The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. - Color changed. - Color: - Invalid color code. - Color reset. - Change segment behavior - Enable SponsorBlock - SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. - Reset color - Filler Tangent / Jokes - Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. - Interaction Reminder (Subscribe) - A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. - Intermission / Intro Animation - An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. - Music: Non-Music Section - Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. - Endcards / Credits - Credits or when the YouTube endcards appear. Not for conclusions with information. - Preview / Recap / Hook - Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. - Unpaid / Self Promotion - Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. - Sponsor - Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. - Skip automatically - Disable - Skipped filler. - Skipped annoying reminder. - Skipped intro. - Skipped intermission. - Skipped intermission. - Skipped multiple segments. - Skipped a non-music section. - Skipped outro. - Skipped preview. - Skipped recap. - Skipped preview. - Skipped self promotion. - Skipped sponsor. - SponsorBlock is temporarily unavailable. - SponsorBlock is temporarily unavailable (status %d). - SponsorBlock is temporarily unavailable (API timed out). - Show a toast if API is unavailable - Shows a toast if the SponsorBlock API is unavailable. - Show a toast when skipping automatically - Shows a toast when a segment is automatically skipped. - "Spoofs the client version to an older version. - -• This will change the appearance of the app, but unknown side effects may occur. -• If later disabled, the old UI may remain until the app data is cleared." - 6.11.52 - Disable real-time lyrics - 7.16.53 - Restore old action bar - Select the spoof app version target. - Spoof app version target - Spoof app version - diff --git a/src/main/resources/music/translations/ro-rRO/strings.xml b/src/main/resources/music/translations/ro-rRO/strings.xml deleted file mode 100644 index 28cef9ec1..000000000 --- a/src/main/resources/music/translations/ro-rRO/strings.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - Filtrează numele componentelor după linie separat. - Editați filtrul personalizat - Activează filtru personalizat pentru a ascunde componentele aspectului. - Activați filtrul personalizat - Viteze de redare personalizate invalide! Resetare la valorile implicite. - Adaugă sau modifică vitezele de redare disponibile. - Modifică vitezele de redare personalizate - Dezactivează subtitrările automate forțate. - Dezactivează subtitrările automate forțate - Setează culoarea barei de navigare la negru. - Activare bară neagră de navigare - Potrivește culoarea mini player-ului și a player-ului pe tot ecranul. - Activare culori potrivire player - "Activați dialogul compact pe telefon. - -Probleme cunoscute: -• Arta albumului pe raftul bibliotecii devine de asemenea mai mică. -• Aspect cronometru somn poate părea neobișnuit." - Activare dialog compact - Afișează jurnalul de depanare. - Activează jurnalul de depanare - Menține player-ul minimizat permanent chiar dacă o altă piesă este redată. - Activare player minimizat forțat - Activează intrarea în modul peisaj după rotirea ecranului pe telefon. - Activare mod peisaj - "Activează codec-ul opus 250/251 atunci când redați audio." - Activează codec opus - Adaugă o nuanță gri la player-ul video pentru a reduce oboseala ochilor. - Activare mod zen - Importă - Copiere - Importă sau exportă setările ca text. - Importă / Exportă - Setări resetate la valorile implicite. - Setări %d importate. - %s nu este instalat. Vă rugăm să-l instalaţi. - Numele pachetului al aplicației externe de descărcare instalate, cum ar fi NewPipe sau YTDLnis. - Numele pachetului de descărcare extern - Ascunde componentele goale în meniul contului. - Ascunde componenta goală - Ascunde elementele meniului contului. - Ascunde meniul contului - Ascunde raftul butonului de pe pagina principală și explorare. - Ascunde buton raft - Ascunde raftul carusel de pe pagina principală și explorare. - Ascunde raft carusel - Ascunde butonul de difuzare. - Ascunde butonul de difuzare - Ascunde bara de categorii de muzică din partea de sus a paginii principale. - Ascunde bara de categorie - Ascunde mânerul în comutatorul de conturi. - Ascunde etichetele - Ascunde butonul de istoric în bara de instrumente. - Ascunde butonul de istoric - Ascunde reclamele înainte de a reda o piesă. - Ascunde reclamele muzicale - Ascunde bara de navigare. - Ascunde bara de navigare - Ascunde etichetele în bara de navigare. - Ascunde eticheta de navigare - Ascunde containerul termenilor și condițiilor de utilizare. - Ascunde containerul termenilor - Reține starea repetării. - Memorează starea repetării - Reține starea redării aleatorii. - Reține starea redării aleatorii - Despre - Datele sunt furnizate de API-ul Returnare YouTube Dislike. Atinge aici pentru a afla mai multe. - Ascunde separatorul butonului apreciez. - Buton compact apreciez - În loc de numărul de dezaprobări, se afișează procentul de dezaprobări. - Dislike-uri ca procentaj - Curăță link-urile de partajare - Setări copiate în clipboard. - 4.27.53 - Dezactivare mod radio în regiunile canadiene - diff --git a/src/main/resources/music/translations/ru-rRU/missing_strings.xml b/src/main/resources/music/translations/ru-rRU/missing_strings.xml deleted file mode 100644 index a687f9a25..000000000 --- a/src/main/resources/music/translations/ru-rRU/missing_strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Don\'t show again - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - diff --git a/src/main/resources/music/translations/ru-rRU/strings.xml b/src/main/resources/music/translations/ru-rRU/strings.xml deleted file mode 100644 index dc5591ecd..000000000 --- a/src/main/resources/music/translations/ru-rRU/strings.xml +++ /dev/null @@ -1,407 +0,0 @@ - - - Продолжить - "MicroG GmsCore не имеет разрешения на запуск в фоновом режиме. - -Следуйте инструкции \"Don't kill my app\" для Вашего телефона и установите MicroG согласно ее. - -Это необходимо для работы приложения." - "Во избежание проблем необходимо отключить оптимизацию батареи для MicroG GmsCore. - -Нажмите кнопку \"Продолжить\" и отключите оптимизацию батареи." - Открыть сайт - Требуется действие - Включите \"Облачные уведомления\" для получения уведомлений. - Открыть GmsCore - GmsCore не установлен. Установите его. - Заменяет заблокированный в некоторых регионах домен, чтобы можно было получать миниатюры плейлистов, аватары каналов и т. д. - Обойти ограничения изображений по региону - Меняет тип диалогового окна \"Поделиться\" из встроенного на системное. - Изменить окно \"Поделиться\" - Хит-парады - Навигатор - Главная - Библиотека - Подписки - Выберите, на какой странице открывается приложение. - Изменить начальную страницу - Настройте, какие компоненты фильтровать, обозначая конец каждого из них новой строкой. - Изменить пользовательский фильтр - Включает пользовательский фильтр для скрытия компонентов интерфейса. - Пользовательский фильтр - Недопустимый пользовательский фильтр: %s. - Недопустимые скорости. Значения сброшены к начальным. - Недопустимые пользовательские скорости воспроизведения. Используются значения по умолчанию. - Настройте доступные скорости воспроизведения. - Изменить скорости - Чтобы открыть ссылку на YouTube Music в RVX Music, включите \"Открывать поддерживаемые ссылки\" и включите поддерживаемые веб-адреса. - Открыть настройки по умолчанию - Отключает автоматическое включение субтитров. - Отключить автоматические субтитры - Отключает анимацию Кайро при запуске приложения. - Отключить анимацию Кайро - Отключает перенаправление на следующий трек при нажатии на кнопку \"Не нравится\". - Отключить переключение при \"Не нравится\" - Отключает свайп для переключения треков в миниплеере. - Отключить жест мини плеера - Отключает свайп для переключения треков в плеере. - Отключить жест плеера - Устанавливает чёрный цвет панели навигации. - Чёрная панель навигации - Меняет адаптивный цвет фона плеера на черный. - Включить черный фон плеера - Цвет мини-проигрывателя соответствует цвету полноэкранного проигрывателя. - Цветовое соответствие проигрывателей - "Включает компактное всплывающее меню на телефонах. - -Известные проблемы: -• Заставки альбомов во вкладке \"Библиотека\" становятся меньше в виде сетки. -• Интерфейс \"Автовыключение\" может необычно появляться." - Компактный вид окна - Вывод журнала отладки включает буфер. - Ведение журналов отладки буфера - Выводит журнал отладки. - Ведение журнала отладки - Удерживает проигрыватель свёрнутым, даже если играет другой трек. - Удерживать проигрыватель свёрнутым - Включает альбомный режим при повороте экрана на телефонах. - Альбомный режим - Включает кнопку следующего трека в миниплеере. - Включить кнопку следующего в миниплеере - Включает кнопку предыдущего трека в миниплеере. - Включить кнопку предыдущего в миниплеере - "Включает аудио кодек opus вместо аудио кодека mp4a. - -Информация: -• Последние Android клиенты используют аудио кодек opus по умолчанию. -• Эта функция подходит только для очень старых клиентов." - Кодек Opus - Включает жест вниз для скрытия миниплеера. - Включить жест скрытия миниплеера - "Включает переключатель \"Обрезать тишину\" во всплывающем меню скорости воспроизведения. - -Информация: -• Эта функция предназначена для подкастов. -• Эта функция все еще находится в разработке, поэтому может работать нестабильно." - Включить обрезание тишины - Режим \"Дзен\" также применяется к подкастам. - Включить режим \"Дзен\" в подкастах - Меняет оттенок фона проигрывателя видео на светло-серый, чтобы уменьшить нагрузку на глаза. - Режим \"Дзен\" - Сброшены до значений по умолчанию. - Перезапустите для правильной загрузки интерфейса - Обновите и перезапустите - Извлечь настройки в файл - Не удалось извлечь настройки. - Настройки успешно извлечены. - Восстановить - Восстановить настройки из файла - Копировать - Восстановить / Извлечь настройки в виде текста - Восстановить или извлечь настройки. - Восстановить / Извлечь настройки - Не удалось восстановить: %s. - Настройки сброшены до начальных. - Восстановлено %d настройки(ек). - Сбросить - ReVanced Extended - "Кнопка \"Скачать\" открывает внешний загрузчик. - -• Переопределяет только кнопку загрузки в плеере. -• Не переопределяет кнопку загрузки во всплывающем меню или библиотеке." - Подменить кнопку \"Скачать\" - Внешний загрузчик - "%1$s не установлен. -Пожалуйста, скачайте %2$s с сайта." - Внимание - %s не установлен. Пожалуйста, установите его. - Имя пакета вашего установленного внешнего загрузчика. Например, NewPipe или YTDLnis. - Имя пакета внешнего загрузчика - Скрывает пустой пункт в меню аккаунта. - Скрыть пустой пункт - Список имен меню учетной записи для фильтрации, разделяемых новой строкой. - Фильтр меню аккаунта - Скрывает элементы меню аккаунта в пользовательском фильтре. - Скрыть меню аккаунта - Скрывает кнопку \"Добавить в плейлист\". - Скрыть кнопку \"Добавить в плейлист\" - Скрывает кнопку \"Комментарии\". - Скрыть кнопку \"Комментарии\" - Скрывает кнопку \"Скачать\". - Скрыть кнопку \"Скачать\" - Скрывает подписи на кнопках действий. - Скрыть подписи кнопок действий - Скрывает кнопки \"Нравится\" и \"Не нравится\". Не работает в старом интерфейсе проигрывателя. - Скрыть кнопки \"Нравится\" и \"Не нравится\" - Скрывает кнопку \"Включить радиостанцию\". - Скрыть кнопку \"Включить радиостанцию\" - Скрывает кнопку \"Поделиться\". - Скрыть кнопку \"Поделиться\" - Скрывает переключатель \"аудио/видео\" в плеере. - Скрыть переключатель \"аудио/видео\" - Скрывает ряд кнопок с главной страницы и со вкладки \"Навигатор\". - Скрыть ряд кнопок - Скрывает карусель треков с главной страницы и со вкладки \"Навигатор\". - Скрыть карусель треков - Скрывает кнопку \"Трансляция\". - Скрыть кнопку \"Трансляция\" - Скрывает панель категорий. - Скрыть панель категорий - Скрывает правила канала в верхней части комментариев. - Скрыть правила канала - Скрывает кнопки метки времени и эмодзи при вводе комментариев. - Метка времени и кнопки эмодзи - Скрывает затемнение, которое появляется при двойном нажатии при перемотке. - Скрыть фильтр двойного нажатия - Скрывает всплывающую кнопку в библиотеке. - Скрыть всплывающую кнопку - Скрыть компонент из 3 столбцов - Скрыть пункт \"Добавить в очередь\" - Скрыть пункт \"Субтитры\" - Скрыть пункт \"Удалить плейлист\" - Скрыть пункт \"Очистить очередь\" - Скрыть пункт \"Скачать\" - Скрыть пункт \"Изменить плейлист\" - Скрыть пункт \"Открыть альбом\" - Скрыть пункт \"Перейти на страницу исполнителя\" - Скрыть пункт \"Перейти к выпуску\" - Скрыть пункт \"Перейти к подкасту\" - Скрыть пункт \"Справка и отзывы\" - Скрыть кнопки \"Нравится\" и \"Не нравится\" - Скрыть пункт \"Включить следующим\" - Скрыть пункт \"Качество\" - Скрыть пункт \"Удалить из библиотеки\" - Скрыть пункт \"Удалить из плейлиста\" - Скрыть пункт \"Пожаловаться\" - Скрыть пункт \"Слушать позже\" - Скрыть пункт \"Сохранить в библиотеке\" - Скрыть пункт \"Добавить в плейлист\" - Скрыть пункт \"Поделиться\" - Скрыть пункт \"Перемешать\" - Скрыть пункт \"Автовыключение\" - Скрыть пункт \"Включить радиостанцию\" - Скрыть пункт \"Статистика для сисадминов\" - Скрыть пункт \"Подписаться / Отменить подписку\" - Скрыть пункт \"Участники и создатели\" - Скрывает полноэкранную рекламу. - Полноэкранная реклама - "Если включено, полноэкранная реклама закрывается через кнопку Закрыть. -Если отключено, полноэкранная реклама блокируется. (могут возникать побочные эффекты)" - "Если включено, полноэкранная реклама закрывается через кнопку Закрыть. -Если отключено, полноэкранная реклама блокируется. (могут возникать побочные эффекты)" - "Если включено, полноэкранная реклама закрывается через кнопку Закрыть. -Если отключено, полноэкранная реклама блокируется. (могут возникать побочные эффекты)" - Закрывать полноэкранную рекламу - Скрывает кнопку \"Поделиться\" в полноэкранном проигрывателе. - Скрыть кнопку \"Поделиться\" в полноэкранном режиме - Скрывает рекламу общего формата. - Скрыть рекламу общего формата - Скрывает электронную почту / @ник в меню смены аккаунтов. - Скрыть электронную почту / @ник - Скрывает кнопку \"История\" на панели инструментов. - Скрыть кнопку \"История\" - Скрывает рекламу перед воспроизведением музыки. - Скрыть музыкальную рекламу - Скрывает панель навигации. - Скрыть панель навигации - Скрывает кнопку \"Навигатор\". - Скрыть кнопку \"Навигатор\" - Скрывает кнопку \"Главная\". - Скрыть кнопку \"Главная\" - Скрывает подписи под кнопками навигации. - Скрыть подписи кнопок навигации - Скрывает кнопку \"Библиотека\". - Скрыть кнопку \"Библиотека\" - Скрывает кнопку \"Семплы\". - Скрыть кнопку \"Семплы\" - Скрывает кнопку \"Платные подписки\". - Скрыть кнопку \"Платные подписки\" - Скрывает кнопку \"Уведомления\" на панели инструментов. - Скрыть кнопку \"Уведомления\" - Скрывает метку \"Содержит прямую рекламу\". - Скрыть \"Содержит прямую рекламу\" - Скрывает полку с заставкой плейлиста в ленте. - Скрыть полку с заставкой плейлиста - Скрывает всплывающую рекламу Premium. - Скрыть всплывающую рекламу Premium - Скрывает баннер продления Premium. - Скрыть баннер продления Premium - Скрывает баннер с уведомлением о промо акции. - Скрыть баннер с уведомлением о промо акции - Скрывает полку \"Семплы\" в ленте. - Скрыть полку \"Семплы\" - Скрыть \"О YouTube Music\" - Скрыть \"Экономия трафика\" - Скрыть \"Скачивание и хранение\" - Скрыть \"Общие\" - Скрыть \"Уведомления\" - Скрыть \"Оформить подписку Music Premium\" - Скрыть \"Семейный центр\" - Скрыть \"Воспроизведение\" - Скрыть \"Конфиденциальность и данные\" - Скрыть \"Рекомендации\" - "Скрывает элементы меню настроек. -При этом скрывается не только меню настроек YT Music, но и меню настроек ReVanced Extended." - Скрыть меню настроек - Скрывает кнопку поиска звука в строке поиска. - Скрыть кнопку поиска звука - Скрывает кнопку \"Обновить\". - Скрыть кнопку \"Обновить\" - Скрывает пункт \"Конфиденциальность • Условия\". - Скрыть \"Конфиденциальность • Условия\" - Скрывает кнопку голосового поиска в строке поиска. - Скрыть кнопку голосового поиска - Аккаунт - Панель действий - Реклама - Выдвижное меню - Основные - Разное - Панель навигации - Плеер - Вернуть имя пользователя YouTube - Вернуть YouTube Dislike - SponsorBlock - Меню настроек - Видео - Запоминает последнюю выбранную скорость воспроизведения. - Запоминать изменения скорости - Показывать всплывающее уведомление при смене скорости воспроизведения по умолчанию. - Показывать всплывающее уведомление - Скорость по умолчанию изменена на %s. - Запоминает состояние переключателя \"Повтор воспроизведения\". - Запоминать состояние повтора - Запоминает состояние переключателя \"Перемешать\". - Запоминать состояние перемешивания - Запоминает последнее выбранное качество видео. - Запоминать изменения качества видео - Показывать всплывающее уведомление при смене качества видео по умолчанию. - Показывать всплывающее уведомление - Качество по умолчанию при моб. сети изменено на %s. - Не удалось установить качество. - Качество по умолчанию при Wi-Fi изменено на %s. - "Убирает окно о нежелательном контенте. -Не обходит возрастное ограничение, просто принимает его автоматически." - Убрать окно о нежелательном контенте - Видео продолжается с текущего времени просмотра при переходе на YouTube. - Продолжить просмотр - Заменяет \"Очистить очередь\" на \"Открыть в YouTube\'. - Замена пункта \"Очистить очередь\" - Смотреть на YouTube - Нерабочая ссылка на видео. - Сохраняет пункт меню \"Пожаловаться\" в комментариях не тронутым. - Сохранить пункт \"Пожаловаться\" - Заменяет \"Пожаловаться\" на \"Скорость воспроизведения\". - Заменить \"Пожаловаться\" - Возвращает всплывающие панели комментариев к старому стилю. - Восстановить старые всплывающие панели комментариев - Возвращает фон проигрывателя к старому стилю. - Восстановить старый фон плеера - "Возвращает интерфейс проигрывателя к старому стилю. -Некоторые вещи могут работать неправильно в старом интерфейсе проигрывателя." - Восстановить старый интерфейс проигрывателя - Возвращает вкладку \"Библиотека\" к старому стилю. (Экспериментальная опция) - Восстановить старый стиль вкладки \"Библиотека\" - \@псевдоним (Имя пользователя) - Выбор формата отображения имени пользователя. - Формат отображения - Имя пользователя (@псевдоним) - Имя пользователя - Заменяет псевдонимы имена пользователей в комментариях. - Включить возврат имени пользователя YouTube - "Чтобы заменить псевдонимы на имена пользователей, необходим ключ разработчика YouTube Data API v3. - -Ежедневная квота для ключей API в бесплатном тарифе составляет 10 000 и 1 квота используется для замены псевдонима на имя пользователя для 1 комментария. - -Нажмите, чтобы узнать, как создать ключ API." - О ключе YouTube Data API - Ключ разработчика для использования API YouTube Data v3. - Ключ YouTube Data API - 1. Перейдите в раздел <a href=%1$s>Создать New Project</a>.<br>2. Нажмите кнопку <b>CREATE</b>.<br>3. Перейдите в <a href=%2$s>YouTube Data API v3</a>.<br>4. Нажмите кнопку <b>ENABLE</b>.<br>5. Нажмите кнопку <b>CREATE CREDENTIALS</b>.<br>6. Выберите <b>Public data</b>.<br>7. Нажмите кнопку <b>NEXT</b>.<br>8. Скопируйте ключ API. <br><br>※ Ключ API нельзя предоставлять другим, поэтому он не включен в Импорт/Экспорт настроек. - Создание ключа разработчика YouTube Data API v3 - Об интеграции - Данные предоставлены Return YouTube Dislike API. Нажмите здесь, чтобы узнать больше. - ReturnYouTubeDislike.com - Скрывает линию после кнопки \"Нравится\". - Компактная кнопка \"Нравится\" - Вместо числа отметок \"Не нравится\", они отображаются как процент. - Кол-во отметок \"Не нравится\" в процентах - Отображает количество отметок \"Не нравится\" в видео. - Включить Return YouTube Dislike - Показывает примерное количество лайков видео. - Показать приблизительное количество лайков - Отметки \"Не нравится\" недоступны (достигнут лимит клиентов сервера API). - Отметки \"Не нравится\" недоступны (состояние %d). - Отметки \"Не нравится\" недоступны (время API истекло). - Отметки \"Не нравится\" недоступны (%s). - Отображает всплывающее уведомление, когда API Return YouTube Dislike недоступен. - Уведомлять, когда API недоступен - Скрыто - Убирает параметры отслеживания запросов из адресов при отправке ссылки. - Подчищать ссылки - Об интеграции - sponsor.ajay.app - Данные предоставлены SponsorBlock API. Нажмите здесь, чтобы узнать больше и увидеть опцию загрузки для других платформ. - Изменить адрес сервера API - Адрес сервера API изменён. - Адрес сервера API недействителен. - Адрес сервера API сброшен. - Адрес, используемый для связи с сервером SponsorBlock. Не изменяйте его, если не знаете, что делаете. - Цвет изменён. - Цвет: - Неверный код цвета. Значения сброшены к начальным. - Цвет сброшен. - Изменить поведение сегмента - Включить SponsorBlock - SponsorBlock - это реализованная с помощью участия сообщества система для пропуска раздражающих частей видео в YouTube. - Сбросить цвет - Отвлечённые темы / Шутки - Сегменты, которые увеличивают длительность видео за счёт отвлечённых тем или шуток, но не требуются для понимания основного содержания. Не включает сегменты, объясняющие контекст или предысторию. - Напоминание о взаимодействии (подписка) - Короткое напоминание поставить отметку \"Нравится\", подписаться на канал или соцсети посреди ролика. Если эта вставка длительная или о чём-то конкретном, она должна классифицироваться как самореклама. - Пауза / Интро - Интервал без фактического содержания. Это может быть пауза, статический кадр или повторяющаяся анимация. Не включает переходы, содержащие информацию. - Музыка: Сегмент без музыки - Только для использования в музыкальных роликах. Разделы музыкальных видео без музыки, которые ещё не охвачены другой категорией. - Конечная заставка / Титры - Титры или время появления конечных заставок YouTube. Не для подведения итогов сказанного в видео. - Предпросмотр / Краткое содержание / Завязка - Краткое содержание предыдущих эпизодов или предварительный просмотр того, что будет в данном видео. - Безвозмездная реклама / Самореклама - Похоже на спонсорскую рекламу, за исключением безвозмездной рекламы или саморекламы. Включает разделы о товарах, пожертвованиях или информации о том, с кем они сотрудничали. - Спонсорская реклама - Платные промоакции, платные рефералы и прямая реклама. Не для саморекламы или бесплатных рекомендаций о делах / создателях / веб-сайтах / продуктах, которые им нравятся. - Автоматически пропускать - Отключить - Пропущен наполнитель. - Пропущено назойливое напоминание. - Пропущено интро. - Пропущена пауза. - Пропущена пауза. - Пропущено несколько сегментов. - Пропущен сегмент без музыки. - Пропущена концовка. - Пропущен предпросмотр. - Пропущено краткое повторение. - Пропущен предпросмотр. - Пропущена самореклама. - Пропущена спонсорская реклама. - SponsorBlock временно недоступен. - SponsorBlock временно недоступен (состояние %d). - SponsorBlock временно недоступен (время API истекло). - Показывать тост, если API недоступен - Отображает всплывающее уведомление, когда SponsorBlock недоступен. - Показать тост когда пропущен сегмент автоматически - Отображает всплывающее уведомление при автоматическом пропуске сегмента. - Настройки скопированы в буфер. - "Подменяет версию клиента на старую. - -• Это изменит внешний вид приложения, но могут возникнуть неизвестные проблемы. -• Если отключить данную опцию после её активации, старый интерфейс может оставаться до тех пор, пока данные приложения не будут очищены." - 4.27.53 - Отключить режим радиостанции в канадских регионах - 6.11.52 - Отключить динамические текста - 7.16.53 - Восстановить старую панель действий - Выберите целевую версию приложения для подмены. - Целевая версия приложения при подмене - Подмена версии приложения - diff --git a/src/main/resources/music/translations/tr-rTR/missing_strings.xml b/src/main/resources/music/translations/tr-rTR/missing_strings.xml deleted file mode 100644 index 2c2593d44..000000000 --- a/src/main/resources/music/translations/tr-rTR/missing_strings.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - Don\'t show again - Change from in-app share sheet to system share sheet. - Change share sheet - To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Disables Cairo splash animation when the app starts up. - Disable Cairo splash animation - Reset to default values. - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - Hides the promotion alert banner. - Hide promotion alert banner - Hide About menu - Hide Data saving menu - Hide Downloads & storage menu - Hide General menu - Hide Notifications menu - Hide Get Music premium menu - Hide Family Center menu - Hide Playback menu - Hide Privacy & data menu - Hide Recommendations menu - Return YouTube Username - Settings menu - Show a toast when changing the default playback speed. - Show a toast - Show a toast when changing the default video quality. - Show a toast - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Username - Replaces handles with usernames in comments. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - Shows the estimated like count of videos. - Show estimated likes - Hidden - 7.16.53 - Restore old action bar - diff --git a/src/main/resources/music/translations/tr-rTR/strings.xml b/src/main/resources/music/translations/tr-rTR/strings.xml deleted file mode 100644 index b0b3f2c20..000000000 --- a/src/main/resources/music/translations/tr-rTR/strings.xml +++ /dev/null @@ -1,358 +0,0 @@ - - - Devam Et - "GmsCore'un arka planda çalışma izni yoktur. - -'Uygulamamı öldürmeyin!' cihazınız için kılavuzu kullanın ve talimatları GmsCore kurulumunuza uygulayın. - -Uygulamanın çalışması için bu gereklidir." - "Sorunları önlemek için GmsCore pil optimizasyonları devre dışı bırakılmalıdır. - -Devam düğmesine dokunun ve pil optimizasyonlarını devre dışı bırakın." - Web sitesini aç - Eylem gerekiyor - Bildirimleri alabilmek için bulut mesajlaşmayı etkinleştirin. - GmsCore\'yi aç - GmsCore yüklü değil. Yükleyin. - Bazı bölgelerde engellenen alan adını değiştirerek oynatma listesi küçük resimlerinin, kanal avatarlarının vb. alınabilmesini sağlar. - Resimlerin bölge kısıtlamalarını atla - Listeler - Keşfet - Ana Sayfa - Kitaplık - Abonelikler - Uygulamanın başlangıç sayfasını değiştirin. - Başlangıç ​​sayfasını değiştir - Yeni satırlarla ayrılmış olarak hangi bileşenlerin filtreleneceğini yapılandırın. - Özel filtreyi düzenle - Düzen bileşenlerini gizlemek için özel filtreyi etkinleştirir. - Özel filtreyi etkinleştir - Geçersiz özel filtre: %s. - Geçersiz özel oynatma hızları. Hızlar varsayılana sıfırlandı. - Geçersiz özel oynatma hızları. Varsayılanlar seçildi. - Mevcut oynatma hızlarını değiştirin. - Özel oynatma hızlarını düzenle - Altyazıların kendiliğinden açılmasını devre dışı bırakır. - Altyazıların kendiliğinden açılmasını kapat - Beğenmedim düğmesine tıklandığında sonraki parçaya yönlendirmeyi devre dışı bırakır. - Beğenmeme yönlendirmesini devre dışı bırak - Mini oynatıcıdaki parçaları değiştirmek için kaydırmayı devre dışı bırakın. - Mini oynatıcı hareketini devre dışı bırak - Oynatıcıdaki parçaları değiştirmek için kaydırmayı devre dışı bırakın. - Oynatıcı hareketini devre dışı bırak - Gezinme çubuğunun rengini siyah yapar. - Siyah gezinme çubuğunu etkinleştir - Oynatıcının arka plan rengini siyaha değiştirir. - Siyah oynatıcı arka planını etkinleştir - Küçültülmüş oynatıcının rengi ile tam ekran oynatıcının rengini eşler. - Oynatıcı renk eşlemesini etkinleştir - "Telefonda kompakt iletişim kutusunu etkinleştirin. - -Bilinen sorunlar: -• Kitaplık sekmesindeki albüm resmi, ızgara halinde düzenlendiğinde küçülür. -• Uyku zamanlayıcısı düzeni olağandışı görünebilir." - Kompakt diyaloğu etkinleştir - Hata ayıklama günlüklerini arabellek dahiline yazdırır. - Hata ayıklama günlüklerine arabelleği kaydet - Hata ayıklama günlüğünü yazdırır. - Hata ayıklama günlüğünü etkinleştir - Başka bir kayıt oynatılıyor ise oynatıcıyı tamamen küçült. - Zorla küçültülmüş pencereyi aktifleştir - Telefonlarda ekran döndürme ile manzara moduna geçiş sağlar. - Yatay Modu Etkinleştir - Mini oynatıcıda sonraki düğmesini etkinleştirir. - Mini oynatıcıyıdaki sonraki düğmesini etkinleştir - Mini oynatıcıda önceki düğmesini etkinleştirir. - Mini oynatıcıdaki önceki düğmesini etkinleştir - "Mp4a ses codec bileşeni yerine opus ses codec bileşenini etkinleştirir. - -Bilgi: -• En yeni Android istemcileri varsayılan olarak opus ses codec bileşenini kullanır. -• Bu yalnızca çok eski istemcilerle sahtecilik yapan kullanıcılar için geçerlidir." - Opus kodeğini etkinleştir - Mini oynatıcıyı kapatmak için aşağı kaydırmayı etkinleştirir. - Mini oynatıcıyı kapatmak için kaydırmayı etkinleştirin - "Oynatma hızı açılır menüsüne 'Sessizliği kırp' geçişini etkinleştirir. - -Bilgi: -• Bu özellik podcast'ler içindir. -• Bu özellik henüz geliştirme aşamasında olduğundan kararsız olabilir." - Kırpma sessizliğini etkinleştir - Zen modu podcast\'lere de uygulanır. - Podcastlerde zen modunu aç/kapa - Video oynatıcıya açık gri bir ton ekleyerek göz yorgunluğunu azaltır. - Zen modunu aç/kapa - Düzeni normal şekilde yüklemek için yeniden başlatın - Yenile ve yeniden başlat - Ayarları bir dosyaya dışa aktar - Ayarlar dışa aktarılamadı. - Ayarlar başarıyla dışa aktarıldı. - İçe aktar - Ayarları dosyadan içe aktar - Kopyala - Ayarları yazı olarak içe / dışa aktar - Ayarları içe veya dışa aktarın. - Ayarları içe / dışa aktar - İçe aktarma başarısız oldu: %s. - Ayarlar varsayılana sıfırlandı. - %d ayar içe aktarıldı. - Sıfırla - ReVanced Extended - "İndir düğmesi harici indiricinizi açar. - -• Yalnızca oynatıcıdaki indirme işlemi düğmesini geçersiz kılar. -• Açılır menü veya kitaplıktaki indirme düğmesini geçersiz kılmaz." - İndirme eylem düğmesini kullan - Harici indirici - "%1$s yüklü değil. -Lütfen web sitesinden %2$s dosyasını indirin." - Uyarı - %s kurulmamış. Lütfen önce indiriniz. - Yüklü olan harici indirme uygulamanızın paket adı, örneğin NewPipe veya YTDLnis gibi. - Harici indirici paket adı - Hesap menüsündeki boş bileşenleri gizler. - Boş bileşenleri gizle - Filtrelenecek hesap menüsü adlarının yeni satırlarla ayrılmış listesi. - Hesap menüsü filtresini düzenle - Özel filtreyi kullanarak hesap menüsü öğelerini gizler. - Hesap menüsünü gizle - Kaydet butonunu gizler. - Kaydet butonunu gizle - \"Yorumlar\" butonunu gizler. - \"Yorumlar\" butonunu gizle - İndir butonunu gizler. - İndir butonunu gizle - Aksiyon butonlarındaki etiketleri gizle. - Aksiyon butonu etiketlerini gizle - Beğenme ve beğenmeme düğmelerini gizler. Eski oynatıcı arayüzünde çalışmayabilir. - \"Beğen\" ve \"Beğenme\" butonlarını gizle - Radyo düğmesini gizler. - Radyo düğmesini gizle - Paylaş butonunu gizler. - Paylaş butonunu gizle - Oynatıcıdaki ses video geçiş anahtarını gizler. - Ses video geçiş anahtarını gizle - Düğme rafını ana sayfadan ve keşfet sekmesinden gizler. - Tuş rafını gizle - Döner rafı ana sayfa ve keşfet sekmesinden gizler. - Atlıkarınca rafını gizle - Yayınlama düğmesini gizler. - \"Yayınla\" butonunu gizle - Kategori çubuğunu gizler. - Kategor barını Gizle - Yorumları bölümünün üst kısmında kanal kurallarını gizler. - Kanal yönergelerini gizle - Yorum yazarken zaman damgası ve emoji düğmelerini gizle. - Zaman damgası ve emoji düğmelerini gizle - Çift tıklayarak sürükleme etkinken kara arayüzü gizler. - Çift tıklama arayüz filtresini gizle - Kitaplıktaki Yüzen Butonu Gizle. - Yüzen Butonu Gizle - 3-sütunlu bileşenleri gizle - Kuyruğa ekle menüsünü gizle - \"Altyazılar\" menüsü - Oynatma listesini sil menüsünü gizle - Kuyruğu kapat menüsünü gizle - İndirme menüsünü gizle - Oynatma listesini düzenle menüsünü gizle - Albüm menüsüne gitme menüsünü gizle - Artist menüsüne gitme menüsünü gizle - Bölüm menüsüne gitmeyi gizle - Podcast menüsüne gitmeyi gizle - Yardım & geri bildirim menüsünü gizle - Beğen ve beğenme butonunu gizle - Sonraki menüyü oynat\'ı gizle - Kalite menüsünü gizle - Kitaplıktan kaldır menüsünü gizle - \"Oynatma listesinden kaldır\" menüsünü gizle - Rapor menüsünü gizle - Bölümü daha sonra için kaydet menüsünü gizle - Kitaplığa kaydet menüsünü gizle - Oynatma listesine kaydet menüsünü gizle - Paylaş menüsünü gizle - Karışık çal menüsünü gizle - Uyku zamanlayıcısı menüsünü gizle - Radyoyu başlat menüsünü gizle - \"Meraklısı için istatikler\" menüsü - Abone ol / Abonelikten çık menüsünü gizle - Şarkı kredileri menüsünü görüntüle - Tam ekran reklamlarını gizler. - Tam ekran reklamlarını gizle - "Etkinleştirilirse Kapat düğmesi aracılığıyla tam ekran reklamlar kapatılır. Devre dışı bırakılırsa tam ekran reklamlar engellenir. (yan etkiler olabilir)" - "Etkinleştirilirse Kapat düğmesi aracılığıyla tam ekran reklamlar kapatılır. Devre dışı bırakılırsa tam ekran reklamlar engellenir. (yan etkiler olabilir)" - "Etkinleştirilirse Kapat düğmesi aracılığıyla tam ekran reklamlar kapatılır. Devre dışı bırakılırsa tam ekran reklamlar engellenir. (yan etkiler olabilir)" - Tam ekran reklamlarını kapat - Tam ekran oynatıcısındaki paylaş butonunu gizler. - Tam ekran\'daki paylaş butonunu gizle - Genel reklamları gizler. - Genel reklamları gizle - Hesap menüsündeki etiketi gizler. - Etiketi gizle - Araç çubuğundaki geçmiş düğmesini gizler. - Geçmiş düğmesini gizle - Bir parça çalınmadan önce reklamları gizler. - Müzik reklamlarını gizle - Gezinme çubuğunu gizler. - Gezinme çubuğunu gizle - Keşfet düğmesini gizler. - Keşfet düğmesini gizle - Ev düğmesini gizler. - Ana sayfa düğmesini gizle - Gezinme çubuğunun altındaki etiketleri gizleyin. - Navigasyon paneli altyazılarını gizle - Kitaplık düğmesini gizler. - Kitaplık düğmesini gizle - Örnek düğmesini gizler. - Örnekler düğmesini gizle - Gūncelle düğmesini gizler. - Gūncelle düğmesini gizle - Araç çubuğundaki bildirim düğmesini gizler. - Bildirim butonunu gizle - Ücretli tanıtım yazısını gizler. - Ücretli tanıtım yazısını gizle - Oynatma listesi kartı rafını ana sayfadan gizler. - Çalma listesi kartı rafını gizle - Premium promosyonu açılır penceresini gizler. - Premium promosyonu açılır penceresini gizler - Premium yenileme başlığını gizler. - Premium yenileme başlığını gizle - Feed\'deki örnek rafını gizler. - Örnek rafını gizle - "Ayarlar menüsünün öğelerini gizleyin. -Bu, yalnızca YT Music ayarlar menüsünü değil aynı zamanda ReVanced Extended ayarlar menüsünü de gizler." - Ayarlar menüsünü gizle - Arama çubuğundaki ses arama düğmesini gizler. - Sesli arama düğmesini gizle - Güncellemek için tıkla butonunu gizler. - Güncellemek için tıkla butonunu gizle - Hizmet Şartları kapsayıcısını gizler. - Terimler kapsayıcısını gizle - Arama çubuğundaki sesli arama düğmesini gizler. - Sesli arama düğmesini gizle - Hesap - Görev Çubuğu - Reklamlar - Açılır menü - Genel - Diğer ayarlar - Gezinti çubuğu - Oynatıcı - Return YouTube Dislike - SponsorBlock - Video - Oynatma hızını değiştirdiğinizde en son seçili oynatma hızı değerini hatırlar. - Oynatma hızı değişimlerini hatırla - Varsayılan hız %s olarak değiştiriliyor. - Tekrarın durumunu hatırlar. - Tekrarın durumunu hatırlar - Karıştır durumunu hatırlar. - Karıştır durumunu hatırlar - Seçili video kalitesini hatırlar. - Video kalitesi değişimlerini hatırla - Mobil ağda varsayılan video kalitesi %s olarak değiştiriliyor. - Kalite ayarlanamadı. - Wi-Fi\'da varsayılan video kalitesi %s olarak değiştiriliyor. - "Görüntüleyicinin takdirine ilişkin iletişim kutusunu kaldırır. Bu yaş sınırlamasını atlamaz. Sadece otomatik olarak kabul ediyor." - İzleyicinin takdirine bağlı iletişim kutusunu kaldır - YouTube\'da izlerken geçerli saatten itibaren izlemeye devam ettirir. - İzlemeye devam et - \'Sırayı kapat\' seçeneğini \'YouTube\'da İzle\' ile değiştirir. - Kuyruğu görmezden gelmeyi değiştir - YouTube\'da izle - Geçersiz video url\'si. - Yorumlardaki rapor menüsü değiştirilmeyecektir. - Yalnızca oynatıcı açılır menüsü için geçerlidir - \'Rapor\'u \'Oynatma hızı\' ile değiştirir. - Rapor menüsünü değiştir - Yorum açılır pencerelerini eski stile döndürür. - Eski yorum açılır panellerini geri getir - Oynatıcı arkaplanını eski stile döndürür. - Eski oynatıcı arka planını geri getir - "Oynatıcı düzenini eski stile döndürün. -Eski oynatıcı düzeninde bazı ayarlar düzgün çalışmayabilir." - Eski oynatıcı düzenini geri getir - Kütüphane rafını eski stile döndürün. -(Deneysel) - Eski stil kitaplık rafını geri getir - Hakkında - Veriler True RYD Worker API tarafından sağlanır. Daha fazlasını öğrenmek için buraya dokunun. - ReturnYouTubeDislike.com - Beğen butonunun ayırıcısını gizler. - Kompakt beğenme düğmesi - Beğenmeme sayısı yerine beğenmeme yüzdesi gösterilir. - Yüzde olarak beğenmemeler - Videoların beğenmeme sayısını gösterir. - Return YouTube Dislike\'ı etkinleştir - Beğenmeme sayısı mevcut değil (istemci API sınırına ulaşıldı). - Beğenmemeler mevcut değil (durum %d). - Beğenmemeler geçici olarak kullanılamıyor (API zaman aşımına uğradı). - Beğenmemeler mevcut değil (%s). - Return YouTube Dislike API mevcut değilse uyarı gösterir. - API mevcut değilse bir uyarı göster - Bağlantıları paylaşırken, tracking query parametrelerini URL\'lerden kaldırır. - Paylaşılan bağlantıları sterilize edin - Hakkında - sponsor.ajay.app - Veriler, SponsorBlock API tarafından sağlanmaktadır. Daha fazla bilgi edinmek ve diğer platformlar için indirmeleri görmek için buraya dokunun. - API URL\'sini değiştir - API URL\'si değiştirildi. - API bağlantısı geçersiz. - API URL\'si sıfırlandı. - SponsorBlock\'un sunucuya çağrı yapmak için kullandığı adres. Ne yaptığınızı bilmiyorsanız bunu değiştirmeyin. - Renk değiştirildi. - Renk: - Renk kodu geçersiz. Renk varsayılan\'a sıfırlandı. - Renk sıfırlandı. - Segment davranışını değiştir - SponsorBlock\'u etkinleştir - SponsorBlock, YouTube videolarının sinir bozucu kısımlarını atlamak için kitle kaynaklı bir sistemdir. - Rengi sıfırla - Dolgu Tanjantı / Şakalar - Sadece videoyu doldurmak ya da mizah için eklenmiş, videonun ana içeriğini anlamak için gerekli olmayan alakasız sahneler. Bu, içerik veya arka plan detayları hakkında bilgi veren kısımları içermemelidir. - Etkileşim Hatırlatıcısı (Abone Ol) - İçeriğin ortasında onları beğenmeniz, abone olmanız veya takip etmeniz için kısa bir hatırlatma. Uzunsa veya belirli bir şeyle ilgiliyse, bunun yerine kendini tanıtma altında olmalıdır. - Ara / Giriş Animasyonu - Gerçek içeriği olmayan bir aralık. Bir duraklama, statik çerçeve veya yinelenen animasyon olabilir. Bilgi içeren geçişleri içermez. - Müzik: Müzik Dışı Bölüm - Yalnızca müzik videolarında kullanım içindir. Henüz başka bir kategoride yer almayan müzik videolarının müziksiz bölümleri. - Bitiş Kartları / Hakkında - Videoda emeği geçenlerin veya video sonunda çıkan kartların gösterildiği kısımlar. Bilgilendirici sona sahip videolar için değil. - Önizleme / Özet / Hook - Videoda veya bir dizinin diğer videolarında neler olduğunu veya neler olduğunu gösteren, tüm bilgilerin başka bir yerde tekrarlandığı klip koleksiyonu. - Karşılıksız/Kişisel Promosyon - Ücretsiz veya kendi kendine tanıtım haricinde Sponsora benzer. Ürünler, bağışlar veya kiminle işbirliği yaptıklarına ilişkin bilgilerle ilgili bölümler içerir. - Sponsor - Ücretli tanıtım, ücretli yönlendirmeler ve doğrudan reklamlar. Kendi tanıtımını yapmak veya beğendikleri olaylara / içerik üreticilerine / web sitelerine / ürünlere ücretsiz olarak atıfta bulunanlar için değil. - Otomatik olarak atla - Devre dışı bırak - Dolgu atlandı. - Rahatsız edici hatırlatıcı atlandı. - Giriş ekranı atlandı. - Ara atlandı. - Ara atlandı. - Birden çok segment atlandı. - Sessiz kısım atlandı. - Kapanış ekranı atlandı. - Ön izleme atlandı. - Seans atlandı. - Ön izleme atlandı. - Kişisel promosyon atlandı. - Sponsor atlandı. - SponsorBlock geçici olarak kullanılamıyor. - SponsorBlock geçici olarak kullanılamıyor (durum %d). - SponsorBlock geçici olarak kullanılamıyor (API zaman aşımına uğradı). - API mevcut olmadığında bir uyarı göster - SponsorBlock API\'nin mevcut olmaması durumunda uyarı gösterilir. - Otomatik olarak atlarken bir uyarı göster - Bölüm otomatik olarak atandığında uyarı gösterilir. - Ayarlar panoya kopyalandı - "İstemciyi sürümünün eski sürümle sahteleştir - -• Bu, uygulamanın görünümünü değiştirir ancak bilinmeyen yan etkiler ortaya çıkabilir. -• Daha sonra kapatılırsa, uygulama verileri temizlenene kadar eski kullanıcı arayüzü kalabilir." - 4.27.53 - Kanada bölgelerinde radyo modunu devre dışı bırakın - 6.11.52 - Gerçek zamanlı şarkı sözlerini devre dışı bırak - Sahte uygulama sürümü hedefini seçin. - Uygulama hedef sürümünü kandırma hedefi - Uygulama Versiyonunu taklit et - diff --git a/src/main/resources/music/translations/uk-rUA/missing_strings.xml b/src/main/resources/music/translations/uk-rUA/missing_strings.xml deleted file mode 100644 index acebd7abf..000000000 --- a/src/main/resources/music/translations/uk-rUA/missing_strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Don\'t show again - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - diff --git a/src/main/resources/music/translations/uk-rUA/strings.xml b/src/main/resources/music/translations/uk-rUA/strings.xml deleted file mode 100644 index d0101bc4b..000000000 --- a/src/main/resources/music/translations/uk-rUA/strings.xml +++ /dev/null @@ -1,407 +0,0 @@ - - - Продовжити - "GmsCore не дозволено працювати у фоні. - -Дотримуйтесь посібника \"Don't kill my app\" для вашого пристрою і застосуйте інструкції для встановлення GmsCore. - -Це необхідно для того, щоб програма працювала." - "Оптимізацію акумулятора GmsCore слід вимкнути, щоб запобігти виникненню проблем. - -Натисніть кнопку продовжити й вимкніть оптимізацію акумулятора." - Відкрити сайт - Потрібна дія - Увімкніть \"Хмарні повідомлення\", щоб отримувати сповіщення. - Відкрити GmsCore - GmsCore не встановлено. Встановіть. - Замінює домен для зображень, заблокований у деяких регіонах, що дозволить отримувати мініатюри списків відтворення, аватари каналів тощо. - Змінити домен зображень - Змінює тип вікна діалогу поширення з вбудованого на системний. - Змінити діалог поширення - Хіт-паради - Навігація - Головна - Бібліотека - Підписка - Виберіть сторінку з якої буде стартувати додаток. - Змінити початкову сторінку - Список рядків конструктора шляхів компонентів для фільтрування, розділених новими рядками. - Редагувати користувацький фільтр - Вмикає користувацькі фільтри для приховування компонентів інтерфейсу. - Увімкнути користувацький фільтр - Недопустимий користувацький фільтр: %s. - Користувацькі швидкості мають бути меншими за %sx. - Неправильні користувацькі швидкості відтворення. - Налаштувати доступні швидкості відтворення. - Редагувати користувацькі швидкості відтворення - Щоб відкривати посилання на YouTube Music у RVX Music, увімкніть \"Відкривати підтримувані посилання\" та активуйте підтримувані веб-адреси. - Відкрити налаштування за замовчуванням - Вимикає автоматичне ввімкнення субтитрів. - Вимкнути примусові авто субтитри - Вимикає сплеш анімацію Каїр під час запуску застосунку. - Вимкнути сплеш анімацію Каїр - Вимикає перенаправлення на наступний трек при натисканні на кнопку \"Не подобається\". - Вимкнути перенаправлення при \"Не подобається\" - Вимикає жести перемикання треків у мініплеєрі. - Вимкнути жести мініплеєра - Вимикає жести перемикання треків у плеєрі. - Вимкнути жести плеєра - Встановлює чорний колір для панелі навігації. - Увімкнути чорну панель навігації - Змінює адаптивний колір фона плеєра на чорний. - Увімкнути чорний фон плеєра - Колір мініплеєра повторює колір повноекранного плеєра. - Увімкнути колірну відповідність плеєрів - "Вмикає компактне спливаюче вікно на телефонах. - -Відомі проблеми: -• Обкладинка альбому на вкладці \"Бібліотека\" стає меншою, якщо вона впорядкована сіткою. -• Вікно \"Таймер сну\" може з'являтися незвично." - Увімкнути компактний вигляд меню - Включає буфер у журнал налагодження. - Увімкнути ведення журналу буфера налагодження - Виводить протокол налагодження. - Увімкнути протоколи налагодження - Тримає плеєр згорнутим, навіть коли відтворюється інший трек. - Увімкнути мініплеєр на постійній основі - Вмикає ландшафтний режим під час повороту екрана на телефонах. - Увімкнути ландшафтний режим - Додає кнопку наступного треку у мініплеєр. - Додати кнопку наступне у мініплеєр - Додає кнопку попереднього треку у мініплеєр. - Додати кнопку попереднє у мініплеєр - "Вмикає кодек OPUS, якщо відповідь від сервера містить кодек OPUS. - -Інформація: -• Останні YouTube Music клієнти за умовчанням використовують аудіокодек OPUS. -• Це буде корисно лише для користувачів, які користуються дуже старими клієнтами." - Увімкнути кодек OPUS - Вмикає жест вниз для закриття мініплеєру. - Увімкнути жест закриття мініплеєру - "Додає перемикач \"Пропуск тиші\" у спливаючому меню швидкості відео. - -Інформація: -• Ця функція призначена для подкастів. -• Ця функція все ще у розробці, тому може бути нестабільною." - Додати перемикач \"Пропуск тиші\" - Режим \"Дзен\" також застосовується до подкастів. - Увімкнути режим \"Дзен\" у подкастах - Змінює колір фона плеєра на світло-сірий, щоб зменшити навантаження на очі. - Увімкнути режим \"Дзен\" - Скинуто до значень за замовчуванням. - Перезапустіть, щоб нормально завантажився макет - Оновити та перезавантажити? - Експорт налаштувань у файл - Не вдалося експортувати налаштування. - Налаштування було вдало експортовано. - Імпортувати - Імпорт налаштувань із файлу - Копіювати - Імпорт або експорт налаштувань у вигляді тексту - Імпортує або експортує налаштування. - Імпорт / Експорт налаштувань - Не вдалося імпортувати налаштування: %s. - Налаштування скинуто до стандартних. - Налаштування в кількості: %d успішно відновлено - Скинути - ReVanced Extended - "Кнопка \"Завантажити\" відкриває ваш зовнішній завантажувач. - -• Підміняє лише кнопку \"Завантажити\" на панелі дій в плеєрі. -• Не підміняє кнопку \"Завантажити\" у спливаючому меню чи бібліотеці." - Підмінити \"Завантажити\" - Зовнішній завантажувач - "%1$s не встановлено. -Будь ласка, завантажте %2$s з сайту." - Увага - %s не встановлено. Будь ласка, встановіть його. - Ім\'я пакета встановленого зовнішнього завантажувача, наприклад NewPipe або YTDLnis. - Ім\'я пакета зовнішнього завантажувача - Приховує порожній простір у меню облікового запису - Приховати порожні компоненти - Список назв меню облікового запису для фільтрування, розділених новими рядками. - Фільтр меню облікового запису - Приховує елементи меню облікового запису використовуючи користувацький фільтр. - Приховати меню облікового запису - Приховує кнопку \"Зберегти\" (в список відтворення) на панелі дій плеєра. - Приховати \"Зберегти\" - Приховує кнопку \"Коментарі\" на панелі дій плеєра. - Приховати \"Коментарі\" - Приховує кнопку \"Завантажити\" на панелі дій плеєра. - Приховати \"Завантажити\" - Приховує підписи кнопок на панелі дій плеєра. - Приховати підписи панелі дій - Приховує кнопки \"Подобається\" та \"Не подобається\". Це не працює в старому інтерфейсі плеєра. - Приховати \"Подобається\" і \"Не подобається\" - Приховує кнопку \"Радіо\" на панелі дій плеєра. - Приховати \"Радіо\" - Приховує кнопку \"Поділитися\" на панелі дій плеєра. - Приховати \"Поділитися\" - Приховує перемикач \"пісня | відео\" у плеєрі. - Приховати перемикач \"пісня | відео\" - Приховує кнопки \"Новинки\", \"Хіт-паради\", \"Настрій і жанри\" на вкладці \"Навігація\". - Приховати категорії в Навігації - Приховує карусель треків на вкладках \"Головна\" та \"Навігація\". - Приховати карусель треків - Приховує кнопку \"Трансляція\" в плеєрі та мініплеєрі. - Приховати кнопку \"Трансляція\" - Приховує панель категорій. - Приховати панель категорій - Приховує правила каналу у верхній частині секції коментарів. - Приховати правила каналу - Приховує кнопки мітки часу та емодзі під час введення коментарів. - Приховати мітку часу та емодзі - Приховує затемнення, яке з’являється під час подвійного натискання для перемотування. - Приховати фільтр подвійного натискання - Приховує плаваючу кнопку у вкладці \"Бібліотека\". - Приховати плаваючу кнопку - Приховати 3-стовпцевий компонент - Приховати \"Додати в чергу\" - Приховати \"Субтитри\" - Приховати \"Видалити список відтворення\" - Приховати \"Відхилити чергу\" - Приховати \"Завантажити\" - Приховати \"Редагувати список відтворення\" - Приховати \"Перейти до альбому\" - Приховати \"Перейти на сторінку виконавця\" - Приховати \"Перейти до випуску\" - Приховати \"Перейти до подкасту\" - Приховати \"Довідка й відгуки\" - Приховати \"Подобається\" і \"Не подобається\" - Приховати \"Відтворити наступним\" - Приховати \"Якість\" - Приховати \"Вилучити з бібліотеки\" - Приховати \"Вилучити зі списку\" - Приховати \"Поскаржитись\" - Приховати \"Слухати згодом\" - Приховати \"Зберегти в бібліотеці\" - Приховати \"Зберегти в списку відтворення\" - Приховати \"Поділитися\" - Приховати \"Перемішати\" - Приховати \"Таймер сну\" - Приховати \"Увімкнути Радіо\" - Приховати \"Статистика для досвідчених користувачів\" - Приховати \"Підписатися / Скасувати підписку\" - Приховати \"Переглянути авторів пісні\" - Приховує повноекранну рекламу. - Приховати повноекранну рекламу - "Якщо ввімкнено, повноекранна реклама закривається за допомогою кнопки Закрити. -Якщо вимкнено, повноекранна реклама блокується. (можуть бути побічні ефекти)" - "Якщо ввімкнено, повноекранна реклама закривається за допомогою кнопки Закрити. -Якщо вимкнено, повноекранна реклама блокується. (можуть бути побічні ефекти)" - "Якщо ввімкнено, повноекранна реклама закривається за допомогою кнопки Закрити. -Якщо вимкнено, повноекранна реклама блокується. (можуть бути побічні ефекти)" - Закривати повноекранну рекламу - Приховує кнопку \"Поділитися\" в повноекранному плеєрі. - Приховати \"Поділитися\" повноекранного режиму - Приховує загальну рекламу. - Приховати загальну рекламу - Приховує електронну пошту / @нік у меню облікових записів. - Приховати електронну пошту / @нік - Приховує кнопку історії на панелі інструментів вкладки \"Бібліотека\". - Приховати кнопку історії - Приховує рекламу перед відтворенням медіа. - Приховати медіарекламу - Приховує панель навігації. - Приховати панель навігації - Приховує кнопку \"Навігація\" на панелі навігації. - Приховати \"Навігація\" - Приховує кнопку \"Головна\" на панелі навігації. - Приховати \"Головна\" - Приховує підписи кнопок на панелі навігації. - Приховати підписи кнопок навігації - Приховує кнопку \"Бібліотека\" на панелі навігації. - Приховати \"Бібліотека\" - Приховує кнопку \"Семпли\" на панелі навігації. - Приховати \"Семпли\" - Приховує кнопку \"Підписка\" на панелі навігації. - Приховати \"Підписка\" - Приховує кнопку сповіщень на панелі інструментів. - Приховати кнопку сповіщень - Приховує мітку \"Містить пряму рекламу\". - Приховати \"Містить пряму рекламу\" - Приховує полицю карток списку відтворення в стрічці. - Приховати полицю карток списку відтворення - Приховує спливаючі вікна реклами підписки Music Premium. - Приховати спливаючу рекламу Premium - Приховує банер поновлення підписки Music Premium. - Приховати банер поновлення Premium - Приховує банер рекламних сповіщень. - Приховати рекламні сповіщення - Приховує полицю \"Семпли для вас\" у стрічці. - Приховати полицю \"Семпли\" - Приховати \"Про YouTube Music\" - Приховати \"Заощадження трафіку\" - Приховати \"Завантаження й зберігання\" - Приховати \"Загальні\" - Приховати \"Сповіщення\" - Приховати \"Підписатися на Music Premium\" - Приховати \"Сімейний Центр\" - Приховати \"Відтворення\" - Приховати \"Дані й конфіденційність\" - Приховати \"Рекомендації\" - "Приховує елементи меню налаштувань. -Це приховує не лише меню налаштувань YT Music, а й меню налаштувань ReVanced Extended." - Приховати меню налаштувань - Приховує кнопку пошуку музики у панелі пошуку. - Приховати кнопку пошуку музики - Приховує кнопку оновлення. - Приховати кнопку оновлення - Приховує контейнер \"Конфіденційність • Умови використання\". - Приховати \"Конфіденційність • Умови використання\" - Приховує кнопку голосового пошуку у панелі пошуку. - Приховати кнопку голосового пошуку - Обліковий запис - Панель дій - Реклама - Спливаюче меню - Загальні - Різне - Панель навігації - Плеєр - Повернути ім\'я користувача YouTube - Return YouTube Dislike - SponsorBlock - Меню налаштувань - Відео - Запам\'ятовує останню вибрану швидкість відтворення. - Запам\'ятовувати зміни швидкості відтворення - Показує тост під час зміни стандартної швидкості відтворення. - Показувати тост - Зміна типової швидкості на %s. - Запам\'ятовує стан кнопки \"Повтор відтворення\". - Запам\'ятовувати стан повтору - Запам\'ятовує стан кнопки \"Перемішати\". - Запам\'ятовувати стан перемішування - Запам\'ятовує останню вибрану якість відео. - Запам\'ятовувати зміни якості відео - Показує тост під час зміни стандартної якості відео. - Показувати тост - Зміна типової якості відео в мобільній мережі на %s. - Не вдалося встановити обрану якість. - Зміна типової якості відео для Wi-Fi мережі на %s. - "Вилучає діалог про небажаний контент. -Це не обходить вікові обмеження. Просто приймає їх автоматично." - Вилучити діалог про небажаний контент - Продовжує відео з поточного моменту під час переходу на YouTube. - Продовжити перегляд - Замінює пункт \"Відхилити чергу\" на пункт \"Дивитись на YouTube\". - Заміна \"Відхилити чергу\" - Дивитись на YouTube - Недійсна url-адреса відео. - Залишає пункт меню \"Поскаржитися\" в коментарях недоторканим. - Залишити \"Поскаржитися\" - Замінює пункт \"Поскаржитися\" на пункт \"Швидкість відео\". - Заміна \"Поскаржитися\" - Повертає старий стиль спливаючих панелей коментарів. - Відновити старі спливаючі панелі коментарів - Повертає фон плеєра до старого стилю. - Відновити старий фон плеєра - "Повертає інтерфейс плеєра до старого стилю. -Деякі функції можуть працювати не належним чином у старому інтерфейсі плеєра." - Відновити старий інтерфейс плеєра - Повертає старий стиль вкладки \"Бібліотека\". (Експериментальна опція) - Відновити старий стиль вкладки \"Бібліотека\" - \@псевдонім (Ім\'я користувача) - Вибрати формат відображення імені користувача. - Формат відображення - Ім\'я користувача (@псевдонім) - Ім\'я користувача - Замінює псевдоніми на імена користувачів у коментарях. - Увімкнути повернення імені користувача YouTube - "Щоб замінити псевдоніми на імена користувачів, потрібен ключ розробника YouTube Data API v3. - -Щоденна квота для ключів API у безкоштовному тарифі становить 10 000, і 1 квота використовується для заміни псевдоніма на ім’я користувача для 1 коментаря. - -Натисніть, щоб дізнатися, як створити ключ API." - Про ключ YouTube Data API - Ключ розробника для використання API YouTube Data v3. - Ключ YouTube Data API - 1. Перейдіть до <a href=%1$s>Створити New Project</a>.<br>2. Натисніть кнопку <b>CREATE</b>.<br>3. Перейдіть до <a href=%2$s>YouTube Data API v3</a>.<br>4. Натисніть кнопку <b>ENABLE</b>.<br>5. Натисніть кнопку <b>CREATE CREDENTIALS</b>.<br>6. Виберіть <b>Public data</b>.<br>7. Натисніть кнопку <b>NEXT</b>.<br>8. Скопіюйте ключ API.<br><br>※ Ключ API не можна надавати іншим, тому його не включено в Імпорт / Експорт налаштувань. - Створення ключа розробника YouTube Data API v3 - Про інтеграцію - Дані дизлайків надаються за допомогою Return YouTube Dislike API. Натисніть тут, щоб дізнатися більше. - ReturnYouTubeDislike.com - Приховує лінію між кнопкою \"Подобається\" та кількістю лайків. - Компактна кнопка \"Подобається\" - Відобажає відсоток замість кількості дизлайків. - Кількість дизлайків у відсотках - Показує кількість дизлайків у треках. - Увімкнути Return YouTube Dislike - Показує приблизну кількість лайків відео. - Показати приблизну кількість лайків - Дизлайки недоступні (досягнуто ліміт клієнтів сервера API). - Дизлайки недоступні (статус %d). - Дизлайки тимчасово недоступні (закінчився час API). - Дизлайки недоступні (%s). - Показує тост, якщо API ReturnYouTubeDislike не доступний. - Показувати тост, якщо API не доступний - Приховано - Видаляє параметри запиту відстеження з URL-адрес під час обміну посиланнями. - Обробляти поширення посилань - Про інтеграцію - sponsor.ajay.app - Дані надаються SponsorBlock API. Натисніть тут, щоб дізнатися більше та побачити завантаження для інших платформ. - Змінити URL-адресу API - Адресу сервера API змінено. - Недійсна адреса сервера API. - Адресу сервера API скинуто. - Адреса, яку SponsorBlock використовує для звернень до сервера. Не змінюйте це, якщо не знаєте, що робите. - Колір змінено. - Колір: - Недійсний код кольору. - Колір скинуто. - Змінити поведінку сегмента - Увімкнути SponsorBlock - SponsorBlock - це краудсорсингова система для пропускання дратівливих частин відео на YouTube. - Скинути колір - Дотичне наповнення / Жарти - Дотичні сцени, додані лише для наповнення або гумору, які не є необхідними для розуміння основного змісту відео. Не включає сегменти, що надають контекст або фонові деталі. - Нагадування про взаємодію (Підписка) - Коротке нагадування про вподобання, підписку або підписку посеред контенту. Якщо воно довге або про щось конкретне, його слід розмістити в розділі самореклами. - Пауза / Вступна Анімація - Інтервал без фактичного контенту. Може бути паузою, статичним кадром або повторюваною анімацією. Не включає переходи, що містять інформацію. - Музика: Немузична секція - Тільки для використання в музичних відео. Секції музичних відео без музики, які не підпадають під іншу категорію. - Кінцеві картки / Титри - Титри або коли з\'являються кінцеві картки YouTube. Не для підсумків з інформацією. - Прев\'ю / Коментарі / Підсумок - Колекція кліпів, які показують, що відбувається або що сталося у відео чи в інших відео серій, де вся інформація повторюється в іншому місці. - Неоплачувана / Самореклама - Подібно до \"Спонсор\", за винятком неоплачуваної або самореклами. Включає секції про товари, пожертви або інформацію про те, з ким вони співпрацювали. - Спонсор - Рекламні інтеграції, реферальні посилання і пряма реклама. Не для самореклами або рекомендацій різних подій / творців / сайтів / продуктів, які подобаються автору відео. - Пропустити автоматично - Вимкнути - Пропущено наповнювач. - Пропущено дратівливе нагадування. - Пропущено вступ. - Пропущено паузу. - Пропущено паузу. - Пропущено декілька сегментів. - Пропущено секцію без музики. - Пропущено закінчення. - Пропущено попередній перегляд. - Пропущено підсумок. - Пропущено попередній перегляд. - Пропущено саморекламу. - Пропущено спонсорську вставку. - SponsorBlock тимчасово недоступний. - SponsorBlock тимчасово недоступний (статус %d). - SponsorBlock тимчасово недоступний (закінчився час APІ). - Показувати тост, якщо API недоступний - Показує тост, якщо API SponsorBlock не доступний. - Показати тост, коли сегмент пропущено автоматично - Показує тост, коли сегмент автоматично пропущено. - Налаштування скопійовано до буфера обміну. - "Підміна версії клієнта на старішу версію. - -• Це змінить зовнішній вигляд програми, але можуть виникнути невідомі побічні ефекти. -• Якщо пізніше вимкнути, старий інтерфейс може залишитися, доки не буде очищено дані програми." - 4.27.53 - Вимкнення режиму радіо в канадських регіонах - 6.11.52 - Вимкнення динамічних текстів (караоке) - 7.16.53 - Відновлення старої панелі дій - Виберіть зі списку цільову версію підробки програми. - Підробити цільову версію програми - Підробити версію програми - diff --git a/src/main/resources/music/translations/vi-rVN/missing_strings.xml b/src/main/resources/music/translations/vi-rVN/missing_strings.xml deleted file mode 100644 index acebd7abf..000000000 --- a/src/main/resources/music/translations/vi-rVN/missing_strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - Don\'t show again - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - diff --git a/src/main/resources/music/translations/vi-rVN/strings.xml b/src/main/resources/music/translations/vi-rVN/strings.xml deleted file mode 100644 index 38e81ae4c..000000000 --- a/src/main/resources/music/translations/vi-rVN/strings.xml +++ /dev/null @@ -1,407 +0,0 @@ - - - Tiếp tục - "Hiện GmsCore không có quyền chạy nền. - -Hãy làm theo hướng dẫn của 'Don't kill my app!' và tiến hành cài đặt GmsCore đúng cách. - -Để ứng dụng hoạt động hiệu quả nhất." - "Tắt tối ưu hoá pin cho GmsCore để tránh các vấn đề phát sinh. - -Nhấn vào nút Tiếp tục và tắt tối ưu hóa pin." - Mở trang web - Hành động cần thiết - Mở GmsCore để kích hoạt Cloud Messaging để nhận thông báo đẩy và các cài đặt khác. - Mở GmsCore - GmsCore chưa được cài đặt. Hãy cài đặt nó đi nào. - Thay thế miền bị chặn ở một số khu vực để có thể thu được được hình thu nhỏ video của danh sách phát, ảnh đại diện kênh, v. v. - Bỏ qua hạn chế khu vực cho hình ảnh - Chuyển giao diện chia sẻ trong ứng dụng sang của hệ thống. - Thay đổi giao diện chia sẻ - Bảng xếp hạng - Khám phá - Trang chủ - Thư viện - Kênh đăng ký - Chọn trang sẽ hiển thị khi bạn khởi động ứng dụng. - Thay đổi trang khởi động - Nhập tên các mục mà bạn muốn lọc được phân cách bằng dòng. - Chỉnh sửa bộ lọc - Ẩn các thành phần không mong muốn bằng bộ lọc tuỳ chỉnh. - Bộ lọc tuỳ chỉnh - Bộ lọc tuỳ chỉnh không hợp lệ: %s. - Tốc độ phát tuỳ chỉnh phải nhỏ hơn %sx. - Tốc độ phát tùy chỉnh không hợp lệ. - Thêm giá trị tốc độ phát mà bạn muốn thay đổi hoặc chỉnh sửa các giá trị tốc độ phát hiện có. - Chỉnh sửa tốc độ phát - Để mở liên kết YouTube Music trong RVX Music, hãy kích hoạt \"Mở các đường liên kết được hỗ trợ\" và thêm các đường liên kết được hỗ trợ. - Mở theo mặc định - Tắt tự động hiển thị phụ đề khi phát video nhạc có phụ đề. - Tắt tự động hiển thị phụ đề - Vô hiệu hóa hoạt ảnh kiểu Cairo khi ứng dụng khởi chạy. - Vô hiệu hóa hoạt ảnh kiểu Cairo - Ngăn chuyển đến bài hát tiếp theo khi nhấn nút Không thích. - Tắt chuyển hướng khi nhấn nút Không thích - Tắt vuốt để chuyển bài hát trong trình phát thu nhỏ. - Tắt cử chỉ trình phát thu nhỏ - Tắt vuốt để chuyển bài hát trong trình phát. - Tắt cử chỉ trình phát - Đặt màu thanh điều hướng phía dưới cùng thành màu đen. - Thanh điều hướng màu đen - Thay đổi màu nền trình phát thành màu đen. - Nền trình phát màu đen - Đồng bộ màu của trình phát thu nhỏ với màu của trình phát. - Trình phát thu nhỏ khớp màu - "Bật trình đơn tuỳ chọn dạng hộp thoại. - -Hạn chế: -• Ảnh bìa Album trong thẻ Thư viện (Danh sách phát, Podcast, Bài hát, Đĩa nhạc, Nghệ sĩ,...) cũng thu gọn theo. -• Bố cục Hẹn giờ ngủ có thể xuất hiện bất thường." - Trình đơn tuỳ chọn thu gọn - Bao gồm bộ đệm trong nhật ký gỡ lỗi. - Bật nhật ký bộ đệm gỡ lỗi - Bật ghi nhật ký gỡ lỗi. - Nhật ký gỡ lỗi - Luôn phát nhạc trong trình phát thu nhỏ bất cứ khi nào bạn nghe một bài hát nằm ngoài trình phát hoặc bắt đầu đài phát. - Luôn phát trong trình phát thu nhỏ - Cho phép ứng dụng tự động xoay theo hướng màn hình mà thiết bị được giữ. - Tự động xoay màn hình - Thêm nút bài hát tiếp theo vào trình phát thu nhỏ. - Thêm nút tiếp theo vào trình phát thu nhỏ - Thêm nút bài hát trước đó vào trình phát thu nhỏ. - Thêm nút trước đó vào trình phát thu nhỏ - "Áp dụng codec OPUS nếu phản hồi của trình phát bao gồm nó. - -Cụ thể: -• Các phiên bản YouTube Music mới nhất sử dụng codec OPUS như mặc định. -• Điều này chỉ áp dụng cho người dùng giả mạo với các phiên bản ứng dụng rất cũ." - Codec OPUS - Vuốt xuống để đóng trình phát thu nhỏ. - Vuốt để đóng trình phát thu nhỏ - "Thêm tính năng Cắt bỏ khoảng lặng vào mục tuỳ chọn tốc độ phát. - - Cụ thể: - • Tính năng này dành cho podcast. - • Tính năng này vẫn đang được phát triển nên có thể chưa ổn định." - Cắt bỏ khoảng lặng - Đồng thời bật chế độ tập trung cho podcast. - Chế độ tập trung cho podcast - Thay đổi nền của trình phát thành màu xám nhạt để giúp bạn giảm mỏi mắt và tập trung hơn. - Chế độ tập trung - Đặt lại về giá trị mặc định. - Vui lòng khởi động lại ứng dụng để các tính năng hoạt động bình thường - Làm mới và khởi động lại - Xuất cài đặt dưới dạng tệp - Xuất cài đặt thất bại. - Cài đặt đã được xuất thành công. - Nhập - Nhập cài đặt từ tệp - Sao chép - Nhập/Xuất cài đặt dưới dạng văn bản - Nhập hoặc xuất các tuỳ chọn cài đặt của bạn. - Nhập/Xuất cài đặt - Nhập cài đặt thất bại: %s. - Đã đặt lại cài đặt về mặc định. - Đã nhập %d cài đặt. - Đặt lại - ReVanced Extended - "Nút tải xuống sẽ mở trình tải xuống bên ngoài của bạn. - -• Chỉ ghi đè lên nút Tải xuống trong trình phát. -• Không ghi đè lên nút Tải xuống trong Trình đơn tuỳ chọn hoặc thẻ Thư viện." - Ghi đè nút tải xuống - Trình tải xuống bên ngoài - "Có vẻ như %1$s chưa được cài đặt. - Vui lòng tải xuống %2$s từ trang web." - Chú ý - %s chưa được cài đặt. Hãy cài đặt và thử lại. - Nhập tên gói ứng dụng trình tải xuống đã cài đặt trên thiết bị của bạn, chẳng hạn như NewPipe hoặc YTDLnis. - Tên gói ứng dụng trình tải xuống - Ẩn các mục trống khỏi trình đơn Tài khoản. - Ẩn mục trống - Nhập tên các mục thành phần của trình đơn Tài khoản mà bạn muốn lọc được phân cách bằng dòng. - Bộ lọc trình đơn Tài khoản - Ẩn các thành phần của trình đơn Tài khoản bằng bộ lọc tuỳ chỉnh. - Ẩn trình đơn Tài khoản - Ẩn nút Lưu trong bảng nút thao tác. - Ẩn nút Lưu - Ẩn nút Bình luận trong bảng nút thao tác. - Ẩn nút Bình luận - Ẩn nút Tải xuống trong bảng nút thao tác. - Ẩn nút Tải xuống - Ẩn tên nút trong bảng nút thao tác. - Ẩn tên nút - Ẩn nút Thích và nút Không thích.\n\nLưu ý: Tuỳ chọn này không hoạt động trong bố cục trình phát kiểu cũ. - Ẩn các nút Thích/Không thích - Ẩn nút Đài phát trong bảng nút thao tác. - Ẩn nút Đài phát - Ẩn nút Chia sẻ trong bảng nút thao tác. - Ẩn nút Chia sẻ - Ẩn nút chuyển đổi Bài hát/Video trong trình phát. - Ẩn nút chuyển đổi Bài hát/Video - Ẩn khối danh mục ở cuối thẻ Trang chủ và đầu thẻ Khám phá. - Ẩn khối danh mục - Ẩn các kệ được cá nhân hoá dựa trên sở thích của bạn khỏi thẻ Trang chủ và thẻ Khám phá. - Ẩn các kệ được cá nhân hoá - Ẩn nút Truyền ở đầu trình phát. - Ẩn nút Truyền - Ẩn thanh danh mục. - Ẩn thanh danh mục - Ẩn các nhãn nguyên tắc (Nguyên tắc cộng đồng, Nguyên tắc của kênh,...) trong phần Bình luận. - Ẩn các nhãn nguyên tắc - Ẩn nút dấu thời gian và các biểu tượng cảm xúc khi đang nhập bình luận. - Ẩn nút dấu thời gian và các biểu tượng cảm xúc - Ẩn lớp phủ tối xuất hiện khi nhấn đúp để tua. - Ẩn lớp phủ khi nhấn đúp để tua - Ẩn nút nổi trong thẻ Thư viện. - Ẩn nút nổi - Ẩn 3 ô thao tác nhanh - Ẩn mục Thêm vào danh sách chờ - Ẩn mục Phụ đề - Ẩn mục Xoá danh sách phát - Ẩn mục Loại bỏ danh sách chờ - Ẩn mục Tải xuống - Ẩn mục Chỉnh sửa danh sách phát - Ẩn mục Chuyển đến đĩa nhạc - Ẩn mục Chuyển đến trang của nghệ sĩ - Ẩn mục Chuyển đến tập podcast - Ẩn mục Chuyển đến podcast - Ẩn mục Trợ giúp & phản hồi - Ẩn các nút Thích và Không thích - Ẩn mục Phát video tiếp theo - Ẩn mục Chất lượng - Ẩn mục Xoá khỏi thư viện - Ẩn mục Xóa khỏi danh sách phát - Ẩn mục Báo vi phạm - Ẩn mục Lưu tập này để thưởng thức sau - Ẩn mục Lưu vào thư viện - Ẩn mục Lưu vào danh sách phát - Ẩn mục Chia sẻ - Ẩn mục Phát ngẫu nhiên - Ẩn mục Hẹn giờ ngủ - Ẩn mục Bắt đầu đài phát - Ẩn mục Thống kê chi tiết - Ẩn mục Đăng ký/Huỷ đăng ký - Ẩn mục Xem thông tin ghi công của bài hát - Ẩn quảng cáo toàn màn hình. - Ẩn quảng cáo toàn màn hình - "Nếu tính năng này bật, quảng cáo toàn màn hình sẽ được đóng thông qua nút Đóng. -Nếu tính năng này tắt, quảng cáo toàn màn hình sẽ bị chặn (có thể có tác dụng phụ)." - "Nếu tính năng này bật, quảng cáo toàn màn hình sẽ được đóng thông qua nút Đóng. -Nếu tính năng này tắt, quảng cáo toàn màn hình sẽ bị chặn (có thể có tác dụng phụ)." - "Nếu tính năng này bật, quảng cáo toàn màn hình sẽ được đóng thông qua nút Đóng. -Nếu tính năng này tắt, quảng cáo toàn màn hình sẽ bị chặn (có thể có tác dụng phụ)." - Đóng quảng cáo toàn màn hình - Ẩn nút Chia sẻ trong trình phát toàn màn hình. - Ẩn nút Chia sẻ trong trình phát toàn màn hình - Ẩn quảng cáo xuất hiện trước khi phát. - Ẩn quảng cáo chung - Ẩn tên người dùng khỏi trình đơn Tài khoản. - Ẩn tên người dùng - Ẩn nút Video đã xem khỏi thanh công cụ. - Ẩn nút Video đã xem - Ẩn quảng cáo xuất hiện trước khi phát nhạc. - Ẩn quảng cáo - Ẩn thanh điều hướng. - Ẩn thanh điều hướng - Ẩn thẻ Khám phá khỏi thanh điều hướng. - Ẩn thẻ Khám phá - Ẩn thẻ Trang chủ khỏi thanh điều hướng. - Ẩn nút Trang chủ - Ẩn tên thẻ trên thanh điều hướng. - Ẩn tên thẻ - Ẩn thẻ Thư viện khỏi thanh điều hướng. - Ẩn thẻ Thư viện - Ẩn thẻ Đoạn nhạc khỏi thanh điều hướng. - Ẩn thẻ Đoạn nhạc - Ẩn thẻ Nâng cấp khỏi thanh điều hướng. - Ẩn thẻ Nâng cấp - Ẩn nút Thông báo khỏi thanh công cụ. - Ẩn nút Thông báo - Ẩn nhãn quảng cáo được tài trợ. - Ẩn nhãn quảng cáo được tài trợ - Ẩn kệ thẻ danh sách phát ở thẻ Trang chủ. - Ẩn kệ thẻ danh sách phát - Ẩn quảng cáo mua Music Premium bật lên. - Ẩn quảng cáo bật lên - Ẩn quảng cáo biểu ngữ mua Music Premium. - Ẩn quảng cáo biểu ngữ - Ẩn biểu ngữ thông báo khuyến mãi. - Ẩn biểu ngữ thông báo khuyến mãi - Ẩn kệ Đoạn nhạc ở thẻ Trang chủ. - Ẩn thẻ Đoạn nhạc - Ẩn mục Giới thiệu về Youtube Music - Ẩn mục Chế độ tiết kiệm dữ liệu - Ẩn mục Nhạc tải xuống & bộ nhớ - Ẩn mục Chung - Ẩn mục Thông báo - Ẩn mục Mua Music Premium - Ẩn mục Trung tâm dành cho gia đình - Ẩn mục Tính năng phát - Ẩn mục Quyền riêng tư & dữ liệu - Ẩn mục Đề xuất - "Ẩn các thành phần của mục Cài đặt. -Khi bật không những ẩn mục Cài đặt YT Music, mà còn ẩn mục Cài đặt ReVanced Extended." - Ẩn mục cài đặt - Ẩn nút Tìm kiếm bằng âm thanh kế bên thanh tìm kiếm. - Ẩn nút Tìm kiếm bằng âm thanh - Ẩn nút Chạm để nâng cấp. - Ẩn nút Chạm để nâng cấp - Ẩn các mục Chính sách quyền riêng tư và Điều khoản dịch vụ khỏi trình đơn Tài khoản. - Ẩn mục Bảo mật và Điều khoản - Ẩn nút Tìm kiếm bằng giọng nói kế bên thanh tìm kiếm. - Ẩn nút Tìm kiếm bằng giọng nói - Tài khoản - Thanh thao tác - Quảng cáo - Trình đơn tuỳ chọn - Tổng quan - Cài đặt khác - Thanh điều hướng - Trình phát - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Trình đơn cài đặt - Video - Lưu giá trị tốc độ phát được chọn gần đây nhất. - Lưu thay đổi tốc độ phát - Hiện một thông báo ngắn khi thay đổi tốc độ phát mặc định. - Hiện một thông báo ngắn - Đã lưu tốc độ phát mặc định thành %s. - Ghi nhớ trạng thái phát lặp lại một danh sách phát hoặc phát lặp lại một bài hát. - Lưu trạng thái phát lặp lại - Ghi nhớ trạng thái phát ngẫu nhiên các bài hát. - Lưu trạng thái phát ngẫu nhiên - Lưu chất lượng video nhạc đã chọn gần đây nhất. - Lưu thay đổi chất lượng video - Hiện một thông báo ngắn khi thay đổi chất lượng mặc định của video. - Hiện một thông báo ngắn - Đã lưu chất lượng video mặc định trên mạng di động thành %s. - Đặt chất lượng video thất bại. - Đã lưu chất lượng video mặc định trên mạng Wi-Fi thành %s. - "Xoá hộp thoại cảnh báo nội dung cần cân nhắc trước khi xem. -\nLưu ý: Tuỳ chọn này chỉ tự động chấp nhận hộp thoại cảnh báo, không thể bỏ qua giới hạn về độ tuổi." - Xoá hộp thoại cảnh báo trước khi xem - Tiếp tục phát video từ thời điểm đã dừng lại khi chuyển sang YouTube. - Tiếp tục phát - Thay thế mục Loại bỏ danh sách chờ bằng mục Xem trên YouTube. - Thay thế mục Loại bỏ danh sách chờ - Xem trên YouTube - URL video không hợp lệ. - Giữ nguyên mục Báo vi phạm trong phần bình luận. - Báo vi phạm trong phần bình luận - Thay thế mục Báo vi phạm bằng mục Tốc độ phát. - Thay thế mục Báo vi phạm - Khôi phục bảng bình luận kiểu cũ. - Bảng bình luận kiểu cũ - Khôi phục nền trình phát về kiểu cũ. - Nền trình phát kiểu cũ - "Khôi phục bố cục trình phát về kiểu cũ. -\nLưu ý: Một số tính năng có thể không hoạt động bình thường trong bố cục trình phát kiểu cũ." - Bố cục trình phát kiểu cũ - Khôi phục lại thẻ Thư viện về kiểu cũ. (Thử nghiệm) - Thẻ Thư viện kiểu cũ - \@handle (Tên người dùng) - Chọn định dạng hiển thị tên người dùng. - Định dạng hiển thị - Tên người dùng (@handle) - Tên người dùng - Hiển thị tên người dùng thay vì tên hiển thị trong phần bình luận. - Kích hoạt Return YouTube Username - "Khoá nhà phát triển YouTube Data API v3 là một mã khoá cho phép các nhà phát triển truy cập lấy dữ liệu từ Youtube, và chúng cũng cần thiết để thay thế @tên hiển thị thành tên người dùng. - -Giới hạn truy cập hàng ngày cho các khoá API trên gói miễn phí là 10000 lần, với mỗi lượt truy cập chỉ áp dụng cho 1 bình luận. - -Nhấp vào đây để xem các bước phát hành khóa API." - Giới thiệu về khoá YouTube Data API - Khoá nhà phát triển để sử dụng YouTube Data API v3. - Khoá Youtube Data API - 1. <a href=%1$s>Tạo dự án mới</a>.<br>2. Ấn vào <b>CREATE</b>.<br>3. Đi tới <a href=%2$s>YouTube Data API v3</a>.<br>4. Ấn vào <b>ENABLE</b>.<br>5. Ấn vào <b>CREATE CREDENTIALS</b>.<br>6. Chọn <b>Public data</b>.<br>7. Ấn vào <b>NEXT</b>.<br>8. Sao chép mã API.<br><br>※ Không nên chia sẻ mã API với người khác, vì vậy chúng cũng không có mặt trong mục Nhập/Xuất cài đặt. - Phát hành mã khoá - Giới thiệu - Dữ liệu về số lượt không thích được cung cấp bởi API Return YouTube Dislike. Nhấn vào đây để tìm hiểu thêm. - ReturnYouTubeDislike.com - Ẩn dấu phân cách giữa nút Thích và số lượt thích. - Nút Thích thu gọn - Hiển thị số lượt không thích dưới dạng tỉ lệ phần trăm. - Số lượt không thích theo phần trăm - Hiển thị số lượt không thích của bài hát và video nhạc. - Kích hoạt Return YouTube Dislike - Hiển thị số lượt thích được ước tính của video. - Số lượt thích ước tính - Số lượt không thích không khả dụng (đã đạt đến giới hạn API máy khách). - Số lượt không thích không khả dụng (trạng thái %d). - Số lượt không thích tạm thời không khả dụng (API đã hết thời gian chờ). - Số lượt không thích không khả dụng (%s). - Hiển thị thông báo ngắn nếu API ReturnYouTubeDislike không khả dụng. - Thông báo ngắn nếu API không khả dụng - Ẩn - Loại bỏ các tham số truy vấn theo dõi khỏi URL khi chia sẻ liên kết. - Liên kết sạch khi chia sẻ - Giới thiệu - sponsor.ajay.app - Dữ liệu được cung cấp bởi API SponsorBlock. Nhấn vào đây để tìm hiểu thêm và xem các bản tải xuống cho các nền tảng khác. - Thay đổi địa chỉ URL của API - Đã thay đổi địa chỉ URL của API SponsorBlock. - Địa chỉ URL của API SponsorBlock không hợp lệ. - Đã đặt lại địa chỉ URL của API SponsorBlock. - Địa chỉ URL của API SponsorBlock được dùng để thực hiện các kết nối đến máy chủ. Không thay đổi địa chỉ này trừ khi bạn biết mình đang làm gì. - Đã thay đổi màu phân đoạn. - Màu: - Mã màu không hợp lệ. - Đã đặt lại màu phân đoạn về mặc định. - Cài đặt phân đoạn - Kích hoạt SponsorBlock - SponsorBlock là một tiện tích được đóng góp bởi cộng đồng nhằm bỏ qua các phân đoạn gây khó chịu trong video YouTube. - Đặt lại màu - Cảnh phụ/Nội dung lạc đề - hài hước - Những cảnh chỉ được thêm vào để bổ sung hoặc mang tính chất hài hước, không bắt buộc phải hiểu nội dung chính của video. Không bao gồm các phân đoạn cung cấp chi tiết bối cảnh. - Nhắc nhở tương tác (Đăng ký) - Một lời nhắc ngắn rằng bạn hãy ấn thích, đăng ký hoặc theo dõi họ ở giữa nội dung. Nếu nó dài hoặc về một cái gì đó cụ thể, thay vào đó nó nên được tự quảng cáo. - Đoạn tạm dừng/Giới thiệu - Khoảng thời gian không có nội dung thực tế, có thể là treo video, khung hình tĩnh hoặc hoạt ảnh lặp lại. Phân đoạn này không bao gồm các phần chuyển tiếp chứa thông tin. - Âm nhạc: Phần không phải nhạc - Chỉ dành cho video âm nhạc. Phần trong video âm nhạc không có nhạc, gồm cả những phần không có trong bản nhạc chính thức. - Đoạn kết thúc/Danh đề - Giới thiệu hoặc khi phần video đề xuất ở màn hình kết thúc của YouTube xuất hiện. Phân đoạn này không bao gồm kết thúc bằng lời nói. - Đoạn xem trước/Tóm tắt/Gây chú ý - Phân đoạn này cho thấy những gì sẽ xảy ra/đã xảy ra trong video hiện tại hoặc các video tiếp theo/trước đó trong cùng một loạt video, bao gồm tất cả thông tin được lặp lại ở một thời điểm khác. - Quảng cáo không được trả tiền/Tự quảng cáo - Tương tự như \"Nhà tài trợ\" ngoại trừ việc nhà sáng tạo không được trả tiền quảng cáo hoặc tự họ quảng cáo. Phân đoạn này cũng bao gồm các sản phẩm hàng hoá được rao bán, khoản quyên góp hoặc thông tin về những người họ đã cộng tác. - Nhà tài trợ - Quảng cáo trả phí, giới thiệu trả phí và quảng cáo trực tiếp. Không nhằm mục đích tự quảng cáo hoặc quảng cáo miễn phí cho mục đích/người sáng tạo/trang web/sản phẩm mà họ thích. - Tự động bỏ qua - Vô hiệu hoá - Đã bỏ qua cảnh phụ/nội dung lạc đề - hài hước. - Đã bỏ qua nhắc nhở tương tác. - Đã bỏ qua phần giới thiệu. - Đã bỏ qua đoạn tạm dừng. - Đã bỏ qua đoạn tạm dừng. - Đã bỏ qua nhiều phân đoạn. - Đã bỏ qua phần không phải nhạc. - Đã bỏ qua phần kết thúc. - Đã bỏ qua đoạn xem trước. - Đã bỏ qua đoạn tóm tắt. - Đã bỏ qua đoạn xem trước. - Đã bỏ qua đoạn tự quảng cáo. - Đã bỏ qua nhà tài trợ. - SponsorBlock tạm thời không khả dụng. - SponsorBlock tạm thời không khả dụng (trạng thái %d). - SponsorBlock tạm thời không khả dụng (API đã hết thời gian chờ). - Hiện thông báo ngắn nếu API không khả dụng - Hiển thị thông báo ngắn nếu API SponsorBlock không khả dụng. - Hiện thông báo ngắn khi tự động bỏ qua - Hiện thông báo ngắn mỗi khi tự động bỏ qua phân đoạn. - Đã sao chép cài đặt sang bảng nhớ tạm. - "Giả mạo phiên bản YouTube Music hiện tại thành phiên bản cũ. - -Lưu ý:\n- Tuỳ chọn này sẽ thay đổi giao diện ứng dụng, tuy nhiên có thể xảy ra các sự cố chưa xác định khác. -- Nếu tắt tuỳ chọn này sau đó, giao diện cũ có thể vẫn tồn tại cho đến khi bạn xoá dữ liệu ứng dụng." - 4.27.53 - Tắt chế độ Đài phát ở một số vùng của Canada - 6.11.52 - Tắt lời bài hát theo thời gian thực - 7.16.53 - Khôi phục thanh thao tác kiểu cũ - Chọn phiên bản YouTube Music mà bạn muốn giả mạo. - Phiên bản giả mạo - Giả mạo phiên bản ứng dụng - diff --git a/src/main/resources/music/translations/zh-rCN/missing_strings.xml b/src/main/resources/music/translations/zh-rCN/missing_strings.xml deleted file mode 100644 index f5bbed024..000000000 --- a/src/main/resources/music/translations/zh-rCN/missing_strings.xml +++ /dev/null @@ -1,155 +0,0 @@ - - - Continue - Don\'t show again - "GmsCore does not have permission to run in the background. - -Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. - -This is required for the app to work." - "GmsCore battery optimizations must be disabled to prevent issues. - -Tap on the continue button and disable battery optimizations." - Open website - Action needed - Enable cloud messaging to receive notifications. - Open GmsCore - GmsCore is not installed. Install it. - Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. - Bypass image region restrictions - Change from in-app share sheet to system share sheet. - Change share sheet - Invalid custom playback speeds. - To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Disables Cairo splash animation when the app starts up. - Disable Cairo splash animation - Disable swipe to change tracks in the miniplayer. - Disable miniplayer gesture - Disable swipe to change tracks in the player. - Disable player gesture - Includes the buffer in the debug log. - Enable debug buffer logging - Reset to default values. - Reset - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - Hides dark overlay that appears when double-tapping to seek. - Hides the promotion alert banner. - Hide promotion alert banner - Hide About menu - Hide Data saving menu - Hide Downloads & storage menu - Hide General menu - Hide Notifications menu - Hide Get Music premium menu - Hide Family Center menu - Hide Playback menu - Hide Privacy & data menu - Hide Recommendations menu - "Hide elements of the settings menu. -This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." - Hide settings menu - Miscellaneous - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Settings menu - Video - Remembers the last playback speed selected. - Remember playback speed changes - Show a toast when changing the default playback speed. - Show a toast - Changing default speed to %s. - Remembers the last video quality selected. - Remember video quality changes - Show a toast when changing the default video quality. - Show a toast - Changing default mobile data quality to %s. - Failed to set quality. - Changing default Wi-Fi quality to %s. - Returns the comments popup panels to the old style. - Restore old comments popup panels - Returns the player background to the old style. - Restore old player background - "Returns the player layout to the old style. -Some features may not work properly in the old player layout." - Restore old player layout - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Username - Replaces handles with usernames in comments. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - ReturnYouTubeDislike.com - Enable Return YouTube Dislike - Shows the estimated like count of videos. - Show estimated likes - Hidden - About - sponsor.ajay.app - Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. - Change API URL - API URL changed. - API URL is invalid. - API URL reset. - The address SponsorBlock uses to make calls to the server. Do not change this unless you know what you\'re doing. - Color changed. - Color: - Invalid color code. - Color reset. - Change segment behavior - Enable SponsorBlock - SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. - Reset color - Filler Tangent / Jokes - Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. - Interaction Reminder (Subscribe) - A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. - Intermission / Intro Animation - An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. - Music: Non-Music Section - Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. - Endcards / Credits - Credits or when the YouTube endcards appear. Not for conclusions with information. - Preview / Recap / Hook - Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. - Unpaid / Self Promotion - Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. - Sponsor - Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. - Skip automatically - Disable - Skipped filler. - Skipped annoying reminder. - Skipped intro. - Skipped intermission. - Skipped intermission. - Skipped multiple segments. - Skipped a non-music section. - Skipped outro. - Skipped preview. - Skipped recap. - Skipped preview. - Skipped self promotion. - Skipped sponsor. - SponsorBlock is temporarily unavailable. - SponsorBlock is temporarily unavailable (status %d). - SponsorBlock is temporarily unavailable (API timed out). - Show a toast if API is unavailable - Shows a toast if the SponsorBlock API is unavailable. - Show a toast when skipping automatically - Shows a toast when a segment is automatically skipped. - 7.16.53 - Restore old action bar - diff --git a/src/main/resources/music/translations/zh-rCN/strings.xml b/src/main/resources/music/translations/zh-rCN/strings.xml deleted file mode 100644 index 38a4f2424..000000000 --- a/src/main/resources/music/translations/zh-rCN/strings.xml +++ /dev/null @@ -1,254 +0,0 @@ - - - 图表 - 探索 - 首页 - 媒体库 - 订阅 - 选择打开应用显示的页面 - 更改起始页面 - 按行分割过滤组件名称 - 编辑自定义过滤隐藏 - 启用自定义过滤 - 自定义过滤隐藏 - 自定义过滤器无效: %s - 无效的自定义播放速度,已重置为默认值 - 添加或更改可用的播放速度 - 编辑自定义播放速度 - 禁止视频播放器自动启用的强制字幕 - 禁用自动字幕 - 点击不喜欢按钮时禁用重定向到下一曲目 - 禁用不喜欢重定向 - 将导航栏设为黑色 - 黑色导航栏 - 将播放器背景颜色更改为黑色 - 启用黑色播放器背景 - 使导航栏播放器与全屏播放器颜色一致. - 匹配播放器颜色 - "在手机上启用紧凑对话框。 - -已知问题: -• 媒体库上的专辑封面也变得更小。 -• 睡眠定时器布局可能出现异常。" - 启用紧凑对话框 - 打印 Debug 日志 - Debug 日志 - 保持播放器最小化,即使播放另一首曲目 - 强制最小化播放器 - 允许通过手机屏幕旋转进入横屏模式 - 横屏模式 - 启用迷你播放器的下一首按钮 - 启用迷你播放器的下一首按钮 - 启用迷你播放器的上一首按钮 - 启用迷你播放器的上一首按钮 - "播放音频使用 250/251 opus 编码" - OPUS 编解码器 - 启用向下滑动以关闭迷你播放器 - 启用滑动以关闭迷你播放器 - "将修改静音开关新增至播放速度弹出式选单 - - 信息: - • 此功能适用于播客 - • 此功能仍在开发中,因此可能不稳定" - 新增修改静音开关 - 同时允许播客的 Zen 模式 - 在播客中启用 Zen 模式 - 在视频播放器上添加灰色阴影以减少眼睛疲劳 - 禅定模式 - 重启应用以正常加载界面布局 - 刷新并重启 - 导出配置到文件 - 导出配置失败 - 导出配置成功 - 导入 - 从文件导入配置 - 复制 - 导入 / 导出配置文本 - 导入或导出设置为文本。 - 导入/导出 - 导入失败:%s - 设置已被重置为默认值。 - 已导入 %d 设置。 - ReVanced Extended - "下载按钮开启您的外部下载器 - - • 仅覆写播放器中的下载动作按钮 - • 不会覆写弹出式功能表或媒体库的下载按钮" - 覆盖下载操作按钮 - 外部下载器 - "%1$s 未安装 -请从网站下载 %2$s" - 警告 - %s未安装,请先安装 - 已安装的外部下载器应用的包名,例如 NewPipe 或 Seal - 外部下载器应用包名 - 隐藏在账户菜单中的空组件。 - 隐藏空组件 - 要过滤的帐户菜单名称列表,每行一个名称 - 账户菜单过滤器 - 隐藏账户菜单元素。 - 隐藏账户菜单 - 隐藏添加到播放列表按钮 - 隐藏添加到播放列表按钮 - 隐藏评论按钮 - 隐藏评论按钮 - 隐藏下载按钮 - 隐藏下载按钮 - 隐藏操作按钮中的标签 - 隐藏操作按钮标签 - 隐藏点赞和点踩按钮(在旧的播放器布局中不生效) - 隐藏点赞和点踩按钮 - 隐藏开启电台按钮 - 隐藏电台按钮 - 隐藏分享按钮 - 隐藏分享按钮 - 隐藏播放器中的音频/视频开关 - 隐藏音频/视频开关 - 隐藏主页和探索中的按钮栏 - 隐藏按钮栏 - 隐藏主页和探索中的播放列表 - 播放列表栏 - 隐藏首页顶部和播放器顶部的投屏按钮 - 投屏按钮 - 隐藏主页顶部的音乐分类 - 隐藏分类 - 隐藏评论顶部的频道指南 - 隐藏频道指南 - 输入评论时隐藏时间戳和表情符号按钮 - 隐藏时间戳和表情按钮 - 隐藏双击叠加层过滤器 - 隐藏媒体库中的悬浮按钮 - 隐蔽悬浮按钮 - 隐藏三列组件 - 隐藏添加到队列选项 - 隐藏字幕菜单选项 - 隐藏删除播放列表选项 - 隐藏清除队列选项 - 隐藏下载选项 - 隐藏编辑播放列表 - 隐藏转到专辑 - 隐藏转到艺术家 - 隐藏跳转到选集菜单 - 隐藏跳转到播客菜单 - 隐藏帮助 & 反馈菜单 - 隐藏点赞和点踩按钮 - 隐藏播放下一首 - 隐藏画质菜单 - 隐藏从媒体库中移除 - 隐藏从播放列表移除菜单 - 隐藏举报菜单 - 隐藏保存剧集到稍后再看菜单 - 隐藏保存到媒体库 - 隐藏保存到播放列表 - 隐藏分享菜单 - 隐藏随机播放菜单 - 隐藏睡眠计时器菜单 - 隐藏开启电台 - 隐藏详细统计信息菜单 - 隐藏订阅 / 退订菜单 - 隐藏歌曲详细信息 - 隐藏全屏广告 - 隐藏全屏广告 - "如果启用,全屏广告将通过关闭按钮关闭 -如果禁用,全屏广告将被屏蔽(可能有副作用)" - "如果启用,全屏广告将通过关闭按钮关闭 -如果禁用,全屏广告将被屏蔽(可能有副作用)" - "如果启用,全屏广告将通过关闭按钮关闭 -如果禁用,全屏广告将被屏蔽(可能有副作用)" - 关闭全屏广告 - 隐藏全屏播放器中的分享按钮 - 隐藏全屏分享按钮 - 隐藏一般广告 - 隐藏一般广告 - 隐藏账号菜单中的句柄 - 隐藏控制列 - 隐藏工具栏中的历史按钮 - 隐藏历史按钮 - 隐藏播放曲目前的广告 - 音乐广告 - 隐藏导航栏 - 隐藏导航栏 - 隐藏在导航栏中的探索按钮 - 隐藏探索按钮 - 隐藏首页按钮 - 隐藏首页按钮 - 隐藏导航栏标签 - 隐藏导航栏标签 - 隐藏媒体库按钮 - 隐藏媒体库按钮 - 隐藏样品按钮 - 隐藏样品按钮 - 隐藏更新按钮 - 隐藏更新按钮 - 隐藏工具栏中的通知按钮 - 隐藏通知按钮 - 隐藏付费推广标签 - 隐藏付费推广标签 - 隐藏订阅中的播放列表卡片 - 隐藏播放列表卡片 - 隐藏 Premium 推广弹出窗口 - 隐藏 Premium 推广弹出窗口 - 隐藏 Premium 续订横幅 - 隐藏 Premium 续订横幅 - 隐藏订阅中的样品栏 - 隐藏样品栏 - 隐藏搜索栏中的音频搜索按钮 - 隐藏音频搜索按钮 - 隐藏点击以更新按钮 - 隐藏点击以更新按钮 - 隐藏服务条款栏 - 隐藏服务条款栏 - 隐藏搜索栏中的语音搜索按钮 - 隐藏语音搜索按钮 - 账号 - 快捷操作栏 - 广告 - 弹出菜单 - 常规设置 - 导航栏 - 播放器 - 记住重复播放状态 - 记住重复播放状态 - 记住随机播放状态 - 记住随机播放状态 - "移除查看器的自由裁量对话框。 -这不会绕过年龄限制。它只会自动同意。" - 移除查看器的自由裁量对话框 - 切换到 YouTube 时从当前时间起继续视频 - 继续观看 - 替换清空队列菜单为在 YouTube上观看 - 替换清空队列菜单 - 在 Youtube上观看 - 视频链接无效 - 保持评论部分中的举报菜单不变 - 在评论中保留举报 - 用播放速度菜单替换举报菜单 - 替换举报菜单 - 将媒体库栏恢复为旧版 (实验性) - 还原旧版媒体库栏 - 关于 - 数据由 Return YouTube Dislike API 提供。点击了解更多信息。 - 隐藏点赞按钮的分隔符 - 紧凑点赞按钮 - 用百分比替换点踩数量 - 点踩百分比 - 显示视频点踩数 - 点踩数不可用(已达到客户端 API 限制) - 点踩数不可用(状态 %d) - 点踩数暂时不可用(API 连接超时) - 点踩数不可用(%s) - 当 Return YouTube Dislike API 不可用时显示提示 - 当 API 不可用时显示提示 - 分享链接时删除跟踪查询参数 - 清理分享链接 - 配置已复制到剪贴板 - "伪装应用版本为旧版本 - -・这将会改变应用的界面,但也可能出现未知的问题 -・如果关掉该项,可能仍然保留旧版界面,清除应用数据以解决该问题" - 4.27.53 - 在加拿大地区禁用收音机模式 - 6.11.52 - 禁用实时歌词 - 选择伪装的应用版本 - 伪装应用版本 - 伪装应用版本 - diff --git a/src/main/resources/music/translations/zh-rTW/missing_strings.xml b/src/main/resources/music/translations/zh-rTW/missing_strings.xml deleted file mode 100644 index c157a055b..000000000 --- a/src/main/resources/music/translations/zh-rTW/missing_strings.xml +++ /dev/null @@ -1,110 +0,0 @@ - - - Continue - Don\'t show again - "GmsCore does not have permission to run in the background. - -Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. - -This is required for the app to work." - "GmsCore battery optimizations must be disabled to prevent issues. - -Tap on the continue button and disable battery optimizations." - Open website - Action needed - Enable cloud messaging to receive notifications. - Open GmsCore - GmsCore is not installed. Install it. - Replaces the domain that is blocked in some regions so that playlist thumbnails, channel avatars, etc. can be received. - Bypass image region restrictions - Change from in-app share sheet to system share sheet. - Change share sheet - To open YouTube Music links in RVX Music, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Disables Cairo splash animation when the app starts up. - Disable Cairo splash animation - Disable swipe to change tracks in the miniplayer. - Disable miniplayer gesture - Disable swipe to change tracks in the player. - Disable player gesture - Includes the buffer in the debug log. - Enable debug buffer logging - Reset to default values. - Reset - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - Hides dark overlay that appears when double-tapping to seek. - Hide double-tap overlay filter - "If it is enabled, fullscreen ads are closed through the Close button. -If it is disabled, fullscreen ads are blocked. (there may be side effects)" - Fullscreen ads are blocked. (there may be side effects) - Fullscreen ads are closed through the Close button. - Close fullscreen ads - Hides the promotion alert banner. - Hide promotion alert banner - Hide About menu - Hide Data saving menu - Hide Downloads & storage menu - Hide General menu - Hide Notifications menu - Hide Get Music premium menu - Hide Family Center menu - Hide Playback menu - Hide Privacy & data menu - Hide Recommendations menu - "Hide elements of the settings menu. -This hides not only the YT Music settings menu, but also the ReVanced Extended settings menu." - Hide settings menu - Miscellaneous - Return YouTube Username - Settings menu - Show a toast when changing the default playback speed. - Show a toast - Show a toast when changing the default video quality. - Show a toast - @handle (Username) - Select the username display format. - Display format - Username (@handle) - Username - Replaces handles with usernames in comments. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - Shows the estimated like count of videos. - Show estimated likes - Hidden - About - sponsor.ajay.app - Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. - Color changed. - Color: - Invalid color code. - Color reset. - Reset color - Skip automatically - Disable - Skipped filler. - Skipped annoying reminder. - Skipped intro. - Skipped intermission. - Skipped intermission. - Skipped multiple segments. - Skipped a non-music section. - Skipped outro. - Skipped preview. - Skipped recap. - Skipped preview. - SponsorBlock is temporarily unavailable. - SponsorBlock is temporarily unavailable (status %d). - SponsorBlock is temporarily unavailable (API timed out). - 7.16.53 - Restore old action bar - diff --git a/src/main/resources/music/translations/zh-rTW/strings.xml b/src/main/resources/music/translations/zh-rTW/strings.xml deleted file mode 100644 index df9387545..000000000 --- a/src/main/resources/music/translations/zh-rTW/strings.xml +++ /dev/null @@ -1,306 +0,0 @@ - - - 圖表 - 探索 - 首頁 - 媒體庫 - 訂閱 - 更改應用程式開啟時的頁面 - 更改應用程式的起始頁面 - 按行分隔篩選元件名稱 - 編輯自訂篩選 - 啟用自訂篩選以隱藏介面元件 - 啟用自訂篩選 - 自訂過濾無效:%s - 自訂速度必須小於 %sx 使用預設值 - 自訂播放速度無效 使用預設值 - 新增或變更可以使用的播放速度 - 編輯自訂播放速度 - 停用在播放器中被強制啟用的字幕 - 停用強制自動字幕 - 點擊不喜歡按鈕時停用重定向到下一首歌曲 - 停用不喜歡重定向 - 將導覽列的顏色設成黑色 - 啟用黑色導覽列 - 將播放器背景顏色設成為黑色 - 啟用黑色播放器背景 - 讓播放列顏色和全螢幕播放器一致 - 啟用彩色播放列 - "在手機上啟用緊湊對話框 - -已知問題: -• 資料庫上的專輯封面會變得很小 -• 睡眠定時器的介面可能會出現錯誤" - 啟用精簡選單 - 列出除錯記錄檔 - 啟用除錯紀錄 - 切換歌曲時保持迷你播放器狀態 - 切換歌曲時保持迷你播放器 - 允許透過手機螢幕旋轉進入橫向模式 - 啟用橫向模式 - 啟用迷你播放器中的下一首按鈕 - 啟用迷你播放器的下一首按鈕 - 啟用迷你播放器中的上一首按鈕 - 啟用迷你播放器上一首按鈕 - "播放音樂時啟用 250/251 opus 解碼器。" - 啟用解碼器覆寫 - 允許向下滑動以關閉迷你播放器 - 啟用滑動來關閉迷你播放器 - "將修改靜音開關新增至播放速度彈出式選單 - - 資訊: - • 此功能適用於播客 - • 此功能仍在開發中,因此可能不穩定" - 新增修改靜音開關 - 播客也適用於護眼模式 - 在播客中啟用護眼模式 - 在影片播放器上增加灰色陰影以減少眼睛疲勞 - 護眼模式 - 重新啟動以套用更改後的介面 - 套用並重新啟動 - 匯出設定到文件 - 無法匯出設定 - 設定已成功匯出 - 匯入 - 從文件導入設定 - 複製 - 以文字形式匯入或匯出設定 - 匯入或匯出設定成文字檔 - 匯入/匯出設定 - 匯入失敗: %s - 設定已重設為預設值 - 已匯入設定: %d - ReVanced 擴充功能 - "下載按鈕開啟您的外部下載器 - - • 僅覆寫播放器中的下載動作按鈕 - • 不會覆寫彈出式功能表或媒體庫的下載按鈕" - 覆蓋下載動作按鈕 - 外部下載器 - "%1$s 未安裝 - 請到網站下載%2$s" - 警告 - %s 尚未安裝. 請先安裝該應用後重試. - 已安裝的外部下載程式的套件名稱,例如 NewPipe 或 Seal - 外部下載程式的套件名稱 - 隱藏帳戶選項中的空白處 - 隱藏帳戶空白處 - 要篩選的帳戶選單名稱列表,以換行符號分隔 - 帳號選單過濾 - 隱藏於自訂過濾器中的帳戶選項元素 - 隱藏帳戶選單 - 隱藏\"新增至播放列表\"按鈕 - 隱藏\"新增至播放列表\"按鈕 - 隱藏留言按鈕 - 隱藏留言按鈕 - 隱藏下載按鈕 - 隱藏下載按鈕 - 隱藏操作按鈕中的標籤 - 隱藏操作按鈕中的標籤 - 隱藏 \"讚\" 與 \"倒讚\" 按鈕 - 隱藏 \"讚\" 與 \"倒讚\" 按鈕 - 隱藏開啟電台按鈕 - 隱藏電台按鈕 - 隱藏分享按鈕 - 隱藏分享按鈕 - 在播放器中隱藏音訊影片切換開關 - 隱藏音訊影片切換開關 - 隱藏位於首頁與探索頁面的主題按鈕 - 隱藏主題按鈕 - 從首頁和探索頁面隱藏播放清單 - 隱藏播放清單 - 隱藏投放按鈕 - 隱藏投放按鈕 - 隱藏音樂分類列表 - 隱藏分類列表 - 隱藏留言頂部的頻道指南 - 隱藏頻道指南 - 輸入留言時隱藏時間戳記和表情符號按鈕 - 隱藏時間戳與表情符號按鈕 - 在媒體庫中隱藏浮動按鈕 - 隱藏浮動按鈕 - 隱藏三列組件 - 隱藏加入到待播清單選項 - 隱藏字幕選項 - 隱藏刪除播放列表選項 - 隱藏從媒體庫刪除選項 - 隱藏下載選項 - 隱藏編輯播放列表選項 - 隱藏前往專輯頁面選項 - 隱藏前往藝人頁面選項 - 隱藏前往插曲選項 - 隱藏前往播客選項 - 隱藏說明& 回報問題選項 - 隱藏讚與倒讚按鈕 - 隱藏播放下一個選項 - 隱藏畫質選項 - 隱藏從媒體庫中移除選項 - 隱藏從播放列表中移除選項 - 隱藏檢舉選項 - 隱藏儲存專輯以供日後使用選項 - 隱藏新增至媒體庫中選項 - 隱藏儲存至播放清單選項 - 隱藏分享選項 - 隱藏隨機播放選項 - 隱藏睡眠計時器選項 - 隱藏開啟電台選項 - 隱藏資訊統計選項 - 隱藏訂閱/取消訂閱選項 - 隱藏查看歌曲製作人員選項 - 隱藏全螢幕廣告 - 隱藏全螢幕廣告 - 在全螢幕播放器中隱藏分享按鈕 - 隱藏全螢幕播放器中的分享按鈕 - 隱藏一般廣告 - 隱藏一般廣告 - 隱藏帳戶選單中的用戶名稱 - 隱藏用戶名稱 - 隱藏工具欄裡的歷史紀錄按鈕 - 隱藏歷史紀錄按鈕 - 隱藏播放歌曲之前的廣告 - 隱藏音樂廣告 - 隱藏導覽列 - 隱藏導覽列 - 隱藏探索按鈕 - 隱藏探索按鈕 - 隱藏首頁按鈕 - 隱藏首頁按鈕 - 隱藏位於導覽列的標籤 - 隱藏導覽列標籤 - 隱藏媒體庫按鈕 - 隱藏媒體庫按鈕 - 隱藏樣品按鈕 - 隱藏樣品按鈕 - 隱藏升級按鈕 - 隱藏升級按鈕 - 在工具欄中隱藏通知按鈕 - 隱藏通知按鈕 - 隱藏付費推廣標籤 - 隱藏付費推廣標籤 - 在探索中隱藏播放清單卡 - 隱藏播放清單卡 - 隱藏 Premium 升級彈出選單 - 隱藏 Premium 升級彈出選單 - 隱藏 Premium 續訂橫幅 - 隱藏 Premium 續訂橫幅 - 在探索中隱藏樣品架 - 隱藏樣品架 - 在搜尋欄裡隱藏聽聲辨曲按鈕 - 隱藏聽聲辨取按鈕 - 隱藏輕觸以重新整理按鈕 - 隱藏輕觸以重新載入按鈕 - 隱藏服務條款 - 隱藏術語 - 在搜尋欄隱藏語音搜尋按鈕 - 隱藏語音搜尋按鈕 - 帳號 - 導覽列 - 廣告 - 彈出式選單 - 一般設定 - 導覽列 - 播放器 - 恢復Youtube 倒讚 - 贊助區塊阻擋(SponsorBlock) - 影片 - 記住上次選擇的播放速度 - 記住播放速度的變更 - 將預設速度更改為 %s - 記住重複播放的狀態 - 記住重複播放狀態 - 記住隨機播放的狀態 - 記住隨機播放狀態 - 記住上次選擇的影片畫質 - 記住影片畫質變更 - 設定行動數據預設的畫質為%s - 畫質設定失敗 - 設定WiFi預設的畫質為%s - "隱藏觀看者判斷對話框 -這並不能繞過年齡限制 -它只是自動接受它" - 隱藏觀看者判斷對話框 - 在Youtube上觀看時,從上次時間繼續觀看 - 繼續觀看 - 替換取消序列選項以在Youtube上觀看 - 替換取消序列 - 在Youtube上觀看 - 未知的影片網址 - 保持留言部分中的報檢舉選項完好無缺 - 將檢舉保留在留言 - 將檢舉替換成播放速度 - 變更檢舉 - 將留言彈出介面恢復為舊樣式 - 恢復舊版留言彈出介面 - 將播放器背景恢復為舊樣式 - 恢復舊版播放器背景 - "將播放器介面恢復為舊樣式 -某些功能在舊的播放器介面中可能無法正常運作" - 恢復舊版播放器介面 - 將媒體庫選項恢復為舊樣式(實驗性功能) - 恢復舊版媒體庫樣式 - 關於 - 資料由 Return YouTube Dislike API 提供 -點擊了解更多資訊 - ReturnYouTubeDislike.com - 隱藏按讚按鈕中間的分隔線 - 緊湊型按讚按鈕 - 將倒讚數以百分比的形式顯示 - 倒讚百分比 - 顯示影片的不喜歡次數 - 啟用恢復Youtube倒讚 - 倒讚顯示不正常(已達到客戶端 API 限制) - 倒讚數無法使用 (狀態 %d) - 倒讚數暫時無法使用 (API 連線超時) - 倒讚數無法使用 (狀態 %s) - 當 Return YouTube Dislike API 無法使用時顯示提示訊息 - 當API無法使用時顯示提示訊息 - 分享連結時從 URL 中刪除追蹤參數。 - 清理分享連結 - 更改 API 網址 - API 網址已更改 - API 網址無效 - API 網址重設 - SponsorBlock 用於調用伺服器位置 -如果您知道自己在做什麼,否則請不要更改此設定 - 更改片段操作 - 啟用SponsorBlock - SponsorBlock 是透過使用者共同編輯新增片段來跳過 YouTube 的惱人片段 - 贅詞、玩笑 - 僅為影片中主要內容所不相關的贅字或幽默片段。這不應有內容或背景細節 - 互動提醒 (訂閱) - 影片中間簡短提醒觀眾來按讚、訂閱或追蹤 -如果片段較長,或是關於某個具體事物,則應分類為自我推廣 - 中場休息、介紹動畫 - 沒有實際內容的片段 -可以是靜止畫面,重複的動畫 -片段內未含有任何資訊 - 音樂:非音樂部分 - 此功能僅供音樂影片使用。本功能僅應該用於音樂錄影帶中並未包含其他類別的段落。 - 結束卡、片尾 - 鳴謝或當 YouTube 結尾資訊卡出現時 -不是含有資訊的總結 - 預覽、回顧、掛勾 - 顯示影片或其他系列影片中即將發生的情況或發生的情況的剪輯集合,其中所有資訊都會在其他地方重複 - 無償、自我推銷 - 類似贊助商廣告,但是非付費或自我推廣 -這包括有關商品、捐贈或與他人的合作資訊 - 贊助商廣告 - 付費推廣、付費推薦和直接廣告 -非自我推廣或免費提及、推薦他們喜歡的事物、創作者、網站、產品 - 已跳過自我宣傳 - 已跳過贊助 - 當API無法使用時顯示提示訊息 - 當 Sponsorblock 無法使用時顯示提示訊息 - 自動跳過片段時顯示提示訊息 - 自動跳過某個片段時顯示提示訊息 - 設定已複製到剪貼簿 - "將客戶端版本偽裝為舊版本 - - • 這將改變應用程式的外觀,但可能會出現未知的錯誤 - • 如果稍後停用,舊的 UI 可能會保留,直到應用程式資料被清除" - 4.27.53 - 在加拿大地區停用電台模式 - 6.11.52 - 停用即時歌詞 - 選擇欲偽裝的應用程式版本 - 偽裝應用程式版本 - 偽裝應用程式版本 - diff --git a/src/main/resources/youtube/branding/mmt_turquoise/splash/values-v31/styles.xml b/src/main/resources/youtube/branding/mmt_turquoise/splash/values-v31/styles.xml deleted file mode 100644 index c7462f74a..000000000 --- a/src/main/resources/youtube/branding/mmt_turquoise/splash/values-v31/styles.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/src/main/resources/youtube/branding/mmt_yellow/splash/values-v31/styles.xml b/src/main/resources/youtube/branding/mmt_yellow/splash/values-v31/styles.xml deleted file mode 100644 index c7462f74a..000000000 --- a/src/main/resources/youtube/branding/mmt_yellow/splash/values-v31/styles.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/src/main/resources/youtube/branding/revancify_blue/splash/values-v31/styles.xml b/src/main/resources/youtube/branding/revancify_blue/splash/values-v31/styles.xml deleted file mode 100644 index 58243062d..000000000 --- a/src/main/resources/youtube/branding/revancify_blue/splash/values-v31/styles.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/src/main/resources/youtube/branding/revancify_red/splash/values-v31/styles.xml b/src/main/resources/youtube/branding/revancify_red/splash/values-v31/styles.xml deleted file mode 100644 index 58243062d..000000000 --- a/src/main/resources/youtube/branding/revancify_red/splash/values-v31/styles.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/src/main/resources/youtube/branding/xisr_yellow/splash/values-v31/styles.xml b/src/main/resources/youtube/branding/xisr_yellow/splash/values-v31/styles.xml deleted file mode 100644 index c7462f74a..000000000 --- a/src/main/resources/youtube/branding/xisr_yellow/splash/values-v31/styles.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/src/main/resources/youtube/branding/youtube/splash/values-v31/styles.xml b/src/main/resources/youtube/branding/youtube/splash/values-v31/styles.xml deleted file mode 100644 index c7462f74a..000000000 --- a/src/main/resources/youtube/branding/youtube/splash/values-v31/styles.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/src/main/resources/youtube/materialyou/drawable-night-v31/new_content_dot_background.xml b/src/main/resources/youtube/materialyou/drawable-night-v31/new_content_dot_background.xml deleted file mode 100644 index 2f80732c2..000000000 --- a/src/main/resources/youtube/materialyou/drawable-night-v31/new_content_dot_background.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/main/resources/youtube/materialyou/drawable-v31/new_content_count_background.xml b/src/main/resources/youtube/materialyou/drawable-v31/new_content_count_background.xml deleted file mode 100644 index ece2cbf17..000000000 --- a/src/main/resources/youtube/materialyou/drawable-v31/new_content_count_background.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/main/resources/youtube/materialyou/drawable-v31/new_content_dot_background.xml b/src/main/resources/youtube/materialyou/drawable-v31/new_content_dot_background.xml deleted file mode 100644 index 71b83998d..000000000 --- a/src/main/resources/youtube/materialyou/drawable-v31/new_content_dot_background.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/main/resources/youtube/materialyou/layout-v31/new_content_count.xml b/src/main/resources/youtube/materialyou/layout-v31/new_content_count.xml deleted file mode 100644 index f7f7d9450..000000000 --- a/src/main/resources/youtube/materialyou/layout-v31/new_content_count.xml +++ /dev/null @@ -1,3 +0,0 @@ - - \ No newline at end of file diff --git a/src/main/resources/youtube/settings/host/values/arrays.xml b/src/main/resources/youtube/settings/host/values/arrays.xml deleted file mode 100644 index 457f20448..000000000 --- a/src/main/resources/youtube/settings/host/values/arrays.xml +++ /dev/null @@ -1,264 +0,0 @@ - - - - @string/revanced_alt_thumbnail_options_entry_1 - @string/revanced_alt_thumbnail_options_entry_2 - @string/revanced_alt_thumbnail_options_entry_3 - @string/revanced_alt_thumbnail_options_entry_4 - - - ORIGINAL - DEARROW - DEARROW_STILL_IMAGES - STILL_IMAGES - - - @string/revanced_alt_thumbnail_stills_time_entry_1 - @string/revanced_alt_thumbnail_stills_time_entry_2 - @string/revanced_alt_thumbnail_stills_time_entry_3 - - - BEGINNING - MIDDLE - END - - - @string/revanced_change_layout_entry_1 - @string/revanced_change_layout_entry_2 - @string/revanced_change_layout_entry_3 - @string/revanced_change_layout_entry_4 - @string/revanced_change_layout_entry_5 - - - ORIGINAL - SMALL_FORM_FACTOR - SMALL_FORM_FACTOR_WIDTH_DP - LARGE_FORM_FACTOR - LARGE_FORM_FACTOR_WIDTH_DP - - - @string/revanced_change_start_page_entry_default - @string/revanced_change_start_page_entry_search - @string/revanced_change_start_page_entry_shorts - @string/revanced_change_start_page_entry_subscriptions - @string/revanced_change_start_page_entry_explore - @string/revanced_change_start_page_entry_library - @string/revanced_change_start_page_entry_liked_videos - @string/revanced_change_start_page_entry_watch_later - @string/revanced_change_start_page_entry_history - @string/revanced_change_start_page_entry_trending - @string/revanced_change_start_page_entry_gaming - @string/revanced_change_start_page_entry_live - @string/revanced_change_start_page_entry_music - @string/revanced_change_start_page_entry_movies - @string/revanced_change_start_page_entry_sports - @string/revanced_change_start_page_entry_browse - @string/revanced_change_start_page_entry_courses - - - ORIGINAL - - SEARCH - SHORTS - - SUBSCRIPTIONS - EXPLORE - LIBRARY - LIKED_VIDEO - WATCH_LATER - HISTORY - TRENDING - GAMING - LIVE - MUSIC - MOVIE - SPORTS - BROWSE - COURSES - - - @string/revanced_change_shorts_repeat_state_entry_default - @string/revanced_change_shorts_repeat_state_entry_repeat - @string/revanced_change_shorts_repeat_state_entry_auto_play - @string/revanced_change_shorts_repeat_state_entry_pause - - - 0 - 1 - 2 - 3 - - - @string/quality_auto - 144p - 240p - 360p - 480p - 720p - 1080p - 1440p - 2160p - - - -2 - 144 - 240 - 360 - 480 - 720 - 1080 - 1440 - 2160 - - - NewPipe - Seal - Tubular - YTDLnis - - - org.schabi.newpipe - com.junkfood.seal - org.polymorphicshade.tubular - com.deniscerri.ytdl - - - https://github.com/TeamNewPipe/NewPipe/releases/latest - https://github.com/JunkFood02/Seal/releases/latest - https://github.com/polymorphicshade/Tubular/releases/latest - https://github.com/deniscerri/ytdlnis/releases/latest - - - NewPipe - Seal - Tubular - YTDLnis - - - org.schabi.newpipe - com.junkfood.seal - org.polymorphicshade.tubular - com.deniscerri.ytdl - - - https://github.com/TeamNewPipe/NewPipe/releases/latest - https://github.com/JunkFood02/Seal/releases/latest - https://github.com/polymorphicshade/Tubular/releases/latest - https://github.com/deniscerri/ytdlnis/releases/latest - - - YTDLnis - - - com.deniscerri.ytdl - - - https://github.com/deniscerri/ytdlnis/releases/latest - - - @string/revanced_miniplayer_type_entry_1 - @string/revanced_miniplayer_type_entry_2 - @string/revanced_miniplayer_type_entry_3 - @string/revanced_miniplayer_type_entry_4 - @string/revanced_miniplayer_type_entry_5 - @string/revanced_miniplayer_type_entry_6 - - - ORIGINAL - PHONE - TABLET - MODERN_1 - MODERN_2 - MODERN_3 - - - @string/revanced_miniplayer_type_entry_1 - @string/revanced_miniplayer_type_entry_2 - @string/revanced_miniplayer_type_entry_3 - - - ORIGINAL - PHONE - TABLET - - - @string/revanced_return_youtube_username_display_format_username_only - @string/revanced_return_youtube_username_display_format_username_handle - @string/revanced_return_youtube_username_display_format_handle_username - - - USERNAME_ONLY - USERNAME_HANDLE - HANDLE_USERNAME - - - @string/revanced_shorts_double_tap_to_like_animation_entry_1 - @string/revanced_shorts_double_tap_to_like_animation_entry_2 - @string/revanced_shorts_double_tap_to_like_animation_entry_3 - @string/revanced_shorts_double_tap_to_like_animation_entry_4 - @string/revanced_shorts_double_tap_to_like_animation_entry_5 - @string/revanced_shorts_double_tap_to_like_animation_entry_6 - - - ORIGINAL - THUMBS_UP - THUMBS_UP_CAIRO - HEART - HEART_TINT - HIDDEN - - - @string/revanced_spoof_streaming_data_type_entry_ios - @string/revanced_spoof_streaming_data_type_entry_android_unplugged - @string/revanced_spoof_streaming_data_type_entry_android_vr - - - IOS - ANDROID_UNPLUGGED - ANDROID_VR - - - @string/revanced_spoof_app_version_target_entry_18_17_43 - @string/revanced_spoof_app_version_target_entry_18_05_40 - @string/revanced_spoof_app_version_target_entry_17_41_37 - - - 18.17.43 - 18.05.40 - 17.41.37 - - - YouTube Music - - - com.google.android.apps.youtube.music - - - -1 - 0 - +1 - +2 - +3 - +4 - +5 - - - -1 - 0 - 1 - 2 - 3 - 4 - 5 - - - @string/revanced_watch_history_type_entry_1 - @string/revanced_watch_history_type_entry_2 - @string/revanced_watch_history_type_entry_3 - - - ORIGINAL - REPLACE - BLOCK - - diff --git a/src/main/resources/youtube/settings/host/values/strings.xml b/src/main/resources/youtube/settings/host/values/strings.xml deleted file mode 100644 index 702fe2f51..000000000 --- a/src/main/resources/youtube/settings/host/values/strings.xml +++ /dev/null @@ -1,1737 +0,0 @@ - - - Enable accessibility controls for the video player? - Your controls are modified because an accessibility service is on. - Continue - Don\'t show again - "GmsCore does not have permission to run in the background. - -Follow the 'Don't kill my app!' guide for your device, and apply the instructions to your GmsCore installation. - -This is required for the app to work." - "GmsCore battery optimizations must be disabled to prevent issues. - -Tap on the continue button and disable battery optimizations." - Open website - Action needed - Enable cloud messaging to receive notifications. - Open GmsCore - GmsCore is not installed. Install it. - "DeArrow provides crowd-sourced thumbnails for YouTube videos. These thumbnails are often more relevant than those provided by YouTube. - -If enabled, video URLs will be sent to the API server and no other data is sent. If a video does not have DeArrow thumbnails, then the original or still captures are shown. - -Tap here to learn more about DeArrow." - DeArrow - Invalid DeArrow API URL. - The URL of the DeArrow thumbnail cache endpoint. - DeArrow API endpoint - Toast is not shown if DeArrow is unavailable. - Toast is shown if DeArrow is unavailable. - Show a toast if API is unavailable - DeArrow temporarily unavailable. (status code: %s) - DeArrow temporarily unavailable. - Home tab - You tab - Original thumbnails - DeArrow & original thumbnails - DeArrow & still captures - Still captures - Player playlists, recommendations - Search results - Still video captures - Still captures are taken from the beginning, middle, or end of each video. These images are built into YouTube and no external API is used. - Still video captures - Using high quality still captures. - Using medium quality still captures. Thumbnails will load faster, but live streams, unreleased, or very old videos may show blank thumbnails. - Use fast still captures - Beginning of video - Middle of video - End of video - Video time to take still captures from - Subscriptions tab - Information is not appended to the timestamp. - "Information is appended to the timestamp. - -Tap to configure the video quality or playback speed. -Tap and hold to toggle the appended information type." - Append timestamp information - Append playback speed. - Append video quality. - Append information type - Ambient mode is disabled in battery saver mode. - Ambient mode is enabled in battery saver mode. - Bypass Ambient mode restrictions - The domain to fetch images from.\nNote: Only enter the domain name, i.e., without the \"https\:\/\/\" prefix. - Alternative domain - Using original image host.\n\nEnabling this can fix missing images that are blocked in some regions. - Using image host yt4.ggpht.com. - Bypass image region restrictions - Original - Phone - Phone (Max 480 dp) - Tablet - Tablet (Min 600 dp) - Change layout - Switch toggles are used. - Text toggles are used. - Change toggle type - In-app share sheet is used. - System share sheet is used. - Change share sheet - Autoplay - Default - Pause - Repeat - Change Shorts repeat state - Browse channels - Courses / Learning - Default - Explore - Gaming - History - Library - Liked videos - Live - Movies - Music - Search - Shorts - Sports - Subscriptions - Trending - Watch later - Change start page - Start page changes only once. - "Start page always changes. - -Limitation: Back button on the toolbar may not work." - Change start page type - Generic header is enabled. - Premium header is enabled. - Change YouTube header - List of component path builder strings to filter, separated by new lines. - Custom filter - Custom filter is disabled. - Custom filter is enabled. - Enable custom filter - Invalid custom filter: %s. - Old style flyout menu is used. - Custom dialog is used. - Custom playback speed menu type - Custom speeds must be less than %sx. - Invalid custom playback speeds. - Add or change available playback speeds. - Edit custom playback speeds - Player overlay opacity must be between 0-100. - Opacity value between 0-100, where 0 is transparent. - Custom player overlay opacity - Type the hex code of the seekbar color. - Custom seekbar color value - To open YouTube links in RVX, enable \'Open supported links\' and enable the supported web addresses. - Open default app settings - Default playback speed - Default video quality on mobile network - Default video quality on Wi-Fi network - Disables ambient mode for fullscreen only. - Ambient mode is enabled in fullscreen. - Ambient mode is disabled in fullscreen. - Disable Ambient mode in fullscreen - Disables ambient mode. - Ambient mode is enabled. - Ambient mode is disabled. - Disable Ambient mode - Forced auto audio tracks are enabled. - Forced auto audio tracks are disabled. - Disable forced auto audio tracks - Forced auto captions are enabled. - Forced auto captions are disabled. - Disable forced auto captions - Auto player popup panels are enabled. - Auto player popup panels are disabled. - Disable player popup panels - "Auto switch mix playlists is enabled when autoplay is turned on. - -Autoplay can be changed in YouTube settings: -Settings → Autoplay → Autoplay next video" - Auto switch mix playlists is disabled. - Disable switch mix playlists - Enabling this feature will disable automatic switching to YouTube Mix when playing music while autoplay is turned on. - Default playback speed is enabled for live streams. - Default playback speed is disabled for live streams. - Disable playback speed for live streams - Default playback speed is enabled for music. - "Default playback speed is disabled for music. - -Limitation: This setting may not apply to videos that do not include the 'Listen on YouTube Music' banner." - Disable playback speed for music - Engagement panel is enabled. - Engagement panel is disabled. - Disable engagement panel - Haptic feedback is enabled. - Haptic feedback is disabled. - Disable chapters haptic feedback - Haptic feedback is enabled. - Haptic feedback is disabled. - Disable scrubbing haptic feedback - Haptic feedback is enabled. - Haptic feedback is disabled. - Disable seek haptic feedback - Haptic feedback is enabled. - Haptic feedback is disabled. - Disable seek undo haptic feedback - Haptic feedback is enabled. - Haptic feedback is disabled. - Disable zoom haptic feedback - Auto HDR brightness is enabled. - Auto HDR brightness is disabled. - Disable auto HDR brightness - HDR video is enabled. - HDR video is disabled. - Disable HDR video - Video orientation follows device settings in fullscreen. - Video orientation is portrait mode in fullscreen. - Disable landscape mode - Like and Dislike buttons will glow when mentioned. - Like and Dislike buttons will not glow when mentioned. - Disable Like and Dislike button glow - "Disable CronetEngine's QUIC protocol." - Disable QUIC protocol - Shorts player will resume on app startup. - Shorts player will not resume on app startup. - Disable resuming Shorts player - Rolling numbers are animated. - Rolling numbers are not animated. - Disable Rolling number animations - Chapters are enabled in the seekbar. - Chapters are disabled in the seekbar. - Disable seekbar chapters - Fountain animation is enabled above the Like button. - Fountain animation is disabled above the Like button. - Disable Like button animation - "Disable '2x>>' while holding down. - -Note: -• Disabling the speed overlay restores the Slide to seek behavior of the old layout. -• Disabling this setting does not forcefully enable the speed overlay." - Disable speed overlay - Splash animation is enabled. - Splash animation is disabled. - Disable splash animation - "Disables the following interactions when the video description is expanded: - -• Tap to scroll. -• Tap and hold to select text." - Disable video description interaction - VP9 codec is enabled. - "VP9 codec is disabled. - -• Maximum resolution is 1080p. -• Video playback will use more internet data than VP9. -• VP9 codec is still used for HDR video." - Disable VP9 codec - Cairo seekbar is disabled. - "Cairo seekbar is enabled. - -Side effect: Cairo theme is also applied to notification dots." - Enable Cairo seekbar - Controls overlay fills the fullscreen. - Controls overlay does not fill the fullscreen. - Enable compact controls overlay - Custom playback speed is disabled. - Custom playback speed is enabled. - Enable custom playback speed - Custom seekbar color is disabled. - Custom seekbar color is enabled. - Enable custom seekbar color - Debug logs do not include the buffer. - Debug logs include the buffer. - Enable debug buffer logging - Debug logs are disabled. - Debug logs are enabled. - Enable debug logging - Default playback speed does not apply to Shorts. - Default playback speed applies to Shorts. - Enable Shorts default playback speed - External browser is disabled. - External browser is enabled. - Enable external browser - Gradient loading screen is disabled. - Gradient loading screen is enabled. - Enable gradient loading screen - Spacing between navigation buttons is normal. - Spacing between navigation buttons is narrow. - Enable narrow navigation buttons - Following default redirect policy. - Bypassing URL redirects. - Enable open links directly - Enable the OPUS codec if the player response includes the OPUS codec. - Enable OPUS codec - Do not save and restore brightness when exiting or entering fullscreen. - Save and restore brightness when exiting or entering fullscreen. - Enable save and restore brightness - Seekbar tapping is disabled. - Seekbar tapping is enabled. - Enable seekbar tapping - "This will restore thumbnails to livestreams that do not have seekbar thumbnails. - -Internet data usage may be higher, and seekbar thumbnails will have a slight delay before showing. - -This feature works best with a very fast internet connection." - Seekbar thumbnails are medium quality. - Seekbar thumbnails are high quality. - Enable high quality thumbnails - Timestamp is disabled. - "Timestamp is enabled. - -Limitations: -• This setting not only enables timestamps, but also allows users to hide the UI by clicking on the player background. -• As this is a feature in the development stage by Google, the layout may be broken." - Enable timestamps - Brightness swipe is disabled. - Brightness swipe is enabled. - Enable brightness gesture - Haptic feedback is disabled. - Haptic feedback is enabled. - Enable haptic feedback - Lowest value of the brightness gesture does not activate auto-brightness. - Lowest value of the brightness gesture activates auto-brightness. - Enable auto-brightness gesture - Touch to activate swipe gesture. - Touch and hold to activate swipe gesture. - Enable press-to-swipe gesture - Swiping up / down will not play the next / previous video. - Swiping up / down will play the next / previous video. - Enable swipe to change video - Volume swipe is disabled. - Volume swipe is enabled. - Enable volume gesture - Navigation bar is opaque. - Navigation bar is translucent. - Enable translucent navigation bar - Entering fullscreen when swiping down below the video player is disabled. - Entering fullscreen when swiping down below the video player is enabled. - Enable watch panel gestures - "Enabling this setting will disable the Settings button in the You tab. - -In this case, please use the following path to access the settings: -You tab → View channel → Menu → Settings" - Enable wide search bar in You tab - Wide search bar is disabled. - Wide search bar is enabled. - Enable wide search bar - Wide search bar hides the YouTube header. - Wide search bar does not hide the YouTube header. - Enable wide search bar with header - Description - "Enter the title of the video description panel in your language. -The Expand video description option may not work if the entered string does not match the video description panel title." - Title in video description panel - Video descriptions are not expanded automatically. - Video descriptions are expanded automatically. - Expand video descriptions - Do you wish to proceed? - Reset to default values. - Restart to load the layout normally - "There is a YouTube server-side bug that causes rolling number text such as likes, views, and upload dates to be hidden for some users. - -A temporary workaround for this issue is to spoof the app version to 19.13.37. - -Do you want to spoof the app version before restarting the app?" - Refresh and restart - Failed to export settings. - Settings were successfully exported. - Export settings to a file. - Export settings - Import - Copy - Import or export settings as text. - Import / Export as text - Failed to import settings. - Settings reset to default. - Settings were successfully imported. - Import settings from a saved file. - Import settings - Reset - Search %s - ReVanced Extended - External downloader - Not installed - "%1$s is not installed. -Please download %2$s from the website." - Warning - %s is not installed. Please install it. - Package name of your installed external downloader app, such as YTDLnis. - Playlist downloader package name - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - Package name of your installed external downloader app, such as NewPipe or YTDLnis. - Video downloader package name - "Videos will be switched to fullscreen in the following situations: - -• When a video is started. -• When a timestamp in the comments is clicked on." - Force fullscreen - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - List of account menu names to filter, separated by new lines. - Account menu filter - "Hide elements of the account menu and You tab. -Some components may not be hidden." - Hide account menu - AI-generated video summary section is shown. - AI-generated video summary section is hidden. - Hide AI-generated video summary section - Album cards are shown. - Album cards are hidden. - Hide album cards - Featured places, Games, and Music sections are shown. - Featured places, Games, and Music sections are hidden. - Hide Attributes section - Autoplay preview container is shown. - Autoplay preview container is hidden. - Hide autoplay preview container - Visit store button is shown. - Visit store button is hidden. - Hide Visit store button - "Hides the following shelves: -• Breaking news -• Continue watching -• Explore more channels -• Listen again -• Shopping -• Watch it again" - Hide carousel shelf - Shown in feed. - Hidden in feed. - Hide in feed - Shown in related videos. - Hidden in related videos. - Hide in related videos - Shown in search results. - Hidden in search results. - Hide in search results - Channel guidelines are shown. - Channel guidelines are hidden. - Hide channel guidelines - Channel member shelf is shown. - Channel member shelf is hidden. - Hide channel member shelf - Links at the top of channel profiles are shown. - Links at the top of channel profiles are hidden. - Hide channel profile links - "Shorts -Playlists -Store" - List of channel tab names to filter, separated by new lines. - Channel tab filter - Channel tab filter is disabled. - Channel tab filter is enabled. - Enable channel tab filter - Channel watermark is shown. - Channel watermark is hidden. - Hide channel watermark - Chapters section is shown. - Chapters section is hidden. - Hide Chapters section - Chips shelf is shown. - Chips shelf is hidden. - Hide chips shelf - Clip button is shown. - Clip button is hidden. - Hide Clip button - Create a Short button is shown. - Create a Short button is hidden. - Hide Create a Short button - Highlighted search links are shown. - Highlighted search links are hidden. - Hide highlighted search links - Thanks button is shown. - Thanks button is hidden. - Hide Thanks button - Timestamp and emoji buttons are shown. - Timestamp and emoji buttons are hidden. - Hide timestamp and emoji buttons - Comments by members banner is shown. - Comments by members banner is hidden. - Hide Comments by members banner - Comments section is shown in home feed. - Comments section is hidden in home feed. - Hide Comments section in home feed - Comments section is shown. - Comments section is hidden. - Hide Comments section - Shown in channel. - Hidden in channel. - Hide in channel - Shown in home feed and related videos. - Hidden in home feed and related videos. - Hide in home feed and related videos - Shown in subscriptions feed. - Hidden in subscriptions feed. - Hide in subscriptions feed - How this content was made section is shown. - How this content was made section is hidden. - Hide Contents section - Crowdfunding box is shown. - Crowdfunding box is hidden. - Hide crowdfunding box - Double-tap overlay filter is shown. - Double-tap overlay filter is hidden. - Hide double-tap overlay filter - Download button is shown. - Download button is hidden. - Hide Download button - End screen cards are shown. - End screen cards are hidden. - Hide end screen cards - Expandable chips are shown. - Expandable chips are hidden. - Hide expandable chip under videos - Expandable shelves are shown. - Expandable shelves are hidden. - Hide expandable shelves - Captions button is shown. - Captions button is hidden. - Hide Captions button - List of flyout menu names to filter, separated by new lines. - Feed flyout menu filter - Feed flyout menu filter is disabled. - Feed flyout menu filter is enabled. - Enable feed flyout menu filter - Search bar is shown. - Search bar is hidden. - Hide search bar - Surveys are shown. - Surveys are hidden. - Hide surveys - Film strip overlay is shown. - Film strip overlay is hidden. - Hide film strip overlay - Floating button is shown. - Floating button is hidden. - Hide floating button - Floating microphone button is shown. - Floating microphone button is hidden. - Hide floating microphone button - For You shelf is shown. - For You shelf is hidden. - Hide For You shelf - Fullscreen ads are shown. - Fullscreen ads are hidden. - Hide fullscreen ads - "Fullscreen ads are blocked. - -Side effect: Community post images may be blocked in fullscreen." - Fullscreen ads are closed through the Close button. - Close fullscreen ads - General ads are shown. - General ads are hidden. - Hide general ads - YouTube Premium promotion is shown. - YouTube Premium promotion is hidden. - Hide YouTube Premium promotion - Gray separators are shown. - Gray separators are hidden. - Hide gray separators - Handle is shown. - Handle is hidden. - Hide handle - Image search button is shown. - Image search button is hidden. - Hide image search button - Image shelves are shown. - Image shelves are hidden. - Hide image shelves - Info cards section is shown. - Info cards section is hidden. - Hide Info cards section - Info cards are shown. - Info cards are hidden. - Hide info cards - Info panels are shown. - Info panels are hidden. - Hide info panels - Join button is shown. - Join button is hidden. - Hide Join button - Key concepts section is shown. - Key concepts section is hidden. - Hide Key concepts section - "Home / Subscription / Search results are filtered to hide content that matches keyword phrases. - -Limitations: -• Shorts cannot be hidden by channel name. -• Some UI components may not be hidden. -• Searching for a keyword may show no results." - About keyword filtering - Surrounding a keyword/phrase with double-quotes will prevent partial matches of video titles and channel names.<br><br>For example,<br><b>\"ai\"</b> will hide the video: <b>How does AI work?</b><br>but will not hide: <b>What does fair use mean?</b> - Match whole words - Comments are not filtered. - Comments are filtered. - Hide comments by keywords - Videos in home feed are not filtered. - Videos in home feed are filtered. - Hide home videos by keywords - "Keywords and phrases to hide, separated by new lines. - -Keywords can be channel names or any text shown in video titles. - -Words with uppercase letters in the middle must be entered with the casing (ie: iPhone, TikTok, LeBlanc)." - Keywords to hide - Search results are not filtered. - Search results are filtered. - Hide search results by keywords - Videos in subscriptions feed are not filtered. - Videos in subscriptions feed are filtered. - Hide subscription videos by keywords - Keyword will hide all videos: %s. - Cannot use keyword: %s. - Add quotes to use keyword: %s. - Keyword has conflicting declarations: %s. - Keyword is too short and requires quotes: %s. - Latest posts are shown. - Latest posts are hidden. - Hide latest posts - Latest videos button is shown. - Latest videos button is hidden. - Hide Latest videos button - Like and Dislike buttons are shown. - Like and Dislike buttons are hidden. - Hide Like and Dislike buttons - Live chat messages are shown.\n\nThis setting applies to Shorts live videos too. - Live chat messages are hidden.\n\nThis setting applies to Shorts live videos too. - Hide live chat messages - Live chat replay button is shown.\n\nIt appears in fullscreen when closing live chat. - Live chat replay button is hidden.\n\nIt appears in fullscreen when closing live chat. - Hide live chat replay button - Hide videos with less than 1,000 views from home feeds that have been uploaded from unsubscribed channels. - Hide low views video - Medical panels are shown. - Medical panels are hidden. - Hide medical panels - Merchandise shelves are shown. - Merchandise shelves are hidden. - Hide merchandise shelves - Mix playlists are shown. - Mix playlists are hidden. - Hide mix playlists - Movies shelves are shown. - Movies shelves are hidden. - Hide movies shelves - Navigation bar is shown. - Navigation bar is hidden. - Hide navigation bar - Create button is shown. - Create button is hidden. - Hide Create button - Home button is shown. - Home button is hidden. - Hide Home button - Navigation labels are shown. - Navigation labels are hidden. - Hide navigation labels - Library button is shown. - Library button is hidden. - Hide Library button - Notifications button is shown. - Notifications button is hidden. - Hide notifications button - Shorts button is shown. - Shorts button is hidden. - Hide Shorts button - Subscriptions button is shown. - Subscriptions button is hidden. - Hide Subscriptions button - Notify me button is shown. - Notify me button is hidden. - Hide Notify me button - Paid promotion label is shown. - Paid promotion label is hidden. - Hide paid promotion label - Playables are shown. - Playables are hidden. - Hide Playables - Autoplay button is shown. - Autoplay button is hidden. - Hide Autoplay button - Captions button is shown. - Captions button is hidden. - Hide Captions button - Cast button is shown. - Cast button is hidden. - Hide Cast button - Collapse button is shown. - Collapse button is hidden. - Hide collapse button - Ambient mode menu is shown. - Ambient mode menu is hidden. - Hide Ambient mode menu - Audio track menu is shown. - Audio track menu is hidden. - Hide Audio track menu - Captions menu footer is shown. - Captions menu footer is hidden. - Hide captions menu footer - Captions menu is shown. - Captions menu is hidden. - Hide Captions menu - 1080p Premium menu is shown. - 1080p Premium menu is hidden. - Hide 1080p Premium menu - Help & feedback menu is shown. - Help & feedback menu is hidden. - Hide Help & feedback menu - Listen with YouTube Music menu is shown. - Listen with YouTube Music menu is hidden. - Hide Listen with YouTube Music menu - Lock screen menu is shown. - Lock screen menu is hidden. - Hide Lock screen menu - Loop video menu is shown. - Loop video menu is hidden. - Hide Loop video menu - More information menu is shown. - More information menu is hidden. - Hide More information menu - Picture-in-picture menu is shown. - Picture-in-picture menu is hidden. - Hide Picture-in-picture menu - Playback speed menu is shown. - Playback speed menu is hidden. - Hide Playback speed menu - Premium controls menu is shown. - Premium controls menu is hidden. - Hide Premium controls menu - Quality menu footer is shown. - Quality menu footer is hidden. - Hide quality menu footer - Quality menu header is shown. - Quality menu header is hidden. - Hide quality menu header - Report menu is shown. - Report menu is hidden. - Hide Report menu - Sleep timer menu is shown. - Sleep timer menu is hidden. - Hide Sleep timer menu - Stable volume menu is shown. - Stable volume menu is hidden. - Hide Stable volume menu - Stats for nerds menu is shown. - Stats for nerds menu is hidden. - Hide Stats for nerds menu - Watch in VR menu is shown. - Watch in VR menu is hidden. - Hide Watch in VR menu - Fullscreen button is shown. - Fullscreen button is hidden. - Hide Fullscreen button - Buttons are shown. - Buttons are hidden. - Hide Previous & Next buttons - Shopping shelf is shown. - Shopping shelf is hidden. - Hide player shopping shelf - YouTube Music button is shown. - YouTube Music button is hidden. - Hide YouTube Music button - Save button is shown. - Save button is hidden. - Hide Save button - Explore the podcast section is shown. - Explore the podcast section is hidden. - Hide Explore the podcast section - Preview comment is shown. - Preview comment is hidden. - Hide preview comment - This changes the size of the comments section, so it is impossible to open a live chat replay in the comments section. - This does not change the size of the comments section, so it is possible to open the live chat replay in the comments section. - Hide preview comment type - Promotion alert banner is shown. - Promotion alert banner is hidden. - Hide promotion alert banner - Comments button is shown. - Comments button is hidden. - Hide Comments button - Dislike button is shown. - Dislike button is hidden. - Hide Dislike button - Like button is shown. - Like button is hidden. - Hide Like button - Live chat button is shown. - Live chat button is hidden. - Hide Live chat button - More button is shown. - More button is hidden. - Hide More button - Open mix playlist button is shown. - Open mix playlist button is hidden. - Hide Open mix playlist button - Open playlist button is shown. - Open playlist button is hidden. - Hide Open playlist button - Save button is shown. - Save button is hidden. - Hide Save button - Share button is shown. - Share button is hidden. - Hide Share button - Quick actions container is shown. - Quick actions container is hidden. - Hide quick actions container - "Hides the following recommended videos: - -• Videos with the Members only tag. -• Videos with phrases such as 'People also watched' underneath." - Hide recommended videos - More videos section in the quick actions container and the related video overlay are shown. - More videos section in the quick actions container and the related video overlay are hidden. - Hide related video overlay - Related videos are shown. - Related videos are hidden. - Hide related videos - "This setting limits the maximum number of layouts that can be loaded on the player screen. - -If the layout of the player screen changes due to server-side changes, unintended layouts may be hidden on the player screen." - Remix button is shown. - Remix button is hidden. - Hide Remix button - Report button is shown. - Report button is hidden. - Hide Report button - Rewards button is shown. - Rewards button is hidden. - Hide Rewards button - Thumbnails in the search term history are shown. - Thumbnails in the search term history are hidden. - Hide search term thumbnails - Seek message is shown. - Seek message is hidden. - Hide seek message - Seek undo message is shown. - Seek undo message is hidden. - Hide seek undo message - Chapter labels next to the timestamp are shown. - Chapter labels next to the timestamp are hidden. - Hide seekbar chapter labels - Video player seekbar is shown. - Video player seekbar is hidden. - Thumbnail seekbar is shown. - Thumbnail seekbar is hidden. - Hide seekbar in video thumbnails - Hide seekbar in video player - Self sponsored cards are shown. - Self sponsored cards are hidden. - Hide self sponsored cards - About menu is shown. - About menu is hidden. - Hide About menu - Accessibility menu is shown. - Accessibility menu is hidden. - Hide Accessibility menu - Account menu is shown. - Account menu is hidden. - Hide Account menu - Autoplay menu is shown. - Autoplay menu is hidden. - Hide Autoplay menu - Billing and payments menu is shown. - Billing and payments menu is hidden. - Hide Billing and payments menu - Captions menu is shown. - Captions menu is hidden. - Hide Captions menu - Connected apps menu is shown. - Connected apps menu is hidden. - Hide Connected apps menu - Data saving menu is shown. - Data saving menu is hidden. - Hide Data saving menu - General menu is shown. - General menu is hidden. - Hide General menu - Manage all history menu is shown. - Manage all history menu is hidden. - Hide Manage all history menu - Live chat menu is shown. - Live chat menu is hidden. - Hide Live chat menu - Notifications menu is shown. - Notifications menu is hidden. - Hide Notifications menu - Background menu is shown. - Background menu is hidden. - Hide Background menu - Watch on TV menu is shown. - Watch on TV menu is hidden. - Hide Watch on TV menu - Family Center menu is shown. - Family Center menu is hidden. - Hide Family Center menu - Try experimental new features menu is shown. - Try experimental new features menu is hidden. - Hide Try experimental new features menu - Privacy menu is shown. - Privacy menu is hidden. - Hide Privacy menu - Purchases and memberships menu is shown. - Purchases and memberships menu is hidden. - Hide Purchases and memberships menu - Hide elements of the YouTube settings menu. - Hide YouTube settings menu - Video quality preferences menu is shown. - Video quality preferences menu is hidden. - Hide Video quality preferences menu - Your data in YouTube menu is shown. - Your data in YouTube menu is hidden. - Hide Your data in YouTube menu - Share button is shown. - Share button is hidden. - Hide Share button - Shop button is shown. - Shop button is hidden. - Hide Shop button - Shopping links are shown. - Shopping links are hidden. - Hide Shopping links - Channel bar is shown. - Channel bar is hidden. - Hide channel bar - Comments button is shown. - Comments button is hidden. - Hide Comments button - Disabled comments button or with label \"0\" is shown. - Disabled comments button or with label \"0\" is hidden. - Hide disabled comments button - Dislike button is shown. - Dislike button is hidden. - Hide Dislike button - "Floating buttons like 'Use this sound' are shown in the Shorts channel tab." - "Floating buttons like 'Use this sound' are hidden in the Shorts channel tab." - Hide floating button - Video link label is shown. - Video link label is hidden. - Hide full video link label - Green screen button is shown. - Green screen button is hidden. - Hide Green screen button - Info panels are shown. - Info panels are hidden. - Hide info panels - Join button is shown. - Join button is hidden. - Hide Join button - Like button is shown. - Like button is hidden. - Hide Like button - Live chat header is shown.\n\nBack button in header will not be hidden. - Live chat header is hidden.\n\nBack button in header will not be hidden. - Hide live chat header - Location button is shown. - Location button is hidden. - Hide location button - Navigation bar is shown. - Navigation bar is hidden. - Hide navigation bar - Paid promotion label is shown. - Paid promotion label is hidden. - Hide paid promotion label - Paused header is shown. - Paused header is hidden. - Hide paused header - Paused overlay buttons are shown. - Paused overlay buttons are hidden. - Hide paused overlay buttons - Button background is shown. - Button background is hidden. - Hide Play & Pause button background - Remix button is shown. - Remix button is hidden. - Hide Remix button - Save music button is shown. - Save music button is hidden. - Hide Save music button - Search suggestions button is shown. - Search suggestions button is hidden. - Hide search suggestions button - Share button is shown. - Share button is hidden. - Hide Share button - Shown in channel. - "Hidden in channel. - -Info: -• Only shelves with the Shorts header on the home tab are hidden." - Hide in channel - Shown in watch history. - Hidden in watch history. - Hide in watch history - Shown in home feed and related videos. - Hidden in home feed and related videos. - Hide in home feed and related videos - Shown in search results. - Hidden in search results. - Hide in search results - Shown in subscriptions feed. - Hidden in subscriptions feed. - Hide in subscriptions feed - "Hides Shorts shelves. - -Side effect: Official headers in search results will be hidden." - Hide Shorts shelves - Shop button is shown. - Shop button is hidden. - Hide Shop button - Shopping button is shown. - Shopping button is hidden. - Hide Shopping button - Sound button is shown. - Sound button is hidden. - Hide sound button - Metadata label is shown. - Metadata label is hidden. - Hide sound metadata label - Stickers are shown. - Stickers are hidden. - Hide stickers - Subscribe button is shown. - Subscribe button is hidden. - Hide Subscribe button - Super Thanks button is shown. - Super Thanks button is hidden. - Hide Super Thanks button - Tagged products are shown. - Tagged products are hidden. - Hide tagged products - Toolbar is shown. - Toolbar is hidden. - Hide toolbar - Trends button is shown. - Trends button is hidden. - Hide Trends button - Use template button is shown. - Use template button is hidden. - Hide Use template button - Use this sound button is shown. - Use this sound button is hidden. - Hide Use this sound button - Title is shown. - Title is hidden. - Hide video title - Show more button is shown. - Show more button is hidden. - Hide Show more button - Snack bar is shown. - Snack bar is hidden. - Hide snack bar - Start trial button is shown. - Start trial button is hidden. - Hide Start trial button - Subscriptions carousel is shown. - Subscriptions carousel is hidden. - Hide subscriptions carousel - Suggested actions are shown. - Suggested actions are hidden. - Hide suggested actions - "This setting has been deprecated. - -Instead, use the 'Settings → Autoplay → Autoplay next video' setting. - -Note: -• If you have any issues with 'Suggested video end screen', try restarting the app." - Suggested video end screen is shown. - "Suggested video end screen is hidden when autoplay is turned off. - -Autoplay can be changed in YouTube settings: -Settings → Autoplay → Autoplay next video" - Hide suggested video end screen - Thanks button is shown. - Thanks button is hidden. - Hide Thanks button - Ticket shelves are shown. - Ticket shelves are hidden. - Hide ticket shelves - Timestamp is shown. - Timestamp is hidden. - Hide timestamp - Timed reactions are shown. - Timed reactions are hidden. - Hide timed reactions - Cast button is shown. - Cast button is hidden. - Hide Cast button - Create button is shown. - Create button is hidden. - Hide Create button - Notifications button is shown. - Notifications button is hidden. - Hide Notifications button - Transcript section is shown. - Transcript section is hidden. - Hide Transcript section - Video ads are shown. - Video ads are hidden. - Hide video ads - "Home / Subscription / Search results are filtered to hide videos with views less or greater than a specified number. - -Limitations: -• Shorts cannot be hidden. -• Videos with 0 views are not filtered." - About view count filtering - Videos in home feed are not filtered. - Videos in home feed are filtered. - Hide home videos by views - Search results are not filtered. - Search results are filtered. - Hide search results by views - Videos in subscriptions feed are not filtered. - Videos in subscriptions feed are filtered. - Hide subscription videos by views - Hide recommended videos with less than a specified number of views.\n\nKnown issue: Videos with 0 views are not filtered. - Hide recommended videos by views - Videos with views greater than this number will be hidden. - Greater than views - Videos with views less than this number will be hidden. - Less than views - K -> 1 000\nM -> 1 000 000\nB -> 1 000 000 000\nviews -> views - Specify your language template for the number of views shown under each video in the user interface. Each key (a letter/word in your language) -> value (meaning of the key) must be on a new line. Keys go before "->" sign. If you change the app or system language you need to reset this setting.\n\nExamples:\nEnglish: 10K views = K -> 1000, views -> views\nSpanish: 10 K vistas = K -> 1000, vistas -> views - View keys - View products banner is shown. - View products banner is hidden. - Hide view products banner - Voice search button is shown. - Voice search button is hidden. - Hide voice search button - Web search results are shown. - Web search results are hidden. - Hide web search results - YouTube Doodles are shown. - YouTube Doodles are hidden. - Hide YouTube Doodles - "YouTube Doodles show up a few days each year. - -If a YouTube Doodle is currently showing in your region and this setting is on, the filter bar below the search bar will also be hidden." - Zoom overlay is shown. - Zoom overlay is hidden. - Hide zoom overlay - Afn Blue - Afn Red - Custom - Stock - MMT - MMT Blue - MMT Green - MMT Orange - MMT Pink - MMT Turquoise - MMT Yellow - Revancify Blue - Revancify Red - Revancify Yellow - Vanced Black - Vanced Light - Xisr Yellow - YouTube - Keeps landscape mode when turning the screen off and on in fullscreen. - The amount of milliseconds the landscape mode is forced after the screen in turned on. - Keep landscape mode timeout - Keep landscape mode - Stock - Double-tap action is disabled. - "Double-tap action is enabled. - -• Double-tap to change the minimized video to a larger size. -• Double-tap once more to change to the original size." - Enable double-tap action - Drag and drop is disabled. - Drag and drop is enabled. - Enable drag and drop - Expand and close buttons are shown. - Buttons are hidden.\n(swipe miniplayer to expand or close) - Hide expand and close buttons - Skip forward and back are shown. - Skip forward and back are hidden. - Hide skip forward and back buttons - Subtexts are shown. - Subtexts are hidden. - Hide subtexts - Miniplayer overlay opacity must be between 0-100. - Opacity value between 0-100, where 0 is transparent. - Overlay opacity - Original - Phone - Tablet - Modern 1 - Modern 2 - Modern 3 - Miniplayer type - Overlay buttons - "Tap to toggle always repeat states. -Tap and hold to toggle pause after repeat states." - Show always repeat button - "Tap to copy video URL. -Tap and hold to copy video URL with timestamp." - "Tap to copy video URL with timestamp. -Tap and hold to copy video timestamp." - Show copy timestamp URL button - Show copy video URL button - Tap to launch external downloader. - Show external download button - Tap to mute volume of the current video. Tap again to unmute. - Show mute volume button - Tap and hold to change button state. - Playback speed reset: %sx. - "Tap to open speed dialog. -Tap and hold to reset playback speed to 1.0x. Tap and hold again to reset back to default speed." - Show speed dialog button - "Tap to generate a playlist of all videos from channel from oldest to newest. -Tap and hold to undo." - Show time-ordered playlist button - "Tap to open whitelist dialog. -Tap and hold to open whitelist setting dialog. - Show whitelist button - If shown, the native playlist download button opens the native in-app downloader. - Native playlist download button is always shown, and in public playlists, it opens your external downloader. - Override playlist download button - Native video download button opens the native in-app downloader. - Native video download button opens your external downloader. - Override video download button - YouTube Music is required to override button action. Tap here to download YouTube Music. - Prerequisite - YouTube Music button opens the native app. - YouTube Music button opens the RVX Music. - Override YouTube Music button - Excluded - Included - Normal - Action buttons - Additional settings - Animation / Feedback - Download button - Experimental Flags - Image region restrictions - Import / Export as file - Import / Export as text - Keyword filter - Others - Overlay buttons - Patch information - Quick actions - Recommended video - Shorts shelves - Suggested actions - Tool used - View count filter - Hide or show elements in account menu and You tab. - Account menu - Hide or show action buttons under videos. - Action buttons - Ads - Alternative thumbnails - Disable Ambient mode or bypass Ambient mode restrictions. - Ambient mode - Hide or show the category bar in the feed, search, and related videos. - Category bar - Hide or show components of the channel bar under videos. - Channel bar - Hide or show components in the channel profile. - Channel profile - Hide or show comments section components. - Comments - Hide or show community posts in the feed and channel. - Community posts - Hide components using custom filters. - Custom filter - Hide or show components of the flyout menu in the feed. - Flyout menu - Feed - Hide or change components related to fullscreen. - Fullscreen - General - Disable or enable haptic feedback. - Haptic feedback - Overrides the click action of in-app buttons. - Hook buttons - Import or export settings. - Import / Export settings - Change the style of the in app minimized player. - Miniplayer - Miscellaneous - Hide or show navigation bar section components. - Navigation bar - Information about applied patches. - Patch information - Hide or show buttons in the video player. - Player buttons - Hide or change components of the flyout menu in the video player. - Flyout menu - Player - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Customize the seekbar components. - Seekbar - Hide elements of the YouTube settings menu. - Settings menu - Hide or show components in the Shorts player. - Shorts player - Shorts - Spoof the streaming data to prevent playback issues. - Spoof streaming data - Swipe controls - Hide or change components located on the toolbar, such as the search bar, toolbar buttons, and header. - Toolbar - Hide or show video description components. - Video description - Hide videos by keywords or views. - Video filter - Video - Change settings related with watch history. - Watch history - Quick actions top margin must be between 0-32. - Configure the spacing from the seekbar to the quick action container, between 0-32. - Quick actions top margin - "Forcefully rejects the software AV1 codec response. -A different codec will be applied after about 20 seconds of buffering." - Reject software AV1 codec response - Fallback process causes about 20 seconds of buffering. - Offset - Playback speed changes only apply to the current video. - Playback speed changes apply to all videos. - Remember playback speed changes - A toast will not be shown when changing the default playback speed. - A toast will be shown when changing the default playback speed. - Show a toast - Changing default speed to %s. - Quality changes only apply to the current video. - Quality changes apply to all videos. - Remember video quality changes - A toast will not be shown when changing the default video quality. - A toast will be shown when changing the default video quality. - Show a toast - Changing default mobile data quality to %s. - Failed to set video quality. - Changing default Wi-Fi quality to %s. - "Removes the viewer discretion dialog. -This does not bypass the age restriction. It just accepts it automatically." - Remove viewer discretion dialog - Replaces the software AV1 codec with the VP9 codec. - Replace software AV1 codec - Channel handle is used. - Channel name is used. - Replace channel handle - Tap to show the remaining time. - Tap to open playback speed or video quality flyout menu. - Replace timestamp action - Replaces the Create button with the Settings button. - Replace Create button - "Tap to open YouTube settings. -Tap and hold to open RVX settings." - "Tap to open RVX settings. -Tap and hold to open YouTube settings." - Action type to assign to button - Seekbar thumbnails will appear in fullscreen. - Seekbar thumbnails will appear above the seekbar. - Restore old seekbar thumbnails - Old video quality menu is not shown. - Old video quality menu is shown. - Restore old video quality menu - @handle (Username) - Display format - Username (@handle) - Username - Handle is used. - Username is used. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - About - Dislike data is provided by the Return YouTube Dislike API. Tap here to learn more. - ReturnYouTubeDislike.com - Like button styled for best appearance. - Like button styled for minimum width. - Compact Like button - Dislikes shown as a number. - Dislikes shown as a percentage. - Dislikes as percentage - Dislikes are not shown. - Dislikes are shown. - Enable Return YouTube Dislike - Estimated likes are hidden. - Estimated likes are shown. - Show estimated likes - Dislikes unavailable (client API limit reached). - Dislikes unavailable (status %d). - Dislikes temporarily unavailable (API timed out). - Dislikes unavailable (%s). - Reload video to vote using Return YouTube Dislike - Dislikes hidden on Shorts. - Dislikes shown on Shorts. - "Dislikes shown on Shorts. - -Limitation: Dislikes may not appear if the user is not logged in or in incognito mode." - Show dislikes on Shorts - Toast is not shown if Return YouTube Dislike is unavailable. - Toast is shown if Return YouTube Dislike is unavailable. - Show a toast if API is unavailable - Hidden - Removes tracking query parameters from the URLs when sharing links. - Sanitize sharing links - "Phrases like '#', 'Fundraiser', 'Shop' and 'products' were shown from the video subtitles." - "Phrases like '#', 'Fundraiser', 'Shop' and 'products' were hidden from the video subtitles." - Sanitize video subtitle - About - sponsor.ajay.app - Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms. - API URL changed. - API URL is invalid. - API URL reset. - Appearance - Color changed. - Color: - Invalid color code. - Color reset. - Creating new segments - Change segment behavior - Automatically hide skip button - Skip button displayed for entire segment. - Skip button hides after several seconds. - Use compact skip button - Skip button styled for best appearance. - Skip button styled for minimum width. - Show create new segment button - Create new segment button is not shown. - Create new segment button is shown. - Enable SponsorBlock - SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos. - Show voting button - Segment voting button is not shown. - Segment voting button is shown. - General - Adjust new segment step - Value must be a positive number. - Number of milliseconds the time adjustment buttons move when creating new segments. - Change API URL - The address SponsorBlock uses to make calls to the server. - Minimum segment duration - Invalid time duration. - Segments shorter than this value (in seconds) will not be shown or skipped. - Enable skip count tracking - Skip count tracking is not enabled. - Lets the SponsorBlock leaderboard know how much time is saved. A message is sent to the leaderboard each time a segment is skipped. - Show a toast when skipping automatically - Toast is not shown. Tap here to see an example. - Toast is shown when a segment is automatically skipped. Tap here to see an example. - Show video length without segments - Full video length shown. - Video length minus the combined segment length is shown in parentheses next to the full video length. - Your private user id - Private user id must be at least 30 characters long. - This should be kept private. This is like a password and should not be shared with anyone. If someone has this, they can impersonate you. - Already read - Read the SponsorBlock guidelines before creating new segments. - Show me - Follow the guidelines - Guidelines contain rules and tips for creating new segments. - View guidelines - Adjust: Mark Start and End Time for segment - Choose the segment category - Verify the Segment - The segment lasts from %1$02d:%2$02d to %3$02d:%4$02d (%5$d minutes %6$02d seconds)\nIs it ready to submit? - The segment is from\n\n%1$s\nto\n%2$s\n\n(%3$s)\n\nReady to submit? - Are the times correct? - Category is disabled in settings. Enable category to submit. - Edit the Segment - Do you want to edit the timing for the start or end of the segment? - Invalid time given. - Edit timing of segment manually - Forward by Specified Time (Default: 150ms) - Set %s as the start or end of a new segment? - end - Mark two locations on the time bar first. - start - now - Preview the segment, and ensure it skips smoothly. - Publish Created Segment - Rewind by Specified Time (Default: 150ms) - Start must be before the end. - Time the segment ends at - Time the segment begins at - New SponsorBlock segment - Reset - Reset color - Filler Tangent / Jokes - Tangential scenes added only for filler or humor that are not required to understand the main content of the video. Does not include segments providing context or background details. - Highlight - The part of the video that most people are looking for. - Interaction Reminder (Subscribe) - A short reminder to like, subscribe, or follow them in the middle of content. If it is long or about something specific, it should instead be under self promotion. - Intermission / Intro Animation - An interval without actual content. Could be a pause, static frame, or repeating animation. Does not include transitions containing information. - Music: Non-Music Section - Only for use in music videos. Sections of music videos without music, that aren\'t already covered by another category. - Endcards / Credits - Credits or when the YouTube endcards appear. Not for conclusions with information. - Preview / Recap / Hook - Collection of clips that show what is coming up or what happened in the video or in other videos of a series, where all information is repeated elsewhere. - Unpaid / Self Promotion - Similar to Sponsor, except for unpaid or self promotion. Includes sections about merchandise, donations, or information about who they collaborated with. - Sponsor - Paid promotion, paid referrals, and direct advertisements. Not for self-promotion or free shout-outs to causes / creators / websites / products they like. - Copy - Failed to export: %s. - Import / Export settings - Your SponsorBlock JSON configuration that can be imported / exported to ReVanced Extended and other SponsorBlock platforms. - Your SponsorBlock JSON configuration that can be imported / exported to ReVanced Extended and other SponsorBlock platforms. This includes your private user id. Be sure to share this wisely. - Failed to import: %s. - Settings imported successfully. - Your settings contain a private SponsorBlock userid.\n\nYour user id is like a password and it should never be shared.\n - Do not show again - Settings copied to clipboard. - Skip automatically - Skip automatically once - Skip - Highlight - Skip filler - Skip to highlight - Skip interact - Skip intro - Skip intermission - Skip intermission - Skip non-music - Skip outro - Skip preview - Skip recap - Skip preview - Skip promo - Skip sponsor - Skip segment - Disable - Show in seek bar - Show a skip button - Skipped filler. - Skipped to highlight. - Skipped annoying reminder. - Skipped intro. - Skipped intermission. - Skipped intermission. - Skipped multiple segments. - Skipped a non-music section. - Skipped outro. - Skipped preview. - Skipped recap. - Skipped preview. - Skipped self promotion. - Skipped sponsor. - Skipped unsubmitted segment. - SponsorBlock temporarily unavailable. - SponsorBlock temporarily unavailable (status %d). - SponsorBlock temporarily unavailable (API timed out). - Stats - Stats temporarily unavailable (API is down). - Loading... - Your reputation is <b>%.2f</b> - You\'ve saved people from <b>%s</b> segments - %1$s hours %2$s minutes - %1$s minutes %2$s seconds - %s seconds - That\'s <b>%s</b> of their lives.<br>Tap here to see the leaderboard. - Tap here to see the global stats and top contributors. - SponsorBlock leaderboard - SponsorBlock is disabled. - You\'ve skipped <b>%s</b> segments - Reset skipped segments counter? - That\'s <b>%s</b>. - You\'ve created <b>%s</b> segments - Tap here to view your segments. - Your username: <b>%s</b> - Tap here to change your username - Unable to change username: Status: %1$d %2$s. - Username successfully changed. - Can\'t submit the segment.\nAlready exists. - Can\'t submit the segment: %s. - Unable to submit segment: %s. - Unable to submit segment.\nRate Limited (too many from the same user or IP). - SponsorBlock is temporarily down. - Unable to submit segment (status: %1$d %2$s). - Segment submitted successfully. - Toast is not shown if SponsorBlock is unavailable. - Toast is shown if SponsorBlock is unavailable. - Show a toast if API is unavailable - Change category - Downvote - Unable to vote for segment: %s. - Unable to vote for segment (API timed out). - Unable to vote for segment (status: %1$d %2$s). - There are no segments to vote for. - Upvote - Settings copied to clipboard. - Timestamp copied to clipboard. (%s) - URL copied to clipboard. - URL with timestamp copied to clipboard. - Original - Thumbs up - Thumbs up (Cairo) - Heart - Heart (Tint) - Hidden - Double-tap animation - Meta panel bottom margin must be between 0-64. - Configure the spacing from the seekbar to the meta panel, between 0-64. - Meta panel bottom margin - Height percentage must be between 0-100 (%). - Configure the height percentage of the empty space left when the navigation bar is hidden, between 0 and 100 (%). - Height percentage of empty space - Press and hold the timestamp to change the Shorts repeat status. - Timestamp long press action - "Shows the video title section in fullscreen. - -Limitation: Video title disappears when clicked." - Show video title section - If autoplay is enabled, the next video will play after the countdown ends. - If autoplay is enabled, the next video will play immediately. - Skip autoplay countdown - "Skips the preloaded buffer at the start of videos to immediately apply the default video quality. - -Info: -• When the video starts, there is a delay of approximately 0.3 seconds. -• Does not apply to HDR videos, live stream videos, or videos shorter than 15 seconds." - Skip preloaded buffer - Toast is not shown. - Toast is shown. - Show a toast when skipping - Turning on this setting may cause video playback issues. - Skipped preloaded buffer. - Speed overlay value must be between 0-8.0. - Speed overlay value between 0-8.0. - Speed overlay value - "Spoofing the client version to the old version. - -• This will change the appearance of the app, but unknown side effects may occur. -• If later turned off, the old UI may remain until clear the app data." - Version not spoofed - Version spoofed - 17.33.42 - Restore old UI layout - 17.41.37 - Restore old playlist shelf - 18.05.40 - Restore old comment input box - 18.17.43 - Restore old player flyout panel - 18.33.40 - Restore old Shorts action bar - 18.38.45 - Restore old default video quality behavior - 18.48.39 - Disable views and likes from being updated in real time - 19.13.37 - Restore old style Rolling number animations - Spoof app version target - Type the spoof app version target. - Edit spoof app version - Spoof app version - "App version will be spoofed to an older version of YouTube. - -This will change the appearance and features of the app, but unknown side effects may occur. - -If later turned off, it is recommended to clear the app data to prevent UI bugs." - "Spoofs the device dimensions to the maximum value. -High quality may be unlocked on some videos that require high device dimensions, but not all videos." - Spoof device dimensions - iOS video codec is AVC (H.264), VP9, or AV1. - iOS video codec is AVC (H.264). - Force iOS AVC (H.264) - "Enabling this might improve battery life and fix playback stuttering. - -AVC (H.264) has a maximum resolution of 1080p, and video playback will use more internet data than VP9 or AV1." - "• Audio track menu is missing. -• Stable volume is not available." - "• Audio track menu is missing. -• Stable volume is not available." - "• Movies or paid videos may not play. -• Livestreams start from the beginning. -• Videos may end 1 second early. -• No opus audio codec." - Spoofing side effects - • Video may not play. - Client used to fetch streaming data is hidden in Stats for nerds. - Client used to fetch streaming data is shown in Stats for nerds. - Show in Stats for nerds - "Streaming data is not spoofed. Video playback may not work." - Streaming data is spoofed. - Spoof streaming data - Android - Android TV - Android VR - iOS - Default client - Turning off this setting may cause video playback issues. - Brightness swipe sensitivity must be between 1-1000 (%). - Configure the minimum distance for brightness swiping between 1 and 1000 (%).\nThe shorter the minimum distance, the faster the brightness level changes. - Brightness swipe sensitivity - Swipe gestures are disabled in Lock screen mode. - Swipe gestures are enabled in Lock screen mode. - Swipe gestures in Lock screen mode - Auto - The amount of threshold for swipe to occur. - Swipe magnitude threshold - The visibility of swipe overlay background. - Swipe background visibility - Swipeable area size cannot be more than 50. - Percentage of swipeable screen area.\n\nNote: This will also change the size of the screen area for the double-tap-to-seek gesture. - Swipe overlay screen size - The text size for swipe overlay. - Swipe overlay text size - The amount of milliseconds the overlay is visible. - Swipe overlay timeout - Volume swipe sensitivity must be between 1-1000 (%). - Configure the minimum distance for volume swiping between 1 and 1000 (%).\n\nThe shorter the minimum distance, the faster the volume level changes.\n\nRecommended volume swipe sensitivity is 100% at 15-volume steps and 10% at 150-volume steps. - Volume swipe sensitivity - "Swaps the positions of the Create button with the Notifications button by spoofing device information. - -• The device may need to be rebooted for a change of this setting to take effect. -• Disabling this setting loads more ads from the server side. -• You should disable this setting to make video ads visible." - Create button is not switched with Notifications button. - "Create button is switched with Notifications button. - -Note: Enabling this also forcibly hides video ads." - Swap Create and Notifications buttons - "Disabling this might load more ads from the server. - -Also, ads will no longer be blocked in Shorts. - -If this setting do not take effect, try switching to Incognito mode." - Stock - RVX Music - %s is not installed. Please install it. - Package name of installed RVX Music. - RVX Music package name - • Watch history is blocked. - "• Follows the watch history settings of Google account. -• Watch history may not work due to DNS or VPN." - • Follows the watch history settings of Google account. - Status of watch history - Click to open the YouTube watch history management. - Manage all history - Original - Replace domain - Block watch history - Watch history type - Failed to add channel \'%1$s\' to the %2$s whitelist. - Channel \'%1$s\' was added to the %2$s whitelist. - There are no whitelisted channels. - Not added to whitelist. - Failed to load channel information. - Added to whitelist. - Playback speed - Remove channel \'%1$s\' from %2$s whitelist? - Failed to remove channel \'%1$s\' from the %2$s whitelist. - Channel \'%1$s\' was removed from the %2$s whitelist. - Check or remove the list of channels added to the whitelist. - Channel whitelist - SponsorBlock - diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_remix_filled_white_24.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_remix_filled_white_24.webp deleted file mode 100644 index a14d4adc6..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_remix_filled_white_24.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_remix_filled_white_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_remix_filled_white_shadowed.webp deleted file mode 100644 index a14d4adc6..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_remix_filled_white_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_comment_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_comment_shadowed.webp deleted file mode 100644 index 7f272fe24..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_comment_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_dislike_off_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_dislike_off_shadowed.webp deleted file mode 100644 index 6ee3b5eb5..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_dislike_off_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_dislike_on_32c.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_dislike_on_32c.webp deleted file mode 100644 index 3aaf383d1..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_dislike_on_32c.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_dislike_on_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_dislike_on_shadowed.webp deleted file mode 100644 index 3aaf383d1..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_dislike_on_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_like_off_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_like_off_shadowed.webp deleted file mode 100644 index 8025cd0ab..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_like_off_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_like_on_32c.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_like_on_32c.webp deleted file mode 100644 index 4c889a255..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_like_on_32c.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_like_on_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_like_on_shadowed.webp deleted file mode 100644 index 4c889a255..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_like_on_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_share_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_share_shadowed.webp deleted file mode 100644 index 59853c62f..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-hdpi/ic_right_share_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_remix_filled_white_24.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_remix_filled_white_24.webp deleted file mode 100644 index 970ddeaeb..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_remix_filled_white_24.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_remix_filled_white_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_remix_filled_white_shadowed.webp deleted file mode 100644 index 970ddeaeb..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_remix_filled_white_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_comment_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_comment_shadowed.webp deleted file mode 100644 index 40b471ab4..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_comment_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_dislike_off_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_dislike_off_shadowed.webp deleted file mode 100644 index 5a839a028..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_dislike_off_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_dislike_on_32c.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_dislike_on_32c.webp deleted file mode 100644 index 93c5fb715..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_dislike_on_32c.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_dislike_on_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_dislike_on_shadowed.webp deleted file mode 100644 index 93c5fb715..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_dislike_on_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_like_off_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_like_off_shadowed.webp deleted file mode 100644 index fcba929e5..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_like_off_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_like_on_32c.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_like_on_32c.webp deleted file mode 100644 index a2149ca24..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_like_on_32c.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_like_on_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_like_on_shadowed.webp deleted file mode 100644 index a2149ca24..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_like_on_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_share_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_share_shadowed.webp deleted file mode 100644 index a75465144..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-mdpi/ic_right_share_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_remix_filled_white_24.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_remix_filled_white_24.webp deleted file mode 100644 index 335520b11..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_remix_filled_white_24.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_remix_filled_white_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_remix_filled_white_shadowed.webp deleted file mode 100644 index 335520b11..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_remix_filled_white_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_comment_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_comment_shadowed.webp deleted file mode 100644 index 5fe1f5d51..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_comment_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_dislike_off_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_dislike_off_shadowed.webp deleted file mode 100644 index 83b9c1da8..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_dislike_off_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_dislike_on_32c.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_dislike_on_32c.webp deleted file mode 100644 index 6c4a46102..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_dislike_on_32c.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_dislike_on_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_dislike_on_shadowed.webp deleted file mode 100644 index 6c4a46102..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_dislike_on_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_like_off_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_like_off_shadowed.webp deleted file mode 100644 index e59653ed1..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_like_off_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_like_on_32c.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_like_on_32c.webp deleted file mode 100644 index 3db533ee9..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_like_on_32c.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_like_on_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_like_on_shadowed.webp deleted file mode 100644 index 3db533ee9..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_like_on_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_share_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_share_shadowed.webp deleted file mode 100644 index 800cce93e..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xhdpi/ic_right_share_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_remix_filled_white_24.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_remix_filled_white_24.webp deleted file mode 100644 index c2ee4badf..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_remix_filled_white_24.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_remix_filled_white_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_remix_filled_white_shadowed.webp deleted file mode 100644 index c2ee4badf..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_remix_filled_white_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_comment_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_comment_shadowed.webp deleted file mode 100644 index b58b8c935..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_comment_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_dislike_off_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_dislike_off_shadowed.webp deleted file mode 100644 index c2f8f3fed..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_dislike_off_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_dislike_on_32c.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_dislike_on_32c.webp deleted file mode 100644 index 86d7f8b2f..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_dislike_on_32c.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_dislike_on_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_dislike_on_shadowed.webp deleted file mode 100644 index 86d7f8b2f..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_dislike_on_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_like_off_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_like_off_shadowed.webp deleted file mode 100644 index 52145c052..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_like_off_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_like_on_32c.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_like_on_32c.webp deleted file mode 100644 index d56e5f992..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_like_on_32c.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_like_on_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_like_on_shadowed.webp deleted file mode 100644 index d56e5f992..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_like_on_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_share_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_share_shadowed.webp deleted file mode 100644 index cb1dfdd56..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxhdpi/ic_right_share_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_remix_filled_white_24.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_remix_filled_white_24.webp deleted file mode 100644 index 09d18ebd5..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_remix_filled_white_24.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_remix_filled_white_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_remix_filled_white_shadowed.webp deleted file mode 100644 index 09d18ebd5..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_remix_filled_white_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_comment_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_comment_shadowed.webp deleted file mode 100644 index b44137aad..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_comment_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_dislike_off_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_dislike_off_shadowed.webp deleted file mode 100644 index 2a48b95d3..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_dislike_off_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_dislike_on_32c.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_dislike_on_32c.webp deleted file mode 100644 index 56d8f864b..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_dislike_on_32c.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_dislike_on_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_dislike_on_shadowed.webp deleted file mode 100644 index 56d8f864b..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_dislike_on_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_like_off_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_like_off_shadowed.webp deleted file mode 100644 index a5859482f..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_like_off_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_like_on_32c.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_like_on_32c.webp deleted file mode 100644 index c2b56b098..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_like_on_32c.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_like_on_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_like_on_shadowed.webp deleted file mode 100644 index c2b56b098..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_like_on_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_share_shadowed.webp b/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_share_shadowed.webp deleted file mode 100644 index 5a9476c23..000000000 Binary files a/src/main/resources/youtube/shorts/actionbuttons/cairo/drawable-xxxhdpi/ic_right_share_shadowed.webp and /dev/null differ diff --git a/src/main/resources/youtube/translations/ar/missing_strings.xml b/src/main/resources/youtube/translations/ar/missing_strings.xml deleted file mode 100644 index 43788e232..000000000 --- a/src/main/resources/youtube/translations/ar/missing_strings.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - Don\'t show again - Courses / Learning - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - AI-generated video summary section is shown. - AI-generated video summary section is hidden. - Hide AI-generated video summary section - MMT Blue - MMT Green - MMT Orange - MMT Pink - MMT Turquoise - MMT Yellow - Revancify Yellow - Vanced Black - Vanced Light - Xisr Yellow - Adjust: Mark Start and End Time for segment - Verify the Segment - Edit the Segment - Forward by Specified Time (Default: 150ms) - Publish Created Segment - Rewind by Specified Time (Default: 150ms) - diff --git a/src/main/resources/youtube/translations/ar/strings.xml b/src/main/resources/youtube/translations/ar/strings.xml deleted file mode 100644 index 5b4ffb59e..000000000 --- a/src/main/resources/youtube/translations/ar/strings.xml +++ /dev/null @@ -1,1716 +0,0 @@ - - - تمكين عناصر التحكم في إمكانية الوصول لمشغل الفيديو؟ - تم تعديل عناصر التحكم الخاصة بك لأن خدمة إمكانية الوصول قيد التشغيل. - استمرار - "ليس لدى GmsCore إذن للتشغيل في الخلفية. - -اتبع دليل 'لا تقتل تطبيقي!' الخاص بهاتفك، وقم بتطبيق التعليمات على تثبيت GmsCore الخاص بك. - -وهذا مطلوب لكي يعمل التطبيق." - "يجب تعطيل تحسينات بطارية GmsCore لمنع حدوث مشكلات. - -اضغط على زر المتابعة وقم بتعطيل تحسينات البطارية." - فتح الموقع - الإجراء مطلوب - تمكين المراسلة السحابية لتلقي الإشعارات. - فتح GmsCore - لم يتم تثبيت GmsCore. قم بتثبيته. - "يوفر DeArrow مُصغَّرات فيديو من مصادر جماعية لمقاطع فيديو YouTube. غالبًا ما تكون مُصغَّرات فيديو هذه ذات صلة أكثر من تلك التي يقدمها موقع YouTube. - -في حالة التمكين، سيتم إرسال عناوين URL للفيديو إلى خادم API ولن يتم إرسال أي بيانات أخرى. إذا لم يكن الفيديو يحتوي على مُصغَّرات لـ DeArrow، فسيتم عرض اللقطات الأصلية أو الثابتة. - -انقر هنا لمعرفة المزيد عن DeArrow." - DeArrow - URL الخاص بـ DeArrow API غير صالح. - The URL of The DeArrow Thumbnail Cache Endpoint. - DeArrow API Endpoint - لا يتم عرض ملاحظة إذا كان DeArrow غير متوفر. - يتم عرض ملاحظة إذا كان DeArrow غير متوفر. - عرض ملاحظة إذا كان API غير متوفر - DeArrow غير متوفر مؤقتًا. (رمز الحالة: %s) - DeArrow غير متوفر مؤقتًا. - علامة تبويب الصفحة الرئيسية - علامة التبويب أنت - المصّغرات الأصلية - DeArrow & المصّغرات الأصلية - DeArrow & اللقطات الثابتة - اللقطات الثابتة - قوائم تشغيل المشغل، التوصيات - نتائج البحث - لقطات الفيديو الثابتة - يتم التقاط اللقطات الثابتة من بداية أو منتصف أو نهاية كل فيديو. هذه الصور مدمجة في YouTube ولا يتم استخدام أي واجهة برمجة تطبيقات خارجية. - لقطات الفيديو الثابتة - استخدام لقطات الفيديو الثابتة بجودة عالية. - استخدام اللقطات متوسطة الجودة. سيتم تحميل المُصغَّرات بشكل أسرع، ولكن البث المباشر أو المقاطع التي لم يتم إصدارها أو القديمة جدًا قد تعرض مُصغَّرات فارغة. - استخدام اللقطات الثابتة السريعة - بداية الفيديو - منتصف الفيديو - نهاية الفيديو - وقت الفيديو لأخذ اللقطات الثابتة منه - علامة تبويب الاشتراكات - لا يتم إضافة المعلومات بطابع الوقت. - "يتم إضافة المعلومات بطابع الوقت." - إضافة معلومات طابع الوقت - إضافة سرعة التشغيل. - إضافة جودة الفيديو. - إضافة نوع المعلومات - تم تعطيل الإضاءة السينمائية في وضع توفير شحن البطارية. - تم تمكين الإضاءة السينمائية في وضع توفير شحن البطارية. - تجاوز قيود الإضاءة السينمائية - النطاق الذي سيتم جلب الصور منه.\nملاحظة: أدخل اسم النطاق فقط، أي بدون بادئة \"https\:\/\/\". - النطاق البديل - استخدام مضيف الصور الأصلية.\n\nتمكين هذا يمكن إصلاح الصور المفقودة المحظورة في بعض المناطق. - استخدام مضيف الصورة yt4.ggpht.com. - تجاوز القيود على منطقة الصورة - الأساسي - الجوّال - الجوّال (الحد الأقصى 480 dp) - الجهاز اللوحي - الجهاز اللوحي (الحد الأدنى 600 dp) - تغيير التخطيط - يتم استخدام مفتاح التبديل. - يتم استخدام مفتاح التبديل النصي. - تغيير نوع التبديل - يتم استخدام لوح مشاركة داخل التطبيق. - يتم استخدام لوح مشاركة النظام. - تغيير لوح مشاركة - التشغيل التلقائي - الافتراضي - إيقاف - تكرار - تغيير حالة تكرار Shorts - تصفح القنوات - الافتراضي - اكتشف - ألعاب فيديو - السجلّ - المكتبة - فيديوهات أعجبتني - بث مباشر - أفلام - موسيقى - البحث - Shorts - رياضة - الاشتراكات - المحتوى الرائج - المشاهدة لاحقًا - تغيير صفحة البداية - تتغير صفحة البدء مرة واحدة فقط. - "تتغير صفحة البدء دائمًا. - -التقييد: قد لا يعمل زر الرجوع الذي على شريط الأدوات." - تغيير نوع صفحة البداية - تم تمكين العلامة العامة. - تم تمكين علامة Premium. - تغيير علامة YouTube - قائمة سلاسل منشئ مسار المكونات المراد تصفيتها، مفصولة بسطور جديدة. - فلتر مخصص - تم تعطيل الفلتر المخصص. - تم تمكين الفلتر المخصص. - تمكين الفلتر المخصص - فلتر مخصص غير صالح: %s. - يتم استخدام القائمة المنبثقة بالمظهر القديم. - يتم استخدام مربع الحوار المخصص. - نوع قائمة سرعة التشغيل المخصصة - يجب أن تكون السرعات المخصصة أقل من %sx. - سرعات التشغيل المخصصة غير صالحة. - إضافة أو تغيير سرعات التشغيل المتاحة. - تعديل سرعة التشغيل المخصصة - يجب أن تكون شفافية واجهة المشغل بين 0-100. - قيمة التعتيم بين 0-100، حيث 0 شفافة. - شفافية واجهة المشغل المخصصة - اكتب رمز اللون للون شريط تقدم الفيديو. - قيمة لون شريط تقدم الفيديو المخصصة - لفتح روابط YouTube في RVX، قم بتمكين \'فتح الروابط المدعومة\' وتمكين عناوين الويب المدعومة. - فتح إعدادات التطبيق الافتراضية - سرعة التشغيل الافتراضية - جودة الفيديو الافتراضية على شبكة الجوَّال - جودة الفيديو الافتراضية على شبكة Wi-Fi - يُعطّل وضع الإضاءة السينمائية في ملء الشاشة فقط. - تم تمكين وضع الإضاءة السينمائية في ملء الشاشة. - تم تعطيل وضع الإضاءة السينمائية في ملء الشاشة. - تعطيل وضع الإضاءة السينمائية في ملء الشاشة - تعطيل وضع الإضاءة السينمائية. - تم تمكين وضع الإضاءة السينمائية. - تم تعطيل وضع الإضاءة السينمائية. - تعطيل وضع الإضاءة السينمائية - تم تمكين المقطع الصوتي التلقائي المفروض. - تم تعطيل المقطع الصوتي التلقائي المفروض. - تعطيل المقطع الصوتي التلقائي المفروض - تم تمكين التَّرْجَمَة التلقائية المفروضة. - تم تعطيل التَّرْجَمَة التلقائية المفروضة. - تعطيل التَّرْجَمَة التلقائية المفروضة - تم تمكين لوحات المشغل المنبثقة تلقائيًا. - تم تعطيل لوحات المشغل المنبثقة تلقائيًا. - تعطيل لوحات المشغل المنبثقة - "تم تمكين التبديل التلقائي لقوائم تشغيل التشكيلة عند تمكين التشغيل التلقائي. - -يمكن تغيير التشغيل التلقائي في إعدادات YouTube: -الإعدادات ← التشغيل التلقائي ← تشغيل الفيديو التالي تلقائيًا" - تم تعطيل التبديل التلقائي لقوائم تشغيل التشكيلة. - تعطيل تبديل قوائم تشغيل التشكيلة - سيؤدي تمكين هذه الميزة إلى تعطيل التبديل التلقائي إلى YouTube Mix عند تشغيل الموسيقى أثناء تمكين التشغيل التلقائي. - تم تمكين سرعة التشغيل الافتراضية للبث المباشر. - تم تعطيل سرعة التشغيل الافتراضية للبث المباشر. - تعطيل سرعة التشغيل للبث المباشر - تم تمكين سرعة التشغيل الافتراضية للموسيقى. - "تم تعطيل سرعة التشغيل الافتراضية للموسيقى. - -التقييد: قد لا ينطبق هذا الإعداد على مقاطع الفيديو التي لا تتضمن لافتة \"الاستماع على YouTube Music\"." - تعطيل سرعة التشغيل للموسيقى - تم تمكين لوحة المشاركة. - تم تعطيل لوحة المشاركة. - تعطيل لوحة المشاركة - تم تمكين الاهتزاز. - تم تعطيل الاهتزاز. - تعطيل الاهتزاز عند الضغط على الفصول - تم تمكين الاهتزاز. - تم تعطيل الاهتزاز. - تعطيل ردود الفعل اللمسية الخانقة - تم تمكين الاهتزاز. - تم تعطيل الاهتزاز. - تعطيل الاهتزاز عند الضغط على شريط تقدم الفيديو - تم تمكين الاهتزاز. - تم تعطيل الاهتزاز. - تعطيل الاهتزاز عند التراجع عن التمرير - تم تمكين الاهتزاز. - تم تعطيل الاهتزاز. - تعطيل الاهتزاز عند التكبير - تم تمكين سطوع HDR التلقائي. - تم تعطيل سطوع HDR التلقائي. - تعطيل سطوع HDR التلقائي - تم تمكين فيديو HDR. - تم تعطيل فيديو HDR. - تعطيل فيديو HDR - اتجاه الفيديو يتبع إعدادات ملء الشاشة في الجهاز. - اتجاه الفيديو هو وضع عمودي في ملء الشاشة. - تعطيل الوضع الأفقي - ستتوهج أزرار أعجبني و لم يعجبني عند ذكرها. - لن تتوهج أزرار أعجبني و لم يعجبني عند ذكرها. - تعطيل توهج زر أعجبني ولم يعجبني - "تعطيل بروتوكول QUIC الخاص بـ CronetEngine." - تعطيل بروتوكول QUIC - سيتم استئناف تشغيل مشغل Shorts عند بدء تشغيل التطبيق. - لن يتم استئناف تشغيل مشغل Shorts عند بدء تشغيل التطبيق. - تعطيل استئناف مشغل Shorts - عدد المشاهدات والإعجابات متحركة. - عدد المشاهدات والإعجابات غير متحركة. - تعطيل عدد المشاهدات والإعجابات في الوقت الفعلي - تم تمكين الفصول في شريط التقدم. - تم تعطيل الفصول في شريط التقدم. - تعطيل فصول شريط التقدم - تم تمكين الرسوم المتحركة الفوّارة فوق زر أعجبني. - تم تعطيل الرسوم المتحركة الفوّارة فوق زر أعجبني. - تعطيل الرسوم المتحركة لزر أعجبني - "تعطيل '2x>>' أثناء الضغط باستمرار. - -ملحوظة: -• يؤدي تعطيل تراكب السرعة إلى استعادة سلوك \"Slide to seek\" للتخطيط القديم. - • لا يؤدي تعطيل هذا الإعداد إلى فرض تمكين تراكب السرعة بالقوة." - تعطيل تراكب السرعة - تم تمكين تأثيرات الحركة. - تم تعطيل تأثيرات الحركة. - تعطيل تأثيرات الحركة - "تعطيل التفاعلات التالية عند توسيع وصف الفيديو: - -• انقر للتمرير. -• انقر مع الاستمرار لتحديد النص." - تعطيل تفاعل وصف الفيديو - تم تمكين ترميز VP9. - "تم تعطيل برنامج ترميز VP9. - -• الحد الأقصى للدقة هو 1080P. -• سيستهلك تشغيل الفيديو بيانات إنترنت أكثر من VP9. -• لا يزال برنامج ترميز VP9 مستخدمًا لفيديو HDR." - تعطيل ترميز VP9 - تم تعطيل شريط تقدم Cairo. - "تم تمكين شريط تقدم Cairo. - -التأثير الجانبي: يتم تطبيق سمة Cairo أيضًا على نقاط الإشعارات." - تمكين شريط تقدم Cairo - يملأ تراكب عناصر التحكم الشاشة الكاملة. - لا يملأ تراكب عناصر التحكم الشاشة الكاملة. - تمكين تراكب التحكم المدمج - تم تعطيل سرعة التشغيل المخصصة. - تم تمكين سرعة التشغيل المخصصة. - تمكين سرعة التشغيل المخصصة - تم تعطيل لون شريط تقدم الفيديو المخصص. - تم تمكين لون شريط تقدم الفيديو المخصص. - تمكين لون شريط تقدم الفيديو المخصص - سجلات التصحيح لا تشمل التخزين المؤقت. - سجلات التصحيح تشمل التخزين المؤقت. - تمكين تسجيل تصحيح المخزن المؤقت - تم تعطيل Debug Logs. - تم تمكين Debug Logs. - تمكين سجلات التصحيح - لا تنطبق سرعة التشغيل الافتراضية على Shorts. - تنطبق سرعة التشغيل الافتراضية على Shorts. - تمكين سرعة التشغيل الافتراضية لفيديوهات Shorts - تم تعطيل المتصفح الخارجي. - تم تمكين المتصفح الخارجي. - تمكين المتصفح الخارجي - تم تعطيل شاشة التحميل المتدرجة الملونة. - تم تمكين شاشة التحميل المتدرجة الملونة. - تمكين شاشة التحميل المتدرجة - المسافة بين أزرار التنقل عادية. - المسافة بين أزرار التنقل أضيق. - تمكين أزرار التنقل الضيقة - يتبع سياسة إعادة التوجيه الافتراضية. - تجاوز عمليات إعادة توجيه URL. - تمكين فتح الروابط بشكل مباشر - تمكين ترميز OPUS إذا كانت استجابة المشغل تتضمن برنامج ترميز OPUS. - تمكين ترميز OPUS - لا تقم بحفظ واستعادة السطوع عند الخروج أو الدخول إلى وضع ملء الشاشة. - حفظ واستعادة السطوع عند الخروج أو الدخول إلى وضع ملء الشاشة. - تمكين حفظ واستعادة السطوع - تم تعطيل النقر على شريط الوقت (شريط تقدم الفيديو). - تم تمكين النقر على شريط الوقت (شريط تقدم الفيديو). - تمكين النقر على شريط التقدم - "سيؤدي هذا إلى استعادة المصغرات للبث المباشر الذي لا يحتوي على مصغرات لشريط التقدم. - -قد يكون استخدام بيانات الإنترنت أعلى، وقد يحدث تأخير طفيف في عرض المصغرات لشريط التقدم. - -تعمل هذه الميزة بشكل أفضل مع اتصال إنترنت سريع للغاية." - مصغرات شريط التقدم متوسطة الجودة. - مصغرات شريط التقدم عالية الجودة. - تمكين المصغرات عالية الجودة - تم تعطيل الطابع الزمني. - "تم تمكين الطابع الزمني. - -القيود: -• لا يعمل هذا الإعداد على تمكين الطوابع الزمنية فحسب، بل يسمح أيضًا للمستخدمين بإخفاء واجهة المستخدم من خلال النقر على خلفية المشغل. -• بما أن هذه ميزة في مرحلة التطوير بواسطة Google، فقد يكون التخطيط معطلاً." - تمكين الطوابع الزمنية - تم تعطيل التحكم بمستوى السطوع عن طريق الإيماءة. - تم تمكين التحكم بمستوى السطوع عن طريق الإيماءة. - تمكين التحكم بالسطوع عن طريق إيماءة التمرير - تم تعطيل الاهتزاز. - تم تمكين الاهتزاز. - تمكين ردود الفعل اللمسية - أدنى قيمة لإيماءة السطوع لا تعمل على تنشيط السطوع التلقائي. - أدنى قيمة لإيماءة السطوع تعمل على تنشيط السطوع التلقائي. - تمكين التحكم بالسطوع عن طريق إيماءة التمرير - المس لتنشيط إيماءة التمرير. - المس مع الاستمرار لتنشيط إيماءة التمرير. - الضغط للتمرير - التمرير لأعلى / لأسفل لن يتم تشغيل الفيديو التالي / السابق. - التمرير لأعلى / لأسفل سيتم تشغيل الفيديو التالي / السابق. - تمكين إيماءة التمرير لتغيير الفيديو - تم تعطيل التحكم بمستوى الصوت عن طريق الإيماءة. - تم تمكين التحكم بمستوى الصوت عن طريق الإيماءة. - تمكين التحكم بالصوت عن طريق إيماءة التمرير - شريط التنقل غير شفاف. - شريط التنقل شفاف. - تمكين شريط التنقل الشفاف - تم تعطيل الدخول إلى وضع ملء الشاشة عن طريق تمرير المنطقة السفلية لمشغل الفيديو. - تم تمكين الدخول إلى وضع ملء الشاشة عن طريق تمرير المنطقة السفلية لمشغل الفيديو. - تمكين إيماءات لوح المشاهدة - "سيؤدي تمكين هذا الإعداد إلى تعطيل زر الإعدادات في علامة التبويب أنت. - -في هذه الحالة، يرجى استخدام المسار التالي للوصول إلى الإعدادات: -علامة التبويب أنت ← عرض القناة ← القائمة ← الإعدادات" - تمكين شريط البحث العريض في علامة التبويب أنت - تم تعطيل شريط البحث العريض. - تم تمكين شريط البحث العريض. - تمكين شريط البحث العريض - يخفي شريط البحث العريض علامة YouTube. - لا يخفي شريط البحث العريض علامة YouTube. - تمكين شريط البحث العريض مع العلامة - الوصف - "أدخل عنوان لوحة وصف الفيديو بلغتك. -قد لا يعمل خيار توسيع وصف الفيديو إذا كانت السلسلة المدخلة لا تتطابق مع عنوان لوحة وصف الفيديو." - العنوان في لوحة وصف الفيديو - لا يتم توسيع أوصاف الفيديو تلقائيًا. - يتم توسيع أوصاف الفيديو تلقائيًا. - توسيع وصف الفيديو - هل ترغب في المتابعة؟ - إعادة التعيين إلى القيم الافتراضية. - إعادة التشغيل لتحميل التخطيط بشكل طبيعي - "يوجد خطأ في خادم YouTube يتسبب في إخفاء نص الأرقام المتتالية مثل الإعجابات والمشاهدات وتواريخ التحميل لبعض المستخدمين. - -الحل المؤقت لهذه المشكلة هو تزييف إصدار التطبيق إلى 19.13.37. - -هل تريد تزييف إصدار التطبيق قبل إعادة تشغيل التطبيق؟" - تحديث وإعادة تشغيل - فشل تصدير الإعدادات. - تم تصدير الإعدادات بنجاح. - تصدير الإعدادات إلى ملف. - تصدير الإعدادات - استيراد - نسخ - استيراد أو تصدير الإعدادات كنص. - استيراد / تصدير كنص - فشل استيراد الإعدادات. - إعادة تعيين الإعدادات إلى الافتراضي. - تم استيراد الإعدادات بنجاح. - استيراد الإعدادات من ملف محفوظ. - استيراد الإعدادات - إعادة تعيين - بحث %s - ReVanced Extended - التنزيل الخارجي - غير مثبّت - "لم يتم تثبيت %1$s. -الرجاء تنزيل %2$s من الموقع." - تحذير - %s لم يتم تثبيته. الرجاء تثبيته. - اسم الحزمة لتطبيق التنزيل الخارجي المثبت لديك، مثل YTDLnis. - اسم حزمة تنزيل قائمة التشغيل - اسم الحزمة لتطبيق التنزيل الخارجي المثبت لديك، مثل NewPipe أو YTDLnis. - اسم حزمة تنزيل الفيديو - "سيتم تحويل مقاطع الفيديو إلى وضع ملء الشاشة في المواقف التالية: - -• عند بدء تشغيل الفيديو. -• عندما يتم النقر على الطابع الزمني في التعليقات." - فرض ملء الشاشة - قائمة بأسماء قائمة الحسابات المراد تصفيتها، مفصولة بسطور جديدة. - تعديل فلتر قائمة الحساب - "إخفاء عناصر قائمة الحساب وعلامة التبويب أنت. -قد لا يتم إخفاء بعض المكونات." - إخفاء قائمة الحساب - يتم عرض بطاقات الألبوم. - تم إخفاء بطاقات الألبوم. - إخفاء بطاقات الألبوم - يتم عرض أقسام الأماكن المميزة، الألعاب، والموسيقى. - تم إخفاء أقسام الأماكن المميزة، الألعاب، والموسيقى. - إخفاء قسم الصفات - يتم عرض حاوية عرض التشغيل التلقائي. - تم إخفاء حاوية عرض التشغيل التلقائي. - إخفاء حاوية عرض التشغيل التلقائي - يتم عرض زر زيارة المتجر. - تم إخفاء زر زيارة المتجر. - إخفاء زر زيارة المتجر - "يخفي الرفوف التالية: -• أخبار عاجلة -• متابعة المشاهدة -• اكتشف المزيد من القنوات -• استمع مجددا -• التسوق -• مشاهده مرة أخرى" - إخفاء الرف الدائري - يُعرض في الموجز. - مخفي في الموجز. - إخفاء في الموجز - يُعرض في مقاطع الفيديو ذات الصلة. - مخفي في مقاطع الفيديو ذات الصلة. - إخفاء في مقاطع الفيديو ذات الصلة - يُعرض في نتائج البحث. - مخفي في نتائج البحث. - إخفاء في نتائج البحث - يتم عرض إرشادات القناة. - تم إخفاء إرشادات القناة. - إخفاء إرشادات القناة - يتم عرض رف أعضاء القناة. - تم إخفاء رف أعضاء القناة. - إخفاء رف أعضاء القناة - يتم عرض الروابط في أعلى ملفات القناة الشخصية. - تم إخفاء الروابط في أعلى ملفات القناة الشخصية. - إخفاء روابط ملف تعريف القناة - "Shorts -قوائم التشغيل -المتجر" - قائمة بأسماء علامة تبويب القناة المراد تصفيتها، مفصولة بسطور جديدة. - تصفية علامة تبويب القناة - تم تعطيل فلتر علامة تبويب القناة. - تم تمكين فلتر علامة تبويب القناة. - تمكين فلتر علامة تبويب القناة - يتم عرض علامة الفيديو المائية. - تم إخفاء علامة الفيديو المائية. - إخفاء العلامة المائية للقناة - يتم عرض قسم الفصول. - تم إخفاء قسم الفصول. - إخفاء قسم الفصول - يتم عرض رف الشرائح. - تم إخفاء رف الشرائح. - إخفاء رف الشرائح - يتم عرض زر إنشاء مقطع. - تم إخفاء زر إنشاء مقطع. - إخفاء زر إنشاء مقطع - يتم عرض زر إنشاء Short. - تم إخفاء زر إنشاء Short. - إخفاء زر إنشاء Short - يتم عرض روابط البحث المميزة. - تم إخفاء روابط البحث المميزة. - إخفاء روابط البحث المميزة - يتم عرض زر شكرًا. - تم إخفاء زر شكرًا. - إخفاء زر شكرًا - يتم عرض أزرار الطوابع الزمنية والرموز التعبيرية. - تم إخفاء أزرار الطوابع الزمنية والرموز التعبيرية. - إخفاء أزرار الطابع الزمني والرموز التعبيرية - يتم عرض لافتة تعليقات من الأعضاء. - تم إخفاء لافتة تعليقات من الأعضاء. - إخفاء لافتة تعليقات من الأعضاء - يتم عرض قسم التعليقات في موجز الصفحة الرئيسية. - تم إخفاء قسم التعليقات في موجز الصفحة الرئيسية. - إخفاء قسم التعليقات في موجز الصفحة الرئيسية - يتم عرض قسم التعليقات. - تم إخفاء قسم التعليقات. - إخفاء قسم التعليقات - يُعرض في القناة. - مخفي في القناة. - إخفاء في القناة - يُعرض في موجز الصفحة الرئيسية والفيديوهات ذات الصلة. - مخفي في موجز الصفحة الرئيسية والفيديوهات ذات الصلة. - إخفاء في موجز الصفحة الرئيسية والفيديوهات ذات الصلة - يُعرض في موجز الاشتراكات. - مخفي في موجز الاشتراكات. - إخفاء في موجز الاشتراكات - يتم عرض قسم كيفية إنشاء هذا المحتوى. - تم إخفاء قسم كيفية إنشاء هذا المحتوى. - إخفاء قسم المحتوى - يتم عرض مربع التمويل الجماعي. - تم إخفاء مربع التمويل الجماعي. - إخفاء مربع التمويل الجماعي - يتم عرض فلتر واجهة النقر المزدوج. - تم إخفاء فلتر واجهة النقر المزدوج. - إخفاء فلتر واجهة النقر المزدوج - يتم عرض زر التنزيل. - تم إخفاء زر التنزيل. - إخفاء زر التنزيل - يتم عرض بطاقات شاشة النهاية. - تم إخفاء بطاقات شاشة النهاية. - إخفاء بطاقات شاشة النهاية - يتم عرض الرقائق القابلة للتوسيع. - تم إخفاء الرقائق القابلة للتوسيع. - إخفاء الشريحة القابلة للتوسع تحت مقاطع الفيديو - يتم عرض الرفوف القابلة للتوسع. - تم إخفاء الرفوف القابلة للتوسع. - إخفاء الرفوف القابلة للتوسع - يتم عرض زر التَرْجَمَة. - تم إخفاء زر التَرْجَمَة. - إخفاء زر التَرْجَمَة في الموجز - قائمة بأسماء القائمة المنبثقة المراد تصفيتها، مفصولة بسطور جديدة. - تصفية القائمة المنبثقة بالموجز - تم تعطيل فلتر القائمة المنبثقة بالموجز. - تم تمكين فلتر القائمة المنبثقة بالموجز. - تمكين فلتر القائمة المنبثقة بالموجز - يتم عرض موجز شريط البحث. - تم إخفاء موجز شريط البحث. - إخفاء موجز شريط البحث - يتم عرض الاستبيانات في الموجز. - تم إخفاء الاستبيانات في الموجز. - إخفاء الاستبيانات في الموجز - يتم عرض تراكب شريط الفيلم. - تم إخفاء تراكب شريط الفيلم. - إخفاء واجهة شريط الفيلم - يتم عرض الزر العائم. - تم إخفاء الزر العائم. - إخفاء الزر العائم - يتم عرض زر الميكروفون العائم. - تم إخفاء زر الميكروفون العائم. - إخفاء زر الميكروفون العائم - يتم عرض رف لـك. - تم إخفاء رف لـك. - إخفاء رف لـك - يتم عرض إعلانات ملء الشاشة. - تم إخفاء إعلانات ملء الشاشة. - إخفاء إعلانات ملء الشاشة - "يتم حظر الإعلانات بملء الشاشة. - -التأثير الجانبي: قد يتم حظر صور منشورات المجتمع في وضع ملء الشاشة." - يتم إغلاق الإعلانات بملء الشاشة من خلال زر الإغلاق. - إغلاق الإعلانات بملء الشاشة - يتم عرض الإعلانات العامة. - تم إخفاء الإعلانات العامة. - إخفاء الإعلانات العامة - يتم عرض الترقية لـ YouTube Premium. - تم إخفاء الترقية لـ YouTube Premium. - إخفاء الترقية إلى YouTube Premium - يتم عرض الفواصل الرمادية. - تم إخفاء الفواصل الرمادية. - إخفاء الفواصل الرمادية - يتم عرض الاسم المعرِّف. - تم إخفاء الاسم المعرِّف. - إخفاء الاسم المعرِّف - يتم عرض زر البحث عن الصورة. - تم إخفاء زر البحث عن الصورة. - إخفاء زر البحث عن الصورة - يتم عرض رفوف الصور. - تم إخفاء رفوف الصور. - إخفاء رفوف الصور - يتم عرض قسم بطاقات المعلومات. - تم إخفاء قسم بطاقات المعلومات. - إخفاء قسم بطاقات المعلومات - يتم عرض بطاقات المعلومات. - تم إخفاء بطاقات المعلومات. - إخفاء بطاقات المعلومات - يتم عرض لوحات المعلومات. - تم إخفاء لوحات المعلومات. - إخفاء لوحات المعلومات - يتم عرض زر الانضمام. - تم إخفاء زر الانضمام. - إخفاء زر الانضمام - يتم عرض قسم المفاهيم الأساسية. - تم إخفاء قسم المفاهيم الأساسية. - إخفاء قسم المفاهيم الأساسية - "الصفحة الرئيسية / الاشتراكات / يتم تصفية نتائج البحث لإخفاء المحتوى الذي يطابق عبارات الكلمات الرئيسية. - - القيود: - • لا يمكن إخفاء فيديوهات Shorts حسب اسم القناة. - • قد لا تكون بعض مكونات واجهة المستخدم مخفية. - • قد لا يؤدي البحث عن كلمة رئيسية إلى ظهور أية نتائج." - حول تصفية الكلمات الرئيسية - سيؤدي وضع علامة اقتباس مزدوجة حول كلمة رئيسية/عبارة إلى منع التطابقات الجزئية لعناوين الفيديو وأسماء القنوات.<br><br>على سبيل المثال،<br><b>\"ai\"</b> سيخفي الفيديو: <b>How does AI work?</b><br>ولكن لن يخفي: <b>What does fair use mean?</b> - مطابقة الكلمات كاملة - لا يتم تصفية التعليقات. - يتم تصفية التعليقات. - إخفاء التعليقات بواسطة الكلمات الرئيسية - لا يتم تصفية الفيديوهات في موجز الصفحة الرئيسية. - يتم تصفية الفيديوهات في موجز الصفحة الرئيسية. - إخفاء فيديوهات الصفحة الرئيسية بواسطة الكلمات الرئيسية - "الكلمات والعبارات الرئيسية التي يجب إخفاؤها، مفصولة بأسطر جديدة. - -يمكن أن تكون الكلمات الرئيسية عبارة عن أسماء قنوات أو أي نص يظهر في عناوين الفيديو. - -يجب إدخال الكلمات التي تحتوي على أحرف كبيرة في المنتصف باستخدام الأحرف الكبيرة (على سبيل المثال: iPhone، TikTok، LeBlanc)." - الكلمات الرئيسية المراد إخفاؤها - لا يتم تصفية نتائج البحث. - يتم تصفية نتائج البحث. - إخفاء نتائج البحث عن طريق الكلمات الرئيسية - لا يتم تصفية الفيديوهات في موجز الاشتراكات. - يتم تصفية الفيديوهات في موجز الاشتراكات. - إخفاء الفيديوهات الخاصة بالاشتراك عن طريق الكلمات الرئيسية - الكلمة الرئيسية ستخفي جميع الفيديوهات: %s. - لا يمكن استخدام الكلمة الرئيسية: %s. - إضافة اقتباسات لاستخدام الكلمة الرئيسية: %s. - الكلمة الرئيسية لها بيانات متضاربة: %s. - الكلمة الرئيسية قصيرة جدًا وتتطلب اقتباسات: %s. - يتم عرض أحدث المشاركات. - تم إخفاء أحدث المشاركات. - إخفاء آخر المشاركات - يتم عرض زر أحدث مقاطع الفيديو. - تم إخفاء زر أحدث مقاطع الفيديو. - إخفاء زر أحدث مقاطع الفيديو - يتم عرض أزرار أعجبني ولم يعجبني. - تم إخفاء أزرار أعجبني ولم يعجبني. - إخفاء أزرار أعجبني ولم يعجبني - يتم عرض رسائل المحادثات المباشرة.\n\nينطبق هذا الإعداد على فيديوهات بث Shorts المباشر أيضًا. - تم إخفاء رسائل المحادثات المباشرة.\n\nينطبق هذا الإعداد على فيديوهات بث Shorts المباشر أيضًا. - إخفاء رسائل المحادثات المباشرة - يتم عرض زر إعادة تشغيل المحادثات المباشرة.\n\nيظهر في وضع ملء الشاشة عند إغلاق المحادثات المباشرة. - تم إخفاء زر إعادة تشغيل المحادثات المباشرة.\n\nيظهر في وضع ملء الشاشة عند إغلاق المحادثات المباشرة. - إخفاء زر إعادة عرض المحادثات المباشرة - إخفاء مقاطع الفيديو التي حصلت على أقل من 1000 مشاهدة من موجز الصفحة الرئيسية التي تم تحميلها من القنوات غير المشترك بها. - إخفاء فيديو المشاهدات المنخفضة - يتم عرض اللوحات الطبية. - تم إخفاء اللوحات الطبية. - إخفاء اللوحات الطبية - يتم عرض رفوف المنتجات. - تم إخفاء رفوف المنتجات. - إخفاء رفوف المنتجات - يتم عرض قوائم تشغيل التشكيلة. - تم إخفاء قوائم تشغيل التشكيلة. - إخفاء قوائم تشغيل التشكيلة - يتم عرض رفوف الأفلام. - تم إخفاء رفوف الأفلام. - إخفاء رفوف الأفلام - يتم عرض شريط التنقل. - تم إخفاء شريط التنقل. - إخفاء شريط التنقل - يتم عرض زر الإنشاء. - تم إخفاء زر الإنشاء. - إخفاء زر الإنشاء - يتم عرض زر الصفحة الرئيسية. - تم إخفاء زر الصفحة الرئيسية. - إخفاء زر الصفحة الرئيسية - يتم عرض تسميات شريط التنقل. - تم إخفاء تسميات شريط التنقل. - إخفاء تسميات شريط التنقل - يتم عرض زر المكتبة. - تم إخفاء زر المكتبة. - إخفاء زر المكتبة - يتم عرض زر الإشعارات. - تم إخفاء زر الإشعارات. - إخفاء زر الإشعارات - يتم عرض زر Shorts. - تم إخفاء زر Shorts. - إخفاء زر Shorts - يتم عرض زر الاشتراكات. - تم إخفاء زر الاشتراكات. - إخفاء زر الاشتراكات - يتم عرض زر تنبيهي. - تم إخفاء زر تنبيهي. - إخفاء زر تنبيهي - يتم عرض تصنيف الترويج المدفوع. - تم إخفاء تصنيف الترويج المدفوع. - إخفاء تصنيف الترويج المدفوع - يتم عرض هيّا نلعب. - تم إخفاء هيّا نلعب. - إخفاء هيّا نلعب - يتم عرض زر التشغيل التلقائي. - تم إخفاء زر التشغيل التلقائي. - إخفاء زر التشغيل التلقائي - يتم عرض زر التَرْجَمَة. - تم إخفاء زر التَرْجَمَة. - إخفاء زر التَرْجَمَة - يتم عرض زر البث. - تم إخفاء زر البث. - إخفاء زر البث - يتم عرض زر الطَيّ. - تم إخفاء زر الطَيّ. - إخفاء زر الطَيّ - يتم عرض قائمة الإضاءة السينمائية. - تم إخفاء قائمة الإضاءة السينمائية. - إخفاء قائمة الإضاءة السينمائية - يتم عرض قائمة المقطع الصوتي. - تم إخفاء قائمة المقطع الصوتي. - إخفاء قائمة المقطع الصوتي - يتم عرض تذييل قائمة التَرْجَمَة. - تم إخفاء تذييل قائمة التَرْجَمَة. - إخفاء تذييل قائمة التَرْجَمَة - يتم عرض قائمة التَرْجَمَة. - تم إخفاء قائمة التَرْجَمَة. - إخفاء قائمة التَرْجَمَة - يتم عرض قائمة 1080p Premium. - تم إخفاء قائمة 1080p Premium. - إخفاء قائمة 1080p Premium - يتم عرض قائمة المساعدة & الملاحظات. - تم إخفاء قائمة المساعدة & الملاحظات. - إخفاء قائمة المساعدة & الملاحظات - يتم عرض قائمة الاستماع مع موسيقى YouTube. - تم إخفاء قائمة الاستماع مع موسيقى YouTube. - إخفاء قائمة الاستماع مع موسيقى YouTube - يتم عرض قائمة قفل الشاشة. - تم إخفاء قائمة قفل الشاشة. - إخفاء قائمة قفل الشاشة - يتم عرض قائمة تكرار الفيديو. - تم إخفاء قائمة تكرار الفيديو. - إخفاء قائمة تكرار الفيديو - يتم عرض قائمة المزيد من المعلومات. - تم إخفاء قائمة المزيد من المعلومات. - إخفاء قائمة المزيد من المعلومات - يتم عرض قائمة صورة داخل صورة. - تم إخفاء قائمة صورة داخل صورة. - إخفاء قائمة صورة داخل صورة - يتم عرض قائمة سرعة التشغيل. - تم إخفاء قائمة سرعة التشغيل. - إخفاء قائمة سرعة التشغيل - يتم عرض قائمة عناصر التحكم في Premium. - تم إخفاء قائمة عناصر التحكم في Premium. - إخفاء قائمة عناصر التحكم في Premium - يتم عرض تذييل قائمة الجودة. - تم إخفاء تذييل قائمة الجودة. - إخفاء تذييل قائمة جودة الفيديو - يتم عرض هيدر قائمة الجودة. - تم إخفاء هيدر قائمة الجودة. - إخفاء جملة جودة الفيديو الحالي - يتم عرض قائمة الإبلاغ. - تم إخفاء قائمة الإبلاغ. - إخفاء قائمة الإبلاغ - يتم عرض قائمة مؤقت النوم. - تم إخفاء قائمة مؤقت النوم. - إخفاء قائمة مؤقت النوم - يتم عرض قائمة مستوى الصوت الثابت. - تم إخفاء قائمة مستوى الصوت الثابت. - إخفاء قائمة مستوى الصوت الثابت - يتم عرض قائمة إحصاءات تقنية. - تم إخفاء قائمة إحصاءات تقنية. - إخفاء قائمة إحصاءات تقنية - يتم عرض قائمة المشاهدة في الوضع الافتراضي. - تم إخفاء قائمة المشاهدة في الوضع الافتراضي. - إخفاء قائمة المشاهدة في الوضع الافتراضي - يتم عرض زر ملء الشاشة. - تم إخفاء زر ملء الشاشة. - إخفاء زر ملء الشاشة - يتم عرض الأزرار. - تم إخفاء الأزرار. - إخفاء أزرار السابق & التالي - يتم عرض رف التسوق. - تم إخفاء رف التسوق. - إخفاء رف مشغل التسوق - يتم عرض زر موسيقى YouTube. - تم إخفاء زر موسيقى YouTube. - إخفاء زر موسيقى YouTube - يتم عرض زر الحفظ. - تم إخفاء زر الحفظ. - إخفاء زر الحفظ - يتم عرض استكشاف قسم بودكاست. - تم إخفاء استكشاف قسم بودكاست. - إخفاء استكشاف قسم البودكاست - يتم عرض تعليق المعاينة. - تم إخفاء تعليق المعاينة. - إخفاء تعليق المعاينة - يؤدي هذا إلى تغيير حجم قسم التعليقات، لذلك من المستحيل فتح إعادة تشغيل الدردشة المباشرة في قسم التعليقات. - لا يغير هذا حجم قسم التعليقات، لذلك من الممكن فتح إعادة الدردشة المباشرة في قسم التعليقات. - إخفاء نوع معاينة التعليق - يتم عرض لافتة تنبيه الترقية. - تم إخفاء لافتة تنبيه الترقية. - إخفاء لافتة تنبيه الترقية - يتم عرض زر التعليقات. - تم إخفاء زر التعليقات. - إخفاء زر التعليقات - يتم عرض زر لم يعجبني. - تم إخفاء زر لم يعجبني. - إخفاء زر لم يعجبني - يتم عرض زر أعجبني. - تم إخفاء زر أعجبني. - إخفاء زر أعجبني - يتم عرض زر المحادثات المباشرة. - تم إخفاء زر المحادثات المباشرة. - إخفاء زر المحادثات المباشرة - يتم عرض زر المزيد. - تم إخفاء زر المزيد. - إخفاء زر المزيد - يتم عرض زر فتح قائمة تشغيل التشكيلة. - تم إخفاء زر فتح قائمة تشغيل التشكيلة. - إخفاء زر فتح قائمة تشغيل التشكيلة - يتم عرض زر فتح قائمة التشغيل. - تم إخفاء زر فتح قائمة التشغيل. - إخفاء زر فتح قائمة التشغيل - يتم عرض زر الحفظ. - تم إخفاء زر الحفظ. - إخفاء زر الحفظ - يتم عرض زر مشاركة. - تم إخفاء زر مشاركة. - إخفاء زر مشاركة - يتم عرض حاوية الإجراءات السريعة. - تم إخفاء حاوية الإجراءات السريعة. - إخفاء حاوية الإجراءات السريعة - "يخفي مقاطع الفيديو الموصى بها التالية: - -• مقاطع الفيديو التي تحمل علامة للأعضاء فقط. -• مقاطع فيديو تحتوي على عبارات مثل 'شاهد الأشخاص أيضًا' أسفلها." - إخفاء الفيديوهات الموصى بها - يتم عرض قسم المزيد من مقاطع الفيديو في حاوية الإجراءات السريعة وواجهة الفيديو ذي الصلة. - تم إخفاء قسم المزيد من مقاطع الفيديو في حاوية الإجراءات السريعة وواجهة الفيديو ذي الصلة. - إخفاء تراكب الفيديو ذي الصلة - يتم عرض الفيديوهات ذات الصلة. - تم إخفاء الفيديوهات ذات الصلة. - إخفاء الفيديوهات ذات الصلة - "يحد هذا الإعداد من الحد الأقصى لعدد التخطيطات التي يمكن تحميلها على شاشة المشغل. - -إذا تغير تخطيط شاشة المشغل بسبب تغييرات على جانب الخادم، فقد يتم إخفاء التخطيطات غير المقصودة على شاشة المشغل." - يتم عرض زر ريمكس. - تم إخفاء زر ريمكس. - إخفاء زر ريمكس - يتم عرض زر الإبلاغ. - تم إخفاء زر الإبلاغ. - إخفاء زر الإبلاغ - يتم عرض زر المكافآت. - تم إخفاء زر المكافآت. - إخفاء زر المكافآت - يتم عرض المصغرات في سجل مصطلحات البحث. - تم إخفاء المصغرات في سجل مصطلحات البحث. - إخفاء مصغرات مصطلحات البحث - يتم عرض رسالة التمرير. - تم إخفاء رسالة التمرير. - إخفاء رسالة التمرير - يتم عرض رسالة تراجع عن التمرير. - تم إخفاء رسالة تراجع عن التمرير. - إخفاء رسالة التراجع عن التمرير - يتم عرض تسميات الفصل المجاورة للطابع الزمني. - تم إخفاء تسميات الفصل المجاورة للطابع الزمني. - إخفاء تسميات فصول شريط التقدم - يتم عرض شريط تقدم الفيديو. - تم إخفاء شريط تقدم الفيديو. - يتم عرض مُصَّغَرة الفيديو بشريط التقدم. - تم إخفاء مُصَّغَرة الفيديو بشريط التقدم. - إخفاء شريط التقدم في مُصَّغَرات الفيديو - إخفاء شريط التقدم في مشغل الفيديو - يتم عرض بطاقات الرعاية الذاتية. - تم إخفاء بطاقات الرعاية الذاتية. - إخفاء بطاقات الرعاية الذاتية - يتم عرض قائمة لمحة. - تم إخفاء قائمة لمحة. - إخفاء قائمة لمحة - يتم عرض قائمة تمكين الوصول. - تم إخفاء قائمة تمكين الوصول. - إخفاء قائمة تمكين الوصول - يتم عرض قائمة الحساب. - تم إخفاء قائمة الحساب. - إخفاء قائمة الحساب - يتم عرض قائمة التشغيل التلقائي. - تم إخفاء قائمة التشغيل التلقائي. - إخفاء قائمة التشغيل التلقائي - يتم عرض قائمة الفوترة والدفعات. - تم إخفاء قائمة الفوترة والدفعات. - إخفاء قائمة الفوترة والدفعات - يتم عرض قائمة الترجمة. - تم إخفاء قائمة الترجمة. - إخفاء قائمة الترجمة - يتم عرض قائمة التطبيقات المرتبطة. - تم إخفاء قائمة التطبيقات المرتبطة. - إخفاء قائمة التطبيقات المرتبطة - يتم عرض قائمة توفير البيانات. - تم إخفاء قائمة توفير البيانات. - إخفاء قائمة توفير البيانات - يتم عرض قائمة عامة. - تم إخفاء قائمة عامة. - إخفاء قائمة عامة - يتم عرض قائمة إدارة كل السجلّ. - تم إخفاء قائمة إدارة كل السجلّ. - إخفاء قائمة إدارة كل السجلّ - يتم عرض قائمة المحادثات المباشرة. - تم إخفاء قائمة المحادثات المباشرة. - إخفاء قائمة المحادثات المباشرة - يتم عرض قائمة الإشعارات. - تم إخفاء قائمة الإشعارات. - إخفاء قائمة الإشعارات - يتم عرض قائمة الخلفية. - تم إخفاء قائمة الخلفية. - إخفاء قائمة الخلفية - يتم عرض قائمة المشاهدة على التلفزيون. - تم إخفاء قائمة المشاهدة على التلفزيون. - إخفاء قائمة المشاهدة على التلفزيون - يتم عرض قائمة مركز العائلة. - تم إخفاء قائمة مركز العائلة. - إخفاء قائمة مركز العائلة - يتم عرض قائمة اختبار ميزات تجريبية جديدة. - تم إخفاء قائمة اختبار ميزات تجريبية جديدة. - إخفاء قائمة اختبار ميزات تجريبية جديدة - يتم عرض قائمة الخصوصية. - تم إخفاء قائمة الخصوصية. - إخفاء قائمة الخصوصية - يتم عرض قائمة عمليات الشراء والاشتراكات. - تم إخفاء قائمة عمليات الشراء والاشتراكات. - إخفاء قائمة عمليات الشراء والاشتراكات - إخفاء العناصر في قائمة إعدادات YouTube. - إخفاء قائمة إعدادات YouTube - يتم عرض قائمة الجودة المفضلة للفيديو. - تم إخفاء قائمة الجودة المفضلة للفيديو. - إخفاء قائمة الجودة المفضلة للفيديو - يتم عرض قائمة بياناتك في YouTube. - تم إخفاء قائمة بياناتك في YouTube. - إخفاء قائمة بياناتك في YouTube - يتم عرض زر مشاركة. - تم إخفاء زر مشاركة. - إخفاء زر مشاركة - يتم عرض زر المتجر. - تم إخفاء زر المتجر. - إخفاء زر التسوق - يتم عرض روابط التسوق. - تم إخفاء روابط التسوق. - إخفاء روابط التسوق - يتم عرض شريط القناة. - تم إخفاء شريط القناة. - إخفاء شريط القناة - يتم عرض قسم التعليقات. - تم إخفاء زر التعليقات. - إخفاء زر التعليقات - يتم عرض زر التعليقات المعطلة أو الذي يحمل العلامة \"0\". - تم إخفاء زر التعليقات المعطلة أو الذي يحمل العلامة \"0\". - إخفاء زر التعليقات المعطلة - يتم عرض زر لم يعجبني. - تم إخفاء زر لم يعجبني. - إخفاء زر لم يعجبني - "يتم عرض الأزرار العائمة مثل 'استخدام هذا الصوت' في علامة تبويب قناة Shorts." - "تم إخفاء الأزرار العائمة مثل 'استخدام هذا الصوت' في علامة تبويب قناة Shorts." - إخفاء الزر العائم - يتم عرض تسمية رابط الفيديو. - تم إخفاء تسمية رابط الفيديو. - إخفاء تسمية رابط الفيديو الكامل - يتم عرض زر الشاشة الخضراء. - تم إخفاء زر الشاشة الخضراء. - إخفاء زر الشاشة الأخضر - يتم عرض لوحات المعلومات. - تم إخفاء لوحات المعلومات. - إخفاء لوحات المعلومات - يتم عرض زر الانضمام. - تم إخفاء زر الانضمام. - إخفاء زر الانضمام - يتم عرض زر أعجبني. - تم إخفاء زر أعجبني. - إخفاء زر أعجبني - يتم عرض Header المحادثات المباشرة.\n\nزر رجوع في Header لن يتم إخفاؤه. - تم إخفاء Header المحادثات المباشرة.\n\nزر رجوع في Header لن يتم إخفاؤه. - إخفاء Header المحادثات المباشرة - يتم عرض زر الموقع. - تم إخفاء زر الموقع. - إخفاء زر الموقع - يتم عرض شريط التنقل. - تم إخفاء شريط التنقل. - إخفاء شريط التنقل - يتم عرض تصنيف الترويج المدفوع. - تم إخفاء تصنيف الترويج المدفوع. - إخفاء تصنيف الترويج المدفوع - يتم عرض علامة Shorts عند توقف الفيديو. - تم إخفاء علامة Shorts عند توقف الفيديو. - إخفاء علامة Shorts أثناء التوقف - يتم عرض أزرار تراكب التوقف. - تم إخفاء أزرار تراكب التوقف. - إخفاء أزرار واجهة التوقف - يتم عرض خلفية الزر. - تم إخفاء خلفية الزر. - إخفاء خلفية زر التشغيل & الإيقاف - يتم عرض زر ريمكس. - تم إخفاء زر ريمكس. - إخفاء زر ريمكس - يتم عرض زر حفظ الموسيقى. - تم إخفاء زر حفظ الموسيقى. - إخفاء زر حفظ الموسيقى - يتم عرض زر اقتراحات البحث. - تم إخفاء زر اقتراحات البحث. - إخفاء زر اقتراحات البحث - يتم عرض زر مشاركة. - تم إخفاء زر مشاركة. - إخفاء زر مشاركة - يُعرض في القناة. - "مخفي في القناة. - -معلومة: -• فقط الأرفف التي تحتوي على عنوان Shorts في علامة تبويب الصفحة الرئيسية تكون مخفية." - إخفاء في القناة - يُعرض في سجل المشاهدة. - مخفي في سجل المشاهدة. - إخفاء في سجل المشاهدة - يُعرض في موجز الصفحة الرئيسية والفيديوهات ذات الصلة. - مخفي في موجز الصفحة الرئيسية والفيديوهات ذات الصلة. - إخفاء في موجز الصفحة الرئيسية والفيديوهات ذات الصلة - يُعرض في نتائج البحث. - مخفي في نتائج البحث. - إخفاء في نتائج البحث - يُعرض في موجز الاشتراكات. - مخفي في موجز الاشتراكات. - إخفاء في موجز الاشتراكات - "إخفاء رفوف Shorts - -التأثير الجانبي: سيتم إخفاء الرؤوس الرسمية في نتائج البحث." - إخفاء رفوف Shorts - يتم عرض زر المتجر. - تم إخفاء زر المتجر. - إخفاء زر التسوق - يتم عرض زر التسوق. - تم إخفاء زر التسوق. - إخفاء زر التسوق - يتم عرض زر الصوت. - تم إخفاء زر الصوت. - إخفاء زر الصوت - يتم عرض تسمية البيانات الوصفية. - تم إخفاء تسمية البيانات الوصفية. - إخفاء تسمية بيانات التعريف الصوتية - يتم عرض الملصقات. - تم إخفاء الملصقات. - إخفاء الملصقات - يتم عرض زر اشتراك. - تم إخفاء زر اشتراك. - إخفاء زر اشتراك - يتم عرض زر Super Thanks. - تم إخفاء زر Super Thanks. - إخفاء زر Super Thanks - يتم عرض المنتجات المعلمة. - تم إخفاء المنتجات المعلمة. - إخفاء المنتجات الموسومة - يتم عرض شريط الأدوات. - تم إخفاء شريط الأدوات. - إخفاء شريط الأدوات - يتم عرض زر الرائجة. - تم إخفاء زر الرائجة. - إخفاء زر الرائجة - يتم عرض زر القالب. - تم إخفاء زر القالب. - إخفاء زر استخدام القالب - يتم عرض زر استخدام هذا الصوت. - تم إخفاء زر استخدام هذا الصوت. - إخفاء زر استخدام هذا الصوت - يتم عرض العنوان. - تم إخفاء العنوان. - إخفاء عنوان الفيديو - يتم عرض زر عرض المزيد. - تم إخفاء زر عرض المزيد. - إخفاء زر عرض المزيد - يتم عرض شريط Snack Bar. - تم إخفاء شريط Snack Bar. - إخفاء شريط Snack Bar - يتم عرض زر بدء التجربة. - تم إخفاء زر بدء التجربة. - إخفاء زر بدء التجربة - يتم عرض دوائر الاشتراكات. - تم إخفاء دوائر الاشتراكات. - إخفاء دوائر الاشتراكات - يتم عرض الإجراءات المقترحة. - تم إخفاء الإجراءات المقترحة. - إخفاء الإجراءات المقترحة - "لقد تم إهمال هذا الإعداد. - - بدلاً من ذلك، استخدم الإعداد \"الإعدادات ← التشغيل التلقائي ← التشغيل التلقائي للفيديو التالي\". - -ملاحظة: -• إذا كانت لديك أية مشكلات تتعلق بـ \"شاشة نهاية الفيديو المقترحة\"، فحاول إعادة تشغيل التطبيق." - يتم عرض شاشة نهاية الفيديو المقترح. - "تم إخفاء شاشة نهاية الفيديو المقترح عند إيقاف التشغيل التلقائي. - -يمكن تغيير التشغيل التلقائي في إعدادات YouTube: -الإعدادات ← التشغيل التلقائي ← تشغيل الفيديو التالي تلقائيًا" - إخفاء شاشة نهاية الفيديو المقترح - يتم عرض زر شكرًا. - تم إخفاء زر شكرًا. - إخفاء زر شكرًا - يتم عرض رفوف التذاكر. - تم إخفاء رفوف التذاكر. - إخفاء رفوف التذاكر - يتم عرض طابع الوقت. - تم إخفاء طابع الوقت. - إخفاء طابع الوقت - يتم عرض ردود الفعل المؤقتة. - تم إخفاء ردود الفعل المؤقتة. - إخفاء ردود الفعل المؤقتة - يتم عرض زر البث. - تم إخفاء زر البث. - إخفاء زر البث - يتم عرض زر الإنشاء. - تم إخفاء زر الإنشاء. - إخفاء زر الإنشاء - يتم عرض زر الإشعارات. - تم إخفاء زر الإشعارات. - إخفاء زر الإشعارات - يتم عرض قسم النص. - تم إخفاء قسم النص. - إخفاء قسم النص - يتم عرض إعلانات الفيديو. - تم إخفاء إعلانات الفيديو. - إخفاء إعلانات الفيديو - "الصفحة الرئيسية / الاشتراكات / تتم تصفية نتائج البحث لإخفاء مقاطع الفيديو التي حصلت على عدد مشاهدات أقل أو أكبر من عدد محدد. - -القيود: - -• لا يمكن إخفاء فيديوهات Shorts. -• لا تتم تصفية الفيديوهات التي حصلت على 0 مشاهدة." - حول تصفية عدد المشاهدات - لا يتم تصفية الفيديوهات في موجز الصفحة الرئيسية. - يتم تصفية الفيديوهات في موجز الصفحة الرئيسية. - إخفاء فيديوهات الصفحة الرئيسية حسب عدد المشاهدات - لا يتم تصفية نتائج البحث. - يتم تصفية نتائج البحث. - إخفاء نتائج البحث حسب عدد المشاهدات - لا يتم تصفية الفيديوهات في موجز الاشتراكات. - يتم تصفية الفيديوهات في موجز الاشتراكات. - إخفاء فيديوهات الاشتراكات حسب عدد المشاهدات - إخفاء الفيديوهات الموصى بها التي حصلت على عدد أقل من عدد معين من المشاهدات.\n\nمشكلة معروفة: لا يتم تصفية الفيديوهات التي حصلت على 0 مشاهدة. - إخفاء الفيديوهات الموصى بها عن طريق المشاهدات - سيتم إخفاء الفيديوهات التي تزيد مشاهداتها عن هذا الرقم. - الأكبر من المشاهدات - سيتم إخفاء الفيديوهات ذات المشاهدات الأقل من هذا الرقم. - الأقل من المشاهدات - K -> 1 000\nM -> 1 000 000\nB -> 1 000 000 000\nمشاهد -> مشاهد - حدد قالب اللغة الخاص بك لعدد مرات المشاهدة الموضحة أسفل كل فيديو في واجهة المستخدم. كل مفتاح (حرف/كلمة في لغتك) -> قيمة (معنى المفتاح) يجب أن يكون على سطر جديد. المفاتيح تذهب قبل علامة \"->\". إذا قمت بتغيير التطبيق أو لغة النظام، فستحتاج إلى إعادة تعيين هذا الإعداد.\n\nأمثلة:\nEnglish: 10K views = K -> 1000, views -> views\nSpanish: 10 K vistas = K -> 1000, vistas -> views - عرض المفاتيح - يتم عرض لافتة عرض المنتجات. - تم إخفاء لافتة عرض المنتجات. - إخفاء لافتة عرض المنتجات - يتم عرض زر البحث الصوتي. - تم إخفاء زر البحث الصوتي. - إخفاء زر البحث الصوتي - يتم عرض نتائج البحث على الويب. - تم إخفاء نتائج البحث على الويب. - إخفاء نتائج بحث الويب - يتم عرض رسومات YouTube. - تم إخفاء رسومات YouTube. - إخفاء رسومات YouTube - "تظهر رسومات YouTube Doodles لعدة أيام كل عام. - -إذا كانت رسومات YouTube Doodles تظهر حاليًا في منطقتك وكان إعداد الإخفاء هذا قيد التشغيل، فسيتم أيضًا إخفاء فلتر الشريط الموجود أسفل شريط البحث." - تم إخفاء تراكب التكبير. - يتم عرض تراكب التكبير. - إخفاء تراكب التكبير - AFN Blue - AFN Red - مخصص - الإفتراضي - MMT - Revancify Blue - Revancify Red - YouTube - يحافظ على الوضع الأفقي عند إيقاف تشغيل الشاشة وتشغيلها في وضع ملء الشاشة. - مقدار اجزاء الثانية التي يتم فرضها على الوضع الأفقي بعد تشغيل الشاشة. - مهلة إبقاء الوضع الأفقي - الإبقاء على الوضع الأفقي - الإفتراضي - تم تعطيل إجراء النقر المزدوج. - "تم تمكين إجراء النقر المزدوج. - -• انقر نقرًا مزدوجًا لتغيير حجم الفيديو المصغر إلى حجم أكبر. -• انقر نقرًا مزدوجًا مرة أخرى للتغيير إلى الحجم الأصلي." - تمكين إجراء النقر المزدوج - تم تعطيل السحب والإفلات. - تم تمكين السحب والإفلات. - تمكين السحب والإفلات - يتم عرض أزرار التوسيع والإغلاق. - تم إخفاء الأزرار.\n(مرر المشغل المصغر للتوسع أو الإغلاق) - إخفاء أزرار التوسيع والإغلاق - يتم عرض التخطي للأمام والخلف. - تم إخفاء التخطي للأمام والخلف. - إخفاء أزرار التخطي للأمام والخلف - يتم عرض النصوص الفرعية. - تم إخفاء النصوص الفرعية. - إخفاء النصوص الفرعية - يجب أن تكون نسبة شفافية واجهة المشغل المصغر بين 0-100. - قيمة الشفافية بين 0-100، حيث يكون 0 شفاف. - شفافية الواجهة - الأصلي - الجوّال - الجهاز اللوحي - حديث 1 - حديث 2 - حديث 3 - نوع المشغل المصغر - زر الواجهة - "اضغط التبديل لتكرار العرض دائمًا -اضغط مع الاستمرار على التبديل لإيقاف تكرار العرض." - عرض زر التكرار دائمًا - "انقر لنسخ عنوان URL الفيديو. -انقر مع الاستمرار لنسخ عنوان URL للفيديو مع الطابع الزمني." - "انقر لنسخ عنوان URL الفيديو مع الطابع الزمني. -انقر مع الاستمرار لنسخ الطابع الزمني للفيديو." - عرض زر نسخ URL مع الطابع الزمني - عرض زر نسخ رابط الفيديو - انقر لتشغيل برنامَج التنزيل الخارجي. - عرض زر التنزيل الخارجي - انقر لكتم صوت الفيديو الحالي. انقر مرة أخرى لإلغاء الكتم. - عرض زر كتم الصوت - انقر مع الاستمرار لتغيير حالة الزر. - إعادة تعيين سرعة التشغيل: %sx. - "انقر لفتح مربع حوار السرعة. -انقر مع الاستمرار لضبط سرعة التشغيل على 1.0x. انقر مع الاستمرار مرة أخرى لإعادة ضبط السرعة الافتراضية." - عرض مربع زر حوار السرعة - "انقر لإنشاء قائمة تشغيل بجميع فيديوهات القناة من الأقدم إلى الأحدث. -انقر مع الاستمرار للتراجع." - عرض زر قائمة التشغيل مرتبة حسب الوقت - \"انقر لفتح مربع حوار القائمة البيضاء. -انقر مع الاستمرار لفتح مربع حوار إعداد القائمة البيضاء. - عرض زر القائمة البيضاء - إذا تم عرضه، فإن زر تنزيل قائمة التشغيل الأصلية يفتح أداة التنزيل الأصلية داخل التطبيق. - يتم دائمًا عرض زر تنزيل قائمة التشغيل الأصلية، وفي قوائم التشغيل العامة، يتم فتح أداة التنزيل الخارجية لديك. - تجاوز زر تنزيل قائمة التشغيل - يفتح زر تنزيل الفيديو أداة التنزيل الأصلية داخل التطبيق. - يفتح زر تنزيل الفيديو الأصلي أداة التنزيل الخارجية. - تجاوز زر تنزيل الفيديو - موسيقى YouTube مطلوبًا لتجاوز إجراء الزر. انقر هنا لتنزيل موسيقى YouTube. - متطلب أساسي - زر موسيقى YouTube يفتح التطبيق الأصلي. - زر موسيقى YouTube يفتح موسيقى RVX. - تجاوز زر موسيقى YouTube - مستبعد - مضمن - عادي - أزرار الإجراء - إعدادات إضافية - تأثير الحركة / رد الفعل - زر التنزيل - تعديلات تجريبية - قيود منطقة الصورة - استيراد / تصدير كملف - استيراد / تصدير كنص - تصفية الكلمات الرئيسية - أخرى - أزرار الواجهة - معلومات التعديل - الإجراءات السريعة - الفيديو الموصى به - رفوف Shorts - الإجراءات المقترحة - الأداة المستخدمة - تصفية عدد المشاهدات - إخفاء أو عرض العناصر في قائمة الحساب وعلامة التبويب أنت. - قائمة الحساب - إخفاء أو عرض أزرار الإجراءات تحت الفيديو. - أزرار الإجراء - الإعلانات - مُصغَّرات فيديو بديلة - تعطيل وضع الإضاءة السينمائية أو تجاوز قيود وضع الإضاءة السينمائية. - وضع الإضاءة السينمائية - إخفاء شريط الفئة أو عرضه في الموجز والبحث ومقاطع الفيديو ذات الصلة. - شريط الفئة - إخفاء أو عرض مكونات شريط القناة تحت مقاطع الفيديو. - شريط القناة - إخفاء أو عرض المكونات في الملف الشخصي للقناة. - ملف القناة - إخفاء أو عرض مكونات قسم التعليقات. - التعليقات - إخفاء أو عرض مشاركات المجتمع في الموجز والقناة. - مشاركات المجتمع - إخفاء المكونات باستخدام عوامل تصفية مخصصة. - فلتر مخصص - إخفاء أو عرض مكونات القائمة المنبثقة في الموجز. - القائمة المنبثقة - الموجز - إخفاء أو تغيير المكونات المتعلقة بملء الشاشة. - ملء الشاشة - عام - تعطيل أو تمكين الاهتزاز. - ردود الفعل اللمسية - يتجاوز إجراء النقر على الأزرار الموجودة داخل التطبيق. - أزرار الإرتباط - إستيراد أو تصدير الإعدادات. - استيراد / تصدير الإعدادات - تغيير نمط المشغل المصغر داخل التطبيق. - المشغل المصغر - خيارات متنوعة - إخفاء أو عرض مكونات قسم شريط التنقل. - شريط التنقل - معلومات عن التعديلات المطبقة. - معلومات التعديل - إخفاء أو عرض الأزرار في مشغل الفيديو. - أزرار المشغل - إخفاء أو تغيير مكونات القائمة المنبثقة في مشغل الفيديو. - القائمة المنبثقة - المشغل - Return YouTube Username - Return YouTube Dislike - SponsorBlock - تخصيص مكونات شريط التقدم. - شريط تقدم الفيديو - إخفاء العناصر في قائمة إعدادات YouTube. - قائمة الإعدادات - إخفاء أو عرض المكونات في مشغل Shorts. - مُشَغِل Shorts - Shorts - تزييف بيانات البث لمنع حدوث مشكلات أثناء التشغيل. - Spoof Streaming Data - التحكم عبر إيماءة التمرير - إخفاء أو تغيير المكونات الموجودة على شريط الأدوات، مثل شريط الأدوات وأزرار شريط الأدوات والعلامة. - شريط الأدوات - إخفاء أو عرض مكونات وصف الفيديو. - وصف الفيديو - إخفاء الفيديوهات بواسطة الكلمات الرئيسية أو المشاهدة. - عامل تصفية الفيديو - الفيديو - تغيير الإعدادات المتعلقة بسجل المشاهدة. - سجل المشاهدة - يجب أن يكون الهامش العلوي للإجراءات السريعة بين 0-32. - تكوين التباعد من شريط التقدم إلى حاوية الإجراء السريع، بين 0-32. - هامش إجراءات سريعة أعلى - "يرفض قسرًا استجابة برنامج ترميز AV1. -سيتم تطبيق برنامج ترميز مختلف بعد حوالي 20 ثانية من التخزين المؤقت." - رفض استجابة برنامج الترميز AV1 - تؤدي العملية الاحتياطية إلى حوالي 20 ثانية من التخزين المؤقت. - الموازن - تنطبق تغييرات سرعة التشغيل على الفيديو الحالي فقط. - تنطبق تغييرات سرعة التشغيل على جميع الفيديوهات. - تذكر التغيرات في سرعة التشغيل - لن يتم عرض ملاحظة عند تغيير سرعة التشغيل الافتراضية. - سيتم عرض ملاحظة عند تغيير سرعة التشغيل الافتراضية. - عرض ملاحظة - تغيير السرعة الافتراضية إلى %s. - تنطبق تغييرات الجودة على الفيديو الحالي فقط. - تنطبق تغييرات الجودة على جميع الفيديوهات. - تذكر تغييرات جودة الفيديو - لن يتم عرض ملاحظة عند تغيير جودة الفيديو الافتراضية. - سيتم عرض ملاحظة عند تغيير جودة الفيديو الافتراضية. - عرض ملاحظة - تغيير جودة بيانات الجوّال الافتراضية إلى %s. - فشل في تعيين جودة الفيديو. - تغيير جودة Wi-Fi الافتراضية إلى %s. - "يزيل مربع حوار تقدير المشاهد. هذا لا يتجاوز القيود العمرية. إنه يقبل ذلك تلقائيًا." - إزالة مربع حوار تقدير المشاهد - يستبدل برنامج الترميز AV1 ببرنامج الترميز VP9. - استبدال برنامج الترميز AV1 - يتم استخدام الاسم المعرِّف. - يتم استخدام اسم القناة. - استبدال معالج القناة - انقر لعرض الوقت المتبقي. - انقر لفتح القائمة المنبثقة لسرعة التشغيل أو جودة الفيديو. - استبدال إجراء الطابع الزمني - استبدال زر الإنشاء بزر الإعدادات. - استبدال زر الإنشاء - "اضغط لفتح إعدادات YouTube. -اضغط مع الاستمرار لفتح إعدادات RVX." - "اضغط لفتح إعدادات RVX. -اضغط مع الاستمرار لفتح إعدادات YouTube." - نوع الإجراء الذي سيتم تعيينه للزر - مصغرات شريط التقدم ستظهر في ملء الشاشة. - مصغرات شريط التقدم ستظهر فوق شريط تقدم الفيديو. - استعادة مصغرات شريط التقدم القديمة - لا يتم عرض قائمة جودة الفيديو القديمة. - يتم عرض قائمة جودة الفيديو القديمة. - استعادة قائمة جودة الفيديو القديمة - (اسم المستخدم) Handle@ - شكل العرض - اسم المستخدم (Handle@) - اسم المستخدم - يتم استخدام الاسم المعرِّف. - يتم استخدام اسم المستخدم. - تمكين إعادة اسم مستخدم YouTube - "مطلوب مفتاح مطور بيانات YouTube API v3 لاستبدال الاسم المعرِّف بـ اسم المستخدم. - -الحصة اليومية لمفاتيح API في الخطة المجانية هي 10.000، ويتم استخدام حصة واحدة لاستبدال الاسم المعرِّف بـ اسم المستخدم لتعليق واحد. - -انقر لمعرفة كيفية إصدار مفتاح API." - لمحة عن مفتاح YouTube Data API - مفتاح المطور لاستخدام بيانات YouTube API v3. - مفتاح API لبيانات YouTube - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ لا ينبغي مشاركة مفتاح API مع الآخرين مطلقًا، لذا فهو غير مدرج في إعدادات الاستيراد / التصدير. - إصدار مفتاح مطور YouTube Data API v3 - لمحة - يتم توفير بيانات لم يعجبني بمقاطع YouTube بواسطة the Return YouTube Dislike API. اضغط هنا لمعرفة المزيد. - ReturnYouTubeDislike.com - زر أعجبني مصمم لأفضل مظهر. - زر أعجبني مصمم لأدنى عرض. - مقاس زر أعجبني - يعرض عدد لم يعجبني كـ رَقَم. - يعرض عدد لم يعجبني كـ نسبة مئوية. - لم يعجبني كــ نسبة مئوية - لا يتم عرض لم يعجبني. - يتم عرض لم يعجبني. - تمكين Return YouTube Dislike - تم إخفاء الإعجابات المقدرة. - يتم عرض الإعجابات المقدرة. - عرض الإعجابات المقدرة - لم يعجبني غير متوفر (تم الوصول إلى حد API العميل). - لم يعجبني غير متوفر (الحالة %d). - لم يعجبني غير متوفر مؤقتًا (انتهت مهلة API). - لم يعجبني غير متوفر (%s). - أعد تحميل الفيديو للتصويت باستخدام Return YouTube Dislike - تم إخفاء لم يعجني في مقاطع Shorts. - يتم عرض لم يعجني في مقاطع Shorts. - "إبداءات لم يعجبني التي تظهر على فيديوهات Shorts. - -التقييد: قد لا تظهر إبداءات لم يعجبني إذا لم يقم المستخدم بتسجيل الدخول أو في وضع التصفح المتخفي." - عرض لم يعجني في مقاطع Shorts - لا يتم عرض الملاحظة في حالة عدم توفر Return YouTube Dislike. - يتم عرض الملاحظة في حالة عدم توفر Return YouTube Dislike. - عرض ملاحظة إذا كان API غير متوفر - مخفي - يزيل معلمات استعلام التتبع من عناوين URL عند مشاركة الروابط. - تطهير روابط المشاركة - "يتم عرض عبارات مثل '#'، 'جمع التبرعات'، 'المتجر' و 'المنتجات' من خلال ترجمات الفيديو." - "تم إخفاء عبارات مثل '#'، 'جمع التبرعات'، 'المتجر' و 'المنتجات' من ترجمات الفيديو." - تطهير ترجمة الفيديو - لمحة - sponsor.ajay.app - يتم توفير البيانات بواسطة SponsorBlock API. اضغط هنا لمعرفة المزيد والتنزيل لمنصات أخرى. - تم تغيير رابط API. - رابط API غير صالح. - إعادة تعيين رابط API. - المظهر - تم تغيير اللون. - اللون: - رمز اللون غير صالح. - إعادة ضبط اللون. - إنشاء مقاطع جديدة - تغيير سلوك المقطع - إخفاء زر التخطي تلقائيًا - يتم عرض زر \"التخطي\" للمقطع بأكمله. - يختفي زر التخطي بعد بضع ثوانٍ. - استخدام زر التخطي المُصَغَّر - زر التخطي مصمم لأفضل مظهر. - زر التخطي مصمم لأدنى عرض. - عرض زر إنشاء مقطع جديد - لا يتم عرض زر إنشاء مقطع جديد. - يتم عرض زر إنشاء مقطع جديد. - تمكين SponsorBlock - مانِع الرُعَاة هو نظام جماعي لتخطي الأجزاء المُمِلَّة في مقاطع YouTube. - عرض زر التصويت - لا يتم عرض زر التصويت على المقطع. - يتم عرض زر التصويت على المقطع. - عام - تعديل تقديم او تأخير المقطع الجديد - يجب أن تكون القيمة رقمًا موجبًا. - أجزاء الثانية في الوقت الذي تتحرك فيها أزرار ضبط الوقت عند إنشاء مقاطع جديدة. - تغيير عنوان API - العنوان الذي يستخدمه SponsorBlock لإجراء اتصالات إلى الخادم. - الحد الأدنى لمدة المقطع - مدة الوقت غير صالحة. - لن يتم عرض أو تخطي المقاطع الأقصر من هذه القيمة (بالثواني). - تمكين تتبع مرات التخطي - تم تعطيل تتبع مرات التخطي. - يُتيح لـ SponsorBlock Leaderboard معرفة مقدار الوقت الذي وفره المشاهدين، يتم إعلام الخادم في كل مرة تتخطى فيها مقطعًا. - عرض ملاحظة عند تخطي المقطع تلقائيًا - لا تظهر الملاحظة. اضغط هنا لمشاهدة مثال. - تظهر الملاحظة عند تخطي مقطع تلقائيًا. اضغط هنا لمشاهدة مثال. - عرض مدة الفيديو بدون المقاطع - يتم عرض مدة الفيديو كاملةً. - يعرض مدة الفيديو ناقصًا منها مدة المقطع المدمج بين قوسين بجوار مدة الفيديو الكامل. - معرف المستخدم User ID الفريد الخاص بك - يجب أن يكون معرف المستخدم الخاص 30 حرفًا على الأقل. - يجب أن يبقى هذا خاصًا. انه مثل كلمة المرور ولا ينبغي مشاركته مع أي شخص. إذا كان شخص ما يملك هذا، فيمكنه انتحال شخصيتك. - تمت قراءتها - من المستحسن قراءة الإرشادات لمانع الرعاة قبل تقديم أي مقطع. - اعرضها - توجد إرشادات - الإرشادات تحتوي على نصائح حول تقديم المقاطع. - عرض الإرشادات - اختر فئة المقطع - المقطع يدوم من %1$02d:%2$02d إلى %3$02d:%4$02d (%5$d دقيقة %6$02d ثانية)\nهل هو جاهز للإرسال؟ - المقطع من\n\n%1$s\nإلى\n%2$s\n\n(%3$s)\n\nجاهز للإرسال؟ - هل الأوقات صحيحة؟ - الفئة معطلة في الإعدادات. تمكين الفئة للإرسال. - هل تود تعديل التوقيت لبداية أو نهاية المقطع؟ - الوقت المحدد غير صحيح. - تعديل توقيت المقطع يدويًا - تعيين %s كبداية أو نهاية لمقطع جديد؟ - النهاية - ضع علامة على موقعين في شريط الوقت أولًا. - البداية - الآن - معاينة المقطع، والتأكد من تخطيه بسلاسة. - يجب أن تكون البداية قبل النهاية. - الوقت الذي ينتهي عنده المقطع - الوقت الذي يبدأ عنده المقطع - مقطع SponsorBlock جديد - إعادة تعيين - إعادة تعيين اللون - خارج الموضوع / النكات - تم إضافة مشاهد ملتقطة خارج الموضوع أو الفكاهة التي ليست مطلوبة لفهم المحتوى الرئيسي للفيديو. لا تتضمن مقاطع توفر تَعبِير أو تفاصيل الخلفية. - الأبرز - جزء الفيديو الذي يبحث عنه معظم الأشخاص. - التذكير بالتفاعل (الاشتراك) - تذكير قصير للإعجاب، أو الاشتراك، أو المتابعة في منتصف المحتوى. إذا كانت طويلة أو تتعلق بشيء محدد، فيجب أن تكون خاضعة للترويج الشخصي بدلاً من ذلك. - المقدمة / فاصل - فاصل زمني بدون محتوى فعلي. يمكن أن يكون توقفًا مؤقتًا أو إطارًا ثابتًا أو تكرار الرسوم المتحركة. لا يتضمن انتقالات تحتوي على معلومات. - الموسيقى: مقطع بدون موسيقى - فقط للاستخدام في مقاطع الفيديو الموسيقية. أقسام مقاطع الفيديو الموسيقية بدون موسيقى، والتي لم يتم تغطيتها بالفعل من قبل فئة أخرى. - الخاتمة / تترات النهاية - تتر النهاية أو عندما تظهر بطاقات نهاية YouTube، نهايات غير منطوقة. ليس للاستنتاجات مع المعلومات. - معاينة / خلاصة / ربط - مجموعة من المقاطع التي توضح ما هو قادم أو ما حدث في الفيديو أو في مقاطع فيديو أخرى من السلسلة، حيث تتكرر جميع المعلومات في مكان آخر. - ترويج شخصي / غير مدفوع الأجر - شبيهة بالراعي، باستثناء ما يتعلق بالإعلانات غير المدفوعة الأجر أو الذاتية. ويشمل ذلك أقسام عن السلع أو التبرعات أو المعلومات المتعلقة بمن تعاونوا مع ناشر المحتوى. - الراعي - الترويج المدفوع الأجر، والتوصيات المدفوعة الأجر، والإعلانات المباشرة. ليس للترويج الشخصي أو التصريحات المجانية للأسباب / المبدعين / مواقع الويب /المنتجات التي يحبونها. - نسخ - فشل تصدير: %s. - استيراد / تصدير الإعدادات - تكوين SponsorBlock JSON الخاص بك والذي يمكن استيراده/تصديره إلى ReVanced Extended ومنصات SponsorBlock الأخرى. - تكوين SponsorBlock الخاص بك كتنسيق JSON الذي يمكن استيراده / تصديره إلى ReVanced Extended وغيره من منصات SponsorBlock الأخرى. يتضمن هذا معرف المستخدم الفريد الخاص بك. تأكد من مشاركته بحكمة. - فشل استيراد: %s. - تم استيراد الإعدادات بنجاح. - تحتوي إعداداتك على معرف مستخدم خاص لـ SponsorBlock.\n\n معرف المستخدم الخاص بك يشبه كلمة المرور ويجب عدم مشاركته أبدًا.\n - لا تعرض مرة أخرى - تم نسخ الإعدادات إلى الحافظة. - التخطي تلقائيًا - التخطي تلقائيًا مرة واحدة - تخطي - الأبرز - تخطي مقطع غير ذي صلة - التخطي لأبرز المشاهد - تخطي التفاعل - تخطي المقدمة - تخطي الفاصل - تخطي الفاصل - تخطي غير الموسيقى - تخطي الخاتمة - تخطي النظرة العامة - تخطي الملخص - تخطي النظرة العامة - تخطي العرض الترويجي - تخطي الراعي - تخطي المقطع - تعطيل - عرض في شريط تقدم الفيديو - عرض زر التخطي - تم تخطي مقطع غير ذي صلة. - تم التخطي لأبرز المشاهد. - تم تخطي تذكير مزعج. - تم تخطي المقدمة. - تم تخطي الفاصل. - تم تخطي الفاصل. - تم تخطي عدة مقاطع. - تم تخطي جزء غير موسيقي. - تم تخطي الخاتمة. - تم تخطي النظرة العامة. - تم تخطي الملخص. - تم تخطي النظرة العامة. - تم تخطي الترويج الشخصي. - تم تخطي الراعي. - تم تخطي المقطع الغير المرسل. - SponsorBlock غير متوفر مؤقتًا. - SponsorBlock غير متوفر مؤقتًا (الحالة %d). - SponsorBlock غير متوفر مؤقتًا (انتهت مهلة API). - إحصائيات - الإحصائيات غير متوفرة مؤقتًا (API معطل). - جار التحميل... - سمعتك <b>%.2f</b> - لقد قمت بحفظ الناس من <b>%s</b> مقطع - %1$s ساعة %2$s دقيقة - %1$s دقيقة %2$s ثانية - %s ثانية - هذا يساوي <b>%s</b> من حياتهم.<br>اضغط هنا لرؤية لوحة المتصدرين. - انقر هنا لرؤية الإحصائيات العالمية وأبرز المساهمين. - SponsorBlock Leaderboard - تم تعطيل SponsorBlock. - لقد قمت بتخطي <b>%s</b> مقطع - إعادة تعيين عداد المقاطع التي تم تخطيها؟ - هذا يساوي <b>%s</b>. - لقد أنشأت <b>%s</b> مقطع - اضغط هنا لعرض المقاطع الخاصة بك. - اسم المستخدم الخاص بك: <b>%s</b> - انقر هنا لتغيير إسم المستخدم الخاص بك - غير قادر على تغيير اسم المستخدم: الحالة: %1$d %2$s. - تم تغيير اسم المستخدم بنجاح. - لا يمكن إرسال هذا المقطع.\nموجود بالفعل. - لا يمكن إرسال المقطع: %s. - غير قادر على إرسال المقطع: الحالة: %s. - غير قادر على إرسال المقطع.\n جارٍ الحد من معدل إرسالك (عدد كبير جدا من نفس المستخدم أو IP). - SponsorBlock متوقف مؤقتًا. - غير قادر على إرسال المقطع (الحالة: %1$d %2$s). - تم إرسال المقطع بنجاح. - لا يتم عرض الملاحظة في حالة عدم توفر SponsorBlock. - يتم عرض الملاحظة في حالة عدم توفر SponsorBlock. - عرض ملاحظة إذا كان API غير متوفر - تغيير الفئة - اعتراض - غير قادر على التصويت للمقطع: %s. - غير قادر على التصويت للمقطع (انتهت مهلة API). - غير قادر على التصويت للمقطع (الحالة: %1$d %2$s). - لا توجد مقاطع للتصويت عليها. - تأييد - تم نسخ الإعدادات إلى الحافظة. - تم نسخ الطابع الزمني إلى الحافظة. (%s) - تم نسخ الرابط إلى الحافظة. - تم نسخ عنوان URL مع الطابع الزمني إلى الحافظة. - الأصلي - أعجبني - أعجبني (Cairo) - قلب - قلب(ملون) - مخفي - تأثير الحركة عند النقر المزدوج - يجب أن يكون الهامش السفلي للوحة التعريف بين 0-64. - تكوين التباعد من شريط التقدم إلى لوحة التعريف، بين 0-64. - الهامش السفلي للوحة التعريف - يجب أن تكون نسبة الارتفاع بين 0-100 (%). - يقوم بتكوين نسبة ارتفاع المساحة الفارغة المتبقية عند إخفاء شريط التنقل، بين 0 و100 (%). - نسبة ارتفاع المساحة الفارغة - اضغط مع الاستمرار على الطابع الزمني لتغيير حالة تكرار فيديوهات Shorts. - إجراء الضغط المطول على الوقت - "يعرض قسم عنوان الفيديو في وضع ملء الشاشة. - -التقييد: يختفي عنوان الفيديو عند النقر عليه." - عرض قسم عنوان الفيديو - إذا تم تمكين التشغيل التلقائي، فسيتم تشغيل الفيديو التالي بعد انتهاء العد التنازلي. - إذا تم تمكين التشغيل التلقائي، فسيتم تشغيل الفيديو التالي على الفور. - تخطي العد التنازلي للتشغيل التلقائي - "لتخطي التخزين المؤقت المحمل مسبقًا في بداية مقاطع الفيديو لتطبيق جودة الفيديو الافتراضية على الفور. - -• عند بَدْء تشغيل الفيديو، يكون هناك تأخير قدره 0.3 ثانية تقريبًا. -• لا ينطبق على مقاطع فيديو HDR, مقاطع فيديو البث المباشر, مقاطع الفيديو التي تقل مدتها عن 15 ثوانٍ." - تخطي التخزين المؤقت المحمل مسبقًا - لا يتم عرض الملاحظة. - يتم عرض الملاحظة. - عرض ملاحظة عند التخطي - تشغيل هذا الإعداد قد يسبب مشاكل في تشغيل الفيديو. - تم تخطي التخزين المؤقت الذي تم تحميله مسبقًا. - يجب أن تكون قيمة تراكب السرعة بين 0-8.0. - قيمة تراكب السرعة بين 0-8.0. - قيمة تراكب السرعة - "خداع نسخة التطبيق الحالية إلى النسخة القديمة. - -• سيؤدي هذا إلى تغيير مظهر التطبيق، ولكن قد تحدث آثار جانبية غير معروفة -• إذا تم إيقاف تشغيله لاحقًا، فقد تظل واجهة المستخدم القديمة حتى يتم مسح بيانات التطبيق." - لم يتم تغيير اصدار التطبيق - تم تغيير اصدار التطبيق - 17.33.42 - استعادة تخطيط واجهة المستخدم القديم - 17.41.37 - استعادة رف قائمة التشغيل القديم - 18.05.40 - استعادة مربع إدخال التعليقات القديم - 18.17.43 - استعادة لوحة المشغل المنبثقة القديمة - 18.33.40 - استعادة شريط إجراءات Shorts القديم - 18.38.45 - استعادة سلوك جودة الفيديو الافتراضي القديم - 18.48.39 - تعطيل تحديث المشاهدات والإعجابات في الوقت الفعلي - 19.13-37 - استعادة نمط الرسوم المتحركة القديم للأرقام المتكررة - الهدف من خِداع إصدار التطبيق - اكتب هدف إصدار التطبيق الوهمي. - تعديل إصدار التطبيق الوهمي - إصدار تطبيق وهمي - "سيتم تغيير إصدار التطبيق إلى إصدار قديم من Youtube. - -سيؤدي هذا إلى تغيير مظهر ومميزات التطبيق، ولكن قد تحدث تأثيرات جانبية غير معروفة. - -إذا تم إيقاف تشغيله لاحقا، من المستحسن مسح بيانات التطبيق لمنع حدوث أخطاء في واجهة المستخدم." - "يوهم أبعاد الجهاز من أجل فتح جودة فيديو أعلى قد لا تكون متوفرة على جهازك. -قد يتم توفير الجودة العالية في بعض مقاطع الفيديو التي تتطلب أبعادًا عالية للجهاز، ولكن ليس كل مقاطع الفيديو." - إيهام أبعاد الجهاز - ترميز فيديو iOS هو AVC (H.264) أو VP9 أو AV1. - ترميز فيديو iOS هو AVC (H.264). - فرض iOS AVC (H.264) - "قد يؤدي تمكين هذا إلى تحسين عمر البطارية وإصلاح مشكلة تقطيع التشغيل. - -يتمتع تنسيق AVC (H.264) بدقة قصوى تبلغ 1080P، وسيستخدم تشغيل الفيديو بيانات إنترنت اكثر من VP9 أو AV1." - "• قائمة المقطع الصوتي مفقودة. -• مستوى الصوت الثابت غير متوفر." - "• قائمة المقطع الصوتي مفقودة. -• مستوى الصوت الثابت غير متوفر." - "• قد لا يتم تشغيل الأفلام أو الفيديوهات المدفوعة. -• يبدأ البث المباشر من البداية. -• قد تنتهي الفيديوهات قبل النهاية بثانية واحدة. -• لا يوجد ترميز الصوت Opus." - التأثيرات الجانبية للتزييف - • قد لا يتم تشغيل الفيديو. - تم إخفاء العميل المستخدم لجلب بيانات البث في إحصاءات تقنية. - يتم عرض العميل المستخدم لجلب بيانات البث في إحصاءات تقنية. - عرض في إحصاءات تقنية - "لا يتم تزييف بيانات البث. قد لا يعمل تشغيل الفيديو." - يتم تزييف بيانات البث. - Spoof Streaming Data - Android - Android TV - Android VR - iOS - العميل الافتراضي - إيقاف تشغيل هذا الإعداد قد يسبب مشاكل في تشغيل الفيديو. - يجب أن تكون حساسية تمرير مستوى السطوع بين 1-1000 (%). - تكوين الحد الأدنى للمسافة لتمرير السطوع بين 1 و1000 (%).\nكلما كانت المسافة الدنيا أقصر، كلما تغيرت مستويات السطوع بشكل أسرع. - حساسية تمرير مستوى السطوع - تم تعطيل إيماءات التمرير في وضع شاشة القفل. - تم تمكين إيماءات التمرير في وضع شاشة القفل. - إيماءات التمرير في وضع قفل الشاشة - تلقائي - مقدار الحد الأدنى للتمرير قبل اكتشاف الإيماءة. - -الافتراضي:0 - مقدار حد التمرير - قيمة شفافية خلفية واجهة التمرير (0-255). - -الافتراضي:127 - شفافية خلفية واجهة إيماءة التمرير - لا يمكن أن يزيد حجم المنطقة القابلة للتمرير السريع عن 50. - النسبة المئوية لمساحة الشاشة القابلة للتمرير السريع.\n\nملاحظة: سيؤدي هذا أيضًا إلى تغيير حجم مساحة الشاشة لإيماءة النقر المزدوج للتقديم أو التأخير. - حجم واجهة إيماءة التمرير - حجم النص على واجهة التمرير. - -الافتراضي:27 - حجم نص واجهة إيماءة التمرير - مقدار الوقت الذي تظهر فيه واجهة التمرير بعد التغيير (بجزء الثانية). - -الافتراضي:500 - مهلة واجهة إيماءة التمرير - يجب أن تكون حساسية تمرير مستوى الصوت بين 1-1000 (%). - تكوين الحد الأدنى للمسافة لتمرير مستوى الصوت بين 1 و1000 (%).\n\nكلما كانت المسافة الدنيا أقصر، كانت تغييرات مستوى الصوت أسرع.\n\nحساسية التمرير الموصى بها لمستوى الصوت هي 100% عند 15 خطوة لمستوى الصوت و10% عند 150 خطوة لمستوى الصوت. - حساسية تمرير مستوى الصوت - "لتبديل مواضع زر الإنشاء و زر الإشعارات عن طريق إيهام معلومات الجهاز. - -• قد يحتاج الجهاز إلى إعادة التشغيل حتى يسري تغيير هذا الإعداد. -• يؤدي تعطيل هذا الإعداد إلى تحميل المزيد من الإعلانات من جانب الخادم. -• يجب عليك تعطيل هذا الإعداد لجعل إعلانات الفيديو مرئية." - لا يتم تبديل زر الإنشاء بزر الإشعارات. - "تم تبديل زر الإنشاء بـزر الإشعارات. - -ملاحظة: يؤدي تمكين هذا أيضًا إلى إخفاء إعلانات الفيديو بالقوة." - تبديل أزرار الإنشاء و الإشعارات - "قد يؤدي تعطيل هذا إلى تحميل المزيد من الإعلانات من الخادم. - -كما لن يتم حظر الإعلانات في فيديوهات Shorts بعد الآن. - -إذا لم يتم تفعيل هذا الإعداد، فحاول التبديل إلى وضع التصفح المتخفي." - الإفتراضي - موسيقى RVX - %s لم يتم تثبيته. الرجاء تثبيته. - اسم الحزمة لموسيقى RVX المثبتة. - اسم حزمة موسيقى RVX - • سجل المشاهدة محظور. - "• يتبع إعدادات سجل المشاهدة لحساب Google. -• قد لا يعمل سجل المشاهدة بسبب DNS أو VPN." - • يتبع إعدادات سجل المشاهدة لحساب Google. - حالة سجل المشاهدة - انقر لفتح إدارة سجل مشاهدة YouTube. - إدارة كل السجلات - الأصلي - استبدال النطاق - حظر سجل المشاهدة - نوع سجل المشاهدة - فشلت إضافة القناة \'%1$s\' إلى القائمة البيضاء %2$s. - تم إضافة القناة \'%1$s\' إلى القائمة البيضاء %2$s. - لا توجد قنوات مدرجة في القائمة البيضاء. - لم يتم إضافتها إلى القائمة البيضاء. - فشل تحميل معلومات القناة. - تمت الإضافة إلى القائمة البيضاء. - سرعة التشغيل - إزالة القناة \'%1$s\' من القائمة البيضاء %2$s؟ - فشلت إزالة القناة \'%1$s\' من القائمة البيضاء %2$s. - تمت إزالة القناة \'%1$s\' من القائمة البيضاء %2$s. - التحقق من قائمة القنوات المضافة إلى القائمة البيضاء أو إزالتها. - قائمة القناة البيضاء - SponsorBlock - diff --git a/src/main/resources/youtube/translations/bg-rBG/missing_strings.xml b/src/main/resources/youtube/translations/bg-rBG/missing_strings.xml deleted file mode 100644 index b78fea5b5..000000000 --- a/src/main/resources/youtube/translations/bg-rBG/missing_strings.xml +++ /dev/null @@ -1,154 +0,0 @@ - - - Don\'t show again - The domain to fetch images from.\nNote: Only enter the domain name, i.e., without the \"https\:\/\/\" prefix. - Alternative domain - Courses / Learning - Chapters are enabled in the seekbar. - Chapters are disabled in the seekbar. - Disable seekbar chapters - "This will restore thumbnails to livestreams that do not have seekbar thumbnails. - -Internet data usage may be higher, and seekbar thumbnails will have a slight delay before showing. - -This feature works best with a very fast internet connection." - Seekbar thumbnails are medium quality. - Seekbar thumbnails are high quality. - Enable high quality thumbnails - "There is a YouTube server-side bug that causes rolling number text such as likes, views, and upload dates to be hidden for some users. - -A temporary workaround for this issue is to spoof the app version to 19.13.37. - -Do you want to spoof the app version before restarting the app?" - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - Highlighted search links are shown. - Highlighted search links are hidden. - Hide highlighted search links - Floating button is shown. - Floating button is hidden. - Hide floating button - 1080p Premium menu is shown. - 1080p Premium menu is hidden. - Hide 1080p Premium menu - Shopping shelf is shown. - Shopping shelf is hidden. - Hide player shopping shelf - Chapter labels next to the timestamp are shown. - Chapter labels next to the timestamp are hidden. - Hide seekbar chapter labels - About menu is shown. - About menu is hidden. - Hide About menu - Accessibility menu is shown. - Accessibility menu is hidden. - Hide Accessibility menu - Account menu is shown. - Account menu is hidden. - Autoplay menu is shown. - Autoplay menu is hidden. - Hide Autoplay menu - Billing and payments menu is shown. - Billing and payments menu is hidden. - Hide Billing and payments menu - Captions menu is shown. - Captions menu is hidden. - Hide Captions menu - Connected apps menu is shown. - Connected apps menu is hidden. - Hide Connected apps menu - Data saving menu is shown. - Data saving menu is hidden. - Hide Data saving menu - Manage all history menu is shown. - Manage all history menu is hidden. - Hide Manage all history menu - Live chat menu is shown. - Live chat menu is hidden. - Hide Live chat menu - Notifications menu is shown. - Notifications menu is hidden. - Hide Notifications menu - Background menu is shown. - Background menu is hidden. - Hide Background menu - Try experimental new features menu is shown. - Try experimental new features menu is hidden. - Hide Try experimental new features menu - Privacy menu is shown. - Privacy menu is hidden. - Hide Privacy menu - Purchases and memberships menu is shown. - Purchases and memberships menu is hidden. - Hide Purchases and memberships menu - Video quality preferences menu is shown. - Video quality preferences menu is hidden. - Hide Video quality preferences menu - Your data in YouTube menu is shown. - Your data in YouTube menu is hidden. - Hide Your data in YouTube menu - Disabled comments button or with label \"0\" is shown. - Disabled comments button or with label \"0\" is hidden. - Hide disabled comments button - Shown in channel. - "Hidden in channel. - -Info: -• Only shelves with the Shorts header on the home tab are hidden." - Hide in channel - YouTube Doodles are shown. - YouTube Doodles are hidden. - Hide YouTube Doodles - "YouTube Doodles show up a few days each year. - -If a YouTube Doodle is currently showing in your region and this setting is on, the filter bar below the search bar will also be hidden." - MMT Blue - MMT Green - MMT Orange - MMT Pink - MMT Turquoise - MMT Yellow - Revancify Yellow - Vanced Black - Vanced Light - Xisr Yellow - Return YouTube Username - @handle (Username) - Display format - Username (@handle) - Username - Handle is used. - Username is used. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - Estimated likes are hidden. - Estimated likes are shown. - Show estimated likes - Hidden - "Phrases like '#', 'Fundraiser', 'Shop' and 'products' were shown from the video subtitles." - "Phrases like '#', 'Fundraiser', 'Shop' and 'products' were hidden from the video subtitles." - Sanitize video subtitle - Adjust: Mark Start and End Time for segment - Verify the Segment - Edit the Segment - Forward by Specified Time (Default: 150ms) - Publish Created Segment - Rewind by Specified Time (Default: 150ms) - Brightness swipe sensitivity must be between 1-1000 (%). - Configure the minimum distance for brightness swiping between 1 and 1000 (%).\nThe shorter the minimum distance, the faster the brightness level changes. - Brightness swipe sensitivity - Volume swipe sensitivity must be between 1-1000 (%). - Configure the minimum distance for volume swiping between 1 and 1000 (%).\n\nThe shorter the minimum distance, the faster the volume level changes.\n\nRecommended volume swipe sensitivity is 100% at 15-volume steps and 10% at 150-volume steps. - Volume swipe sensitivity - diff --git a/src/main/resources/youtube/translations/bg-rBG/strings.xml b/src/main/resources/youtube/translations/bg-rBG/strings.xml deleted file mode 100644 index 9eddfe572..000000000 --- a/src/main/resources/youtube/translations/bg-rBG/strings.xml +++ /dev/null @@ -1,1556 +0,0 @@ - - - Включване на контролите за достъпност на видеоплеaра? - Вашите контроли са променени, защото е активирана услуга за достъпност. - Продължи - "GmsCore няма разрешение да работи във фонов режим. -Следвайте инструкциите „Don't kill my app!“ за вашето устройство и го приложете към вашия GmsCore. Това е необходимо, за да работи приложението." - "За да избегнете проблеми е необходимо да изключите оптимизацията на батерията за GmsCore. - -Натиснете \"Продолжи\" и изкючере оптимизацията на батерията." - Отвори интернет адрес \"website\" - Нужно е действие - Включете облачното известяване за да поучавате известия. - Отвори GmsCore - GmsCore не е инсталиран. Инсталирайте го. - "DeArrow предоставя миниатюри на публиката за видеоклипове. Тези миниатюри често са по-подходящи от тези, предоставени от самия YouTube. Ако е активирано, URL адресите на видео ще бъдат изпратени до API сървъра, без да се изпращат други данни. Ако видеоклипът няма миниатюри на DeArrow, ще се покажат или неговите оригинални миниатюри, или заснети кадри. Щракнете, за да научите повече за DeArrow." - Относно DeArrow - Невалиден DeArrow API URL. - URL адресът на крайната точка за съхранение на миниатюри DeArrow. - DeArrow API адрес - Не се показва известие, ако DeArrow не е наличен. - Показва се известие, ако DeArrow не е наличен. - Показване на известие, ако API не е наличен - DeArrow временно не е наличен. (код на състоянието: %s) - DeArrow временно не е наличен. - Начало /Home/ - Раздел \"Вие\" - Оригинални миниатюри - DeArrow & оригинални миниатюри - DeArrow & Неподвижни кадри - Неподвижни кадри - Плейлисти, предложения - Резултати от търсенето - Неподвижни миниатюри - Неподвижните кадри се вземат от началото / средата / края на всяко видео. Тези изображения са вградени в YouTube и не се използва външен API. - Неподвижни миниатюри - Използване на висококачествени снимки. - Използват се кадри със средно качество. Миниатюрите ще се зареждат по-бързо, но видеоклипове на живо, неиздадени или много стари може да показват празни миниатюри. - Използване на бързо заснемане на кадри - Начало на видеото - Средата на видеото - Края на видеото - Времето на видеоклипа, от който ще бъдат взети кадрите - Абонаменти - Индикатор на времето за възпроизвеждане е Изкл. - "Индикатор на времето за възпроизвеждане е Вкл." - Индикатор на времето за възпроизвеждане - Индикатор за скорост на възпроизвеждане. - Индикатор за качество на видеото. - Добавете тип информация - Подсветката около видеото е изключен при пестене на енергия. - Подсветката около видеото е включен при пестене на енергия. - Заобикаляне на ограниченията за подсветка около видеото - Оригиналният домейн се използва за зареждане на изображения\n\nАктивирането на тази настройка може да коригира зареждането на изображения, които са блокирани в някои региони. - Домейнът yt4.ggpht.com се използва за зареждане на изображения. - Прескочете забраната за зареждане на изображение - Оригинал - Телефон - Телефон (Max 480 dip) - Таблет - Таблет (Мин 600 dip) - Промяна на резолюция - Използват се бутони за превключване. - Използват се превключващи бутони с текст. - Промяна на типа превключване на настройките - Списък с приложения за спделяне – вграден. - Използва се от системният лист за споделяне. - Списък със приложения за споделяне - Автоматично изпълнение - По подразбиране - Пауза - Повтори - Промяна на състоянието - повторение на Shorts - Разглеждане на канала - По подразбиране - Проучване - Игри - История - Библиотека - Харесани - На Живо - Филми - Музика - Търсене - Shorts - Спорт - Абонаменти - Популярни - За Гледане по-късно - Промяна на началната страница - Началната страница се променя само веднъж. - "Началната страница винаги се променя. - -Ограничение: Бутонът за връщане назад в лентата с инструменти може да не работи." - Промяна на типа на началната страница - Включен стандартен логотип. - Включен логотип Premium. - Промяна на логото на YouTube - Списък с низове за изграждане на пътя на компонента, които да се филтрират, разделени с нов ред. - Потребителски филтър - Потребителският филтър е деактивиран. - Потребителският филтър е активиран. - Активиране на потребителските филтри - Невалиден потребителски филтър: %s. - Използва се падащ панел в стар стил. - Използва се персонализиран диалогов прозорец. - Персонализиран панел за скорост на възпроизвеждане - Скоростите по избор трябва да са по-малки от %sx. Връщане на стойностите по подразбиране. - Невалидна скорост на видеото. Връщане на стойности по подразбиране. - Добавяне или смяна на възможните скорости. - Редактиране на скоростите по избор на видеото - Непрозрачността на наслагването на плейъра трябва да бъде между 0-100. Нулирайте стойностите по подразбиране. - Стойност на прозрачност между 0-100, където 0 е прозрачно. - Прозрачност на на плейъра - Въведете кода за цвят на лентата за време. - Стойност по избор за цвят на лентата за време - За да отваряте връзки към YouTube с помощта на RVX, конфигурирайте „Отваряне на поддържани връзки“ и активирайте поддържаните уеб адреси, които искате. - Отвори настройките по подразбиране - Скорост на възпроизвеждане по подразбиране - Предпочитано качество при мобилни данни - Предпочитано качество при Wi-Fi - Disables ambient mode for fullscreen only. - Подсветка около видеото е активиран на цял екран. - Подсветка около видеото е деактивирана на цял екран. - Деактивирайте подсветка около видеото на цял екран - Disables ambient mode. - Подсветка около видеото е активирана. - Подсветка около видеото е деактивирана. - Деактивирайте подсветката около видеото - Задължителните аудио записи са активирани. - Задължителните аудио записи са деактивирани. - Принудителните автоматични аудио пътеки са деактивирани - Принудителните автоматични субтититри са включени. - Принудителните автоматични субтититри са изключени. - Изкл. принудителни автоматични субтититри - Изскачащите панели на плейъра са активирани. - Изскачащите панели на плейъра са деактивирани. - Изскачащи прозорци на плейъра - "Автоматичното превключване на миксирани плейлисти е активирано, когато автоматичното пускане е включено. -Автоматичното пускане може да се промени в настройките на YouTube: -Настройки → Автоматично пускане → Автоматично пускане на следващия видеоклип" - Автоматичното превключване на миксирани плейлисти е изключено. - Деактивирайте превключването на микс плейлисти - Активирането на тази функция ще деактивира автоматичното превключване към YouTube Mix при възпроизвеждане на музика, докато автоматичното пускане е включено. - Скоростта на възпроизвеждане по подразбиране е активирана за потоци на живо. - Скоростта на възпроизвеждане по подразбиране е деактивирана за потоци на живо. - Деактивирайте скоростта на възпроизвеждане за потоци на живо - Скоростта на възпроизвеждане по подразбиране е активирана за музика. - "Скоростта на възпроизвеждане по подразбиране не се прилага за музикални видеоклипове - - Тази настройка може да не се прилага за видеоклипове, които не включват функцията „Слушане с YouTube Music“." - Деактивирайте скоростта на възпроизвеждане за музика - Панелът за взаимодействие е активиран. - Панелът за взаимодействие е деактивиран. - Панел за взаимодействие - Вибрация за обратна връзка е включена. - Вибрация за обратна връзка е изключена. - Изкл. вибрация за обратна връзка при главите - Вибрация за обратна връзка е включена. - Вибрация за обратна връзка е изключена. - Изкл. вибрация при плъзгащи контроли - Вибрация за обратна връзка е включена. - Вибрация за обратна връзка е изключена. - Изкл. хаптична обратна връзка при превъртане - Вибрация за обратна връзка е включена. - Вибрация за обратна връзка е изключена. - Изкл. вибрация за обратна връзка при превъртане - Вибрация за обратна връзка е включена. - Вибрация за обратна връзка е изключена. - Изкл. вибрация при зум - Автоматичната яркост при HDR е включена. - Автоматичната яркост при HDR е изключена. - Деактивирайте автоматичната HDR яркост - HDR клиповете са включени. - HDR клиповете са изключени. - Изкл. HDR клипове - Пейзажният режим в режим на цял екран е активиран. - Пейзажният режим в режим на цял екран е деактивиран. - Пейзажен режим при отиване на цял екран - Бутоните \"Харесвам\" и \"Не харесвам\" ще светят, когато бъдат натискани. - Бутоните \"Харесвам\" и \"Не харесвам\" няма да светят, когато бъдат натискани. - Изключете подсветката на бутоните „Харесвам“ и „Не харесвам“ - "Изключване на CronetEngine's QUIC протокол." - Изключване на QUIC протокол - Shorts плейъра при стартиране на приложението се показва. - Shorts плейъра при стартиране на приложението е скрит. - Скриване на Shorts плейъра при стартиране на приложението - Анимацията на числа в реално време е активирана. - Анимацията на числа в реално време е деактивирана. - Анимация на числа в реално време - Анимацията на фонтан е активирана над бутона „Харесва ми“. - Анимацията на фонтан е деактивирана над бутона „Харесва ми“. - Деактивиране анимацията на бутона „Харесва ми“ - "Деактивирайте „Възпроизвеждане с 2x>> скорост при продължително натискане. - -Бележки: -• Активирането на тази настройка ще възстанови функцията „Плъзнете наляво или надясно за търсене“. -• Деактивирането на тази настройка не налага активирането на скоростния интерфейс." - Скрива бутона за скорост - Новата начална анимация е включена. - Новата начална анимация е изключена. - Анимация при стартиране на приложението - "Деактивирайте следните взаимодействия, когато се отвори описанието на видеоклипа: - -• Докоснете за превъртане. -• Натиснете продължително, за да изберете текст." - Деактивирайте взаимодействието с описание на видеоклипа - VP9 кодек е включен. - "Кодек VP9 е деактивиран. -• Максималната разделителна способност е 1080p. -• Възпроизвеждането на видео ще използва повече интернет данни от VP9. -• За да получите възпроизвеждане на HDR, HDR видеото все още използва кодека VP9." - Деактивирайте кодека VP9 - Темата Кайро в лентата за напредък е деактивирана. - "Лентата за прогрес на тема Кайро е активирана. -Страничен ефект: -Темата Кайро се прилага и към точките за известия." - Лента за прогрес на тема Кайро - По-малките стилови контроли са деактивирани. - По-малките стилови контроли са активирани. - Вкл. компактни контроли - Скоростта по избор на видеото е изключена. - Скоростта по избор на видеото е включена. - Вкл. на скорост на видеото по избор - Стойността за избор на цвят на лентата за време е изключена. - Стойността за избор на цвят на лентата за време е включена. - Промяна на цвета на индикатора за време - Файлове с отчети за грешки не включват буфера. - Файлове с отчети за грешки в буфера. - Вкл. отчети за грешки - Отчетите за грешки са изключени. - Дневникът за остраняване на грешки е активиран. - Активиране на регистрирането на грешки - Скоростта на възпроизвеждане по подразбиране не се прилага за Shorts. - Скоростта на възпроизвеждане по подразбиране се прилага за Shorts. - Променете скоростта на възпроизвеждане на Shorts - Външния браузър е изключен. - Външния браузър е включен. - Включване на външен браузър - Екранът за зареждане с градиент е деактивиран. - Екранът за зареждане с градиент е активиран. - Градиентен екрана за зареждане - Разстоянието между бутоните за навигация ежнормално. - Разстоянието между бутоните за навигация е по-тясно. - Бутони за навигация в тесен стил - Следване на правилата за пренасочване по подразбиране. - Заобикаляне на URL пренасочвания. - Отваряне на връзки директно - Активирайте кодека OPUS, ако съдържанието в плейъра е подходящо за кодека. - Активирайте кодека OPUS - Не се запазва или възстаовява яркостта при включване или изключване на цял екран. - Запазване и възстаовяване яркостта при включване или изключване на цял екран. - Вкл. запазване и възстановяване на яркост - Докосването на лентата за време е изключено. - Докосването на лентата за време е включено. - Активиране на докосването на лентата за време - Времевите показатели са деактивирани. - "Времевите показатели са активирани. -Ограничения: -• Тази настройка позволява не само времеви показатели, но позволява скриване на елементи от потребителския интерфейс чрез докосване на фона на екрана за възпроизвеждане. -• Тъй като това е функция на Google, която все още се разработва, оформлението може да се развали." - Активиране на дата и час - Задаването на яркост чрез плъзгане е изключено. - Задаването на яркост чрез плъзгане е включено. - Задаване на яркост чрез плъзгане - Вибрация за обратна връзка е изключена. - Вибрация за обратна връзка е включена. - Разрешаване на вибрация - Когато намалите яркостта с жест до минимум, автоматичната яркост Не се активира. - Когато намалите яркостта с жест до минимум, се активира автоматична яркост. - Управление авто-яркост с жестове - Докоснете, за да активирате жеста за плъзгане. - Докоснете и задръжте, за да активирате жеста за плъзгане. - Активиране на плъзгащи контроли - Жестовете нагоре/надолу няма да възпроизведат следващия/предишния видеоклип. - Жестове нагоре/надолу за възпроизвеждане на следващо/предишно видео. - Активирайте жестове за превключване на видеоклипове - Настройването на звука чрез плъзгане е изключено. - Настройването на звука чрез плъзгане е включено. - Настройване на звука чрез плъзгане - Навигационната лента е непрозрачна. - Навигационната лента е полупрозрачна. - Полупрозрачна лента за навигация - Превключването към цял екран чрез плъзгане на долната част на плейъра е деактивирано. - Превключването към цял екран чрез плъзгане на долната част на плейъра е активирано. - Жест за превключване на цял екран - "Тази опция ще деактивира бутона \"Настройки\" в раздела \"Вие\" -Използвайте следната последователност: -раздел \"Вие\" -> Страница на канала -> Меню -> Настройки" - Активирайте широката лента за търсене в раздела \"Вие\" - Широката лента за търсене е изключена - Широката лента за търсене е включена - Широка лента за търсене - Широка лента за търсене замества логото на YouTube. - Логото на YouTube се появява до широката лента за търсене. - Широка лента за търсене с лого - Описание - "Въведете заглавие в описанието на видеоклипа на вашия език. -Опцията „Отваряне на описанието автоматично“ може да не работи, ако стойността на заглавието не съвпада със заглавието в описанието." - Заглавие в панела с описание на видеоклипа - Описанието на видеоклипа не се отваря автоматично. - Описанието на видеоклипа се отваря автоматично. - Автоматично отваряне на видео описание - Желаете ли да продължите? - Възстановяване на стандартните стойности. - Рестартирайте, за да заредите оформлението нормално - Опреснете и рестартирайте - Неуспешно експортиране на настройките. - Настройките са експортирани успешно. - Експортирайте настройките във файл. - Експорт на настройки - Внасяне на настройки - Копирай - Импортирайте или експортирайте настройки като текст. - Импортирайте / Експортирайте настройки в текст - Неуспешно импортиране на настройките. - Настройките се нулират до стойностите по подразбиране. - Настройките са импортирани успешно. - Импортиране на настройки от файл. - Внасяне на настройки - Нулирай - Търсене %s - Настройки ReVanced - Външна програма за изтегляне - Не е инсталирано - "%1$s не е инсталиран. -Моля, изтеглете %2$s от уебсайта." - Внимание - %s не е инсталирано. Моля инсталирайте го. - Име на пакета на приложението за изтегляне като NewPipe или YTDLnis. - Име на приложението за изтегляне - Име на пакета на приложението за изтегляне като NewPipe или YTDLnis. - Име на приложението за изтегляне на видео - "Видеоклиповете ще превключат в режим на цял екран в следните случаи: - -• Когато започне видео. -• Когато се натисне клеймо за време в коментарите." - Принудително винаги на цял екран - Разделен с нов ред списък с имена на раздели, на страници, на канали за филтриране. - Промяна на филтъра на менюто на акаунта - "Скриване на елементи от менюто на акаунта и раздела \"Вие\". -Някои компоненти не могат да бъдат скрити." - Скриване на менюто на акаунта - Албумните карти се показват. - Албумните карти са скрити. - Скриване на албумни карти - Показват се секциите „Популярни места“, „Игри“ и „Музика“ под описанието. - Секциите „Популярни места“, „Игри“ и „Музика“ под описанието са скрити. - Раздел с функции - Прегледа на авт. изпълнение се показва. - Прегледа на авт. изпълнение е скрит. - Скриване на прегледа на авт. изпълнение - Бутон за разглеждане на магазина се показва. - Бутон за разглеждане на магазина е скрит. - Бутон за разглеждане на магазина - "Скрива следните рафтове: -- Извънредни новини -- Продължете да гледате -- Разгледайте още канали -- Пазаруване -- Гледайте отново" - Скриване на рафта с Препоръчани - Показване на горната лента с категории в емисията. - Скриване на горната лента с категории в емисията. - Скриване на горната лента с категории в емисията - Свързани видеоклипове се показват. - Сродните видеоклипове са скрити. - Скриване в сродни видеоклипове - Резултатите от търсенето соказват. - Резултатите от търсенето са скрити. - Скриване на резултатите от уеб търсенето - Насоките на канала се показват. - Насоките на канала са скрити. - Скриване на насоките на канала - Рафта с членуващи се показва. - Рафта с членуващи е скрит. - Скриване на секцията с членуващи - Показани са в горната част връзки към профила на канала. - Връзките в горната част на профила на канала са скрити. - Скриване на връзките към канала - "Примери: - Shorts -Плейлисти -Маркет" - Разделен с нов ред списък с имена на раздели, на страници, на канали за филтриране. - Промяна на филтъра за раздели на канала - Филтърът за раздела на канала е деактивиран. - Филтърът за раздела на канала е активиран. - Използвайте филтър за раздели на канали - Водният знак на канала се показва. - Водният знак на канала е скрит. - Воден знак на канала - Секцията с заглавия се показва. - Разделът със секции е скрит. - Скриване на секцията с глави - Филмовите рафтове се показват. - Филмовите рафтове са скрити. - Скриване на филмовите рафтове - Бутона за клип се показва. - Бутона за клип е скрит. - Скриване на бутона за клип - Бутонът за създаване на Кратко видео се показва. - Бутонът за създаване на Кратко видео е скрит. - Бутон за създаване на Shorts - Бутона за благодарност се показва. - Бутона за благодарност е скрит. - Скриване на бурона за благодарност - Бутоните за клеймо за време и емотикони се показват. - Бутоните за клеймо за време и емотикони са скрити. - Скриване на инструмента за избор на емоджи и клеймо за време - Банера за коментари от членове се показва. - Банера за коментари от членове е скрит. - Скриване на банер за коментари от членове - Секцията с коментари в началната емисия се показва. - Секцията с коментари в началната емисия е скрита. - Скриване на секцията с коментари в началната емисия - Секцията с коментари се показва. - Секцията с коментари е скрита. - Скриване на секцията с коментари - Показват се в канала. - Скрит в канала. - Скриване на страницата на канала - Свързаните видеоклипове се показват. - Свързаните видеоклипове са скрити. - Скриване в емисията свързаните видеоклипове - Показва се в емисията „Абонаменти“. - Скрит в емисията „Абонаменти“. - Публикации в общността и в абонаменти - Появява се. - -Относно раздела „Как е създадено това съдържание“. - Разделът „Как е създадено това съдържание“ е скрит. - Секция със съдържание - Кутията за дарения се показва. - Кутията за дарения е скрита. - Дарителска кутия - Филтърът за наслагване с двойно докосване се показва. - Филтърът за наслагване с двойно докосване е скрит. - Докоснете два пъти, за да скриете филтъра за наслагване - Бутона за изтегляне се показва. - Бутона за изтегляне е скрит. - Скриване на бутона за изтегляне - Препоръките в края се показват - Препоръките в края са скрити - Скриване на препоръките в края - Показват се. - Падащите менюта са скрити. - Скриване на показващи се раздели под видеоклипове - Показват се разширяеми секции. - Разширяващите се секции са скрити. - Разширяеми секции - Бутона за субтити се показва. - Бутона за субтити е скрит. - Бутон за субтити - Разделен с нов ред списък с имена на раздели, на страници, на канали за филтриране. - Филтър за изскачащо меню на лентата - Изскачащите менюта за емисии са изключени. - Изскачащите менюта за емисии са включени. - Филтър за изскачащо меню на лентата - Лентата за търсене в емисията се показва. - Лентата за търсене в емисията е скрита. - Лента за търсене в емисията - Анкетите за емисии се показват. - Анкетите за емисии са скрити. - Скриване на анкети в емисиите - Филмовата лента се показва. - Филмовата лента е скрита - Скриване на филмовата лента - Скриване на изскачащ бутон - Плаващия бутон за микрофона се показва. - Плаващия бутон за микрофона е скрит. - Плаващ бутон за микрофона - Секцията \'За Вас\' се показва. - Секцията \'За Вас\' е скрита. - Скриване на секцията \'За Вас\' - Рекламите в режим на цял екран са показани. - Рекламите в режим на цял екран са скрити. - Скриване на рекламите в режим на цял екран - "Рекламите на цял екран са блокирани. - -Страничен ефект: Изображенията в публикации в общността може да бъдат блокирани на цял екран." - Рекламите на цял екран се затварят чрез бутона Затвори. - Как да затворите реклами на цял екран - Общите реклами се показват. - Общите реклами са скрити. - Скриване на общите реклами - YouTube Premium промоциите се показват. - YouTube Premium промоциите са скрити. - Скриване на YouTube Premium промоциите - Сивите разделители са показани. - Сивите разделители са скрити. - Скриване на сивия разделител - Идентификаторът се показва. - Идентификаторът е скрит. - Скриване на връзки - Бутон за търсене на изображения се показва. - Бутон за търсене на изображения е скрит. - Бутон за търсене на изображения - Рафтовете със снимки се показват. - Рафтовете със снимки са скрити. - Скриване на рафтовете със снимки - Разделът за информационни карти е показан. - Разделът за информационни карти е скрит. - Скриване на раздела за информационни карти - Информационните карти се показват. - Информационните карти са скрити. - Скриване на инфо. карти - Информационните панели се показват - Информационните панели са скрити. - Скриване на информационните панели - Бутона за клип се показва. - Бутона за присъединяване е скрит - Скриване на бутон за присъединяване - Раздел „Ключови понятия“ се показват. - Раздел „Ключови понятия“ са скрити. - Раздел „Ключови понятия“ - "Началната страница, публикациите и резултатите от търсенето се филтрират, за да се скрие съдържание, което съответства на ключови думи. -Ограничения: -•Някои Shorts може да не са скрити. -• Някои елементи на потребителския интерфейс може да не са скрити. -• Търсенето на дума -ключ може да не покаже никакви резултати." - За филтриране с ключови думи - Ограждането на ключова дума/фраза с двойни кавички ще предотврати частични съвпадения на заглавия на видеоклипове и имена на канали<br><br>Например,<br><b>\"ai\"</b> ще скрие видеоклипа: <b>How does AI work?</b><br>но няма да скрие: <b>What does fair use mean?</b> - Съвпадение на цялата дума - Коментарите не се филтрират. - Коментарите се филтрират. - Скриване на коментари по ключови думи - Видеоклиповете в раздела Начало не се филтрират по ключови думи. - Видеоклиповете в раздела Начало се филтрират с помощта на ключови думи. - Скриване на видеоклипове в началната страница с ключови думи - "Ключови думи и фрази, които да бъдат скрити, разделени с нови редове\n\nДуми с главни букви в средата трябва да бъдат въведени с големи букви (например: iPhone, TikTok, LeBlanc)." - Ключови думи, които да бъдат скрити - Резултатите от търсенето не се филтрират по ключови думи. - Резултатите от търсенето се филтрират с помощта на ключови думи. - Скриване на резултати от търсения с ключови думи - Видеоклиповете в емисията за абонаменти не се филтрират. - Видеоклиповете в емисията за абонаменти се филтрират. - Скриване на видеоклипове от абонаменти с ключови думи - Ключовата дума „%1$s“ ще скрие всички видеоклипове. - Невалидна ключова дума. Не може да се използва: „%s“ като филтър - Добавете кавички, за да използвате ключова дума: %s. - Ключовата дума има противоречиви твърдения: %s. - Ключовата дума е твърде кратка и изисква кавички: %s. - Последните постове се показват. - Последните постове са скрити. - Скриване на последните постове - Бутона за последните клипове се показва. - Бутона за последните клипове е скрит. - Скриване на бутон за последните клипове - Бутоните \"Харесвам\" и \"Не харесвам\" се показват. - Бутоните \"Харесвам\" и \"Не харесвам\" са скрити. - Скриване на бутоните за харесване и нехаресване - Показват се съобщения от чат на живо.\n\nТази настройка се отнася и за кратки видеоклипове на живо. - Съобщенията в чата на живо са скрити.\n\nТази настройка се отнася и за кратки видеоклипове на живо. - Съобщения за чат на живо - Бутонът за повторно възпроизвеждане на чат на живо се показва.\n\nНе се появява на цял екран при затваряне на чат на живо. - Бутонът за повторно възпроизвеждане на чат на живо е скрит.\n\nНе се появява на цял екран при затваряне на чат на живо. - Бутон за повторение на чата на живо - Скрийте видеоклипове с по-малко от 1000 гледания от емисията и от канали, за които сте се абонирали. - Скриване на видеоклипове с малко гледания - Медицински панел се показва. - Медицински панел скрит. - Медицински информационен панел - Банерите за стоки се показват. - Банерите за стоки са скрити. - Скриване банерите за стоки - Плейлист микса се показва. - Плейлист микса е скрит. - Скриване на микс плейлист - Филмовите рафтове се показват. - Филмовите рафтове са скрити. - Скриване на филмовите рафтове - Навигационната лента се показва. - Навигационната лента е скрита. - Скриване лентата за навигация - Бутона за създаване се показва. - Бутонът за създаване е скрит. - Бутон Създай клип - Бутона за начало се показва - Бутона за начало е скрит - Скриване на бутон за Начало - Навигационния панел се показва. - Навигационния панел е скрит. - Навигационен панел - Бутона за библиотека се показва - Бутона за библиотека е скрит - Бутона за Библиотека - Бутонът за известия се показва. - Бутонът за известия е скрит. - Бутон за Известия - Бутона за кратки клипове се показва. - Бутона за кратки клипове е скрит. - Бутон кратки клипове Shorts - Бутона за абонаменти се показва. - Бутона за абонаменти е скрит. - Бутона за Абонаменти - Бутона \"Уведоми ме\" се показва. - Бутона \"Уведоми ме\" е скрит. - Скриване на бутона \"Уведоми ме\" - Промоционалните етикети се показват. - Промоционалните етикети са скрити. - Скриване на платените промоции - Игрите в YouTube се показват. - Игри в YouTube са скрити. - Игри в YouTube - Бутона за авто. изпълнение се показва. - Бутона за авто. изпълнение е скрит. - Скриване на бутона за авто. изпълнение - Бутона за субтити се показва. - Бутона за субтити е скрит. - Скриване на бутона за Субтитри - Бутонът за предаване се показва. - Бутонът за предаване е скрит. - Скриване на бутона за предаване на Тв - Бутон за минимизиране се показва. - Бутон за минимизиране е скрит. - Бутон за минимизиране - Менюто за подсветка около видеото се показва. - Менюто за подсветка около видеото е скрито. - Подсветка около видеото - Менюто “Audio Track” се показва. - Менюто “Audio Track” е скрито. - Меню на аудио - Долният колонтитул на менюто с надписи се показва. - Долният колонтитул на менюто с надписи е скрит. - Скриване на менюто за избор на качество - Менюто за субтитрие се показва. - Менюто за субтитрие скрито. - Скриване на менюто за субтитри - Менюто & за помощ се показва. - Менюто & за помощ е скрито. - Скриване на менюто & за помощ - Слушане с YouTube Music се показва. - Слушане с YouTube Music е скрито. - Скриване на менюто слушане с YouTube Music - Менюто на заключен екран се показва. - Менюто на заключен екран е скрито. - Скриване меню на заключен екран - Менюто за повторение се показва. - Менюто за повторение е скрито. - Скриване на менюто за повторение - Менюто за повече информация се показва. - Менюто за повече информация е скрито. - Меню за повече информация - Менюто Картина в картината се показва. - Менюто Картина в картината е скрито. - Меню \"Картина в картината\" - Менюто за скорост на видеото се показва. - Менюто за скорост на видеото е скрито. - Меню за скорост на видеото - Менюто за премиум контроли се пказва. - Менюто за премиум контроли е скрито. - Скриване на менюто за премиум контроли - Предложение в менюто за избор на качество се показва. - Предложение в менюто за избор на качество е скрито. - Скриване на подсказка в менюто за избор на качество - Предложение в менюто за избор на качество се показва. - Предложение в менюто за избор на качество е скрито. - Скриване на подсказка в менюто за избор на качество - Менюто за докладване се показва. - Менюто за докладване е скрито. - Скрий Меню за докладване - Менюто на таймера за заспиване се показва. - Менюто на таймера за заспиване е скрито. - Скрийте менюто „Изчакване на заспиване“ - Стабилно ниво на звука се показва. - Постоянно ниво на звука е скрито. - Скрийте елемента \"Стабилно ниво на звука\" - Менюто \"Статистика за системни администратори\" се показва. - Менюто \"Статистика за системни администратори\" е скрито. - Меню \"Статистика за сис. администратори\" - Менюто за гледане в VR се показва. - Менюто за гледане в VR е скрито. - Меню за гледане в VR - Бутон за цял екран се показва. - Бутон за цял екран е скрит. - Бутон за Цял екран - Бутоните се показват. - Бутоните са скрити. - Скриване предишен & следващ бутон - Бутрона за YouTube Music се показва. - Бутрона за YouTube Music е скрит. - Скриване на бутона за YouTube Music - Бутон \"Запазване\" се показва. - Бутон \"Запазване\" е скрит. - Бутон \"Запазване\" - Показва се секцията „Подкасти“. - Разделът „Подкасти“ е скрит. - Скрийте секцията „Подкасти“ - Прегледа на коментар се показва. - Прегледа на коментар е скрит. - Скр. преглед на коментар - Това преоразмерява секцията за коментари, така че е невъзможно да се отвори повторението на чата на живо в секцията за коментари. - Това не променя размера на секцията за коментари, така че е възможно да отворите повторението на чата на живо в секцията за коментари. - Скриване на типа коментар за визуализация - Банерът за известия за промоциите се показва. - Банерът за известия за промоциите е скрит. - Скриване на банери с известия за промоция - Бутона за коментиране се показва. - Бутон за коментари е скрит. - Скриване на бутона за коментари - Бутона за нехаресване се показва. - Бутона за нехаресване е скрит. - Скриване на бутона за нехаресване - Бутона за харесване се показва. - Бутона за харесване е скрит. - Скриване на бутона за харесване - Бутона за чат се показва. - Бутона за чат е скрит. - Скриване на бутона за чат - Бутона за повече се показва. - Бутона за повече е скрит. - Скриване на бутон за Още - Бутонът за отваряне на микс плейлист се показва. - Бутона за микс плейлист е скрит. - Скриване на бутона за отваряне на микс плейлист - Бутонът за отваряне на плейлиста се показва. - Бутона за микс плейлист е скрит. - Скриване на бутона за плейлист - Бутон \"Запазване\" се показва - Бутон \"Запазване\" е скрит - Бутон \"Запазване\" - Бутона за споделяне се показва. - Бутона за споделяне е скрит. - Скриване на бутона за споделяне - Бързи действия се показват. - Бързи действия са скрити. - Скриване на меню с Бързи действия - "Скрива следните препоръчани видеоклипове: - -• С етикет „Само за членове“. -• С фрази като „Хората също са гледали“ под видеоклипа. -• От канали, за които не сте абонирани (по-малко от 1000 гледания)." - Скриване на Препоръчани видеоклипове - Показан. -Има предвид раздела за още видеоклипове в бързи действия и свързани видеоклипове. - Скрит. -Скрива раздела с още видеоклипове в бързи действия и свързани видеоклипове. - Свързани видеоклипове - Сродните клипове се показват. - Сродните клипове са скрити. - Скриване в сродни видеоклипове - "Тази настройка ограничава максималния брой оформления, които могат да бъдат заредени на екрана на плейъра. - -Ако оформлението на екрана на плейъра се промени поради промени от страна на сървъра, нежеланите оформления може да бъдат скрити на екрана на плейъра." - Бутона за ремикс се показва. - Бутона за ремикс е скрит. - Скриване на бутона за ремикс - Бутона са докладване се показва. - Бутона са докладване е скрит. - Скриване на бутона за докладване - Бутона за наградите се показва. - Бутона за наградите е скрит. - Бутон \"Награди\" - Показват се миниатюри в историята на търсене. - Миниатюрите в историята на търсене са скрити. - Подсказки, миниатюри на думи за търсене - Съобщението при превъртане се показва. - Съобщението при превъртане е скрито. - Скриване на съобщение при превъртане - Съобщение при пренавиване се показва. - Съобщение при пренавиване е скрито. - Съобщение при пренавиване - Лентата за време на плейъра се показва. - Лентата за време на плейъра е скрита. - Миниатюрите се показват. - Миниатюрите са скрити. - Миниатюри на лентата за възпроизвеждане - Скриване на лента за време на плейъра - Самоспонсорираните карти се показват. - Самоспонсорираните карти са скрити. - Скриване на самоспонсорирани карти - Скриване на менюто на акаунта - Менюто за субтитрие скрито. - Скриване на менюто за субтитри - Основни настройки се показват. - Основни настройки са скрити. - Меню \"Основни настройки\" - Менюто за гледане на ТВ се показва. - Менюто за гледане на ТВ е скрито. - Меню за гледане на Телевизор - Меню \"Семеен център\" се показва. - Меню \"Семеен център\" е скрито. - Меню \"Семеен център\" - Скриване на елементи в менюто с настройки на YouTube. - Филтриране на менюто с настройки на YouTube - Бутона за споделяне се показва. - Бутона за споделяне е скрит. - Скриване на бутона за споделяне - Бутона за пазаруване се показва - Бутона за пазаруване е скрит - Скриване на бутона за пазаруване - Връзките за пазаруване се показват. - Връзките за пазаруване са скрити. - Скриване на връзки за пазаруване - Лентата на канала е показана. - Лентата на канала е скрита. - Скриване на лентата на канала - Бутон за коментари се показва. - Бутон за коментари е скрит. - Скриване на бутона за коментари - Бутона за нехаресване се показва. - Бутона за нехаресване е скрит. - Скриване на бутона за нехаресване - "Изскачащи бутони като „Използване на този звук“ Се показват в раздела Shorts." - "Изскачащи бутони като „Използване на този звук“ са скрити в раздела Shorts." - Скриване на изскачащ бутон - Етикетът за видео връзка се показва. - Етикетът за видео връзка е скрит. - Скриване на етикет за връзка към видеоклипа - Зелен бутон на екрана се показва. - Зелен бутон на екрана е скрит. - Зелен бутон на екрана - Информационните панели се показват. - Информационните панели са скрити. - Скриване на информационните панели - Бутона за присъединяване се показва - Бутона за присъединяване е скрит. - Скриване на бутон за присъединяване - Бутона за харесване се показва. - Бутона за харесване е скрит. - Скриване на бутона за харесване - Показва се раздела Чата на живо.\n\nБутонът за връщане назад в раздела няма да бъде скрит. - Раздела Чата на живо е скрит.\n\nБутонът за връщане назад в Раздел няма да бъде скрит. - Скриване на раздела на чата на живо - Бутон за \"Местоположение\" се показва. - Бутон за \"Местоположение\" е скрит. - Бутон за \"Местоположение\" - Навигационната лента се показва. - Навигационната лента е скрита. - Скриване лентата за навигация - Промоционалните етикети се показват. - Промоционалните етикети са скрити. - Скриване на платените промоции - При поставяне на пауза заглавие не се скрива. - Заглавието на пауза е скрито. - Скриване на заглавието на пауза - Бутоните при пауза се показват. - Бутоните при пауза се скриват. - Показване на бутони при пауза - Показва се фонът на бутоните. - Фонът на бутоните е скрит. - Скриване на фона на бутона за възпроизвеждане & и пауза - Бутона за ремикс се показва. - Бутона за ремикс е скрит. - Скриване на бутона за ремикс - Показан е бутонът за запазване на музика. - Бутонът за запазване на музика е скрит. - Скриване на бутона Запазване на музика - Бутон „Предложения за търсене“ се показва. - Бутон „Предложения за търсене“ е скрит. - Бутон „Предложения за търсене“ - Бутона за споделяне се показва. - Бутона за споделяне е скрит. - Скриване на бутона за споделяне - Показват се в историята на гледане. - Скрити в историята на гледане. - Скриване в историята на гледане - Показва се в емисиите „начало“ и „подобни видеоклипове“. - Скрити в емисиите „начало“ и „подобни видеоклипове“. - Скриване в „начало“ и „подобни видеоклипове“ - Резултатите от търсенето се показват. - Резултатите от търсенето са скрити. - Скриване на резултатите от уеб търсенето - Показва се в емисията „Абонаменти“. - Лавицата за кратки видеоклипове в емисията за абонаменти е скрита. - Скриване в емисията „Абонаменти“ - "Скрива рафтовете за кратки видеа - -Известен проблем: Официалните заглавки в резултатите от търсенето са скрити." - Скрийте рафтовете Shorts - Бутона за пазаруване се показва. - Бутона за пазаруване е скрит. - Скриване на бутона за пазаруване - Бутона за пазаруване се показва. - Бутона за пазаруване е скрит. - Бутон \"Пазаруване\" - Бутона за Звук се показва. - Бутона за Звук е скрит. - Скрийте бутона „Звук“ - Метаданни се показват. - Метаданни са скрити. - Скриване на музикални метаданни - Стикери са показани. - Стикерите са скрити. - Скриване на стикери - Показва се бутонът „Абониране“. - Бутонът „Абониране“ е скрит. - Скрийте бутона „Абониране“ - Бутон \"Специални благодарности\" се показва. - Бутон \"Специални благодарности\" е скрит. - Бутон \"Специални благодарности\" - Маркираните продукти се показват. - Маркираните продукти са скрити. - Скриване на маркираните продукти - Лента с инструменти се показва. - Лента с инструменти е скрита. - Скриване на лентата с инструменти - Показва се. - Скрит. - Бутон „Набиращи популярност“ - Бутон за \"Използване на шаблон\" се показва. - Бутон за \"Използване на шаблон\" е скрит. - Бутон за \"Използване на шаблон\" - Бутон „Използване на този звук“ се показва. - Бутон „Използване на този звук“ е скрит. - Бутон „Използване на този звук“ - Заглавието се показва. - Заглавието е скрито. - Заглавие на видеото - Бутона Покажи още се показва. - Бутона Покажи още е скрит. - Скриване на бутона Покажи още - Лентата на състоянието се показва. - Лентата на състоянието е скрита. - Скриване на лентата за състояние - Бутона за пробен период се показва. - Бутона за пробен период е скрит. - Скриване на бутона за стартиране на пробен период - Показва се лентата „Абонаменти“. - Лентата „Абонаменти“ е скрита. - Секция на канала в раздела „Публикации“ - Предложенията за действе се показват. - Предложенията за действе са скрити - Препоръчителни действия - "This setting has been deprecated. - -Instead, use the 'Settings → Autoplay → Autoplay next video' setting. - -Note: -• If you have any issues with 'Suggested video end screen', try restarting the app." - Препоръчаният видеоклип се показва в края на възпроизвеждането. - "Екранът за предложения на видеоклипове е скрит, когато автоматичното пускане е изключено. - -Автоматичното пускане може да се промени в настройките на YouTube: -Настройки → Автоматично пускане → Автоматично пускане на следващия видеоклип" - Препоръчан видеоклип в края на възпроизвеждането - Бутона за благодарност се показва. - Бутона за благодарност е скрит. - Скриване на бурона за благодарност - Рафтовете с билети се показват. - Рафтовете с билети са скрити. - Секция за билети - Клеймо за време се показва. - Времето клеймо е скрито. - Времево клеймо на видеоклипа - Времевите реакции се показват. - Времевите реакции са скрити. - Скриване на времевите реакции - Бутонът за предаване се показва. - Бутонът за предаване е скрит. - Скриване на бутона за предаване на Тв - Бутона за създаване се показва. - Бутонът за създаване е скрит. - Бутон Създай клип - Бутонът за известия се показва. - Бутонът за известия е скрит. - Бутон за Известия - Разделът за транскрипция е показан. - Разделът за транскрипция е скрит. - Скриване на раздела за транскрипция - Видео рекламите се показват. - Видео рекламите са скрити. - Скриване на видео рекламите - "Начало / Абонамент / Резултатите от търсенето се филтрират, за да скрият видеоклипове с гледания, по-малки или по-големи от определен брой." - Относно филтрирането на броя показвания - Видеоклиповете в раздела Начало не се филтрират. - Видеоклиповете в раздела Начало се филтрират. - Фильтр за видео в \"Начало\" по гледания - Резултатите от търсенето не се филтрират. - Резултатите от търсенето се филтрират. - Филтриране на резултатите от търсенето - Видеоклиповете в емисията за абонаменти не се филтрират. - Видеоклиповете в емисията за абонаменти се филтрират. - Видеоклипове в раздела Абонаменти по показвания - Скрийте видеоклиповете с по-малко или повече гледания от предпочитанията ви.\n\nИзвестен проблем: Видеоклиповете с 0 гледания не са правилно филтрирани. - Скриване на видеоклипове въз основа на броя гледания - Видеоклиповете с повече гледания от този брой няма да бъдат показани. - Ограничение за максимален брой гледания - Видеоклиповете с повече гледания от този брой ще бъдат скрити. - Минимално ограничение за гледане - K -> 1 000\nM -> 1 000 000\nB -> 1 000 000 000\nviews -> views - Езиков шаблон за брой изгледи (букви/дума на вашия език) -> стойност (ключова стойност) трябва да е на нов ред преди знака \"->\". Ако превключите езика на приложението или системата, трябва да нулирате тази настройка.\n\nПримери:\nEnglish: 10K views = K -> 1000, views -> views\nБългарски: 10 K показвания = K -> 1000, показвания -> views - Ключове за редактиране - Продуктовите банери се показват. - Продуктовите банери са скрити. - Скриване на банера за преглед на продукти - Бутон за гласово търсене се показва. - Бутон за гласово търсене на е скрит. - Бутон за \"гласово търсене\" - Резултатите от уеб търсенето са показани. - Резултатите от уеб търсенето са скрити. - Скриване на резултатите от уеб търсенето - Интерфейс за мащабиране се показва. - Интерфейс за мащабиране ескрит. - Интерфейс за мащабиране - AFN Синя - AFN Червена - По избор - По подразбиране - MMT - Revancify синя - Revancify Червена - YouTube - Запазва пейзажен режим при изключване и включване на цял екран. - Броят милисекунди, за да принудите пейзажния режим да работи. - Запазване на времето за изчакване в пейзажен режим - Запазете пейзажен режим - По подразбиране - Действието с двойно докосване е деактивирано. - "Действието с двойно докосване е активирано. - -• Докоснете двукратно, за да промените минимизирания видеоклип към по-голям размер. -• Докоснете два пъти още веднъж, за да промените към оригиналния размер." - Действие с двойно докосване - Деактивирано минимизирано плъзгане и пускане на екрана. - Активирано е минимизирано плъзгане и пускане на екрана. - Разрешете плъзгане и местене на мини-плеера - Бутони за разширяване и свиване на екрана са видими. - Бутоните са скрити.\n(плъзнете миниплейъра, за да разширите или затворите) - Бутони за разширяване и свиване на екрана - Бутони за напред и назад са показани. - Бутони за напред и назад са скрити. - Бутони за напред и назад - Подтекстовете се показват. - Подтекстовете са скрити. - Възпроизвеждане на екранни текстове, етикети - Прозрачността на менюто на плейъра трябва да бъде между 0-100. Нулирайте стойностите по подразбирне. - Стойност на прозрачност между 0-100, където 0 е прозрачно. - Прозрачност на менютата - Оригинал - Телефон - Таблет - Модерен 1 - Модерен 2 - Модерен 3 - Минимизиран тип екран за гледане - Бутони в плеъра - "Докоснете, за да превключите повторение -Докоснете и задръжте, за да превключите на пауза след повтарвне." - Показване бутон за авт. повторение - "Докоснете, за да копирате URL адреса на видеоклипа -Докоснете и задръжте, за да копирате URL адреса на видеоклипа с маркер за време." - "Натиснете, за да копирате URL адреса на видеоклипа с клеймо за време. -Натиснете и задръжте, за да копирате клеймо за време." - Копиране на връзка с времеви печат - Показване на бутона за копиране на URL адреса на видеоклипа - Докоснете за избор на външно свалящо приложение. - Показване на бутона за изтегляне чрез външно приложение - Натискането на бутона изключва/включва звука на текущото видео. - Бутон за изключване на звука - Натиснете продължително, за да промените състоянието на бутона. - Нулиране на скоростта на възпроизвеждане: %sx. - "Бутон за регулиране на скоростта на възпроизвеждане. Натиснете, за да отворите прозореца за промяна на скоростта на възпроизвеждане 1.0x." - Показване бутон за скорост - "Докоснете, за да генерирате плейлист с всички видеоклипове в канала от най-старите до най-новите, натиснете продължително, за да отмените." - Бутон за показване на подредени по време плейлисти - Натиснете - Отворете \"Бял списък\". -Натиснете и задръжте - Отворете настройките на \"Бял списък\". - Пок. бутон за \"Бял списък\" - Бутонът за изтегляне на YouTube отваря собствената програма за изтегляне на приложението. - Бутонът за изтегляне от YouTube отваря вашата външна програма за изтегляне. - Преназначаване на бутона за изтегляне на плейлист - Бутонът за изтегляне на YouTube отваря собствената програма за изтегляне на приложението. - Бутонът за изтегляне от YouTube отваря вашата външна програма за изтегляне. - Действие на бутона \"Изтегляне\" за видео - Изисква се YouTube Music, за да замени действието на бутона. Докоснете тук, за да изтеглите YouTube Music. - Изисквания - Бутонът YouTube Music отваря вграденото приложение. - Бутонът YouTube Music отваря RVX Music. - Замяна на бутона YouTube Music - Изключване - Включване - Нормално - Бутони за действие - Допълнителни настройки - Анимация / Обратна връзка - Бутон Изтегляне - Експериментални настройки - Ограничения за областта на изображението - Импортиране / Експортиране като файл - Импортирайте / Експортирайте настройки в текст - Филтър по ключова дума - Други - Добавете бутони към екрана за възпроизвеждане - Информация за корекции - Бързи действия - Препоръчани видеоклипове - Shorts рафтове - Предложени действия - Ползвани инструменти - Филтрирайте по брой гледания - Скриване или показване на елементи в менюто на акаунта и раздела Вие. - Меню на акаунта - Скриване или показване на бутони за действие под видеоклипове. - Бутони за действие - Реклами - Алтернативни миниатюри - Изключете подсветка около видеото или прескочете ограничението в режим за пестене на батерията. - Подсветка около видеото - Скриване или показване на лентата с категории в емисията, резултатите от търсенето и свързаните видеоклипове. - Панел с категорий - Скрийте или покажете елементи от лентата на канала под видеоклипа. - Панел на канала - Скриване или показване на елементи на страницата с канали. - Страница на канала - Скриване или показване на секцията за коментари. - Коментари - Скрийте или покажете публикации в общността в емисията и канала. - Публикации в общността - Скриване на компоненти с помощта на потребителски филтри. - Потребителски филтър - Скрийте или покажете компонентите на падащото меню в лентата с помощта на филтър. - Падащо меню - Начална страница - Скрийте или променете елементи, свързани с режим на цял екран. - Цял екран - Основни настройки - Деактивирайте или активирайте вибрационен отговор за събития. - Вибрация при докосване (обратна връзка) - Замяна на действията на бутоните в приложението. - Настройки за действие на бутоните - Импортиране / Експортиране на настройките. - Импортиране / Експортиране на настройките - Променете стила на минимизирания екран за възпроизвеждане. - Минимизиран екран за възпроизвеждане - Разни - Скриване или показване на елементи от лентата за навигация. - Лента за навигация - Информация за приложените корекции. - Информация за корекции - Скриване или показване на бутони на екрана на видеоплейъра. - Бутони на екрана за възпроизвеждане - Скриване или промяна на елементи от изскачащото меню на екрана на видеоплейъра. - Падащо меню - Плейър - Return YouTube Dislike (показва нехаресванията) - SponsorBlock - Конфигуриране на компоненти Скалата за възпроизвеждане. - Времева Скала на възпроизвеждане - Скриване на елементи в менюто с настройки на YouTube. - Меню с настройки - Скриване или показване на компоненти в Shorts плейъра. - Плейър за кратки клипове - Shorts - Подправете поточно предаваните данни, за да предотвратите проблеми с възпроизвеждането. - Подправяне на поточни данни - Плъзгащи контроли - Скрива или променя елементи, разположени в лентата с инструменти, като бутони на лентата с инструменти, лента за търсене, заглавия. - Лента с инструменти - Скриване или показване на компонентите от описанието на видеоклиповете. - Описание на видеото - Скриване на видеоклипове въз основа на ключови думи, брой гледания или тяхната продължителност. - Видео филтри - Видео - Променя настройките, за хронологията на гледане. - История на гледане - Височината трябва да е между 0-32, нулиране. - Промяна на височината на лентата за прогрес, стойности между 0-32. - Височина на лентата за напредък - "Принудително отхвърляне на софтуерния кодек AV1 -След приблизително 20 секунди буфериране ще бъде приложен друг кодек." - Отхвърлете софтуерния кодек AV1 - Буфериране поради софтуерен кодек Av1 (прибл. 20 сек.). - Компенсиране - Промените в скоростта на възпроизвеждане се отнасят само за текущия видеоклип. - Промените в скоростта на възпроизвеждане се отнасят за всички видеоклипове. - Запомнете промените в скоростта на възпроизвеждане - Няма съобщение в долната част на екрана при промяна на скоростта на възпроизвеждане по подразбиране. - При промяна на скоростта на възпроизвеждане по подразбиране в долната част на екрана се появява съобщение. - Покажи съобщение - Смяна на скоростта на видеото на %s. - Промените в качеството се отнасят само за текущия видеоклип. - Промените в качеството се отнасят за всички видеоклипове. - Запомнете промените в качеството на видеото - Няма съобщение в долната част на екрана при промяна на качеството на видеото по подразбиране. - Съобщението се появява в долната част на екрана при промяна на качеството на видеото по подразбиране. - Покажи съобщение - Смяна на качеството при мобилни данни на %s. - Грешка при настройка на качеството на видеото. - Смяна на качеството при Wi-Fi на%s. - "Премахва диалоговите прозорци. Това не заобикаля възрастовите ограничения, но ги приема автоматично." - Прозорец за възрастово ограничение - Заменя софтуерния кодек AV1 с кодека VP9. - Сменете софтуерния кодек AV1 - Използва се псевдонимът на канала. - Използва се името на канала. - Заменете псевдонима на канала - Докоснете, за да видите оставащото време. - Докоснете, за да отворите менюто за скорост на възпроизвеждане или качество на видеото. - Променя действието на индикатора за време - Заменете „Създаване“ с бутон за настройки. - Заменете бутона \"Създаване\" - "Докоснете, за да отворите настройките на YouTube. -Докоснете и задръжте, за да отворите настройките на RVX." - "Докоснете, за да отворите настройките на RVX. -Докоснете и задръжте, за да отворите настройките на YouTube." - Тип действие за назначаване на бутона - Миниатюрите се показват в режим на цял екран. - Над лентата за възпроизвеждане се появяват миниатюри. - Стари миниатюри на времевата линия - Старото меню за видео качество е скрито. - Показва се старото меню за видео качество. - Възстановете старото меню за качество на видеото - За програмата - Данните за нехаресване са от Return YouTube Dislike API. Докоснете за да научите повече. - ReturnYouTubeDislike.com - Компактният бутон „Харесва ми“ е деактивиран. - Включен компактен бутон \"Харесва ми\". - Компактен бутон за харесване - Нехаресванията се показват като число. - Нехаресванията се показват като процент. - Нехаресвания като процент - Нехаресванията не се показват. - Нехаресванията се показват. - Вкл. на Return YouTube Dislike - Нехаресванията не са достъпни (достигнат лимит на API). - Нехаресванията не са налични (status %d). - Нехаресванията временно не са налични (изтече времето за изчакване на API). - Нехаресванията не са налични (%s). - Презареждане на видеото за гласуване чрез ReturnYouTubeDislike - Нехаресванията са скрити в кратките клипове. - Нехаресванията се показват в кратките клипове. - "Нехаресванията се показват в Shorts -Ограничение: Нехаресванията може да не се показват в режим „инкогнито“ или ако не сте влезли в акаунта си." - Пок. нехаресвания в кратките клипове - Не се показва известие, ако ReturnYouTube Dislike не е наличен. - Показва известие, ако Return YouTube Dislike не е наличен. - Показване на известие, ако API не е наличен - Премахва параметрите на заявката за проследяване от URL адресите при споделяне на връзки. - Почистване на споделените връзки - Относно - sponsor.ajay.app - Данните са предоставени от SponsorBlock API. Докоснете тук за повече информация и изтеглияния. - URL API е променен. - URL адресът е невалиден. - Нулиране URL адреса, на API. - Външен вид - Цветът е променен - Цвят: - Невалидна стойност за цвета - Цветът е възстановен - Създаване на нови части - Промени поведението на сигмента - Авт. скриване на бутона за пропускане - Бутона за пропускане се показва за цялата част. - Бутона за пропускане се скрива след няколко секунди. - Компактен бутон за пропускане - Най-добър изглед на бутона за пропускане. - Мин. ширина на бутона за пропускане. - Показване на бутона за нова част - Бутона създаване за нова част не се показва. - Бутона създаване за нова част се показва - Включване на SponsorBlock - SponsorBlock е система за прескачане на досадни части от видеоклиповете в YouTube. - Пок. бутона за гласуване - Бутонът \"Гласуване\" за сегмент е скрит. - Показва се бутонът \"Гласуване\" за сегмент. - Основен - Настройване стъпка на новата част - Стойността трябва да е положително число. - Милисекундите с който се преместват бутоните за настройка при създаване на част. - Промяна URL на API - Адресът, който SponsorBlock използва засвързване към сървъра. - Минимална продължителност на сегмента - Невалидна продължителност. - Части, по-кратки от тази стойност (в секунди) няма да бъдат пропускани или показвани. - Активиране проследяването на броя пропускания - Прослед. на броя пропускания е изкл. - Показва в класацията на SponsorBlock колко време е спестено. Съобщение се изпраща при всяка пропусната част. - Показв. известие при автом. пропуск. част - Известието не се показва. Докоснете тук за пример. - Показване на известие при автоматично пропусната част. Докоснете тук за пример. - Показване на дължината на видеото без сигментите - Цялата дължина на видето се показва. - Дължината на видеоклипа минус комбинираната дължина на сегмента е показана в скоби до пълната дължина на видеоклипа. - Вашият уникален потребителски id - Личният Id трябва да е с дължина поне 30 знака. - Това трябва да се пази тайно. То е като парола и не трябва да се споделя с никого. Ако някой го притежава, той може да се представи вместо вас. - Вече ги прочетох - Прочетете указанията на SponsorBlock преди да създадете нови части. - Покажи ми - Следвайте указанията - Указанията съдържат правила и съвети за създаване на нови части. - Вижте указанията - Изберете категорията на частта - The segment lasts from %1$02d:%2$02d to %3$02d:%4$02d (%5$d minutes %6$02d seconds)\nIs it ready to submit? - Сегментът продължава от\n\n%1$s\nдо\n%2$s\n\n(%3$s)\n\nГотов ли е за изпращане? - Времената точни ли са? - Категорията е изкл. в настройките. Вкл. я за да можете да изпратите. - Желаете ли да редактирате времената за начало или край на частта? - Зададено е невалидно време. - Ръчно редактиране на времената на частта - Задаване на %s като начало или край на нов раздел? - край - Първо маркирайте две места в лентата за времето. - начало - сега - Преглед и проверка на частта за нормално пропускане. - Началото трябва да бъде преди края. - Частта свършва до - Частта започва от - Нова част в SponsorBlock - Възстанови - Връщане на цветовете - Пълнеж/Шеги - Сцени извън темата, добавени само за пълнеж или хумор, които не са необходими за разбирането на основното съдържание на видеоклипа. Това не трябва да включва сегменти, предоставящи контекст или справочни данни. - Акцентиране - Частта от видеото която повечето хора търсят. - Напомняне за действие (абониране) - Когато има кратко напомняне да харесате, да се абонирате или да последвате канала по средата на съдържанието. Ако е дълго или за нещо специфично, трябва да е под „самореклама“. - Пауза / Начална анимация - Интервал без реално съдържание. Може да бъде пауза, статичен кадър, повтаряща се анимация. Това не трябва да се използва за преходи, съдържащи информация. - Музика: Част без музика - За използване само в музикални видеоклипове. Това трябва да се използва само за части от музикални видеоклипове, които вече не са обхванати от друга категория. - Крайни карти / информация - Информация или когато се показват крайните карти на YouTube. Не за заключения с информация. - Преглед/Обобщение - Колекция от клипове, които показват какво предстои в този видеоклип или в други видеоклипове от поредицата, където цялата информация се повтаря по-късно във видеоклипа. - Неплатена/Самореклама - Подобно на „спонсорство“, но за безплатна реклама или самореклама. -Това включва търговия със стоки, дарения или информация с кого каналът има сътрудничество. - Спонсори - Платена промоция, платени препоръки и директни реклами. Не за самореклама или безплатни препоръки за каузи/създатели/уебсайтове/продукти, които се харесват на автора. - Копирай - Неуспешно експортиране на %s. - Импортиране / Експортиране на настройките - Вашата JSON конфигурация на SponsorBlock може да бъде импортирана/експортирана в ReVanced Extended и други платформи на SponsorBlock. - Вашата JSON конфигурация на SponsorBlock може да бъде импортирана/експортирана в ReVanced Extended и други платформи на SponsorBlock. Той съдържа вашия личен документ за самоличност. Бъдете внимателни, когато го споделяте с други. - Неуспешно импортиране: %s. - Настройките са успешно въстановени. - Вашите настройки на SponsorBlock съдържат лично Id.\n\nВашето Id е като парола и не трябва да се споделя с никого\n - Не показвай отново - Настройките са копирани в клипборда. - Автоматично пропускане - Авт. пропускане веднъж - Пропусни - Акценти - Пропусни пълнеж - Пропусни до акцент - Пропусни подканване - Пропусни въведение - Пропусната пауза - Пропусната пауза - Пропусни част без музика - Пропусни заключение - Пропусни преглед - Пропуснете обобщението - Пропусни преглед - Пропусни промоция - Пропусни спонсор - Пропусни част - Деактивиране - Показв. в лентата за време - Пок. бутон за пропускане - Пропуснат пълнеж - Пропуснато до акцент - Пропуснато досадно напомняне - Пропуснато въведение - Пропусната пауза. - Пропусната пауза. - Пропуснати множество части - Пропусната част без музика - Пропуснато заключение - Пропуснат преглед. - Пропуснато повторение. - Пропуснат преглед. - Пропусната самореклама. - Пропуснат спонсор - Пропуснат неизпратен сегмент - SponsorBlock временно не е наличен. - SponsorBlock временно не е наличен (status %d). - SponsorBlock временно не е наличен (API timed out). - Статистика - Статистиката е временно недостъпна (API не работи). - Зареждане... - Репутацията ви е <b>%.2f</b> - Спасихте хората от <b>%s</b> сегменти - %1$s часове %2$s минути - %1$s минути %2$s секунди - %s секунди - Това е <b>%s</b> от живота им.<br>Щракнете, за да видите класацията. - Докоснете за да видите статистиката и тези допринесли най-много. - SponsorBlock класация - SponsorBlock е деактивиран. - Пропуснали сте <b>%s</b> части - Нулиране на брояча на пропуснати части? - Това е <b>%s</b>. - Създадохте <b>%s</b> части - Докоснете тук, за да видите вашите сегменти. - Вашето потр. име: <b>%s</b> - Докоснете за промяна потребителското име - Не може да се промени потреб. име: Състояние: %1$d%2$s. - Потребителското име е успешно променено. - Не може да се изпрати частта.\nВече съществува. - Не може да се изпрати частта: %s. - Не може да се изпрати сегмент: %s. - Не може да се изпрати частта.\nБроят е ограничен (Твърде много от един и същ потребител или IP). - SponsorBlock временно не работи. - Не могат да се изпратят сигменти: (статус:%1$d %2$s). - Частта е изпратена успешно. - Не се показва известие, ако Api на SponsorBlock не е наличен. - Показва се известие, ако Api на SponsorBlock не е наличен. - Показване на известие, ако API не е наличен - Промяна на категорията - Отрицателен вот - Не може да се гласува за сигмента: %s. - Не може да се гласува за сегмент (изтече времето за изчакване на API). - Не може да се гласува за частите: (статус: %1$d %2$s). - Няма сегменти, за които да гласувате. - Положителен вот - Настройките са копирани в клипборда. - Времевата отметка е копирана в клипборда. (%s) - Връзката е копирана в клипборда. - URL адрес с отметка за време, копиран в клипборда. - Оригинал - Харесва ми - Палец нагоре (тема Кайро) - Сърце - Сърце (цветно) - Скрит - Анимация на двойно докосване - Полето в долната част на мета панела трябва да е между 0-64, Нулирайте по подразбиране. - Отстъп от лентата за възпроизвеждане към панела „мета“ Диапазон от 0 до 64. - Долно поле на \"мета\" панела - Процентът на височината трябва да е между 0-100 (%). - Конфигурира процента на височината на оставащото празно пространство, когато лентата за навигация е скрита, между 0 и 100 (%). - Мярка в проценти на празното пространство - Натиснете и задръжте клеймото за време, за да превключите състоянието на повторение на Shorts. - Продължително натискане на времето - "Показва раздела със заглавието на видеоклипа на цял екран. - -Ограничение: Заглавието на видеоклипа изчезва при щракване." - Показване на раздел със заглавие на видеоклипа - Ако автоматичното пускане е активирано, следващият видеоклип ще се възпроизведе след края на обратното броене. - Ако автоматичното пускане е активирано, следващият видеоклип ще се възпроизведе веднага. - Незабавно автоматично пускане - "Пропуска предварително заредения буфер в началото на видеоклиповете, за да приложи незабавно качеството на видеото по подразбиране. - -Информация: -• Когато видеото започне, има забавяне от приблизително 0,3 секунди. -• Не се прилага за HDR видеоклипове, видеоклипове на живо или видеоклипове, по-кратки от 15 секунди." - Пропусни предварително зареден буфер - Уведомлението е скрито. - Уведомлението се показва. - Уведомление при пропуске - Активирането на тази настройка може да причини проблеми с възпроизвеждането на видео. - Пропуснат предварително зареден буфер. - Скоростта трябва да е между 0-8.0, нулирайте. - Стойност на скоростта, приложена при продължително натискане, между 0 и 8,0. - Стойност на скоростния интерфейс - "Spoofing the client version to the old version. - -• This will change the appearance of the app, but unknown side effects may occur. -• If later turned off, the old UI may remain until clear the app data." - Не подправена версия - Подправена версия - 17.30.34 - Възстановява стария изглед - 17.41.37 - Връщане на секцията с плейлиста към стария стил - 18.05.40 - Възстановяване на старото поле за писане на коментари - 18.17.43 - Възстановяване на стария стил на изскачащия панел - 18.33.40 - Възстановяване на старата лентата с действия за Shorts - 18.38.45 - Възстановяване на старото поведение на качеството на видеото по подразбиране - 18.48.39 - Деактивира изгледите и харесванията да се актуализират в реално време - 19.13.37 - Стар стил на анимация - въртящи се числа - Версия за модификация на приложението - Въведете версията на приложението, която да се приложи. - Редактирайте версията на приложението, която да бъде приложена - Променете версията на приложението - "Версията на приложението YouTube ще бъде променена на по-стара. -Това ще промени външния вид и функциите на приложението, но ако по-късно се деактивира, се препоръчва да изчистите данните на приложението, за да избегнете грешки в потребителския интерфейс." - "Преоразмерява вашето устройство, за да покже видеоклипове с по-високо качество, които може да не са налични на вашето устройство." - Лъжливи параметри на устройството - видео кодекът на iOS е AVC (H.264), VP9, or AV1. - видео кодекът на iOS е AVC (H.264). - Принудително AVC (H.264) за iOS - "Активирането на това може да подобри живота на батерията и да коригира прекъсванията при възпроизвеждане. - -AVC (H.264) има максимална разделителна способност при 1080p и възпроизвеждането на видео ще използва повече интернет данни от VP9 или AV1." - "• Липсва менюто за избор на аудио." - "• Липсва менюто за избор на аудио." - "• Филми или платени видеоклипове може да не се възпроизвеждат." - Ефекти от замяната - • Видеото може да не се възпроизведе. - Клиентът, използван за получаване на данни за поток, е скрит в Статистика за системни администратори. - Клиентът, използван за получаване на данни за потока, се показва в Статистика за системни администратори. - Показване в \"Разширени статистики\" - "Данните за поточно предаване не са подправени. Възпроизвеждането на видео може да не работи." - Данните за поточно предаване са подправени. - Подправяне на поточни данни - Андроид - Android TV - Android VR - iOS - Клиент по подразбиране - Изключването на тази настройка може да причини проблеми с възпроизвеждането на видео. - Жестовете за плъзгане са деактивирани в режим „Заключен екран“. - Жестовете за плъзгане са активирани в режим „Заключен екран“. - Плъзне в режим Заключен екран - Авто - Амплитудата на движение, разпозната като жест. - Праг на величината на плъзгане - Видимостта на фона на плъзгащите контроли. - Видимост на фона на плъзгащите контроли - Областта за плъзгане не може да бъде по-голяма от 50. Нулиране на стойността по подразбиране. - Процентът от площта на екрана, който може да се плъзне.\n\nЗабележка: Това също засяга зоната с двойно докосване за придвижване напред/назад във видеоклипа. - Размер на областта за жестове - Размера на текста на плъзгащите контроли. - Размер на текста при плъзгане - Време за което плъзгащата контрола е видима. - Задръжка на плъзгащата контрола за показване - "Разменя позициите на бутона Създаване с бутона Известия чрез подправяне на информация за устройството. - -• Може да се наложи устройството да се рестартира, за да влезе в сила промяната на тази настройка. -• Деактивирането на тази настройка зарежда повече реклами от страната на сървъра. -• Трябва да деактивирате тази настройка, за да направите видео рекламите видими." - Бутоните \"Създаване\" и \"Известия\" не са разменени. - "Бутонът за създаване се заменя с бутона за известия - -Забележка: Активирането на тази опция също скрива видеореклами." - Разменете бутоните „Създаване“ с „Известия“ - "Деактивирането на тази настройка може да доведе до зареждане на повече реклами от сървъра. - - Освен това рекламите може да се показват в Shorts. - -Ако деактивирането не влезе в сила, опитайте да превключите към режим „инкогнито“." - По подразбиране - RVX Music - %s не е инсталирано. Моля инсталирайте го. - Името на пакета с инсталиран RVX Music. - Име на пакета RVX Music - • Хронологията на гледане е блокирана. - "• Следва настройките на хронологията на сърфирането в акаунта ви в Google. -• Историята на сърфиране може да не работи поради DNS или VPN." - • Следва настройките на хронологията на сърфирането в акаунта ви в Google. - Преглед на състоянието на хронологията - Кликнете, за да отворите управлението на хронологията на гледане в YouTube. - Управление на цялата история - Оригинал - Замени домейна - Блокиране на хронологията на гледане - Тип хронология на гледане - Неуспешно добавяне на \"%1$s\" канала към %2$s белия списък. - Каналът %1$s е добавен в %2$s белия списък. - Няма канали в белия списък. - Не е добавен в Бял списък. - Неуспешно зареждане на информация за канала. - Добавен към белия списък. - Скорост на възпроизвеждане - Премахване на канала „%1$s“ от белия списък на %2$s? - Неуспешно премахване на канала %1$s от %2$s белия списък. - Каналът \"%1$s\" е премахнат от %2$s белия списък. - Проверка или премахване на листа с канали доб. в белия списък. - Добавяне на канал към белия списък - SponsorBlock - diff --git a/src/main/resources/youtube/translations/de-rDE/missing_strings.xml b/src/main/resources/youtube/translations/de-rDE/missing_strings.xml deleted file mode 100644 index 373e48d64..000000000 --- a/src/main/resources/youtube/translations/de-rDE/missing_strings.xml +++ /dev/null @@ -1,377 +0,0 @@ - - - Don\'t show again - Invalid DeArrow API URL. - Original - Phone - Phone (Max 480 dp) - Tablet - Tablet (Min 600 dp) - Change layout - In-app share sheet is used. - System share sheet is used. - Change share sheet - Courses / Learning - Start page changes only once. - "Start page always changes. - -Limitation: Back button on the toolbar may not work." - Change start page type - "Auto switch mix playlists is enabled when autoplay is turned on. - -Autoplay can be changed in YouTube settings: -Settings → Autoplay → Autoplay next video" - Auto switch mix playlists is disabled. - Disable switch mix playlists - Enabling this feature will disable automatic switching to YouTube Mix when playing music while autoplay is turned on. - Default playback speed is enabled for music. - "Default playback speed is disabled for music. - -Limitation: This setting may not apply to videos that do not include the 'Listen on YouTube Music' banner." - Disable playback speed for music - Chapters are enabled in the seekbar. - Chapters are disabled in the seekbar. - Disable seekbar chapters - Fountain animation is enabled above the Like button. - Fountain animation is disabled above the Like button. - Disable Like button animation - VP9 codec is enabled. - "VP9 codec is disabled. - -• Maximum resolution is 1080p. -• Video playback will use more internet data than VP9. -• VP9 codec is still used for HDR video." - Disable VP9 codec - Do not save and restore brightness when exiting or entering fullscreen. - Save and restore brightness when exiting or entering fullscreen. - Enable save and restore brightness - "This will restore thumbnails to livestreams that do not have seekbar thumbnails. - -Internet data usage may be higher, and seekbar thumbnails will have a slight delay before showing. - -This feature works best with a very fast internet connection." - Seekbar thumbnails are medium quality. - Seekbar thumbnails are high quality. - Enable high quality thumbnails - Reset to default values. - "There is a YouTube server-side bug that causes rolling number text such as likes, views, and upload dates to be hidden for some users. - -A temporary workaround for this issue is to spoof the app version to 19.13.37. - -Do you want to spoof the app version before restarting the app?" - Package name of your installed external downloader app, such as YTDLnis. - Playlist downloader package name - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - Package name of your installed external downloader app, such as NewPipe or YTDLnis. - Video downloader package name - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - AI-generated video summary section is shown. - AI-generated video summary section is hidden. - Hide AI-generated video summary section - Highlighted search links are shown. - Highlighted search links are hidden. - Hide highlighted search links - How this content was made section is shown. - How this content was made section is hidden. - Hide Contents section - Expandable shelves are shown. - Expandable shelves are hidden. - Hide expandable shelves - Floating button is shown. - Floating button is hidden. - Hide floating button - Surrounding a keyword/phrase with double-quotes will prevent partial matches of video titles and channel names.<br><br>For example,<br><b>\"ai\"</b> will hide the video: <b>How does AI work?</b><br>but will not hide: <b>What does fair use mean?</b> - Match whole words - Add quotes to use keyword: %s. - Keyword has conflicting declarations: %s. - Keyword is too short and requires quotes: %s. - Navigation bar is shown. - Navigation bar is hidden. - Hide navigation bar - Ambient mode menu is shown. - Ambient mode menu is hidden. - Hide Ambient mode menu - 1080p Premium menu is shown. - 1080p Premium menu is hidden. - Hide 1080p Premium menu - Sleep timer menu is shown. - Sleep timer menu is hidden. - Hide Sleep timer menu - Shopping shelf is shown. - Shopping shelf is hidden. - Hide player shopping shelf - Promotion alert banner is shown. - Promotion alert banner is hidden. - Hide promotion alert banner - Related videos are shown. - Related videos are hidden. - Hide related videos - "This setting limits the maximum number of layouts that can be loaded on the player screen. - -If the layout of the player screen changes due to server-side changes, unintended layouts may be hidden on the player screen." - Chapter labels next to the timestamp are shown. - Chapter labels next to the timestamp are hidden. - Hide seekbar chapter labels - About menu is shown. - About menu is hidden. - Hide About menu - Accessibility menu is shown. - Accessibility menu is hidden. - Hide Accessibility menu - Account menu is shown. - Account menu is hidden. - Hide Account menu - Autoplay menu is shown. - Autoplay menu is hidden. - Hide Autoplay menu - Billing and payments menu is shown. - Billing and payments menu is hidden. - Hide Billing and payments menu - Captions menu is shown. - Captions menu is hidden. - Hide Captions menu - Connected apps menu is shown. - Connected apps menu is hidden. - Hide Connected apps menu - Data saving menu is shown. - Data saving menu is hidden. - Hide Data saving menu - General menu is shown. - General menu is hidden. - Hide General menu - Manage all history menu is shown. - Manage all history menu is hidden. - Hide Manage all history menu - Live chat menu is shown. - Live chat menu is hidden. - Hide Live chat menu - Notifications menu is shown. - Notifications menu is hidden. - Hide Notifications menu - Background menu is shown. - Background menu is hidden. - Hide Background menu - Watch on TV menu is shown. - Watch on TV menu is hidden. - Hide Watch on TV menu - Family Center menu is shown. - Family Center menu is hidden. - Hide Family Center menu - Try experimental new features menu is shown. - Try experimental new features menu is hidden. - Hide Try experimental new features menu - Privacy menu is shown. - Privacy menu is hidden. - Hide Privacy menu - Purchases and memberships menu is shown. - Purchases and memberships menu is hidden. - Hide Purchases and memberships menu - Video quality preferences menu is shown. - Video quality preferences menu is hidden. - Hide Video quality preferences menu - Your data in YouTube menu is shown. - Your data in YouTube menu is hidden. - Hide Your data in YouTube menu - Disabled comments button or with label \"0\" is shown. - Disabled comments button or with label \"0\" is hidden. - Hide disabled comments button - "Floating buttons like 'Use this sound' are shown in the Shorts channel tab." - "Floating buttons like 'Use this sound' are hidden in the Shorts channel tab." - Hide floating button - Green screen button is shown. - Green screen button is hidden. - Hide Green screen button - Location button is shown. - Location button is hidden. - Hide location button - Paused header is shown. - Paused header is hidden. - Hide paused header - Save music button is shown. - Save music button is hidden. - Hide Save music button - Search suggestions button is shown. - Search suggestions button is hidden. - Hide search suggestions button - Shown in channel. - "Hidden in channel. - -Info: -• Only shelves with the Shorts header on the home tab are hidden." - Hide in channel - Shopping button is shown. - Shopping button is hidden. - Hide Shopping button - Stickers are shown. - Stickers are hidden. - Hide stickers - Trends button is shown. - Trends button is hidden. - Hide Trends button - Use template button is shown. - Use template button is hidden. - Hide Use template button - Use this sound button is shown. - Use this sound button is hidden. - Hide Use this sound button - "Home / Subscription / Search results are filtered to hide videos with views less or greater than a specified number. - -Limitations: -• Shorts cannot be hidden. -• Videos with 0 views are not filtered." - About view count filtering - Videos in home feed are not filtered. - Videos in home feed are filtered. - Hide home videos by views - Search results are not filtered. - Search results are filtered. - Hide search results by views - Videos in subscriptions feed are not filtered. - Videos in subscriptions feed are filtered. - Hide subscription videos by views - YouTube Doodles are shown. - YouTube Doodles are hidden. - Hide YouTube Doodles - "YouTube Doodles show up a few days each year. - -If a YouTube Doodle is currently showing in your region and this setting is on, the filter bar below the search bar will also be hidden." - MMT Blue - MMT Green - MMT Orange - MMT Pink - MMT Turquoise - MMT Yellow - Revancify Yellow - Vanced Black - Vanced Light - Xisr Yellow - Tap to mute volume of the current video. Tap again to unmute. - Show mute volume button - If shown, the native playlist download button opens the native in-app downloader. - Native playlist download button is always shown, and in public playlists, it opens your external downloader. - Override playlist download button - Native video download button opens the native in-app downloader. - Native video download button opens your external downloader. - Override video download button - YouTube Music is required to override button action. Tap here to download YouTube Music. - Prerequisite - YouTube Music button opens the native app. - YouTube Music button opens the RVX Music. - Override YouTube Music button - Download button - Suggested actions - Overrides the click action of in-app buttons. - Hook buttons - Hide or show navigation bar section components. - Navigation bar - Return YouTube Username - Spoof the streaming data to prevent playback issues. - Spoof streaming data - Change settings related with watch history. - Watch history - Offset - A toast will not be shown when changing the default playback speed. - A toast will be shown when changing the default playback speed. - Show a toast - A toast will not be shown when changing the default video quality. - A toast will be shown when changing the default video quality. - Show a toast - @handle (Username) - Display format - Username (@handle) - Username - Handle is used. - Username is used. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - Estimated likes are hidden. - Estimated likes are shown. - Show estimated likes - Hidden - "Phrases like '#', 'Fundraiser', 'Shop' and 'products' were shown from the video subtitles." - "Phrases like '#', 'Fundraiser', 'Shop' and 'products' were hidden from the video subtitles." - Sanitize video subtitle - Invalid time duration. - Adjust: Mark Start and End Time for segment - Verify the Segment - Edit the Segment - Forward by Specified Time (Default: 150ms) - Set %s as the start or end of a new segment? - Publish Created Segment - Rewind by Specified Time (Default: 150ms) - Tap here to view your segments. - Height percentage must be between 0-100 (%). - Configure the height percentage of the empty space left when the navigation bar is hidden, between 0 and 100 (%). - Height percentage of empty space - Turning on this setting may cause video playback issues. - 19.13.37 - Restore old style Rolling number animations - iOS video codec is AVC (H.264), VP9, or AV1. - iOS video codec is AVC (H.264). - Force iOS AVC (H.264) - "Enabling this might improve battery life and fix playback stuttering. - -AVC (H.264) has a maximum resolution of 1080p, and video playback will use more internet data than VP9 or AV1." - "• Audio track menu is missing. -• Stable volume is not available." - "• Audio track menu is missing. -• Stable volume is not available." - "• Movies or paid videos may not play. -• Livestreams start from the beginning. -• Videos may end 1 second early. -• No opus audio codec." - Spoofing side effects - • Video may not play. - Client used to fetch streaming data is hidden in Stats for nerds. - Client used to fetch streaming data is shown in Stats for nerds. - Show in Stats for nerds - "Streaming data is not spoofed. Video playback may not work." - Streaming data is spoofed. - Spoof streaming data - Android - Android TV - Android VR - iOS - Default client - Turning off this setting may cause video playback issues. - Brightness swipe sensitivity must be between 1-1000 (%). - Configure the minimum distance for brightness swiping between 1 and 1000 (%).\nThe shorter the minimum distance, the faster the brightness level changes. - Brightness swipe sensitivity - Volume swipe sensitivity must be between 1-1000 (%). - Configure the minimum distance for volume swiping between 1 and 1000 (%).\n\nThe shorter the minimum distance, the faster the volume level changes.\n\nRecommended volume swipe sensitivity is 100% at 15-volume steps and 10% at 150-volume steps. - Volume swipe sensitivity - Create button is not switched with Notifications button. - "Create button is switched with Notifications button. - -Note: Enabling this also forcibly hides video ads." - "Disabling this might load more ads from the server. - -Also, ads will no longer be blocked in Shorts. - -If this setting do not take effect, try switching to Incognito mode." - RVX Music - %s is not installed. Please install it. - Package name of installed RVX Music. - RVX Music package name - • Watch history is blocked. - "• Follows the watch history settings of Google account. -• Watch history may not work due to DNS or VPN." - • Follows the watch history settings of Google account. - Status of watch history - Click to open the YouTube watch history management. - Manage all history - Original - Replace domain - Block watch history - Watch history type - diff --git a/src/main/resources/youtube/translations/de-rDE/strings.xml b/src/main/resources/youtube/translations/de-rDE/strings.xml deleted file mode 100644 index 753b65a18..000000000 --- a/src/main/resources/youtube/translations/de-rDE/strings.xml +++ /dev/null @@ -1,1353 +0,0 @@ - - - Bedienungshilfen für den Video-Player aktivieren? - Ihre Steuerungen wurden angepasst, da ein Barrierefreiheitsdienst aktiviert ist. - Fortsetzen - "GmsCore hat keine Berechtigung um im Hintergrund zu laufen. - -Folge der 'Don't kill my app!' Anleitung für dein Gerät and wende die Anweisungen auf deine GmsCore Installation an. - -Dies wird zum Funktionieren der App benötigt." - "GmsCore Akku-Optimierungen müssen deaktiviert werden um Probleme zu vermeiden. - -Drücke Weiter und deaktiviere Akku-Optimierungen." - Webseite öffnen - Action needed - Cloud-Nachrichteneinstellungen aktivieren, um Benachrichtigungen zu erhalten - GmsCore öffnen - GmsCore ist nicht installiert. Bitte installiere es. - "DeArrow stellt Crowdsourcing-Thumbnails für YouTube-Videos bereit. Diese Thumbnails sind oft relevanter als die von YouTube bereitgestellten. - -Wenn aktiviert, werden Video-URLs an den API-Server gesendet und es werden keine anderen Daten gesendet. Wenn ein Video keine DeArrow-Thumbnails hat, werden die Original- oder Standbilder angezeigt. - -Tippen Sie hier, um mehr über DeArrow zu erfahren." - Über DeArrow - Die URL des Endpunkts der DeArrow-Thumbnail-Cache. Ändern Sie dies nicht, wenn Sie nicht wissen, was Sie machen. - DeArrow API Endpunkt - Keine Benachrichtigung wird angezeigt, wenn DeArrow nicht erreichbar ist. - Eine Benachrichtigung wird angezeigt, wenn DeArrow nicht erreichbar ist. - Benachrichtigung bei API-Ausfall - DeArrow ist temporär nicht erreichbar. (Statuscode: %s) - DeArrow ist temporär nicht erreichbar. - Startseite - Mein YouTube - Originale Vorschaubilder - DeArrow & originale Vorschaubilder - DeArrow & still captures - Still captures - Playlisten, Vorschläge - Suchresultate - Zeige noch Videoaufnahmen - Aufnahmen werden von Anfang / Mitte / Ende jedes Videos aufgenommen. Diese Bilder sind in YouTube eingebaut und es wird keine externe API verwendet. - Über Videoaufnahmen - Benutze Aufnahmen hoher Qualität. - Benutze Aufnahmen mittlerer Qualität. Thumbnails werden schneller laden, aber Live-Streams, unveröffentlichte, und sehr alte Videos zeigen möglicherweise leere Thumbnails. - Benutze schnelle Still-Aufnahmen - Anfang von einem Video - Mitte eines Videos - Ende eines Videos - Videozeit von der Stand-Aufnahmen aufgenommen werden - Abonnements - Das Anhängen von Zeitstempelinformationen ist deaktiviert - "Zeitstempelinformationen anhängen ist aktiviert" - Zeitstempelinformationen anhängen - Wiedergabegeschwindigkeit anhängen - Videoqualität hinzufügen - Informationstyp anhängen - Ambient Modus im Energiesparmodus deaktiviert - Ambient-Modus im Energiesparmodus aktiviert. - Einschränkungen des Umgebungsmodus umgehen - Die Domäne, von der Bilder abgerufen werden sollen.\nHinweis: Geben Sie nur den Domain-Namen ein, d.h. ohne das Präfix \"https\:\/\/\". - Alternative Domäne - Verwendung des Originalbild-Hosts.\n\nAktivieren kann fehlende Bilder beheben, die in einigen Regionen blockiert sind. - Verwenden von Image Host yt4.ggpht.com. - Bildregion-Beschränkungen umgehen - Switch Toggles - Text Toggles - Toggle Typ ändern - Autoplay - Standard - Pause - Wiederholen - Change shorts repeat state - Kanäle durchstöbern - Standard - Entdecken - Spiele - Verlauf - Bibliothek - Videos die mir gefallen - Live - Filme - Musik - Suchen - Shorts - Sport - Abonnements - Beliebt - Später ansehen - Startseite ändern - Standard Header wird verwendet. - Premium Header wird verwendet. - YouTube Header ändern - Komponenten nach zeilengetrennten Namen filtern - Benutzerdefinierten Filter bearbeiten - Benutzerdefinierter Filter ist deaktiviert - Benutzerdefinierter Filter ist aktiviert - Benutzerdefinierte Filter aktivieren - %s ist ein ungültiger benutzerdefinierter Filter. - Altes Flyout Menü wird verwendet. - Benutzerdefinierter Dialog wird verwendet. - Menü-Typ für benutzerdefinierte Wiedergabegeschwindigkeiten - Ungültige benutzerdefinierte Wiedergabegeschwindigkeiten. Auf Standardwerte zurücksetzen. - Ungültige benutzerdefinierte Wiedergabegeschwindigkeiten. Auf Standardwerte zurücksetzen. - Verfügbare Wiedergabegeschwindigkeiten hinzufügen oder ändern - Benutzerdefinierte Wiedergabegeschwindigkeiten bearbeiten - Die Transparenz der Spieler-Überlagerung muss zwischen 0-100 liegen. Zurückgesetzt auf Standardwerte. - Deckkraft Wert zwischen 0-100, wobei 0 transparent ist. - Benutzerdefinierte Spieler-Überlagerung Deckkraft - Hex-Code der Suchleisten-Farbe eingeben - Benutzerdefinierter Farbwert für die Suchleiste - Um RVX in einem externen Browser zu öffnen, aktivieren Sie \'Unterstützte Links öffnen\' und aktivieren Sie die unterstützen Web-Adressen - Standard-App-Einstellungen öffnen - Standard Wiedergabegeschwindigkeit - Standard Videoqualität im Mobilfunk - Standard-Videoqualität im Wlan - Disables ambient mode for fullscreen only. - Der Inaktivitätsmodus ist im Vollbild aktiviert. - Ambient-Modus ist im Vollbild deaktiviert. - Ambient-Modus im Vollbildmodus deaktivieren - Disables ambient mode. - Ambient Modus ist aktiviert. - Ambient Modus ist deaktiviert. - Ambient-Modus deaktivieren - Erzwungene automatische Audiospuren sind aktiviert. - Erzwungene automatische Audiospuren sind deaktiviert. - Erzwungene automatische Audiospuren sind deaktivieren - Erzwungene automatische Untertitel sind aktiviert - Erzwungene automatische Untertitel sind deaktiviert - Deaktiviere erzwungene automatische Untertitel - Auto-Player-Popup-Panels sind deaktiviert. - Auto-Player-Popup-Panels sind deaktiviert. - Player-Popup-Panels deaktivieren - Standard Wiedergabegeschwindigkeit ist im Live-Stream aktiviert - Standard Wiedergabegeschwindigkeit ist im Live-Stream deaktiviert - Wiedergabegeschwindigkeit im Live-Stream deaktivieren - Engagement panel is enabled. - Engagement panel is disabled. - Disable engagement panel - Haptisches Feedback ist aktiviert. - Haptisches Feedback ist deaktiviert. - Deaktiviere Haptisches Feedback bei Kapiteln - Haptisches Feedback ist aktiviert. - Haptisches Feedback ist deaktiviert. - Deaktiviere haptisches Feedback beim Wischen - Haptisches Feedback ist aktiviert. - Haptisches Feedback ist deaktiviert. - Haptisches Suchfeedback deaktivieren - Haptisches Feedback ist aktiviert. - Haptisches Feedback ist deaktiviert. - Haptisches Suchfeedback deaktivieren - Haptisches Feedback ist aktiviert. - Haptisches Feedback ist deaktiviert. - Deaktiviere Haptisches Feedback beim Zoomen - Automatische HDR-Helligkeit ist aktiviert - Automatische HDR-Helligkeit ist deaktiviert - Deaktiviere automatische HDR-Helligkeit - HDR-Video ist aktiviert - HDR-Video ist deaktiviert - HDR-Video deaktivieren - Querformat im Fullscreen Modus aktiviert - Querformat im Fullscreen Modus deaktiviert - Querformat deaktivieren - Die „Gefällt mir“ und „Gefällt mir nicht“ Schaltflächen leuchten, wenn sie erwähnt werden. - Die „Gefällt mir“ und „Gefällt mir nicht“ Schaltflächen leuchten nicht, wenn sie erwähnt werden. - Leuchten der „Gefällt mir“ und „Gefällt mir nicht“ Schaltflächen deaktivieren - "CronetEngine's QUIC-Protokoll deaktivieren" - QUIC-Protokoll deaktivieren - Shorts-Player aktiv beim Start der Anwendung - Shorts-Player aktiv beim Start der Anwendung. - Shorts-Player beim App-Start ausblenden - Rolling numbers are animated. - Rolling numbers are not animated. - Disable rolling number animations - "Deaktiviere 'Abspielen mit 2x Geschwindigkeit' während du gedrückt hältst. - -Information: -• Deaktiviere das Geschwindigkeits-Overlay stellt das 'Slide to Suchen' Verhalten des alten Layouts wieder her. -• Deaktivieren dieser Einstellung aktiviert nicht die Geschwindigkeitsüberlagerung." - Geschwindigkeitsüberlagerung deaktivieren - Splash Animation ist aktiviert. - Splashanimation ist deaktiviert. - Splashanimation deaktivieren - "Deaktiviert die folgenden Interaktionen, wenn die Videobeschreibung erweitert wird: - -• Zum Scrollen tippen. -• Tippen und halten um Text auszuwählen." - Videobeschreibungsinteraktion deaktivieren - Kairoer Suchleiste ist deaktiviert. - "Kairo Suchleiste ist aktiviert. - -Seiteneffekt: Kairo Thema wird auch auf Benachrichtigungspunkte angewendet." - Kairo Suchleiste aktivieren - Kompaktsteuerungs-Overlay ist deaktiviert - Kompaktsteuerungs-Overlay ist aktiviert - Kompaktsteuerungs-Overlay aktivieren - Benutzerdefinierte Wiedergabegeschwindigkeit ist deaktiviert - Benutzerdefinierte Wiedergabegeschwindigkeit ist aktiviert - Benutzerdefinierte Wiedergabegeschwindigkeit aktivieren - Die benutzerdefinierte Farbe der Suchleiste ist deaktiviert - Die benutzerdefinierte Farbe der Suchleiste ist aktiviert - Eigene Suchleistenfarbe aktivieren - Debug-Protokolle enthalten keinen Puffer. - Debug-Protokolle enthalten Puffer. - Debug-Pufferprotokollierung aktivieren - Debug-Logs sind deaktiviert - Debug-Logs sind aktiviert - Debug-Protokollierung aktivieren - Die Standard-Wiedergabegeschwindigkeit gilt nicht für Shorts. - Die Standard-Wiedergabegeschwindigkeit gilt für Shorts. - Standard Wiedergabegeschwindigkeit für Shorts aktivieren - Externer Browser ist deaktiviert - Externer Browser ist aktiviert - Aktiviere externen Browser - Ladebildschirm für den Verlauf ist deaktiviert. - Ladebildschirm für den Verlauf ist aktiviert. - Farbverlauf-Ladebildschirm aktivieren - Abstand zwischen den Navigationstasten ist normal. - Der Abstand zwischen den Navigationstasten ist schmal. - Aktiviere schmale Navigationstasten - Standard-Umleitungsrichtlinie folgen - Umgehung von URL-Umleitungen - Links direkt öffnen - Aktiviere den OPUS-Codec, wenn die Antwort des Players den OPUS-Codec enthält. - OPUS Codec aktivieren - Tippen der Suchleiste ist deaktiviert - Tippen der Suchleiste ist aktiviert - Aktiviere Suchleisten-Tippen (Video Fortschrittsbalken) - Der Zeitstempel ist ausgeblendet. - "Zeitstempel ist aktiviert. - -Bekannte Probleme: Da dies eine Funktion in der Entwicklungsphase von Google ist, kann das Layout beschädigt werden." - Zeitstempel aktivieren - Helligkeit Wischen ist deaktiviert - Helligkeit Wischen ist aktiviert - Aktivierung der Helligkeitsgesten - Haptisches Feedback ist deaktiviert. - Haptisches Feedback ist aktiviert. - Haptisches Feedback aktivieren - Auch wenn die Helligkeit durch Wischen auf 0 gesetzt wird, ist die automatische Helligkeit nicht aktiviert. - Wenn die Helligkeit durch Wischen 0 erreicht wird, wird die automatische Helligkeit aktiviert. - Aktiviere Auto-Helligkeit durch Wischen - Berühren, um die Wischgeste zu aktivieren. - Berühren und halten, um die Wischgeste zu aktivieren. - Aktiviere Drücken-zu-Wischgeste - Wischen nach oben / nach unten wird nicht das nächste / vorherige Video abspielen. - Wischen nach oben / unten wird das nächste / vorherige Video abspielen. - Wischen zum Ändern des Videos aktivieren - Lautstärkegeste ist deaktiviert - Lautstärkegeste ist aktiviert - Aktiviere Lautstärkegesten - Navigationsleiste ist nicht transparent. - Navigationsleiste ist transparent. - Transparente Navigationsleiste aktivieren - Vollbild-Eingabe beim Herunterwischen unter dem Videoplayer ist deaktiviert. - Entering fullscreen when swiping down below the video player is enabled. - Enable watch panel gestures - "Aktivieren dieser Einstellung deaktiviert die Einstellungsschaltfläche in der Registerkarte \"Sie\". - -In diesem Fall verwenden Sie bitte den folgenden Pfad: -Sie Registerkarte > Kanal > Menü > Einstellungen." - Aktiviere breite Suchleiste in deinem Tab - Breite Suchleiste ist deaktiviert - Breite Suchleiste ist aktiviert - Aktiviere breite Suchleiste - Breite Suchleiste enthält nicht YouTube Header. - Breite Suchleiste enthält YouTube Header - Breite Suchleiste mit Header aktivieren - Beschreibung - "Geben Sie den Titel des Video-Beschreibungsfensters in Ihrer Sprache ein. -Die Option \"Videobeschreibung\" kann nicht funktionieren, wenn die eingegebene Zeichenkette nicht mit dem Titel des Videobeschreibungsfensters übereinstimmt." - Titel im Video-Beschreibungsfeld - Videobeschreibungen werden nicht automatisch erweitert. - Videobeschreibungen werden automatisch erweitert. - Videobeschreibungen erweitern - Möchtest Du fortfahren? - Neustarten um das Layout zu laden - Aktualisieren und Neustarten - Fehler beim Exportieren der Einstellungen. - Einstellungen wurden erfolgreich exportiert. - Exportiere Einstellungen zu Datei. - Einstellungen exportieren - Importieren - Kopieren - Einstellungen als Text importieren / exportieren. - Importieren / Exportieren als Text - Fehler beim Importieren der Einstellungen. - Einstellungen zurücksetzen - Einstellungen wurden erfolgreich importiert. - Einstellungen aus Datei Importieren - Einstellungen importieren - Zurücksetzen - Suche %s - ReVanced Extended - Externer Downloader - Nicht installiert - "%1$s ist nicht installiert. -Bitte lade %2$s von der Webseite herunter." - Warnung - %s ist nicht installiert. Bitte installieren. - "Videos werden in den folgenden Situation im Vollbild wieder gegeben: - -• Wenn ein Video gestartet wird. -• Wenn ein Zeitstempel in den Kommentaren geklickt wird." - Vollbild erzwingen - Liste der Filter für das Account Menü, durch Zeilenumbrüche getrennt. - Konto-Menüfilter bearbeiten - "Elemente des Account Menüs und Mein YouTube. -Manche Komponenten könnten nicht versteckt werden." - Account Menü verstecken - Albumkarten werden angezeigt. - Albumkarten werden ausgeblendet. - Albumkarten ausblenden - Vorgestellte Orte, Spiele und Musikbereiche werden angezeigt. - Ausgewählte Orte, Spiele und Musiksektionen sind versteckt. - Attributbereich ausblenden - Autoplay-Vorschau-Container wird angezeigt - Autoplay-Vorschau-Container wird ausgeblendet - Autoplay-Vorschau-Container ausblenden - Die Schaltfläche Store durchsuchen wird angezeigt - Die Schaltfläche Store durchsuchen wird ausgeblendet - Shop-Button ausblenden - "Versteckt folgende Abschnitte: -- Aktuelle Nachrichten -- Weiterschauen -- Entdecke mehr Kanäle -- Shopping -- Erneut anschauen" - Karussellregal ausblenden - In Feed angezeigt. - In Feed versteckt. - Kategorieleiste im Feed ausblenden - In verwandten Videos angezeigt. - Versteckt in verwandten Videos. - In verwandten Videos ausblenden - In den Suchergebnissen angezeigt. - Versteckt in Suchergebnissen. - In Suchergebnissen ausblenden - Kanalrichtlinien werden angezeigt - Kanalrichtlinien sind versteckt - Verstecke Kanalrichtlinien - Kanalmitgliedschaft-Abschnitt wird angezeigt - Kanalmitgliedschaft-Abschnitt wird versteckt - Kanalmitgliedschaft ausblenden - Links am oberen Rand des Kanalprofils werden angezeigt. - Links am oberen Rand des Kanalprofils werden versteckt. - Links vom Kanalprofil ausblenden - "Verkürzt -Playlists -Store" - Liste der zu filternden Kanalnamen, getrennt durch Zeilenumbrüche. - Kanal-Tab-Filter - Kanal-Tab-Filter ist deaktiviert. - Kanal-Tab-Filter ist aktiviert. - Aktivieren der Benutzerdefinierten Filter - Kanal-Wasserzeichen werden angezeigt - Kanal-Wasserzeichen sind versteckt - Verstecke Kanal-Wasserzeichen - Chapters sections are shown. - Chapters sections are hidden. - Hide chapters sections - Chips-Abschnitt wird angezeigt - Chips-Abschnitt wird versteckt. - Chips-Abschnitt verstecken - Clip-Button wird angezeigt. - Clip-Button wird versteckt. - Verstecke Clip-Button - Erstelle Short-Schaltfläche wird angezeigt. - Erstelle Short-Schaltfläche ist ausgeblendet. - Verstecke das Erstellen der Short-Schaltfläche - \"Danke\" Schaltfläche wird angezeigt. - \"Danke\" Schaltfläche wird versteckt. - Verstecke \"Danke\" Schaltfläche - Timestamp and emoji buttons are shown. - Timestamp and emoji buttons are hidden. - Hide timestamp and emoji buttons - Kommentare nach Mitglieder-Banner werden angezeigt. - Kommentare nach Mitglieder-Banner sind versteckt. - Kommentare nach Mitglieder-Banner ausblenden - Kommentarbereich wird im Home Feed angezeigt. - Kommentar-Sektion ist im Home Feed versteckt. - Kommentarbereich im Startfeed ausblenden - Kommentarbereich wird angezeigt - Kommentar-Bereich ist versteckt - Verstecke den Kommentarbereich - Im Kanal anzeigen. - Verstecke im Kanal. - Verstecke im Kanal - Im Home Feed und verwandte Videos anzeigen. - Versteckt in Home Feed und verwandten Videos. - Verstecke im Home Feed und verwandten Videos - Community-Beiträge im Abonnement-Feed werden angezeigt - Community-Beiträge im Abonnement-Feed sind versteckt - Community-Beiträge im Abo-Feed verstecken - Crowdfunding-Box wird angezeigt - Crowdfunding-Box ist versteckt - Verstecke Crowdfunding-Box - Filter für Doppeltippen wird angezeigt. - Doppeltipp-Overlay-Filter ausgeblendet. - Doppeltipp-Overlay-Filter ausblenden - Download-Schaltfläche wird angezeigt. - Download-Schaltfläche ist ausgeblendet. - Download-Schaltfläche verstecken - Endbildschirmkarten werden angezeigt - Endbildschirmkarten werden ausgeblendet - Endbildschirmkarten ausblenden - Erweiterbare Chips werden angezeigt - Erweiterbare Chips sind versteckt - Erweiterbaren Chip unter Videos ausblenden - Schaltfläche \"Untertitel\" wird angezeigt. - Schaltfläche \"Untertitel\" ist versteckt. - Verstecke \"Untertitel\" Schaltfläche - Liste der Filter für das Account Menü, durch Zeilenumbrüche getrennt. - Feed Flyout Menüfilter - Feed Flyout-Menüfilter ist deaktiviert. - Feed Flyout Menüfilter ist aktiviert. - Feed Flyout Menüfilter aktivieren - Feed Suchleiste wird angezeigt. - Feed Suchleiste ist ausgeblendet. - Feed Suchleiste ausblenden - Feed-Umfragen werden angezeigt - Feed-Umfragen sind versteckt - Feed-Umfragen verstecken - Filmstreifen-Overlay wird angezeigt - Filmstreifen-Overlay ist versteckt - Verstecke Filmstreifen-Overlay - Die schwebende Schaltfläche „Mikrofon“ wird angezeigt - Die schwebende Schaltfläche „Mikrofon“ ist ausgeblendet - Die schwebende Schaltfläche „Mikrofon“ ausblenden - \'For You\' shelves are shown. - Chips-Abschnitt wird versteckt. - Für dich ausblenden - Vollbildwerbung wird angezeigt. - Vollbildwerbung wird versteckt. - Vollbildwerbung verstecken - "Vollbildwerbung ist blockiert. - -Nebeneffekt: Community-Beitragsbilder können im Vollbildmodus blockiert werden." - Vollbildanzeigen werden über die Schaltfläche „Schließen“ geschlossen. - Vollbildanzeige schließen - Allgemeine Werbung wird angezeigt. - Allgemeine Werbung ist ausgeblendet. - Allgemeine Werbung ausblenden - YouTube Premium Werbung wird angezeigt. - YouTube Premium Werbung wird ausgeblendet. - YouTube Premium Werbung ausblenden - Graue Trennzeichen werden angezeigt - Graue Trennzeichen sind ausgeblendet - Graue Trennzeichen ausblenden - Handle wird angezeigt. - Handle is hidden. - Verstecke Handle - Bildsuche-Schaltfläche wird angezeigt. - Bildsuche-Schaltfläche ist ausgeblendet. - Bildsuche-Schaltfläche ausblenden - Bildregale werden angezeigt - Bildregale sind versteckt - Verstecke Bildregale - Infokarten-Abschnitte werden angezeigt - Infokarten-Abschnitte sind versteckt - Infokarten-Abschnitte verstecken - Infokarten werden angezeigt - Infokarten sind versteckt - Verstecke Infokarten - Infokarten werden angezeigt - Infokarten werden ausgeblendet - Info-Panels ausblenden - Verstecken-Button wird angezeigt - Teilnehmen-Button ist versteckt - Teilnehmen-Schaltfläche verstecken - Schlüsselkonzepte werden angezeigt. - Schlüsselkonzepte sind ausgeblendet. - Schlüsselkonzeptsektion ausblenden - "Home / Abonnements / Suchergebnisse werden gefiltert, um Inhalte zu verstecken, die zu Stichwortwörtern passen. - -Einschränkungen: -• Einige Shorts dürfen nicht ausgeblendet werden. -• Einige UI-Komponenten können nicht ausgeblendet werden. -• Die Suche nach einem Schlüsselwort kann keine Ergebnisse zeigen." - Über Schlüsselwortfilter - Kommentare werden nicht gefiltert. - Kommentare werden gefiltert. - Verstecke Kommentare nach Schlüsselwörtern - Videos im Home Feed werden nicht gefiltert. - Videos im Home Feed werden gefiltert. - Heimvideos nach Schlüsselwörtern ausblenden - "Keywords und Ausdrücke zu verstecken, getrennt durch neue Zeilen. -Wörter mit Großbuchstaben in der Mitte müssen im Gehäuse eingegeben werden (z.B. iPhone, TikTok, LeBlanc)." - Suchbegriffe ausblenden - Suchergebnisse sind nicht gefiltert. - Suchergebnisse sind gefiltert. - Suchergebnisse mit Schlüsselwörtern ausblenden - Videos in Abonnements Feed werden nicht gefiltert. - Videos in Abonnements Feed werden gefiltert. - Abonnementvideos mit Schlüsselwörtern ausblenden - Schlüsselwort \'%1$s\' wird alle Videos ausblenden. - Ungültiges Schlüsselwort. Kann \'%s\' nicht als Filter verwenden - Neueste Beiträge werden angezeigt - Neueste Beiträge sind versteckt - Neueste Beiträge ausblenden - Neuste Videos-Schaltfläche wird angezeigt. - \"Neustes Video\" Button ist versteckt. - Verstecke \"Neustes Video\" Button - Die Schaltflächen „Gefällt mir“ und „Gefällt mir nicht“ werden angezeigt. - Die Schaltflächen „Gefällt mir“ und „Gefällt mir nicht“ sind ausgeblendet. - Hide like and dislike buttons - Live-Chat-Nachrichten werden angezeigt.\n\nDiese Einstellung gilt auch für Shorts Live-Videos. - Live-Chat-Nachrichten sind ausgeblendet.\n\nDiese Einstellung gilt auch für Shorts Live-Videos. - Live-Chat-Nachrichten verbergen - Der Live-Chat-Replay-Button ist verborgen.\n\nEr erscheint im Vollbild, wenn Live-Chat geschlossen wird. - Der Live-Chat-Replay-Button ist verborgen.\n\nEr erscheint im Vollbild, wenn Live-Chat geschlossen wird. - Live-Chat Replay-Button ausblenden - Verstecke Videos mit weniger als 1000 Aufrufen von nicht abonnierten Kanälen von der Startseite. - Videos mit wenigen Aufrufen verstecken - Medizinische Infokarten werden angezeigt - Medizinische Infokarten sind versteckt - Medizinische Infokarten verstecken - Merchandise-Abschnitte werden angezeigt. - Merchandise-Abschnitte sind versteckt. - Merchandise-Abschnitt verstecken - Mix-Playlisten werden angezeigt. - Mix-Playlisten sind ausgeblendet. - Verstecke Mix-Playlisten - Filmabschnitte werden angezeigt. - Filmabschnitte sind versteckt. - Verstecke Filmabschnitt - Erstellen Schaltfläche wird angezeigt. - Erstellen Schaltfläche ist ausgeblendet. - Verstecke Erstellen Schaltfläche - Home Schaltfläche wird angezeigt - Home Schaltfläche ist ausgeblendet - Home Schaltfläche ausblenden - Navigationslabel wird angezeigt - Navigationslabel ist versteckt - Navigationslabel ausblenden - Bibliothek-Button wird angezeigt - Bibliothek-Button ist versteckt - Bibliothek-Button verstecken - Benachrichtigungsschaltfläche wird angezeigt. - Benachrichtigungs-Button ist ausgeblendet. - Benachrichtigungs-Schaltflächen ausblenden - Shorts Button wird angezeigt. - Shorts Button ist versteckt. - Verstecke Shorts Schaltfläche - Abonnement-Button wird angezeigt. - Abonnement-Button ist versteckt. - Abonnement-Button ausblenden - Die Schaltfläche „Benachrichtigen“ wird angezeigt. - Die Schaltfläche „Benachrichtigen“ ist ausgeblendet. - Schaltfläche „Benachrichtigen“ im Feed ausblenden - Label für bezahlte Promotion wird angezeigt. - Label für bezahlte Promotion wird versteckt. - Verstecke Label für bezahlte Promotion - Spielbare Elemente werden angezeigt. - Spielbare Elemente sind ausgeblendet. - Spielbare Elemente ausblenden - Autoplay button is shown. - Autoplay button is hidden. - Hide autoplay button - Captions button is shown. - Captions button is hidden. - Hide captions button - Cast button is shown. - Cast button is hidden. - Hide cast button - Collapse button is shown. - Collapse button is hidden. - Hide collapse button - Audio track menu is shown. - Audio track menu is hidden. - Hide audio track menu - Captions menu footer is shown. - Captions menu footer is hidden. - Hide captions menu footer - Captions menu is shown. - Captions menu is hidden. - Hide captions menu - Help & feedback menu is shown. - Help & feedback menu is hidden. - Hide help & feedback menu - Anhören mit YouTube Musikmenü wird angezeigt. - Anhören mit YouTube Musikmenü ist versteckt. - Verstecke Anhören mit YouTube Musikmenü - Lock screen menu is shown. - Lock screen menu is hidden. - Hide lock screen menu - Loop video menu is shown. - Loop video menu is hidden. - Hide loop video menu - Das Menü „Weitere Informationen“ wird angezeigt. - More information menu is hidden. - Hide more information menu - Picture-in-picture menu is shown. - Picture-in-picture menu is hidden. - Hide picture-in-picture menu - Playback speed menu is shown. - Playback speed menu is hidden. - Hide playback speed menu - Premium controls menu is shown. - Premium controls menu is hidden. - Hide premium controls menu - Quality menu footer is shown. - Quality menu footer is hidden. - Hide quality menu footer - Der Header des Qualitätsmenüs wird angezeigt. - Der Header des Qualitätsmenüs ist ausgeblendet. - Qualitätsmenü-Kopfzeile ausblenden - Report menu is shown. - Report menu is hidden. - Hide report menu - Stable volume menu is shown. - Stable volume menu is hidden. - Hide stable volume menu - Stats for nerds menu is shown. - Stats for nerds menu is hidden. - Hide stats for nerds menu - Watch in VR menu is shown. - Watch in VR menu is hidden. - Hide watch in VR menu - Fullscreen button is shown. - Fullscreen button is hidden. - Hide fullscreen button - Buttons are shown. - Buttons are hidden. - Hide previous & next button - YouTube Musik Button wird angezeigt. - YouTube Musik Button ist ausgeblendet. - YouTube Musik Button ausblenden - Save to playlist button is shown. - Save to playlist button is hidden. - Hide save to playlist button - Podcast-Abschnitte werden angezeigt. - Podcast-Abschnitte sind ausgeblendet. - Podcast-Abschnitte ausblenden - Vorschau-Kommentar wird angezeigt - Vorschau-Kommentar ist versteckt - Verstecke Vorschau-Kommentar - This changes the size of the comment section, so it is impossible to open a live chat replay in the comment section. - This does not change the size of the comment section, so it is possible to open the live chat replay in the comment section. - Hide preview comment type - Kommentar Button wird angezeigt. - Kommentar Button wird versteckt. - Verstecke Kommentar Button - Der Dislike-Button wird angezeigt. - Der Dislike-Button ist versteckt. - Verstecke den Dislike-Button - Schaltfläche \"Gefällt mir\" wird angezeigt. - \"Gefällt mir\" Schaltfläche ist versteckt. - Verstecke \"Gefällt mir\" Button - Live-Chat-Schaltfläche wird angezeigt. - Live-Chat-Schaltfläche ist versteckt. - Verstecke Live-Chat-Schaltfläche - Die Schaltfläche „Mehr“ wird angezeigt. - \"Mehr\" Button wird versteckt. - Verstecke \"Mehr\" Button - Die Schaltfläche „Mix-Wiedergabeliste öffnen“ wird angezeigt. - Mix-Playlist-Button ist ausgeblendet. - Schaltfläche „Mix-Wiedergabeliste öffnen“ ausblenden - Wiedergabeliste öffnen wird angezeigt. - Wiedergabelisten-Button ist ausgeblendet. - Verstecke Playlist Schaltfläche - Speichern-Taste wird angezeigt. - Speichern-Taste ist versteckt. - Schaltfläche „Speichern“ ausblenden - Teilen-Schaltfläche wird angezeigt. - Teilen-Schaltfläche ist versteckt. - Verstecke \"Teilen\" Schaltfläche - \"Schnellaktionen\" Container wird angezeigt - \"Schnellaktionen\" Container wird versteckt - Verstecke \"Schnellaktionen\" Container - "Versteckt die folgenden empfohlenen Videos: - -• Videos mit dem Tag nur für Mitglieder. -• Videos mit Sätzen wie \"Menschen auch gesehen\" unten." - Empfohlene Videos ausblenden - Im Schnellaktionscontainer werden weitere Videos angezeigt sowie die zugehörige Videoüberlagerung. - Related video overlay is hidden. - Hide related video overlay - Remix button is shown. - Remix button is hidden. - Hide remix button - Report button is shown. - Report button is hidden. - Hide report button - Rewards button is shown. - Rewards button is hidden. - Hide rewards button - Suchbegriff-Vorschaubilder werden angezeigt. - Suchbegriff-Vorschaubilder werden ausgeblendet. - Suchbegriff-Vorschaubilder ausblenden - Suche Nachricht wird angezeigt. - Nachricht suchen ist ausgeblendet. - Suchnachricht verbergen - Rückgängig suchen Nachricht wird angezeigt. - Rückgängig suchen Nachricht ist versteckt. - Suche rückgängig machen - Suchleiste für Video-Player wird angezeigt - Suchleiste für Video-Player wird versteckt - Thumbnail-Suchleiste wird angezeigt - Thumbnail-Suchleiste ist versteckt - Verstecke Thumbnail-Suchleiste - Verstecke Video-Player-Suchleiste - Selbstgesponserte Karten werden angezeigt. - Selbstgesponserte Karten sind versteckt. - Verstecke selbstgesponserte Karten - Elemente im YouTube-Einstellungsmenü verstecken - YouTube Einstellungsmenü verstecken - Share button is shown. - Share button is hidden. - Hide share button - Shop-Schaltfläche wird angezeigt. - Shop-Schaltfläche wird versteckt. - Hide shop button - Shopping links are shown. - Shopping links are hidden. - Hide shopping links - Channel bar is shown. - Channel bar is hidden. - Kanalleiste ausblenden - Comments button is shown. - Comments button is hidden. - Verstecke Kommentar Button - Dislike-Button wird angezeigt. - Dislike-Button ist versteckt. - Verstecke den Dislike-Button - Video link label is shown. - Video link label is hidden. - Verstecke vollständige Video-Linkbezeichnung - Info panels are shown. - Info panels are hidden. - Info-Panels ausblenden - Join button is shown. - Join button is hidden. - Teilnehmen-Schaltfläche verstecken - Like button is shown. - Like button is hidden. - Verstecke \"Gefällt mir\" Button - Live-Chat-Kopfzeile wird angezeigt.\n\nZurück Button wird nicht ausgeblendet. - Live-Chat-Kopfzeile wird ausgeblendet.\n\nZurück Button wird nicht ausgeblendet. - Live-Chat-Kopfzeile ausblenden - Navigation bar is shown. - Navigationsleiste ist versteckt. - Navigationsleiste verstecken - Paid promotion label is shown. - Paid promotion label is hidden. - Verstecke Label für bezahlte Promotion - Paused overlay buttons are shown. - Paused overlay buttons are hidden. - Pausierte Overlay-Tasten ausblenden - Button-Hintergrund wird angezeigt. - Button-Hintergrund wird ausgeblendet. - Wiedergabe & Pause Hintergrund ausblenden - Remix button is shown. - Remix button is hidden. - Verstecke Remix Button - Share button is shown. - Share button is hidden. - Verstecke \"Teilen\" Schaltfläche - Shown in watch history. - Hidden in watch history. - Verstecke im Beobachtungsverlauf - Im Home Feed und verwandte Videos anzeigen. - Versteckt in Home Feed und verwandten Videos. - Verstecke im Home Feed und verwandten Videos - In den Suchergebnissen angezeigt. - Versteckt in Suchergebnissen. - In Suchergebnissen ausblenden - Community-Beiträge im Abonnement-Feed werden angezeigt. - Community-Beiträge im Abonnement-Feed sind versteckt. - Community-Beiträge im Abo-Feed verstecken - "Verstecke Shorts Regale. - -Nebeneffekt: Offizielle Kopfzeilen in Suchergebnissen werden ausgeblendet." - Verstecke Ausschnitte aus Shorts in Kanälen - Shop button is shown. - Shop button is hidden. - Shop-Schaltfläche verstecken - Sound button is shown. - Sound button is hidden. - Hide sound button - Metadata label is shown. - Metadata label is hidden. - Sound-Metadaten-Label ausblenden - Subscribe button is shown. - Subscribe button is hidden. - Abonnement-Button ausblenden - \"Super Thanks\" Button wird angezeigt. - \"Super Thanks\" Button wird ausgeblendet. - Super Dankeschön ausblenden - Markierte Produkte werden angezeigt. - Markierte Produkte sind ausgeblendet. - Markierte Produkte ausblenden - Symbolleiste wird angezeigt. - Symbolleiste ist versteckt. - Toolbar verstecken - Title is shown. - Title is hidden. - Videotitel ausblenden - Die Schaltfläche „Mehr anzeigen“ wird angezeigt. - Die Schaltfläche „Mehr anzeigen“ ist ausgeblendet. - Schaltfläche „Mehr anzeigen“ ausblenden - Snackbar wird angezeigt - Snackbar ist versteckt - Snackbar verstecken - Button für Probeabo wird angezeigt. - Button für Probeabo wird ausgeblendet. - Button für Probeabo ausblenden - Karussell für Abonnements wird angezeigt. - Abonnement-Karussell ist versteckt. - Abonnement-Karussell ausblenden - Empfohlene Aktionen werden angezeigt - Vorgeschlagene Aktionen sind versteckt - Verstecke empfohlene Vorschläge - "This setting has been deprecated. - -Instead, use the 'Settings → Autoplay → Autoplay next video' setting. - -Note: -• If you have any issues with 'Suggested video end screen', try restarting the app." - Empfohlene Video-Endbildschirm wird angezeigt. - "Der Endbildschirm für vorgeschlagene Videos ist ausgeblendet, wenn die Autoplay-Funktion deaktiviert ist. - -Autoplay kann in den YouTube-Einstellungen geändert werden: -Einstellungen → Autoplay → Nächstes Video automatisch abspielen" - Verstecke vorgeschlagenes Video-End-Bildschirm - \"Danke\" Schaltfläche wird angezeigt. - \"Danke\" Schaltfläche wird versteckt. - Verstecke \"Danke\" Schaltfläche - Ticket-Abschnitte werden angezeigt - Ticket-Abschnitte sind versteckt - Ticket-Abschnitte verstecken - Der Zeitstempel wird angezeigt - Der Zeitstempel ist ausgeblendet - Zeitstempel ausblenden - Zeitgesteuerte Reaktionen werden angezeigt - Zeitgesteuerte Reaktionen sind versteckt - Zeitgesteuerte Reaktionen verstecken - Der Cast button wird angezeigt. - Cast Button ist versteckt. - Verstecke Erstellen Schaltfläche - Erstelle Schaltfläche wird angezeigt. - Erstelle Schaltfläche ist ausgeblendet. - Verstecke Erstellen Schaltfläche - Benachrichtigungsschaltfläche wird angezeigt. - Benachrichtigungsschaltfläche ist ausgeblendet. - Benachrichtigungsschaltfläche ausblenden - Transkriptabschnitte werden angezeigt - Transkriptabschnitte sind ausgeblendet - Transkriptabschnitte ausblenden - Videowerbung wird angezeigt. - Videowerbung ist versteckt. - Verstecke Videowerbung - Empfohlene Videos mit weniger als einer bestimmten Anzahl von Ansichten ausblenden.\n\nBekanntes Problem: Videos mit 0 Ansichten werden nicht gefiltert. - Empfohlene Videos nach Ansichten ausblenden. - Videos mit Ansichten, die größer als diese Zahl sind, werden ausgeblendet. - Größer als Ansichten - Videos mit weniger Aufrufen als diese Zahl werden ausgeblendet. - Weniger als Anrufe - K -> 1 000\nM -> 1 000 000\nB -> 1 000 000 000\nviews -> Aufrufe - Geben Sie Ihre Sprachvorlage für die Anzahl der Aufrufe an, die unter jedem Video in der Benutzeroberfläche angezeigt werden. Jeder Schlüssel (ein Buchstaben/ein Wort in Ihrer Sprache) -> Wert (Bedeutung des Schlüssels) muss auf einer neuen Zeile liegen. Schlüssel gehen vor dem \"->\" Zeichen. Wenn Sie die App oder die Systemsprache ändern, müssen Sie diese Einstellung zurücksetzen.\n\nBeispiele:\nDeutsch: 10K views = K -> 1000, views -> views\nSpanisch: 10 K vistas = K -> 1000, vistas -> views - Schlüssel anzeigen - Das Banner „Produkte anzeigen“ wird angezeigt. - Das Banner „Produkte anzeigen“ ist ausgeblendet. - Produkt Banner ausblenden - Sprachsuche wird angezeigt. - Sprachsuche ist ausgeblendet. - Sprachsuche ausblenden - Web Suchergebnisse werden angezeigt. - Web Suchergebnisse sind versteckt. - Web Suchergebnisse verbergen - Zoom-Overlay wird angezeigt. - Zoom-Overlay ist ausgeblendet. - Zoom-Overlay ausblenden - Afn Blau - Afn Rot - Custom - Stock - MMT - Revancify Blue - Revancify Red - YouTube - Bewahrt den Querformat beim Ausschalten des Bildschirms im Vollbild. - The amount of milliseconds the landscape mode is forced after the screen in turned on. - Timeout für den Wechsel zum Querformat - Querformat behalten - Stock - Aktion zum Doppeltippen ist deaktiviert. - "Doppeltes Tippen ist aktiviert. - -• Moderne 1: Doppeltippen, um das minimierte Video auf eine größere Größe zu ändern. -• Moderne 2, 3: Doppeltippen, um das minimierte Video zu schließen." - Doppeltes Tippen - Drag & Drop ist deaktiviert. - Drag & Drop ist aktiviert. - Aktiviere Drag & Drop - Erweitern und Schließen Schaltflächen werden angezeigt. - Tasten sind ausgeblendet.\n(wischen Sie den Miniplayer zum erweitern oder schließen) - Ausklappen und Schließen der Tasten ausblenden - Vor- und zurückspringen wird angezeigt. - Vorwärts springen und zurück sind versteckt. - Vorwärts- und Rückwärts-Buttons ausblenden - Untertitel werden angezeigt. - Untertitel sind versteckt. - Untertitel ausblenden - Die Transparenz des Miniplayers muss zwischen 0-100 liegen. Zurückgesetzt auf Standardwerte. - Deckkraft Wert zwischen 0-100, wobei 0 transparent ist. - Deckkraft der Überlagerung - Original - Telefon - Tablet - Modern 1 - Modern 2 - Modern 3 - Miniplayer Typ - Overlay-Schaltfläche - "Tap to toggle always repeat states. -Tap and hold to toggle pause after repeat states." - Show always repeat button - "Tap to copy video URL. -Tap and hold to copy video URL with timestamp." - "Tap to copy video URL with timestamp. -Tap and hold to copy video timestamp." - Show copy timestamp URL button - Show copy video URL button - Tap to launch external downloader. - Show external download button - Tap and hold to change button state. - Wiedergabegeschwindigkeit zurücksetzen: %sx. - "Tippen um den Geschwindigkeitsdialog zu öffnen. -Tippen und halten um die Wiedergabegeschwindigkeit auf 1.0x zurückzusetzen. Halten Sie erneut gedrückt, um die Standardgeschwindigkeit wiederherzustellen." - Zeige Geschwindigkeitsdialog Taste - "Tippen, um eine Playlist aller Videos vom Kanal von ältestem bis neuestem zu erstellen. -Tippen und gedrückt halten, um rückgängig zu machen." - Zeitgeordnete Wiedergabelistenschaltfläche anzeigen - \"Tippen, um den Whitelist-Dialog zu öffnen. -Tippen und halten Sie, um den Einstellungsdialog für die Whitelist anzuzeigen. - Zeige Whitelist-Button - Excluded - Included - Normal - Aktionsschaltflächen - Additional settings - Animation / Feedback - Experimentelle Flags - Bildregion-Beschränkungen - Als Datei importieren / exportieren - Als Text importieren / exportieren - Stichwortfilter - Others - Overlay buttons - Patch-Informationen - Quick actions - Empfohlene Videos - Shorts shelves - Tool used - Zählerfilter anzeigen - Zeige oder verstecke Element im Account Menü und mein YouTube. - Account Menü - Aktionsschaltflächen unter Videos ausblenden oder anzeigen. - Aktionsschaltflächen - Werbung - Alternatives Vorschaubild - Ambient-Modus deaktivieren oder Einschränkungen des Ambient-Modus umgehen. - Ambient-Modus - Verstecke oder zeige die Kategorieleiste im Feed, Suche und verwandten Videos an. - Kategorieleiste - Komponenten der Kanalleiste unter Videos ausblenden oder anzeigen. - Kanalleiste - Komponenten im Kanalprofil ausblenden oder anzeigen. - Kanal Profil - Komponenten der Kommentarsektion ausblenden oder anzeigen. - Kommentare - Verstecke oder zeige Community-Beiträge im Feed und Kanal. - Community Beiträge - Komponenten mit Benutzerdefinierten Filtern verstecken - Benutzerdefinierter Filter - Komponenten des Flyout-Menüs im Feed verstecken oder anzeigen. - Flyout Menü - Feed - Komponenten im Zusammenhang mit Vollbild ausblenden oder ändern. - Vollbild - Allgemein - Haptisches Feedback aktivieren oder deaktivieren - Haptisches Feedback - Einstellungen importieren / exportieren - Einstellungen importieren / exportieren - Ändern Sie den Stil des in App minimierten Players. - Miniplayer - Sonstiges - Informationen über angewandte Patches - Patch-Informationen - Hide or show buttons in the video player. - Player buttons - Komponenten des Flyout-Menüs im Feed verstecken oder anzeigen. - Flyout Menü - Player - Return YouTube Dislike - SponsorBlock - Die Suchleisten-Komponenten anpassen. - Suchleiste - Elemente im YouTube-Einstellungsmenü verstecken - Einstellungsmenü - Elemente im YouTube-Einstellungsmenü verstecken - Shorts player - Shorts - Wischgesten - Verstecke oder ändere Toolbar-Komponenten, wie die Suchleiste, Buttons und Header. - Werkzeugleiste - Komponenten der Videobeschreibung ausblenden oder anzeigen. - Videobeschreibung - Videos mit Schlüsselwörtern oder Ansichten ausblenden. - Video filter - Video - Der obere Rand der Schnellaktionen muss zwischen 0-32 liegen. Zurückgesetzt auf Standardwerte. - Konfigurieren Sie den Abstand von der Suchleiste auf die Meta-Leiste zwischen 0-32. - Quick actions top margin - "Forcefully rejects the software AV1 codec response. -A different codec will be applied after about 20 seconds of buffering." - Reject software AV1 codec response - Das Fallback-Verfahren führt zu etwa 20 Sekunden Pufferung. - Playback speed changes only apply to the current video. - Playback speed changes apply to all videos. - Remember playback speed changes - %s ändert Standardgeschwindigkeit. - Qualitätseinstellungen werden nur für das aktuelle Video angewendet - Qualitätseinstellungen werden für alle Videos angewendet - Qualitätseinstellungen merken - Changing default mobile data quality to %s. - Failed to set video quality. - Changing default Wi-Fi quality to %s. - "Entfernt den Diskretionsdialog des Betrachters. -Dies umgeht nicht die Altersbeschränkung. Es akzeptiert ihn nur automatisch." - Diskretion des Betrachters entfernen - Replaces the software AV1 codec with the VP9 codec. - Replace software AV1 codec - Kanalhandle wird verwendet. - Kanalname wird verwendet. - Kanalhandle ersetzen - Tap to show the remaining time. - Tap to open playback speed or video quality flyout menu. - Replace time stamp action - Ersetzt die Erstellen-Schaltfläche mit Einstellungen. - Ersetze Erstellen-Schaltfläche - "Tippen, um die YouTube-Einstellungen zu öffnen. -Tippe und halte zum Öffnen der RVX-Einstellungen." - "Tippen, um die RVX-Einstellungen zu öffnen. -Tippe und halte zum Öffnen der YouTube-Einstellungen." - Aktionstyp zum Zuweisen der Taste - Thumbnails der Suchleiste werden im Vollbild angezeigt. - Suchleiste Miniaturansichten werden über der Suchleiste angezeigt. - Alte Suchleiste Thumbnails wiederherstellen - Altes Qualitätsmenü wird nicht angezeigt - Altes Qualitätsmenü wird angezeigt - Altes Qualitätsmenü wiederherstellen - Über - Dislikes Daten werden von der True RYD Worker API zur Verfügung gestellt. Tippe hier, um mehr zu erfahren. - ReturnYouTubeDislike.com - Like-Button für bestes Aussehen - Like-Button für minimale Breite - Kompakter Like-Button - Dislikes als Nummer angezeigt - Dislikes als Prozentsatz angezeigt - Dislikes als Prozentsatz - Dislikes werden nicht angezeigt - Dislikes werden angezeigt - Return YouTube Dislike aktivieren - Dislikes nicht verfügbar (Client-API Limit erreicht) - Dislikes unavailable (status %d). - Dislikes temporarily unavailable (API timed out). - Dislikes unavailable (%s). - Reload video to vote using Return YouTube Dislike - Dislikes bei Shorts versteckt - Dislikes bei Shorts angezeigt - "Dislikes bei Shorts angezeigt - -Einschränkung: Dislikes werden im Inkognito Modus nicht angezeigt." - Dislikes bei Shorts anzeigen - Benachrichtigung wird nicht angezeigt, wenn „Return YouTube Dislike“ nicht verfügbar ist. - Benachrichtigung wird angezeigt, wenn „Return YouTube Dislike“ nicht verfügbar ist. - Zeige eine Benachrichtigung an, wenn die API nicht verfügbar ist - Removes tracking query parameters from the URLs when sharing links. - Sanitize sharing links - Über - sponsor.ajay.app - Die Daten werden von der SponsorBlock API bereitgestellt. Tippen Sie hier, um mehr zu erfahren und Downloads für andere Plattformen zu sehen - API-URL wurde geändert - API-URL ist ungültig - API-URL zurücksetzen. - Erscheinungsbild - Farbe geändert - Farbe: - Ungültiger Farbcode - Farben zurücksetzen - Neue Segmente erstellen - Segment Verhalten ändern - Überspringen Button automatisch verstecken - Überspringen Button für das ganze Segment anzeigen - Überspringen Button nach wenigen Sekunden verstecken - Kompakten Überspringen Button verwenden - Überspringen Button mit bestem Aussehen. - Überspringen Button mit minimaler Breite. - Button für neues Segment anzeigen - Button für neues Segment wird nicht angezeigt - Button für neues Segment wird angezeigt - SponsorBlock aktivieren - SponsorBlock ist ein crowd-sourced System um nervende Teile von YouTube Videos zu überspringen. - Button zur Abstimmung zeigen - Button zur Abstimmung wird versteckt. - Button zur Abstimmung wird angezeigt. - Allgemein - Adjust new segment step - Wert muss eine positive Zahl sein - Number of milliseconds the time adjustment buttons move when creating new segments. - API-URL ändern - Die Addresse zum API-Server von SponsorBlock - Minimale Segment-Dauer - Segment kürzer als dieser Werte (in Sekunden) werden nicht angezeigt oder übersprungen. - Enable skip count tracking - Skip count tracking is not enabled. - Lets the SponsorBlock leaderboard know how much time is saved. A message is sent to the leaderboard each time a segment is skipped. - Zeige eine Benachrichtigung an, wenn automatisch übersprungen wird - Benachrichtigung wird nicht angezeigt. Tippen Sie hier, um ein Beispiel zu sehen. - Benachrichtigung wird angezeigt, wenn ein Segment automatisch übersprungen wird. Tippen Sie hier, um ein Beispiel zu sehen. - Video-Länge ohne Segmente anzeigen - Gesamte Video-Länge wird angezeigt - Video-Länge ohne die gesamte Segment-Länge wird in Klammern neben der gesamten Video-Länge angezeigt - Ihre private Benutzer-ID - Benutzer-ID darf nicht leer sein - Dies sollte privat gehalten werden. Es ist wie ein Passwort und sollte nicht an andere weitergegeben werden. Wenn jemand es hat, kann er sich für Sie ausgeben - Bereits gelesen - Es wird empfohlen, die SponsorBlock Richtlinien zu lesen, bevor ein Segment gesendet wird - Zeig es mir - Befolgen Sie die Richtlinien - Richtlinien enthalten Tipps und Regeln zum Einreichen von Segmenten - Richtlinien anzeigen - Wähle eine Segmentkategorie aus - The segment lasts from %1$02d:%2$02d to %3$02d:%4$02d (%5$d minutes %6$02d seconds)\nIs it ready to submit? - Das Segment ist von\n\n\n%1$s\nbis\n%2$s\n\n(%3$s)\n\nBereit zum Absenden? - Sind die Zeiten korrekt? - Category is disabled in settings. Enable category to submit. - Willst du die Start- oder Endzeiten für den Abschnitt bearbeiten? - Ungültige Zeit angegeben - Zeiten des Abschnitts manuell bearbeiten - Ende - Mark two locations on the time bar first. - Start - Jetzt - Preview the segment, and ensure it skips smoothly. - Start must be before the end. - Time the segment ends at - Time the segment begins at - Neues SponsorBlock Segment - Zurücksetzen - Farbe zurücksetzen - Füller / Witze - Nebensächliche Szenen, die nur als Füller oder Witz dienen und nicht benötigt werden um den Hauptinhalt des Videos zu verstehen. Dies bezieht sich nicht auf Segmente, die Kontext oder Hintergrunddetails liefern. - Highlight - Der Abschnitt des Videos, nach dem die meisten Menschen suchen. - Interaktionserinnerung (Abonnieren) - Wenn es eine kurze Erinnerung zum \"Daumen hoch\", Abonnieren, oder Folgen in der Mitte von Inhalt gibt. Falls es lang oder über etwas Bestimmtes ist, sollte es stattdessen \"Unbezahlt/Eigenwerbung\" sein. - Unterbrechung/Introanimation - Ein Videosegment ohne richtigen Inhalt. Kann eine Pause, ein Standbild oder eine sich wiederholende Animation sein. Dies sollte nicht für Übergänge verwendet werden, die Informationen enthalten. - Musik: Nicht-Musik-Bereich - Nur für den Gebrauch in Musikvideos. Abschnitte von Musikvideos ohne Musik, die noch nicht von einer anderen Kategorie abgedeckt sind. - Endkarten/Credits - Credits oder wenn die YouTube-Endkarten erscheinen. Nicht für Abschluss mit Informationen. - Vorschau/Zusammenfassung - Sammlung von Clips, die zeigen, was in diesem Video passiert oder was anderen Videos einer Serie passiert ist, wo alle Informationen später im Video wiederholt werden. - Unbezahlt / Eigenwerbung - Ähnlich wie \"Gesponsorte Videosegmente\", jedoch für unbezahlte oder Eigenwerbung. Dies beinhaltet Bereiche über Merchandise, Spenden, oder Informationen darüber, mit wem zusammengearbeitet wurde. - Sponsor - Bezahlte Werbung, bezahlte Empfehlungen und direkte Werbung, nicht für Eigenwerbung, kostenlose Fremdwerbung oder Empfehlungen für Anlässe/Personen/Webseiten/Produkte. - Kopieren - Export fehlgeschlagen: %s. - Einstellungen importieren / exportieren - Deine SponsorBlock JSON Konfiguration, die zu ReVanced Extended und anderen SponsorBlock Platformen importiert / exportiert werden kann - Deine SponsorBlock JSON Konfiguration, die zu ReVanced Extended und anderen SponsorBlock Platformen importiert / exportiert werden kann. Dies enthält deine private Nutzer-ID. Teile das mit Vorsicht. - Importieren fehlgeschlagen: %s. - Einstellungen erfolgreich importiert. - Your settings contain a private SponsorBlock userid.\n\nYour user id is like a password and it should never be shared.\n - Do not show again - Einstellungen in Zwischenablage kopiert. - Automatisch überspringen - Einmalig automatisch Überspringen - Überspringen - Highlight - Füller überspringen - Zum Highlight springen - Interaktion überspringen - Intro überspringen - Unterbrechung überspringen - Unterbrechung überspringen - Nicht-Musik überspringen - Outro überspringen - Vorschau überspringen - Rückblick überspringen - Vorschau überspringen - Promo überspringen - Sponsor überspringen - Segment überspringen - Deaktivieren - In der Suchleiste anzeigen - Überspringen Schaltfläche anzeigen - Füller übersprungen - Zum Highlight gesprungen - Nervige Erinnerung übersprungen - Intro übersprungen - Unterbrechung übersprungen. - Unterbrechung übersprungen. - Mehrere Segmente übersprungen - Nicht-Musik-Sektion übersprungen - Outro übersprungen - Vorschau übersprungen - Rückblick übersprungen. - Vorschau übersprungen. - Eigenwerbung übersprungen - Sponsor übersprungen - Nicht übermitteltes Segment übersprungen - SponsorBlock temporarily unavailable. - SponsorBlock temporarily unavailable (status %d). - SponsorBlock temporarily unavailable (API timed out). - Statistiken - Statistiken vorübergehend nicht verfügbar (API ist nicht verfügbar) - Loading... - Your reputation is <b>%.2f</b> - You\'ve saved people from <b>%s</b> segments - %1$s hours %2$s minutes - %1$s minutes %2$s seconds - %s seconds - That\'s <b>%s</b> of their lives.<br>Tap here to see the leaderboard. - Hier tippen, um die globalen Statistiken und Top-Mitwirkende zu sehen - SponsorBlock Rangliste - SponsorBlock is disabled. - You\'ve skipped <b>%s</b> segments - Zähler für übersprungene Segmente zurücksetzen? - That\'s <b>%s</b>. - You\'ve created <b>%s</b> segments - Your username: <b>%s</b> - Hier tippen, um deinen Benutzernamen zu ändern - Benutzername konnte nicht geändert werden: Status: %1$d %2$s. - Benutzername wurde erfolgreich geändert - Das Segment kann nicht abgesendet werden. \nBereits vorhanden - Das Segment kann nicht gesendet werden: %s - Unable to submit segment: %s. - Unable to submit segment.\nRate Limited (too many from the same user or IP). - Segmente können nicht übermittelt werden (API Timeout) - Segmente können nicht übermittelt werden (status: %1$d %2$s). - Segment erfolgreich gesendet - Benachrichtigung wird nicht angezeigt, wenn „SponsorBlock“ nicht verfügbar ist. - Benachrichtigung wird angezeigt, wenn „SponsorBlock“ nicht verfügbar ist. - Zeige eine Benachrichtigung an, wenn die API nicht verfügbar ist - Kategorie ändern - Negativ bewerten - Kann nicht für Segment abstimmen: %s - Unable to vote for segment (API timed out). - Kann nicht für Segment abstimmen (status: %1$d %2$s). - Es gibt keine Segmente zur Abstimmung - Positiv bewerten - Settings copied to clipboard. - Time stamp copied to clipboard. (%s) - URL copied to clipboard. - URL with timestamp copied to clipboard. - Original - Mag ich - Mag ich (Cairo) - Herz - Herz (Farbton) - Ausgeblendet - Doppeltipp-Animation - Der untere Rand des Meta-Panels muss zwischen 0-64 liegen. Zurückgesetzt auf Standardwerte. - Konfigurieren Sie den Abstand von der Suchleiste auf die Meta-Leiste zwischen 0-64. - Meta-Panel unteren Rand - Drücken und halten Sie den Zeitstempel, um den Wiederholungsstatus der Shorts zu ändern. - Zeitstempel Aktion für langes Drücken - "Shows the video title section in fullscreen. - -Limitation: Video title disappears when clicked." - Show video title section - Wenn Autoplay aktiviert ist, wird das nächste Video nach dem Countdown abgespielt. - Ist Autoplay aktiviert, wird das nächste Video sofort abgespielt. - Autotoplay Countdown überspringen - "Skips the preloaded buffer at the start of videos to immediately apply the default video quality. - -Info: -• When the video starts, there is a delay of approximately 0.3 seconds. -• Does not apply to HDR videos, live stream videos, or videos shorter than 15 seconds." - Skip preloaded buffer - Benachrichtigung wird nicht angezeigt. - Benachrichtigung wird angezeigt. - Zeige eine Benachrichtigung beim Überspringen an - Skipped preloaded buffer. - Die Geschwindigkeit der Überlagerung muss zwischen 0-8.0 liegen. Zurückgesetzt auf Standardwerte. - Geschwindigkeitsüberschreitung zwischen 0-8.0. - Geschwindigkeitsüberlagerung - "Spoofing the client version to the old version. - -• This will change the appearance of the app, but unknown side effects may occur. -• If later turned off, the old UI may remain until clear the app data." - Version nicht gefälscht - Version gefälscht - 17.33.42 - Altes Benutzerlayout wiederherstellen - 17.41.37 - Alte Wiedergabeliste wiederherstellen - 18.05.40 - Altes Kommentarfeld wiederherstellen - 18.17.43 - Wiederherstellen alter Spieler Flyout Panel - 18.33.40 - Alte Shorts Aktionsleiste wiederherstellen - 18.38.45 - Altes Standard-Video-Qualitätsverhalten wiederherstellen - 18.48.39 - Deaktiviert Ansichten und gefällt es, in Echtzeit aktualisiert zu werden - Spoof App Versionsziel - Geben Sie das Spoof App Versionsziel ein. - Bearbeite App-Version fälschen - Spoof App Version - "Die App-Version wird auf eine ältere Version von YouTube gefälscht. - -Dies wird das Aussehen und die Funktionen der App verändern, aber unbekannte Nebeneffekte können auftreten. - -Wenn später ausgeschaltet wird empfohlen, die App-Daten zu löschen, um UI-Fehler zu verhindern." - "Spoofs the device dimensions in order to unlock higher video qualities that may not be available on your device." - Spoof device dimensions - Swipe gestures are disabled in \'Lock screen\' mode. - Swipe gestures are enabled in \'Lock screen\' mode. - Swipe gestures in \'Lock screen\' mode - Auto - Der Schwellenwert für das Wischen - Wischgrößen-Schwellenwert - Die Sichtbarkeit des Wischen Overlay-Hintergrunds - Wischen Hintergrund Sichtbarkeit - Die Größe des Wischbereichs darf nicht mehr als 50 betragen. Zurückgesetzt auf Standardwert. - Prozentsatz der wischbaren Bildschirmfläche.\n\nHinweis: Dies wird auch die Größe des Bildschirmbereichs für die Geste mit doppeltem Tippen ändern. - Wischüberlagerungsgröße - Die Textgröße für Wischüberlagerung - Wischüberlagerung Textgröße - Die Anzahl der Millisekunden, die das Overlay sichtbar ist - Swipe Overlay-Zeitüberschreitung - "Tauscht die Positionen des Knopfes Erstellen mit der Schaltfläche \"Benachrichtigungen\" aus, indem die Geräteinformationen getäuscht werden. - -• Das Gerät muss neu gestartet werden, um diese Einstellung zu ändern. -• Deaktivieren dieser Einstellung lädt mehr Werbung von der Serverseite. -• Sie sollten diese Einstellung deaktivieren, um Video-Werbung sichtbar zu machen." - Erstellungs- und Benachrichtigungs-Buttons tauschen - Stock - Der Kanal \'%1$s\' konnte nicht auf die Whitelist für %2$s gesetzt werden. - Der Kanal \'%1$s\' wurde auf die Whitelist für %2$s gesetzt. - Es gibt keine Kanäle auf der Whitelist. - Nicht zur Whitelist hinzugefügt. - Fehler beim Laden der Kanalinformationen. - Zur Whitelist hinzugefügt. - Wiedergabegeschwindigkeit - Kanal \'%1$s\' von %2$s entfernen? - Der Kanal \'%1$s\' konnte nicht von der Whitelist für %2$s entfernt werden. - Der Kanal \'%1$s\' wurde von der Whitelist für %2$s entfernt. - Überprüfen oder die Liste der Kanäle entfernen, die zur Whitelist hinzugefügt wurden. - Kanal Whitelist - Sponsorenblock - diff --git a/src/main/resources/youtube/translations/el-rGR/missing_strings.xml b/src/main/resources/youtube/translations/el-rGR/missing_strings.xml deleted file mode 100644 index fdfdb4e6d..000000000 --- a/src/main/resources/youtube/translations/el-rGR/missing_strings.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - Don\'t show again - Courses / Learning - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - MMT Blue - MMT Green - MMT Orange - MMT Pink - MMT Turquoise - MMT Yellow - Revancify Yellow - Vanced Black - Vanced Light - Xisr Yellow - Adjust: Mark Start and End Time for segment - Verify the Segment - Edit the Segment - Forward by Specified Time (Default: 150ms) - Publish Created Segment - Rewind by Specified Time (Default: 150ms) - diff --git a/src/main/resources/youtube/translations/el-rGR/strings.xml b/src/main/resources/youtube/translations/el-rGR/strings.xml deleted file mode 100644 index 493d5c257..000000000 --- a/src/main/resources/youtube/translations/el-rGR/strings.xml +++ /dev/null @@ -1,1740 +0,0 @@ - - - Ενεργοποίηση των στοιχείων ελέγχου προσβασιμότητας για το πρόγραμμα αναπαραγωγής βίντεο; - Τα στοιχεία ελέγχου σας τροποποιούνται επειδή είναι ενεργή κάποια υπηρεσία προσβασιμότητας. - Συνέχεια - "Το MicroG GmsCore δεν έχει άδεια να τρέχει στο παρασκήνιο. - -Ακολουθήστε τον οδηγό \"Don't kill my app\" για το τηλέφωνό σας και εφαρμόστε τις οδηγίες στο MicroG. - -Αυτό απαιτείται για να λειτουργήσει η εφαρμογή." - "Οι βελτιστοποιήσεις μπαταρίας στο MicroG GmsCore πρέπει να απενεργοποιηθούν για την αποφυγή προβλημάτων. - -Πατήστε το κουμπί «Συνέχεια» και απενεργοποιήστε τις βελτιστοποιήσεις μπαταρίας για το MicroG." - Άνοιγμα ιστοσελίδας - Απαιτείται ενέργεια - Ενεργοποιήστε τις ρυθμίσεις cloud messaging για να λαμβάνετε ειδοποιήσεις. - Άνοιγμα του MicroG GmsCore - Το MicroG GmsCore δεν είναι εγκατεστημένο. Εγκαταστήστε το. - "Το DeArrow παρέχει μικρογραφίες από το κοινό για τα βίντεο. Οι μικρογραφίες αυτές είναι συχνά πιο σχετικές από εκείνες που παρέχει το ίδιο το YouTube. Αν ενεργοποιηθεί, οι διευθύνσεις URL των βίντεο θα στέλνονται στον διακομιστή API χωρίς να στέλνονται άλλα δεδομένα. Αν κάποιο βίντεο δεν έχει μικρογραφίες DeArrow, θα εμφανιστούν είτε οι αρχικές του μικρογραφίες είτε λήψεις ακίνητων καρέ. - -Πατήστε για να μάθετε περισσότερα για το DeArrow." - Σχετικά με το DeArrow - Μη έγκυρο URL για το DeArrow API. - Η διεύθυνση URL του τελικού σημείου αποθήκευσης μικρογραφιών DeArrow. - Διεύθυνση API του DeArrow - Δεν εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης αν το DeArrow δεν είναι διαθέσιμο. - Εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης αν το DeArrow δεν είναι διαθέσιμο. - Εμφάνιση μηνύματος αν το API δεν είναι διαθέσιμο - DeArrow προσωρινά μη διαθέσιμο. (κωδικός: %s) - DeArrow προσωρινά μη διαθέσιμο. - Καρτέλα «Αρχική» - Καρτέλα «Εσείς» - Αρχικές μικρογραφίες - DeArrow & Αρχικές μικρογραφίες - DeArrow & Ακίνητα καρέ - Ακίνητα καρέ - Λίστες αναπαραγωγής, προτάσεις - Αποτελέσματα αναζήτησης - Ακίνητα καρέ - Τα ακίνητα καρέ λαμβάνονται από την αρχή / μέση / τέλος κάθε βίντεο. Αυτές οι εικόνες είναι ενσωματωμένες στο YouTube και δεν χρησιμοποιείται εξωτερικό API. - Σχετικά με τις λήψεις ακίνητων καρέ - Χρησιμοποιούνται ακίνητα καρέ υψηλής ποιότητας. - Χρησιμοποιούνται ακίνητα καρέ μέτριας ποιότητας. Οι μικρογραφίες θα φορτώνουν γρηγορότερα, αλλά τα ζωντανά, μη κυκλοφορημένα η πολύ παλιά βίντεο ενδέχεται να εμφανίζουν κενές μικρογραφίες. - Χρήση γρήγορων λήψεων καρέ - Αρχή του βίντεο - Μέση του βίντεο - Τέλος του βίντεο - Ο χρόνος του βίντεο από τον οποίο θα ληφθούν τα καρέ - Καρτέλα «Εγγραφές» - Η προσθήκη πληροφοριών στη χρονοσφραγίδα είναι απενεργοποιημένη. - "Οι πληροφορίες προσθέτονται στην χρονοσφραγίδα. - -Πατήστε για να ρυθμίσετε την ποιότητα του βίντεο ή την ταχύτητα αναπαραγωγής. -Πατήστε παρατεταμένα για εναλλαγή του τύπου της πληροφορίας που προσθέτεται." - Προσθήκη πληροφοριών χρονοσφραγίδας - Εμφάνιση της ταχύτητας αναπαραγωγής. - Εμφάνιση της ποιότητας βίντεο. - Τύπος πληροφορίας που θα εμφανίζεται - Η λειτουργία περιβάλλοντος απενεργοποιείται σε λειτουργία εξοικονόμησης μπαταρίας. - Η λειτουργία περιβάλλοντος παραμένει ενεργή σε λειτουργία εξοικονόμησης μπαταρίας. - Παράκαμψη περιορισμών λειτουργίας περιβάλλοντος - Το domain από το οποίο θα ληφθούν οι εικόνες.\nΣημείωση: Εισάγετε μόνο το όνομα του domain, χωρίς το πρόθεμα \"https\:\/\/\". - Εναλλακτικό domain - Χρησιμοποιείται το αρχικό domain για την φόρτωση εικόνων\n\nΗ ενεργοποίηση αυτής της ρύθμισης μπορεί να διορθώσει την φόρτωση εικόνων που είναι μπλοκαρισμένες σε ορισμένες περιοχές. - Χρησιμοποιείται το domain yt4.ggpht.com για την φόρτωση εικόνων. - Παράκαμψη μπλοκαρίσματος φόρτωσης εικόνων - Προεπιλογή - Κινητό - Κινητό (Μέγιστο dp 480) - Τάμπλετ - Τάμπλετ (Μέγιστο dp 600) - Αλλαγή διεπαφής - Εμφανίζονται εναλλαγές διακόπτη. - Εμφανίζονται εναλλαγές κειμένου. - Τύπος εναλλαγής ρυθμίσεων - Χρησιμοποιείται το μενού κοινοποίηση της εφαρμογής. - Χρησιμοποιείται το μενού κοινοποίηση της συστήματός σας. - Αλλαγή μενού κοινοποίησης - Αυτόματη αναπαραγωγή - Προεπιλογή - Παύση - Επανάληψη - Αλλαγή κατάστασης επανάληψης Shorts - Περιήγηση καναλιών - Προεπιλογή - Εξερεύνηση - Παιχνίδια - Ιστορικό - Βιβλιοθήκη - Βίντεο που σας αρέσουν - Live - Ταινίες - Μουσική - Αναζήτηση - Shorts - Αθλητικά - Εγγραφές - Τάσεις - Παρακολούθηση αργότερα - Αλλαγή αρχικής σελίδας - Η αρχική σελίδα αλλάζει μόνο μία φορά. - "Η αρχική σελίδα αλλάζει πάντα. - -Περιορισμός: Το κουμπί επιστροφής στη γραμμή εργαλείων ενδέχεται να μη λειτουργεί." - Αλλαγή τύπου αρχικής σελίδας - Η επικεφαλίδα Premium είναι απενεργοποιημένη. - Η επικεφαλίδα Premium είναι ενεργοποιημένη. - Επικεφαλίδα Premium - Λίστα από συμβολοσειρές στοιχείων που θα φιλτραριστούν, διαχωρισμένες με νέες γραμμές. - Επεξεργασία προσαρμοσμένου φίλτρου - Το προσαρμοσμένο φίλτρο χρήστη είναι απενεργοποιημένο. - Το προσαρμοσμένο φίλτρο χρήστη είναι ενεργοποιημένο. - Προσαρμοσμένο φίλτρο - Μη έγκυρο φίλτρο: %s. - Εμφανίζεται το αναδυόμενο μενού παλιού στυλ. - Εμφανίζεται προσαρμοσμένο παράθυρο. - Τύπος μενού ταχύτητας αναπαραγωγής - Οι ταχύτητες πρέπει να είναι μικρότερες από %sx. - Μη έγκυρες ταχύτητες αναπαραγωγής. - Προσθέστε ή αλλάξτε τις διαθέσιμες ταχύτητες αναπαραγωγής. - Επεξεργασία ταχυτήτων αναπαραγωγής - Η αδιαφάνεια πρέπει να είναι μεταξύ 0-100. - Τιμή αδιαφάνειας μεταξύ 0-100, όπου το 0 είναι διαφανές. - Αδιαφάνεια φόντου οθόνης αναπαραγωγής - Εισάγετε τον κωδικό hex του χρώματος της γραμμής προόδου. - Τιμή χρώματος γραμμής προόδου - Για να ανοίγουν οι συνδέσμοι YouTube στο RVX, ενεργοποιήστε το «Άνοιγμα υποστηριζόμενων συνδέσμων» και τις υποστηριζόμενες διευθύνσεις ιστού. - Άνοιγμα ρυθμίσεων προεπιλεγμένων εφαρμογών - Προεπιλεγμένη ταχύτητα αναπαραγωγής - Προεπιλεγμένη ποιότητα βίντεο με δεδομένα κινητής τηλεφωνίας - Προεπιλεγμένη ποιότητα βίντεο με Wi-Fi - Απενεργοποίηση της λειτουργίας περιβάλλοντος σε λειτουργία πλήρους οθόνης. - Η λειτουργία περιβάλλοντος είναι ενεργοποιημένη στη λειτουργία πλήρους οθόνης. - Η λειτουργία περιβάλλοντος είναι απενεργοποιημένη στη λειτουργία πλήρους οθόνης. - Απενεργοποίηση σε πλήρη οθόνη - Απενεργοποίηση της λειτουργίας περιβάλλοντος πάντα. - Η λειτουργία περιβάλλοντος είναι ενεργοποιημένη. - Η λειτουργία περιβάλλοντος είναι απενεργοποιημένη. - Απενεργοποίηση λειτουργίας περιβάλλοντος - Τα υποχρεωτικά κομμάτια ήχου είναι ενεργοποιημένα. - Τα υποχρεωτικά κομμάτια ήχου είναι απενεργοποιημένα. - Απενεργοποίηση υποχρεωτικών κομματιών ήχου - Οι υποχρεωτικοί αυτόματοι υπότιτλοι εμφανίζονται. - Οι υποχρεωτικοί αυτόματοι υπότιτλοι είναι απενεργοποιημένοι. - Απενεργοποίηση υποχρεωτικών αυτόματων υπότιτλων - Εμφανίζονται. - Κρυμμένα. - Αναδυόμενα παράθυρα οθόνης αναπαραγωγής - "Η αυτόματη εναλλαγή λιστών αναπαραγωγής μίξης είναι ενεργοποιημένη όταν η αυτόματη αναπαραγωγή είναι επίσης ενεργοποιημένη. - -Η αυτόματη αναπαραγωγή μπορεί να αλλαχτεί στις ρυθμίσεις YouTube: -Ρυθμίσεις → Αυτόματη αναπαραγωγή → Αυτόματη αναπαραγωγή επόμενου βίντεο" - Η αυτόματη εναλλαγή λιστών αναπαραγωγής μίξης είναι απενεργοποιημένη. - Απενεργοποίηση εναλλαγής λιστών αναπαραγωγής μίξης - Η ενεργοποίηση αυτής της ρύθμισης θα απενεργοποιήσει την αυτόματη εναλλαγή σε YouTube Mix κατά την αναπαραγωγή μουσικής ενώ η αυτόματη αναπαραγωγή είναι ενεργοποιημένη. - Η προεπιλεγμένη ταχύτητα αναπαραγωγής εφαρμόζεται σε ζωντανές μεταδόσεις. - Η προεπιλεγμένη ταχύτητα αναπαραγωγής δεν εφαρμόζεται σε ζωντανές μεταδόσεις. - Απενεργοποίηση αλλαγής ταχύτητας σε ζωντανές μεταδόσεις - Η προεπιλεγμένη ταχύτητα εφαρμόζεται σε βίντεο μουσικής. - "Η προεπιλεγμένη ταχύτητα αναπαραγωγής δεν εφαρμόζεται για τα βίντεο μουσικής. - -Περιορισμός: Αυτή η ρύθμιση ενδέχεται να μην ισχύει για τα βίντεο που δεν περιλαμβάνουν την λειτουργία «Ακρόαση με YouTube Music»." - Απενεργοποίηση αλλαγής ταχύτητας σε βίντεο μουσικής - Ο πίνακας αλληλεπίδρασης είναι ενεργοποιημένος. - Ο πίνακας αλληλεπίδρασης είναι απενεργοποιημένος. - Απενεργοποίηση πίνακα αλληλεπίδρασης - Η απόκριση δόνησης είναι ενεργοποιημένη. - Η απόκριση δόνησης είναι απενεργοποιημένη. - Απενεργοποίηση απόκρισης δόνησης κατά την αλλαγή κεφαλαίων - Η απόκριση δόνησης είναι ενεργοποιημένη. - Η απόκριση δόνησης είναι απενεργοποιημένη. - Απενεργοποίηση απόκρισης δόνησης κατά τη λειτουργία ακριβής αναζήτησης - Η απόκριση δόνησης είναι ενεργοποιημένη. - Η απόκριση δόνησης είναι απενεργοποιημένη. - Απενεργοποίηση απόκρισης δόνησης στο σύρσιμο για αναζήτηση - Η απόκριση δόνησης είναι ενεργοποιημένη. - Η απόκριση δόνησης είναι απενεργοποιημένη. - Απενεργοποίηση απόκρισης δόνησης του «Αφήστε για ακύρωση» - Η απόκριση δόνησης είναι ενεργοποιημένη. - Η απόκριση δόνησης είναι απενεργοποιημένη. - Απενεργοποίηση απόκρισης δόνησης κατά την χειρονομία ζουμ - Η αυτόματη φωτεινότητα HDR είναι ενεργοποιημένη. - Η αυτόματη φωτεινότητα HDR είναι απενεργοποιημένη. - Απενεργοποίηση αυτόματης φωτεινότητας HDR - Τα βίντεο που υποστηρίζουν HDR θα παίζουν σε HDR ποιότητα. - Τα βίντεο που υποστηρίζουν HDR δεν θα παίζουν σε HDR ποιότητα. - Απενεργοποίηση βίντεο HDR - Η οριζόντια λειτουργία σε λειτουργία πλήρους οθόνης είναι ενεργοποιημένη. - Η οριζόντια λειτουργία σε λειτουργία πλήρους οθόνης είναι απενεργοποιημένη. - Απενεργοποίηση οριζόντιας λειτουργίας - Τα κουμπιά «Μου αρέσει» και «Δεν μου αρέσει» θα λάμπουν όταν αναφέρονται. - Τα κουμπιά «Μου αρέσει» και «Δεν μου αρέσει» δεν θα λάμπουν όταν αναφέρονται. - Απενεργοποίηση λάμψης κουμπιών «Μου αρέσει» και «Δεν μου αρέσει» - "Απενεργοποίηση πρωτοκόλλου QUIC του CronetEngine. - -Αυτή η λειτουργία αποτρέπει την συμπίεση και αποσυμπίεση των βίντεο κατά την αναπαραγωγή, τα οποία μπορούν να προκαλέσουν κολλήματα, ενδέχεται όμως να χρησιμοποιηθούν περισσότερα δεδομένα." - Απενεργοποίηση πρωτοκόλλου QUIC - Ο αναπαραγωγέας Shorts θα συνεχιστεί κατά την εκκίνηση της εφαρμογής. - Ο αναπαραγωγέας Shorts δεν θα συνεχιστεί κατά την εκκίνηση της εφαρμογής. - Απενεργοποίηση συνέχισης των Shorts - Οι αριθμοί κινούνται αυξανόμενοι εκθετικά. - Οι αριθμοί δεν κινούνται αυξανόμενοι εκθετικά. - Απενεργοποίηση κινήσεων αριθμών - Ο διαχωρισμός της γραμμής προόδου σε κεφάλαια είναι ενεργοποιημένος. - Ο διαχωρισμός της γραμμής προόδου σε κεφάλαια είναι απενεργοποιημένος. - Απενεργοποίηση κεφαλαίων γραμμής προόδου - Το εφέ κίνησης πάνω από το κουμπί «Μου αρέσει» είναι ενεργοποιημένο. - Το εφέ κίνησης πάνω από το κουμπί «Μου αρέσει» είναι απενεργοποιημένο. - Απενεργοποίηση εφέ κουμπιού «Μου αρέσει» - "Απενεργοποίηση του «Παίζοντας με 2x ταχύτητα» κατά το παρατεταμένο πάτημα. - -Σημειώσεις: -• Ενεργοποιώντας αυτή τη ρύθμιση θα επαναφερθεί η λειτουργία «Σύρετε αριστερά η δεξιά για αναζήτηση». -• Σε περίπτωση απενεργοποίησης αυτής της ρύθμισης δεν εξαναγκάζεται η ενεργοποίηση της διεπαφής ταχύτητας." - Απενεργοποίηση διεπαφής ταχύτητας - Το εφέ εκκίνησης της εφαρμογής είναι ενεργοποιημένο. - Το εφέ εκκίνησης της εφαρμογής είναι απενεργοποιημένο. - Απενεργοποίηση εφέ εκκίνησης εφαρμογής - "Απενεργοποίηση των ακόλουθων αλληλεπιδράσεων όταν ανοίγεται η περιγραφή του βίντεο: - -• Πάτημα για κύλιση. -• Παρατεταμένο πάτημα για επιλογή κειμένου." - Απενεργοποίηση αλληλεπίδρασης περιγραφής βίντεο - Ο κωδικοποιητής VP9 είναι ενεργοποιημένος. - "Ο κωδικοποιητής VP9 είναι απενεργοποιημένος. - -• Η μέγιστη ανάλυση είναι 1080p. -• Η αναπαραγωγή βίντεο θα χρησιμοποιεί περισσότερα δεδομένα Internet από τον VP9. -• Για την αναπαραγωγή βίντεο τύπου HDR, ο κωδικοποιητής VP9 εξακολουθεί να χρησιμοποιείται." - Απενεργοποίηση κωδικοποιητή VP9 - Η γραμμή προόδου του θέματος Cairo είναι απενεργοποιημένη. - "Η γραμμή προόδου του θέματος Cairo είναι ενεργοποιημένη. - -Παρενέργεια: Το θέμα Cairo εφαρμόζεται επίσης στις τελείες ειδοποιήσεων." - Γραμμή προόδου θέματος Cairo - Τα στοιχεία ελέγχου μικρότερου στυλ είναι απενεργοποιημένα. - Τα στοιχεία ελέγχου μικρότερου στυλ είναι ενεργοποιημένα. - Στοιχεία ελέγχου μικρότερου στυλ - Η προσαρμοσμένη ταχύτητα αναπαραγωγής είναι απενεργοποιημένη. - Η προσαρμοσμένη ταχύτητα αναπαραγωγής είναι ενεργοποιημένη. - Προσαρμοσμένη ταχύτητα αναπαραγωγής - Το προσαρμοσμένο χρώμα γραμμής προόδου είναι απενεργοποιημένο. - Το προσαρμοσμένο χρώμα γραμμής προόδου είναι ενεργοποιημένο. - Προσαρμοσμένο χρώμα γραμμής προόδου - Η καταγραφή εντοπισμού σφαλμάτων δεν περιλαμβάνει το proto buffer. - Η καταγραφή εντοπισμού σφαλμάτων περιλαμβάνει το proto buffer. - Συμπερίληψη του buffer στην καταγραφή - Η καταγραφή εντοπισμού σφαλμάτων είναι απενεργοποιημένη. - Η καταγραφή εντοπισμού σφαλμάτων είναι ενεργοποιημένη. - Καταγραφή εντοπισμού σφαλμάτων - Η προεπιλεγμένη ταχύτητα αναπαραγωγής δεν εφαρμόζεται στα Shorts. - Η προεπιλεγμένη ταχύτητα αναπαραγωγής εφαρμόζεται στα Shorts. - Αλλαγή προεπιλεγμένης ταχύτητας Shorts - Οι συνδέσμοι ανοίγουν εσωτερικά στην εφαρμογή. - Οι συνδέσμοι ανοίγουν σε εξωτερικό πρόγραμμα περιήγησης. - Εξωτερικό πρόγραμμα περιήγησης - Η οθόνη φόρτωσης θα έχει στατική απόχρωση φόντο. - Η οθόνη φόρτωσης θα έχει σταδιακές αποχρώσεις φόντο. - Διαβαθμισμένη οθόνη φόρτωσης - Το διάστημα μεταξύ των κουμπιών πλοήγησης δεν είναι στενότερο. - Το διάστημα μεταξύ των κουμπιών πλοήγησης είναι στενότερο. - Κουμπιά πλοήγησης στενού στυλ - Οι ανακατευθύνσεις URL δεν παρακάμπτονται κατά το άνοιγμα συνδέσμων. - Οι ανακατευθύνσεις URL παρακάμπτονται κατά το άνοιγμα συνδέσμων. - Παράκαμψη ανακατευθύνσεων συνδέσμων - Ενεργοποίηση του κωδικοποιητή OPUS αν η ανταπόκριση του προγράμματος αναπαραγωγής τον περιλαμβάνει. - Ενεργοποίηση κωδικοποιητή OPUS - Η φωτεινότητα δεν αποθηκεύεται ούτε επαναφέρεται κατά την έξοδο ή την είσοδο σε πλήρη οθόνη. - Η φωτεινότητα αποθηκεύεται και επαναφέρεται κατά την έξοδο ή την είσοδο σε πλήρη οθόνη. - Αποθήκευση και επαναφορά φωτεινότητας - Το πάτημα γραμμής προόδου είναι απενεργοποιημένο. - Το πάτημα γραμμής προόδου είναι ενεργοποιημένο. - Πάτημα γραμμής προόδου - "Αυτό θα επαναφέρει τις μικρογραφίες σε ζωντανές μεταδόσεις που δεν έχουν μικρογραφίες γραμμής προόδου. - -Η χρήση δεδομένων internet ενδέχεται να είναι υψηλότερη, και οι μικρογραφίες της γραμμής προόδου θα έχουν μια μικρή καθυστέρηση πριν εμφανιστούν. - -Αυτή η ρύθμιση λειτουργεί καλύτερα με γρήγορη σύνδεση internet." - Οι μικρογραφίες της γραμμής προόδου είναι μέτριας ποιότητας. - Οι μικρογραφίες της γραμμής προόδου είναι υψηλής ποιότητας. - Μικρογραφίες υψηλής ποιότητας - Οι χρονοσφραγίδες είναι απενεργοποιημένες. - "Οι χρονοσφραγίδες είναι ενεργοποιημένες. - -Περιορισμοί: -• Αυτή η ρύθμιση ενεργοποιεί όχι μόνο τις χρονοσφραγίδες, αλλά επιτρέπει την απόκρυψη των στοιχείων UI πατώντας στο φόντο της οθόνης αναπαραγωγής. -• Δεδομένου ότι αυτή είναι μια λειτουργία της Google που βρίσκεται ακόμη στο στάδιο ανάπτυξης, η διάταξη μπορεί να χαλάσει." - Ενεργοποίηση χρονοσφραγίδων - Η αλλαγή φωτεινότητας με χειρονομία σάρωσης στην αριστερή πλευρά της οθόνης είναι απενεργοποιημένη. - Η αλλαγή φωτεινότητας με χειρονομία σάρωσης στην αριστερή πλευρά της οθόνης είναι ενεργοποιημένη. - Έλεγχος σάρωσης για Φωτεινότητα - Η απόκριση δόνησης είναι απενεργοποιημένη. - Η απόκριση δόνησης είναι ενεργοποιημένη. - Απόκριση δόνησης - Η χαμηλότερη τιμή της χειρονομίας φωτεινότητας δεν ενεργοποιεί την αυτόματη φωτεινότητα. - Η χαμηλότερη τιμή της χειρονομίας φωτεινότητας ενεργοποιεί την αυτόματη φωτεινότητα. - Αυτόματη φωτεινότητα με σάρωση - Το παρατεταμένο πάτημα για ενεργοποίηση της χειρονομίας σάρωσης είναι απενεργοποιημένο. - Το παρατεταμένο πάτημα για ενεργοποίηση της χειρονομίας σάρωσης είναι ενεργοποιημένο. - Παρατεταμένο πάτημα για σάρωση - Η χειρονομία εναλλαγής βίντεο στη λειτουργία πλήρους οθόνης είναι απενεργοποιημένη. - Η χειρονομία εναλλαγής βίντεο στη λειτουργία πλήρους οθόνης είναι ενεργοποιημένη. - -Σύρετε προς τα πάνω / κάτω για αναπαραγωγή του επόμενου / προηγούμενου βίντεο. - Χειρονομία εναλλαγής βίντεο στην πλήρη οθόνη - Η αλλαγή έντασης ήχου με χειρονομία σάρωσης στη δεξιά πλευρά της οθόνης είναι απενεργοποιημένη. - Η αλλαγή έντασης ήχου με χειρονομία σάρωσης στη δεξιά πλευρά της οθόνης είναι ενεργοποιημένη. - Έλεγχος σάρωσης για Ένταση Ήχου - Η γραμμή πλοήγησης είναι αδιαφανής. - Η γραμμή πλοήγησης είναι ημιδιαφανής. - Ημιδιαφανή γραμμή πλοήγησης - Η εναλλαγή σε λειτουργία πλήρους οθόνης σύροντας την περιοχή κάτω από την οθόνη αναπαραγωγής είναι απενεργοποιημένη. - Η εναλλαγή σε λειτουργία πλήρους οθόνης σύροντας την περιοχή κάτω από την οθόνη αναπαραγωγής είναι ενεργοποιημένη. - Χειρονομία εναλλαγής σε πλήρη οθόνη - "Η ενεργοποίηση αυτής της ρύθμισης θα απενεργοποιήσει το κουμπί ρυθμίσεων στην καρτέλα «Εσείς» - -Σε αυτή την περίπτωση, για πρόσβαση στις ρυθμίσεις χρησιμοποιήστε το εξής μονοπάτι: -Καρτέλα «Εσείς» → Προβολή καναλιού → Μενού → Ρυθμίσεις." - Ευρεία γραμμή αναζήτησης στο «Εσείς» - Η ευρεία γραμμή αναζήτησης είναι απενεργοποιημένη. - Η ευρεία γραμμή αναζήτησης είναι ενεργοποιημένη. - Ευρεία γραμμή αναζήτησης - Η ευρεία γραμμή αναζήτησης δεν περιλαμβάνει την επικεφαλίδα του YouTube. - Η ευρεία γραμμή αναζήτησης περιλαμβάνει την επικεφαλίδα του YouTube. - Συμπερίληψη της επικεφαλίδας - Περιγραφή - "Εισάγετε τον τίτλο του πίνακα περιγραφής βίντεο στη γλώσσα σας. -Η λειτουργία «Αυτόματο άνοιγμα περιγραφής βίντεο» ενδέχεται να μη λειτουργήσει αν η εισαγόμενη συμβολοσειρά σας δεν ταιριάζει με τον τίτλο." - Τίτλος του πίνακα περιγραφής βίντεο - Η περιγραφή βίντεο ανοίγεται χειροκίνητα. - Η περιγραφή βίντεο ανοίγεται αυτόματα. - Αυτόματο άνοιγμα περιγραφής βίντεο - Θέλετε να συνεχίσετε; - Έγινε επαναφορά στις προεπιλεγμένες τιμές. - Επανεκκίνηση ώστε να φορτωθεί σωστά η διάταξη - "Υπάρχει ένα σφάλμα από πλευράς διακομιστή του YouTube το οποίο προκαλεί να μην εμφανίζονται κάποιοι αριθμοί όπως τα like, οι προβολές, και οι ημερομηνίες μεταμόρφωσης για κάποιους χρήστες. - -Μια προσωρινή λύση για αυτό το θέμα είναι να γίνει παραποίηση της έκδοσης εφαρμογής σε 19.13.37. - -Θέλετε να γίνει παραποίηση της έκδοσης εφαρμογής πριν γίνει επανεκκίνηση εφαρμογής;" - Ανανέωση και επανεκκίνηση - Αποτυχία εξαγωγής ρυθμίσεων. - Οι ρυθμίσεις εξήχθησαν με επιτυχία. - Εξαγωγή ρυθμίσεων σε αρχείο. - Εξαγωγή ρυθμίσεων - Εισαγωγή - Αντιγραφή - Εισαγωγή ή εξαγωγή των ρυθμίσεων ως κείμενο. - Εισαγωγή / Εξαγωγή ως κείμενο - Η εισαγωγή ρυθμίσεων απέτυχε. - Οι ρυθμίσεις επαναφέρθηκαν στις προεπιλογές. - Οι ρυθμίσεις εισήχθησαν με επιτυχία. - Εισαγωγή ρυθμίσεων από αποθηκευμένο αρχείο. - Εισαγωγή ρυθμίσεων - Επαναφορά - Αναζήτηση %s - ReVanced Extended - Εξωτερικό πρόγραμμα λήψης - Δεν έχει εγκατασταθεί - "Το %1$s δεν είναι εγκατεστημένο. -Παρακαλούμε εγκαταστήστε το %2$s από την ιστοσελίδα." - Προειδοποίηση - %s δεν έχει εγκατασταθεί. Παρακαλούμε εγκαταστήστε το. - Όνομα πακέτου της εγκατεστημένης σας εξωτερικής εφαρμογής λήψης (π.χ YTLDnis). - Όνομα πακέτου προγράμματος λήψης λίστας αναπαραγωγής - Όνομα πακέτου της εγκατεστημένης σας εξωτερικής εφαρμογής λήψης (π.χ NewPipe, YTLDnis). - Όνομα πακέτου προγράμματος λήψης βίντεο - "Τα βίντεο θα αλλάξουν σε λειτουργία πλήρους οθόνης στις ακόλουθες περιπτώσεις: - -• Όταν ξεκινάει ένα βίντεο. -• Όταν πατιέται μια χρονοσήμανση στα σχόλια." - Εξαναγκασμός πλήρης οθόνης πάντα - Λίστα ονομάτων των επιλογών του μενού λογαριασμού για φιλτράρισμα, διαχωρισμένα με νέες γραμμές. - Επεξεργασία φίλτρου μενού λογαριασμού - "Απόκρυψη στοιχείων του μενού λογαριασμού και της καρτέλας «Εσείς». -Κάποια στοιχεία ενδέχεται να μην κρύβονται." - Φιλτράρισμα μενού λογαριασμού - Εμφανίζεται. - Κρυμμένη. - Σύνοψη βίντεο παραγμένου με τεχνητή νοημοσύνη - Εμφανίζονται. - Κρυμμένες. - Κάρτες άλμπουμ - Εμφανίζεται. - -Αυτή η ενότητα αφορά τα «Προτεινόμενα μέρη», Παιχνίδια και Μουσικές ενότητες. - Κρυμμένη. - -Αυτή η ενότητα αφορά τα «Προτεινόμενα μέρη», Παιχνίδια και Μουσικές ενότητες. - Ενότητα χαρακτηριστικών - Εμφανίζεται. - Κρυμμένη. - Προεπισκόπηση αυτόματης αναπαραγωγής επόμενου βίντεο - Εμφανίζεται. - Κρυμμένο. - Κουμπί περιήγησης στο κατάστημα - "Απόκρυψη ενοτήτων όπως: -• Έκτακτη είδηση -• Συνέχεια παρακολούθησης -• Εξερευνήστε περισσότερα κανάλια -• Ακούστε ξανά -• Αγορές -• Παρακολουθήστε ξανά" - Οριζόντιες ενότητες προτάσεων - Εμφανίζεται. - Κρυμμένη. - Απόκρυψη στη ροή - Εμφανίζεται. - Κρυμμένη. - Απόκρυψη στα σχετικά βίντεο - Εμφανίζεται. - Κρυμμένη. - Απόκρυψη στα αποτελέσματα αναζήτησης - Εμφανίζονται. - Κρυμμένες. - Οδηγίες κοινότητας - Εμφανίζεται. - Κρυμμένη. - Ενότητα συνδρομητή καναλιού - Εμφανίζονται. - Κρυμμένοι. - Σύνδεσμοι κορυφής σελίδας καναλιού - "Shorts -Playlists -Κοινότητα" - Λίστα ονομάτων καρτελών σελίδας καναλιού για φιλτράρισμα, διαχωρισμένα με νέες γραμμές. - Επεξεργασία φίλτρου καρτελών καναλιού - Το φίλτρο καρτελών καναλιού είναι απενεργοποιημένο. - Το φίλτρο καρτελών καναλιού είναι ενεργοποιημένο. - Φιλτράρισμα καρτελών καναλιού - Εμφανίζεται. - Κρυμμένο. - Υδατογράφημα καναλιού - Εμφανίζεται. - Κρυμμένη. - Ενότητα κεφαλαίων βίντεο - Εμφανίζεται. - Κρυμμένη. - Ενότητα σχετιζόμενων λέξεων - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Κλιπ» - Εμφανίζεται. - Κρυμμένο. - Κουμπί δημιουργίας Shorts - Εμφανίζονται. - Κρυμμένοι. - Επισημασμένοι συνδέσμοι αναζήτησης - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Σας ευχαριστούμε» - Εμφανίζονται. - Κρυμμένα. - Κουμπιά χρονοσήμανσης και emoji - Εμφανίζεται. - Κρυμμένη. - Ετικέτα «Σχόλια από μέλη» - Εμφανίζεται. - Κρυμμένη. - Ενότητα σχολίων στην αρχική ροή - Εμφανίζεται. - Κρυμμένη. - Ενότητα σχολίων - Εμφανίζονται. - Κρυμμένες. - Απόκρυψη στη σελίδα καναλιού - Εμφανίζονται. - Κρυμμένες. - Απόκρυψη στη ροή και στα σχετικά βίντεο - Εμφανίζονται. - Κρυμμένες. - Απόκρυψη στη ροή «Εγγραφές» - Εμφανίζεται. - -Αφορά την ενότητα «Πως δημιουργήθηκε αυτό το περιεχόμενο». - Κρυμμένη. - -Αφορά την ενότητα «Πως δημιουργήθηκε αυτό το περιεχόμενο». - Ενότητα περιεχομένου - Εμφανίζεται. - Κρυμμένο. - Πλαίσιο δωρεών - Εμφανίζεται. - Κρυμμένο. - Μαύρο φόντο κατά το διπλό πάτημα - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Λήψη» - Εμφανίζονται. - Κρυμμένες. - Κάρτες τελικής οθόνης - Εμφανίζονται. - Κρυμμένα. - Επεκτάσιμα πλαίσια κάτω από τα βίντεο - Εμφανίζονται. - Κρυμμένες. - Επεκτάσιμες ενότητες - Εμφανίζεται. - Κρυμμένο. - Κουμπί υπότιτλων - Λίστα ονομάτων των επιλογών του αναδυόμενου μενού για φιλτράρισμα, διαχωρισμένα με νέες γραμμές. - Επεξεργασία φίλτρου αναδυόμενων μενού ροής - Το φιλτράρισμα του αναδυόμενου μενού στη ροή είναι απενεργοποιημένο. - Το φιλτράρισμα του αναδυόμενου μενού στη ροή είναι ενεργοποιημένο. - Φιλτράρισμα του αναδυόμενου μενού στη ροή - Εμφανίζεται. - Κρυμμένη. - Γραμμή αναζήτησης - Εμφανίζονται. - Κρυμμένες. - Έρευνες - Η χειρονομία αναζήτησης καρέ-καρέ είναι ενεργοποιημένη. - Η χειρονομία αναζήτησης καρέ-καρέ είναι απενεργοποιημένη. - Απενεργοποίηση ακριβής αναζήτησης - Εμφανίζεται. - Κρυμμένο. - Αιωρούμενο κουμπί - Εμφανίζεται. - Κρυμμένο. - Αιωρούμενο κουμπί μικροφώνου - Εμφανίζεται. - Κρυμμένη. - Ενότητα «Για εσάς» - Εμφανίζονται. - Κρυμμένες. - Διαφημίσεις πλήρους οθόνης - "Οι διαφημίσεις πλήρους οθόνης έχουν αποκλειστεί. - -Περιορισμός: Οι εικόνες δημοσιεύσεων κοινότητας ενδέχεται να μην ανοίγουν σε λειτουργία πλήρους οθόνης." - Οι διαφημίσεις πλήρους οθόνης κλείνουν μέσω του κουμπιού κλεισίματος. - Κλείσιμο διαφημίσεων πλήρους οθόνης - Εμφανίζονται. - Κρυμμένες. - Γενικές διαφημίσεις - Εμφανίζονται. - Κρυμμένες. - Προωθήσεις YouTube Premium - Εμφανίζονται. - Κρυμμένα. - Γκρι διαχωριστικά - Εμφανίζονται. - Κρυμμένα. - Ψευδώνυμο & διεύθυνση e-mail - Εμφανίζεται. - Κρυμμένο. - Κουμπί αναζήτησης εικόνας - Εμφανίζεται. - Κρυμμένη. - Ενότητα εικόνων - Εμφανίζεται. - Κρυμμένη. - Ενότητα καρτών πληροφοριών - Εμφανίζονται. - Κρυμμένες. - Κάρτες πληροφοριών - Εμφανίζονται. - Κρυμμένα. - Πάνελ πληροφοριών - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Συμμετοχή» - Εμφανίζεται. - Κρυμμένη. - Ενότητα σχετιζόμενων εννοιών - "Οι καρτέλες «Αρχική», «Εγγραφές» και τα αποτελέσματα αναζήτησης φιλτράρονται για απόκρυψη περιεχομένου που ταιριάζει με τις λέξεις-κλειδιά. - -Περιορισμοί: -• Τα Shorts δεν γίνεται να κρύβονται με βάση το όνομα καναλιού. -• Κάποια στοιχεία UI ενδέχεται να μην κρύβονται. -• Η αναζήτηση για μια λέξη-κλειδί ενδέχεται να μην εμφανίζει κανένα αποτέλεσμα." - Σχετικά με το φιλτράρισμα λέξεων-κλειδιών - Περιβάλλοντας μια λέξη-κλειδί / φράση με διπλά εισαγωγικά θα αποτρέψει μερικές αντιστοιχίες των τίτλων βίντεο και των ονομάτων καναλιών<br><br>Για παράδειγμα,<br><b>\"ai\"</b> θα κρύψει το βίντεο: <b>How does AI work?</b><br>αλλά δεν θα κρύψει: <b>What does fair use mean?</b> - Ταίριασμα ολόκληρων λέξεων - Τα σχόλια δε φιλτράρονται από λέξεις-κλειδιά. - Τα σχόλια φιλτράρονται με τη χρήση λέξεων-κλειδιών. - Φιλτράρισμα σχολίων - Τα βίντεο στην καρτέλα «Αρχική» δε φιλτράρονται από λέξεις-κλειδιά. - Τα βίντεο στην καρτέλα «Αρχική» φιλτράρονται με τη χρήση λέξεων-κλειδιών. - Φιλτράρισμα καρτέλας «Αρχική» - "Λέξεις-κλειδιά και φράσεις προς απόκρυψη, διαχωρισμένες με νέες γραμμές. -Οι λέξεις-κλειδιά μπορεί να είναι ονόματα καναλιών ή κείμενο που εμφανίζεται σε τίτλους των βίντεο. -Οι λέξεις με κεφαλαία γράμματα στη μέση πρέπει να είναι ευαίσθητες στην πεζότητα (π.χ: iPhone, TikTok, LeBlanc)." - Λέξεις-κλειδιά για απόκρυψη - Τα αποτελέσματα αναζήτησης δε φιλτράρονται από λέξεις-κλειδιά. - Τα αποτελέσματα αναζήτησης φιλτράρονται με τη χρήση λέξεων-κλειδιών. - Φιλτράρισμα αποτελεσμάτων αναζήτησης - Τα βίντεο στην καρτέλα «Εγγραφές» δε φιλτράρονται από λέξεις-κλειδιά. - Τα βίντεο στην καρτέλα «Εγγραφές» φιλτράρονται με τη χρήση λέξεων-κλειδιών. - Φιλτράρισμα καρτέλας «Εγγραφές» - Η λέξη θα κρύβει όλα τα βίντεο: %s. - Αδυναμία χρήσης λέξης: %s. - Προσθέστε εισαγωγικά για χρήση της λέξης: %s. - Η λέξη έχει αντικρουόμενες δηλώσεις: %s. - Η λέξη είναι πολύ μικρή και απαιτεί εισαγωγικά: %s. - Εμφανίζονται. - Κρυμμένες. - Τελευταίες αναρτήσεις - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Τελευταία βίντεο» - Εμφανίζονται. - Κρυμμένα. - Κουμπιά «Μου αρέσει» & «Δεν μου αρέσει» - Εμφανίζονται.\n\nΑυτή η ρύθμιση ισχύει και για τις ζωντανές μεταδόσεις Shorts. - Κρυμμένα.\n\nΑυτή η ρύθμιση ισχύει και για τις ζωντανές μεταδόσεις Shorts. - Μηνύματα ζωντανής συνομιλίας - Εμφανίζεται στη λειτουργία πλήρους οθόνης μετά το κλείσιμο της ζωντανής συνομιλίας. - Κρυμμένο. - Κουμπί επανάληψης ζωντανής συζήτησης - Απόκρυψη των βίντεο με λιγότερες από 1,000 προβολές από τη ροή τα οποία ανήκουν σε κανάλια που δεν είστε συνδρομητές. - Απόκρυψη βίντεο χαμηλών προβολών - Εμφανίζονται. - Κρυμμένα. - Πάνελ ιατρικών πληροφοριών - Εμφανίζεται. - Κρυμμένη. - Ενότητα εμπορευμάτων - Εμφανίζονται. - Κρυμμένες. - Λίστες αναπαραγωγής μίξης - Εμφανίζεται. - Κρυμμένη. - Ενότητα ταινιών - Εμφανίζεται. - Κρυμμένη. - Γραμμή πλοήγησης - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Δημιουργία» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Αρχική» - Εμφανίζονται. - Κρυμμένες. - Ονομασίες κουμπιών - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Βιβλιοθήκη» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Ειδοποιήσεις» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Shorts» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Εγγραφές» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Να λαμβάνω ειδοποιήσεις» - Εμφανίζονται. - Κρυμμένες. - Ετικέτες προώθησης επί πληρωμή - Εμφανίζονται. - Κρυμμένα. - Παιχνίδια YouTube - Εμφανίζεται. - Κρυμμένο. - Κουμπί αυτόματης αναπαραγωγής - Εμφανίζεται. - Κρυμμένο. - Κουμπί υπότιτλων - Εμφανίζεται. - Κρυμμένο. - Κουμπί μετάδοσης - Εμφανίζεται. - Κρυμμένο. - Κουμπί ελαχιστοποίησης - Εμφανίζεται. - Κρυμμένο. - Μενού «Λειτουργία περιβάλλοντος» - Εμφανίζεται. - Κρυμμένο. - Μενού «Κομμάτι ήχου» - Εμφανίζονται. - Κρυμμένες. - Οδηγίες του μενού «Υπότιτλοι» - Εμφανίζεται. - Κρυμμένο. - Μενού «Υπότιτλοι» - Εμφανίζεται. - Κρυμμένο. - Μενού ποιότητας 1080p Premium - Εμφανίζεται. - Κρυμμένο. - Μενού «Βοήθεια & σχόλια» - Εμφανίζεται. - Κρυμμένο. - Μενού «Ακρόαση με YouTube Music» - Εμφανίζεται. - Κρυμμένο. - Μενού «Οθόνη κλειδώματος» - Εμφανίζεται. - Κρυμμένο. - Μενού «Επανάληψη βίντεο» - Εμφανίζεται. - Κρυμμένο. - Μενού «Περισσότερα» - Εμφανίζεται. - Κρυμμένο. - Μενού «Picture-in-picture» - Εμφανίζεται. - Κρυμμένο. - Μενού «Ταχύτητα αναπαραγωγής» - Εμφανίζεται. - Κρυμμένο. - Μενού «Έλεγχοι Premium» - Εμφανίζονται. - Κρυμμένες. - Οδηγίες του μενού «Ποιότητα» - Εμφανίζεται. - Κρυμμένη. - Επικεφαλίδα του μενού «Ποιότητα» - Εμφανίζεται. - Κρυμμένο. - Μενού «Αναφορά» - Εμφανίζεται. - Κρυμμένο. - Μενού «Χρονόμετρο ύπνου» - Εμφανίζεται. - Κρυμμένο. - Μενού «Σταθερή ένταση» - Εμφανίζεται. - Κρυμμένο. - Μενού «Στατιστικά για σπασίκλες» - Εμφανίζεται. - Κρυμμένο. - Μενού «Προβολή σε VR» - Εμφανίζεται. - Κρυμμένο. - Κουμπί λειτουργίας πλήρους οθόνης - Εμφανίζονται. - Κρυμμένα. - Κουμπιά προηγούμενου & επόμενου βίντεο - Εμφανίζεται. - Κρυμμένη. - Ενότητα αγορών οθόνης αναπαραγωγής - Εμφανίζεται. - Κρυμμένο. - Κουμπί YouTube Music - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Αποθήκευση» - Εμφανίζεται. - Κρυμμένη. - Ενότητα εκπομπής - Εμφανίζεται. - Κρυμμένη. - Προεπισκόπηση σχολίου - Αυτό αλλάζει το μέγεθος της ενότητας σχολίων, οπότε είναι αδύνατο να ανοιχτεί η επανάληψη ζωντανής συνομιλίας στην ενότητα σχολίων. - Αυτό δεν αλλάζει το μέγεθος της ενότητας σχολίων, οπότε μπορεί να ανοιχτεί η επανάληψη ζωντανής συνομιλίας στην ενότητα σχολίων. - Τύπος απόκρυψης προεπισκόπησης σχολίου - Εμφανίζονται. - Κρυμμένες. - Ετικέτες προειδοποίησης προώθησης - Εμφανίζεται. - Κρυμμένο. - Κουμπί σχολίων - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Δεν μου αρέσει» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Μου αρέσει» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Ζωντανή συζήτηση» - Εμφανίζεται. - Κρυμμένο. - Κουμπί περισσότερων ενεργειών - Εμφανίζεται. - Κρυμμένο. - Κουμπί ανοίγματος λίστας αναπαραγωγής μίξης - Εμφανίζεται. - Κρυμμένο. - Κουμπί ανοίγματος playlist - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Αποθήκευση» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Κοινοποίηση» - Εμφανίζονται. - Κρυμμένες. - Γρήγορες ενέργειες - "Απόκρυψη των παρακάτω προτεινόμενων βίντεο: - -• Βίντεο με ετικέτα «Μόνο για Μέλη». -• Βίντεο με φράσεις όπως «Άλλοι χρήστες παρακολούθησαν επίσης» στο κάτω μέρος τους." - Απόκρυψη προτεινόμενων βίντεο - Εμφανίζεται. - -Αφορά την ενότητα περισσότερων βίντεο στις γρήγορες ενέργειες και το σχετιζόμενο βίντεο. - Κρυμμένο. - -Αφορά την ενότητα περισσότερων βίντεο στις γρήγορες ενέργειες και το σχετιζόμενο βίντεο. - Σχετιζόμενο βίντεο - Εμφανίζονται. - Κρυμμένα. - Σχετιζόμενα βίντεο - "Αυτή η ρύθμιση περιορίζει τον μέγιστο αριθμό διατάξεων που μπορούν να εμφανιστούν στην οθόνη αναπαραγωγής. - -Στην περίπτωση που η διάταξη της οθόνης αναπαραγωγής αλλάξει λόγω αλλαγών από πλευράς του διακομιστή, ορισμένες διατάξεις της ενδέχεται να κρυφτούν χωρίς να είναι επιθυμητό." - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Remix» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Αναφορά» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Επιβραβεύσεις» - Εμφανίζονται. - Κρυμμένες. - Μικρογραφίες όρων αναζήτησης - Εμφανίζεται. - Κρυμμένη. - Οδηγία συρσίματος γραμμής προόδου - Εμφανίζεται. - Κρυμμένη. - Οδηγία «Αφήστε για ακύρωση» - Εμφανίζονται. - Κρυμμένες. - Τίτλοι κεφαλαίων δίπλα στη χρονοσφραγίδα - Εμφανίζεται. - Κρυμμένη. - Εμφανίζεται. - Κρυμμένη. - Γραμμή προόδου στις μικρογραφίες βίντεο - Γραμμή προόδου οθόνης αναπαραγωγής - Εμφανίζονται. - Κρυμμένες. - Κάρτες αυτοχρηματοδότησης - Εμφανίζεται. - Κρυμμένο. - Μενού «Σχετικά με» - Εμφανίζεται. - Κρυμμένο. - Μενού «Προσβασιμότητα» - Εμφανίζεται. - Κρυμμένο. - Μενού «Λογαριασμός» - Εμφανίζεται. - Κρυμμένο. - Μενού «Αυτόματη αναπαραγωγή» - Εμφανίζονται. - Κρυμμένο. - Μενού «Χρέωση και πληρωμές» - Εμφανίζεται. - Κρυμμένο. - Μενού «Υπότιτλοι» - Εμφανίζεται. - Κρυμμένο. - Μενού «Συνδεδεμένες εφαρμογές» - Εμφανίζεται. - Κρυμμένο. - Μενού «Εξοικονόμηση δεδομένων» - Εμφανίζεται. - Κρυμμένο. - Μενού «Γενικά» - Εμφανίζεται. - Κρυμμένο. - Μενού «Διαχείριση όλου του ιστορικού» - Εμφανίζεται. - Κρυμμένο. - Μενού «Ζωντανή συζήτηση» - Εμφανίζεται. - Κρυμμένο. - Μενού «Ειδοποιήσεις» - Εμφανίζεται. - Κρυμμένο. - Μενού «Παρασκήνιο» - Εμφανίζεται. - Κρυμμένο. - Μενού «Παρακολουθήστε σε TV» - Εμφανίζεται. - Κρυμμένο. - Μενού «Κέντρο οικογένειας» - Εμφανίζεται. - Κρυμμένο. - Μενού «Δοκιμάστε νέες πειραματικές λειτουργίες» - Εμφανίζεται. - Κρυμμένο. - Μενού «Απόρρητο» - Εμφανίζεται. - Κρυμμένο. - Μενού «Αγορές και συνδρομές» - Απόκρυψη στοιχείων στο μενού ρυθμίσεων του YouTube. - Φιλτράρισμα του μενού ρυθμίσεων YouTube - Εμφανίζεται. - Κρυμμένο. - Μενού «Προτιμήσεις ποιότητας βίντεο» - Εμφανίζεται. - Κρυμμένο. - Μενού «Τα δεδομένα σας στο YouTube» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Κοινοποίηση» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Κατάστημα» - Εμφανίζονται. - Κρυμμένοι. - Σύνδεσμοι αγορών - Εμφανίζεται. - Κρυμμένη. - Γραμμή καναλιού - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Σχόλια» - Εμφανίζεται. - -Αφορά το κουμπί σχολίων όταν αυτά είναι απενεργοποιημένα ή δεν υπάρχουν καθόλου σχόλια για το τρέχον βίντεο (0). - Κρυμμένο. - -Αφορά το κουμπί σχολίων όταν αυτά είναι απενεργοποιημένα ή δεν υπάρχουν καθόλου σχόλια για το τρέχον βίντεο (0). - Κουμπί απενεργοποιημένων σχολίων - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Δεν μου αρέσει» - "Εμφανίζονται. - -Αφορά τα αιωρούμενα κουμπιά όπως το «Χρήση αυτού του ήχου» στην καρτέλα Shorts του καναλιού." - "Κρυμμένα. - -Αφορά τα αιωρούμενα κουμπιά όπως το «Χρήση αυτού του ήχου» στην καρτέλα Shorts του καναλιού." - Αιωρούμενα κουμπιά - Εμφανίζεται. - Κρυμμένη. - Ετικέτα συνδέσμου πλήρους βίντεο - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Green screen» - Εμφανίζονται. - Κρυμμένα. - Πάνελ πληροφοριών - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Συμμετοχή» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Μου αρέσει» - Εμφανίζεται. - Κρυμμένο.\n\nΤο κουμπί επιστροφής στην επικεφαλίδα δεν θα είναι κρυμμένο. - Επικεφαλίδα ζωντανής συνομιλίας - Εμφανίζεται. - Κρυμμένο. - Κουμπί τοποθεσίας - Η γραμμή πλοήγησης εμφανίζεται κατά την αναπαραγωγή Shorts. - Η γραμμή πλοήγησης είναι κρυμμένη κατά την αναπαραγωγή Shorts. - Γραμμή πλοήγησης - Εμφανίζονται. - Κρυμμένες. - Ετικέτες προώθησης επί πληρωμή - Εμφανίζεται. - Κρυμμένο. - Λογότυπο Shorts κατά την παύση - Εμφανίζονται. - Κρυμμένα. - Κουμπιά εμφάνισης κατά την παύση - Εμφανίζεται. - Κρυμμένο. - Φόντο κουμπιών παύσης & αναπαραγωγής - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Remix» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Αποθήκευση μουσικής» - Εμφανίζεται. - Κρυμμένο. - Κουμπί προτάσεων αναζήτησης - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Κοινοποίηση» - Εμφανίζονται. - "Κρυμμένα. - -Πληροφορίες: -• Μόνο οι ενότητες με την επικεφαλίδα Shorts στην καρτέλα «Αρχική» του καναλιού είναι κρυμμένες." - Απόκρυψη στη σελίδα καναλιού - Εμφανίζεται. - Κρυμμένη. - Απόκρυψη στο ιστορικό παρακολούθησης - Εμφανίζεται. - Κρυμμένη. - Απόκρυψη στην καρτέλα «Αρχική» και στα σχετικά βίντεο - Εμφανίζεται. - Κρυμμένη. - Απόκρυψη στα αποτελέσματα αναζήτησης - Εμφανίζεται. - Κρυμμένη. - Απόκρυψη στην καρτέλα «Εγγραφές» - "Απόκρυψη της ενότητας Shorts. - -Παρενέργεια: Οι τίτλοι ενοτήτων στα αποτελέσματα αναζήτησης δεν εμφανίζονται." - Απόκρυψη των Shorts - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Κατάστημα» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Αγορές» - Εμφανίζεται. - Κρυμμένο. - Κουμπί ήχου - Εμφανίζεται. - Κρυμμένη. - Ετικέτα μεταδεδομένων ήχου - Εμφανίζεται. - Κρυμμένα. - Αυτοκόλλητα - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Εγγραφή» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Αγορά Super Thanks» - Εμφανίζονται. - Κρυμμένες. - Ετικέτες προϊόντων - Εμφανίζεται. - Κρυμμένη. - Γραμμή εργαλείων - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Τάσεις» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Χρήση προτύπου» - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Χρήση αυτού του ήχου» - Εμφανίζεται. - Κρυμμένος. - Τίτλος του βίντεο - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Εμφάνιση περισσότερων» - Εμφανίζονται. - Κρυμμένα. - Μηνύματα αλληλεπίδρασης - Εμφανίζεται. - Κρυμμένο. - Κουμπί έναρξης δοκιμής - Εμφανίζεται. - Κρυμμένη. - Ενότητα καναλιών καρτέλας «Εγγραφές» - Εμφανίζονται. - Κρυμμένες. - Προτεινόμενες ενέργειες - "Αυτή η ρύθμιση έχει καταργηθεί. Αντ' αυτού, χρησιμοποιήστε τη ρύθμιση 'Ρυθμίσεις → Αυτόματη αναπαραγωγή → Αυτόματη αναπαραγωγή επόμενου βίντεο'. - -Σημείωση: -• Αν έχετε οποιοδήποτε θέμα με την τελική οθόνη προτεινόμενου βίντεο, προσπαθήστε να επανεκκινήσετε την εφαρμογή." - Εμφανίζεται. - "Κρυμμένη. -Ισχύει μόνο όταν η αυτόματη αναπαραγωγή είναι απενεργοποιημένη. - -Η αυτόματη αναπαραγωγή μπορεί να αλλαχτεί στις ρυθμίσεις YouTube: -'Ρυθμίσεις → Αυτόματη αναπαραγωγή → Αυτόματη αναπαραγωγή επόμενου βίντεο'" - Τελική οθόνη προτεινόμενου βίντεο - Εμφανίζεται. - Κρυμμένο. - Κουμπί «Σας ευχαριστούμε» - Εμφανίζεται. - Κρυμμένη. - Ενότητα εισιτηρίων - Εμφανίζεται. - Κρυμμένη. - Χρονοσφραγίδα βίντεο - Εμφανίζονται. - Κρυμμένες. - Χρονισμένες αντιδράσεις - Εμφανίζεται. - Κρυμμένο. - Κουμπί μετάδοσης - Εμφανίζεται. - Κρυμμένο. - Κουμπί δημιουργίας - Εμφανίζεται. - Κρυμμένο. - Κουμπί ειδοποιήσεων - Εμφανίζεται. - Κρυμμένη. - Ενότητα απομαγνητοφώνησης - Εμφανίζονται. - Κρυμμένες. - Διαφημίσεις βίντεο - "Οι καρτέλες «Αρχική», «Εγγραφές» και τα αποτελέσματα αναζήτησης φιλτράρονται για απόκρυψη βίντεο με προβολές λιγότερες ή περισσότερες από τον καθορισμένο σας αριθμό. - -Περιορισμοί: -• Τα Shorts δε φιλτράρονται. -• Τα βίντεο με 0 προβολές δε φιλτράρονται." - Σχετικά με το φιλτράρισμα με βάση τον αριθμό προβολών - Τα βίντεο στην καρτέλα «Αρχική» δε φιλτράρονται με βάση τον αριθμό προβολών. - Τα βίντεο στην καρτέλα «Αρχική» φιλτράρονται με βάση τον αριθμό προβολών. - Φιλτράρισμα καρτέλας «Αρχική» - Τα αποτελέσματα αναζήτησης δε φιλτράρονται με βάση τον αριθμό προβολών. - Τα αποτελέσματα αναζήτησης φιλτράρονται με βάση τον αριθμό προβολών. - Φιλτράρισμα αποτελεσμάτων αναζήτησης - Τα βίντεο στην καρτέλα «Εγγραφές» δε φιλτράρονται με βάση τον αριθμό προβολών. - Τα βίντεο στην καρτέλα «Εγγραφές» φιλτράρονται με βάση τον αριθμό προβολών. - Φιλτράρισμα καρτέλας «Εγγραφές» - Απόκρυψη των βίντεο με προβολές λιγότερες ή περισσότερες από την προτίμησή σας.\n\nΓνωστό θέμα: Τα βίντεο με 0 προβολές δε φιλτράρονται σωστά. - Απόκρυψη βίντεο βάσει αριθμού προβολών - Τα βίντεο με περισσότερες προβολές από αυτόν τον αριθμό δεν θα εμφανίζονται. - Μέγιστο όριο προβολών - Τα βίντεο με λιγότερες προβολές από αυτόν τον αριθμό δεν θα εμφανίζονται. - Ελάχιστο όριο προβολών - χιλ. -> 1 000\nεκ. -> 1 000 000\nδισ. -> 1 000 000 000\nπροβολές -> views - Γλωσσικό πρότυπο για τον αριθμό προβολών κάτω από κάθε βίντεο. Κάθε κλειδί (λέξη ή γράμμα στη γλώσσα σας) -> τιμή (σημασία του κλειδιού) βρίσκεται σε νέα γραμμή. Τα κλειδιά μπαίνουν πριν από το \"->\". Αν αλλάξετε γλώσσα εφαρμογής ή συστήματος, θα χρειαστεί επαναρύθμιση. Παραδείγματα:\nΕλληνικά: 10 χιλιάδες προβολές = χιλ. -> 1000, προβολές -> views\nΑγγλικά: 10K views = K -> 1000, views -> views - Επεξεργασία κλειδιών - Εμφανίζονται. - Κρυμμένες. - Ετικέτες για προβολή προϊόντων - Εμφανίζεται. - Κρυμμένο. - Κουμπί φωνητικής αναζήτησης - Εμφανίζονται. - Κρυμμένα. - Αποτελέσματα αναζήτησης στο διαδίκτυο - Εμφανίζονται. - Κρυμμένα. - YouTube Doodles - "Τα YouTube Doodles εμφανίζονται για μερικές μέρες κάθε χρόνο. - -Αν ένα YouTube Doodle εμφανίζεται αυτή τη στιγμή στην περιοχή σας και αυτή η ρύθμιση απόκρυψης του είναι ενεργοποιημένη, τότε η γραμμή κατηγοριών κάτω από τη γραμμή αναζήτησης θα είναι κρυμμένη επίσης." - Εμφανίζονται. - Κρυμμένες. - Ειδοποιήσεις αλληλεπιδράσης διεπαφής ζουμ - AFN Blue - AFN Red - Προσαρμοσμένο - Προεπιλογή - MMT - Revancify Blue - Revancify Red - YouTube - Διατήρηση της οριζόντιας λειτουργίας στο κλείσιμο και άνοιγμα της οθόνης ενώ βρίσκεστε σε λειτουργία πλήρους οθόνης. - Ο αριθμός των χιλιοστών δευτερολέπτου που θα εξαναγκάζεται η οριζόντια λειτουργία. - Χρονικό όριο οριζόντιας λειτουργίας - Διατήρηση οριζόντιας λειτουργίας - Προεπιλογή - Η ενέργεια διπλού πατήματος είναι απενεργοποιημένη. - "Η ενέργεια διπλού πατήματος είναι ενεργοποιημένη. - -• Διπλό πάτημα για αλλαγή του ελαχιστοποιημένου βίντεο σε μεγαλύτερο μέγεθος. -• Διπλό πάτημα ξανά για αλλαγή πίσω στο αρχικό μέγεθος." - Διπλό πάτημα για ενέργεια - Η λειτουργία μεταφοράς και απόθεσης της ελαχιστοποιημένης οθόνης είναι απενεργοποιημένη. - Η λειτουργία μεταφοράς και απόθεσης της ελαχιστοποιημένης οθόνης είναι ενεργοποιημένη. - Λειτουργία μεταφοράς και απόθεσης - Εμφανίζονται. - Κρυμμένα.\n(σύρετε την ελαχιστοποιημένη οθόνη αναπαραγωγής για επέκταση ή κλείσιμο του βίντεο) - Κουμπιά επέκτασης και κλεισίματος - Εμφανίζονται. - Κρυμμένα. - Κουμπιά παράλειψης και επιστροφής - Εμφανίζονται. - Κρυμμένα. - Κείμενα οθόνης αναπαραγωγής - Η αδιαφάνεια πρέπει να ναι μεταξύ 0-100. - Τιμή αδιαφάνειας μεταξύ 0-100, όπου το 0 είναι διαφανές. - Αδιαφάνεια φόντου παρασκηνίου - Αρχικός - Τηλεφώνου - Τάμπλετ - Μοντέρνος 1 - Μοντέρνος 2 - Μοντέρνος 3 - Τύπος ελαχιστοποιημένης οθόνης αναπαραγωγής - Προσθήκη κουμπιών στην οθόνη αναπαραγωγής - "Κουμπί συνεχούς επανάληψης του βίντεο. -Πατήστε για ενεργοποίηση ή απενεργοποίηση της λειτουργίας συνεχούς επανάληψης." - Συνεχής επανάληψη βίντεο - "Κουμπί αντιγραφής συνδέσμου URL του βίντεο. -Πατήστε για να αντιγράψετε τον σύνδεσμο. -Πατήστε παρατεταμένα για να αντιγράψετε τον σύνδεσμο με χρονική σήμανση." - "Κουμπί αντιγραφής συνδέσμου URL του βίντεο με χρονική σήμανση. -Πατήστε για να αντιγράψετε τον σύνδεσμο με χρονική σήμανση. -Πατήστε παρατεταμένα για να αντιγράψετε την χρονοσήμανση του βίντεο." - Aντιγραφή συνδέσμου με χρονική σήμανση - Αντιγραφή συνδέσμου βίντεο - Κουμπί εξωτερικής λήψης του βίντεο. -Πατήστε για εκκίνηση του προεπιλεγμένου σας εξωτερικού προγράμματος λήψης. - Εξωτερική λήψη του βίντεο - Κουμπί σίγασης του βίντεο. -Πατήστε για σίγαση έντασης του τρέχοντος βίντεο. -Πατήστε ξανά για κατάργηση της σίγασης. - Σίγαση βίντεο - Πατήστε παρατεταμένα για αλλαγή της κατάστασης κουμπιού. - Η ταχύτητας αναπαραγωγής επαναφέρθηκε: %sx. - "Κουμπί ρύθμισης ταχύτητας αναπαραγωγής. -Πατήστε για να ανοίξετε το παράθυρο αλλαγής ταχύτητας αναπαραγωγής. -Πατήστε παρατεταμένα για επαναφορά ταχύτητας αναπαραγωγής σε 1.0x." - Αλλαγή ταχύτητας αναπαραγωγής - "Κουμπί δημιουργίας χρονικά-διατεταγμένης λίστας αναπαραγωγής. -Πατήστε για να δημιουργήσετε μια λίστα αναπαραγωγής όλων των βίντεο του καναλιού από το παλαιότερο στο νεότερο. -Πατήστε παρατεταμένα για αναίρεση." - Χρονικά-διατεταγμένη λίστα αναπαραγωγής - Κουμπί λίστας επιτρεπόμενων. -Πατήστε για να ανοίξετε το παράθυρο λίστας επιτρεπόμενων. -Πατήστε παρατεταμένα για να ανοίξετε το παράθυρο ρυθμίσεων λίστας επιτρεπόμενων. - Λίστα επιτρεπόμενων - Αν εμφανίζεται, το κουμπί λήψης λίστας αναπαραγωγής ανοίγει το εγγενές πρόγραμμα λήψης του YouTube. - Το κουμπί λήψης λίστας αναπαραγωγής εμφανίζεται πάντα, και σε δημόσιες λίστες αναπαραγωγής ανοίγει το εξωτερικό πρόγραμμα λήψης σας. - Μετατροπή κουμπιού λήψης λίστας αναπαραγωγής - Το κουμπί λήψης του YouTube ανοίγει το εγγενές πρόγραμμα λήψης της εφαρμογής. - Το κουμπί λήψης του YouTube ανοίγει το εξωτερικό πρόγραμμα λήψης σας. - Μετατροπή κουμπιού λήψης βίντεο - Το YouTube Music είναι απαραίτητο για την μετατροπή ενέργειας του κουμπιού. Πατήστε για να κατεβάσετε το YouTube Music. - Προαπαιτούμενο - Το κουμπί YouTube Music ανοίγει την εγγενή εφαρμογή. - Το κουμπί YouTube Music ανοίγει τo RVX Music. - Μετατροπή κουμπιού YouTube Music - Εξαιρέθηκε - Συμπεριλήφθηκε - Κανονική - Κουμπιά ενεργειών - Πρόσθετες ρυθμίσεις - Εφέ / Απόκριση - Κουμπί «Λήψη» - Πειραματικές Λειτουργίες - Περιορισμοί περιοχής εικόνων - Εισαγωγή / Εξαγωγή ως αρχείο - Εισαγωγή / Εξαγωγή ως κείμενο - Φίλτρο λέξεων-κλειδιών - Άλλα - Προσθήκη κουμπιών στην οθόνη αναπαραγωγής - Πληροφορίες τροποποίησης - Γρήγορες ενέργειες - Προτεινόμενα βίντεο - Ενότητα Shorts - Προτεινόμενες ενέργειες - Χρησιμοποιούμενο εργαλείο - Φίλτρο αριθμού προβολών - Απόκρυψη ή εμφάνιση στοιχείων στο μενού λογαριασμού και στην καρτέλα «Εσείς». - Μενού λογαριασμού - Απόκρυψη ή εμφάνιση κουμπιών κάτω από τα βίντεο. - Κουμπιά ενεργειών - Διαφημίσεις - Εναλλακτικές μικρογραφίες - Παράκαμψη περιορισμών λειτουργίας περιβάλλοντος ή απενεργοποίηση της. - Λειτουργία περιβάλλοντος - Απόκρυψη ή εμφάνιση της γραμμής κατηγοριών στη ροή, αποτελέσματα αναζήτησης και στα σχετικά βίντεο. - Γραμμή κατηγοριών - Απόκρυψη ή εμφάνιση στοιχείων γραμμής καναλιών κάτω από τα βίντεο. - Γραμμή καναλιού - Απόκρυψη ή εμφάνιση στοιχείων στην σελίδα καναλιών. - Σελίδα καναλιού - Απόκρυψη ή εμφάνιση στοιχείων στα σχόλια. - Σχόλια - Απόκρυψη ή εμφάνιση δημοσιεύσεων κοινότητας στη ροή και στη σελίδα καναλιού. - Δημοσιεύσεις κοινότητας - Απόκρυψη στοιχείων χρησιμοποιώντας προσαρμοσμένα φίλτρα. - Προσαρμοσμένο φίλτρο - Απόκρυψη ή εμφάνιση στοιχείων του αναδυόμενου μενού στη ροή. - Αναδυόμενο μενού ρυθμίσεων - Ροή - Απόκρυψη ή αλλαγή στοιχείων που σχετίζονται με τη λειτουργία πλήρους οθόνης. - Λειτουργία πλήρους οθόνης - Γενικά - Απενεργοποίηση η ενεργοποίηση της απόκρισης δόνησης. - Απόκριση δόνησης - Μετατροπή ενέργειας πατήματος των κουμπιών της εφαρμογής. - Μετατροπή κουμπιών - Εισαγωγή ή εξαγωγή των ρυθμίσεών σας. - Εισαγωγή / Εξαγωγή ρυθμίσεων - Αλλαγή του στυλ της ελαχιστοποιημένης οθόνης αναπαραγωγής. - Ελαχιστοποιημένη οθόνη αναπαραγωγής - Διάφορα - Απόκρυψη ή εμφάνιση των στοιχείων της γραμμής πλοήγησης. - Γραμμή πλοήγησης - Πληροφορίες σχετικά με τις εφαρμοσμένες τροποποιήσεις. - Πληροφορίες τροποποίησης - Απόκρυψη ή εμφάνιση κουμπιών στην οθόνη αναπαραγωγής βίντεο. - Κουμπιά οθόνης αναπαραγωγής - Απόκρυψη ή αλλαγή στοιχείων του αναδυόμενου μενού της οθόνης αναπαραγωγής βίντεο. - Αναδυόμενο μενού ρυθμίσεων - Οθόνη αναπαραγωγής - Return YouTube Username - Return YouΤube Dislike - SponsorBlock - Προσαρμογή των στοιχείων της γραμμής προόδου. - Γραμμή προόδου βίντεο - Απόκρυψη στοιχείων στο μενού ρυθμίσεων του YouTube. - Μενού ρυθμίσεων - Απόκρυψη ή εμφάνιση στοιχείων στην οθόνη αναπαραγωγής Shorts. - Οθόνη αναπαραγωγής Shorts - Shorts - Παραποίηση των δεδομένων ροής για την αποφυγή προβλημάτων αναπαραγωγής. - Παραποίηση δεδομένων ροής - Έλεγχος με σάρωση οθόνης - Απόκρυψη ή αλλαγή στοιχείων που βρίσκονται στη γραμμή εργαλείων, όπως τα κουμπιά, την γραμμή αναζήτησης, ή την επικεφαλίδα. - Γραμμή εργαλείων - Απόκρυψη ή εμφάνιση στοιχείων της περιγραφής βίντεο. - Περιγραφή βίντεο - Απόκρυψη βίντεο με βάση λέξεις-κλειδιά, αριθμό προβολών ή τη διάρκειά τους. - Φιλτράρισμα των βίντεο - Βίντεο - Διαχείριση των ρυθμίσεων που σχετίζονται με το ιστορικό παρακολούθησης. - Ιστορικό παρακολούθησης - Το ύψος πρέπει να είναι μεταξύ 0-32. - Αλλαγή ύψους της γραμμής προόδου, τιμές μεταξύ 0-32. - Ύψος γραμμής προόδου - "Εξαναγκαστική απόρριψη της απόκρισης του κωδικοποιητή λογισμικού AV1. -Μετά από περίπου 20 δευτερόλεπτα φόρτωσης, θα γίνεται αλλαγή σε διαφορετικό κωδικοποιητή." - Απόρριψη απόκρισης κωδικοποιητή AV1 - Η διαδικασία προκαλεί περίπου 20 δευτερόλεπτα φόρτωσης. - Μετατόπιση - Οι αλλαγές ταχύτητας αναπαραγωγής ισχύουν μόνο για το τρέχον βίντεο. - Οι αλλαγές ταχύτητας αναπαραγωγής ισχύουν για όλα τα βίντεο. - Απομνημόνευση αλλαγών ταχύτητας αναπαραγωγής - Δεν εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης κατά την αλλαγή προεπιλεγμένης ταχύτητας αναπαραγωγής. - Εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης κατά την αλλαγή προεπιλεγμένης ταχύτητας αναπαραγωγής. - Εμφάνιση μηνύματος - Η προεπιλεγμένη ταχύτητα άλλαξε σε %s. - Οι αλλαγές ποιότητας ισχύουν μόνο για το τρέχον βίντεο. - Οι αλλαγές ποιότητας ισχύουν για όλα τα βίντεο. - Απομνημόνευση αλλαγών ποιότητας βίντεο - Δεν εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης κατά την αλλαγή προεπιλεγμένης ποιότητας βίντεο. - Εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης κατά την αλλαγή προεπιλεγμένης ποιότητας βίντεο. - Εμφάνιση μηνύματος - Η προεπιλεγμένη ποιότητα δεδομένων άλλαξε σε %s. - Αποτυχία ορισμού ποιότητας βίντεο. - Η προεπιλεγμένη ποιότητα με Wi-Fi άλλαξε σε %s. - "Αφαίρεση του παραθύρου προειδοποίησης ηλικιακού περιορισμού. -Αυτό δεν παρακάμπτει τον ηλικιακό περιορισμό, απλά τον αποδέχεται αυτόματα." - Παράθυρο ηλικιακού περιορισμού - Αντικατάσταση του κωδικοποιητή λογισμικού AV1 με τον κωδικοποιητή VP9. - Αντικατάσταση κωδικοποιητή AV1 - Εμφανίζεται το ψευδώνυμο καναλιού. - Εμφανίζεται το όνομα καναλιού. - Αντικατάσταση ονόματος καναλιού - Πατήστε για να δείτε τον χρόνο που απομένει. - Πατήστε για να ανοίξετε το μενού ταχύτητας αναπαραγωγής ή ποιότητας βίντεο. - Αντικατάσταση ενέργειας χρονοσφραγίδας - Αντικατάσταση του κουμπιού δημιουργίας με το κουμπί ρυθμίσεων. - Αντικατάσταση κουμπιού δημιουργίας - "Πατήστε για να ανοίξετε τις ρυθμίσεις YouTube. -Πατήστε παρατεταμένα για να ανοίξετε τις ρυθμίσεις RVX." - "Πατήστε για να ανοίξετε τις ρυθμίσεις RVX. -Πατήστε παρατεταμένα για να ανοίξετε τις ρυθμίσεις του YouTube." - Συμπεριφορά κουμπιού ρυθμίσεων - Οι μικρογραφίες προεπισκόπησης εμφανίζονται σε πλήρη οθόνη. - Οι μικρογραφίες προεπισκόπησης εμφανίζονται πάνω από τη γραμμή προόδου. - Παλιές μικρογραφίες γραμμής προόδου - Εμφανίζεται το μενού αλλαγής ποιότητας βίντεο νέου στυλ. - Εμφανίζεται το μενού αλλαγής ποιότητας βίντεο παλιού στυλ. - Μενού ποιότητας βίντεο παλιού στυλ - \@ψευδώνυμο (Όνομα χρήστη) - Μορφή εμφάνισης - Όνομα χρήστη (@ψευδώνυμο) - Όνομα χρήστη - Εμφανίζεται το ψευδώνυμο. - Εμφανίζεται το όνομα χρήστη. - Επαναφορά ονομάτων χρήστη στα σχόλια - "Για να γίνει αντικατάσταση του ψευδωνύμου με όνομα χρήστη, απαιτείται κλειδί προγραμματιστή YouTube Data API v3. - -Η ημερήσια ποσόστωση για τα κλειδιά API στο δωρεάν πακέτο είναι 10,000, και χρησιμοποιείται 1 ποσόστωση για την αντικατάσταση ψευδωνύμου με όνομα χρήστη για 1 σχόλιο. - -Πατήστε για να δείτε πώς να εκδώσετε ένα κλειδί API." - Σχετικά με το κλειδί YouTube Data API - Το κλειδί προγραμματιστή για τη χρήση του YouTube Data API v3. - Κλειδί YouTube Data API - 1. Μεταβείτε στη <a href=%1$s>δημιουργία νέου project</a>.<br>2. Πατήστε το κουμπί <b>CREATE</b>. <br>3. Μεταβείτε στην επιλογή <a href=%2$s>YouTube Data API v3</a>.<br>4. Πατήστε το κουμπί <b>ENABLE</b>.<br>5. Πατήστε το κουμπί <b>CREATE CREDENTIALS</b>.<br>6. Επιλέξτε την επιλογή <b>Public data</b>.<br>7. Πατήστε το κουμπί <b>NEXT</b>.<br>8. Αντιγράψτε το κλειδί API.<br><br>※ Το κλειδί API δεν πρέπει να το μοιράζεστε ποτέ με άλλους, οπότε δεν περιλαμβάνεται κατά την Εισαγωγή / Εξαγωγή ρυθμίσεων. - Έκδοση κλειδιού προγραμματιστή YouTube Data API v3 - Σχετικά με - Τα δεδομένα Dislike παρέχονται από το Return YouTube Dislike API. Πατήστε για να μάθετε περισσότερα. - ReturnYouTubeDislike.com - Το κουμπί «Μου αρέσει» είναι διαμορφωμένο για καλύτερη εμφάνιση. - Το κουμπί «Μου αρέσει» είναι διαμορφωμένο για ελάχιστο μέγεθος. - Κουμπί «Μου αρέσει» μικρότερου στυλ - Τα «Δεν μου αρέσει» εμφανίζονται ως αριθμός. - Τα «Δεν μου αρέσει» εμφανίζονται ως ποσοστό. - Εμφάνιση ως ποσοστό - Τα «Δεν μου αρέσει» δεν εμφανίζονται. - Τα «Δεν μου αρέσει» εμφανίζονται. - Επιστροφή του «Δεν μου αρέσει» στο YouTube - Τα εκτιμώμενα like δεν εμφανίζονται. - Τα εκτιμώμενα like εμφανίζονται. - Εμφάνιση εκτιμώμενων likes - Δεδομένα dislike μη διαθέσιμα (το όριο API έχει επιτευχθεί). - Δεδομένα dislike μη διαθέσιμα (κατάσταση %d). - Δεδομένα dislike προσωρινά μή διαθέσιμα (καθυστέρηση API). - Δεδομένα dislike μη διαθέσιμα (%s). - Επαναφορτώστε το βίντεο για να ψηφίσετε χρησιμοποιώντας το Return YouTube Dislike - Τα dislike δεν εμφανίζονται στα Shorts. - Τα dislike εμφανίζονται στα Shorts. %s - "Τα «Δεν μου αρέσει» εμφανίζονται στα Shorts. - -Περιορισμός: Τα «Δεν μου αρέσει» ενδέχεται να μην εμφανίζονται σε λειτουργία ανώνυμης περιήγησης ή αν δεν έχετε συνδεθεί στον λογαριασμό σας." - Εμφάνιση στα Shorts - Δεν εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης αν το Return YouTube Dislike δεν είναι διαθέσιμο. - Εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης αν το Return YouTube Dislike δεν είναι διαθέσιμο. - Εμφάνιση μηνύματος αν το API δεν είναι διαθέσιμο - Κρυμμένο - Αφαίρεση των παραμέτρων παρακολούθησης από τις διευθύνσεις URL κατά την κοινοποίηση συνδέσμων. - Καθαρισμός συνδέσμων κοινοποίησης - "Φράσεις όπως «#», «Έρανος», «Κατάστημα» και «N προϊόντα» εμφανίζονται στους υπότιτλους των βίντεο." - "Φράσεις όπως «#», «Έρανος», «Κατάστημα» και «προϊόντα» είναι κρυμμένες από τους υπότιτλους των βίντεο." - Καθάρισμα υπότιτλων βίντεο - Σχετικά με - sponsor.ajay.app - Τα δεδομένα παρέχονται από το SponsorBlock API. Πατήστε για να μάθετε περισσότερα και να δείτε λήψεις για άλλες πλατφόρμες. - Η διεύθυνση URL του API άλλαξε. - Η διεύθυνση URL του API δεν είναι έγκυρη. - Η διεύθυνση URL του API επαναφέρθηκε. - Εμφάνιση - Το χρώμα άλλαξε. - Χρώμα: - Μη έγκυρος κωδικός χρώματος. - Το χρώμα επαναφέρθηκε. - Δημιουργία νέων τμημάτων - Αλλαγή συμπεριφοράς τμημάτων - Αυτόματη απόκρυψη κουμπιού παράλειψης - Το κουμπί παράλειψης εμφανίζεται καθ\' όλη τη διάρκεια του τμήματος. - Το κουμπί παράλειψης εξαφανίζεται μετά από μερικά δευτερόλεπτα. - Κουμπί παράλειψης μικρότερου στυλ - Το κουμπί παράλειψης είναι διαμορφωμένο για καλύτερη εμφάνιση. - Το κουμπί παράλειψης είναι διαμορφωμένο για ελάχιστο μέγεθος. - Εμφάνιση κουμπιού δημιουργίας νέου τμήματος - Το κουμπί δημιουργίας νέου τμήματος δεν εμφανίζεται. - Το κουμπί δημιουργίας νέου τμήματος εμφανίζεται. - Ενεργοποίηση του SponsorBlock - Το SponsorBlock είναι ένα σύστημα που προέρχεται από το κοινό για παράλειψη ενοχλητικών τμημάτων σε βίντεο YouTube. - Εμφάνιση κουμπιού ψηφοφορίας τμημάτων - Το κουμπί ψηφοφορίας τμημάτων δεν εμφανίζεται. - Το κουμπί ψηφοφορίας τμημάτων εμφανίζεται. - Γενικά - Ρύθμιση βήματος νέου τμήματος - Η τιμή πρέπει να είναι θετικός αριθμός. - Ο αριθμός των χιλιοστών του δευτερολέπτου που μετακινούνται τα κουμπιά ρύθμισης χρόνου κατά τη δημιουργία νέων τμημάτων. - Αλλαγή διεύθυνσης API - Η διεύθυνση που χρησιμοποιεί το SponsorBlock για να κάνει κλήσεις στο διακομιστή. - Ελάχιστη διάρκεια τμήματος - Μη έγκυρη διάρκεια χρόνου. - Τμήματα μικρότερα από την καθορισμένη τιμή (σε δευτερόλεπτα) δε θα παραλείπονται ούτε θα εμφανίζονται. - Mετρητής παραλείψεων τμημάτων - Ο μετρητής παραλείψεων δεν είναι ενεργός. - Επιτρέπει στον πίνακα κατάταξης SponsorBlock να γνωρίζει πόσος χρόνος εξοικονομήθηκε. Αποστέλλεται ένα μήνυμα στον πίνακα κατάταξης κάθε φορά που παραλείπεται ένα τμήμα. - Εμφάνιση μηνύματος κατά την αυτόματη παράλειψη - Δεν εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης. Πατήστε για να δείτε ένα παράδειγμα. - Εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης όταν ένα τμήμα παραλείπεται αυτόματα. Πατήστε για να δείτε ένα παράδειγμα. - Εμφάνιση μήκους βίντεο χωρίς τα τμήματα - Εμφανίζεται το πλήρες μήκος του βίντεο. - Εμφανίζεται το μήκος βίντεο μείον όλα τα τμήματα, σε παρένθεση δίπλα στο πλήρες μήκος βίντεο. - Το ιδιωτικό σας αναγνωριστικό χρήστη - Το ιδιωτικό αναγνωριστικό χρήστη πρέπει να είναι τουλάχιστον 30 χαρακτήρες. - Αυτό πρέπει να μείνει μυστικό. Είναι σαν έναν κωδικό που δεν πρέπει να μοιραστείτε με κανέναν. Εάν κάποιος αποκτήσει αυτόν τον κωδικό, μπορεί να σας υποδυθεί. - Τις διάβασα ήδη - Διαβάστε τις οδηγίες του SponsorBlock πριν δημιουργήσετε νέα τμήματα. - Δείξε μου - Ακολουθήστε τις οδηγίες - Οι οδηγίες περιέχουν κανόνες και συμβουλές σχετικά με την υποβολή τμημάτων. - Προβολή οδηγιών - Επιλέξτε την κατηγορία τμήματος - Το τμήμα έχει διάρκεια από %1$02d:%2$02d έως %3$02d:%4$02d (%5$d λεπτά και %6$02d δευτερόλεπτα)\nΕίναι έτοιμο για υποβολή; - Το τμήμα είναι από\n\n%1$s\nσε\n%2$s\n\n(%3$s)\n\nΈτοιμο για υποβολή; - Είναι σωστοί οι χρόνοι; - Η κατηγορία είναι απενεργοποιημένη στις ρυθμίσεις. Ενεργοποιήστε την κατηγορία για υποβολή. - Θέλετε να επεξεργαστείτε τον χρονισμό του τμήματος από την αρχή ή από το τέλος του τμήματος; - Δόθηκε μη έγκυρος χρόνος. - Επεξεργασία χρονισμού του τμήματος χειροκίνητα - Ορισμός %s ως αρχή ή τέλος ενός νέου τμήματος; - τέλος - Σημειώστε δύο σημεία στην γραμμή προόδου πρώτα. - αρχή - τώρα - Κάντε προεπισκόπηση του τμήματος, και σιγουρευτείτε ότι παραλείπεται σωστά. - Η αρχή πρέπει να είναι πριν το τέλος. - Χρόνος λήξης του τμήματος - Χρόνος έναρξης του τμήματος - Νέο τμήμα SponsorBlock - Επαναφορά - Επαναφορά χρώματος - Εφαπτομενικές Σκηνές / Αστεία - Παρεμβατικές σκηνές που προστίθενται μόνο για γέμισμα ή χιούμορ και δεν είναι απαραίτητες για την κατανόηση του κύριου περιεχομένου του βίντεο. Δεν περιλαμβάνει τμήματα που παρέχουν πλαίσιο ή λεπτομέρειες υποβάθρου. - Αποκορύφωμα - Το μέρος του βίντεο που ψάχνουν οι περισσότεροι άνθρωποι. - Υπενθύμιση Αλληλεπίδρασης (Εγγραφή) - Όταν υπάρχει μια σύντομη υπενθύμιση για να προσθέσετε το βίντεο στα βίντεο που σας αρέσουν, να εγγραφείτε ή να τους ακολουθήσετε στην μέση του περιεχομένου. Αν είναι μεγάλο ή αφορά κάτι συγκεκριμένο, θα πρέπει να είναι στην κατηγορία αυτοπροώθησης. - Διάλειμμα / Εισαγωγή - Χρονικό διάστημα χωρίς πραγματικό περιεχόμενο. Θα μπορούσε να είναι μια παύση, ένα στατικό καρέ ή μια επαναλαμβανόμενη κίνηση. Δεν περιλαμβάνει μεταβάσεις που περιέχουν πληροφορίες. - Μουσική: Τμήμα χωρίς μουσική - Μόνο για χρήση σε βίντεο μουσικής. Τμήματα χωρίς μουσική σε βίντεο μουσικής, που δεν καλύπτονται ήδη από άλλη κατηγορία. - Τελική Οθόνη / Συντελεστές - Όταν εμφανίζονται οι συντελεστές ή τα προτεινόμενα βίντεο των καναλιών. Όχι για επίλογους που περιέχουν πληροφορίες. - Προεπισκόπηση / Περίληψη - Συλλογή από κλιπ που δείχνουν τι έρχεται ή τι συνέβη στο βίντεο ή σε άλλα βίντεο μιας σειράς, όπου όλες οι πληροφορίες επαναλαμβάνονται αλλού. - Αφιλοκέρδεια / Αυτοπροώθηση - Παρόμοιο με το «Χορηγός» αλλά για μη κερδοσκοπικό σκοπό ή για προσωπική προώθηση. Περιλαμβάνει τμήματα σχετικά με εμπορεύματα, δωρεές ή πληροφορίες για το με ποιους συνεργάστηκαν. - Χορηγός - Προώθηση επί πληρωμή, παραπομπές επί πληρωμή και άμεσες διαφημίσεις. Όχι για αυτοπροώθηση ή δωρεάν αναφορές σε σκοπούς / δημιουργούς / ιστοσελίδες / προϊόντα που τους αρέσουν. - Αντιγραφή - Η εξαγωγή απέτυχε: %s. - Εισαγωγή / Εξαγωγή ρυθμίσεων - Οι ρυθμίσεις SponsorBlock σας σε μορφή JSON που μπορούν να εισαχθούν / εξαχθούν στο ReVanced Extended και σε άλλες πλατφόρμες SponsorBlock. - Οι ρυθμίσεις SponsorBlock σας σε μορφή JSON που μπορούν να εισαχθούν / εξαχθούν στο ReVanced Extended και σε άλλες πλατφόρμες SponsorBlock. Αυτό περιλαμβάνει το ιδιωτικό σας αναγνωριστικό χρήστη. Φροντίστε να το μοιραστείτε με σύνεση. - Η εισαγωγή απέτυχε: %s. - Οι ρυθμίσεις εισήχθησαν επιτυχώς. - Οι ρυθμίσεις σας περιέχουν ένα ιδιωτικό αναγνωριστικό χρήστη SponsorBlock.\n\nΑυτό είναι σαν έναν κωδικό πρόσβασης και δεν πρέπει ποτέ να το μοιραστείτε.\n - Να μην εμφανιστεί ξανά - Οι ρυθμίσεις αντιγράφηκαν στο πρόχειρο. - Αυτόματη παράλειψη - Αυτόματη παράλειψη μία φορά - Παράλειψη - Αποκορύφωμα - Παράλειψη σπατάλης χρόνου - Μετάβαση στο αποκορύφωμα - Παράλειψη αλληλεπίδρασης - Παράλειψη εισαγωγής - Παράλειψη διακοπής - Παράλειψη διακοπής - Παράλειψη μη-μουσικού - Παράλειψη επιλόγου - Παράλειψη προεπισκόπησης - Παράλειψη ανακεφαλαίωσης - Παράλειψη προεπισκόπησης - Παράλειψη προώθησης - Παράλειψη χορηγού - Παράλειψη τμήματος - Απενεργοποίηση - Εμφάνιση στη γραμμή προόδου - Εμφάνιση κουμπιού παράλειψης - Παραλείφθηκε η σπατάλη χρόνου. - Έγινε μετάβαση στο αποκορύφωμα. - Παραλείφθηκε η ενοχλητική υπενθύμιση. - Παραλείφθηκε η εισαγωγή. - Παραλείφθηκε η διακοπή. - Παραλείφθηκε η διακοπή. - Παραλείφθηκαν πολλαπλά τμήματα. - Παραλείφθηκε τμήμα χωρίς μουσική. - Παραλείφθηκε ο επίλογος. - Παραλείφθηκε η προεπισκόπηση. - Παραλείφθηκε η ανακεφαλαίωση. - Παραλείφθηκε η προεπισκόπηση. - Παραλείφθηκε η αυτοπροώθηση. - Παραλείφθηκε ο χορηγός. - Παραλείφθηκε μη υποβληθέν τμήμα. - SponsorBlock προσωρινά μη διαθέσιμο. - SponsorBlock προσωρινά μη διαθέσιμο (κατάσταση %d). - SponsorBlock προσωρινά μη διαθέσιμο (καθυστέρηση API). - Στατιστικά - Στατιστικά προσωρινά μη διαθέσιμα (API εκτός λειτουργίας). - Φόρτωση... - Η φήμη σας είναι <b>%.2f</b> - Έχετε σώσει άλλους από <b>%s</b> τμήματα - %1$s ώρες %2$s λεπτά - %1$s λεπτά %2$s δευτερόλεπτα - %s δευτερόλεπτα - Αυτό είναι <b>%s</b> από τις ζωές τους.<br>Κάντε κλικ για να δείτε τον πίνακα κατάταξης. - Πατήστε για να δείτε τα παγκόσμια στατιστικά και τους κορυφαίους συνεισφέροντες. - Πίνακας κατάταξης SponsorBlock - Το SponsorBlock είναι απενεργοποιημένο. - Έχετε παραλείψει <b>%s</b> τμήματα - Επαναφορά του μετρητή τμημάτων που παραλείφθηκαν; - Αυτό είναι <b>%s</b>. - Δημιουργήσατε <b>%s</b> τμήματα - Πατήστε για να δείτε τα τμήματα σας. - Το όνομα χρήστη σας: <b>%s</b> - Πατήστε για να αλλάξετε το όνομα χρήστη σας - Αδυναμία αλλαγής ονόματος χρήστη: Κατάσταση: %1$d %2$s. - Το όνομα χρήστη άλλαξε επιτυχώς. - Αδυναμία υποβολής του τμήματος.\nΥπάρχει ήδη. - Αδυναμία υποβολής του τμήματος: %s. - Αδυναμία υποβολής τμήματος: %s. - Αδυναμία υποβολής τμήματος.\nΌριο συχνότητας (πάρα πολλά από τον ίδιο χρήστη ή την IP). - Το SponsorBlock είναι προσωρινά εκτός λειτουργίας. - Αδυναμία υποβολής τμήματος (κατάσταση: %1$d %2$s). - Το τμήμα υποβλήθηκε επιτυχώς. - Δεν εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης αν το SponsorBlock δεν είναι διαθέσιμο. - Εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης αν το SponsorBlock δεν είναι διαθέσιμο. - Εμφάνιση μηνύματος αν το API δεν είναι διαθέσιμο - Αλλαγή κατηγορίας - Αρνητική ψήφος - Αδυναμία ψηφοφορίας για το τμήμα: %s. - Αδυναμία ψηφοφορίας για το τμήμα (καθυστέρηση API). - Αδυναμία ψηφοφορίας για το τμήμα (κατάσταση: %1$d %2$s). - Δεν υπάρχουν τμήματα προς ψήφιση. - Θετική ψήφος - Οι ρυθμίσεις αντιγράφηκαν στο πρόχειρο. - Η χρονική σήμανση αντιγράφηκε στο πρόχειρο. (%s) - Η διεύθυνση URL αντιγράφηκε στο πρόχειρο. - Η διεύθυνση URL με χρονική σήμανση αντιγράφηκε στο πρόχειρο. - Προεπιλογή - Αντίχειρας προς τα πάνω - Αντίχειρας προς τα πάνω (Θέμα Cairo) - Καρδιά - Καρδιά (χρωματιστή) - Τίποτα - Εφέ διπλού πατήματος - Το βάθος πρέπει να είναι μεταξύ 0-64. - Αλλαγή βάθους της γραμμής προόδου, τιμές μεταξύ 0-64. - Βάθος γραμμής προόδου - Το ποσοστό ύψους πρέπει να είναι μεταξύ 0-100 (%). - Ρύθμιση του ποσοστού ύψους του κενού χώρου που απομένει όταν η γραμμή πλοήγησης είναι κρυμμένη, μεταξύ 0 και 100 (%). - Ποσοστό ύψους του κενού χώρου - Πατήστε παρατεταμένα την χρονοσφραγίδα για να αλλάξει η κατάσταση επανάληψης των Shorts. - Ενέργεια πατήματος χρονοσφραγίδας - "Εμφάνιση τίτλου του βίντεο σε πλήρη οθόνη. - -Περιορισμός: Ο τίτλος εξαφανίζεται όταν πατηθεί." - Εμφάνιση τίτλου βίντεο - Αν είναι ενεργοποιημένη η αυτόματη αναπαραγωγή, το επόμενο βίντεο παίζει αφού τελειώσει η αντίστροφη μέτρηση. - Αν είναι ενεργοποιημένη η αυτόματη αναπαραγωγή, το επόμενο βίντεο παίζει χωρίς αντίστροφη μέτρηση. - Άμεση αυτόματη αναπαραγωγή - "Παράλειψη προφόρτωσης στην αρχή του βίντεο, ώστε να γίνει άμεση εφαρμογή της προεπιλεγμένης ποιότητας. - -• Κατά την έναρξη του βίντεο, υπάρχει μια καθυστέρηση περίπου 0.3 δευτερολέπτων. -• Δεν εφαρμόζεται σε βίντεο HDR, ζωντανές μεταδόσεις ή βίντεο μικρότερα από 15 δευτερόλεπτα." - Παράλειψη προφόρτωσης βίντεο - Δεν εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης. - Εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης. - Εμφάνιση μηνύματος κατά την παράλειψη - Η ενεργοποίηση αυτής της ρύθμισης μπορεί να προκαλέσει προβλήματα αναπαραγωγής βίντεο. - Η προφόρτωση βίντεο παραλείφθηκε. - Η ταχύτητα πρέπει να ναι μεταξύ 0-8.0. - Τιμή ταχύτητας που εφαρμόζεται κατά το παρατεταμένο πάτημα, μεταξύ 0 και 8.0. - Αλλαγή τιμής διεπαφής ταχύτητας - "Τροποποίηση έκδοσης της εφαρμογής σε παλιότερη έκδοση. - -Αυτό θα αλλάξει την εμφάνιση της εφαρμογής, αλλά ενδέχεται να προκύψουν άγνωστες παρενέργειες. -Εάν αργότερα απενεργοποιηθεί, η παλιά εμφάνιση μπορεί να παραμείνει μέχρι να διαγραφούν τα δεδομένα της εφαρμογής." - Η έκδοση δεν παραποιείται. - Η έκδοση παραποιείται. - 17.33.42 - Επαναφορά της παλιάς εμφάνισης - 17.41.37 - Επαναφορά ενότητας λίστας αναπαραγωγής στο παλιό στυλ - 18.05.40 - Επαναφορά πλαισίου εισαγωγής σχολίων στο παλιό στυλ - 18.17.43 - Επαναφορά αναδυόμενου πίνακα της οθόνης αναπαραγωγής στο παλιό στυλ - 18.33.40 - Επαναφορά γραμμής ενεργειών Shorts στο παλιό στυλ - 18.38.45 - Επαναφορά της παλιάς συμπεριφοράς προεπιλεγμένης ποιότητας βίντεο - 18.48.39 - Απενεργοποίηση ενημέρωσης των προβολών & αριθμού των «Μου αρέσει» σε πραγματικό χρόνο - 19.13.37 - Επαναφορά των παλιών εφέ κίνησης αριθμών - Έκδοση παραποίησης της εφαρμογής - Πληκτρολογήστε την έκδοση εφαρμογής που θα εφαρμοστεί. - Επεξεργασία έκδοσης εφαρμογής που θα εφαρμοστεί - Παραποίηση έκδοσης εφαρμογής - "Η έκδοση της εφαρμογής YouTube θα παραποιηθεί σε παλιότερη. - -Αυτό θα αλλάξει την εμφάνιση και τα χαρακτηριστικά της εφαρμογής, αλλά ενδέχεται να εμφανιστούν άγνωστες παρενέργειες. - -Αν αργότερα απενεργοποιηθεί, συνιστάται η εκκαθάριση δεδομένων της εφαρμογής για την αποφυγή σφαλμάτων UI." - "Παραποίηση διαστάσεων συσκευής στη μέγιστη τιμή. -Ενδέχεται να ξεκλειδωθούν υψηλότερες ποιότητες σε κάποια βίντεο που απαιτούν υψηλές διαστάσεις συσκευής, αλλά όχι σε όλα τα βίντεο." - Παραποίηση διαστάσεων συσκευής - Ο κωδικοποιητής βίντεο iOS είναι ο AVC (H.264), ο VP9 ή ο AV1. - Ο κωδικοποιητής βίντεο iOS είναι ο AVC (H.264). - Εξαναγκασμός iOS AVC (H.264) - "Ενεργοποιώντας αυτόν τον κωδικοποιητή ίσως βελτιωθεί η κατανάλωση ενέργειας και ίσως διορθωθούν μικροκολλήματα αναπαραγωγής. - -Ο AVC (H.264) ωστόσο έχει μέγιστη ανάλυση 1080p, και η αναπαραγωγή βίντεο καταναλώνει περισσότερα δεδομένα internet από τον VP9 ή τον AV1." - "• Το μενού «Κομμάτι ήχου» λείπει. -• Η λειτουργία «Σταθερή ένταση» δεν είναι διαθέσιμη." - "• Το μενού «Κομμάτι ήχου» λείπει. -• Η λειτουργία «Σταθερή ένταση» δεν είναι διαθέσιμη." - "• Οι ταινίες ή τα επί πληρωμή βίντεο ενδέχεται να μην αναπαράγονται. -• Οι ζωντανές μεταδόσεις ξεκινούν από την αρχή κατά την αναπαραγωγή. -• Τα βίντεο μπορεί να τελειώνουν 1 δευτερόλεπτο νωρίτερα. -• Ο κωδικοποιητής ήχου opus δεν είναι διαθέσιμος." - Παρενέργειες παραποίησης - • Τα βίντεο ενδέχεται να μην αναπαράγονται. - Το πρόγραμμα πελάτη που χρησιμοποιείται για τη λήψη δεδομένων ροής δεν εμφανίζεται στο μενού «Στατιστικά για σπασίκλες». - Το πρόγραμμα πελάτη που χρησιμοποιείται για τη λήψη δεδομένων ροής εμφανίζεται στο μενού «Στατιστικά για σπασίκλες». - Εμφάνιση στο «Στατιστικά για σπασίκλες» - "Τα δεδομένα ροής δεν παραποιούνται. Η αναπαραγωγή βίντεο ενδέχεται να μη λειτουργεί σωστά." - Τα δεδομένα ροής παραποιούνται. - Παραποίηση δεδομένων ροής - Android - Android TV - Android VR - iOS - Προεπιλογή - Η απενεργοποίηση αυτής της ρύθμισης ενδέχεται να προκαλέσει προβλήματα αναπαραγωγής βίντεο. - Η ευαισθησία σάρωσης πρέπει να ναι μεταξύ 1-1000 (%). - Ρύθμιση της ευαισθησίας σάρωσης για αλλαγή της φωτεινότητας, μεταξύ 1 και 1000 (%).\nΌσο μικρότερη η ελάχιστη απόσταση, τόσο πιο γρήγορα αλλάζει το επίπεδο φωτεινότητας. - Ευαισθησία σάρωσης φωτεινότητας - Οι χειρονομίες σάρωσης είναι απενεργοποιημένες στη λειτουργία «Οθόνη κλειδώματος». - Οι χειρονομίες σάρωσης είναι ενεργοποιημένες στη λειτουργία «Οθόνη κλειδώματος». - Χρήση στη λειτουργία «Οθόνη κλειδώματος» - Αυτόματη - Ελάχιστο πλάτος κίνησης αναγνωρίσιμο ως χειρονομία σάρωσης. - Κατώτατο όριο μεγέθους σάρωσης - Η ορατότητα του φόντου σάρωσης στο παρασκήνιο. - Ορατότητα φόντου σάρωσης - Το μέγεθος οθόνης πρέπει να ναι μικρότερο από 50. - Ποσοστό επιφάνειας της οθόνης που μπορεί να γίνει η σάρωση.\n\nΣημείωση: Αυτό θα αλλάξει επίσης το μέγεθος της περιοχής οθόνης της χειρονομίας διπλού πατήματος για αναζήτηση. - Μέγεθος περιοχής οθόνης σάρωσης - Το μέγεθος κειμένου στοιχείων ελέγχου του φόντου σάρωσης. - Μέγεθος κειμένου φόντου σάρωσης - Το χρονικό διάστημα των χιλιοστών του δευτερολέπτου που είναι ορατό το φόντο σάρωσης. - Χρονικό όριο φόντου σάρωσης - Η ευαισθησία σάρωσης πρέπει να ναι μεταξύ 1-1000 (%). - Ρύθμιση της ευαισθησίας σάρωσης για αλλαγή της έντασης ήχου, μεταξύ 1 και 1000 (%).\nΌσο μικρότερη η ελάχιστη απόσταση, τόσο πιο γρήγορα αλλάζει το επίπεδο έντασης.\nΗ προτεινόμενη ευαισθησία είναι 100% για 15 βήματα έντασης και 10% για 150 βήματα έντασης. - Ευαισθησία σάρωσης έντασης ήχου - "Εναλλαγή θέσεων των κουμπιών «Δημιουργία» και «Ειδοποιήσεις» παραποιώντας τις πληροφορίες συσκευής. - -• Όταν ενεργοποιηθεί, μπορεί να μη λειτουργήσει μέχρι να γίνει επανεκκίνηση της συσκευής σας. -• Η ενεργοποίηση αυτής της ρύθμισης εξαναγκάζει επίσης την απενεργοποίηση των διαφημίσεων βίντεο." - Δεν γίνεται εναλλαγή θέσεων των κουμπιών «Δημιουργία» και «Ειδοποιήσεις». - "Γίνεται εναλλαγή θέσεων των κουμπιών «Δημιουργία» και «Ειδοποιήσεις». - -Σημείωση: Η ενεργοποίηση αυτής της ρύθμισης εξαναγκάζει επίσης την απόκρυψη των διαφημίσεων βίντεο." - Εναλλαγή «Δημιουργία» με «Ειδοποιήσεις» - "Η απενεργοποίηση αυτής της ρύθμισης μπορεί να έχει ως αποτέλεσμα την φόρτωση περισσότερων διαφημίσεων από τον διακομιστή. - -Επίσης, ενδέχεται να εμφανίζονται διαφημίσεις στα Shorts. - -Αν η απενεργοποίηση δεν τεθεί σε ισχύ, δοκιμάστε να μεταβείτε σε λειτουργία ανώνυμης περιήγησης." - Προεπιλογή - RVX Music - %s δεν έχει εγκατασταθεί. Παρακαλούμε εγκαταστήστε το. - Όνομα πακέτου του εγκατεστημένου RVX Music. - Όνομα πακέτου RVX Music - Το ιστορικό παρακολούθησης είναι αποκλεισμένο. - "• Ακολουθούνται οι ρυθμίσεις ιστορικού παρακολούθησης του λογαριασμού Google σας. -• Το ιστορικό παρακολούθησης μπορεί να μη λειτουργεί λόγω του DNS σας ή χρήσης VPN." - • Ακολουθούνται οι ρυθμίσεις ιστορικού παρακολούθησης του λογαριασμού Google σας. - Κατάσταση ιστορικού παρακολούθησης - Πατήστε για άνοιγμα της διαχείρισης του ιστορικού παρακολούθησης του YouTube. - Διαχείριση όλου του ιστορικού - Αρχικός - Αντικατάσταση του domain - Αποκλεισμός ιστορικού παρακολούθησης - Τύπος ιστορικού παρακολούθησης - Αποτυχία προσθήκης καναλιού \'%1$s\' στη λίστα επιτρεπόμενων %2$s. - Το κανάλι \'%1$s\' προστέθηκε στη λίστα επιτρεπόμενων %2$s. - Δεν υπάρχουν κανάλια στην λίστα επιτρεπόμενων. - Δεν προστέθηκε στη λίστα επιτρεπόμενων. - Αποτυχία φόρτωσης πληροφοριών καναλιού. - Προστέθηκε στη λίστα επιτρεπόμενων. - Ταχύτητα αναπαραγωγής - Κατάργηση καναλιού \'%1$s\' από τη λίστα επιτρεπόμενων %2$s; - Αποτυχία κατάργησης καναλιού \'%1$s\' από τη λίστα επιτρεπόμενων %2$s. - Το κανάλι \'%1$s\' καταργήθηκε από τη λίστα επιτρεπόμενων %2$s. - Ελέγξτε ή καταργήστε την λίστα των καναλιών που έχουν προστεθεί στη λίστα επιτρεπόμενων. - Λίστα επιτρεπόμενων καναλιών - SponsorBlock - diff --git a/src/main/resources/youtube/translations/es-rES/missing_strings.xml b/src/main/resources/youtube/translations/es-rES/missing_strings.xml deleted file mode 100644 index d514aa21f..000000000 --- a/src/main/resources/youtube/translations/es-rES/missing_strings.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - Don\'t show again - Courses / Learning - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - Disabled comments button or with label \"0\" is shown. - Disabled comments button or with label \"0\" is hidden. - Hide disabled comments button - MMT Blue - MMT Green - MMT Orange - MMT Pink - MMT Turquoise - MMT Yellow - Revancify Yellow - Vanced Black - Vanced Light - Xisr Yellow - Adjust: Mark Start and End Time for segment - Verify the Segment - Edit the Segment - Forward by Specified Time (Default: 150ms) - Publish Created Segment - Rewind by Specified Time (Default: 150ms) - diff --git a/src/main/resources/youtube/translations/es-rES/strings.xml b/src/main/resources/youtube/translations/es-rES/strings.xml deleted file mode 100644 index 04a8752ee..000000000 --- a/src/main/resources/youtube/translations/es-rES/strings.xml +++ /dev/null @@ -1,1701 +0,0 @@ - - - ¿Activar controles de accesibilidad para el reproductor de vídeo? - Tus controles se modifican porque un servicio de accesibilidad está activado. - Continuar - "GmsCore no tiene permiso para ejecutarse en segundo plano. - -Sigue la guía \"No cerrar mi aplicación\" para tu teléfono y aplica las instrucciones a tu instalación de MicroG. - -Esto es necesario para que la aplicación funcione." - "Las optimizaciones de la batería para GmsCore deben estar desactivadas para evitar problemas. - -Pulsa el botón de continuar y desactiva las optimizaciones de la batería." - Abrir sitio web - Acción necesaria - Activa la mensajería en la nube para recibir notificaciones. - Abrir GmsCore - GmsCore no está instalado. Instálalo. - "DeArrow proporciona miniaturas de origen colectivo para los vídeos de YouTube. Estas miniaturas son a menudo más relevantes que las proporcionadas por YouTube. - -Si se activa, las URL de vídeo se enviarán al servidor API y no se enviará ningún otro dato. Si un video no tiene miniaturas de DeArrow, entonces se muestran las capturas originales o fijas. - -Pulsa aquí para saber más sobre DeArrow." - DeArrow - URL de la API de DeArrow no válida. - La URL del punto final de la caché de miniaturas de DeArrow. - Punto final de la API de DeArrow - No se muestra el mensaje si DeArrow no está disponible. - Se muestra el mensaje si DeArrow no está disponible. - Mostrar mensaje si la API no está disponible - DeArrow temporalmente no disponible. (código de estado: %s) - DeArrow temporalmente no disponible. - Pestaña de inicio - Pestaña Tú - Miniaturas originales - DeArrow y miniaturas originales - DeArrow y capturas fijas - Capturas fijas - Listas de reproducción, recomendaciones - Resultados de búsqueda - Capturas de vídeo - Las capturas fijas se toman del principio / mitad / final de cada vídeo. Estas imágenes están integradas en YouTube y no se utiliza ninguna API externa. - Capturas de vídeo fijas - Utilizando capturas fijas de alta calidad. - Utilizando capturas fijas de calidad media. Las miniaturas se cargarán más rápido, pero los vídeos en directo, inéditos o muy antiguos pueden mostrar miniaturas en blanco. - Usar capturas fijas rápidas - Principio del vídeo - Mitad del vídeo - Final del vídeo - Tiempo de vídeo para tomar capturas fijas - Pestaña de suscripciones - La adición de información en la marca de tiempo está desactivada. - "La adición de información en la marca de tiempo está activada." - Añadir información en marca de tiempo - Añadir velocidad de reproducción. - Añadir calidad de vídeo. - Añadir tipo de información - El modo ambiente está desactivado en el modo de ahorro de batería. - El modo ambiente está activado en el modo de ahorro de batería. - Omitir restricciones del modo ambiente - El dominio del que se obtendrán las imágenes.\nNota: Introduzca solo el nombre del dominio, es decir, sin el prefijo \"https\:\/\/\". - Dominio alternativo - Utilizando el host de imágenes original.\n\nAl activar esto se pueden arreglar las imágenes faltantes que están bloqueadas en algunas regiones. - Utilizando host de imágenes yt4.ggpht.com. - Omitir restricciones de región de imágenes - Original - Teléfono - Teléfono (máx. 480 dpi) - Tablet - Tablet (min. 600 dpi) - Cambiar diseño - Se utilizan interruptores cambiables. - Se utilizan interruptores de texto. - Cambiar tipo de interruptores - Se utiliza la hoja de compartir incorporada. - Se utiliza la hoja de compartir del sistema. - Cambiar hoja de compartir - Reproducción automática - Predeterminada - Pausar - Repetir - Cambiar estado de repetición de Shorts - Explorar canales - Predeterminada - Explorar - Juegos - Historial - Biblioteca - Vídeos gustados - En directo - Películas - Música - Búsqueda - Shorts - Deportes - Suscripciones - Tendencias - Ver más tarde - Cambiar página de inicio - La página de inicio solo cambia una vez. - "La página de inicio siempre cambia. - -Limitación: Es posible que el botón Atrás de la barra de herramientas no funcione." - Cambiar tipo de página de inicio - La cabecera genérica está activada. - La cabecera Premium está activada. - Cambiar cabecera de YouTube - Lista de cadenas del constructor de rutas de componentes a filtrar separadas por una nueva línea. - Filtro personalizado - El filtro personalizado está desactivado. - El filtro personalizado está activado. - Activar filtro personalizado - Filtro personalizado no válido: %s. - Se utiliza el antiguo estilo del panel desplegable. - Se utiliza un diálogo personalizado. - Tipo de menú de velocidad personalizada de reproducción. - Las velocidades personalizadas deben ser inferiores a %sx. Utilizando valores predeterminados. - Velocidades personalizadas de reproducción no válidas. Utilizando valores predeterminados. - Añadir o cambiar las velocidades de reproducción disponibles. - Editar velocidades personalizadas de reproducción - La opacidad de la superposición del reproductor debe estar entre 0-100. Restablezca a los valores predeterminados. - Valor de opacidad entre 0-100, donde 0 es transparente. - Opacidad personalizada de superposición del reproductor - Escribe el código hexadecimal del color de la barra de progreso. - Valor de color personalizado de barra de progreso - Para abrir RVX en un navegador externo, activa \"Abrir enlaces compatibles\" y activa las direcciones web compatibles. - Abrir ajustes predeterminados de la app - Velocidad predeterminada de reproducción - Calidad predeterminada de vídeo en red móvil - Calidad predeterminada de vídeo en red Wi-Fi - Desactiva el modo ambiente solo para pantalla completa. - El modo ambiente está activado en pantalla completa. - El modo ambiente está desactivado en pantalla completa. - Desactivar modo ambiente en pantalla completa - Desactiva el modo ambiente. - El modo ambiente está activado. - El modo ambiente está desactivado. - Desactivar modo ambiente - Las pistas de audio automáticas forzadas están activadas. - Las pistas de audio automáticas forzadas están desactivadas. - Desactivar pistas de audio automáticas forzadas - Los subtítulos automáticos forzados están activados. - Los subtítulos automáticos forzados están desactivados. - Desactivar subtítulos automáticos forzados - Los paneles emergentes del reproductor automático están desactivados. - Los paneles emergentes del reproductor automático están activados. - Desactivar paneles emergentes del reproductor - "El cambio automático de listas de reproducción Mix está activado cuando la reproducción automática está activada. - -La reproducción automática se puede cambiar en la configuración de YouTube: -Configuración → Reproducción automática → Reproducción automática del siguiente vídeo" - El cambio automático de listas de reproducción Mix está desactivado. - Desactivar cambio de listas de reproducción Mix - Al activar esta función, se desactivará el cambio automático a YouTube Mix al reproducir música con la reproducción automática activada. - La velocidad predeterminada de reproducción está activada en directo. - La velocidad predeterminada de reproducción está desactivada en directo. - Desactivar la velocidad de reproducción en directo - La velocidad predeterminada de reproducción está activada para la música. - "La velocidad predeterminada de reproducción está desactivada para la música. - -Limitación: Es posible que este ajuste no se aplique a los vídeos que no incluyan el banner \"Escuchar en YouTube Music\"." - Desactivar velocidad de reproducción para música - El panel de interacción está activado. - El panel de interacción está desactivado. - Desactivar panel de interacción - La vibración está activada. - La vibración está desactivada. - Desactivar vibración de los capítulos - La vibración está activada. - La vibración está desactivada. - Desactivar vibración al deslizar - La vibración está activada. - La vibración está desactivada. - Desactivar vibración al desplazarse - La vibración está activada. - La vibración está desactivada. - Desactivar vibración al deshacer desplazamiento - La vibración está activada. - La vibración está desactivada. - Desactivar vibración al hacer zoom - El brillo HDR automático está activado. - El brillo HDR automático está desactivado. - Desactivar brillo HDR automático - El vídeo HDR está activado. - El vídeo HDR está desactivado. - Desactivar vídeo HDR - La orientación del vídeo sigue la configuración del dispositivo en pantalla completa. - La orientación del vídeo es vertical en pantalla completa. - Desactivar modo horizontal - Los botones de me gusta y no me gusta brillarán cuando se mencionen. - Los botones de me gusta y no me gusta no brillarán cuando se mencionen. - Desactivar brillo de botones de me gusta y no me gusta - "Desactiva el protocolo QUIC de CronetEngine." - Desactivar protocolo QUIC - El reproductor de Shorts se reanudará al iniciar la app - El reproductor de Shorts no se reanudará al iniciar la app - Desactivar reanudación del reproductor de Shorts - Los números rodantes están animados. - Los números rodantes no están animados. - Desactivar animaciones de números rodantes - Los capítulos están activados en la barra de progreso. - Los capítulos están desactivados en la barra de progreso. - Desactivar capítulos en barra de progreso - La animación de la fuente está activada sobre el botón de me gusta. - La animación de la fuente está desactivada sobre el botón de me gusta. - Desactivar animación del botón de me gusta - "Desactiva \"Reproducir a velocidad x2\" mientras mantienes pulsado. - -Nota: -• Al desactivar la superposición de velocidad se restablece el comportamiento \"Deslizar para desplazarse\" del antiguo diseño. -• Este ajuste no obliga a activar la superposición de velocidad." - Desactivar superposición de velocidad - La animación de bienvenida está activada. - La animación de bienvenida está desactivada. - Desactivar animación de bienvenida - "Desactiva las siguientes interacciones cuando se expande la descripción del vídeo: - -• Pulsar para desplazarse. -• Mantener pulsado para seleccionar texto." - Desactivar interacción de descripción de vídeo - El códec VP9 está activado. - "El códec VP9 está desactivado. - -• La resolución máxima es 1080p. -• La reproducción de vídeo utilizará más datos de Internet que VP9. -• Para obtener reproducción HDR, el vídeo HDR sigue utilizando el códec VP9." - Desactivar códec VP9 - La barra de progreso Cairo está desactivada. - "La barra de progreso Cairo está activada. - -Efecto secundario: El tema Cairo también se aplica a los puntos de notificación." - Activar barra de progreso Cairo - La superposición de controles ocupa la pantalla completa. - La superposición de controles no ocupa la pantalla completa. - Activar superposición compacta de controles - La velocidad personalizada de reproducción está desactivada. - La velocidad personalizada de reproducción está activada. - Activar velocidad personalizada de reproducción - El color personalizado de la barra de progreso está desactivado. - El color personalizado de la barra de progreso está activado. - Activar color personalizado de barra de progreso - Los registros de depuración no incluyen el búfer. - Los registros de depuración incluyen el búfer. - Incluir búfer en registro de depuración - Los registros de depuración están desactivados. - Los registros de depuración están activados. - Activar registro de depuración - La velocidad predeterminada de reproducción no se aplica a los Shorts. - La velocidad predeterminada de reproducción se aplica a los Shorts. - Activar velocidad predeterminada de reproducción de Shorts - El navegador externo está desactivado. - El navegador externo está activado. - Activar navegador externo - La pantalla de carga de degradado está desactivada. - La pantalla de carga de degradado está activada. - Activar pantalla de carga de degradado - El espacio entre los botones de navegación no se hace más estrecho. - El espacio entre los botones de navegación se hace más estrecho. - Activar botones de navegación estrechos - Siguiendo la política predeterminada de redireccionamiento. - Omitiendo los redireccionamientos de URL. - Activar apertura de enlaces directamente - Activa el códec OPUS si la respuesta del reproductor incluye el códec OPUS. - Activar códec OPUS - No se guarda ni restaura el brillo al salir o entrar en pantalla completa. - Guarda y restaura el brillo al salir o entrar en pantalla completa. - Activar guardar y restaurar brillo - La pulsación en la barra de progreso está desactivada. - La pulsación en la barra de progreso está activada. - Activar pulsación en barra de progreso - "Esto restaurará las miniaturas de las transmisiones en directo que no tengan miniaturas en la barra de progreso. - -El uso de datos de Internet puede ser mayor, y las miniaturas en la barra de progreso tendrán una ligera demora antes de mostrarse. - -Esta función funciona mejor con una conexión a Internet muy rápida." - Las miniaturas en la barra de progreso son de calidad media. - Las miniaturas en la barra de progreso son de alta calidad. - Activar miniaturas de alta calidad - La marca de tiempo está desactivada. - "La marca de tiempo está activada. - -Problema conocido: Al tratarse de una función en fase de desarrollo por parte de Google, el diseño puede estar roto." - Activar marcas de tiempo - El control del brillo al deslizar está desactivado. - El control del brillo al deslizar está activado. - Activar gesto de brillo - La vibración está desactivada. - La vibración está activada. - Activar vibración - El valor más bajo del gesto de brillo no activa el brillo automático. - El valor más bajo del gesto de brillo activa el brillo automático. - Activar gesto de brillo automático - Pulsa para activar el gesto de deslizar. - Mantén pulsado para activar el gesto de deslizar. - Activar gesto de pulsar para deslizar - Deslizar hacia arriba / abajo no reproducirá el vídeo siguiente / anterior. - Deslizar para cambiar de vídeo está activado. - -Desliza hacia arriba / abajo para reproducir el vídeo siguiente / anterior. - Activar deslizamiento para cambiar de vídeo en pantalla completa - El control del volumen al deslizar está desactivado. - El control del volumen al deslizar está activado. - Activar gesto de volumen - La barra de navegación es opaca. - La barra de navegación es translúcida. - Activar barra de navegación translúcida - El cambio a pantalla completa deslizando el dedo por la zona inferior del reproductor está desactivado. - El cambio a pantalla completa deslizando el dedo por la zona inferior del reproductor está activado. - Activar gestos del panel de visualización - "Al activar este ajuste, se desactiva el botón de configuración de la pestaña Tú. - -En este caso, utiliza la siguiente ruta: -Pestaña Tú > Ver canal > Menú > Configuración." - Activar barra de búsqueda ancha en pestaña Tú - La barra de búsqueda ancha está desactivada. - La barra de búsqueda ancha está activada. - Activar barra de búsqueda ancha - La barra de búsqueda ancha no incluye la cabecera de YouTube. - La barra de búsqueda ancha incluye la cabecera de YouTube. - Activar barra de búsqueda ancha con cabecera - Descripción - "Ingresa un título en el panel de descripción del vídeo. -Estos caracteres varían dependiendo de tu idioma. -\"Expandir descripción de vídeo\" puede no funcionar si guardas una cadena incorrecta." - Título en panel de descripción del vídeo - La descripción del vídeo se expande manualmente. - La descripción del vídeo se expande automáticamente. - Expandir descripción de vídeo - ¿Quieres continuar? - Restablecer valores predeterminados. - Reiniciar para cargar el diseño normalmente - "Existe un error en el servidor de YouTube que hace que el texto de los números rodantes, como los \"Me gusta\", las visualizaciones y las fechas de subida, se oculte para algunos usuarios. - -Una solución temporal para este problema es falsificar la versión de la aplicación a 19.13.37. - -¿Quieres falsificar la versión de la aplicación antes de reiniciarla?" - Actualizar y reiniciar - Error al exportar los ajustes. - Los ajustes se han exportado correctamente. - Exporta los ajustes a un archivo. - Exportar ajustes - Importar - Copiar - Importar o exportar los ajustes como texto. - Importar / Exportar como texto - Error al importar los ajustes. - Los ajustes se restablecieron a los valores predeterminados. - Los ajustes se han importado correctamente. - Importa los ajustes desde un archivo guardado. - Importar ajustes - Restablecer - Buscar en ajustes - ReVanced Extended - Descargador externo - No instalado - "%1$s no está instalado. -Descarga %2$s desde el sitio web." - Advertencia - %s no está instalado. Por favor, instálalo. - Nombre del paquete de tu aplicación de descargas externas instalada, como YTDLnis. - Nombre del paquete del descargador de listas de reproducción - Nombre del paquete de tu aplicación de descargas externas instalada, como NewPipe o YTDLnis. - Nombre del paquete del descargador de vídeo - "Los vídeos pasarán a pantalla completa en las siguientes situaciones: - -• Cuando se pulsa sobre una marca de tiempo en los comentarios. -• Cuando se inicia un vídeo." - Forzar pantalla completa - Lista de nombres del menú de la cuenta a filtrar separados por una nueva línea. - Filtro de menú de cuenta - "Oculta elementos del menú de la cuenta y de la pestaña Tú. -Algunos componentes pueden no estar ocultos." - Ocultar menú de cuenta - La sección de resumen de vídeo generado por IA está visible. - La sección de resumen de vídeo generado por IA está oculta. - Ocultar sección de resumen de vídeo generado por IA - Las tarjetas del álbum están visibles. - Las tarjetas del álbum están ocultas. - Ocultar tarjetas de álbum - Las secciones de lugares destacados, juegos y música están visibles. - Las secciones de lugares destacados, juegos y música están ocultas. - Ocultar sección de atributos - El contenedor de vista previa de reproducción automática está visible. - El contenedor de vista previa de reproducción automática está oculto. - Ocultar contenedor de vista previa de reproducción automática - El botón de explorar tienda está visible. - El botón de explorar tienda está oculto. - Ocultar botón de explorar tienda - "Oculta las siguientes estanterías: -• Noticias de última hora -• Seguir viendo -• Explorar más canales -• Volver a escuchar -• Compras -• Volver a ver" - Ocultar estante de carrusel - Visible en el feed. - Oculto en el feed. - Ocultar en feed - Visible en los vídeos relacionados. - Oculto en los vídeos relacionados. - Ocultar en vídeos relacionados - Visible en los resultados de búsqueda. - Oculto en los resultados de búsqueda. - Ocultar en resultados de búsqueda - Las directrices de canales están visibles. - Las directrices de canales están ocultas. - Ocultar directrices de canales - El estante de miembros del canal está visible. - El estante de miembros del canal está oculto. - Ocultar estante de miembros del canal - Los enlaces en la parte superior del perfil del canal están visibles. - Los enlaces en la parte superior del perfil del canal están ocultos. - Ocultar enlaces del perfil del canal - "Shorts -Listas de reproducción -Tienda" - Lista de nombres de pestañas del canal a filtrar separados por una nueva línea. - Filtro de pestañas del canal - El filtro de pestañas del canal está desactivado. - El filtro de pestañas del canal está activado. - Activar filtro de pestañas del canal - La marca de agua del canal está visible. - La marca de agua del canal está oculta. - Ocultar marca de agua del canal - Las secciones de capítulos están visibles. - Las secciones de capítulos están ocultas. - Ocultar secciones de capítulos - El estante de fichas está visible. - El estante de fichas está oculto. - Ocultar estante de fichas - El botón de clip está visible. - El botón de clip está oculto. - Ocultar botón de clip - El botón de crear Shorts está visible. - El botón de crear Shorts está oculto. - Ocultar botón de crear Shorts - Los enlaces de búsqueda destacados están visibles. - Los enlaces de búsqueda destacados están ocultos. - Ocultar enlaces de búsqueda destacados - El botón de gracias está visible. - El botón de gracias está oculto. - Ocultar botón de gracias - Los botones de marca de tiempo y emoji están visibles. - Los botones de marca de tiempo y emoji están ocultos. - Ocultar botones de marca de tiempo y emoji - El banner de comentarios de los miembros está visible. - El banner de comentarios de los miembros está oculto. - Ocultar banner de comentarios de los miembros - La sección de comentarios está visible en el feed de inicio. - La sección de comentarios está oculta en el feed de inicio. - Ocultar sección de comentarios en feed de inicio - La sección de comentarios está visible. - La sección de comentarios está oculta. - Ocultar sección de comentarios - Visible en el canal. - Oculto en el canal. - Ocultar en canal - Visible en el feed de inicio y los vídeos relacionados. - Oculto en el feed de inicio y los vídeos relacionados. - Ocultar en feed de inicio y vídeos relacionados - Visible en el feed de suscripciones. - Oculto en el feed de suscripciones. - Ocultar en feed de suscripciones - La sección de cómo se hizo este contenido está visible. - La sección de cómo se hizo este contenido está oculta. - Ocultar sección de contenido - La caja de Crowdfunding está visible. - La caja de Crowdfunding está oculta. - Ocultar caja de Crowdfunding - El filtro de superposición de doble toque está visible. - El filtro de superposición de doble toque está oculto. - Ocultar filtro de superposición de doble toque - El botón de descargar está visible. - El botón de descargar está oculto. - Ocultar botón de descargar - Las tarjetas de la pantalla final están visibles. - Las tarjetas de la pantalla final están ocultas. - Ocultar tarjetas de pantalla final - Las fichas ampliables están visibles. - Las fichas ampliables están ocultas. - Ocultar fichas ampliables bajo los vídeos - Los estantes ampliables están visibles. - Los estantes ampliables están ocultos. - Ocultar estantes ampliables - El botón de subtítulos está visible. - El botón de subtítulos está oculto. - Ocultar botón de subtítulos del feed - Lista de nombres del menú desplegable a filtrar separados por una nueva línea. - Filtro del menú desplegable del feed - El filtro del menú desplegable del feed está desactivado. - El filtro del menú desplegable del feed está activado. - Activar filtro del menú desplegable del feed - La barra de búsqueda del feed está visible. - La barra de búsqueda del feed está oculta. - Ocultar barra de búsqueda del feed - Las encuestas del feed están visibles. - Las encuestas del feed están ocultas. - Ocultar encuestas del feed - La superposición de la tira de película está visible. - La superposición de la tira de película está oculta. - Ocultar superposición de tira de película - El botón flotante está visible. - El botón flotante está oculto. - Ocultar botón flotante - El botón flotante del micrófono está visible. - El botón flotante del micrófono está oculto. - Ocultar botón flotante del micrófono - Las estanterías Para Ti están visibles. - Las estanterías Para Ti están ocultas. - Ocultar estanterías Para Ti - Los anuncios en pantalla completa están visibles. - Los anuncios en pantalla completa están ocultos. - Ocultar anuncios en pantalla completa - "Los anuncios en pantalla completa están bloqueados. - -Limitación: la imagen de la publicación de la comunidad en pantalla completa puede estar bloqueada." - Los anuncios en pantalla completa se cierran mediante el botón de cerrar. - Cerrar anuncios en pantalla completa - Los anuncios generales están visibles. - Los anuncios generales están ocultos. - Ocultar anuncios generales - La promoción de YouTube Premium está visible. - La promoción de YouTube Premium está oculta. - Ocultar promoción de YouTube Premium - Los separadores grises están visibles. - Los separadores grises están ocultos. - Ocultar separadores grises - El nombre de usuario está visible. - El nombre de usuario está oculto. - Ocultar nombre de usuario - El botón de búsqueda de imágenes está visible. - El botón de búsqueda de imágenes está oculto. - Ocultar botón de búsqueda de imágenes - Los estantes de imágenes están visibles. - Los estantes de imágenes están ocultos. - Ocultar estantes de imágenes - Las secciones de las tarjetas de información están visibles. - Las secciones de las tarjetas de información están ocultas. - Ocultar secciones de tarjetas de información - Las tarjetas de información están visibles. - Las tarjetas de información están ocultas. - Ocultar tarjetas de información - Los paneles de información están visibles. - Los paneles de información están ocultos. - Ocultar paneles de información - El botón de unirme está visible. - El botón de unirme está oculto. - Ocultar botón de unirme - La sección de conceptos clave está visible. - La sección de conceptos clave está oculta. - Ocultar sección de conceptos clave - "El inicio / las suscripciones / los resultados de búsqueda se filtran para ocultar los contenidos que coinciden con frases de palabras clave. - -Limitaciones: -• Algunos Shorts pueden no estar ocultos. -• Algunos componentes de la interfaz pueden no estar ocultos. -• La búsqueda de una palabra clave puede no mostrar resultados." - Acerca del filtrado de palabras clave - Al rodear una palabra o frase clave con comillas dobles se evitarán las coincidencias parciales de títulos de vídeo y nombres de canales.<br><br>Por ejemplo:<br><b>\"ia\"</b> ocultará el vídeo: <b>¿Cómo funciona la IA?</b><br>pero no ocultará: <b>¿Cómo funciona la justicia?</b> - Coincidir palabras completas - Los comentarios no están filtrados. - Los comentarios están filtrados. - Ocultar comentarios por palabras clave - Los vídeos en el feed de inicio no están filtrados. - Los vídeos en el feed de inicio están filtrados. - Ocultar vídeos de inicio por palabras clave - "Palabras clave y frases a ocultar, separadas por nuevas líneas. -Las palabras con letras mayúsculas en el medio deben introducirse con las mayúsculas (p. ej.: iPhone, TikTok, LeBlanc)." - Palabras clave a ocultar - Los resultados de búsqueda no están filtrados. - Los resultados de búsqueda están filtrados. - Ocultar resultados de búsqueda por palabras clave - Los vídeos en el feed de suscripciones no están filtrados. - Los vídeos en el feed de suscripciones están filtrados. - Ocultar vídeos de suscripciones por palabras clave - La palabra clave \"%1$s\" es demasiado amplia y ocultará todos los vídeos - Palabra clave no válida. No se puede utilizar: \"%s\" como filtro - Añadir comillas para utilizar la palabra clave: %s. - La palabra clave tiene declaraciones contradictoras: %s. - La palabra clave es demasiado corta y requiere comillas: %s. - Las últimas publicaciones están visibles. - Las últimas publicaciones están ocultas. - Ocultar últimas publicaciones - El botón de últimos vídeos está visible. - El botón de últimos vídeos está oculto. - Ocultar botón de últimos vídeos - Los botones de me gusta y no me gusta están visibles. - Los botones de me gusta y no me gusta están ocultos. - Ocultar botones de me gusta y no me gusta - Los mensajes de chat en directo están visibles.\n\nEste ajuste se aplica también a los vídeos en directo de Shorts. - Los mensajes de chat en directo están ocultos.\n\nEste ajuste se aplica también a los vídeos en directo de Shorts. - Ocultar mensajes de chat en directo - Se muestra el botón de repetición del Live Chat.\n\nAparece en pantalla completa al cerrar el Live Chat. - El botón de repetición de Live Chat está oculto.\n\nAparece en pantalla completa al cerrar el Live Chat. - Ocultar botón de repetición de chat en directo - Oculta los vídeos con menos de 1.000 visualizaciones de los feeds de inicio que hayan sido subidos desde canales a los que no estás suscrito. - Ocultar vídeos con pocas visualizaciones - Los paneles médicos están visibles. - Los paneles médicos están ocultos. - Ocultar paneles médicos - Los estantes de mercancía están visibles. - Los estantes de mercancía están ocultos. - Ocultar estantes de mercancía - La lista de reproducción Mix está visible. - La lista de reproducción Mix está oculta. - Ocultar lista de reproducción Mix - Los estantes de películas están visibles. - Los estantes de películas están ocultos. - Ocultar estantes de películas - La barra de navegación está visible. - La barra de navegación está oculta. - Ocultar barra de navegación - El botón de crear está visible. - El botón de crear está oculto. - Ocultar botón de crear - El botón de inicio está visible. - El botón de inicio está oculto. - Ocultar botón de inicio - La etiqueta de navegación está visible. - La etiqueta de navegación está oculta. - Ocultar etiqueta de navegación - El botón de biblioteca está visible. - El botón de biblioteca está oculto. - Ocultar botón de biblioteca - El botón de notificaciones está visible. - El botón de notificaciones está oculto. - Ocultar botón de notificaciones - El botón de Shorts está visible. - El botón de Shorts está oculto. - Ocultar botón de Shorts - El botón de suscripciones está visible. - El botón de suscripciones está oculto. - Ocultar botón de suscripciones - El botón de notificarme está visible. - El botón de notificarme está oculto. - Ocultar botón de notificarme - La etiqueta de promoción pagada está visible. - La etiqueta de promoción pagada está oculta. - Ocultar etiqueta de promoción pagada - Los reproducibles están visibles. - Los reproducibles están ocultos. - Ocultar reproducibles - El botón de reproducción automática está visible. - El botón de reproducción automática está oculto. - Ocultar botón de reproducción automática - El botón de subtítulos está visible. - El botón de subtítulos está oculto. - Ocultar botón de subtítulos - El botón de transmitir está visible. - El botón de transmitir está oculto. - Ocultar botón de transmitir - El botón de contraer está visible. - El botón de contraer está oculto. - Ocultar botón de contraer - El menú \"Modo ambiente\" está visible. - El menú \"Modo ambiente\" está oculto. - Ocultar menú \"Modo ambiente\" - El menú \"Pista de audio\" está visible. - El menú \"Pista de audio\" está oculto. - Ocultar menú \"Pista de audio\" - El pie de página del menú de subtítulos está visible. - El pie de página del menú de subtítulos está oculto. - Ocultar pie de página del menú de subtítulos - El menú \"Subtítulos\" está visible. - El menú \"Subtítulos\" está oculto. - Ocultar menú \"Subtítulos\" - El menú \"1080p Premium\" está visible. - El menú \"1080p Premium\" está oculto. - Ocultar menú \"1080p Premium\" - El menú \"Ayuda y comentarios\" está visible. - El menú \"Ayuda y comentarios\" está oculto. - Ocultar menú \"Ayuda y comentarios\" - El menú \"Escuchar con YouTube Music\" está visible. - El menú \"Escuchar con YouTube Music\" está oculto. - Ocultar menú \"Escuchar con YouTube Music\" - El menú \"Bloquear pantalla\" está visible. - El menú \"Bloquear pantalla\" está oculto. - Ocultar menú \"Bloquear pantalla\" - El menú \"Reproducción en bucle\" está visible. - El menú \"Reproducción en bucle\" está oculto. - Ocultar menú \"Reproducción en bucle\" - El menú \"Más información\" está visible. - El menú \"Más información\" está oculto. - Ocultar menú \"Más información\" - El menú \"Imagen en imagen\" está visible. - El menú \"Imagen en imagen\" está oculto. - Ocultar menú \"Imagen en imagen\" - El menú \"Velocidad de reproducción\" está visible. - El menú \"Velocidad de reproducción\" está oculto. - Ocultar menú \"Velocidad de reproducción\" - El menú \"Controles premium\" está visible. - El menú \"Controles premium\" está oculto. - Ocultar menú \"Controles premium\" - El pie de página del menú de calidad está visible. - El pie de página del menú de calidad está oculto. - Ocultar pie de página del menú de calidad - La cabecera del menú de calidad está visible. - La cabecera del menú de calidad está oculta. - Ocultar cabecera del menú de calidad - El menú \"Denunciar\" está visible. - El menú \"Denunciar\" está oculto. - Ocultar menú \"Denunciar\" - El menú \"Temporizador\" está visible. - El menú \"Temporizador\" está oculto. - Ocultar menú \"Temporizador\" - El menú \"Regular volumen\" está visible. - El menú \"Regular volumen\" está oculto. - Ocultar menú \"Regular volumen\" - El menú \"Estadísticas para nerds\" está visible. - El menú \"Estadísticas para nerds\" está oculto. - Ocultar menú \"Estadísticas para nerds\" - El menú \"Ver en realidad virtual\" está visible. - El menú \"Ver en realidad virtual\" está oculto. - Ocultar menú \"Ver en realidad virtual\" - El botón de pantalla completa está visible. - El botón de pantalla completa está oculto. - Ocultar botón de pantalla completa - Los botones están visibles. - Los botones están ocultos. - Ocultar botones de anterior y siguiente - El estante de compras está visible. - El estante de compras está oculto. - Ocultar estante de compras del reproductor - El botón de YouTube Music está visible. - El botón de YouTube Music está oculto. - Ocultar botón de YouTube Music - El botón de guardar está visible. - El botón de guardar está oculto. - Ocultar botón de guardar - Las secciones de podcast están visibles. - Las secciones de podcast están ocultas. - Ocultar secciones de podcast - La vista previa de comentarios está visible. - La vista previa de comentarios está oculta. - Ocultar vista previa de comentarios - Esto cambia el tamaño de la sección de comentarios, por lo que es imposible abrir una repetición del chat en directo en la sección de comentarios. - Esto no cambia el tamaño de la sección de comentarios, por lo que es posible abrir la repetición del chat en directo en la sección de comentarios. - Ocultar vista previa de tipo de comentarios - El banner de alerta de promoción está visible. - El banner de alerta de promoción está oculto. - Ocultar banner de alerta de promoción - El botón de comentarios está visible. - El botón de comentarios está oculto. - Ocultar botón de comentarios - El botón de no me gusta está visible. - El botón de no me gusta está oculto. - Ocultar botón de no me gusta - El botón de me gusta está visible. - El botón de me gusta está oculto. - Ocultar botón de me gusta - El botón de chat en directo está visible. - El botón de chat en directo está oculto. - Ocultar botón de chat en directo - El botón de más está visible. - El botón de más está oculto. - Ocultar botón de más - El botón de abrir lista de reproducción Mix está visible. - El botón de abrir lista de reproducción Mix está oculto. - Ocultar botón de abrir lista de reproducción Mix - El botón de abrir lista de reproducción está visible. - El botón de abrir lista de reproducción está oculto. - Ocultar botón de abrir lista de reproducción - El botón de guardar está visible. - El botón de guardar está oculto. - Ocultar botón de guardar - El botón de compartir está visible. - El botón de compartir está oculto. - Ocultar botón de compartir - El contenedor de acciones rápidas está visible. - El contenedor de acciones rápidas está oculto. - Ocultar contenedor de acciones rápidas - "Oculta los siguientes vídeos recomendados: - -• Vídeos con etiqueta \"Solo para miembros\". -• Vídeos con frases como \"La gente también vio\" en la parte inferior del vídeo. -• Vídeos subidos desde canales a los que no estás suscrito y que tienen menos de 1,000 visualizaciones." - Ocultar vídeos recomendados - La superposición de vídeo relacionado está visible. - La superposición de vídeo relacionado está oculta. - Ocultar superposición de vídeo relacionado - Los vídeos relacionados están visibles. - Los vídeos relacionados están ocultos. - Ocultar vídeos relacionados - "Este ajuste limita el número máximo de diseños que se pueden cargar en la pantalla del reproductor. - -Si el diseño de la pantalla del reproductor cambia debido a cambios en el servidor, es posible que se oculten diseños no deseados en la pantalla del reproductor." - El botón de remix está visible. - El botón de remix está oculto. - Ocultar botón de remix - El botón de denunciar está visible. - El botón de denunciar está oculto. - Ocultar botón de denunciar - El botón de recompensas está visible. - El botón de recompensas está oculto. - Ocultar botón de recompensas - Las miniaturas en el historial de términos de búsqueda están visibles. - Las miniaturas en el historial de términos de búsqueda están ocultas. - Ocultar miniaturas en términos de búsqueda - El mensaje al desplazarse está visible. - El mensaje al desplazarse está oculto. - Ocultar mensaje al desplazarse - El mensaje al deshacer desplazamiento está visible. - El mensaje al deshacer desplazamiento está oculto. - Ocultar mensaje al deshacer desplazamiento - Las etiquetas de los capítulos junto a la marca de tiempo están visibles. - Las etiquetas de los capítulos junto a la marca de tiempo están ocultas. - Ocultar etiquetas de capítulos en barra de progreso - La barra de progreso en el reproductor de vídeo está visible. - La barra de progreso en el reproductor de vídeo está oculta. - La barra de progreso en miniaturas está visible. - La barra de progreso en miniaturas está oculta. - Ocultar barra de progreso en miniaturas de vídeo - Ocultar barra de progreso en reproductor de vídeo - Las tarjetas autopatrocinadas están visibles. - Las tarjetas autopatrocinadas están ocultas. - Ocultar tarjetas autopatrocinadas - El menú \"Información\" está visible. - El menú \"Información\" está oculto. - Ocultar menú \"Información\" - El menú \"Accesibilidad\" está visible. - El menú \"Accesibilidad\" está oculto. - Ocultar menú \"Accesibilidad\" - El menú \"Cuenta\" está visible. - El menú \"Cuenta\" está oculto. - Ocultar menú \"Cuenta\" - El menú \"Reproducción automática\" está visible. - El menú \"Reproducción automática\" está oculto. - Ocultar menú \"Reproducción automática\" - El menú \"Facturación y pagos\" está visible. - El menú \"Facturación y pagos\" está oculto. - Ocultar menú \"Facturación y pagos\" - El menú \"Subtítulos\" está visible. - El menú \"Subtítulos\" está oculto. - Ocultar menú \"Subtítulos\" - El menú \"Aplicaciones conectadas\" está visible. - El menú \"Aplicaciones conectadas\" está oculto. - Ocultar menú \"Aplicaciones conectadas\" - El menú \"Ahorro de datos\" está visible. - El menú \"Ahorro de datos\" está oculto. - Ocultar menú \"Ahorro de datos\" - El menú \"General\" está visible. - El menú \"General\" está oculto. - Ocultar menú \"General\" - El menú \"Gestionar todo el historial\" está visible. - El menú \"Gestionar todo el historial\" está oculto. - Ocultar menú \"Gestionar todo el historial\" - El menú \"Chat en directo\" está visible. - El menú \"Chat en directo\" está oculto. - Ocultar menú \"Chat en directo\" - El menú \"Notificaciones\" está visible. - El menú \"Notificaciones\" está oculto. - Ocultar menú \"Notificaciones\" - El menú \"Segundo plano\" está visible. - El menú \"Segundo plano\" está oculto. - Ocultar menú \"Segundo plano\" - El menú \"Ver en la televisión\" está visible. - El menú \"Ver en la televisión\" está oculto. - Ocultar menú \"Ver en la televisión\" - El menú \"Centro Familiar\" está visible. - El menú \"Centro Familiar\" está oculto. - Ocultar menú \"Centro Familiar\" - El menú \"Prueba las nuevas funciones experimentales\" está visible. - El menú \"Prueba las nuevas funciones experimentales\" está oculto. - Ocultar menú \"Prueba las nuevas funciones experimentales\" - El menú \"Privacidad\" está visible. - El menú \"Privacidad\" está oculto. - Ocultar menú \"Privacidad\" - El menú \"Compras y suscripciones\" está visible. - El menú \"Compras y suscripciones\" está oculto. - Ocultar menú \"Compras y suscripciones\" - Ocultar elementos del menú de configuración de YouTube. - Ocultar menú de configuración de YouTube - El menú \"Preferencias de calidad de vídeo\" está visible. - El menú \"Preferencias de calidad de vídeo\" está oculto. - Ocultar menú \"Preferencias de calidad de vídeo\" - El menú \"Tus datos en YouTube\" está visible. - El menú \"Tus datos en YouTube\" está oculto. - Ocultar menú \"Tus datos en YouTube\" - El botón de compartir está visible. - El botón de compartir está oculto. - Ocultar botón de compartir - El botón de comprar está visible. - El botón de comprar está oculto. - Ocultar botón de comprar - Los enlaces de compra están visibles. - Los enlaces de compra están ocultos. - Ocultar enlaces de compra - La barra de canales está visible. - La barra de canales está oculta. - Ocultar barra de canales - El botón de comentarios está visible. - El botón de comentarios está oculto. - Ocultar botón de comentarios - El botón de no me gusta está visible. - El botón de no me gusta está oculto. - Ocultar botón de no me gusta - "Los botones flotantes como \"Utilizar este sonido\" se muestran en la pestaña Shorts del canal." - "Los botones flotantes como \"Utilizar este sonido\" se ocultan en la pestaña Shorts del canal." - Ocultar botón flotante - La etiqueta de enlace de vídeo está visible. - La etiqueta de enlace de vídeo está oculta. - Ocultar etiqueta de enlace de vídeo completo - El botón de la pantalla verde está visible. - El botón de la pantalla verde está oculto. - Ocultar botón de pantalla verde - Los paneles de información están visibles. - Los paneles de información están ocultos. - Ocultar paneles de información - El botón de unirme está visible. - El botón de unirme está oculto. - Ocultar botón de unirme - El botón de me gusta está visible. - El botón de me gusta está oculto. - Ocultar botón de me gusta - La cabecera del chat en directo está visible.\n\nEl botón de volver atrás en la cabecera no se ocultará. - La cabecera del chat en directo está oculta.\n\nEl botón de volver atrás en la cabecera no se ocultará. - Ocultar cabecera del chat en directo - El botón de ubicación está visible. - El botón de ubicación está oculto. - Ocultar botón de ubicación - La barra de navegación está visible. - La barra de navegación está oculta. - Ocultar barra de navegación - La etiqueta de promoción pagada está visible. - La etiqueta de promoción pagada está oculta. - Ocultar etiqueta de promoción pagada - La cabecera pausada está visible. - La cabecera pausada está oculta. - Ocultar cabecera pausada - Los botones superpuestos en pausa están visibles. - Los botones superpuestos en pausa están ocultos. - Ocultar botones superpuestos en pausa - El fondo del botón está visible. - El fondo del botón está oculto. - Ocultar fondo del botón de reproducir y pausar - El botón de remix está visible. - El botón de remix está oculto. - Ocultar botón de remix - El botón de guardar música está visible. - El botón de guardar música está oculto. - Ocultar botón de guardar música - El botón de sugerencias de búsqueda está visible. - El botón de sugerencias de búsqueda está oculto. - Ocultar botón de sugerencias de búsqueda - El botón de compartir está visible. - El botón de compartir está oculto. - Ocultar botón de compartir - Visible en el canal. - "Oculto en el canal. - -Información: -• Solo se ocultan las estanterías con la cabecera Shorts en la pestaña de inicio." - Ocultar en canal - Visible en el historial de reproducciones. - Oculto en el historial de reproducciones. - Ocultar en historial de reproducciones - Visible en el feed de inicio y los vídeos relacionados. - Oculto en el feed de inicio y los vídeos relacionados. - Ocultar en feed de inicio y vídeos relacionados - Visible en los resultados de búsqueda. - Oculto en los resultados de búsqueda. - Ocultar en resultados de búsqueda - Visible en el feed de suscripciones. - Oculto en el feed de suscripciones. - Ocultar en feed de suscripciones - "Oculta los estantes de Shorts. - -Limitación: las cabeceras oficiales en los resultados de búsqueda estarán ocultas." - Ocultar estantes de Shorts - El botón de comprar está visible. - El botón de comprar está oculto. - Ocultar botón de comprar - El botón de compras está visible. - El botón de compras está oculto. - Ocultar botón de compras - El botón de sonido está visible. - El botón de sonido está oculto. - Ocultar botón de sonido - La etiqueta de metadatos está visible. - La etiqueta de metadatos está oculta. - Ocultar etiqueta de metadatos de sonido - Los stickers están visibles. - Los stickers están ocultos. - Ocultar stickers - El botón de suscribirse está visible. - El botón de suscribirse está oculto. - Ocultar botón de suscribirse - El botón de súper gracias está visible. - El botón de súper gracias está oculto. - Ocultar botón de súper gracias - Los productos etiquetados están visibles. - Los productos etiquetados están ocultos. - Ocultar productos etiquetados - La barra de herramientas está visible. - La barra de herramientas está oculta. - Ocultar barra de herramientas - El botón de tendencias está visible. - El botón de tendencias está oculto. - Ocultar botón de tendencias - El botón de utilizar plantilla está visible. - El botón de utilizar plantilla está oculto. - Ocultar botón de utilizar plantilla - El botón de utilizar este sonido está visible. - El botón de utilizar este sonido está oculto. - Ocultar botón de utilizar este sonido - El título está visible. - El título está oculto. - Ocultar título de vídeo - El botón de mostrar más está visible. - El botón de mostrar más está oculto. - Ocultar botón de mostrar más - La barra de notificaciones está visible. - La barra de notificaciones está oculta. - Ocultar barra de notificaciones - El botón de iniciar prueba está visible. - El botón de iniciar prueba está oculto. - Ocultar botón de iniciar prueba - El carrusel de suscripciones está visible. - El carrusel de suscripciones está oculto. - Ocultar carrusel de suscripciones - Las acciones sugeridas están visibles. - Las acciones sugeridas están ocultas. - Ocultar acciones sugeridas - "Este ajuste ha quedado obsoleto. - -En su lugar, utiliza la configuración \"Configuración → Reproducción automática → Reproducción automática del siguiente vídeo\"." - La pantalla final del vídeo sugerido está visible. - "La pantalla final del vídeo sugerido se oculta cuando la reproducción automática está desactivada. - -La reproducción automática se puede cambiar en la configuración de YouTube: -\"Configuración → Reproducción automática → Reproducción automática del siguiente vídeo\"" - Ocultar vídeo sugerido en pantalla final - El botón de gracias está visible. - El botón de gracias está oculto. - Ocultar botón de gracias - Los estantes de tickets están visibles. - Los estantes de tickets están ocultos. - Ocultar estantes de tickets - La marca de tiempo está visible. - La marca de tiempo está oculta. - Ocultar marca de tiempo - Las reacciones cronometradas están visibles. - Las reacciones cronometradas están ocultas. - Ocultar reacciones cronometradas - El botón de transmitir está visible. - El botón de transmitir está oculto. - Ocultar botón de transmitir - El botón de crear está visible. - El botón de crear está oculto. - Ocultar botón de crear - El botón de notificaciones está visible. - El botón de notificaciones está oculto. - Ocultar botón de notificaciones - Las secciones de transcripción están visibles. - Las secciones de transcripción están ocultas. - Ocultar secciones de transcripción - Los anuncios de vídeo están visibles. - Los anuncios de vídeo están ocultos. - Ocultar anuncios de vídeo - "El inicio / Las suscripciones / Los resultados de búsqueda se filtran para ocultar los vídeos con visualizaciones inferiores o superiores a un número determinado. - -Limitaciones: -• Los Shorts no se pueden ocultar. -• Los vídeos con 0 visualizaciones no se filtran." - Acerca del filtrado del contador de visualizaciones - Los vídeos en el feed de inicio no están filtrados. - Los vídeos en el feed de inicio están filtrados. - Ocultar vídeos de inicio por visualizaciones - Los resultados de búsqueda no están filtrados. - Los resultados de búsqueda están filtrados. - Ocultar resultados de búsqueda por visualizaciones - Los vídeos en el feed de suscripciones no están filtrados. - Los vídeos en el feed de suscripciones están filtrados. - Ocultar vídeos de suscripciones por visualizaciones - Oculta vídeos recomendados con menos de un número determinado de visualizaciones. - Ocultar vídeos recomendados por visualizaciones - Los vídeos con visualizaciones mayores que este número serán ocultados. - Visualizaciones mayores - Los vídeos con visualizaciones menores que este número serán ocultados. - Visualizaciones menores - K -> 1 000\nM -> 1 000 000\nB -> 1 000 000 000\nvisualizaciones -> views - Especifica tu plantilla de idioma para el número de visualizaciones mostradas debajo de cada vídeo en la interfaz de usuario. Cada clave (una letra/palabra en tu idioma) -> valor (significado de la clave) debe estar en una nueva línea. Las claves van antes del signo \"->\". Si cambias el idioma de la aplicación o del sistema tienes que restablecer este ajuste.\n\nEjemplos:\nInglés: 10K views = K -> 1000, views -> views\nEspañol: 10 K visualizaciones = K -> 1000, visualizaciones -> views - Claves de visualización - El banner de ver productos está visible. - El banner de ver productos está oculto. - Ocultar banner de ver productos - El botón de búsqueda por voz está visible. - El botón de búsqueda por voz está oculto. - Ocultar botón de búsqueda por voz - Los resultados de la búsqueda web están visibles. - Los resultados de la búsqueda web están ocultos. - Ocultar resultados de búsqueda web - Los Doodles de YouTube están visibles. - Los Doodles de YouTube están ocultos. - Ocultar Doodles de YouTube - "Los Doodles de YouTube aparecen unos pocos días al año. - -Si un Doodle de YouTube está actualmente mostrándose en tu región y este ajuste de ocultar está activado, entonces la barra de filtros debajo de la barra de búsqueda también estará oculta." - La superposición del zoom está visible. - La superposición del zoom está oculta. - Ocultar superposición del zoom - Afn azul - Afn rojo - Personalizado - Predeterminado - MMT - Revancify Blue - Revancify Red - YouTube - Mantiene el modo horizontal al apagar y encender la pantalla en pantalla completa. - La cantidad de milisegundos que se fuerza el modo horizontal. - Mantener tiempo de espera del modo horizontal - Mantener modo horizontal - Predeterminada - La acción de doble toque está desactivada. - "La acción de doble toque está activada. - -• Moderno 1: tocar dos veces para cambiar el vídeo minimizado a un tamaño mayor. -• Moderno 2, 3: tocar dos veces para cerrar el vídeo minimizado." - Acción de doble toque - Arrastrar y soltar está desactivado. - Arrastrar y soltar está activado. - Activar arrastrar y soltar - Los botones de expandir y cerrar están visibles. - Los botones están ocultos.\n(pasa el dedo por el minirreproductor para ampliarlo o cerrarlo) - Ocultar botones de expandir y cerrar - Avanzar y retroceder están visibles. - Avanzar y retroceder están ocultos. - Ocultar botones de avanzar y retroceder - Los subtextos están visibles. - Los subtextos están ocultos. - Ocultar subtextos - La opacidad del minirreproductor debe estar entre 0-100. Restablezca a los valores predeterminados. - Valor de opacidad entre 0-100, donde 0 es transparente. - Opacidad de superposición - Original - Teléfono - Tablet - Moderno 1 - Moderno 2 - Moderno 3 - Tipo de minirreproductor - Botón superpuesto - "Pulsa para activar los estados de repetición continua. -Mantén pulsado para pausar después de los estados de repetición." - Mostrar botón de repetición continua - "Pulsa para copiar la URL del vídeo. -Mantén pulsado para copiar la URL del vídeo con la marca de tiempo." - "Pulsa para copiar la URL del vídeo con la marca de tiempo. -Mantén pulsado para copiar la marca de tiempo del vídeo." - Mostrar botón de copiar URL con marca de tiempo - Mostrar botón de copiar URL del vídeo - Pulsa para iniciar el descargador externo. - Mostrar botón de descarga externa - Pulsa para silenciar el volumen del vídeo actual. Pulsa de nuevo para reactivar el sonido. - Mostrar botón de silenciar volumen - Mantén pulsado para cambiar el estado del botón. - Velocidad de reproducción restablecida (1.0x). - "Pulsa para abrir el diálogo de velocidad. -Mantén pulsado para establecer la velocidad de reproducción en 1.0x." - Mostrar botón de diálogo de velocidad - "Pulse para generar una lista de reproducción de todos los vídeos del canal desde el más antiguo hasta el más nuevo. -Toca y mantén para deshacer." - Mostrar botón de lista de reproducción ordenada - \"Toque para abrir el diálogo de la lista blanca. -Toque y mantenga presionado para abrir el diálogo de configuración de la lista blanca. - Mostrar botón de lista blanca - El botón nativo de descarga de listas de reproducción abre el descargador nativo de la aplicación. - El botón nativo de descarga de listas de reproducción abre tu descargador externo. - Reemplazar botón de descarga de listas de reproducción - El botón nativo de descarga de vídeo abre el descargador nativo de la aplicación. - El botón nativo de descarga de vídeo abre tu descargador externo. - Reemplazar botón de descarga de vídeo - Se requiere YouTube Music para reemplazar la acción del botón. Pulsa aquí para descargar YouTube Music. - Requisito previo - El botón de Youtube Music abre la app nativa. - El botón de YouTube Music abre RVX Music. - Reemplazar botón de YouTube Music - Excluidos - Incluidos - Normal - Botones de acción - Ajustes adicionales - Animación / Comentarios - Botón de descarga - Funciones experimentales - Restricciones de región de imágenes - Importar / Exportar como archivo - Importar / Exportar como texto - Filtro de palabras clave - Otros - Botones superpuestos - Información de parches - Acciones rápidas - Vídeos recomendados - Estantes de Shorts - Acciones sugeridas - Herramientas utilizadas - Filtro de contador de visualizaciones - Ocultar o mostrar elementos en el menú de la cuenta y la pestaña Tú. - Menú de cuenta - Ocultar o mostrar botones de acción bajo los vídeos. - Botones de acción - Anuncios - Miniaturas alternativas - Omite las restricciones del modo ambiente o desactiva el modo ambiente. - Modo ambiente - Ocultar o mostrar la barra de categorías en el feed, la búsqueda y vídeos relacionados. - Barra de categorías - Ocultar o mostrar los componentes de la barra de canales bajo los vídeos. - Barra de canales - Ocultar o mostrar los componentes en el perfil del canal. - Perfil del canal - Ocultar o mostrar los componentes de la sección de comentarios. - Comentarios - Ocultar o mostrar las publicaciones de la comunidad en el feed y el canal. - Publicaciones de la comunidad - Oculta componentes utilizando filtros personalizados. - Filtro personalizado - Ocultar o mostrar componentes del menú desplegable en el feed. - Menú desplegable - Feed - Ocultar o cambiar los componentes relacionados con pantalla completa. - Pantalla completa - General - Desactiva o activa la vibración. - Vibración - Reemplaza la acción de clic de los botones dentro de la aplicación. - Botones de enganche - Importar o exportar los ajustes. - Importar / Exportar ajustes - Cambia el estilo del reproductor minimizado de la aplicación. - Minirreproductor - Otros - Ocultar o mostrar los componentes de la sección de la barra de navegación. - Barra de navegación - Información sobre los parches aplicados. - Información de parches - Ocultar o mostrar botones en vídeos. - Botones del reproductor - Ocultar o cambiar componentes del menú desplegable en el reproductor de vídeo. - Menú desplegable - Reproductor - Devolver usuario de YouTube - Return YouTube Dislike - SponsorBlock - Personalizar los componentes de la barra de progreso. - Barra de progreso - Ocultar elementos del menú de configuración de YouTube. - Menú de configuración - Ocultar o mostrar los componentes en el reproductor de Shorts. - Reproductor de Shorts - Shorts - Falsifica los datos de transmisión para evitar problemas de reproducción. - Falsificar datos de transmisión - Controles deslizantes - Ocultar o cambiar los componentes situados en la barra de herramientas, como los botones, la barra de búsqueda o la cabecera. - Barra de herramientas - Ocultar o mostrar los componentes de la descripción del vídeo. - Descripción del vídeo - Ocultar vídeos por palabras clave o visualizaciones. - Filtro de vídeo - Vídeo - Cambiar ajustes relacionados con el historial de reproducciones. - Historial de reproducciones - El margen superior de las acciones rápidas debe estar entre 0-32. Restablezca a los valores predeterminados. - Configura el espacio entre la barra de progreso y el contenedor de acciones rápidas, entre 0-32. - Margen superior de acciones rápidas - "Rechaza fuertemente la respuesta del códec AV1 del software. -Después de unos 20 segundos de búfer, cambia a un códec diferente." - Rechazar respuesta del códec AV1 del software - El proceso de Fallback causa unos 20 segundos de búfer. - Desplazamiento - Los cambios de velocidad de reproducción solo se aplican al vídeo actual. - Los cambios de velocidad de reproducción se aplican a todos los vídeos. - Recordar cambios de velocidad de reproducción - No se mostrará un mensaje al cambiar la velocidad de reproducción predeterminada. - Se mostrará un mensaje al cambiar la velocidad de reproducción predeterminada. - Mostrar un mensaje - Cambiando la velocidad predeterminada a %s. - Los cambios de calidad solo se aplican al vídeo actual. - Los cambios de calidad se aplican a todos los vídeos. - Recordar cambios de calidad de vídeo - No se mostrará un mensaje al cambiar la calidad de vídeo predeterminada. - Se mostrará un mensaje al cambiar la calidad de vídeo predeterminada. - Mostrar un mensaje - Cambiando la calidad predeterminada con datos móviles a %s. - Error al establecer la calidad de vídeo. - Cambiando la calidad predeterminada con Wi-Fi a %s. - "Elimina el diálogo de discreción del espectador. -Esto no evita la restricción de edad. Solo la acepta automáticamente." - Eliminar diálogo de discreción del espectador - Reemplaza el códec AV1 del software con el códec VP9. - Reemplazar códec AV1 del software - Se utiliza el nombre de usuario del canal. - Se utiliza el nombre del canal. - Reemplazar nombre de usuario del canal - Pulsa para mostrar el tiempo restante. - Pulsa para abrir el menú desplegable de velocidad de reproducción o de calidad de vídeo. - Reemplazar acción de marca de tiempo - Reemplaza el botón de crear con el botón de ajustes. - Reemplazar botón de crear - "Pulsa para abrir la configuración de YouTube. -Mantén pulsado para abrir los ajustes de RVX." - "Pulsa para abrir los ajustes de RVX. -Mantén pulsado para abrir la configuración de YouTube." - Tipo de acción a asignar al botón - Las miniaturas de la barra de progreso aparecerán en pantalla completa. - Las miniaturas de la barra de progreso aparecerán encima de la barra de progreso. - Restaurar antiguas miniaturas de barra de progreso - El antiguo menú de calidad de vídeo está oculto. - El antiguo menú de calidad de vídeo está visible. - Restaurar antiguo menú de calidad de vídeo - \@identificador (Nombre de usuario) - Formato de visualización - Nombre de usuario (@identificador) - Nombre de usuario - Se utiliza el identificador. - Se utiliza el nombre de usuario. - Activar devolver usuario de YouTube - "Se requiere la clave de desarrollador de la API v3 de datos de YouTube para reemplazar el identificador con el nombre de usuario. - -La cuota diaria para las claves de API en el plan gratuito es de 10,000, y se utiliza 1 cuota para reemplazar el identificador con el nombre de usuario en 1 comentario. - -Toca para ver cómo crear una clave de API." - Acerca de la clave API de datos de YouTube - La clave de desarrollador para utilizar la API v3 de datos de YouTube. - Clave de API de datos de YouTube - 1. Ve a <a href=%1$s>Crear un nuevo proyecto</a>.<br>2. Pulsa en el botón <b>CREAR</b>.<br>3. 3. Ve a <a href=%2$s>API v3 de datos de YouTube</a>.<br>4. Pulsa en el botón <b>HABILITAR</b>.<br>5. Pulsa en <b>CREAR</b>. Pulsa en el botón <b>CREAR CREDENCIALES</b>.<br>6. Selecciona la opción <b>Datos públicos</b>.<br>7. Pulsa en el botón <b> SIGUIENTE</b>.<br>8. Copia la clave API.<br><br>※ La clave API nunca debe ser compartida con otros, por lo que no se incluye en los ajustes de Importar / Exportar. - Crear clave de desarrollador de API v3 de datos de YouTube - Información - Los datos de no me gusta son proporcionados por la API de Return YouTube Dislike. -Pulsa aquí para obtener más información. - ReturnYouTubeDislike.com - El botón de me gusta está diseñado para una mejor apariencia. - El botón de me gusta está diseñado para un ancho mínimo. - Botón compacto de me gusta - Los no me gusta se muestran como número. - Los no me gusta se muestran como porcentaje. - Porcentaje de no me gusta - Los no me gusta están ocultos. - Los no me gusta están visibles. - Activar Return YouTube Dislike - Los me gusta estimados están ocultos. - Los me gusta estimados están visibles. - Activar me gusta estimados - Los no me gusta no están disponibles (se alcanzó el límite de la API del cliente). - Los no me gusta no están disponibles (estado %d). - Los no me gusta están temporalmente no disponibles (la API no responde). - Los no me gusta no están disponibles (%s). - Recargar vídeo para votar utilizando Return YouTube Dislike - Los no me gusta están ocultos en los Shorts. - Los no me gusta están visibles en los Shorts. - "Los no me gusta están visibles en los Shorts. - -Limitación: es posible que los no me gusta no aparezcan en modo incógnito." - Mostrar no me gusta en Shorts - No se muestra el mensaje si Return YouTube Dislike no está disponible. - Se muestra el mensaje si Return YouTube Dislike no está disponible. - Mostrar mensaje si la API no está disponible - Oculto - Elimina los parámetros de consulta de seguimiento de las URL al compartir enlaces. - Desinfectar enlaces compartidos - "Frases como \"#\", \"Tienda\" y \"N productos\" se muestran en los subtítulos de vídeo." - "Frases como \"#\", \"Tienda\" y \"N productos\" se ocultan en los subtítulos de vídeo." - Desinfectar subtítulos de vídeo - Información - sponsor.ajay.app - Los datos son proporcionados por la API de SponsorBlock. Pulsa aquí para aprender más y ver las descargas para otras plataformas. - La URL de la API fue cambiada. - La URL de la API no es válida. - La URL de la API fue restablecida. - Apariencia - Color cambiado. - Color: - Código de color no válido. - Color restablecido. - Creación de nuevos segmentos - Cambiar comportamiento de segmentos - Ocultar automáticamente el botón de omitir - El botón de omitir se muestra durante todo el segmento. - El botón de omitir se oculta después de unos segundos. - Usar botón compacto de omitir - El botón de omitir está diseñado para una mejor apariencia. - El botón de omitir está diseñado para una anchura mínima. - Mostrar botón de crear nuevo segmento - El botón de crear nuevo segmento está oculto. - El botón de crear nuevo segmento está visible. - Activar SponsorBlock - SponsorBlock es un sistema colaborativo para omitir partes molestas en vídeos de YouTube. - Mostrar botón de votación - El botón de votación del segmento no está visible. - El botón de votación del segmento está visible. - General - Ajuste de nuevo segmento - El valor debe ser un número positivo. - Número de milisegundos que se mueven los botones de ajuste de tiempo al crear nuevos segmentos. - Cambiar URL de API - La dirección que SponsorBlock utiliza para hacer llamadas al servidor. - Duración mínima del segmento - Duración de tiempo no válida. - Los segmentos más cortos que este valor (en segundos) no serán mostrados o omitidos. - Activar seguimiento del conteo de omisiones - El seguimiento del conteo de omisiones no está activado. - Permite que la tabla de clasificación de SponsorBlock sepa cuánto tiempo se ha ahorrado. Se envía un mensaje a la tabla de clasificación cada vez que se omite un segmento. - Mostrar mensaje al omitir segmento automáticamente - Mensaje no mostrado. Pulse aquí para ver un ejemplo. - Mensaje que se muestra cuando un segmento se omite automáticamente. Pulse aquí para ver un ejemplo. - Mostrar duración del vídeo sin segmentos - La duración completa del vídeo está visible. - Duración del vídeo sin todos los segmentos, que se muestran entre paréntesis junto a la duración completa del vídeo. - Tu ID de usuario privado - El ID de usuario privado debe tener al menos 30 caracteres. - Esto debe mantenerse en privado. Es como una contraseña y no debe compartirse con nadie. Si alguien lo tiene, puede hacerse pasar por ti. - Ya leídas - Lee las directrices de SponsorBlock antes de crear nuevos segmentos. - Muéstrame - Sigue las directrices - Las directrices contienen reglas y consejos para la creación de nuevos segmentos. - Ver directrices - Selecciona la categoría del segmento - El segmento dura de %1$02d:%2$02d hasta %3$02d:%4$02d (%5$d minutos %6$02d segundos)\n¿Está listo para ser enviado? - El segmento es desde\n\n%1$s\nhasta\n%2$s\n\n(%3$s)\n\n¿Está listo para ser enviado? - ¿Son correctos los tiempos? - La categoría está desactivada en los ajustes. Activa la categoría para enviar. - ¿Quieres editar el tiempo para el inicio o el final del segmento? - Tiempo no válido. - Editar tiempo del segmento manualmente - ¿Establecer %s como el inicio o el final de un nuevo segmento? - final - Primero marca dos ubicaciones en la barra de tiempo. - inicio - ahora - Previsualiza el segmento y asegúrate de que se omite sin problemas. - El inicio debe ser antes del final. - Tiempo en que finaliza el segmento - Tiempo en que inicia el segmento - Nuevo segmento de SponsorBlock - Restablecer - Restablecer color - Tangente de relleno / Chistes - Escenas tangenciales añadidas solo para relleno o humor que no son necesarias para entender el contenido principal del vídeo. No incluye segmentos que proporcionen contexto o detalles de fondo. - Destacado - La parte del vídeo que la mayoría de la gente busca. - Recordatorio de interacción (suscripción) - Un breve recordatorio para que des me gusta, te suscribas o les sigas en medio del contenido. Si es largo o trata sobre algo específico, debería ir en la sección de promoción propia. - Intermedio / Animación de introducción - Un intervalo sin contenido real. Puede ser una pausa, un fotograma estático o una animación que se repite. No incluye transiciones que contengan información. - Música: sección sin música - Solo para utilizar en vídeos musicales. Secciones de vídeos musicales sin música, que no estén ya cubiertas por otra categoría. - Tarjetas finales / Créditos - Créditos o cuando aparecen las tarjetas finales de YouTube. No es para conclusiones con información. - Adelanto / Resumen / Enganche - Colección de clips que muestran lo que está por venir o lo que sucedió en el vídeo o en otros vídeos de una serie, donde toda la información se repite en otra parte. - Promoción propia / no pagada - Similar a \"Patrocinador\", excepto cuando se trata de promoción propia o no pagada. Incluye secciones sobre mercancía, donaciones o información sobre con quién colaboraron. - Patrocinador - Promoción pagada, referencias pagadas y anuncios directos. No es para promoción propia ni para menciones gratuitas a causas, creadores, sitios web o productos que les gusten. - Copiar - Error al exportar: %s. - Importar / Exportar ajustes - Tus ajustes de SponsorBlock en formato JSON que pueden ser importados / exportados a ReVanced Extended y otras plataformas de SponsorBlock. - Tus ajustes de SponsorBlock en formato JSON que pueden ser importados / exportados a ReVanced Extended y otras plataformas de SponsorBlock. Esto incluye tu ID de usuario privado. Asegúrate de compartir esto sabiamente. - Error al importar: %s. - Ajustes importados correctamente. - Tus ajustes contienen un ID de usuario privado de SponsorBlock.\n\nTu ID de usuario es como una contraseña y nunca debe compartirse.\n - No volver a mostrar - Ajustes copiados en el portapapeles. - Omitir automáticamente - Omitir automáticamente una vez - Omitir - Destacado - Omitir relleno - Ir a destacado - Omitir interacción - Omitir introducción - Omitir intermedio - Omitir intermedio - Omitir sin música - Omitir créditos - Omitir adelanto - Omitir resumen - Omitir adelanto - Omitir promoción - Omitir patrocinador - Omitir segmento - Desactivado - Mostrar en barra de progreso - Mostrar botón de omitir - Relleno omitido. - Saltado a destacado. - Recordatorio molesto omitido. - Introducción omitida. - Intermedio omitido. - Intermedio omitido. - Varios segmentos omitidos. - Sección sin música omitida. - Créditos omitidos. - Adelanto omitido. - Resumen omitido. - Adelanto omitido. - Promoción propia omitida. - Patrocinador omitido. - Segmento no enviado omitido. - SponsorBlock temporalmente no disponible. - SponsorBlock temporalmente no disponible (estado %d). - SponsorBlock temporalmente no disponible (la API no responde). - Estadísticas - Estadísticas temporalmente no disponibles (la API está inactiva). - Cargando... - Tu reputación es de <b>%.2f</b> - Has salvado personas de <b>%s</b> segmentos - %1$s horas %2$s minutos - %1$s minutos %2$s segundos - %s segundos - Eso es <b>%s</b> de sus vidas.<br>Pulsa aquí para ver la tabla de clasificación. - Pulsa aquí para ver las estadísticas globales y los mejores colaboradores. - Tabla de clasificación de SponsorBlock - SponsorBlock está desactivado. - Has omitido <b>%s</b> segmentos - ¿Restablecer el contador de segmentos omitidos? - Eso es <b>%s</b>. - Has creado <b>%s</b> segmentos - Pulsa aquí para ver tus segmentos. - Tu nombre de usuario: <b>%s</b> - Pulsa aquí para cambiar tu nombre de usuario - No se puede cambiar el nombre de usuario: Estado: %1$d %2$s. - Nombre de usuario cambiado correctamente. - No se puede enviar el segmento.\nYa existe. - No se puede enviar el segmento: %s. - No se puede enviar el segmento: %s. - No se puede enviar el segmento.\nTasa limitada (demasiada del mismo usuario o IP). - SponsorBlock está temporalmente inactivo. - No se puede enviar el segmento (estado: %1$d %2$s). - Segmento enviado correctamente. - No se muestra el mensaje si SponsorBlock no está disponible. - Se muestra el mensaje si SponsorBlock no está disponible. - Mostrar mensaje si la API no está disponible - Cambiar categoría - Voto negativo - No se puede votar por el segmento: %s. - No se puede votar por el segmento (la API no responde). - No se puede votar por el segmento (estado: %1$d %2$s). - No hay segmentos por los cuales votar. - Voto positivo - Ajustes copiados en el portapapeles. - Marca de tiempo copiada en el portapapeles. (%s) - URL copiada en el portapapeles. - URL con marca de tiempo copiada en el portapapeles. - Original - Pulgares arriba - Cairo - Corazón - Corazón (Tinte) - Oculto - Animación de doble toque - El margen inferior del panel meta debe estar entre 0-64. Restablezca a los valores predeterminados. - Configura el espaciado desde la barra de progreso al panel meta, entre 0-64. - Margen inferior del panel meta - El porcentaje de altura debe estar entre 0-100 (%). - Configura el porcentaje de altura del espacio vacío izquierdo cuando la barra de navegación está oculta, entre 0 y 100 (%). - Porcentaje de altura del espacio vacío - Mantén pulsada la marca de tiempo para cambiar el estado de repetición de los Shorts. - Acción de pulsación larga en marca de tiempo - "Muestra la sección de título de vídeo en pantalla completa. - -Limitación: el título de vídeo desaparece cuando se pulsa." - Mostrar sección de título de vídeo - Si la reproducción automática está activada, el siguiente vídeo se reproducirá después de que termine la cuenta atrás. - Si la reproducción automática está activada, el siguiente vídeo se reproducirá sin cuenta atrás. - Omitir cuenta atrás de reproducción automática - "Omite el búfer precargado al iniciar el vídeo para evitar la demora en la aplicación de la calidad predeterminada del video. - -• Cuando comienza el vídeo, hay una demora de aproximadamente 0.3 segundos, pero la calidad predeterminada de vídeo se aplica inmediatamente. -• No se aplica a vídeos HDR, vídeos en directo ni vídeos de menos de 15 segundos." - Omitir búfer precargado - El mensaje está oculto. - El mensaje está visible. - Mostrar un mensaje al omitir - Activar este ajuste puede causar problemas de reproducción de vídeo. - Búfer precargado omitido. - El valor de la superposición de velocidad debe estar entre 0-8.0. Restablezca a los valores predeterminados. - Valor de superposición de velocidad entre 0-8.0. - Valor de superposición de velocidad - "Falsifica la versión del cliente a la versión antigua. - -• Esto cambiará la apariencia de la app, pero pueden producirse efectos secundarios desconocidos. -• Si más tarde se desactiva, la antigua interfaz de usuario puede permanecer hasta que se borren los datos de la app." - Versión no falsificada - Versión falsificada - 17.33.42 - Restaura el antiguo diseño de la interfaz de usuario - 17.41.37 - Restaura la antigua estantería de listas de reproducción - 18.05.40 - Restaura la antigua caja de entrada de comentarios - 18.17.43 - Restaura el antiguo panel desplegable del reproductor - 18.33.40 - Restaura la antigua barra de acción de Shorts - 18.38.45 - Restaura el antiguo comportamiento de la calidad predeterminada de vídeo - 18.48.39 - Desactiva la actualización en tiempo real de las visualizaciones y los me gusta - 19.13.37 - Restaura el antiguo estilo de las animaciones de números rodantes - Versión a falsificar de la app - Escribe la versión a falsificar de la app. - Editar versión falsa de la app - Falsificar versión de la app - "La versión de la aplicación será una versión antigua de YouTube. - -Esto cambiará la apariencia y las características de la aplicación, pero pueden producirse efectos secundarios desconocidos. - -Si se desactiva más tarde, se recomienda borrar los datos de la aplicación para evitar errores en la interfaz de usuario." - "Falsifica las dimensiones del dispositivo para desbloquear calidades de vídeo superiores que pueden no estar disponibles en tu dispositivo." - Falsificar dimensiones del dispositivo - El códec de vídeo de iOS es AVC (H.264), VP9 o AV1. - El códec de vídeo de iOS es AVC (H.264). - Forzar iOS AVC (H.264) - "Activar esto podría mejorar la duración de la batería y solucionar el problema de reproducción entrecortada. - -AVC (H.264) tiene una resolución máxima de 1080p, y la reproducción de vídeo utilizará más datos de Internet que VP9 o AV1." - "• Falta el menú \"Pista de audio\". -• \"Regular volumen\" no está disponible." - "• Falta el menú \"Pista de audio\". -• \"Regular volumen\" no está disponible." - "• Las películas o vídeos de pago no pueden reproducirse." - Efectos secundarios de falsificación - • El vídeo no puede reproducirse. - El cliente utilizado para obtener datos de transmisión no se muestra en estadísticas para nerds. - El cliente utilizado para obtener datos de transmisión se muestra en estadísticas para nerds. - Mostrar en estadísticas para nerds - "Los datos de transmisión no están falsificados. Es posible que la reproducción de vídeo no funcione." - Los datos de transmisión están falsificados. - Falsificar datos de transmisión - Android - Android TV - Android VR - iOS - Cliente predeterminado - Desactivar este ajuste puede causar problemas de reproducción de vídeo. - La sensibilidad de deslizamiento del brillo debe estar entre 1-1000 (%). - Configura la distancia mínima para el deslizamiento de brillo entre 1 y 1000 (%).\nCuanto menor sea la distancia mínima, más rápido cambiará el nivel de brillo. - Sensibilidad de deslizamiento del brillo - Los gestos deslizantes están desactivados en el modo \"Bloquear pantalla\". - Los gestos deslizantes están activados en el modo \"Bloquear pantalla\". - Gestos deslizantes en modo \"Bloquear pantalla\" - Automático - La cantidad de umbral para que ocurra el deslizamiento. - Umbral de magnitud de deslizamiento - La visibilidad del fondo de superposición de deslizamiento. - Visibilidad de fondo de deslizamiento - El tamaño del área deslizable no puede ser superior a 50. Restablezca al valor predeterminado. - Porcentaje de área de pantalla deslizable.\n\nNota: esto también cambiará el tamaño del área de pantalla para el gesto de doble toque para desplazarse. - Tamaño de pantalla de superposición deslizante - El tamaño del texto para la superposición de deslizamiento. - Tamaño del texto de superposición de deslizamiento - La cantidad de milisegundos que la superposición es visible. - Tiempo de espera de superposición de deslizamiento - La sensibilidad de deslizamiento del volumen debe estar entre 1-1000 (%). - Configura la distancia mínima para el deslizamiento del volumen entre 1 y 1000 (%).\n\nCuanto menor sea la distancia mínima, más rápido cambiará el nivel de brillo.\n\nSe recomienda una sensibilidad de deslizamiento del volumen del 100% en pasos de 15 niveles de volumen y del 10% en pasos de 150 niveles de volumen. - Sensibilidad de deslizamiento de volumen - "Cambia las posiciones del botón de crear y del botón de notificaciones falsificando la información del dispositivo. - -• Aunque cambies este ajuste, es posible que no surta efecto hasta que reinicies el dispositivo. -• Al desactivar este ajuste se cargan más anuncios desde el servidor. -• Debes desactivar este ajuste para que los anuncios de vídeo sean visibles." - El botón de crear no se cambia por el botón de notificaciones. - "El botón de crear se cambia por el botón de notificaciones. - -Nota: Al activar esto también se ocultan forzosamente los anuncios de vídeos." - Cambiar botón de crear con el de notificaciones - "Desactivar esto podría cargar más anuncios del servidor. - -Además, los anuncios ya no se bloquearán en Shorts. - -Si este ajuste no surte efecto, prueba a cambiar al modo incógnito." - Predeterminado - RVX Music - %s no está instalado. Por favor, instálalo. - Nombre del paquete de RVX Music instalado. - Nombre del paquete de RVX Music - • El historial de reproducciones no funciona. - "• Sigue la configuración del historial de reproducciones de la cuenta de Google. -• Es posible que el historial de reproducciones no funcione debido a DNS o VPN." - • Sigue la configuración del historial de reproducciones de la cuenta de Google. - Acerca del historial de reproducciones - Pulsa para abrir la administración del historial de reproducciones de YouTube. - Administrar todo el historial - Original - Reemplazar dominio - Bloquear historial de reproducciones - Tipo de historial de reproducciones - Error al añadir el canal %1$s a la lista blanca %2$s. - El canal %1$s se agregó a la lista blanca %2$s. - No hay canales en esta lista. - No añadido a esta lista. - Error al cargar la información del canal. - Añadido a esta lista. - Velocidad de reproducción - ¿Eliminar el canal %1$s de la lista %2$s? - Error al eliminar el canal %1$s de la lista %2$s. - El canal %1$s fue eliminado de la lista %2$s. - Verifique o elimine la lista de canales agregados a la lista blanca. - Lista blanca de canales - SponsorBlock - diff --git a/src/main/resources/youtube/translations/fr-rFR/missing_strings.xml b/src/main/resources/youtube/translations/fr-rFR/missing_strings.xml deleted file mode 100644 index c7e1ce32d..000000000 --- a/src/main/resources/youtube/translations/fr-rFR/missing_strings.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - Don\'t show again - Courses / Learning - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - AI-generated video summary section is shown. - AI-generated video summary section is hidden. - Hide AI-generated video summary section - Disabled comments button or with label \"0\" is shown. - Disabled comments button or with label \"0\" is hidden. - Hide disabled comments button - MMT Blue - MMT Green - MMT Orange - MMT Pink - MMT Turquoise - MMT Yellow - Revancify Yellow - Vanced Black - Vanced Light - Xisr Yellow - Adjust: Mark Start and End Time for segment - Verify the Segment - Edit the Segment - Forward by Specified Time (Default: 150ms) - Publish Created Segment - Rewind by Specified Time (Default: 150ms) - diff --git a/src/main/resources/youtube/translations/fr-rFR/strings.xml b/src/main/resources/youtube/translations/fr-rFR/strings.xml deleted file mode 100644 index 3624a8b68..000000000 --- a/src/main/resources/youtube/translations/fr-rFR/strings.xml +++ /dev/null @@ -1,1703 +0,0 @@ - - - Activer les contrôles d\'accessibilité pour le lecteur vidéo ? - Vos contrôles sont modifiés car un service d\'accessibilité est activé. - Continuer - "GmsCore n'a pas les permissions pour fonctionner en arrière-plan. - -Suivez le guide \"Don't kill my app!\" pour votre appareil, et appliquez les instructions sur GmsCore. - -Requis pour que l'application fonctionne." - "L'optimisation de la batterie de GmsCore doit être désactivé pour éviter tout problème. - -Cliquez sur le bouton Continuer et désactivez les optimisations de la batterie." - Ouvrir le site web - Action requise  - Activez la messagerie cloud pour recevoir les notifications. - Ouvrir GmsCore - GmsCore n\'est pas installé. Veuillez l\'installer. - "DeArrow propose des miniatures YouTube grâce à un service d'entraide. Ces miniatures sont souvent plus intéressantes que celles fournies par YouTube. - -Si activée, l'URL des vidéos seront envoyés sur le serveur API et aucune autre données ne sera envoyée. Si la vidéo n'a pas de miniatures DeArrow, l'original ou celle capturé sera affiché. - -Cliquez ici pour en savoir plus sur DeArrow." - DeArrow - URL de l\'API DeArrow invalide. - L\'URL du point de connexion au cache des miniatures DeArrow. - Point de connexion à l\'API DeArrow - N\'affiche pas de message si DeArrow est indisponible. - Affiche un message si DeArrow est indisponible. - Afficher un message si l\'API est indisponible - DeArrow est momentanément indisponible. (code de statut : %s) - DeArrow est momentanément indisponible. - Onglet \"Accueil\" - Onglet \"Vous\" - Miniatures originales - DeArrow & miniatures originales - DeArrow & miniatures capturées - Méthode de capture - Listes de lecture, recommandations - Résultats de recherche - Miniatures alternatives extraites de la vidéo - Les miniatures peuvent être capturées du début, du milieu ou de la fin de chaque vidéo. Les miniatures sont intégrées à YouTube, aucune API externe n\'est utilisée. - Informations sur la capture de miniature - Utiliser des captures de haute qualité. - Utiliser des captures de qualité moyenne. Les miniatures chargeront rapidement, mais les diffusions en direct, les vidéos non publiées ou très anciennes peuvent ne pas afficher de miniatures. - Utiliser des captures rapides - Début de la vidéo - Milieu de la vidéo - Fin de la vidéo - Quelle partie de la vidéo utiliser pour la capture - Onglet \"Abonnements\" - L\'information n\'est pas ajoutée à côté de l\'horodatage (durée de la vidéo). - "L'information est ajoutée à côté de l'horodatage (durée de la vidéo)." - Ajouter info. à côté de la durée - Ajoute la vitesse de lecture. - Ajoute la qualité vidéo. - Type d\'information à ajouter - Le mode ambiant est désactivé en mode économie d\'énergie. - Le mode ambiant est activé en mode économie d\'énergie. - Contourner les restrictions du mode ambiant - Le domaine depuis lequel récupérer les images.\Nnote : Saisissez uniquement le nom de domaine, c\'est-à-dire, sans le préfixe \"https\:\/\/\". - Domaine alternatif - Utilisation des images originales.\n\nActiver ceci peut corriger les images manquantes qui sont bloquées dans certaines régions. - Utilisation des images hébergées par yt4.ggpht.com. - Contourner les restrictions des images selon les régions - Original - Téléphone - Téléphone (Max 480 dpi) - Tablette - Tablette (Min 600 dpi) - Modifier la mise en page - Les boutons à bascule sont utilisés. - Les boutons à bascule avec textes sont utilisés. - Type de bouton à bascule - L\'onglet \"Partager avec\" de l\'application est utilisée. - L\'onglet \"Partager avec\" du système est utilisée. - Modifier l\'onglet \"Partager avec\" - Lecture auto. - Par défaut - Pause - Répéter - Répétition des shorts - Parcourir les chaînes - Par défaut - Explorer - Jeux vidéos - Historique - Bibliothèque - Vidéos \"J\'aime\" - Direct - Films - Musique - Rechercher sur YouTube - Shorts - Sports - Abonnements - Tendances - Regarder plus tard - Modifier la page de démarrage - La page de démarrage est modifiée une seule fois. - "La page de démarrage est toujours modifiée. - -Limitation : Le bouton Retour de la barre d'outils peut ne pas fonctionner." - Type de modification de la page de démarrage - L\'en-tête original est activé. - L\'en-tête YouTube Premium est activé. - Changer l\'en-tête YouTube - Filtrer la liste des noms du composant séparés par un saut de ligne. - Modifier le filtre personnalisé - Le filtre personnalisé est désactivé. - Le filtre personnalisé est activé. - Activer le filtre personnalisé - Filtre personnalisé invalide : %s. - L\'ancien style du menu déroulant est utilisé. - Affichage de sélection de vitesse personnalisé. - Personnaliser le menu \"Vitesse de lecture\" - Les vitesses personnalisées doivent être inférieures à %sx. - Valeur des vitesses de lecture invalide. - Ajouter ou modifier les vitesses de lecture disponibles. - Saisir des vitesses de lecture personnalisées - L\'opacité du voile du lecteur doit être entre 0-100. - Valeur d\'opacité entre 0-100, 0 étant transparent. - Personnaliser l\'opacité du voile du lecteur - Saisissez le code hexadécimal de la couleur de la barre de progression. - Couleur perso. barre de progression - Pour ouvrir les liens YouTube sur RVX, activez l\'option \'Ouvrir les liens compatibles\' et activez les liens Web compatibles. - Ouvrir les paramètres \"Liens compatibles\" - Vitesse de lecture par défaut - Qualité vidéo par défaut sur les données mobiles - Qualité vidéo par défaut sur le réseau Wi-Fi - Désactive le mode ambiant en plein écran - Le mode ambiant est activé en plein écran. - Le mode ambiant est désactivé en plein écran. - Désactiver le Mode ambiant en plein écran - Désactive le mode ambiant - Le mode ambiant est activé. - Le mode ambiant est désactivé. - Désactiver le Mode ambiant - Les pistes audio automatiques forcées sont activés. - Les pistes audio automatiques forcées sont désactivé. - Désact. les pistes audio forcés - Les sous-titres automatiques forcés sont activés. - Les sous-titres automatiques forcés sont désactivés. - Désact. les sous-titres forcés - Les fenêtres pop-up du lecteur automatique sont activés. - Les fenêtres pop-up du lecteur automatique sont désactivées. - Fenêtres pop-up du lecteur automatique - "Le mélange auto des playlists est activé lorsque la lecture automatique est activé. - -La lecture automatique peut être modifiée dans les paramètres de YouTube : -Paramètres → Lecture automatique → Lecture automatique de la vidéo suivante" - Le mélange auto des playlists est désactivé. - Désactiver le mélange des playlists mix - Activer cette fonction désactivera le passage automatique à YouTube Mix lors de la lecture de musique lorsque la lecture automatique est activée. - La vitesse de lecture par défaut est activée pour les diffusions en direct. - La vitesse de lecture par défaut est désactivée pour les diffusions en direct. - Desact. vitesse lecture des diffusions en direct - La vitesse de lecture par défaut est activée pour la musique. - "La vitesse de lecture par défaut est désactivée pour la musique. - -Limitation : Ce paramètre peut ne pas s'appliquer aux vidéos qui n'incluent pas la bannière \"Écouter sur YouTube Music\"." - Désactiver la vitesse de lecture pour la musique - La description en plein écran est activée. - La description en plein écran est désactivée. - Désactiver description en plein écran - La vibration est activée. - La vibration est désactivée. - Désact. vibration de sélection de chapitres - La vibration est activée. - La vibration est désactivée. - Desact. Vibration pendant le glissement - La vibration est activée. - La vibration est désactivée. - Desact. vibration de la barre de progression - La vibration est activée. - La vibration est désactivée. - Desact. vibration barre de progression relaché - La vibration est activée. - La vibration est désactivée. - Désact. vibration lors du zoom - La luminosité HDR automatique est activée. - La luminosité HDR automatique est désactivée. - Luminosité HDR automatique - Les vidéos en HDR sont activés. - Les vidéos en HDR sont désactivés. - Déaactiver les vidéos HDR - Le passage en mode paysage en plein écran est activé. - Le passage en mode paysage en plein écran est désactivé. - Désactiver le mode paysage - Les boutons \"J\'aime\" et \"Je n\'aime pas\" s\'illuminerons lorsqu\'ils sont mentionné. - Les boutons \"J\'aime\" et \"Je n\'aime pas\" ne s\'illuminerons pas lorsqu\'ils sont mentionné. - Désac. lueur des \"J\'aime\" et \"Je n\'aime pas\" - "Désactiver le protocole QUIC de CronetEngine." - Protocole QUIC - Les shorts reprennent au démarrage de l\'application. - Les shorts ne reprennent pas au démarrage de l\'application. - Désac. \"Reprendre la lecture\" sur les Shorts - Animation en temps réel des nombres est activé. - Animation en temps réel des nombres est désactivé. - Désactiver l\'animation en temps réel des nombres - Les chapitres sont activés sur la barre de progression. - Les chapitres sont désactivés sur la barre de progression. - Désac. chapitres sur la barre de progression - L\'animation en fontaine est activé au-dessus du bouton j\'aime. - L\'animation en fontaine est désactivé au-dessus du bouton j\'aime. - Désactiver l\'animation du bouton J\'aime - "Désactive '2x>>' en appuyant longuement. - -Note : -• Désactiver le contrôle de vitesse restaure de l'option \"Faites glisser pour rechercher\" de l'ancienne mise en page. -• Désactiver ce paramètre ne force pas l'activation de contrôle vitesse." - Désactiver le contrôle de la vitesse - L\'animation de démarrage est activé. - L\'animation de démarrage est désactivé. - Désact. l\'animation de démarrage - "Désactive les interactions suivantes lorsque la description de la vidéo est développée : - -• Appuyez pour défiler. -• Appuyez longuement pour sélectionner du texte." - Désac. interaction avec la description vidéo - Le codec VP9 est activé. - "Le codec VP9 est désactivé. - -• La résolution maximale est en 1080p. -• La lecture vidéo utilisera plus de données internet que le VP9. -• Le codec VP9 est également utilisé pour les vidéos HDR." - Désactiver le codec VP9 - La barre de progression Cairo est désactivé. - "La barre de progression Cairo est activé. - -Effet secondaire : le thème Cairo peut également s'appliquer sur les points de notification." - Activer la barre de progression Cairo - La voile des contrôles remplit le plein écran. - La voile des contrôles ne remplit pas le plein écran. - Activer le fond des contrôles compacts - Les vitesses de lecture personnalisées sont désactivées. - Les vitesses de lecture personnalisées sont activées. - Activer vitesses de lecture perso. - La couleur personnalisée de la barre de progression est désactivée. - La couleur personnalisée de la barre de progression est activée. - Activer une couleur perso. barre de progression - Les journaux de débogage n\'incluent pas de tampon. - Les journaux de débogage incluent le tampon. - Activer info. mémoire tampon dans journal de débogage - Les journaux de débogage sont désactivés. - Le journal de débogage est activés. - Activer le journal de débogage - La vitesse de lecture par défaut ne s\'applique pas aux Shorts. - La vitesse de lecture par défaut s\'applique aux Shorts. - Activ. vitesses de lecture shorts par défaut - Le navigateur externe est désactivé. - Le navigateur externe est activé. - Activer le navigateur externe - Le dégradé pendant l\'écran de chargement est désactivé. - Le dégradé pendant l\'écran de chargement est activé. - Activer dégradé pendant le chargement - L\'espacement entre les boutons de la barre de navigation sont normaux. - L\'espacement entre les boutons de la barre de navigation sont réduit. - Activer les boutons de navigation compacts - Suit la règle de redirection par défaut. - Contourne les redirections URL. - Activer l\'ouverture des liens directement - Active le codec OPUS si la réponse du lecteur inclut le codec OPUS. - Activer le Codec OPUS - N\'enregistre et ne restaure pas la luminosité en quittant ou en entrant en plein écran. - Enregistrer et restaurer la luminosité en quittant ou en entrant en plein écran. - Activer enregistr. et restaur. de la luminosité - L\'appui sur la barre de progression est désactivé. - L\'appui sur la barre de progression est activé. - Activer l\'appui sur la barre de progression - "Cela va restaurer les miniatures des diffusions en direct n'ayant pas de miniatures dans la barre de progression. - -L'utilisation des données Internet peut être plus élevée et les miniatures de la barre de progression s'afficheront avec un léger retard. - -Cette fonction fonctionne mieux avec une connexion internet très rapide." - Les miniatures de la barre de sélection sont en qualité moyenne. - Les miniatures de la barre de progression sont en haute qualité. - Activer les miniatures en haute qualité - L\'horodatage est désactivé. - "L'horodatage est activé. - -Limitations : -• Ce paramètre permet non seulement d'activer les horodatages, mais aussi de masquer l'interface utilisateur en cliquant sur l'arrière-plan du lecteur. -• Comme il s'agit d'une fonctionnalité en cours de développement par Google, la mise en page peut-être incorrecte." - Activer l\'horodatage - Les gestes de luminosité sont désactivé. - Les gestes de luminosité sont activé. - Activer les gestes de luminosité - La vibration est désactivé. - La vibration est activé. - Activer la vibration - La valeur la plus basse du geste de luminosité désactive la luminosité automatique. - La valeur la plus basse du geste de luminosité active la luminosité automatique. - Activer les gestes de luminosité auto - Appuyez pour activer les gestes de commande. - Appuyez longuement pour activer les gestes de commande. - Activer les gestes de commande - Les gestes vers le haut / bas ne lira pas la vidéo suivante / précédente. - Les gestes vers le haut / bas pour lire la vidéo suivante / précédente. - Activer les gestes pour changer de vidéo - Les gestes de volume sont désactivé. - Les gestes de volume sont activé. - Activer les gestes de volume - La barre de navigation est opaque. - La barre de navigation est translucide. - Activer la barre de navigation translucide - Le passage plein écran en faisant glisser vers le bas sous le lecteur vidéo est désactivé. - Le passage plein écran en faisant glisser vers le bas sous le lecteur vidéo est activé. - Activer les gestes inférieurs du lecteur - "Activer ce paramètre désactive le bouton \"Paramètres\" dans l'onglet \"Vous\". - -Dans ce cas, veuillez utiliser le chemin suivant pour accéder aux paramètres : -Vous → Afficher la chaîne → Menu → Paramètres" - Activer la barre de recherche large dans l\'onglet \"Vous\" - La barre de recherche large est désactivée. - La barre de recherche large est activée. - Activer la barre de recherche large - La barre de recherche large masque l\'en-tête YouTube. - La barre de recherche large ne masque pas l\'en-tête YouTube. - Activ. Barre recherche large avec en-tête - Description - "Entrez un titre dans la description de la vidéo dans votre langue. -L'option \"Ouvrir la description automatiquement\" risque de ne pas fonctionner si la valeur du titre ne correspond pas au titre dans la description." - Titre dans la description de la vidéo - La description s\'ouvre manuellement. - La description s\'ouvre automatiquement. - Ouvrir la description automatiquement - Voulez-vous continuer ? - Réinitialiser les valeurs par défaut. - Redémarrer pour charger l\'interface correctement - "Il existe un bug côté serveur de YouTube qui empêche l'animation en temps réel des nombres, tels que les mentions \"J'aime\", les vues et les dates de mise en ligne pour certains utilisateurs. - -Un moyen de contourner temporairement ce problème est de falsifier la version de l'application en version 19.13.37. - -Voulez-vous falsifier la version de l'application et redémarrer ?" - Appliquer et redémarrer ? - Échec de l\'exportation des paramètres. - Les paramètres ont été exportés avec succès. - Exporter les paramètres vers un fichier. - Exporter les paramètres - Importer - Copier - Importer ou exporter les paramètres sous forme de texte. - Importer / Exporter sous forme de texte - Échec de l\'importation des paramètres. - Les paramètres ont étés réinitialisés. - Les paramètres ont été importés avec succès. - Importer les paramètres depuis un fichier enregistré. - Importer les paramètres - Réinitialiser - Rechercher sur %s - ReVanced Extended - Téléchargeur externe - Non installé - "%1$s n'est pas installé. -Veuillez télécharger %2$s à partir du site web." - Attention - %s n\'est pas installé. Veuillez l’installer. - Nom de package du téléchargeur externe installé, telle que YTDLnis. - Nom du paquet du téléchargeur de la playlist - Nom du paquet de l\'appli de téléchargement externe installée, telle que NewPipe ou YTDLnis. - Nom du paquet du téléchargeur vidéo - "La vidéo passera en mode plein écran dans les situations suivantes : - -• Lors du démarrage de la vidéo. -• Clic sur un horodatage dans les commentaires." - Forcer le plein écran - Liste de noms du menu de compte à filtrer, séparés par un saut de ligne. - Filtre du menu du compte - "Masque les éléments du menu du compte et de l'onglet \"Vous\". -Certains composants peuvent ne pas être masqués." - Masquer le menu du compte - Les fiches d\'album sont affichées. - Les fiches d\'album sont masquées. - Masquer les fiches d\'album - Les sections \"Lieux Populaires\", \"Jeux\" et \"Musique\" présent sous la description sont affichés. - Les sections \"Lieux Populaires\", \"Jeux\" et \"Musique\" présent sous la description sont masquées. - Masquer la section \"Mentions\" - La prévisualisation automatique des vidéos est affiché. - La prévisualisation automatique des vidéos est masqué. - Masqu. Prévisualisation de lecture automatique - Le bouton \"Visiter la boutique\" est affiché. - Le bouton \"Visiter la boutique\" est masqué. - Masquer le bouton \"Visiter la boutique\" - "Masque les étagères suivantes : -• Actualités -• Continuer à regarder -• Explorer d'autres chaînes -• Écouter à nouveau -• Produits -• Regarder à nouveau" - Masquer les étagères à suggestions - Affiché dans les flux. - Masqué dans les flux. - Masquer dans les flux - Affiché dans \"Plus de vidéos\". - Masqué dans \"Plus de vidéos\". - Masquer dans les vidéos similaires - Affiché dans les résultats de recherches. - Masqué dans les résultats de recherches. - Masquer dans les résultats de recherches - Les règles de la chaîne sont affichées. - Les règles de la chaîne sont masquées. - Masquer les règles de la chaîne - Les membres de la chaîne sont affichés. - Les membres de la chaîne sont masqués. - Masquer les membres de la chaîne - Les liens en haut de la chaîne sont affichés. - Les liens en haut de la chaîne sont masqués. - Masquer les liens de la chaîne - "Shorts -Playlists -Boutique" - Liste des filtres de l\'onglet chaîne, séparés par un saut de ligne. - Filtre de l\'onglet chaîne - Le filtre de l\'onglet chaîne est désactivé. - Le filtre de l\'onglet chaîne est activé. - Activer le filtre de l\'onglet chaîne - Le filigrane de chaîne est affiché. - Le filigrane de chaîne est masqué. - Masquer le filigrane de chaine - La section des chapitres est affiché. - La section des chapitres est masqué. - Masquer la section des chapitres - L\'étagère \"Vous pourriez aussi aimer\" est affichée. - L\'étagère \"Vous pourriez aussi aimer\" est masquée. - Masquer des étagères - Le bouton \"Extrait\" est affiché. - Le bouton \"Extrait\" est masqué. - Masquer le bouton \"Extrait\" - Le bouton \"Créer un Short\" est affiché. - Le bouton \"Créer un Short\" est masqué. - Masquer le bouton \"Créer un Short\" - Les liens de recherche en surbrillance sont affichés. - Les liens de recherche en surbrillance sont masqués. - Marquer les liens de recherche en surbrillance - Le bouton \"Merci\" est affiché. - Le bouton \"Merci\" est masqué. - Masquer le bouton \"Merci\" - Le sélecteur d\'émoji et l\'horodatage sont affichés. - Le sélecteur d\'émoji et l\'horodatage sont masqués. - Masquer le sélecteur d\'émoji et horodatage - Les commentaires des membres sont affichés. - Les commentaires des membres sont cachés. - Masquer les commentaires des membres - La section commentaires dans le flux accueil est affiché. - La section commentaires dans le flux accueil est masqué. - Masquer la section commentaires dans le flux accueil - La section des commentaires est affichée. - La section des commentaires est masquée. - Masquer la section des commentaires - Affiché sur les chaînes. - Masqué sur les chaînes. - Masquer sur les chaînes - Affiché dans les flux \"accueil\" et \"vidéos similaires\". - Masqué dans les flux \"accueil\" et \"vidéos similaires\". - Masquer dans les flux \"accueil\" et \"vidéos similaires\" - Affiché dans le flux \"Abonnements\". - Masqué dans le flux \"Abonnements\". - Masquer dans le flux \"Abonnements\" - La section \"Comment ce contenu a été créé\" est affiché. - La section \"Comment ce contenu a été créé\" est masqué. - Masquer la section \"Contenu\" - La boîte de collecte de fonds est affiché. - La boîte de collecte de fonds est masqué. - Masquer la boîte de collecte de fonds - Le voile lors du double appui est affiché. - Le voile lors du double appui est masqué. - Masquer le voile sombre du double appui - Le bouton \"Télécharger\" est affiché. - Le bouton \"Télécharger\" est masqué. - Masquer le bouton \"Télécharger\" - Les vidéos suggérées à la fin d\'une vidéo sont affichées. - Les vidéos suggérées à la fin d\'une vidéo sont masquées. - Masquer les cartes d\'écran de fin - Les menus déroulants sont affichés. - Les menus déroulants sont masqués. - Masquer les menus déroulants sous les vidéos - Les étagères coulissantes sont affichés. - Les étagères coulissantes sont masqués. - Masquer les étagères coulissantes - Le bouton \"Sous-titres\" est affiché. - Le bouton \"Sous-titres\" est masqué. - Masquer les \"Sous-titres\" dans les flux - Liste de menus déroulant à filtrer, séparés par un saut de ligne. - Filtre du menu déroulant - Le filtre du menu déroulant est désactivé. - Le filtre du menu déroulant est activé. - Activer le filtre du menu déroulant - La barre de recherche dans les flux sont affichés. - La barre de recherche dans les flux sont masqués. - Masquer barre de recherche dans les flux - Les sondages dans les flux sont affichés. - Les sondages dans les flux sont masqués. - Masquer les sondages dans les flux - La bande de film est affichée. - La bande de film est masquée. - Masquer la bande de film - Les boutons flottants sont affichés. - Les boutons flottants sont masqués. - Masquer les boutons flottants - Le bouton \"Micro\" est affiché. - Le bouton \"Micro\" est masqué. - Masquer le bouton \"Micro\" - La catégorie \"Pour vous\" est affichée. - La catégorie \"Pour vous\" est masquée. - Masquer la catégorie \"Pour vous\" - Les publicités en plein écran sont affichées. - Les publicités en plein écran sont masquées. - Masquer les publicités en plein écran - "Les publicités en plein écran sont bloquées. - -Effet secondaire : Les images des posts communautaires peuvent être bloquées en plein écran." - Les publicités en plein écran sont fermées grâce au bouton \"Fermer\". - Fermer les publicités en plein écran - Les publicités générales sont affichées. - Les publicités générales sont masquées. - Masquer les publicités générales - Publicités pour YouTube Premium affichées. - Les publicités pour YouTube Premium sont masquées. - Masquer les pubs pour YouTube Premium - Les séparateurs gris sont affichés. - Les séparateurs gris sont masqués. - Masquer les séparateurs gris - L\'identifiant est affiché. - L\'identifiant est masqué. - Masquer l\'identifiant - Le bouton \"Recherche d\'images\" est affiché. - Le bouton \"Recherche d\'images\" est masqué. - Masquer le bouton \"Recherche d\'images\" - Les étagères à images sont affichées. - Les étagères à images sont masquées. - Masquer les étagères à images - La section des fiches info est affichée. - La section des fiches info est masquée. - Masquer les fiches \"Infos\" - Les fiches infos sont affichées. - Les fiches infos sont masquées. - Masquer les fiches infos - Les panneaux d\'information sont affichés. - Les panneaux d\'information sont masqués. - Masquer les panneaux informations - Le bouton \"Rejoindre\" est affiché. - Le bouton \"Rejoindre\" est masqué. - Masquer le bouton \"Rejoindre\" - La section \"Concepts clés\" est affiché. - La section \"Concepts clés\" est masqué. - Masquer la section \"Concepts clés\" - "Les onglets \"Pages d'accueil\" / \"Abonnements\" / Résultats de recherche sont filtrés pour masquer le contenu correspondant aux mots clés. - -Limitations : -• Les shorts ne peuvent pas être masqués par le nom de la chaîne. -• Certains éléments de l'interface utilisateur peuvent ne pas être masqués. -• La recherche par mot-clé peut n'afficher aucun résultat." - À propos du filtrage par mots-clés - Le fait de placer un mot-clé ou une expression entre guillemets permet d\'éviter les correspondances partielles entre les titres de vidéos et les noms des chaînes.<br><br>Par exemple,<br><b>\"ia\"</b> masquera la vidéo : <b>Comment fonctionne les IA ?</b><br>mais ne masquera pas : <b>Quelles études pour devenir commercial ?</b> - Faire correspondre des mots complets - Les commentaires ne sont pas filtrés. - Les commentaires sont filtrés. - Filtrer les commentaires par mot-clés - Les vidéos dans le flux \"accueil\" ne sont pas filtrées. - Les vidéos dans le flux \"accueil\" sont filtrées. - Filtrer la page d\'accueil par mot-clés - "Mots-clés et phrases à masquer, séparés par des sauts de lignes. - -Les mots-clés peuvent être des noms de chaînes ou tout texte figurant dans les titres des vidéos. - -Les mots comportant des majuscules au milieu doivent être saisis de la même façon (par exemple : iPhone, TikTok, TheoBabac)." - Mots-clés à masquer - Les résultats de recherche ne sont pas filtrés. - Les résultats de recherche sont filtrés. - Filtrer les recherches par mots-clés - Les vidéos dans le flux \"Abonnements\" ne sont pas filtrées. - Les vidéos dans le flux \"Abonnements\" sont filtrées. - Filtrer \"Abonnements\" par mots-clés - Ce mot-clé va masquer toutes les vidéos : %s. - Impossible d\'utiliser ce mot-clé : %s. - Ajouter des guillemets pour utiliser un mot-clé : %s. - Le mot-clé a des déclarations en conflit : %s. - Le mot-clé est trop court et nécessite des guillemets : %s. - Les posts récents sont affichés. - Les posts récents sont masqués. - Masquer les posts récents - Le bouton \"Vidéos récentes\" est affiché. - Le bouton \"Vidéos récentes\" est masqué. - Masquer le bouton \"Vidéos récentes\" - Les boutons \"J\'aime\" et \"Je n\'aime pas\" sont affichés. - Les boutons \"J\'aime\" et \"Je n\'aime pas\" sont masqués. - Masquer les \"J\'aime\" et \"Je n\'aime pas\" - Les messages du chat en direct sont affichés.\n\nCe paramètre s\'applique également sur les vidéos Shorts en direct. - Les messages du chat en direct sont masqués.\n\nCe paramètre s\'applique également sur les vidéos Shorts en direct. - Masquer les messages du chat en direct - Le bouton \"Rediffusion du chat en direct\" est affiché.\n\nPeut apparaître en plein écran lors de la fermeture du chat en direct. - Le bouton \"Rediffusion du chat en direct\" est masqué.\n\nPeut apparaître en plein écran lors de la fermeture du chat en direct. - Masquer le bouton \"Rediffusion du chat en direct\" - Masque les vidéos avec moins de 1,000 vues dans le flux \"accueil\" qui ont été mis en ligne par des personnes dont vous n\'êtes pas abonnés. - Masquer les vidéos peu vues - Les panneaux d\'infos médicaux sont affichés. - Les panneaux d\'infos médicaux sont masqués. - Masquer les panneaux d\'infos médicaux - L\'étagère \"Effectuer des achats dans le magasin ...\" est affichée. - L\'étagère \"Effectuer des achats dans le magasin ...\" est masquée. - Masquer l\'étagère \"Effectuer des achats...\" - Les playlists mix sont affichés. - Les playlists mix sont masqués. - Masquer les playlists mix - Les étagères \"Vos films et séries\" sont affichés. - Les étagères \"Vos films et séries\" sont masqués. - Masquer \"Vos films et séries\" - La barre de navigation est affichée. - La barre de navigation est masqué. - Masquer la barre de navigation - Le bouton \"Créer\" est affiché. - Le bouton \"Créer\" est masqué. - Masquer le bouton \"Créer\" - Le bouton \"Accueil\" est affiché. - Le bouton \"Accueil\" est masqué. - Masquer le bouton \"Accueil\" - Le nom des catégories sont affichés. - Le nom des catégories sont masqués. - Masquer les noms des catégories - Le bouton \"Bibliothèque\" est affiché. - Le bouton \"Bibliothèque\" est masqué. - Masquer le bouton \"Bibliothèque\" - Le bouton \"Notifications\" est affiché. - Le bouton \"Notifications\" est masqué. - Masquer le bouton \"Notifications\" - Le bouton \"Shorts\" est affiché. - Le bouton Shorts est masqué. - Masquer le bouton \"Shorts\" - Le bouton \"Abonnements\" est affiché. - Le bouton \"Abonnements\" est masqué. - Masquer le bouton \"Abonnements\" - Le bouton \"Recevoir une Notification\" est affiché. - Le bouton \"Recevoir une Notification\" est masqué. - Masquer \"Recevoir une Notification\" - La bannière \"Inclut une communication commerciale\" est affiché. - La bannière \"Inclut une communication commerciale\" est masqué. - Masquer la bannière \"Communication commerciale\" - Les \"Jeux intégrés\" sont affichés. - Les \"Jeux intégrés\" sont masqués. - Masquer \"Jeux intégrés\" - Le bouton \"Lecture automatique\" est affiché. - Le bouton \"Lecture automatique\" est masqué. - Masquer le bouton \"Lecture automatique\" - Le bouton \"Sous-titres\" est affiché. - Le bouton \"Sous-titres\" est masqué. - Masquer le bouton \"Sous-titres\" - Le bouton \"Caster\" est affiché. - Le bouton \"Caster\" est masqué. - Masquer le bouton \"Caster\" - Le bouton \"Réduire\" est affiché. - Le bouton \"Réduire\" est masqué. - Masquer le bouton \"Réduire\" - Le menu \"Mode Ambiant\" est affiché. - Le menu \"Mode Ambiant\" est masqué. - Masquer le menu \"Mode Ambiant\" - Le menu \"Piste audio\" est affiché. - Le menu \"Piste audio\" est masqué. - Masquer le menu \"Piste audio\" - Le message du menu \"sous-titre\" est affiché. - Le message du menu \"sous-titre\" est masqué. - Masquer les conseils des sous-titres - Le menu \"Sous-titres\" est affiché. - Le menu \"Sous-titres\" est masqué. - Masquer le menu \"Sous-titres\" - Le menu 1080p Premium est affiché. - Le menu 1080p Premium est masqué. - Masquer le menu 1080p Premium - Le menu \"Aide et commentaires\" est affiché. - Le menu \"Aide et commentaires\" est masqué. - Masquer le menu \"Aide et commentaires\" - Le menu \"Écouter avec YouTube Music\" est affiché. - Le menu \"Écouter avec YouTube Music\" est masqué. - Masquer le menu \"Écouter avec YouTube Music\" - Le menu \"Verrouiller l\'écran\" est affiché. - Le menu \"Verrouiller l\'écran\" est masqué. - Masquer le menu \"Verrouiller l\'écran\" - Le menu \"Lecture en boucle\" est affiché. - Le menu \"Lecture en boucle\" est masqué. - Masquer le menu \"Lecture en boucle\" - Le menu \"Plus d\'infos\" est affiché. - Le menu \"Plus d\'infos\" est masqué. - Masquer le menu \"Plus d’infos\" - Le menu \"Picture-in-picture\" est affiché. - Le menu \"Picture-in-picture\" est masqué. - Masquer le menu \"Picture-in-picture\" - Le menu \"Vitesse de lecture\" est affiché. - Le menu \"Vitesse de lecture\" est masqué. - Masquer le menu \"Vitesse de lecture\" - Le menu \"Commandes Premium\" est affiché. - Le menu \"Commandes Premium\" est masqué. - Masquer le menu \"Commandes Premium\" - Le message du menu \"Qualité vidéo\" est affiché. - Le message du menu \"Qualité vidéo\" est masqué. - Masquer le message du menu \"Qualité\" - L\'en-tête du menu qualité est affiché. - L\'en-tête du menu qualité est masqué. - Masquer l\'en-tête du menu qualité - Le menu \"Signaler\" est affiché. - Le menu \"Signaler\" est masqué. - Masquer le menu \"Signaler\" - Le menu \"Délai de mise en veille\" est affiché. - Le menu \"Délai de mise en veille\" est masqué. - Masquer le menu \"Délai de mise en veille\" - Le menu \"Volume stable\" est affiché. - Le menu \"Volume stable\" est masqué. - Masquer le menu \"Volume stable\" - Le menu \"Statistiques avancées\" est affiché. - Le menu \"Statistiques avancées\" est masqué. - Masquer le menu \"Statistiques avancées\" - Le menu \"Regarder en VR\" est affiché. - Le menu \"Regarder en VR\" est masqué. - Masquer le menu \"Regarder en VR\" - Le bouton \"Plein écran\" est affiché. - Le bouton \"Plein écran\" est masqué. - Masquer le bouton \"Plein écran\" - Les boutons sont affichés. - Les boutons sont masqués. - Masquer les boutons \"Précédent & Suivant\" - L\'étagère des produits est affiché. - L\'étagère des produits est masqué. - Masquer l\'étagère des produits sur le lecteur - Le bouton \"YouTube Music\" est affiché. - Le bouton \"YouTube Music\" est masqué. - Masquer le bouton \"YouTube Music\" - Le bouton \"Enregistrer\" est affiché. - Le bouton \"Enregistrer\" est masqué. - Masquer le bouton \"Enregistrer\" - La section \"Podcasts\" est affiché. - La section \"Podcasts\" est masquée. - Masquer la section \"Podcasts\" - L\'aperçu des commentaires est affiché. - L\'aperçu des commentaires est masqué. - Masquer l\'aperçu des commentaires - Cela modifie la taille de la section commentaires, il est donc impossible d\'ouvrir la section \"Rediffusion du chat en direct\" dans la section commentaires. - Cela ne modifie pas la taille de la section commentaires, il est donc possible d\'ouvrir la section \"Rediffusion du chat en direct\" dans la section commentaires. - Masquer le type d\'aperçu des commentaires - La bannière d\'alerte de promotion est affichée. - La bannière d\'alerte de promotion est masquée. - Masquer la bannière d\'alerte de promotion - La section des commentaires est affichée. - La section des commentaires est masquée. - Masquer le bouton \"Commentaire\" - Le bouton \"Je n\'aime pas\" est affiché. - Le bouton \"Je n\'aime pas\" est masqué. - Masquer le bouton \"Je n\'aime pas\" - Le bouton \"J\'aime\" est affiché. - Le bouton \"J\'aime\" est masqué. - Masquer le bouton \"J\'aime\" - Le bouton \"Chat en direct\" est affiché. - Le bouton \"Chat en direct\" est masqué. - Masquer le bouton \"Chat en direct\" - Le bouton \"Plus\" est affiché. - Le bouton \"Plus\" est masqué. - Masquer le bouton \"Plus\" - Le bouton \"Ouvrir la playlist mix\" est affiché. - Le bouton \"Ouvrir la playlist mix\" est masqué. - Masquer bouton \"Ouvrir la playlist mix\" - Le bouton \"Ouvrir la playlist\" est affiché. - Le bouton \"Ouvrir la playlist\" est masqué. - Masquer le bouton \"Ouvrir la playlist\" - Le bouton \"Enregistrer\" est affiché. - Le bouton \"Enregistrer\" est masqué. - Masquer le bouton \"Enregistrer\" - Le bouton \"Partager\" est affiché. - Le bouton \"Partager\" est masqué. - Masquer le bouton \"Partager\" - Les boutons d\'actions rapides sont affichés. - Les boutons d\'actions rapides sont masqués. - Masquer tout les boutons d\'actions rapides - "Masque les vidéos recommandées suivants : - -• Les vidéos avec la mention \"Réservé aux membres\". -• Les vidéos avec des phrases telles que \"Les internautes ont aussi regardé cette vidéo\" en dessous." - Masquer les vidéos recommandées - La section \'Plus de vidéos\' dans l\'action rapide et les vidéos suggérés sont affichées. - La section \'Plus de vidéos\' dans l\'action rapide et la superposition de vidéos suggérés sont masquées. - Masquer les vidéos associés - Les vidéos similaires sont affichés. - Les vidéos similaires sont masqués. - Masquer les vidéos similaires - "Ce paramètre limite le nombre maximum de mises en page pouvant être chargées sur l'écran du lecteur. - -Si la mise en page de l'écran du lecteur change en raison de modifications côté serveur, des mises en page non désirées risquent d'être masquées sur l'écran du lecteur." - Le bouton \"Remixer\" est affiché. - Le bouton \"Remixer\" est masqué. - Masquer le bouton \"Remixer\" - Le bouton \"Signaler\" est affiché. - Le bouton \"Signaler\" est masqué. - Masquer le bouton \"Signaler\" - Le bouton \"Récompense\" est affiché. - Le bouton \"Récompense\" est masqué. - Masquer le bouton \"Récompense\" - Les miniatures sur la barre de recherche sont affichées. - Les miniatures sur la barre de recherche sont masquées. - Masquer les miniatures pendant la recherche - Les bandeaux de messages sont affichés. - Les bandeaux de messages sont masqués. - Masquer les bandeaux de messages - Le bandeau \"Relâchez pour annuler\" est affiché. - Le bandeau \"Relâchez pour annuler\' est masqué. - Masquer le bandeau \"Relâchez pour annuler\" - Les noms de chapitres situés à côté de l\'horodatage sont affichés. - Les noms de chapitres situés à côté de l\'horodatage sont masqués. - Masq. noms des chapitres sur la barre de progression - La barre de progression sur le lecteur est affiché. - La barre de progression sur le lecteur est masqué. - Les miniatures de la barre de progression sont affichés. - Les miniatures de la barre de progression sont masqués. - Masq. barre de progression des miniatures - Masquer la barre de progression - Les cartes autosponsorisées sont affichées. - Les cartes autosponsorisées sont masquées. - Masquer les cartes autosponsorisées - Le menu \'À propos\' est affiché. - Le menu \'À propos\' est masqué. - Masquer le menu \'À propos\' - Le menu \'Accessibilité\' est affiché. - Le menu \'Accessibilité\' est masqué. - Masquer le menu \'Accessibilité\' - Le menu \'Compte\' est affiché. - Le menu \'Compte\' est masqué. - Masquer le menu \'Compte\' - Le menu \'Lecture automatique\' est affiché. - Le menu \'Lecture automatique\' est masqué. - Masquer le menu \'Lecture automatique\' - Le menu \'Facturations et paiements\' est affiché. - Le menu \'Facturations et paiements\' est masqué. - Masquer le menu \'Facturations et paiements\' - Le menu \'Sous-titres\' est affiché. - Le menu \'Sous-titres\' est masqué. - Masquer le menu \'Sous-titres\' - Le menu \'Applications connectées\' est affiché. - Le menu \'Applications connectées\' est masqué. - Masquer le menu \'Applications connectées\' - Le menu \'Économie de données\' est affiché. - Le menu \'Économie de données\' est masqué. - Masquer le menu \'Économie de données\' - Le menu \"Paramètres généraux\' est affiché. - Le menu \"Paramètres généraux\' est masqué. - Masquer le menu \'Paramètres généraux\' - Le menu \'Gérer tout l\'historique\' est affiché. - Le menu \'Gérer tout l\'historique\' est masqué. - Masquer le menu \'Gérer tout l\'historique\' - Le menu \'Chat en direct\' est affiché. - Le menu \'Chat en direct\' est masqué. - Masquer le menu \'Chat en direct\' - Le menu \'Notifications\' est affiché. - Le menu \'Notifications\' est masqué. - Masquer le menu \'Notifications\' - Le menu \'Arrière-plan\' est affiché. - Le menu \'Arrière-plan\' est masqué. - Masquer le menu \'Arrière-plan\' - Le menu \'Regarder sur un téléviseur\' est affiché. - Le menu \'Regarder sur un téléviseur\' est masqué. - Masquer le menu \'Regarder sur un téléviseur\' - Le menu \'Centre pour la famille\' est affiché. - Le menu \'Centre pour la famille\' est masqué. - Masquer le menu \'Centre pour la famille\' - Le menu \'Testez de nouvelles fonctionnalités expérimentales\' est affiché. - Le menu \'Testez de nouvelles fonctionnalités expérimentales\' est masqué. - Masquer le menu \'Testez de nouvelles fonctionnalités expérimentales\' - Le menu \'Confidentialité\' est affiché. - Le menu \'Confidentialité\' est masqué. - Masquer le menu \'Confidentialité\' - Le menu \'Achats et abonnement\' est affiché. - Le menu \'Achats et abonnement\' est masqué. - Masquer le menu \'Achats et abonnement\' - Masque des éléments dans le menu paramètres YouTube. - Masquer des paramètres YouTube - Le menu \'Préférences de qualité vidéo\' est affiché. - Le menu \'Préférences de qualité vidéo\' est masqué. - Masquer le menu \'Préférences de qualité vidéo\' - Le menu \'Vos données dans YouTube\' est affiché. - Le menu \'Vos données dans YouTube\' est masqué. - Masquer le menu \'Vos données dans YouTube\' - Le bouton \"Partager\" est affiché. - Le bouton \"Partager\" est masqué. - Masquer le bouton \"Partager\" - Le bouton \"Magasin\" est affiché. - Le bouton \"Magasin\" est masqué. - Masquer le bouton \"Magasin\" - Les liens de produits sont affichés. - Les liens des produits sont masqués. - Masquer les liens des produits - La barre de chaine est affiché. - La barre de chaine est masqué. - Masquer la barre de la chaîne - La section \"Commentaires\" est affiché. - La section \"Commentaires\" est masqué. - Masquer le bouton \"Commentaires\" - Le bouton \"Je n\'aime pas\" est affiché. - Le bouton \"Je n\'aime pas\" est masqué. - Masquer le bouton \"Je n\'aime pas\" - "Les boutons flottants comme \"Utiliser ce son\" sont affichés dans l'onglet Shorts des chaînes." - "Les boutons flottants comme \"Utiliser ce son\" sont masqués dans l'onglet Shorts des chaînes." - Masquer les boutons flottants - Le lien vers la vidéo complète est affiché. - Le lien vers la vidéo complète est masqué. - Masquer le lien de la vidéo complète - Le bouton \'fond vert\' est affiché. - Le bouton \'fond vert\' est masqué. - Masquer le bouton \'fond vert\' - Les panneaux d\'information sont affichés. - Les panneaux d\'information sont masqués. - Masquer les panneaux d\'information - Le bouton \"Rejoindre\" est affiché. - Le bouton \"Rejoindre\" est masqué. - Masquer le bouton \"Rejoindre\" - Le bouton \"J\'aime\" est affiché. - Le bouton \"J\'aime\" est masqué. - Masquer le bouton \"J\'aime\" - L\'en-tête du chat en direct est affiché.\n\nLe bouton de retour sur l\'en-tête ne sera pas masqué. - L\'en-tête du chat en direct est masqué.\n\nLe bouton de retour sur l\'en-tête ne sera pas masqué. - Masquer l\'en-tête du chat en direct - Le bouton \"Localisation\" est affiché. - Le bouton \"Localisation\" est masqué. - Masquer le bouton \"Localisation\" - La barre de navigation est affichée. - La barre de navigation est masqué. - Masquer la barre de navigation - La bannière \"Inclut une communication commerciale\" est affichée. - La bannière \"Inclut une communication commerciale\" est masquée. - Masquer bannière \"Communication commerciale\" - L\'en-tête en pause est affiché. - L\'en-tête en pause est masqué. - Masquer l\'en-tête en pause - Le fond du bouton \"Pause\" est affiché. - Le fond du bouton \"Pause\" est masqué. - Masq. fond du bouton \"Pause\" - Le fond du bouton est affiché. - Le fond du bouton est masqué. - Masquer fond du bouton Lecture & Pause - Le bouton \"Remixer\" est affiché. - Le bouton \"Remixer\" est masqué. - Masquer le bouton \"Remixer\" - Le bouton \"Enregistrer la musique\" est affiché. - Le bouton \"Enregistrer la musique\" est masqué. - Masquer le bouton \"Enregistrer la musique\" - Le bouton \"Suggestions de recherche\" est affiché. - Le bouton \"Suggestions de recherche\" est masqué. - Masquer le bouton \"Suggestions de recherche\" - Le bouton \"Partager\" est affiché. - Le bouton \"Partager\" est masqué. - Masquer le bouton \"Partager\" - Affiché sur les chaînes. - "Masqué sur les chaînes. - -Information : -• Seules les étagères dont l'en-tête est Shorts dans l'onglet d'accueil sont masquées." - Masquer sur les chaînes - Affiché dans \"Historique\". - Masqué dans \"Historique\". - Masquer dans \"Historique\" - Affiché dans les flux \"accueil\" et \"vidéos similaires\". - Masqué dans les flux \"accueil\" et \"vidéos similaires\". - Masquer dans les flux \"accueil\" et \"vidéos similaires\" - Affiché dans les résultats de recherches. - Masqué dans les résultats de recherches. - Masquer dans les résultats de recherches - Affiché dans le flux \"Abonnements\". - Masquer dans le flux \"Abonnements\". - Masquer dans le flux \"Abonnements\" - "Masque les propositions de Shorts. - -Effet secondaire : Les fiches officielles dans les résultats de recherche sont masquées." - Masquer les étagères à Shorts - Le bouton \"Magasin\" est affiché. - Le bouton \"Magasin\" est masqué. - Masquer le bouton \"Magasin\" - Le bouton \"Produit\" est affiché. - Le bouton \"Produit\" est masqué. - Masquer le bouton \"Produit\" - Le bouton \"Son\" est affiché. - Le bouton \"Son\" est masqué. - Masquer le bouton \"Son\" - Les métadonnées de la musique sont affichés. - Les métadonnées de la musique sont masqués. - Masquer les métadonnées de la musique - Les stickers sont affichés. - Les stickers sont masqués. - Masquer les stickers - Le bouton \"S\'abonner\" est affiché. - Le bouton \"S\'abonner\" est masqué. - Masquer le bouton \"S\'abonner\" - Le bouton \"Remercier\" est affiché. - Le bouton \"Remercier\" est masqué. - Masquer le bouton \"Remercier\" - Les produits associés sont affichés. - Les produits associés sont masqués. - Masquer les produits associés - La barre d\'outils est affiché. - La barre d\'outils est masqué. - Masquer la barre d\'outils - Le bouton \"Tendance\" est affiché. - Le bouton \"Tendance\" est masqué. - Masquer le bouton \"Tendance\" - Le bouton \"Utiliser le modèle\" est affiché. - Le bouton \"Utiliser le modèle\" est masqué. - Masquer le bouton \"Utiliser le modèle\" - Le bouton \"Utiliser ce son\" est affiché. - Le bouton \"Utiliser ce son\" est masqué. - Masquer le bouton \"Utiliser ce son\" - Le titre est affiché. - Le titre est masqué. - Masquer le titre de la vidéo - Le bouton \"Voir plus\" est affiché. - Le bouton \"Voir plus\" est masqué. - Masquer le bouton \"Voir plus\" - Les barres d\'actions présentes en haut ou en bas de l\'écran permettant généralement de rafraîchir la page sont affiché. - Les barres d\'actions présentes en haut ou en bas de l\'écran permettant généralement de rafraîchir la page sont masquée. - Masquer les barres d\'actions - Le bouton \"Démarrer l\'essai\" est affiché. - Le bouton \"Démarrer l\'essai\" est masqué. - Masquer le bouton \"Démarrer l\'essai\" - La barre \"Abonnements\" est affiché. - La barre \"Abonnements\" est masqué. - Masquer la barre \"Abonnements\" - Les actions suggérées sont affichées. - Les actions suggérées sont masquées. - Masquer les suggestions d\'actions - "Cette option est obsolète. - -À la place, utilisez l'option 'Paramètres → Lecture automatique → Lecture automatique de la vidéo suivante'." - Les suggestions de vidéos à la fin sont affichés. - "Les suggestions de vidéos à l'écran de fin sont masqué lorsque la lecture automatique est désactivée. - -La lecture automatique peut être modifiée dans les paramètres de YouTube : -'Paramètres → Lecture automatique → Lecture automatique de la vidéo suivante'" - Masq. suggestions vidéos à la fin de vidéo - Le bouton \"Merci\" est affiché. - Le bouton \"Merci\" est masqué. - Masquer le bouton \"Merci\" - Les étagères à tickets sont affichés. - Les étagères à tickets sont masqués. - Masquer les étagères à tickets - L\'horodatage est affiché. - L\'horodatage est masqué. - Masquer l\'horodatage - Les réactions en temps réel sont affichés. - Les réactions en temps réel sont masqués. - Masquer les réactions en temps réel - Le bouton \"Caster\" est affiché. - Le bouton \"Caster\" est masqué. - Masquer le bouton \"Caster\" - Le bouton \"Créer\" est affiché. - Le bouton \"Créer\" est masqué. - Masquer le bouton \"Créer\" - Le bouton \"Notifications\" est affiché. - Le bouton \"Notifications\" est masqué. - Masquer le bouton \"Notifications\" - La section \"Transcription\" est affiché. - La section \"Transcription\" est masqué. - Masquer la section \"Transcription\" - Les publicités vidéos sont affichées. - Les publicités vidéos sont masquées. - Masquer les publicités vidéo - "Les onglets Accueil / Abonnement et les résultats de la recherche sont filtrés pour masquer les vidéos dont le nombre de vues est inférieur ou supérieur à un nombre spécifié. - -Limitations : -- Les shorts ne peuvent pas être masqués. -- Les vidéos avec 0 vue ne sont pas filtrées." - À propos du filtrage par nombre de vues - Les vidéos de la page d\'accueil ne sont pas filtrés. - Les vidéos de la page d\'accueil sont filtrés. - Masquer les vidéos de la page d\'accueil par vues - Les résultats de recherche ne sont pas filtrés. - Les résultats de recherche sont filtrés. - Masquer les résultats de recherche par vues - Les vidéos dans l\'onglet Abonnement ne sont pas filtrés. - Les vidéos dans l\'onglet Abonnement sont filtrés. - Masquer les vidéos de l\'onglet Abonnement par vues - Masque les vidéos recommandées ayant un nombre spécifié inférieur au nombre de vues.\n\nProblème connu : Les vidéos avec 0 vue ne sont pas filtrés. - Masquer les vidéos recommandées par vues - Les vidéos supérieurs à ce nombre de vues sont masqués. - Supérieur au nombre de vues - Les vidéos inférieurs à ce nombre de vues sont masqués. - Inférieur au nombre de vues - K -> 1000\nM -> 1 000 000\nMd -> 1 000 000 000\nviews -> vues - Spécifiez le modèle de langue pour le nombre de vues affiché sous chaque vidéo dans l\'application. Chaque clé (lettre/mot dans votre langue) -> (signification de la clé) doit être sur une nouvelle ligne. Les clés sont placées avant le signe \"->\". Si vous changez la langue de l\'application ou du système, vous devez réinitialiser ce paramètre.\n\nExemples:\nFrançais: 10K vues = K -> 1000, vues -> vues\nAnglais: 10K views = K -> 1000, views -> vues - Voir les filtres - La bannière \"Afficher les produits\" est affichée. - La bannière \"Afficher les produits\" est masquée. - Masquer la bannière \"Afficher les produits\" - Le bouton \"Recherche vocale \" est affiché. - Le bouton \"Recherche vocale \" est masqué. - Masquer le bouton \"Recherche vocale\" - Les résultats web sont affichés. - Les résultats web sont masqués. - Masquer les résultats web - Les Doodles YouTube sont affichés. - Les Doodles YouTube sont masqués. - Masquer les Doodles YouTube - "Les Doodles YouTube apparaissent quelques fois par an. - -Si un Doodle YouTube est actuellement diffusé dans votre région et que ce paramètre est activé, les filtres situés à côté de la barre de recherche sera également masquée." - Le voile du zoom est affiché. - Le voile du zoom est masqué. - Masquer le voile du zoom - Afn Bleu - Afn Rouge - Personnalisé - Officiel - MMT - Revancify Bleu - Revancify Rouge - Youtube - Maintient le mode paysage lorsque l\'écran est éteint et rallumé en mode plein écran. - Durée en millisecondes pendant lesquelles le mode paysage est forcé après l\'allumage de l\'écran. - Durée du maintien du mode paysage - Maintenir le mode paysage - Officiel - L\'action du double appui est désactivé. - "L'action du double appui est activé. - -• Double-appui pour agrandir la vidéo minimisé. -• Double-appui une seconde fois pour revenir à la taille d'origine." - Activer l\'action du double appui - Le glisser-déposer est désactivé. - Le glisser-déposer est activé. - Activer le glisser-déposer - Les boutons agrandir et fermer sont affichés. - Les boutons sont masqués.\n(glissez le minilecteur pour agrandir ou fermer) - Masquer les boutons agrandir et fermer - Les boutons \"Avancer\" et \"Reculer\" sont affichés. - Les boutons \"Avancer\" et \"Reculer\" sont masqués. - Masquer les boutons \"Avancer\" et \"Reculer\" - Les sous-textes sont affichés. - Les sous-textes sont masqués. - Masquer les sous-textes - L\'opacité du minilecteur doit être compris entre 0-100. - Valeur d\'opacité entre 0-100, 0 étant transparent. - Opacité du mini lecteur - Original - Téléphone - Tablette - Moderne 1 - Moderne 2 - Moderne 3 - Style du minilecteur - Ajouts de boutons - "Appuyez pour basculer entre les états de répétition. -Appuyez longuement pour activer la pause après les états de répétition." - Afficher bouton \"Lecture en boucle\" - "Appuyez pour copier l'URL de la vidéo -Appuyez longuement pour copier l'URL horodaté de la vidéo." - "Appuyez pour copier le lien de la vidéo avec l'horodatage. -Appuyez longuement pour copier la vidéo horodatée." - Afficher bouton \"Copier lien horodaté\" - Afficher bouton \"Copier le lien\" - Appuyez pour lancer le téléchargeur externe. - Aff. bouton téléchargement externe - Appuyez pour couper le son de la vidéo en cours. Appuyez à nouveau pour rétablir le son. - Afficher un bouton \"Sourdine\" - Appuyez longuement pour modifier l\'état du bouton. - Vitesse de lecture réinitialisée : %sx. - "Appuyez pour ouvrir des paramètres de vitesse -Appuyez longuement pour revenir à la vitesse de lecture à 1.0x. Appuyez longuement à nouveau rétablir les vitesses par défaut." - Afficher bouton \"Vitesse de lecture\" - "Appuyez pour générer une playlist de toutes les vidéos de la chaîne de la plus ancienne à la plus récente. -Appuyez longuement pour annuler." - Afficher bouton playlist chronologique - Appuyez pour ouvrir la liste blanche. -Appuyez longuement pour ouvrir les paramètres de la liste blanche. - Afficher le bouton \"Liste blanche\" - Si affiché, le bouton \"Télécharger\" natif de la playlist ouvre le téléchargeur natif de l\'appli. - Le bouton de téléchargement natif de la playlist est toujours affiché, tandis que les playlists publiques utilisera votre téléchargeur externe. - Remplacer le bouton de téléchargement de la playlist - Le bouton \"Télécharger\" natif ouvre le téléchargeur de l\'appli. - Le bouton \"Télécharger\" natif ouvre votre téléchargeur externe. - Remplacer le bouton de téléchargement de la vidéo - YouTube Music est requis pour remplacer l\'action du bouton. Cliquez ici pour télécharger YouTube Music. - Prérequis - Le bouton \"YouTube Music\" ouvre l\'appli natif. - Le bouton \"YouTube Music\" ouvre RVX Music. - Remplacer le bouton \"YouTube Music\" - Exclus - Appliqué - Normal - Boutons d\'action - Paramètres supplémentaires - Animation / Retour d\'expérience - Bouton \"Télécharger\" - Options expérimentales - Restriction des images selon les régions - Importer / Exporter sous forme de fichier - Importer / Exporter sous forme de texte - Filtre par mots-clés - Autres - Ajouts de boutons - Informations sur les patchs - Actions rapides - Vidéo recommandée - Étagères Shorts - Actions suggérées - Outil utilisé - Filtre du compteur de vues - Masque ou affiche des éléments dans le menu du compte et dans l\'onglet \"Vous\". - Menu du compte - Masque ou affiche les boutons sous les vidéos. - Boutons sous la vidéo - Publicités - Miniatures alternatives - Désactive ou contourne les restrictions du Mode ambiant. - Mode ambiant - Masque ou affiche la barre de catégorie dans les flux, recherche, et vidéos similaires. - Barre de catégorie - Masque ou affiche des éléments de la barre de chaîne sous la vidéo. - Barre de chaîne - Masque ou affiche des éléments dans le profil de la chaîne. - Profil de la chaîne - Masque ou affiche des éléments de la section commentaires. - Commentaires - Masque ou affiche les posts communautaires dans les flux et sur les chaînes. - Posts communautaires - Masque les éléments utilisant des filtres personnalisés. - Filtre personnalisé - Masque ou Affiche des options du menu déroulant dans les flux. - Menu déroulant - Flux - Masque ou change les éléments liés au plein écran. - Plein écran - Interface - Désactive ou active la vibration. - Vibration - Remplace l\'action des boutons in-app. - Boutons d\'action - Importer ou exporter les paramètres. - Importer / Exporter les paramètres - Change le style du lecteur minimisé de l\'application. - Minilecteur - Paramètres avancés - Masque ou affiche les éléments de la barre de navigation. - Barre de navigation - Informations sur les patchs appliqués. - Informations sur les patchs - Masque ou affiche les boutons sur les vidéos. - Boutons du lecteur - Masque ou modifie des options du menu déroulant dans le lecteur vidéo. - Menu \"Paramètre\" déroulant - Lecteur - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Personnaliser les éléments de la barre de progression. - Barre de progression - Masque des éléments dans le menu paramètre YouTube. - Menu paramètre - Masque ou affiche des composants dans le lecteur Shorts. - Lecteur Shorts - Shorts - Falsifier les données de diffusion en direct afin d\'éviter les problèmes de lecture. - Falsifier les données de diffusion en direct - Contrôles par gestes - Masque ou change les éléments situés dans la barre d\'outils, tels que les boutons de la barre d\'outils, la barre de recherche, l\'en-tête. - Barre d\'outils - Masque ou affiche les éléments de la description de la vidéo. - Description vidéo - Masque les vidéos par mot-clés ou par vues. - Filtre vidéo - Vitesses et qualités vidéo - Modifie les paramètres liés à l\'historique de visionnage. - Historique de visionnage - La hauteur de l\'action rapide doit être comprise entre 0 et 32. - Configure l\'espacement entre la barre de progression et le conteneur d\'actions rapides, entre 0 et 32. - Hauteur de la barre de progression - "Force le rejet de la réponse du codec AV1. -Un codec différent sera appliqué après environ 20 secondes de mise en mémoire tampon." - Rejeter la réponse du codec AV1 - Le processus de rejet entraîne une mise en mémoire tampon d\'environ 20 secondes. - Décalage - La modification de la vitesse de lecture est appliqué pour la vidéo en cours. - La modification de vitesse de lecture est appliqué pour toutes les vidéos. - Enreg. modif. de la vitesse de lecture - Un message ne sera pas affiché lorsque vous modifiez la vitesse de lecture par défaut. - Un message sera affiché lorsque vous modifiez la vitesse de lecture par défaut. - Afficher un message - Vitesse de lecture modifiée par %s. - La modification de la résolution est appliqué pour la vidéo en cours. - La modification de la résolution est appliqué pour toutes les vidéos. - Enreg. modification de la qualité - Un message ne sera pas affiché lorsque vous modifiez la qualité vidéo par défaut. - Un message sera affiché lorsque vous modifiez la qualité vidéo par défaut. - Afficher un message - La résolution sur les données mobiles a été modifiée par %s. - Impossible de définir la qualité vidéo. - La résolution sur le Wi-Fi a été modifiée par %s. - "Supprime le message \"Confirmer votre âge\". -Cela ne contourne pas la restriction d'âge, mais le confirme automatiquement." - Suppr. Message \"Confirmer votre âge\" - Remplacer le codec AV1 par le codec VP9. - Remplacer le codec AV1 - L\'identifiant de la chaîne est utilisé. - Le nom de la chaîne est utilisé. - Remplacer l\'identifiant de la chaîne - Appuyez pour afficher le temps restant. - Appuyez pour ouvrir le menu déroulant de la vitesse de lecture ou de la qualité vidéo. - Modifie l\'action de l\'horodatage - Remplace le bouton \"Créer\" par le bouton \"Paramètre\". - Remplacer le bouton \"Créer\" - "Appuyez pour ouvrir les paramètres YouTube. -Appuyez longuement pour ouvrir les paramètres RVX." - "Appuyez pour ouvrir les paramètres RVX. -Appuyez longuement pour ouvrir les paramètres YouTube." - Action à attribuer au bouton - Les miniatures de la barre de progression sont affichées en mode plein écran. - Les anciennes miniatures sont affichées au-dessus de barre de progression. - Restaure les anciennes miniatures de la barre de progression - Masque la nouvelle interface de qualité vidéo. - Affiche l\'ancienne interface de qualité vidéo. - Restaur. ancien. interface de qualité vidéo - \@identifiant (Nom d\'utilisateur) - Format d\'affichage - Nom d\'utilisateur (@identifiant) - Nom d\'utilisateur - L\'identifiant est utilisé. - Nom d\'utilisateur utilisé. - Activer Return YouTube Username - "La clé YouTube Data API v3 est nécessaire pour remplacer les identifiants par des noms d'utilisateurs. - -Le quota journalier pour les clés API sur le plan gratuit est de 10 000, 1 quota est utilisé pour remplacer l'identifiant par un nom d'utilisateur pour 1 commentaire. - -Cliquez ici pour découvrir comment créer une clé API." - À propos de la clé YouTube Data API - La clé de développeur pour utiliser YouTube Data API v3. - Clé API des données YouTube - 1. Allez sur <a href=%1$s>Nouveau projet</a>.<br>2. Cliquez sur le bouton <b> Créer</b>.<br>3. Allez sur <a href=%2$s>YouTube Data API v3</a>.<br>4. Cliquez sur le bouton <b>ACTIVER</b>.<br>5. Cliquez sur le bouton <b>CRÉER DES IDENTIFIANTS</b>.<br>6. Sélectionnez l\'option <b>Données Publiques</b>.<br>7. Cliquez sur le bouton <b>SUIVANT</b>.<br>8. Copiez la clé API.<br><br>※ La clé API ne doit jamais être partagée avec d\'autres personnes, par conséquent, elle n\'est pas incluse dans les paramètres Importer / Exporter. - Obtenir une clé développeur pour YouTube Data API v3 - À propos - Les données des \"Je n\'aime pas\" sont fournies par l\'API de Return YouTube Dislike. Appuyez ici pour en savoir plus. - ReturnYouTubeDislike.com - Le bouton \"J\'aime\" s\'affiche avec une meilleure apparence. - Le bouton \"J\'aime\" s\'affiche avec une taille réduite. - Bouton \"J\'aime\" compact - Affiche les \"Je n\'aime pas\" en nombre. - Afficher les \"Je n\'aime pas\" en pourcentage. - \"Je n\'aime pas\" en pourcentage - Les \"Je n\'aime pas\" ne sont pas affichés. - Les \"Je n\'aime pas\" sont affichés. - Activer Return YouTube Dislike - Les \"J\'aime\" estimés sont masqués. - Les \"J\'aime\" estimés sont affichés. - Afficher les \"J\'aime\" estimés - Les \"Je n\'aime pas\" sont indisponibles (le client a atteint la limite de l\'API). - Les \"Je n\'aime pas\" sont indisponible (status %d). - Les \"Je n\'aime pas\" sont temporairement indisponible (API obsolète). - Les \"Je n\'aime pas\" sont indisponible (%s). - Recharger la vidéo pour voter avec Return YouTube Dislike - Les \"Je n\'aime pas\" sur les Shorts sont masqués. - Affiche les \"Je n\'aime pas\" sur les Shorts. - "Affiche les \"Je n'aime pas\" sur les Shorts. - -Limitation : les \"Je n'aime pas\" ne seront pas affichées si vous n'êtes pas connectés ou en mode incognito." - Afficher les \"Je n\'aime pas\" sur les Shorts - N\'affiche pas de message si Return YouTube Dislike est indisponible. - Affiche un message si Return YouTube Dislike est indisponible. - Affiche un message si l\'API est indisponible - Masqué - Supprime les paramètres de suivi (tracking) des URL lors du partage de liens. - Nettoyer les liens partagés - "Les phrases telles que '#', 'Financement' 'Magasin' et 'produits' seront affichés sur les sous-titres vidéos." - "Les phrases telles que '#', 'Financement' 'Magasin' et 'produits' seront masqués sur les sous-titres vidéos." - Nettoyer les sous-titres vidéo - À propos - sponsor.ajay.app - Les données sont fournies par l\'API SponsorBlock. Cliquez ici pour en savoir plus et voir les téléchargements pour d\'autres plateformes. - L\'URL de l\'API a été modifiée. - L\'URL de l\'API est invalide. - L\'URL de l\'API a été réinitialisé. - Apparence - Couleur modifiée. - Couleur : - Code couleur invalide. - Couleur réinitialisée. - Création de nouveaux segments - Modifier le fonctionnement des segments - Masquer auto. le bouton \"Passer\" - Le bouton \"Passer\" est affichée tout le long du segment. - Masque le bouton \"Passer\" après quelques secondes. - Utiliser un bouton \"Passer\" compact - Meilleure apparence pour le bouton \"Passer\". - La taille du bouton \"Passer\" est réduit. - Afficher le bouton \"Créer un segment\" - Le bouton \"Créer un nouveau segment\" est masqué. - Le bouton \"Créer un nouveau segment\" est affiché. - Activer SponsorBlock - SponsorBlock est un service d\'entraide permettant de passer des parties gênantes des vidéos YouTube. - Afficher le bouton \"Voter\" - Le bouton \"Voter\" des segments est masqué. - Le bouton \"Voter\" des segments est affiché. - Général - Ajuster le nouveau segment - La valeur doit être un nombre positif. - Ajuster la durée en millisecondes lors de la création de nouveaux segments. - Modifier l\'URL de l\'API - L\'adresse qu\'utilise SponsorBlock pour contacter le serveur. - Durée minimale du segment - Durée invalide. - Les segments plus courts que cette valeur (en secondes) ne pourront être affichés ou passés. - Activer le compteur de segments passés - Le compteur de segments passés n\'est pas activé. - Permet au classement de SponsorBlock de savoir combien de temps a été gagné. Un message est envoyé au classement chaque fois qu\'un segment est sauté. - Afficher un message lors du passage auto - N\'affiche pas de message. Appuyez ici pour voir un exemple. - Affiche un message lorsqu\'un segment est automatiquement passé. Appuyez ici pour voir un exemple. - Afficher le temps sans compter les segments - La durée complète de la vidéo est affichée. - Affiche la durée de la vidéo sans compter les segments (à côté de la durée totale de la vidéo). - Votre identifiant privé - L\'identifiant d\'utilisateur privé doit comporter au moins 30 caractères. - Cela doit rester privé. Il s\'agit d\'un mot de passe qui ne doit être communiqué à personne. Si quelqu’un l\'a, il peut usurper votre identité ! - Déjà lu - Lisez les directives de SponsorBlock avant de créer un nouveau segment. - Afficher - Suivez les directives - Les directives contiennent des règles et des conseils pour créer de nouveaux segments. - Afficher les directives - Choisissez la catégorie du segment - Le segment dure de %1$02d:%2$02d à %3$02d:%4$02d (%5$d minutes et %6$02d secondes)\nEst-il prêt à être soumis ? - Le segment est de\n\n%1$s\nà\n%2$s\n\n(%3$s)\n\nPrêt à être soumis ? - La durée est-elle correcte ? - La catégorie est désactivée dans les paramètres. Activez la catégorie pour soumettre. - Souhaitez-vous modifier la durée du début ou de la fin du segment ? - La durée spécifiée est invalide. - Éditer la durée du segment manuellement - Définir %s comme début ou fin du nouveau segment ? - fin - Marquez d\'abord deux points sur la barre de progression. - début - maintenant - Prévisualisez le segment et assurez-vous qu\'il se déroule correctement. - Le début doit précéder la fin. - Définir la fin du segment à - Définir le début du segment à - Nouveau segment SponsorBlock - Réinitialiser - Réinitialiser la couleur - Remplissage / Blagues - Scènes secondaires ajoutées uniquement à des fins de remplissage ou d\'humour qui ne sont pas nécessaires à la compréhension de la vidéo. N\'inclus pas les segments fournissant un contexte ou des détails utiles. - Point clé - Partie de la vidéo que la plupart des personnes veulent voir. - Rappel d\'interaction (S\'abonner) - Un bref rappel pour aimer, s\'abonner ou pour les suivre au milieu du contenu. S\'il est long ou s\'il traite d\'un sujet spécifique, il devrait être placé dans la section \"auto-promotion\". - Intro / Entracte - Un intervalle sans contenu réel. Il peut s\'agir d\'une pause, d\'une image fixe ou d\'une animation répétitive. Ne comprends pas des passages contenant des informations. - Musique : Section non musicale - Uniquement destiné pour les vidéos musicales. Sections de vidéos musicales sans musique, qui ne sont pas déjà couvertes par une autre catégorie. - Outro / Crédits - Crédits ou lorsque les cartes de fin de vidéo YouTube apparaissent. Ne pas utiliser pour des conclusions avec des informations. - Aperçu / Récapitulatif - Aperçu de clips qui montrent ce qui est à venir ou ce qui s\'est passé dans la vidéo ou dans d\'autres vidéos de la série, lorsque les informations sont répétées ailleurs. - Auto-promotion / Non Rémunérée - Similaire à \'Sponsor\', à l\'exception de la promotion non rémunérée ou l\'autopromotion. Comprend les sections produits, les dons, et des informations sur les partenaires avec lesquels ils ont collaboré. - Sponsor - Promotion rémunérée, parrainage rémunérés et publicités directes. Ne concerne pas l\'autopromotion ou les mentions gratuites pour des causes / créateurs / sites web / produits qu\'ils apprécient. - Copier - Exportation échouée : %s. - Importer / Exporter des paramètres - Votre configuration SponsorBlock au format JSON pouvant être importée/exportée sur ReVanced Extended et sur d\'autres plateformes SponsorBlock. - Votre configuration SponsorBlock au format JSON pouvant être importée/exportée sur ReVanced Extended et sur d\'autres plateformes SponsorBlock. Cela inclut votre identifiant d\'utilisateur privé. Partagez-la prudemment. - Importation échouée : %s. - Paramètres importés avec succès. - Vos paramètres contiennent un identifiant utilisateur SponsorBlock privé.\n\nVotre identifiant d\'utilisateur est comme un mot de passe et ne doit jamais être partagé.\n - Ne plus afficher ce message - Paramètres copiés dans le presse-papier. - Passer automatiquement - Passer automatiquement une fois - Passer - Point clé - Passer le remplissage - Passer au point clé - Passer l\'interaction - Passer l\'intro - Passer entracte - Passer entracte - Passer le non musical - Passer l\'outro - Passer l\'aperçu - Passer le résumé - Passer l\'aperçu - Passer la promotion - Passer le sponsor - Passer le segment - Désactiver - Afficher dans la barre de progression - Afficher un bouton \"Passer\" - Remplissages passés. - Passé au point clé. - Rappel ennuyeux passé. - Intro passée. - Entracte passé. - Entracte passé. - Plusieurs segments passés. - Section non musicale passée. - Outro passée. - Aperçu passé. - Résumé passé. - Aperçu passé. - Autopromotion passée. - Sponsor passé. - Segment non soumis passé. - SponsorBlock est temporairement indisponible. - SponsorBlock est temporairement indisponible (status %d). - SponsorBlock est temporairement indisponible (API obsolète). - Statistiques - Les statistiques sont temporairement indisponibles (API indisponible). - Chargement... - Votre réputation est de <b>%.2f</b> - Vous avez sauvé les utilisateurs de <b>%s</b> segments - %1$s heures et %2$s minutes - %1$s minutes et %2$s secondes - %s secondes - Cela représente <b>%s</b> de leur vie.<br>Appuyez ici pour voir le classement. - Appuyez ici pour voir les statistiques globales et les meilleurs contributeurs. - Classement SponsorBlock - SponsorBlock est désactivé. - Vous avez passé <b>%s</b> segment(s) - Voulez-vous réinitialiser le compteur de segment ? - Cela représente <b>%s</b>. - Vous avez créé <b>%s</b> segment(s) - Appuyez ici pour voir vos segments. - Votre nom d\'utilisateur : <b>%s</b> - Modifier votre nom d\'utilisateur - Impossible de changer le nom d\'utilisateur : Statut : %1$d %2$s. - Nom d\'utilisateur modifié avec succès. - Impossible de soumettre le segment.\nExiste déjà. - Impossible de soumettre le segment : %s. - Impossible de soumettre le segment : %s. - Impossible de soumettre le segment.\nLimite de requêtes dépassée (utilisateur/IP). - SponsorBlock est temporairement indisponible. - Impossible de soumettre le segment (état : %1$d %2$s). - Segment soumis avec succès. - N\'affiche pas de message si SponsorBlock est indisponible. - Affiche un message si SponsorBlock est indisponible. - Afficher un message si l\'API est indisponible - Changer de catégorie - Voter contre - Impossible de voter pour le segment : %s. - Impossible de soumettre les segments (API obsolète). - Impossible de voter pour le segment (état : %1$d %2$s). - Il n\'y a aucun segments pour lesquels voter. - Voter pour - Paramètres copiés dans le presse-papier. - Lien avec horodatage copié dans le presse-papiers. (%s) - URL copié dans le presse-papier. - URL avec horodatage copié dans le presse-papier. - Original - Pouce en l\'air - Pouce en l\'air (Cairo) - Cœur - Cœur (Teinte) - Masqué - Animation lors du double appui - La marge en bas du panneau méta doit être entre 0-64. - Configurer l\'espace de la barre de progression au panneau méta, entre 0-64. - Marge en bas du panneau Meta - La hauteur en pourcentage doit être entre 0-100 (%). - Configure la hauteur en pourcentage de l\'espace vide à gauche lorsque la barre de navigation est cachée, entre 0 et 100 (%). - Hauteur en pourcentage de l\'espace vide - Appuyez longuement sur l\'horodatage pour modifier l\'état de répétition des Shorts. - Action de l\'horodatage appui long - "Affiche la section du titre de la vidéo en plein écran. - -Limitation : Le titre de la vidéo disparaît lorsque vous cliquez dessus." - Afficher la section titre de la vidéo - Si la lecture automatique est activée, la vidéo suivante sera lue après la fin du compte à rebours. - Si la lecture automatique est activée, la vidéo suivante sera lue immédiatement. - Ignorer le compteur lecture auto - "Passe le tampon préchargé au début de la vidéo pour appliquer immédiatement la qualité vidéo. - -Info : -• Au démarrage de la vidéo, il y a un délai d'environ 0.3 seconde. -• Ne s'applique pas aux vidéos HDR, aux diffusions en direct et aux vidéos de moins de 15 secondes." - Tampon préchargé - Le message est masqué. - Le message est affiché. - Affic. message lors du passage - Activer ce paramètre peut entraîner des problèmes de lecture vidéo. - Tampon préchargé passé. - La valeur de la vitesse de lecture doit être comprise entre 0-8.0. - La valeur de vitesse de lecture doit être comprise entre 0-8.0. - Valeur de \"Vitesse de lecture\" - "Falsification de l'ancienne version du client - -Celle-ci modifiera l'apparence de l'application, mais des effets indésirables inconnus peuvent se produire -Si elle est désactivée ultérieurement, l'ancienne interface utilisateur peut subsister jusqu'à ce que les données de l'application soient effacées" - Version non falsifiée - Version falsifiée - 17.33.42 - Restaure l\'ancienne interface - 17.41.37 - Restaure l\'ancien menu \"Playlist\" - 18.05.40 - Restaure l\'ancien menu \"Commentaires\" - 18.17.43 - Restaure l\'ancien menu déroulant du lecteur - 18.33.40 - Restaure l\'ancienne barre d\'action Shorts - 18.38.45 - Restaure l\'ancien menu de qualité vidéo - 18.48.39 - Désactive les \"vues\" et \"j\'aime\" en temps réel - 19.13.37 - Restaure l\'ancienne animation en temps réel des nombres - Choisir la version à falsifier - Saisissez la version de l\'application à falsifier. - Saisir la version à falsifier - Falsifier la version de l\'app - "La version de l'application sera falsifiée par une ancienne version de YouTube. - -Cela modifie l'apparence et les fonctionnalités de l'application, mais des effets inconnus peuvent se produire. - -Si désactivé ultérieurement, il est recommandé d'effacer les données de l'application pour éviter des bugs d'interface." - "Falsifie les dimensions de l'appareil a la valeur maximale. -Les hautes qualités peuvent être débloquées sur certaines vidéos qui requièrent des appareils ayant des dimensions élevées, mais pas sur toutes les vidéos." - Falsifier les dimensions de l\'appareil - Les codecs vidéos d\'iOS sont AVC (H.264), VP9, ou AV1. - Le codec vidéo d\'iOS est AVC (H.264). - Forcer iOS AVC (H.264) - "Activer ce paramètre peut améliorer l'autonomie de la batterie et résoudre les problèmes de lecture. - -AVC (H.264) a une résolution maximale de 1080p, et la lecture vidéo utilisera plus de données internet que le VP9 ou le AV1." - "• Le menu \"Piste Audio\" est manquant. -• Le volume stable n'es pas disponible." - "• Le menu \"Piste Audio\" est manquant. -• Le volume stable n'es pas disponible." - "• Les films ou les vidéos payantes peuvent ne pas être lus. -• Les diffusions en direct commencent au début. -• Les vidéos peuvent se terminer une seconde avant. -• Pas de codec audio opus." - Effets inconnus de la falsification - • Les vidéos peuvent ne pas être lus. - Le client utilisé pour récupérer les données de lecture en direct est masqué dans \"Statistiques pour les nerds\". - Le client utilisé pour récupérer les données de lecture en direct est affiché dans \"Statistiques pour les nerds\". - Afficher dans \"Statistiques pour les nerds\" - "Les données de lecture en direct ne sont pas falsifiées. La lecture vidéo peut ne pas fonctionner." - Les données de lecture en direct sont falsifiées. - Falsifier les données de lecture en direct - Android - Android TV - Android VR - iOS - Client par défaut - Désactiver ce paramètre peut entraîner des problèmes de lecture vidéo. - La sensibilité des gestes de luminosité doit être comprise entre 1-1000 (%). - Configurez la sensibilité minimale des gestes de luminosité entre 1 et 1000 (%).\nPlus la sensibilité minimale est courte, plus le niveau de luminosité change rapidement. - Sensibilité des gestes de luminosité - Les contrôles par gestes sont désactivés en mode \"Écran verrouillé\". - Les contrôles par gestes sont activés en mode \"Écran verrouillé\". - Gestes en mode \"Écran verrouillé\" - Auto - L\'intensité du mouvement à effectuer pour que les gestes se produise. - Intensité des gestes - La visibilité de l\'opacité du voile lors des gestes. - Visibilité du voile lors des gestes - La zone glissable ne peut pas être supérieure à 50. - Pourcentage de la zone de l\'écran pouvant être glissée.\n\nNote : Cela affecte également la zone du double appui pour avancer/reculer dans la vidéo. - Taille de la zone de gestes - La taille du texte pendant le voile lors du geste. - Taille du texte superposé - La durée en millisecondes pendant laquelle la superposition est visible. - Durée du voile lors des gestes - La sensibilité des gestes de volume doit être comprise entre 1-1000 (%). - Configurer la sensibilité minimale des gestes de volume entre 1 et 1000 (%).\n\nPlus la sensibilité minimale est courte, plus le niveau du volume change rapidement.\n\nLa sensibilité recommandée des gestes de volume est de 100 % par paliers de 15 volumes et de 10 % par paliers de 150 volumes. - Sensibilité des gestes de volume - "Échange la position des boutons \"Créer\" et \"Notifications\" en falsifiant les informations de l'appareil. - -• L'appareil peut nécessiter un redémarrage pour que ce paramètre prenne effet. -• Désactiver ce paramètre charge plus de publicités depuis le serveur. -• Vous devez désactiver ce paramètre afin de rendre les publicités vidéo visibles." - Le bouton \"Créer\" n\'est pas échangé avec le bouton \"Notification\". - "Le bouton \"Créer\" est échangé avec le bouton \"Notification\". - -Note : Activer ceci masquera également les publicités vidéos." - Échanger \"Créer\" et \"Notifications\" - "Désactiver ceci pourrait charger plus de publicités depuis le serveur. - -Également, les publicités ne seront plus bloquées sur les Shorts. - -Si ce paramètre ne fait pas effet, essayer de passer en mode Incognito." - Officiel - RVX Music - %s n\'est pas installé. Veuillez l’installer. - Nom du paquet de RVX Music installé. - Nom du paquet de RVX Music - • L\'historique de visionnage est bloqué. - "• Suit les paramètres de l'historique de visionnage du compte Google. -• L'historique de visionnage peut ne pas fonctionner en raison d'un DNS ou d'un VPN." - • Suit les paramètres de l\'historique de visionnage du compte Google. - Statut de l\'historique de visionnage - Cliquez pour ouvrir la gestion de l\'historique de visionnage YouTube. - Gérer tout l\'historique - Original - Remplacer le domaine - Bloquer l\'historique de visionnage - Type d\'historique de visionnage - Impossible d\'ajouter la chaîne \'%1$s\' à la liste blanche \'%2$s\'. - La chaîne \'%1$s\' a été ajoutée à la liste blanche \'%2$s\'. - Il n\'y a pas de chaînes dans la liste blanche. - Non ajouté à la liste blanche. - Impossible de charger les informations de la chaîne. - Ajouté à la liste blanche. - Vitesse de lecture - Supprimer la chaîne \'%1$s\' de la liste blanche \'%2$s\'? - Impossible de supprimer la chaîne \'%1$s\' de la liste blanche \'%2$s\'. - La chaîne \'%1$s\' a été retirée de la liste blanche \'%2$s\'. - Vérifier ou supprimer les chaînes ajoutés à la liste blanche. - Liste blanche des chaînes - SponsorBlock - diff --git a/src/main/resources/youtube/translations/hu-rHU/missing_strings.xml b/src/main/resources/youtube/translations/hu-rHU/missing_strings.xml deleted file mode 100644 index 43788e232..000000000 --- a/src/main/resources/youtube/translations/hu-rHU/missing_strings.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - Don\'t show again - Courses / Learning - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - AI-generated video summary section is shown. - AI-generated video summary section is hidden. - Hide AI-generated video summary section - MMT Blue - MMT Green - MMT Orange - MMT Pink - MMT Turquoise - MMT Yellow - Revancify Yellow - Vanced Black - Vanced Light - Xisr Yellow - Adjust: Mark Start and End Time for segment - Verify the Segment - Edit the Segment - Forward by Specified Time (Default: 150ms) - Publish Created Segment - Rewind by Specified Time (Default: 150ms) - diff --git a/src/main/resources/youtube/translations/hu-rHU/strings.xml b/src/main/resources/youtube/translations/hu-rHU/strings.xml deleted file mode 100644 index 76636b731..000000000 --- a/src/main/resources/youtube/translations/hu-rHU/strings.xml +++ /dev/null @@ -1,1689 +0,0 @@ - - - Engedélyezi a videolejátszó akadálymentesítési vezérlőit? - Az eszközök azért módosulnak, mert egy akadálymentesítési szolgáltatás be van kapcsolva. - Folytatás - "A MicroG GmsCore nem rendelkezik engedéllyel, hogy a háttérben futhasson.\n\nKövesse a \"Don't kill my app\" útmutatót a telefonjához és alkalmazza a leírtakat a MicroG telepítésre.\n\nEz szükséges az alkalmazás működéséhez." - "A hibák megelőzése érdekében a MicroG GmsCore akkuoptimalizálását ki kell kapcsolni.\n\nKoppintson a folytatás gombra és kapcsolja ki az akkuoptimalizálást." - Webhely megnyitása - Művelet szükséges - Engedélyezze a felhő alapú üzenetküldést az értesítések fogadásához. - GmsCore megnyitása - MicroG GmsCore nincs telepítve. Telepítse. - "A DeArrow közösségi bélyegképeket biztosít a YouTube videókhoz. Ezek a bélyegképek gyakran helytállóbbak, mint a YouTube által biztosítottak. - -Ha engedélyezve van, a videó URL-je elküldésre kerül az API szerverre, de más adat nem lesz elküldve. Ha egy videónak nincs DeArrow bélyegképe, akkor az eredeti vagy egy pillanatkép jelenik meg. - -Koppints ide, ha többet szeretnél megtudni a DeArrow-ról." - A DeArrow-ról - Érvénytelen DeArrow API URL. - A DeArrow bélyegkép cache végpont URL-je. - DeArrow API végpont - Nem jelenik meg üzenet, ha a DeArrow nem elérhető. - Üzenet megjelenítése, ha a DeArrow nem elérhető. - Üzenet megjelenítése, ha az API nem elérhető - A DeArrow átmenetileg nem érhető el. (státusz kód: %s) - A DeArrow átmenetileg nem elérhető. - Kezdőlap fül - Te lap - Eredeti indexképek - DeArrow & eredeti indexképek - DeArrow és pillanatképek - Pillanatképek - Lejátszó lejátszási listák, ajánlások - Keresési találatok - Állóképfelvételek - Pillanatképek minden videó elejéről/közepéről/végéről készülnek. Ezek a képek be vannak építve a YouTube-ba és nem használnak külső API-t. - Videó pillanatképek - Magas minőségű pillanatképeket használ. - Közepes minőségű pillanatképeket használ. A bélyegképek gyorsabban betöltődnek, de az élő közvetítések, kiadatlan vagy nagyon régi videók üres bélyegképeket jeleníthetnek meg. - Gyors pillanatképek használata - Videó kezdete - Videó közepe - Videó vége - Pillanatkép készítésének ideje - Előfizetések oldal - Az információ nem kerül hozzáadásra az időbélyeghez. - "Az információ hozzáadásra kerül az időbélyeghez." - Időbélyegző információ hozzáfűzése - Lejátszási sebesség hozzáadása. - Videóminőség hozzáadása. - Információ típusának hozzáfűzése - A mozifilmes világítás mód le van tiltva akkumulátorkímélő üzemmódban. - A mozifilmes világítás mód engedélyezett akkumulátorkímélő üzemmódban. - Megkerüli a mozifilmes világítás mód korlátozásait - A domain, ahonnan a képeket le lehet hívni.\nMegjegyzés: Csak a domainnevet írd be, azaz a \"https\:\/\/\" előtag nélkül. - Alternatív domain - Az eredeti cím használata a képekhez.\n\nEnnek engedélyezése javíthatja a hiányzó képeket, amelyek bizonyos régiókban le vannak tiltva. - Az yt4.ggpht.com cím használata a képekhez. - Területi kép-korlátozások megkerülése - Eredeti - Telefon - Telefon (max 480 dpi) - Tablet - Tablet (min 600 dpi) - Elrendezés megváltoztatása - Kapcsolók használata. - Szövegkapcsolók használata. - Kapcsolótípus megváltoztatása - Az app-on belüli megosztási lap van használatban. - A rendszer megosztási lap van használatban. - Megosztási lap módosítása - Automatikus lejátszás - Alapértelmezett - Szünet - Ismétlés - Shorts ismétlési állapotának módosítása - Csatornák böngészése - Alapértelmezett - Felfedezés - Játékok - Előzmények - Könyvtár - Kedvelt videók - Élő - Filmek - Zene - Keresés - Shorts - Sport - Feliratkozások - Felkapott - Megnézem később - Kezdőlap megváltoztatása - A kiinduló lap csak egyszer változik. - "A kiinduló lap mindig változik. - -Korlátozás: Előfordulhat, hogy az eszköztár Vissza gombja nem működik." - Kiinduló lap megváltoztatása - Az általános fejléc van engedélyezve. - A prémium fejléc van engedélyezve. - YouTube fejléc módosítása - A szűrendő összetevők listája új sorokkal elválasztva. - Egyéni szűrő - Az egyéni szűrő le van tiltva. - Az egyéni szűrő engedélyezett. - Egyéni szűrő engedélyezése - Érvénytelen egyéni szűrő: %s. - A régi stílusú flyout menüt használja. - Egyéni párbeszédpanelt használ. - Egyedi lejátszási sebesség menü típusa - Ennek kevesebbnek kell lenniük, mint %s. Alap értékek használata. - Érvénytelen sebesség. Az alap értékek használata. - Az elérhető lejátszási sebességek módosítása vagy hozzáadás - Egyedi lejátszási sebesség - A lejátszó átlátszóságának 0 és 100 között kell lennie. Visszaállítás alapértelmezettre. - Átlátszósági érték 0 és 100 között, ahol a 0 az átlátszó. - Egyéni lejátszó átlátszóság beállítása - Írja be a keresősáv színének hexadecimális kódját. - Keresősáv egyéni színe - A YouTube linkek megnyitásához az RVX-ben engedélyezze az \'Támogatott linkek megnyitása\' és engedélyezze a támogatott webcímeket. - Alapértelmezett program beállítások megnyitása - Alapértelmezett lejátszási sebesség - Alapértelmezett videó minőség mobilhálózaton - Alapértelmezett videó minőség Wi-Fi hálózaton - Letiltja a \'Mozifilmes világítás\' módot teljes képernyőn. - A mozifilmes világítás engedélyezett teljes képernyőn. - A mozifilmes világítás le van tiltva teljes képernyőn. - Mozifilmes világítás mód letiltása teljes képernyőn - Mindig tiltsa le \'Mozifilmes világítás\' módot. - A mozifilmes világítás engedélyezett. - A mozifilmes világítás letiltva. - Mozifilmes világítás mód letiltása - A kényszerített automatikus hangsávok engedélyezve vannak. - A kényszerített automatikus hangsávok le vannak tiltva. - Kényszerített automatikus hangsávok letiltása - A kényszerített automatikus feliratok engedélyezve vannak. - A kényszerített automatikus feliratok le vannak tiltva. - Kényszerített automatikus feliratok letiltása - Az automatikus lejátszó felugró panelek engedélyezve vannak. - Az automatikus lejátszó felugró panelek le vannak tiltva. - Lejátszó felugró panelek letiltása - "Az automatikus mix lejátszási listák engedélyezve vannak, ha az automatikus lejátszás be van kapcsolva. - -Az automatikus lejátszás a YouTube beállításaiban módosítható: -Beállítások → Automatikus lejátszás → Következő videó automatikus lejátszása" - Az automatikus mix lejátszási listák le vannak tiltva. - Mix lejátszási listák letiltása - A funkció engedélyezése letiltja az automatikus váltást a YouTube Mix szolgáltatásra, amikor zenét játszik le, miközben az automatikus lejátszás be van kapcsolva. - Az alapértelmezett lejátszási sebesség engedélyezve van az élő közvetítésnél. - Az alapértelmezett lejátszási sebesség le van tiltva élő közvetítésnél. - Lejátszási sebesség letiltása élő közvetítésnél - Az alapértelmezett lejátszási sebesség engedélyezett zene lejátszásnál. - "A zene alapértelmezett lejátszási sebessége le van tiltva. - -Korlátozás: Előfordulhat, hogy ez a beállítás nem vonatkozik azokra a videókra, amelyek nem tartalmazzák a „Hallgassa meg a YouTube Musicon” bannert." - Lejátszási sebesség zenéhez kiválasztás elrejtése - Az interakciós panel engedélyezve. - Az interakciós panel letiltva. - Interakciós panel letiltása - A haptikus visszajelzés be van kapcsolva. - A haptikus visszajelzés ki van kapcsolva. - Fejezetek haptikus jelzésének kikapcsolása - A haptikus visszajelzés be van kapcsolva. - A haptikus visszajelzés ki van kapcsolva. - Súrolás haptikus visszajelzésének letiltása - A haptikus visszajelzés be van kapcsolva. - A haptikus visszajelzés ki van kapcsolva. - Lejátszó csúszka haptikus visszajelzésének letiltása - A haptikus visszajelzés be van kapcsolva. - A haptikus visszajelzés ki van kapcsolva. - Keresés visszavonás haptikus visszajelzésének kikapcsolása - A haptikus visszajelzés be van kapcsolva. - A haptikus visszajelzés ki van kapcsolva. - Nagyítás haptikus visszajelzésének letiltása - Az automatikus HDR fényerő engedélyezett. - Az automatikus HDR fényerő le van tiltva. - Automatikus HDR fényerő letiltása - A HDR videó engedélyezve van. - A HDR videó le van tiltva. - HDR videó letiltása - A videó tájolása követi a készülék beállításait fullscreenben. - A videó tájolása portré mód fullscreenben. - Fekvő mód letiltása - A tetszik és nem tetszik gombok ragyognak megnyomáskor. - A tetszik és nem tetszik gombok nem ragyognak megnyomáskor. - Tetszik és nem tetszik gombok ragyogásának letiltása - "A CronetEngine QUIC protokoll letiltása." - QUIC protokoll letiltása - A Shorts lejátszó újraindul az alkalmazás indításakor. - A Shorts lejátszó nem indul újra az alkalmazás indításakor. - Shorts lejátszó folytatásának letiltása - A gördülő számok animálva vannak - A gördülő számok nem animáltak - Gördülőszám-animációk letiltása - A keresősáv fejezetei engedélyezettek. - A keresősáv fejezetei le vannak tiltva. - Keresősáv fejezeteinek letiltása - A szökőkút animáció engedélyezve van a Like gombon. - A szökőkút animáció le van tiltva a Like gombon. - Like gomb animáció elrejtése - "Letiltja a '2x>>' funkciót, hosszan nyomva tartásra. - -Megjegyzés: -• A gyorsított lejátszás letiltásával visszaállítható a keresősáv korábbi 'Csúsztatás kereséshez' viselkedése. -• A beállítás letiltása nem erőlteti a gyorsított lejátszást." - Gyorsított lejátszás letiltása - Az indító animáció engedélyezve. - Az indító animáció letiltva. - Indító animáció letiltása - "Letiltja a következő interakciókat, ha a videoleírás kibővítve van: - -•Koppintás a görgetéshez. -•Koppintás és tartás a szöveg kijelöléséhez." - Videoleírás interakció letiltása - VP9 kodek engedélyezve van. - "A VP9 kodek le van tiltva. - -• A maximális felbontás 1080p. -• A videolejátszás több internetes adatot használ, mint a VP9. -• A HDR lejátszáshoz a HDR videó továbbra is a VP9 kodeket használja." - VP9 kodek letiltása - A Cairo keresősáv le van tiltva. - "A Cairo keresősáv engedélyezett. - -Mellékhatás: a Cairo stílus az értesítési pontokra is alkalmazódik." - Cairo keresősáv engedélyezése - A vezérlők felülete kitölti a teljes képernyőt. - A vezérlők felülete nem tölti ki a teljes képernyőt. - Kompakt vezérlők fedés engedélyezése - Az egyéni lejátszási sebesség le van tiltva. - Az egyéni lejátszási sebesség engedélyezve van. - Egyéni lejátszási sebesség engedélyezése - Az egyéni keresősáv szín le van tiltva. - Az egyéni keresősáv szín engedélyezett. - Egyéni keresősáv szín engedélyezése - A hibakeresési naplók nem tartalmazzák a puffert. - A hibakeresési naplók tartalmazzák a puffert. - Hibakeresési puffernaplózás engedélyezése - A hibakeresési napló le van tiltva. - A hibakeresési napló engedélyezve van. - Hibakeresési naplózás engedélyezése - Az alapértelmezett lejátszási sebesség nem vonatkozik a Shortokra. - Az alapértelmezett lejátszási sebesség vonatkozik a Shortokra. - Shortok alapértelmezett lejátszási sebességének engedélyezése - A külső böngésző le van tiltva. - A külső böngésző engedélyezve van. - Külső böngésző engedélyezése - A színátmenetes betöltési képernyő le van tiltva. - A színátmenetes betöltési képernyő engedélyezve van. - Színátmenetes betöltési képernyő engedélyezése - A navigációs gombok közötti távolság normális. - A navigációs gombok közötti távolság szűk. - Keskeny navigációs gombok engedélyezése - Az alapértelmezett átirányítási rendet követi. - Az URL átirányítások kikerülése. - Közvetlen link megnyitások engedélyezése - Engedélyezi az OPUS kodeket, ha a lejátszó válasza tartalmazza az OPUS kodeket. - OPUS kodek engedélyezése - Ne mentse és állítsa vissza a fényerőt, amikor kilép vagy belép a teljes képernyőbe. - A fényerő mentése és visszaállítása teljes képernyőből való kilépéskor vagy belépéskor. - A fényerő mentésének és visszaállításának engedélyezése - A keresősávon történő érintés ki van kapcsolva. - A keresősávon történő érintés engedélyezve van. - Érintés engedélyezése a kereső sávon - "Ezzel visszaállítja az indexképeket az olyan élő közvetítésekhez, amelyek nem rendelkeznek keresősáv-bélyegképekkel. - -Az internetes adathasználat magasabb lehet, és a keresősáv bélyegképei kis késéssel jelennek meg. - -Ez a funkció nagyon gyors internetkapcsolat mellett működik a legjobban." - A keresősáv bélyegképei közepes minőségűek. - A keresősáv bélyegképei kiváló minőségűek. - Jó minőségű miniatűrök engedélyezése - Fejezetek letiltva. - "Az időbélyeg engedélyezve van. - -Korlátozások: -• Ez a beállítás nem csak az időbélyegeket engedélyezi, hanem lehetővé teszi a felhasználók számára a felhasználói felület elrejtését is a lejátszó hátterére kattintva. -• Mivel ez a funkció a Google fejlesztési szakaszában van, előfordulhat, hogy az elrendezés hibás." - Fejezetek engedélyezése - A csúsztatásos fényerő vezérlés le van tiltva. - A csúsztatásos fényerő vezérlés engedélyezve van. - Fényerő csúsztatás engedélyezése - A haptikus visszajelzés le van tiltva. - A haptikus visszajelzés engedélyezve van. - Haptikus visszajelzés engedélyezése - A fényerő gesztus legkisebb értékénél az automatikus fényerő nem kapcsol be. - A fényerő gesztus legkisebb értékénél az automatikus fényerő bekapcsol. - Automatikus fényerő bekapcsolása gesztussal - Érintse meg a csúsztatás engdélyezéséhez. - Érintse meg és tartsa nyomva a csúsztatás engedélyezéséhez. - Nyomva húzás engedélyezése - A fel / le csúsztatással a következő / előző videó nem kerül lejátszásra. - A fel / le csúsztatással a következő / előző videó lejátszásra kerül. - Engedélyezze a csúsztatással videó váltást teljes képernyőn - A csúsztatásos hangerő vezérlés le van tiltva. - A csúsztatásos hangerő vezérlés engedélyezve van. - Hangerő csúsztatás engedélyezése - A navigációs sáv nem áttetsző. - A navigációs sáv áttetsző. - Áttetsző navigációs sáv engedélyezése - A videólejátszó alatti lefelé húzás esetén a teljes képernyős módra váltás ki van kapcsolva. - A videólejátszó alatti lefelé húzás esetén a teljes képernyős módra váltás be van kapcsolva. - Nézőpanel bekapcsolása gesztussal - "Ha bekapcsolja ezt a beállítást, letiltja a beállítások gombot a Te lapon. - -Ebben az esetben kérjük használja a következő utat a beállításokhoz való hozzáféréshez: -Te lap → Csatorna megtekintése → Menü → Beállítások" - Széles kereső sáv engedélyezése a Te lapon - A széles keresősáv le van tiltva. - A széles keresősáv engedélyezve van. - Széles keresősáv engedélyezése - A széles keresősáv elakarja a YouTube fejlécét. - A széles keresősáv nem takarja el a YouTube fejlécét. - Széles, fejléces keresősáv engedélyezése - Leírás - "Írja be a videoleírás panel címét az Ön nyelvén. -A 'Videoleírások kibővítése' nem működik, ha a beírt sztring nem egyezik meg a videoleírás panel címével. " - Cím a videoleírás panelen - A videoleírások nem bővülnek ki automatikusan. - A videoleírások automatikusan kibővülnek. - Videoleírások kibővítése - Szeretnéd folytatni? - Visszaállítás az alapértelmezett értékekre. - Indítsa újra a rendszert a normál elrendezés betöltéséhez - "Van egy YouTube szerveroldali hiba, ami miatt a gördülő számokat tartalmazó szövegek, például a tetszésnyilvánítások, a megtekintések és a feltöltési dátumok el vannak rejtve egyes felhasználók számára. - -A probléma ideiglenes megoldása az alkalmazás verziójának 19.13.37-re való hamisítása. - -Szeretné hamisítani az alkalmazás verzióját az alkalmazás újraindítása előtt?" - Frissítés és újraindítás - A beállítások exportálása sikertelen. - A beállítások exportálása sikeres. - Beállítások exportálása egy fájlba. - Beállítások exportálása - Importálás - Másol - Beállítások importálása vagy exportálása szövegként. - Importálás / exportálás szövegként - A beállítások importálása sikertelen. - Visszaállítás - A beállítások sikeresen importálva. - Beállítások importálása egy mentett fájlból. - Beállítások importálása - Visszaállítás - %s keresése - ReVanced Extended - Külső letöltéskezelő - Nincs telepítve - "A(z) %1$s nincs telepítve. -Töltsd le a(z) %2$s weboldalról." - Figyelmeztetés - %s nincs telepítve. Kérlek telepítsd. - A telepített külső letöltő alkalmazás csomagneve, például YTDLnis. - Lejátszási lista letöltő csomag neve - A telepített külső letöltő alkalmazás csomagneve, például NewPipe vagy YTDLnis. - Videó letöltő csomag neve - "A videókat a következő esetekben váltjuk át teljes képernyős módba: - -• Amikor egy videót elindítanak. -• Amikor egy időbélyegre kattintanak a hozzászólásokban." - Teljes képernyő erőltetése - A fiók menüben szűrendő menüpontok listája, új sorokkal elválasztva. - Fiók menü szűrő - "Fiókmenü és az Ön lap elemeinek elrejtése. -Előfordulhat, hogy egyes komponensek nincsenek elrejtve." - Fiókmenü elrejtése - Az album kártyák láthatóak. - Az album kártyák el vannak rejtve. - Album kártyák elrejtése - A kiemelt helyek, a Játékok és a Zene szakaszok láthatóak. - A kiemelt helyek, a Játékok és a Zene szakaszok elrejtve. - Tulajdonságok szakasz elrejtése - Az automatikus lejátszás előnézeti kerete látható. - Az automatikus lejátszás előnézeti kerete el van rejtve. - Automatikus lejátszás előnézeti tároló elrejtése - Az áruház böngészése gomb látható. - Az áruház böngészése gomb el van rejtve. - Áruház böngészése gomb elrejtése - "A következő polcokat rejtse el: -• Friss hírek -• Folytassa a nézést -• Fedezze fel további csatornákat -• Hallgassa meg újra -• Vásárlás -• Nézze meg újra" - Forduló polc elrejtése - Megjelenik a hírfolyamban. - Elrejtve a hírfolyamban. - Rejtse el a hírfolyamban - Megjelenik a kapcsolódó videókban. - Elrejtve a kapcsolódó videókban. - Rejtse el a kapcsolódó videókban - Megjelenik a keresési eredményekben. - Elrejtve a keresési eredményekben. - Rejtse el a keresési eredményekben - A csatorna irányelvei megjelenítve - A csatorna irányelvei elrejtve - Csatornák irányelveinek elrejtése - A csatornatagok polca megjelenik - A csatornatagok polca rejtett - Csatornatag polc elrejtése - A csatorna profil tetején lévő linkek láthatóak. - A csatorna profil tetején lévő linkek el vannak rejtve. - Csatorna profil tetején lévő linkek elrejtése - "Shortok -Lejátszási listák -Áruház" - Szűrendő csatornafül neveinek listája, új sorral elválasztva. - Csatornafül szűrő - A csatornafül szűrő ki van kapcsolva. - A csatornafül szűrő be van kapcsolva. - Csatornafül szűrő bekapcsolása - A csatorna vízjele látható. - A csatorna vízjel el van rejtve. - Csatorna vízjel elrejtése - A fejezetek szakaszai megjelennek. - A fejezetek szakaszai el vannak rejtve. - Fejezetek szakaszainak elrejtése - A vágások polc látható. - A vágások polc elrejtve. - Vágások polc elrejtése - A klip gomb látható. - A klip gomb el van rejtve. - Klip gomb elrejtése - A \'Short létrehozása\' gomb látható. - A \'Short létrehozása\' gomb el van rejtve. - Short létrehozás gomb elrejtése - A kiemelt keresési hivatkozások láthatóak. - A kiemelt keresési hivatkozások el vannak rejtve. - Kiemelt keresési hivatkozások elrejtése - A Köszönöm gomb megjelenik. - A Köszönöm gomb el van rejtve. - Köszönöm gomb elrejtése - A megjegyzés időbélyegzője és a hangulatjelek gombjai megjelennek - A megjegyzés időbélyegzője és az emoji gombok el vannak rejtve - Időbélyeg és az emoji gombok elrejtése - A csatornatagok megjegyzései rész látható. - A csatornatagok megjegyzései rész el van rejtve. - Rejtse el a csartornatagok megjegyzései részt - A hozzászólások része látható a kezdőlapon. - A hozzászólások része el van rejtve a kezdőlapon. - Rejtse el a hozzászólások részét a kezdőlapon - A megjegyzések szekció megjelenik - A megjegyzések szekció rejtett - A megjegyzések szekció elrejtése - Megjelenítve a csatornában. - Elrejtve a csatornában. - Elrejtése a csatornában - Megjelenítve a kezdőlapon és a kapcsolódó videóknál. - Elrejtve a kezdőlapon és a kapcsolódó videóknál. - Elrejtés a kezdőlapon és a kapcsolódó videóknál - Megjelenítve a feliratkozások között. - Elrejtve a feliratkozások között. - Elrejtés a feliratkozások között - A tartalom készítésének módja rész látható. - A tartalom készítésének módja rész elrejtve. - A tartalmak szekció elrejtése - A közösségi finanszírozás látható. - A közösségi finanszírozás el van rejtve. - Közösségi finanszírozás elrejtése - A dupla koppintásos átfedés szűrő látható. - A dupla koppintásos átfedés szűrő elrejtve. - Dupla koppintás átfedés szűrő elrejtése - A letöltés gomb látható - A letöltés gomb el van rejtve - Letöltés elrejtése - A záróképernyő kártyák láthatóak. - A záróképernyő kártyák el vannak rejtve. - Záróképernyő kártyák elrejtése - A bővíthető vágások megjelennek. - A bővíthető vágások el vannak rejtve. - Bővíthető vágások elrejtése a videók alatt - A kinyitható polcok láthatóak. - A kinyitható polcok el vannak rejtve. - Kinyitható polcok elrejtése - A Feliratok gomb megjelenik. - A Feliratok gomb el van rejtve. - Rejtsd el a hírfolyam feliratok gombját - Szűrendő lebegő menü neveinek listája, új sorokkal elválasztva. - Hírfolyam lebegő menü szűrő - A hírfolyam lebegő menü szűrő ki van kapcsolva. - A hírfolyam lebegő menü szűrő engedélyezve van. - Hírfolyam lebegő menü szűrő engedélyezése - A hírfolyam keresősáv látható. - A hírfolyam keresősáv elrejtve. - Hírfolyam keresősáv elrejtése - A hírfolyam kérdőívek láthatóak. - A hírfolyam kérdőívek el vannak rejtve. - Hírfolyam kérdőívek elrejtése - A filmszalag fedés látható. - A filmszalag fedés el van rejtve. - Filmszalag fedés elrejtése - A lebegő gomb látható. - A lebegő gomb el van rejtve. - Lebegő gomb elrejtése - A lebegő mikrofon gomb látható. - A lebegő mikrofon gomb el van rejtve. - Lebegő mikrofon gomb elrejtése - A polc megjelenik - A polc rejtett - \"Neked\" polc elrejtése a csatorna oldalon - A teljes képernyős hirdetések láthatók. - A teljes képernyős hirdetések rejtve vannak. - Teljes képernyős hirdetések elrejtése - "A teljes képernyős hirdetések letiltva. - -Korlátozás: előfordulhat, hogy a közösségi bejegyzés képe a teljes képernyőn le van tiltva." - A teljes képernyős reklámok a Bezár gombbal záródnak. - Teljes képernyős hirdetések bezárása - Az általános hirdetések láthatóak. - Az általános hirdetések el vannak rejtve. - Általános hirdetések elrejtése - A YouTube Prémium promóció látható. - A YouTube Prémium promóció el van rejtve. - YouTube Prémium promóció elrejtése - A szürke elválasztók láthatóak - A szürke elválasztók el vannak rejtve - Szürke elválasztó elrejtése - A kezelő látható. - A kezelő el van rejtve. - Kezelő elrejtése - A képkeresés gomb látható. - A képkeresés gomb elrejtve. - Képkeresés gomb elrejtése - A kép polcok láthatóak. - A kép polcok el vannak rejtve. - Kép polc elrejtése - Az infó kártyák rész látható - Az infó kártyák rész el van rejtve - Infó kártyák rész elrejtése - Az info kártyák láthatóak. - Az info kártyák el vannak rejtve. - Infó kártyák elrejtése - Az info panelek láthatóak. - Az info panelek el vannak rejtve. - Infó panelek elrejtése - A csatlakozás gomb látható. - A csatlakozás gomb el van rejtve. - Csatlakozás gomb elrejtése - A kulcs koncepciók rész látható. - A kulcs koncepciók rész elrejtve. - Kulcs koncepciók rész elrejtése - "Kezdőlapi / Feliratkozási / Keresési eredmények szűrve vannak egyező kulcsszavak alapján. - -Korlátozások: -• A rövidfilmeket nem lehet elrejteni a csatornanév alapján. -• Előfordulhat, hogy egyes felhasználói felület-összetevők nincsenek elrejtve. -• Előfordulhat, hogy a kulcsszó keresése nem ad eredményt." - A kulcsszó alapú szűrésről - Ha egy kulcsszót/kifejezést dupla idézőjelekkel vesz körül, akkor elkerülhető a videócímek és a csatornanevek részleges egyezése<br><br>Például:<br><b>\"ai\"</b> elrejti a videót: <b>Hogyan működik az AI?</b><br>de nem rejti el: <b>Mit jelent a fair use?</b> - Teljes szóegyezések - A kommentek nincsenek szűrve. - A kommentek szűrve vannak. - Kommentek elrejtése kulcsszavak alapján - A videók a kezdőlapon nincsenek szűrve. - A videók a kezdőlapon szűrve vannak. - Videók elrejtése a kezdőlapon kulcsszavak alapján - "Elrejteni kívánt kulcsszavak és kifejezések, új sorokkal elválasztva.\n\nA kulcsszavak lehetnek csatornanevek vagy bármilyen szöveg, amely a videók címében látható.\n\nA középen nagybetűs szavakat a kis- és nagybetűkkel együtt kell megadni (pl. iPhone, TikTok, LeBlanc)." - Elrejtendő kulcsszavak - A keresési eredmények nincsenek szűrve. - A keresési eredmények szűrve vannak. - Keresési eredmények elrejtése kulcsszavak alapján - A feliratkozott videók nincsenek szűrve. - A feliratkozott videók kulcsszavak alapján szűrve vannak. - Feliratkozott videók elrejtése kulcsszavak alapján - A kulcsszó elrejti az összes videót: %s. - Érvénytelen kulcsszó:\'%s. - Adjon hozzá idézőjeleket a következő kulcsszóhoz: %s. - A kulcsszónak ütköző deklarációi vannak: %s. - A keresőszó túl rövid, és idézőjeleket igényel: %s. - A legutóbbi bejegyzések láthatóak. - A legutóbbi bejegyzések el vannak rejtve. - Legutóbbi bejegyzések elrejtése - A \'Legújabb videók\' gomb megjelenik. - A \'Legújabb videók\' gomb el van rejtve. - Rejtse el a \'Legújabb videók\' gombot - A tetszik és nem tetszik gombok láthatóak - A tetszik és nem tetszik gombok rejtve vannak - Tetszik és nem tetszik elrejtése - Az élő csevegés üzenetei megjelennek.\n\nEz a beállítás a Shorts élő videókra is vonatkozik. - Az élő csevegés üzenetei el vannak rejtve.\n\nEz a beállítás a Shorts élő videókra is vonatkozik. - Élő csevegés üzeneteinek elrejtése - Az élő csevegés visszajátszása gomb látható.\n\nAz élő csevegés bezárásakor teljes képernyőn jelenik meg. - Az élő csevegés visszajátszása gomb rejtve van.\n\nAz élő csevegés bezárásakor teljes képernyőn jelenik meg. - Élő csevegés gomb elrejtése - Az 1000-nél kevesebb megtekintést elért videók elrejtése a Kezdőlapon, amiket a leiratkozott csatornákról töltöttek fel. - Alacsony nézettségű videók elrejtése - Az egészségügyi panelek láthatóak. - Az egészségügyi panelek el vannak rejtve. - Egészségügyi panelek elrejtése - Az árupolcok láthatóak. - Az árupolcok el vannak rejtve. - Rejtsd el a árucikkek polcokat - Az egyveleg lejátszási listák megjelennek. - Az egyveleg lejátszási listák el vannak rejtve. - Egyveleg lejátszási listák elrejtése - A filmek polcai láthatóak. - A filmek polcai el vannak rejtve. - Film polcok elrejtése - A navigációs sáv látható. - A navigációs sáv el van rejtve. - Navigációs sáv elrejtése - A létrehozás gomb látható. - A létrehozás gomb el van rejtve. - Létrehozás gomb elrejtése - A kezdőlap gomb látható. - A kezdőlap gomb el van rejtve. - Kezdőlap gomb elrejtése - A navigációs sáv látható. - A navigációs sáv el van rejtve. - Navigációs sáv elrejtése - A könyvtár gomb látható. - A könyvtár gomb el van rejtve. - Könyvtár gomb elrejtése - Az értesítések gomb látható. - Az értesítések gomb el van rejtve. - Értesítések gomb elrejtése - A Shorts gomb látható. - A Shorts gomb el van rejtve. - Shorts gomb elrejtése - Az előfizetések gomb látható. - Az előfizetések gomb el van rejtve. - Feliratkozás gomb elrejtése - Az \'Értesítsen\' gomb megjelenik. - Az \'Értesítsen\' gomb el van rejtve. - \"Értesítést kérek\" gomb elrejtése - A fizetett promóciós címke látható. - A fizetett promóció címke rejtve van - Fizetett promóció címke elrejtése - A játékszoba megjelenik - A játékszoba rejtett - Lejátszható elemek elrejtése - Az automatikus lejátszás gomb megjelenik. - Az automatikus lejátszás gomb el van rejtve. - Automatikus lejátszás gomb elrejtése - A Feliratok gomb megjelenik. - A Feliratok gomb el van rejtve. - Feliratok gomb elrejtése - A Cast gomb megjelenik. - A Cast gomb el van rejtve. - Szereplők gomb elrejtése - Az Összecsukás gomb megjelenik. - Az Összecsukás gomb el van rejtve. - Összecsukás gomb elrejtése - A \'Mozifilmes világítás\' menü látható. - A \'Mozifilmes világítás\' menü el van rejtve. - \'Mozifilmes világítás\' menü elrejtése - Az audiosáv menü megjelenik. - Az audiosáv menü el van rejtve. - Audió nyomkövető menü elrejtése - A feliratok menü lábléce megjelenik. - A feliratok menü lábléce el van rejtve. - Feliratok menü láblécének elrejtése - A feliratok menü megjelenik. - A feliratok menü el van rejtve. - Feliratok menü elrejtése - A 1080p Premium menü látható. - A 1080p Premium menü el van rejtve. - 1080p Premium menü elrejtése - A segítség és visszajelzés menü megjelenik. - A segítség és visszajelzés menü el van rejtve. - Segítség és visszajelzés menü elrejtése - A YouTube Music-kal való hallgatás menü megjelenik. - A YouTube Music-kal való hallgatás menü el van rejtve. - Hallgatás a YouTube Music-al menü elrejtése - A zárolás képernyő menü megjelenik. - A zárolás képernyő menü el van rejtve. - Zárolási képernyő menü elrejtése - A videó ismétlés menü megjelenik. - A videó ismétlés menü el van rejtve. - Videó ismétlés menü elrejtése - A további információk menü megjelenik. - A további információk menü el van rejtve. - További információ menü elrejtése - A kép-a-képben menü látható. - A kép-a-képben menü el van rejtve. - Kép-a-képben menü elrejtése - A lejátszási sebesség menü megjelenik. - A lejátszási sebesség menü el van rejtve. - Lejátszási sebesség menü elrejtése - A Prémium vezérlők menü megjelenik. - A Prémium vezérlők menü el van rejtve. - Prémium vezérlők menü elrejtése - A minőség menü lábléce megjelenik. - A minőség menü lábléce el van rejtve. - Minőség menü láblécének elrejtése - A minőség menü fejléce látható. - A minőség menü fejléce elrejtve. - Minőség menü fejléc elrejtése - A jelentés menü megjelenik. - A jelentés menü el van rejtve. - Jelentés menü elrejtése - Az elalvási időzítő látható. - Az elalvási időzítő el van rejtve. - Elalvási időzítő elrejtése - A stabil hangerő menü megjelenik. - A stabil hangerő menü el van rejtve. - Rejtsd el a stabil hangerő menüt - A statisztikák a kockáknak menü megjelenik. - A statisztikák a kockáknak menü el van rejtve. - Statisztikák a nagyoknak menü elrejtése - A VR-ben nézés menü megjelenik. - A VR-ben nézés menü el van rejtve. - Nézés VR-ban menü elrejtése - A teljes képernyős gomb megjelenik. - A teljes képernyős gomb rejtve van. - Teljes képernyős gomb elrejtése - A gombok megjelennek. - A gombok el vannak rejtve. - Előző és következő gomb elrejtése - A bevásárló polc látható. - A bevásárló polc el van rejtve. - Lejátszó bevásárló polcának elrejtése - YouTube Music gomb megjelenik. - YouTube Music gomb rejtve van. - YouTube Zene gomb elrejtése - A mentés gomb látható - A mentés gomb el van rejtve - Mentés elrejtése - A podcast szakasz látható - A podcast szakasz rejtve van - Podcast szakasz elrejtése - A megjegyzés előnézet látható. - A megjegyzés előnézet el van rejtve. - Megjegyzés előnézet elrejtése - Ez megváltoztatja a megjegyzés rész méretét, így nem lehet a megjegyzés részben élő chat választ nyitni. - Ez nem változtatja meg a megjegyzés rész méretét, így lehet a megjegyzés részben élő chat választ nyitni. - Megjegyzés előnézet elrejtés típusa - A promóciós figyelmeztető banner látható. - A promóciós figyelmeztető banner el van rejtve. - Promóciós figyelmeztető banner elrejtése - A hozzászólás gomb megjelenik. - A hozzászólás gomb el van rejtve. - Hozzászólás gomb elrejtése - A nem tetszik gomb látható. - A nem tetszik gomb el van rejtve. - Nem tetszik gomb elrejtése - A tetszik gomb látható. - A tetszik gomb el van rejtve. - Tetszik gomb elrejtése - A élő chat gomb látható. - A élő chat gomb el van rejtve. - Élő chat gomb elrejtése - A Tovább gomb megjelenik. - A Tovább gomb el van rejtve. - További gomb elrejtése - A mix lejátszási lista megnyitása gomb látható. - A mix lejátszási lista megnyitása gomb el van rejtve. - Mix lejátszási lista gomb elrejtése - A lejátszási lista megnyitása gomb látható. - A lejátszási lista megnyitása gomb el van rejtve. - Lejátszási lista gomb elrejtése - A lejátszási listához mentés gomb látható. - A lejátszási listához mentés gomb el van rejtve. - Lejátszási listához mentés gomb elrejtése - A megosztás gomb látható. - A megosztás gomb el van rejtve. - Megosztás gomb elrejtése - A gyorsműveletek megjelennek. - A gyorsműveletek el vannak rejtve. - Gyorsműveletek konténer elrejtése - "Elrejti a következő ajánlott videókat: - -• Csak a tagok címkével ellátott videók. -• Olyan videók, amelyek alatt olyan kifejezések szerepelnek, mint „Mások is megnézték”." - Ajánlott videók elrejtése - A kapcsolódó videó átfedése látható. - A kapcsolódó videó átfedése el van rejtve. - Kapcsolódó videó átfedésének elrejtése - A kapcsolódó videók láthatóak. - A kapcsolódó videók el vannak rejtve. - Kapcsolódó videók elrejtése - "Ez a beállítás korlátozza a lejátszó képernyőjére betölthető elrendezések maximális számát. - -Ha a lejátszó képernyőjének elrendezése a szerveroldali változtatások miatt megváltozik, akkor esetleg a nem kívánt elrendezések elrejthetők a lejátszó képernyőjén." - A remix gomb megjelenik - A remix gomb el van rejtve - Remix elrejtése - A bejelentés gomb látható - A bejelentés gomb el van rejtve. - Bejelentés elrejtése - A jutalmak gomb látható. - A jutalmak gomb el van rejtve. - Jutalmak gomb elrejtése - A bélyegképek a keresési kifejezések előzményeiben láthatóak. - A bélyegképek a keresési kifejezések előzményeiben el vannak rejtve. - Keresési kifejezések bélyegképeinek elrejtése - A kereső üzenet látható. - A kereső üzenet el van rejtve. - Kereső üzenet elrejtése - A keresés visszavonása üzenet látható. - A keresés visszavonása üzenet el van rejtve. - Keresés visszavonása üzenetet elrejtése - Az időbélyeg melletti fejezetcímkék láthatóak. - Az időbélyeg melletti fejezetcímkék el vannak rejtve. - Keresősáv fejezetcímkéinek elrejtése - A videólejátszó folyamatsávja megjelenik - A videólejátszó folyamatsávja el van rejtve - A minilejátszó folyamatsávja megjelenik - A minilejátszó folyamatsávja el van rejtve - Folyamatsáv elrejtése a minilejátszóban - Folyamatsáv elrejtése a videólejátszóban - Az önpromóciós kártyák láthatóak. - Az önpromóciós kártyák el vannak rejtve. - Önpromóciós kártyák elrejtése - A névjegy menü látható. - A névjegy menü el van rejtve. - Névjegy menü elrejtése - A kisegítő lehetőségek menü látható. - A kisegítő lehetőségek menü el van rejtve. - Kisegítő lehetőségek menü elrejtése - A fiókmenü látható. - A fiókmenü el van rejtve. - Fiókmenü elrejtése - Az automatikus lejátszás menü látható. - Az automatikus lejátszás menü el van rejtve. - Automatikus lejátszás menü elrejtése - A számlázás és fizetés menü látható. - A számlázás és fizetés menü el van rejtve. - Számlázás és fizetés menü elrejtése - A feliratok menü látható. - A feliratok menü el van rejtve. - Feliratok menü elrejtése - A csatlakoztatott alkalmazások menü látható. - A csatlakoztatott alkalmazások menü el van rejtve. - Csatlakoztatott alkalmazások menü elrejtése - Az adatmegtakarító menü látható. - Az adatmegtakarító menü el van rejtve. - Adatmegtakarító menü elejtése - Az Általános menü látható. - Az Általános menü el van rejtve. - Általános menü elrejtése - A minden előzmény kezelése menü látható. - A minden előzmény kezelése menü el van rejtve. - Minden előzmény kezelése menü elrejtése - Az élő csevegés menü látható. - Az élő csevegés menü el van rejtve. - Élő csevegés menü elrejtése - Az értesítések menü látható. - Az értesítések menü el van rejtve. - Értesítések menü elrejtése - A háttér menü látható. - A háttér menü el van rejtve. - Háttér menü elrejtése - A megtekintés a tévében menü látható. - A megtekintés a tévében menü el van rejtve. - Megtekintés a tévében menü elrejtése - A családi központ menü látható. - A családi központ menü el van rejtve. - Családi központ menü elrejtése - A próbálja ki a kísérleti új funkciókat menü látható. - A próbálja ki a kísérleti új funkciókat menü el van rejtve. - Próbálja ki a kísérleti új funkciókat menü elrejtése - Az adatvédelem menü látható. - Az adatvédelem menü el van rejtve. - Adatvédelem menü elrejtése - A vásárlások és tagságok menü látható. - A vásárlások és tagságok menü el van rejtve. - Vásárlások és tagságok menü elrejtése - Elrejti a YouTube beállítások menü elemeit. - YouTube beállítások menü elrejtése - A videóminőség beállítás menü látható. - A videóminőség beállítás menü el van rejtve. - Videóminőség beállítás menü elrejtése - Az adataid a YouTube-on menü látható. - Az adataid a YouTube-on menü el van rejtve. - Adataid a YouTube-on menü elrejtése - A megosztás gomb látható - A megosztás gomb el van rejtve - Megosztás elrejtése - A vásárlás gomb látható. - A Vásárlás gomb el van rejtve. - Üzlet gomb elrejtése - A vásárlási linkek láthatóak - A vásárlási linkek rejtve vannak - Vásárlási linkek elrejtése a videó leírásában - A csatornasáv látható. - A csatornasáv el van rejtve. - Csatornasáv elrejtése - A megjegyzések gomb látható. - A megjegyzések gomb el van rejtve. - Megjegyzések gomb elrejtése - A letiltott megjegyzések gomb vagy \'null\' címkével látható. - A letiltott megjegyzések gomb vagy \'null\' címkével el van rejtve. - Rejtsd el a letiltott megjegyzések gombot - A nem tetszik gomb látható. - A nem tetszik gomb el van rejtve. - Nem tetszik gomb elrejtése - "Az olyan lebegő gombok, mint a „Használja ezt a hangot”, a Shorts csatorna lapon láthatóak." - "Az olyan lebegő gombok, mint a „Használja ezt a hangot”, el vannak rejtve a Shorts csatorna lapon." - Lebegő gomb elrejtése - A videólink címke látható. - A videólink címke el van rejtve. - Teljes videólink címke elrejtése - A zöld képernyő gomb látható. - A zöld képernyő gomb el van rejtve. - Zöld képernyő gomb elrejtése - Az infó panelek láthatóak. - Az info panelek el vannak rejtve. - Infó panelek elrejtése - A csatlakozás gomb látható. - A csatlakozás gomb el van rejtve. - Csatlakozás gomb elrejtése - A tetszik gomb látható. - A tetszik gomb el van rejtve. - Tetszik gomb elrejtése - Az élő csevegés fejléce látható.\n\nA fejlécben található Vissza gomb nem lesz elrejtve. - Az élő csevegés fejléce el van rejtve.\n\nA fejlécben található Vissza gomb nem lesz elrejtve. - Élő csevegés fejléc elrejtése - A hely gomb látható. - A hely gomb el van rejtve. - Hely gomb elrejtése - A navigációs sáv látható. - A navigációs sáv el van rejtve. - Navigációs sáv elrejtése - A fizetett promóciós címke látható. - A fizetett promóciós címke el van rejtve. - Fizetett promóciós címke elrejtése - A szüneteltetett fejléc látható. - A szüneteltetett fejléc elrejtve. - Szüneteltetett fejléc elrejtése - A szüneteltetett fedő gombok láthatóak. - A szüneteltetett videóvezérlő gombok el vannak rejtve. - Szüneteltetett videóvezérlő gombok elrejtése - A gomb háttere látható. - A gomb háttere elrejtve. - Lejátszás & Szünet gomb hátterének elrejtése - A remix gomb látható. - A remix gomb el van rejtve. - Remix gomb elrejtése - A zene mentés gomb látható. - A zene mentés gomb el van rejtve. - Zene mentés gomb elrejtése - A keresési javaslatok gomb látható. - A keresési javaslatok gomb el van rejtve. - Keresési javaslatok gomb elrejtése - A megosztás gomb látható. - A megosztás gomb el van rejtve. - Megosztás gomb elrejtése - A csatornában látható. - "A csatornában el van rejtve. - -Információ: -• Csak azok a polcok lesznek elrejtve a kezdőlapon, amelyeknél a Shorts fejléce látható." - Elrejtés a csatornában - Megjelenítve a nézési előzmények között. - Elrejtve a nézési előzmények között. - Elrejtés a nézési előzmények között - Látható a kezdőlapon és a kapcsolódó videóknál. - Elrejtve a kezdőlapon és a kapcsolódó videóknál. - Elrejtés a kezdőlapon és a kapcsolódó videóknál - Megjelenítve a keresési eredmények között. - Elrejtve a keresési eredmények között. - Elrejtés a keresési eredmények között - Megjelenítve a feliratkozások között. - Elrejtve a feliratkozások között. - Elrejtés a feliratkozások között - "Elrejti a Shorts polcokat. - -Mellékhatás: A hivatalos fejlécek a keresési eredményekben el lesznek rejtve." - Shorts polcok elrejtése - A vásárlás gomb látható. - A vásárlás gomb el van rejtve. - Vásárlás gomb elrejtése - A vásárlás gomb látható. - A vásárlás gomb el van rejtve - Vásárlás gomb elrejtése - A hang gomb látható. - A hang gomb el van rejtve. - Hang gomb elrejtése - A metaadat címke látható. - A metaadat címke el van rejtve. - Hang metadata címke elrejtése - A matricák láthatóak. - A matricák el vannak rejtve. - Matricák elrejtése - A feliratkozás gomb látható. - A feliratkozás gomb el van rejtve. - Feliratkozás gomb elrejtése - A Szuper köszönet gomb látható. - A Szuper köszönet gomb el van rejtve. - Szuper köszönet gomb elrejtése - A címkézett termékek láthatóak. - A címkézett termékek el vannak rejtve. - Címkézett termékek elrejtése - Az eszköztár látható. - Az eszköztár el van rejtve. - Eszköztár elrejtése - A Trend gomb látható. - A Trend gomb el van rejtve. - Trend gomb elrejtése - A sablon használata gomb látható. - A sablon használata gomb el van rejtve. - Sablon használata gomb elrejtése - Az ennek a zenének a használata gomb látható. - Az ennek a zenének a használata gomb el van rejtve. - Ennek a zenének a használata gomb elrejtése - A cím látható. - A cím el van rejtve. - Videó címének elrejtése - A gomb megjelenik - A gomb el van rejtve - \'Továbbiak megjelenítése\' gomb elrejtése - Az üzenet sáv látható. - Az üzenet sáv el van rejtve. - Üzenet sáv elrejtése - A próba indítása gomb látható. - A próba indítása gomb el van rejtve. - Próba indítása gomb elrejtése - A feliratkozások rész látható. - A feliratkozások rész elrejtve. - Feliratkozások rész elrejtése - A javasolt műveletek láthatóak. - A javasolt műveletek el vannak rejtve. - Javasolt műveletek elrejtése - "This setting has been deprecated. - -Instead, use the 'Settings → Autoplay → Autoplay next video' setting. - -Note: -• If you have any issues with 'Suggested video end screen', try restarting the app." - Megjelenik a javasolt videó záróképernyője. - "A javasolt videó záróképernyője el van rejtve, ha az automatikus lejátszás ki van kapcsolva. - -Az automatikus lejátszás a YouTube beállításaiban módosítható: -Beállítások → Automatikus lejátszás → Következő videó automatikus lejátszása" - Javasolt videó végoldali képernyő elrejtése - A köszönet gomb látható. - A köszönet gomb el van rejtve. - Köszönet gomb elrejtése - A jegy polcok láthatóak. - A jegy polcok elrejtve. - Jegy polcok elrejtése - Az időtartam látható. - Az időtartam el van rejtve. - Időtartam elrejtése - Az időzített reakciók láthatóak. - Az időzített reakciók el vannak rejtve. - Időzített reakciók elrejtése - Az átküldés gomb látható. - Az átküldés gomb el van rejtve. - Átküldés gomb elrejtése - A létrehozás gomb látható. - A létrehozás gomb el van rejtve. - Létrehozás gomb elrejtése - Az értesítések gomb látható. - Az értesítések gomb el van rejtve. - Értesítések gomb elrejtése - Az átirat rész megjelenik - Az átirat rész el van rejtve - Átirat rész elrejtése - A videó hirdetések láthatók - A videó hirdetések el vannak rejtve - Videó hirdetések elrejtése - "Kezdőlap / Feliratkozás / Keresés eredményei szűrve vannak, hogy elrejtse a meghatározott számnál kisebb vagy nagyobb nézettségű videókat. - -Korlátozások: -• A Shortokat nem lehet elrejteni. -• A 0 megtekintésű videók nincsenek kiszűrve." - Szűrés nézettség alapján névjegy - A videók a kezdőlapon nincsenek szűrve. - A videók a kezdőlapon szűrve vannak. - Videók elrejtése a kezdőlapon a nézettség alapján - A keresési eredmények nincsenek szűrve. - A keresési eredmények szűrve vannak. - Keresési eredmények elrejtése nézettség alapján - A feliratkozott videók nincsenek szűrve. - A feliratkozott videók szűrve vannak. - Feliratkozott videók elrejtése nézettség alapján - Az ajánlott videók elrejtése, ha a megtekintések száma kevesebb a megadott számnál.\n\nIsmert probléma: a 0 megtekintésű videókat a rendszer nem szűri. - Ajánlott videók elrejtése megtekintések alapján - Az ennél nagyobb nézettségű videók el lesznek rejtve. - Nagyobb a nézettsége, mint - Az ennél kisebb nézettségű videók el lesznek rejtve. - Kisebb a nézettsége, mint - K -> 1 000\nM -> 1 000 000\nB -> 1 000 000 000\nviews -> megtekintések - Adja meg nyelvi sablonját a videók alatt megjelenő nézetek számához a felhasználói felületen. Minden kulcs (a nyelvében levő betű/szó) -> érték (a kulcs jelentése) új sorban legyen. A kulcsok a \"->\" előtt helyezkednek el. Ha megváltoztatja az alkalmazás vagy a rendszer nyelvét, akkor vissza kell állítania ezt a beállítást.\n\nPéldák:\nAngol: 10K views = K -> 1000, views -> megtekintések\nSpanyol: 10 K vistas = K -> 1000, vistas -> megtekintések - Kulcsok megtekintése - A termékek megtekintése banner látható. - A termékek megtekintése banner el van rejtve. - Termékek megtekintése banner elrejtése - A hangkeresés gomb látható. - A hangkeresés gomb el van rejtve. - Hangkeresés gomb elrejtése - A webes keresési találatok megjelennek - A webes keresési találatok rejtve vannak - Webes keresési találatok elrejtése - A YouTube Doodles látható. - A YouTube Doodles el van rejtve. - YouTube Doodles elrejtése - "A YouTube emblémák minden évben néhány napra megjelennek.\n\nHa jelenleg egy YouTube embléma látható a régiódban, és az elrejtése be van kapcsolva, akkor a keresősáv alatti szűrősáv is el lesz rejtve." - A nagyítás fedés látható. - A nagyítás fedés elrejtve. - Nagyítás fedés elrejtése - Kék Afn - Piros Afn - Egyéni - Alap - MMT - Revancify Kék - Revancify Piros - YouTube - Megtartja a fekvő módot a képernyő ki- és bekapcsolásakor teljes képernyőn. - Az ezredmásodpercek száma, amelyek alatt a tájkép mód erőltetett, miután a képernyőt bekapcsolták. - Fekvő mód tartás időtúllépése - Tartsa a fekvő módot - Alap - Dupla koppintás művelet letiltva. - "A dupla koppintás művelet engedélyezve van. - -• Koppintson duplán a kicsinyített videó nagyobb méretre való váltásához. -• Koppintson még egyszer duplán az eredeti méretre váltáshoz." - Dupla koppintás művelet engedélyezése - A Fogd és vidd letiltva. - A Fogd és vidd engedélyezve. - Fogd és vidd engedélyezése - A kibontás és bezárás gombok láthatóak. - A gombok elrejtve.\n(csúsztasd a minilejátszót a kibontáshoz vagy bezáráshoz) - Kibontás és bezárás gombok elrejtése - Az előre és hátra ugrás láthatóak. - Az előre és hátra ugrás elrejtve. - Előre és vissza ugrás gombok elrejtése - Az alszövegek megjelennek. - Az alszövegek elrejtve. - Alszövegek elrejtése - A minilejátszó átlátszóságának 0 és 100 között kell lennie. Visszaállítás alapértelmezettre. - Átlátszósági érték 0 és 100 között, ahol a 0 az átlátszó. - Átfedés átlátszósága - Eredeti - Telefon - Tablet - Modern 1 - Modern 2 - Modern 3 - Minilejátszó típusa - Átfedés gomb - "Tap to toggle always repeat states. -Tap and hold to toggle pause after repeat states." - Folyamatos ismétlés gomb megjelenítése - "Érintse meg a videó URL-jének másolásához. -Tartsa nyomva a videó URL-jének időbélyeggel való másolásához." - "Érintse meg a videó URL-jének időbélyeggel való másolásához. -Tartsa nyomva a videó időbélyegének másolásához." - Videó URL másolása időbélyeggel gomb megjelenítése - Videó URL-jének másolása gomb megjelenítése - Érintse meg a külső videóletöltő indításához. - A külső letöltés gomb megjelenítése - Érintsd meg a gombot az aktuális videó hangerejének elnémításához. Érintsd meg újra a némítás feloldásához. - Némítás gomb megjelenítése - Érintse meg és tartsa lenyomva a gomb állapotának megváltoztatásához. - Lejátszási sebesség visszaállítva (1.0x). - "Érintse meg a sebesség ablak megnyitásához. -Tartsa nyomva a sebesség alaphelyzetbe állításához." - A lejátszási sebesség gomb megjelenítése - "Koppints a lejátszási lista létrehozásához a csatorna összes videójával a legrégebbitől a legújabbig. -Érintsd meg és tartsa lenyomva a visszavonáshoz." - Időben rendezett lejátszási lista gomb megjelenítése - \"Érintsd meg az engedélyezőlista párbeszédpanel megnyitásához. -Érintsd meg és tartsd lenyomva az engedélyezőlista beállítási párbeszédpanel megnyitásához. - Kivétellista gomb megjelenítése - Az eredeti lejátszási lista letöltése gomb megnyitja az eredeti, beépített letöltőt. - Az eredeti lejátszási lista letöltése gomb megnyitja a külső letöltőt. - Lejátszási lista letöltése gomb felülbírálása - Az eredeti letöltés gomb megnyitja az eredeti, beépített letöltőt. - Az eredeti letöltés gomb megnyitja a külső letöltőt. - Videó letöltési gomb felülbírálása - A YouTube Music szükséges a gombművelet felülbírálásához. Koppints ide a YouTube Music letöltéséhez. - Előfeltétel - A YouTube Music gomb az erdetit app-ot nyitja meg. - A YouTube Music gomb az RVX Music-ot indítja. - YouTube Music gomb felülbírálása - Kizárva - Befoglalt - Normál - Akció gombok - További beállítások - Animáció / Visszajelzés - Letöltés gomb - Kísérleti funkciók - Régiós kép korlátozások - Importálás / Exportálás fájlként - Importálás / Exportálás szövegként - Kulcsszó szűrő - Egyéb - Felület gombok - Patch információ - Gyors műveletek - Ajánlott videó - Shorts polcok - Javasolt intézkedések - Használt eszköz - Megtekintések szűrő - Elemek elrejtése vagy megjelenítése a fiók menüben és a Te fülön. - Fiók menü - Videók alatti műveletgombok elrejtése vagy megjelenítése. - Művelet gombok - Hirdetések - Alternatív miniatűrök - Tiltsa le a mozifilmes világítás módot, vagy kerülje meg a korlátozásait. - Mozifilmes világítás mód - Rejtse el vagy jelenítse meg a kategória sávot a hírfolyamban, keresésben és a kapcsolódó videókban. - Kategória sáv - Rejtse el vagy mutassa meg a videók alatt található csatornasáv elemeit. - Csatorna sáv - Komponensek elrejtése vagy megjelenítése a csatornaprofilban. - Csatorna profil - Rejtse el vagy mutassa a hozzászólások rész elemeit. - Hozzászólások - Közösségi bejegyzések elrejtése vagy megjelenítése a hírfolyamban és a csatornán. - Közösségi bejegyzések - Komponensek elrejtése egyéni szűrőkkel. - Egyéni szűrő - Komponensek elrejtése vagy megjelenítése a hírfolyam előugró menüjében. - Felugró menü - Hírfolyam - Teljes képernyős móddal kapcsolatos elemek elrejtése vagy módosítása. - Teljes képernyő - Általános - Haptikus visszajelzés engedélyezése vagy letiltása. - Haptikus visszajelzés - Felülbírálja az alkalmazáson belüli gombok kattintási műveletét. - Gombok felülbírálása - Beállítások importálása vagy exportálása. - Beállítások importálása / exportálása - Módosítsa az alkalmazáson belüli minilejátszó stílusát. - Minilejátszó - Vegyes - Navigációs sáv komponenseinek láthatósága. - Navigációs sáv - Információk az alkalmazott javításokról. - Patch információ - A videólejátszó gombok elrejtése vagy megjelenítése. - Lejátszó gombok - A videólejátszóban található kinyíló menü elemeinek elrejtése vagy módosítása. - Felugró menü - Lejátszó - YouTube-felhasználónév visszaadása - Visszatérés a YouTube Dislike-ba - Szponzor Blokk - Szabja meg a keresősáv komponenseit. - Keresősáv - Elrejti a YouTube beállítások menü elemeit. - Beállítások menü - Komponensek elrejtése vagy megjelenítése a Shorts lejátszójában. - Shorts lejátszó - Shorts - Az adatfolyam meghamisítása, hogy elkerülje a lejátszási problémákat. - Adatfolyam meghamisítása - Húzásvezérlések - Elrejt vagy megváltoztat komponenseket az eszköztáron, mint például a keresősáv, eszköztár gombok és fejléc. - Eszköztár - Rejtsd el vagy mutasd a videóleírás komponenseit. - Videóleírás - A videók elrejtése kulcsszavak vagy nézettség alapján. - Videó szűrő - Videó - A megtekintési előzményekhez kapcsolódó beállítások módosítása. - Megtekintési előzmények - A gyors műveletek felső margójának 0 és 32 között kell lennie. Visszaállítás alapértelmezettre. - Állítsa be a gyorsművelet-tároló és a csúszka közötti távolságot 0-32 között. - Gyorsműveletek felső margó - "Erőteljesen elutasítja a szoftveres AV1 codec válaszát. -Egy másik kodek kerül alkalmazásra kb. 20 másodperc pufferezés után." - Elutasítja a szoftver AV1 kodek választ - Az alapfolyamat kb. 20 másodpercig pufferez. - Eltolás - A lejátszási sebesség módosítása csak a jelenlegi videóra érvényes - A lejátszási sebesség módosítása minden videóra érvényes - Lejátszási sebesség módosításainak megjegyzése - Nem fog felugró értesítés látszani, amikor megváltozik az alapértelmezett lejátszási sebesség. - Egy felugró értesítés fog látszani, amikor megváltozik az alapértelmezett lejátszási sebesség. - Mutass egy felugró értesítést - Alapértelmezett sebesség módosítva: %s - Felbontás változtatások alkalmazása a jelenlegi videóra - Felbontás változtatások alkalmazása az összes videóra - Felbontás változtatások mentése - Nem fog felugró értesítés látszani, amikor megváltozik az alapértelmezett videó minőség. - Egy felugró értesítés fog látszani, amikor megváltozik az alapértelmezett videó minőség. - Mutass egy felugró értesítést - mobil - Nem sikerült beállítani a videóminőséget. - wifi - "Eltávolítja a nézői belátás párbeszédpanelt. -Ez nem kerüli meg a korhatárt. Csak automatikusan fogadja el." - Távolítsa el a nézői diszkréciós párbeszédpanelt - A szoftver AV1 kodeket a VP9 kodekkel helyettesíti. - Szoftver AV1 kodek cseréje - A csatorna kezelő használatban van. - A csatorna név használatban van. - Cserélje ki a csatorna kezelőt - Érintse meg, hogy megjelenjen a hátralévő idő. - Érintse meg a lejátszási sebesség vagy a videóminőség felugró menüjének megnyitásához. - Időbélyeg művelet megváltoztatása - A létrehozás gombot beállítások gombra cseréli. - Létrehozás gomb cseréje - "Érintse meg a YouTube-beállítások megnyitásához. -Érintse meg és tartsa lenyomva az RVX beállítások megnyitásához." - "Érintse meg az RVX beállítások megnyitásához. -Érintse meg és tartsa lenyomva a YouTube-beállítások megnyitásához." - Gombhoz rendelhető művelet típusa - A keresősáv bélyegképei megjelennek a teljes képernyőn - A keresősáv bélyegképei megjelennek a keresősáv felett - Régi keresősáv bélyegképek visszaállítása - A régi videóminőség menü nem jelenik meg - A régi videóminőség menü jelenik meg - Régi videóminőség menü visszaállítása - \@kezelő (felhasználónév) - Megjelenítési formátum - Felhasználónév (@kezelő) - Felhasználónév - A kezelő van használatban. - A felhasználónév van használatban. - A YouTube-felhasználónév visszaadás engedélyezése - "A YouTube Data API v3 fejlesztői kulcsa szükséges ahhoz, hogy a Kezelő-t Felhasználónév-re cseréljék. - -Az API-kulcsok napi kvótája az ingyenes csomagban 10 000, és 1 kvótával cseréli le a Kezelő-t a Felhasználónévre 1 megjegyzés esetén. - -Kattintson az API-kulcs kiadás folyamatának megtekintéséhez." - A YouTube Data API-kulcsról - A fejlesztői kulcs a YouTube Data API v3 használatához. - YouTube adat API kulcs - 1. Nyissa meg a(z) <a href=%1$s>Új projekt létrehozását</a>.<br>2. Kattintson a <b>LÉTREHOZÁS</b> gomb.<br>3. Lépj a <a href=%2$s>YouTube Data API v3</a> oldalára.<br>4. Kattintson az <b>Engedélyezés</b> gombra.<br>5. Kattintson a <b>HITELESÍTÉSI ADATOK LÉTREHOZÁSA</b> gombra.<br>6. Válassza ki a <b>Nyilvános adatok</b> lehetőség.<br>7. Kattintson a <b>KÖVETKEZŐ</b> gombra.<br>8. Másolja ki az API-kulcsot.<br><br>※ Az API-kulcsot soha ne ossza meg másokkal, így az nem szerepel az importálási/exportálási beállításokban. - YouTube Data API v3 fejlesztői kulcs kiadás - Rólunk - Az adatokat a Return YouTube Dislike API biztosítja. További információért koppintson ide - ReturnYouTubeDislike.com - A tetszik gomb a legjobb megjelenésre formázva - A tetszik gomb minimális szélességre formázva - Kompakt tetszik gomb - A nem tetszések számként jelennek meg - A nem tetszések százalékban jelennek meg - Nem tetszések százalékban - A nem tetszések nem jelennek meg - A nem tetszések megjelennek - Return YouTube Dislike - A becsült kedvelések el vannak rejtve. - A becsült kedvelések láthatóak. - Becsült kedvelések megjelenítése - A nem tetszik funkció nem elérhető - A nem tetszik funkció nem elérhető (állapot: %d) - A nem tetszik funkció átmenetileg nem elérhető - A nem tetszik funkció nem elérhető (%s) - Töltse újra a videót a Return YouTube Dislike-hoz - A nem tetszések el vannak rejtve a Shorts videóknál - A Shorts videók nem tetszései láthatóak. - "A Shorts videók nem tetszései láthatóak. - -Korlátozás: A nem tetszések lehet nem jelennek meg kijelentkezett felhasználóval vagy inkognitó módban." - Shorts videók nem tetszéseiek megjelenítése - Nem jelenik meg üzenet, ha a Return YouTube Dislike nem elérhető - Üzenet megjelenítése, ha a Return YouTube Dislike nem elérhető - Üzenet megjelenítése, ha az API nem elérhető - Rejtett - Linkek megosztásakor eltávolítja a nyomkövetés lekérdezési paramétereket az URL-ekből. - Megosztási linkek tisztítása - "Az olyan kifejezések, mint a „#”, „Adománygyűjtés”, „Üzlet” és „termékek”, láthatóak a videófeliratokban." - "Az olyan kifejezések, mint a „#”, „Adománygyűjtés”, „Üzlet” és „termékek”, el lettek rejtve a videófeliratokban." - Videó feliratának tisztítása - Rólunk - sponsor.ajay.app - Az adatokat a SponsorBlock API biztosítja. Koppintson ide, ha többet szeretne megtudni és megtekintené a letöltéseket más platformokra - API URL megváltoztatva - API URL érvénytelen - API URL alaphelyzetbe állítása - Megjelenés - A szín megváltoztatva - Szín: - Érvénytelen színkód - Szín alaphelyzetbe - Új szegmensek létrehozása - A szegmens viselkedésének módosítása - Automatikusan elrejti a kihagyás gombot - A kihagyás gomb a teljes szakasz alatt megjelenik - A kihagyás gomb néhány másodperc után eltűnik - Kompakt kihagyás gomb használata - A kihagyás gomb a legjobb megjelenésre formázva - A kihagyás gomb minimális szélességre formázva - Az új szegmens létrehozása gomb megjelenítése - Az új szegmens létrehozása gomb nem jelenik meg - Az új szegmens létrehozása gomb megjelenik - SponsorBlock bekapcsolása - A SponsorBlock egy közösségi rendszer a zavaró részek kihagyására a YouTube videókon - Szavazás gomb megjelenítése - Szegmens szavazás gomb elrejtve - Szegmens szavazás gomb megjelenítve - Általános - Új szegmens léptetés beállítása - Az értéknek pozitív számnak kell lennie - Ezredmásodpercek száma, ameddig az időbeállító gombok léptetnek új szegmensek létrehozásakor - API URL módosítása - Az a cím, amelyet a SponsorBlock a szervere eléréséhez használ - Minimális szegmens időtartam - Érvénytelen időtartam. - A beállított értéknél (másodpercben) rövidebb szakaszokat nem hagyja ki vagy jeleníti meg. - Átugrásszámláló bekapcsolása - A kihagyások számának követése nem engedélyezett - Értesíti a SponsorBlock ranglistáját, hogy mennyi időt takarított meg. Minden egyes szakasz kihagyásakor üzenetet küld a ranglistának - Felugró üzenet megjelenítése az automatikusan kihagyott szakaszoknál - Felugró üzenet nem látható. Koppintson ide egy példa megtekintéséhez - Felugró üzenet megjelenítése, ha a szakasz automatikusan ki lett hagyva. Koppintson ide egy példa megtekintéséhez - A videó hosszának megjelenítése szegmensek nélkül - A videó teljes hossza látható - A videó hossza mínusz minden szegmens, zárójelben a teljes videó hossza mellett - Az Ön privát felhasználói azonosítója - A privát felhasználói azonosítónak legalább 30 karakter hosszúnak kell lennie - Ezt bizalmasan kell kezelni. Olyan mint egy jelszó és senkivel sem ajánlott megosztani. Ha valaki megszerzi, meg tudja személyesíteni önt - Már elolvastam - Olvassa el a SponsorBlock irányelveket szegmensek beküldése előtt - Mutasd - Kövesse az irányelveket - Az irányelvek tippeket és szabályokat tartalmaznak a szegmensek beküldésével kapcsolatban - Irányelvek megtekintése - Válassza ki a szakasz kategóriáját - The segment lasts from %1$02d:%2$02d to %3$02d:%4$02d (%5$d minutes %6$02d seconds)\nIs it ready to submit? - A szegmens:\n\n%1$s-tól\n\n%2$s-ig\n\n(%3$s)\n\nKészen állsz a beküldésre? - Helyesek az időpontok? - A kategória letiltva a beállításokban. Engedélyezze a beküldéshez. - Akarja szerkeszteni a rész kezdetének vagy végének időzítését? - Érvénytelen idő van megadva - A szakasz időzítésének kézi beállítása - Beállítja a(z) %s-t az új szakasz kezdetének vagy végének? - végpont - Előbb jelöljön meg két pontot az idősávon - kezdőpont - most - Szakasz előnézete a zökkenőmentesen kihagyás érdekében - A kezdetnek a vége előtt kell lennie - A szakasz végének időpontja: - A szakasz kezdetének időpontja: - Új SponsorBlock szakasz - Visszaállítás - Színek visszaállítása - Érintőleges tartalom/Viccek - Csak töltelék vagy humornak hozzáadott részek, amik nem szükségesek a videó fő tartalmának megértéséhez. Ne tartalmazzon olyan szegmenseket, amik kontextust, vagy háttérinformációt szolgáltatnak - Kiemelt - A videónak azon része, amit a legtöbben keresnek - Interakció emlékeztető (Feliratkozás) - Egy rövid emlékeztető arról, hogy like-oljunk, iratkozzunk fel, vagy kövessük őket a tartalom közben. Ha ez hosszabb vagy egy adott témáról szól, inkább az önpromóció alá tartozik. - Megszakítás/Intro animáció - Egy részlet tartalom nélkül. Lehet szünet, álló képkocka, vagy ismétlődő animáció. Nem használandó információt tartalmazó átmeneteknél - Zene: zenementes rész - Csak zenei videókhoz használható. Zenei videók zene nélküli részei, amelyek még nem tartoznak más kategóriába - Záróképernyő/Köszönetek - Stáblista, vagy amikor megjelennek a YouTube zárókártyák. Nem tartozik bele az információt tartalmazó összegzés - Előzetes/Ismétlés - Olyan klipek gyűjteménye, amik azt mutatják, hogy mi következik majd ebben, vagy a sorozat más videóiban és minden információ megismétlődik később a videóban - Nem fizetett hirdetés/önpromóció - Hasonló a „szponzorhoz”, csak ez nem fizetett, vagy saját promóció. Beletartoznak a saját árucikkek, adományok, illetve információk azokról, akikkel együttműködtek - Szponzor - Fizetett promóció, vagy közvetlen reklám. Nem önpromóció, vagy ingyenes említése ügyeknek/tartalomkészítőknek/weboldalaknak/termékeknek amik tetszenek nekik - Másolás - Nem sikerült exportálni ezt: %s - Beállítások importálása/exportálása - Az Ön SponsorBlock JSON-konfigurációja, amely importálható/exportálható ReVanced és más SponsorBlock platformokra - A SponsorBlock JSON-konfigurációja, amely importálható/exportálható a ReVanced és más SponsorBlock platformokra. Ez magában foglalja a privát felhasználói azonosítóját is. Ügyeljen arra, hogy ezt bölcsen ossza meg - Nem sikerült importálni ezt: %s - A beállítások sikeresen importálva - A beállításai tartalmazzák a privát SponsorBlock felhasználói azonosítót.\n\nA felhasználói azonosító olyan, mint egy jelszó, és soha nem szabad megosztani.\n - Ne jelenjen meg többet - A beállításokat a vágólapra másoltuk. - Automatikus kihagyás - Automatikus kihagyás egyszer - Kihagyás - Kiemelt - Töltelékrész kihagyása - Ugrás az kiemelthez - Interakció kihagyása - Intro kihagyása - Szünet kihagyása - Szünet kihagyása - Nem zenei rész kihagyása - Outro kihagyása - Előnézet kihagyása - Recap kihagyása - Előnézet kihagyása - Promóció kihagyása - Szponzor kihagyása - Szakasz kihagyása - Letiltás - Megjelenítés a folyamatsávban - Kihagyás gomb megjelenítése - Töltelékrész kihagyva - Kihagyva a kiemelthez - Zavaró emlékeztető kihagyva - Intro kihagyva - Szünet kihagyva - Szünet kihagyva - Több szakasz kihagyva - Zenementes rész kihagyva - Outro kihagyva - Bevezető kihagyva - Recap kihagyva - Bevezető kihagyva - Önpromóció kihagyva - Szponzor kihagyva - Beküldésre váró rész kihagyva - A SponsorBlock átmenetileg nem elérhető - A SponsorBlock jelenleg nem elérhető (állapot %d) - A SponsorBlock jelenleg nem elérhető (API időtúllépés) - Statisztikák - A statisztikák átmenetileg nem elérhetőek (API leállt) - Betöltés... - Az ön hírneve: <b>%.2f</b> - <b>%s</b> szegmenstől mentettél meg másokat - %1$s óra %2$s perc - %1$s perc %2$s másodperc - %s másodperc - Ez <b>%s</b> az életükből.<br>Koppintson a ranglista megtekintéséhez - Koppintson ide a globális statisztikák és a kiemelt közreműködők megtekintéséhez - SponsorBlock ranglista - A SponsorBlock ki van kapcsolva - <b>%s</b> szakaszt kihagyott - Visszaállítja a kihagyott szakaszok számlálóját? - Ez <b>%s</b> - <b>%s</b> szegmenst készítettél - Koppintson ide a szegmensek megtekintéséhez. - Az ön felhasználóneve: <b>%s</b> - Koppintson ide a felhasználónév megváltoztatásához - A felhasználónév nem módosítható: Állapot: %1$d %2$s - A felhasználónév sikeresen módosítva - Nem sikerült beküldeni a szakaszt.\nMár létezik - Nem lehet beküldeni a szegmenst: %s - Nem lehet beküldeni a szegmenst: %s - Nem sikerült beküldeni a szegmenst.\nGyakorisági korlát (Túl sok beküldés) - A SponsorBlock átmenetileg nem működik - Nem lehet beküldeni a szegmenst (állapot: %1$d %2$s) - A szakasz sikeresen beküldve - Nem látható üzenet, ha a SponsorBlock nem elérhető - Üzenet látható, ha a SponsorBlock nem elérhető - Üzenet megjelenítése, ha az API nem elérhető - Kategória megváltoztatása - Leszavazás - Nem lehet szavazni a szegmensre: %s - Nem lehet szavazni a szegmensre (API időtúllépés) - Nem lehet szavazni erre (állapot: %1$d %2$s) - Nincsenek szakaszok, amikre szavazni lehet - Pozitív szavazás - A beállítások a vágólapra másolva. - Az időbélyeg a vágólapra másolva. (%s) - Az URL a vágólapra másolva - Az URL időbélyeggel a vágólapra másolva - Eredeti - Felfelé menő hüvelykujj - Felfelé menő hüvelykujj (Cairo) - Szív - Szív (színárnyalatos) - Rejtett - Dupla koppintás animáció - A Meta panel alsó margójának 0-64 között kell lennie. - Állítsd be a keresősáv és a meta panel közötti távolságot 0-64 között. - Meta panel alsó margó - A magasság százalékának 0-100 (%) között kell lennie. - Beállítja a navigációs sáv elrejtésekor megmaradó üres terület magasságának arányát 0 és 100 (%) között. - Az üres hely magasságának százaléka - Nyomd meg és tartsd lenyomva az időbélyeget a Shortok ismétlés állapotának megváltoztatásához. - Időbélyegző hosszú nyomás művelet - "Megjeleníti a videócím szakaszt teljes képernyős módban. - -Korlátozás: A videó címe eltűnik, ha rákattint." - Videócím szakasz megjelenítése - Ha az automatikus lejátszás engedélyezve van, a következő videó a visszaszámlálás befejezése után lejátszódik. - Ha az automatikus lejátszás engedélyezve van, a következő videó azonnal lejátszódik. - Automatikus lejátszás visszaszámlálás átugrása - "Átugorja a videók kezdetén az előzetesen betöltött puffert, hogy azonnal alkalmazza az alapértelmezett videóminőséget. - -Info: -• Amikor a videó elindul, kb. 0,3 másodperces késés van. -• Nem vonatkozik HDR videókra, élő stream videókra, vagy 15 másodpercnél rövidebb videókra." - Előtöltött puffer kihagyása - A felugró értesítés nem látható. - Felugró értesítés látható. - Pirítós mutatása átugoráskor - A beállítás bekapcsolása videolejátszási problémákat okozhat. - Kihagyott előtöltött puffer. - Az egyéni lejátszási sebességnek 0 és 8.0 között kell lennie. Visszaállítás az alapértelmezett értékekre. - Gyorsított lejátszás érték 0-8.0 között. - Gyorsított lejátszás értéke - "A kliens verziójának hamisítása régi verzióra. - -• Ez megváltoztatja az alkalmazás megjelenését, de ismeretlen mellékhatások léphetnek fel. -• Ha később kikapcsolja, a régi felhasználói felület megmaradhat az alkalmazás adatainak törléséig." - Verzió nincs hamisítva - Verzió hamisítás - 17.33.42 - Visszaállítás a régi felhasználói felületre - 17.41.37 - Régi lejátszási lista polc visszállítása - 18.05.40 - Régi megjegyzési szövegdoboz visszaállítása - 18.17.43 - Régi előugró lejátszói ablak visszaállítása - 18.33.40 - Régi Shorts művelet sáv visszaállítása - 18.38.45 - Visszaállítja a régi alapértelmezett videó minőség viselkedést - 18.48.39 - Letiltja a \'Megtekintések\' és a \'Kedvelések\' valós idejű frissítését - 19.13.37 - Visszaállítja a régi stílusú gördülő szám animációkat - Hamis alkalmazásverzió - Írja be a hamis alkalmazásverziót. - Hamisított alkalmazásverzió célja - Alkalmazásverzió hamisítása - "App verzió hamisítása egy régebbi YouTube verzióra. - -Ez meg fogja változtatni az app működését és kinézetét és nem várt mellékhatások előfordulhatnak. - -Ha kikapcsolja, akkor ajánlott törölni az app adatait, hogy elkerülje a UI hibákat." - "Meghamisítja az eszköz méreteit annak érdekében, hogy feloldjon olyan jobb videóminőséget, amely esetleg nem érhető el az eszközön." - Eszközméret hamisítása - Az iOS videokodek AVC (H.264), VP9 vagy AV1. - Az iOS videokodek AVC (H.264). - Kényszerített iOS AVC (H.264) - "Ennek engedélyezése javíthatja az akkumulátor élettartamát, és kijavíthatja a lejátszás akadozását.\n\nAz AVC (H.264) maximális felbontása 1080p, és a videolejátszás több internetadatot használ, mint a VP9 vagy az AV1." - "• Az audiosáv menü hiányzik." - "• Az audiosáv menü hiányzik." - "• Előfordulhat, hogy a filmeket vagy a fizetős videókat nem lehet lejátszani. -• Az élő közvetítések az elejétől kezdődnek." - Hamisítás mellékhatásai - • A videó esetleg nem játszódik le. - Az adatfolyam lekérésére használt kliens a statisztikában kockáknak nem látható. - Az adatfolyam lekérésére használt kliens a statisztikában kockáknak látható. - Megjelenítés a statisztikában kockáknak - "Az adatfolyam nincs meghamisítva. Lehet, hogy a videó lejátszás nem működik." - Az adatfolyam hamisított. - Adatfolyam meghamisítása - Android - Android TV - Android VR - iOS - Alapértelmezett kliens - A beállítás kikapcsolása videólejátszási problémákat okozhat. - A fényerő csúsztatás érzékenységének 1 és 1000 (%) között kell lennie. - Állítsa be a minimális távolságot a fényerő csúsztatásához 1 és 1000 (%) között.\nMinél kisebb a minimális távolság, annál gyorsabban változik a fényerő szintje. - Fényerő csúsztatás érzékenység - A csúsztatási mozdulatok le vannak tiltva a \'Képernyő lezárása\' módban. - A csúsztatási mozdulatok engedélyezve vannak a \'Képernyő lezárása\' módban. - A csúsztatási mozdulatok a \'Képernyő lezárása\' módban - Automatikus - A csúsztatáshoz szükséges küszöbérték. - A csúsztatás küszöbértéke - A csúsztatási átfedés hátterének láthatósága - A csúsztatás hátterének láthatósága - A lehúzható terület mérete nem lehet nagyobb 50-nél. Visszaállítás az alapértelmezettre. - Az lehúzható képernyőterület százalékos aránya.\n\n -Megjegyzés: Ezzel a képernyőterület méretét is megváltoztatja, ahol érzékeli az ugráshoz szükséges dupla koppintást. - Csúsztatható átfedés mérete - A csúsztatási fedés szövegmérete. - Csúsztatási-átfedés szövegmérete - Az átfedés láthatóságának időtartama ezredmásodpercben - Csúsztatási átfedés időkorlátja - A hangerő csúsztatás érzékenységének 1 és 1000 (%) között kell lennie. - Állítsa be a hangerő csúsztatásának minimális távolságát 1 és 1000 (%) között.\n\nMinél kisebb a minimális távolság, annál gyorsabban változik a hangerő.\n\nAz ajánlott hangerő-csúsztatási érzékenység 100% 15 hangerős lépéseknél és 10% 150 hangerős lépéseknél. - Hangerő csúsztatás érzékenység - "A létrehozás gomb és az értesítés gomb helyzetét cseréli ki a készülék információinak meghamisításával. - -• A készüléket esetleg újra kell indítani a beállítás változtatásának érvényesítéséhez. -• Ha ezt a beállítást letiltja, több hirdetést tölt be a szerver oldalról. -• Ha a videó hirdetések láthatóak akarja tenni, letiltja ezt a beállítást." - A Létrehozás gomb nincs felcserélve az Értesítések gombbal. - "A Létrehozás gomb megcserélése az Értesítések gombbal.\n\nMegjegyzés: Engedélyezés esetén a videó hirdetéseket is elrejti." - Létrehozás és értesítések gombok felcserélése - "Ennek kikapcsolása esetleg több reklámot tölt be a szerverről. - -Tovább, a reklámok nem lesznek tiltva a Shortokban. - -Ha ez a beállítás nem működik, váltson inkognító módra." - Készlet - RVX Music - %s nincs telepítve. Kérlek telepítsd. - A telepített RVX Music csomag neve. - RVX Music csomag név - • A megtekintési előzmények le vannak tiltva. - "• Követi a Google-fiók megtekintési előzményeinek beállításait. -• Előfordulhat, hogy a megtekintési előzmények nem működik a DNS vagy a VPN miatt." - • Követi a Google-fiók megtekintési előzményeinek beállításait. - Megtekintési előzmények állapota - Kattintson a YouTube megtekintési előzmények kezelésének megnyitásához. - Előzmények kezelése - Eredeti - Domain cseréje - Megtekintési előzmények tiltása - Megtekintési előzmények típusa - Nem sikerült hozzáadni a(z) \'%1$s\' csatornát a(z) %2$s engedélyezőlistához. - \'%1$s\' csatorna hozzá lett adva a %2$s kivételekhez. - Nincsen kivételezett csatornák. - Nem lett hozzáadva a kivételekhez. - Csatornainformációk betöltése sikertelen. - Hozzáadva a kivétellistához. - Lejátszási sebesség - Eltávolítod a(z) \'%1$s\' csatornát a(z) %2$s engedélyezőlistáról? - Nem sikerült eltávolítani a(z) \'%1$s\' csatornát a(z) %2$s engedélyezőlistáról. - \'%1$s\' csatorna törölve a(z) %2$s engedélyezőlistáról. - Ellenőrizd vagy távolítsd el az kivétellistához hozzáadott csatornákat. - Csatorna kivétellista - Szponzor Blokk - diff --git a/src/main/resources/youtube/translations/it-rIT/missing_strings.xml b/src/main/resources/youtube/translations/it-rIT/missing_strings.xml deleted file mode 100644 index 8956daf6e..000000000 --- a/src/main/resources/youtube/translations/it-rIT/missing_strings.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - AI-generated video summary section is shown. - AI-generated video summary section is hidden. - Hide AI-generated video summary section - MMT Orange - MMT Pink - MMT Turquoise - diff --git a/src/main/resources/youtube/translations/it-rIT/strings.xml b/src/main/resources/youtube/translations/it-rIT/strings.xml deleted file mode 100644 index 06e8062ef..000000000 --- a/src/main/resources/youtube/translations/it-rIT/strings.xml +++ /dev/null @@ -1,1725 +0,0 @@ - - - Vuoi attivare i controlli di accessibilità del riproduttore? - I tuoi controlli sono diversi poiché un servizio di accessibilità è attivato. - Continua - Non mostrare di nuovo - "GmsCore non ha il permesso di eseguire in background. - -Segui la guida 'Non uccidere la mia app!' Per il tuo dispositivo, e applica le istruzioni alla tua installazione di GmsCore. - -Questo è necessario per l'app per funzionare." - "Le ottimizzazioni della batteria GmsCore devono essere disabilitate per evitare problemi. - -Tocca il pulsante continua e disabilita le ottimizzazioni della batteria." - Apri sito - Azione necessaria - Abilitare la messaggistica cloud per ricevere notifiche. - Apri GmsCore - GmsCore non è installato. Installarlo. - "DeArrow fornisce thumbnails provenienti da crowdsourcing per i video di YouTube. Questi thumbnails sono spesso più rilevanti di quelle fornite da YouTube. - -Se attivato, gli URL dei video verranno inviati al server API e non vengono inviati altri dati. Se un video non ha thumbnails DeArrow, vengono mostrate i thumbnails originali o le catture statiche. - -Tocca qui per saperne di più su DeArrow." - DeArrow - URL API DeArrow non valido - L\'URL dell\'endpoint della cache dei thumbnails DeArrow. - Endpoint API di DeArrow - La notifica toast è nascosta se DeArrow non è disponibile. - La notifica toast è visibile se DeArrow non è disponibile. - Mostra una notifica toast se l\'API non è disponibile - DeArrow temporaneamente non disponibile (Stato: %s) - DeArrow temporaneamente non disponibile - Scheda Home - Scheda Tu - Thumbnails originali - DeArrow e thumbnails originali - DeArrow e catture statiche - Catture statiche - Playlist e video consigliati - Risultati di ricerca - Thumbnails alternativi - Le catture statiche sono prese dall\'inizio, metà o fine di ogni video. Queste immagini sono integrate in YouTube e non viene usata nessuna API esterna. - Catture statiche del video - Le catture statiche di alta qualità sono attivati. - Le catture statiche di media qualità sono attivati. I thumbnails caricheranno più velocemente, ma i video dal vivo, quelli non rilasciati e molto vecchi potrebbero mostrare thumbnails vuoti. - Attiva le catture statiche veloci - Inizio - Metà - Fine - Il punto del video da cui prendere le catture statiche - Scheda Iscrizioni - L\'informazione nel timestamp è nascosta. - "L'informazione nel timestamp è visibile. - -Tocca e tieni premuto l'informazione mostrata per cambiare il tipo." - Mostra l\'informazione nel timestamp - Velocità di riproduzione. - Qualità. - Cambia il tipo di informazione da mostrare - La modalità Ambient in modalità risparmio energetico è disattivata. - La modalità Ambient in modalità risparmio energetico è attivata. - Bypassa le restrizioni della modalità Ambient - Il dominio da cui prelevare le immagini.\nNota: Inserisce solo il nome di dominio, cioè senza il prefisso \"https\:\/\/\". - Dominio alternativo - Usando l\'host originale per le immagini.\n\nL\'attivazione di questa impostazione può correggere le immagini mancanti, poichè sono bloccate in alcune regioni. - Usando l\'host yt4.ggpht.com per le immagini. - Bypassa le restrizioni regionali delle immagini - Originale - Telefono - Telefono (Massimo 480 dp) - Tablet - Tablet (Minimo 600 dp) - Cambia l\'interfaccia - Levetta. - Testo. - Cambia il tipo di interruttore - Nativo. - Sistema. - Cambia la schermata di condivisione - Riproduzione automatica - Predefinito - Pausa - Ripeti - Cambia lo stato di ripetizione degli Shorts - Esplora canali - Corsi / Istruzione - Predefinita (Home) - Esplora - Giochi - Cronologia - Tu (Raccolta) - Video piaciuti - Live - Film e TV - Musica - Cerca - Shorts - Sport - Iscrizioni - Tendenze - Guarda più tardi - Cambia la scheda iniziale - La pagina iniziale cambia solo una volta. - "La pagina iniziale cambia sempre. - -Nota: il pulsante Indietro della barra degli strumenti potrebbe non funzionare." - Cambia il tipo di pagina iniziale - Nativa. - Premium. - Cambia l\'intestazione di YouTube - L\'elenco dei componenti da filtrare, separati da nuove righe. - Modifica il filtro personalizzato - Il filtro personalizzato è disattivato. - Il filtro personalizzato è attivato. - Attiva il filtro personalizzato - Filtro personalizzato non valido (%s) - Vecchio menù a comparsa. - Finestra personalizzata. - Cambia il tipo di menù della velocità di riproduzione personalizzata - Velocità personalizzate di riproduzione non valide, ripristinate al predefinito - Velocità di riproduzione personalizzata non valida. Utilizzando i valori predefiniti. - Aggiungi, rimuovi o modifica le velocità di riproduzione. - Modifica le velocità personalizzate del video - L\'opacità della sovrapposizione del riproduttore deve essere tra 0 e 100 - Il valore dell\'opacità è tra 0 e 100, dove 0 è trasparente. - L\'opacità personalizzata della sovrapposizione del riproduttore - Digita il codice esadecimale del colore della barra di avanzamento. - Valore del colore personalizzato della barra di avanzamento - Per aprire RVX da un\'applicazione esterna, attiva \"Apri collegamenti supportati\" e abilita gli indirizzi web supportati. - Apri le impostazioni predefinite dell\'app - Velocità di riproduzione predefinita - Qualità predefinita dei video con connessione dati - Qualità predefinita dei video con Wi-Fi - Disattiva la modalità Ambient a schermo intero - La modalità Ambient è attivata a schermo intero. - La modalità Ambient è disattivata a schermo intero. - Disattiva la modalità Ambient a schermo intero. - Disattiva sempre la modalità Ambient - La modalità Ambient è attivata. - La modalità Ambient è disattivata. - Disattiva la modalità Ambient - Le tracce audio automatiche forzate sono attivate. - Le tracce audio automatiche forzate sono disattivate. - Disattiva le tracce audio automatiche forzate - I sottotitoli automatici forzati sono attivati. - I sottotitoli automatici forzati sono disattivati. - Disattiva i sottotitoli automatici forzati - Il pannello pop-up automatico del riproduttore è attivato. - Il pannello pop-up automatico del riproduttore è disattivato. - Disattiva il pannello pop-up automatico del riproduttore - "Il cambio automatico alle playlist miste è attivato quando la riproduzione automatica è attivata. - -La riproduzione automatica può essere modificata nelle impostazioni di YouTube: -Impostazioni → Riproduzione automatica → Telefono cellulare/tablet" - Il cambio automatico alle playlist miste è disattivato. - Disattiva il cambio automatico alle playlist miste - L\'attivazione di questa impostazione disattiva il cambio automatico alle playlist miste durante la riproduzione di musica con la riproduzione automatica attiva. - La velocità di riproduzione predefinita nei video dal vivo è attivata - La velocità di riproduzione predefinita nei video dal vivo è disattivata - Disattiva la velocità di riproduzione predefinita nei video dal vivo - La velocità di riproduzione predefinita è abilitata per la musica. - "La velocità di riproduzione predefinita è disabilitata per la musica. - -Limitazione: Questa impostazione potrebbe non applicarsi ai video che non includono il banner 'Ascolta su YouTube Music'." - Disabilita la velocità di riproduzione per la musica - Il pannello di coinvolgimento è abilitato. - Il pannello di coinvolgimento è disabilitato. - Disabilita pannello di coinvolgimento - La vibrazione tattile dei capitoli è attivata. - La vibrazione tattile dei capitoli è disattivata. - Disattiva la vibrazione dei capitoli - La vibrazione tattile del trascinamento è attivata. - La vibrazione tattile del trascinamento è disattivata. - Disattiva la vibrazione del trascinamento - La vibrazione tattile dello scorrimento è attivata. - La vibrazione tattile dello scorrimento è disattivata. - Disattiva la vibrazione di scorrimento - La vibrazione tattile dell\'annullamento è attivata. - La vibrazione tattile dell\'annullamento è disattivata. - Disattiva la vibrazione dell\'annullamento - La vibrazione tattile dello zoom è attivata. - La vibrazione tattile dello zoom è disattivata. - Disattiva la vibrazione dello zoom - La luminosità HDR automatica è attivata. - La luminosità HDR automatica è disattivata. - Disattiva la luminosità HDR automatica - L\'HDR dei video è attivato. - L\'HDR dei video è disattivato. - Disattiva l\'HDR dei video - La modalità orizzontale a schermo intero è attivata. - La modalità orizzontale a schermo intero è disattivata. - Disattiva la modalità orizzontale a schermo intero - I pulsanti Mi Piace e Non Mi Piace brilleranno quando menzionati. - I pulsanti Mi Piace e Non Mi Piace non brilleranno quando menzionati. - Disattiva il bagliore dei pulsanti Mi Piace e Non Mi Piace - "Disattiva il protocollo QUIC di CronetEngine." - Disattiva il protocollo QUIC - Il riproduttore degli Shorts riprenderà all\'avvio dell\'app. - Il riproduttore degli Shorts non riprenderà all\'avvio dell\'app. - Disattiva la ripresa del riproduttore degli Shorts - L\'effetto contatore dei numeri è attivato. - L\'effetto contatore dei numeri è disattivato. - Disattiva l\'effetto contatore dei numeri - I capitoli sono abilitati nella barra di avanzamento. - I capitoli sono disabilitati nella barra di avanzamento. - Disabilita i capitoli della barra di avanzamento - L\'animazione sopra il pulsante Mi Piace è attivata. - L\'animazione sopra il pulsante Mi Piace è disattivata. - Disattiva l\'animazione sopra il pulsante Mi Piace - "Disattiva la funzione \"2x>>\" quando tieni premuto. - -Note: -• La disattivazione della sovrapposizione della velocità ripristinerà il gesto \"Scorri la barra di avanzamento\" della vecchia interfaccia. -• La disattivazione di questa impostazione non attiva forzatamente la sovrapposizione della velocità." - Disattiva la sovrapposizione della velocità - L\'animazione di avvio è attivata. - L\'animazione di avvio è disattivata. - Disattiva l\'animazione di avvio - "Disabilita le seguenti interazioni quando la descrizione del video viene espansa: - -• Tocca per scorrere. -• Tocca e tieni premuto per selezionare il testo." - Disabilita l\'interazione della descrizione video - Il codec video VP9 è attivato. - "Il codec video VP9 è disattivato. - -• La risoluzione massima è 1080p. -• La riproduzione utilizzerà più dati internet di VP9. -• VP9 è usato per la riproduzione HDR." - Disattiva il codec video VP9 - La barra di avanzamento Cairo e disabilitata. - "La barra di avanzamento Cairo è abilitata. - -Effetto collaterale: il tema Cairo viene applicato anche ai punti di notifica." - Abilita la barra di avanzamento Cairo - La sovrapposizione dei controlli compatti è disattivata. - La sovrapposizione dei controlli compatti è attivata. - Attiva la sovrapposizione dei controlli compatti - La velocità di riproduzione personalizzata è disattivata. - La velocità di riproduzione personalizzata è attivata. - Attiva la velocità di riproduzione personalizzata - Il colore personalizzato della barra di avanzamento è disattivato. - Il colore personalizzato della barra di avanzamento è attivato. - Attiva il colore personalizzato della barra di avanzamento - Il registro del debug non include il buffer. - Il registro del debug include il buffer. - Attiva il registro del debug con il buffer - Il registro del debug è disattivato. - Il registro del debug è attivato. - Attiva il registro del debug - La velocità di riproduzione predefinita negli Shorts è disattivata. - La velocità di riproduzione predefinita negli Shorts è attivata. - Attiva la velocità di riproduzione predefinita negli Shorts - Il browser esterno è disattivato. - Il browser esterno è attivato. - Attiva il browser esterno - La schermata di caricamento gradiente è disattivata. - La schermata di caricamento gradiente è attivata. - Attiva la schermata di caricamento gradiente - Lo spazio tra i pulsanti di navigazione è normale. - Lo spazio tra i pulsanti di navigazione è ristretto. - Attiva i pulsanti di navigazione compatti - Seguendo la regola predefinita di reindirizzamento. - Bypassando i reindirizzamenti degli URL. - Attiva l\'apertura diretta dei link - Attiva il codec audio Opus se la risposta del riproduttore lo include. - Attiva il codec audio Opus - Non salva e ripristina la luminosità uscendo ed entrando a schermo intero. - Salva e ripristina la luminosità uscendo ed entrando a schermo intero. - Attiva il salvataggio e il ripristino della luminosità - Il tocco della barra di avanzamento è disattivato. - Il tocco della barra di avanzamento è attivato. - Attiva il tocco della barra di avanzamento - "Questo ripristinerà le miniature ai livestreams che non hanno miniature nella barra di avanzamento. - -utilizzo dei dati Internet può essere più alto, e le miniature della barra di avanzamento avranno un leggero ritardo prima di mostrare. - -Questa funzione funziona meglio con una connessione internet molto veloce." - Le miniature della barra di avanzamento sono di media qualità. - Le miniature della barra di avanzamento sono di alta qualità. - Abilita miniature di alta qualità - Timestamp è disabilitato. - "Timestamp è abilitato. - -Problema noto: Poiché questa è una caratteristica nella fase di sviluppo di Google, il layout potrebbe essere rotto." - Abilita timestamp - Il gesto della luminosità è disattivato. - Il gesto della luminosità è attivato. - Attiva il gesto della luminosità - La vibrazione tattile è disattivata. - La vibrazione tattile è attivata. - Attiva la vibrazione tattile - Il valore più basso del gesto della luminosità non attiva la luminosità automatica. - Il valore più basso del gesto della luminosità attiva la luminosità automatica. - Attiva il gesto della luminosità automatica - Il gesto Premi-per-Trascinare è disattivato. - Il gesto Premi-per-Trascinare è attivato. - Attiva il gesto Premi-per-Trascinare - Trascinando verso l\'alto non verrà riprodotto il video successivo e trascinando verso il basso non verrà riprodotto il video precedente. - Trascinando verso l\'alto verrà riprodotto il video successivo e trascinando verso il basso verrà riprodotto il video precedente. - Attiva il gesto per cambiare video - Il gesto del volume è disattivato. - Il gesto del volume è attivato. - Attiva il gesto del volume - La barra di navigazione traslucida è disattivata. - La barra di navigazione traslucida è attivata. - Attiva la barra di navigazione traslucida - Il gesto per passare a schermo intero trascinando verso il basso sotto il riproduttore è disattivato. - Il gesto per passare a schermo intero trascinando verso il basso sotto il riproduttore è attivato. - Attiva i gesti del pannello di controllo - "L'attivazione di questa impostazione disattiverà il pulsante Impostazioni nella scheda Tu. - -In questo caso, utilizzare i seguenti percorsi per accedere alle impostazioni: -• Scheda Tu → Visualizza canale → 3 punti verticali → Impostazioni -• Scheda Home → Pulsante Esplora → Tendenze → 3 punti verticali → Impostazioni" - Attiva la barra di ricerca estesa nella scheda Tu - La barra di ricerca estesa è disattivata. - La barra di ricerca estesa è attivata. - Attiva la barra di ricerca estesa - La barra di ricerca estesa con l\'intestazione è disattivata. - La barra di ricerca estesa con l\'intestazione è attivata. - Attiva la barra di ricerca estesa con l\'intestazione - Descrizione - "Inserisci il titolo del pannello descrizione video nella tua lingua. -'Espandi descrizione video' potrebbe non funzionare se la stringa inserita non corrisponde al titolo del pannello di descrizione video. " - Titolo nel pannello descrizione video - Le descrizioni video non vengono espanse automaticamente. - Le descrizioni video vengono espanse automaticamente. - Espandi descrizioni video - Desideri procedere? - Ripristinato ai valori predefiniti - Riavvia per caricare l\'interfaccia normalmente - "C'è un bug sul lato server di YouTube che fa sì che il numero di mi piace e non mi piace, le visualizzazioni e le date di caricamento siano nascosti per alcuni utenti. - -Una soluzione temporanea è quella di camuffare la versione dell'app a 19.13.37. - -Vuoi camuffare la versione prima di riavviare l'app?" - Aggiorna e riavvia - Esportazione delle impostazioni non riuscita - Impostazioni esportate con successo - Esporta le impostazioni in un file. - Esporta le impostazioni - Importa - Copia - Esporta o importa le impostazioni come testo. - Esporta o importa come testo. - Importazione delle impostazioni non riuscita - Impostazioni ripristinati al predefinito - Impostazioni importate con successo - Importa le impostazioni dal file esportato. - Importa le impostazioni - Ripristina - Cerca su %s - ReVanced Extended - Downloader esterno - Non installato - "%1$s non è installato. -Si prega di scaricare %2$s dal sito web." - Attenzione - %s non è installato. Per favore installalo. - Il nome del pacchetto dell\'app di download esterna installata, ad esempio YTDLnis. - Nome del pacchetto del downloader esterno delle playlist - Il nome del pacchetto dell\'app di download esterna installata, ad esempio NewPipe o YTDLnis. - Nome del pacchetto del downloader esterno dei video - "Il video passerà automaticamente a schermo intero nelle seguenti situazioni: -• Toccando su un timestamp nei commenti. -• Appena il video inizia." - Forza lo schermo intero - Visualizza la finestra di ottimizzazione per GMSCore a ogni avvio dell\'applicazione. - Mostra la finestra di ottimizzazione per GMSCore - L\'elenco dei nomi dei menù degli account da filtrare, separati da nuove righe. - Modifica il filtro dei menù dell\'account - "Nascondi i componenti dei menù dell'account e della scheda Tu. -Nota: alcuni componenti potrebbero non essere nascosti." - Nascondi i menù dell\'account - Le schede degli album sono visibili. - Le schede degli album sono nascoste. - Nascondi le schede degli album - Luoghi in evidenza, Giochi e sezioni Musica sono mostrati. - Luoghi in evidenza, Giochi e sezioni Musica sono nascosti. - Nascondi sezione Attributi - Il contenitore dell\'anteprima della riproduzione automatica è visibile. - Il contenitore dell\'anteprima della riproduzione automatica è nascosto. - Nascondi il contenitore dell\'anteprima della riproduzione automatica - Il pulsante Esplora Negozio è visibile. - Il pulsante Esplora Negozio è nascosto. - Nascondi il pulsante Esplora Negozio - "Nasconde i seguenti scaffali: -• Ultime notizie -• Continua a guardare -• Guarda di nuovo -• Ascolta di nuovo -• Esplora altri canali -• Shopping" - Nascondi lo scaffale a carosello - È visibile nelle schede. - È nascosta nelle schede. - Nascondi nelle schede - È visibile nei video correlati. - È nascosta nei video correlati. - Nascondi nei video correlati - È visibile nei risultati di ricerca. - È nascosta nei risultati di ricerca. - Nascondi nei risultati di ricerca - Le linee guida dei canali sono visibili. - Le linee guida dei canali sono nascoste. - Nascondi le linee guida dei canali - Lo scaffale degli abbonati è visibile. - Lo scaffale degli abbonati è nascosto. - Nascondi lo scaffale degli abbonati - I link in cima al profilo dei canali sono visibili. - I link in cima al profilo dei canali sono nascosti. - Nascondi i link in cima al profilo dei canali - "Shorts -Playlist -Negozio" - L\'elenco dei nomi delle schede dei canali da filtrare, separati da nuove righe. - Filtro delle schede dei canali - Il filtro delle schede dei canali è disattivato. - Il filtro delle schede dei canali è attivato. - Attiva il filtro delle schede dei canali - Il watermark nei video è visibile. - Il watermark nei video è nascosto. - Nascondi il watermark nei video - Le sezioni dei capitoli sono mostrate. - Le sezioni dei capitoli sono nascoste. - Nascondi le sezioni capitoli - Lo scaffale dei chip è visibile. - Lo scaffale dei chip è nascosto. - Nascondi lo scaffale dei chip - Il pulsante Clip è visibile. - Il pulsante Clip è nascosto. - Nascondi il pulsante Clip - Il pulsante Crea Shorts è visibile. - Il pulsante Crea Shorts è nascosto. - Nascondi il pulsante Crea Shorts - I collegamenti di ricerca evidenziati sono mostrati. - I collegamenti di ricerca evidenziati sono nascosti. - Nascondi collegamenti di ricerca evidenziati - Il pulsante Grazie è visibile. - Il pulsante Grazie è nascosto. - Nascondi il pulsante Grazie - I pulsanti timestamp ed emoji sono visibili. - I pulsanti timestamp ed emoji sono nascosti. - Nascondi i pulsanti timestamp ed emoji - Il banner dei Commenti dei membri è visibile. - Il banner dei Commenti dei membri è nascosto. - Nascondi il banner dei Commenti dei membri - La sezione Commenti è visibile nella scheda Home. - La sezione Commenti è nascosta nella scheda Home. - Nascondi la sezione Commenti nella scheda Home - La sezione Commenti è visibile. - La sezione Commenti è nascosta. - Nascondi la sezione Commenti - Sono visibili nel canale. - Sono nascosti nel canale. - Nascondi nel canale - Sono visibili nella scheda Home e nei video correlati. - Sono nascosti nella scheda Home e nei video correlati. - Nascondi nella scheda Home e nei video correlati - Sono visibili nella scheda Iscrizioni. - Sono nascosti nella scheda Iscrizioni. - Nascondi nella scheda Iscrizioni - Come è stato fatto questo contenuto la sezione è mostrata. - Come questo contenuto è stato fatto sezione è nascosto. - Nascondi sezione Contenuti - Il riquadro della raccolta fondi è visibile. - Il riquadro della raccolta fondi è nascosto. - Nascondi il riquadro della raccolta fondi - La sovrapposizione del doppio tocco è visibile. - La sovrapposizione del doppio tocco è nascosto. - Nascondi la sovrapposizione del doppio tocco - Il pulsante Scarica è visibile. - Il pulsante Scarica è nascosto. - Nascondi il pulsante Scarica - Le schede finali sono visibili. - Le schede finali sono nascoste. - Nascondi le schede finali - Il chip espandibile sotto i video è visibile. - Il chip espandibile sotto i video è nascosto. - Nascondi il chip espandibile sotto i video - Gli scaffali espandibili sono visibili. - Gli scaffali espandibili sono nascosti. - Nascondi gli scaffali espandibili - Il pulsante Sottotitoli è visibile. - Il pulsante Sottotitoli è nascosto. - Nascondi il pulsante Sottotitoli - L\'elenco dei nomi dei menù a comparsa da filtrare, separati da nuove righe. - Filtro dei menù a comparsa - Il filtro dei menù a comparsa è disattivato. - Il filtro dei menù a comparsa è attivato. - Attiva il filtro dei menù a comparsa - La barra di ricerca è visibile. - La barra di ricerca è nascosta. - Nascondi la barra di ricerca - I sondaggi sono visibili. - I sondaggi sono nascosti. - Nascondi i sondaggi - La sovrapposizione della pellicola è visibile. - La sovrapposizione della pellicola è nascosta. - Nascondi la sovrapposizione della pellicola - Il pulsante fluttuante è visibile. - Il pulsante fluttuante è nascosto. - Nascondi il pulsante fluttuante - Il pulsante Microfono fluttuante è visibile. - Il pulsante Microfono fluttuante è nascosto. - Nascondi il pulsante Microfono fluttuante - Lo scaffale Per Te è visibile. - Lo scaffale Per Te è nascosto. - Nascondi lo scaffale Per Te - Gli annunci a schermo intero sono visibili. - Gli annunci a schermo intero sono nascosti. - Nascondi gli annunci a schermo intero - "Gli annunci a schermo intero sono bloccati. - -Limitazione: Le immagini dei post della community a schermo intero potrebbero essere bloccate." - Gli annunci a schermo intero sono chiusi tramite il pulsante Chiudi. - Chiudi gli annunci a schermo intero - Gli annunci generali sono visibili. - Gli annunci generali sono nascosti. - Nascondi gli annunci generali - La promozione di YouTube Premium è visibile. - La promozione di YouTube Premium è nascosta. - Nascondi la promozione di YouTube Premium - I separatori grigi sono visibili. - I separatori grigi sono nascosti. - Nascondi i separatori grigi - L\'handle è visibile. - L\'handle è nascosto. - Nascondi l\'handle - Il pulsante di ricerca immagine è visibile. - Il pulsante di ricerca immagine è nascosto. - Nascondi il pulsante di ricerca immagine - Lo scaffale delle immagini è visibile. - Lo scaffale delle immagini è nascosto. - Nascondi lo scaffale delle immagini - La sezione Schede Informative è visibile - La sezione Schede Informative è nascosta - Nascondi la sezione Schede Informative - Le schede informative sono visibili. - Le schede informative sono nascoste. - Nascondi le schede informative - I pannelli informativi sono visibili. - I pannelli informativi sono nascosti. - Nascondi i pannelli informativi - Il pulsante Abbonati è visibile. - Il pulsante Abbonati è nascosto. - Nascondi il pulsante Abbonati - La sezione dei concetti chiave è mostrata. - La sezione dei concetti chiave è nascosta. - Nascondi sezione concetti chiave - "Le schede Home e Iscrizioni e i risultati di ricerca sono filtrati per nascondere i video che soddisfano le parole chiave o frasi. - -Note: -• Gli Shorts non possono essere nascosti in base al nome del canale. -• Alcuni componenti della interfaccia potrebbero non essere nascosti. -• La ricerca di una parola chiave potrebbe non mostrare alcun risultato." - Informazioni sul filtro in base a parole chiave - Racchiudendo una parola chiave o frase tra virgolette doppie si impediranno corrispondenze parziali dei titoli dei video e dei nomi dei canali.<br><br>Ad esempio:<br><b>\"ia\"</b> nasconderà il video <b>\"Come funziona la IA?\"</b><br>Ma non nasconderà il video <b>\"Cosa significa imparzialità?\"</b> - Combacia parole intere - I commenti non sono filtrati. - I commenti sono filtrati. - Nascondi i commenti in base a parole chiave - I video della scheda Home non sono filtrati. - I video della scheda Home sono filtrati. - Nascondi i video della scheda Home in base a parole chiave - "L'elenco delle parole chiave e frasi da nascondere, separate da nuove righe. - -Le parole chiave possono essere nomi di canali o qualsiasi testo mostrato nei titoli dei video. - -Le parole con lettere maiuscole all'interno devono essere inserite con il maiuscolo (ad esempio: iPhone, TikTok, LeBlanc)." - Parole chiave da nascondere - I risultati di ricerca non sono filtrati. - I risultati di ricerca sono filtrati. - Nascondi i risultati di ricerca in base a parole chiave - I video della scheda Iscrizioni non sono filtrati. - I video della scheda Iscrizioni sono filtrati. - Nascondi i video della scheda Iscrizioni in base a parole chiave - La parola chiave nasconderà tutti i video: %s - Non è possibile usare la parola chiave: %s - Aggiungi le virgolette per usare la parola chiave: %s - La parola chiave ha dichiarazioni in conflitto: %s - La parola chiave è troppo corta e richiede le virgolette: %s - I post più recenti sono visibili. - I post più recenti sono nascosti. - Nascondi i post più recenti - Il pulsante Video Più Recenti è visibile. - Il pulsante Video Più Recenti è nascosto. - Nascondi il pulsante Video Più Recenti - I pulsanti Mi Piace e Non Mi Piace sono visibili. - I pulsanti Mi Piace e Non Mi Piace sono nascosti. - Nascondi i pulsanti Mi Piace e Non Mi Piace - I messaggi della chat dal vivo sono visibili.\n\nQuesta impostazione si applica anche agli Shorts dal vivo. - I messaggi della chat dal vivo sono nascosti.\n\nQuesta impostazione si applica anche agli Shorts dal vivo. - Nascondi i messaggi della chat dal vivo - Il pulsante Replay della Chat dal vivo è visibile.\n\nApparirà a schermo intero quando si chiude la chat dal vivo. - Il pulsante Replay della Chat dal vivo è nascosto.\n\nApparirà a schermo intero quando si chiude la chat dal vivo. - Nascondi il pulsante replay della chat dal vivo - Nascondi i video con meno di 1.000 visualizzazioni dalla scheda Home che sono stati caricati dai canali a cui non sei iscritto. - Nascondi i video con poche visualizzazioni - I pannelli medici sono visibili. - I pannelli medici sono nascosti. - Nascondi i pannelli medici - Le sezioni del merchandising sono visibili. - Le sezioni del merchandising sono nascoste. - Nascondi le sezioni del merchandising - Le playlist miste sono visibili. - Le playlist miste sono nascoste. - Nascondi le playlist miste - La sezione dei film è visibile. - La sezione dei film è nascosta. - Nascondi la sezione dei film - La barra di navigazione è visibile. - La barra di navigazione è nascosta. - Nascondi la barra di navigazione - Il pulsante Crea è visibile. - Il pulsante Crea è nascosto. - Nascondi il pulsante Crea - Il pulsante Home è visibile. - Il pulsante Home è nascosto. - Nascondi il pulsante Home - Le etichette sono visibili. - Le etichette sono nascoste. - Nascondi le etichette - Il pulsante Tu (Raccolta) è visibile. - Il pulsante Tu (Raccolta) è nascosto. - Nascondi il pulsante Tu (Raccolta) - Il pulsante Notifiche è visibile. - Il pulsante Notifiche è nascosto. - Nascondi il pulsante Notifiche - Il pulsante Shorts è visibile. - Il pulsante Shorts è nascosto. - Nascondi il pulsante Shorts - Il pulsante Iscrizioni è visibile. - Il pulsante Iscrizioni è nascosto. - Nascondi il pulsante Iscrizioni - Il pulsante Avvisami è visibile. - Il pulsante Avvisami è nascosto. - Nascondi il pulsante Avvisami - L\'etichetta della promozione a pagamento è visibile. - L\'etichetta della promozione a pagamento è nascosta. - Nascondi l\'etichetta della promozione a pagamento - Lo scaffale Giochi Interattivi è visibile. - Lo scaffale Giochi Interattivi è nascosto. - Nascondi lo scaffale Giochi Interattivi - Il pulsante Riproduzione Automatica è visibile. - Il pulsante Riproduzione Automatica è nascosto. - Nascondi il pulsante Riproduzione Automatica - Il pulsante Sottotitoli è visibile. - Il pulsante Sottotitoli è nascosto. - Nascondi il pulsante Sottotitoli - Il pulsante Trasmetti è visibile. - Il pulsante Trasmetti è nascosto. - Nascondi il pulsante Trasmetti - Il pulsante Comprimi è visibile. - Il pulsante Comprimi è nascosto. - Nascondi il pulsante Comprimi - Il menù Modalità Ambient è visibile. - Il menù Modalità Ambient è nascosto. - Nascondi il menù Modalità Ambient - Il menù Traccia Audio è visibile. - Il menù Traccia Audio è nascosto. - Nascondi il menù Traccia Audio - La parte inferiore del menù Sottotitoli è visibile. - La parte inferiore del menù Sottotitoli è nascosta. - Nascondi la parte inferiore del menù Sottotitoli - Il menù Sottotitoli è visibile. - Il menù Sottotitoli è nascosto. - Nascondi il menù Sottotitoli - Il menù 1080p Premium è visibile. - Il menù 1080p Premium è nascosto. - Nascondi il menù 1080p Premium - Il menù Guida e Feedback è visibile. - Il menù Guida e Feedback è nascosto. - Nascondi il menù Guida e Feedback - Il menù Ascolta con YouTube Music è visibile. - Il menù Ascolta con YouTube Music è nascosto. - Nascondi il menù Ascolta con YouTube Music - Il menù Blocca Schermo è visibile. - Il menù Blocca Schermo è nascosto. - Nascondi il menù Blocca Schermo - Il menù Loop del Video è visibile. - Il menù Loop del Video è nascosto. - Nascondi il menù Loop del Video - Il menù Altro da ... è visibile. - Il menù Altro da ... è nascosto. - Nascondi il menù Altro da ... - Il menù Picture-in-Picture è visibile. - Il menù Picture-in-Picture è nascosto. - Nascondi il menù Picture-in-Picture - Il menù Velocità di Riproduzione è visibile. - Il menù Velocità di Riproduzione è nascosto. - Nascondi il menù Velocità di Riproduzione - Il menù Controlli per l\'Ascolto è visibile. - Il menù Controlli per l\'Ascolto è nascosto. - Nascondi il menù Controlli per l\'Ascolto - La parte inferiore del menù Qualità è visibile. - La parte inferiore del menù Qualità è nascosta. - Nascondi la parte inferiore del menù Qualità - La parte superiore del menù Qualità è visibile. - La parte superiore del menù Qualità è nascosta. - Nascondi la parte superiore del menù Qualità - Il menù Segnala è visibile. - Il menù Segnala è nascosto. - Nascondi il menù Segnala - Il menù Timer di Sospensione è visibile. - Il menù Timer di Sospensione è nascosto. - Nascondi il menù Timer di Sospensione - Il menù Volume Stabile è visibile. - Il menù Volume Stabile è nascosto. - Nascondi il menù Volume Stabile - Il menù Statistiche per Nerd è visibile. - Il menù Statistiche per Nerd è nascosto. - Nascondi il menù Statistiche per Nerd - Il menù Guarda in VR è visibile. - Il menù Guarda in VR è nascosto. - Nascondi il menù Guarda in VR - Il pulsante Schermo Intero è visibile. - Il pulsante Schermo Intero è nascosto. - Nascondi il pulsante Schermo Intero - I pulsanti Precedente e Successivo sono visibili. - I pulsanti Precedente e Successivo sono nascosti. - Nascondi i pulsanti Precedente e Successivo - Lo scaffale dei prodotti è visibile. - Lo scaffale dei prodotti è nascosto. - Nascondi lo scaffale dei prodotti - Il pulsante YouTube Music è visibile. - Il pulsante YouTube Music è nascosto. - Nascondi il pulsante YouTube Music - Il pulsante Salva è visibile. - Il pulsante Salva è nascosto. - Nascondi il pulsante Salva - La sezione Podcast è visibile - La sezione Podcast è nascosta - Nascondi la sezione Podcast - Il commento di anteprima è visibile. - Il commento di anteprima è nascosto. - Nascondi il commento di anteprima - Questo cambia la dimensione della sezione Commenti, rendendo impossibile aprire il replay del live chat nella sezione commenti. - Questo non cambia la dimensione della sezione Commenti, rendendo possibile aprire il replay del live chat nella sezione commenti. - Nascondi il tipo di commento di anteprima - Il banner di avviso promozionale è visibile. - Il banner di avviso promozionale è nascosto. - Nascondi il banner di avviso promozionale - Il pulsante Commenti è visibile. - Il pulsante Commenti è nascosto. - Nascondi il pulsante Commenti - Il pulsante Non Mi Piace è visibile. - Il pulsante Non Mi Piace è nascosto. - Nascondi il pulsante Non Mi Piace - Il pulsante Mi Piace è visibile. - Il pulsante Mi Piace è nascosto. - Nascondi il pulsante Mi Piace - Il pulsante Chat dal Vivo è visibile. - Il pulsante Chat dal Vivo è nascosto. - Nascondi il pulsante Chat dal Vivo - Il pulsante Altro è visibile. - Il pulsante Altro è nascosto. - Nascondi il pulsante Altro - Il pulsante Apri Mix Playlist è visibile. - Il pulsante Apri Mix Playlist è nascosto. - Nascondi il pulsante Apri Mix Playlist - Il pulsante Apri Playlist è visibile. - Il pulsante Apri Playlist è nascosto. - Nascondi il pulsante Apri Playlist - Il pulsante Salva è visibile. - Il pulsante Salva è nascosto. - Nascondi il pulsante Salva - Il pulsante Condividi è visibile. - Il pulsante Condividi è nascosto. - Nascondi il pulsante Condividi - Il contenitore delle azioni rapide è visibile. - Il contenitore delle azioni rapide è nascosto. - Nascondi il contenitore delle azioni rapide - "Nasconde i seguenti video consigliati con: -• Tag \"Riservato agli abbonati\". -• Frasi come \"Le persone hanno guardato anche questo video\" sotto." - Nascondi i video consigliati - La sezione \'Più video\' nel contenitore delle azioni rapide e la sovrapposizione video correlato sono mostrate. - La sezione \'Più video\' nel contenitore delle azioni rapide e la sovrapposizione video correlato sono nascoste. - Nascondi sovrapposizione video correlato - I video correlati sono visibili. - I video correlati sono nascosti. - Nascondi i video correlati - "Questa impostazione limita il numero massimo di interfacce che possono essere caricate sulla schermata del riproduttore. - -Se l'interfaccia della schermata del riproduttore cambia a causa di modifiche lato server, potrebbero esserci delle interfacce indesiderate nascosti nella schermata del riproduttore." - Il pulsante Remix è visibile. - Il pulsante Remix è nascosto. - Nascondi il pulsante Remix - Il pulsante Segnala è visibile. - Il pulsante Segnala è nascosto. - Nascondi il pulsante Segnala - Il pulsante Ricompense è visibile. - Il pulsante Ricompense è nascosto. - Nascondi pulsante Ricompense - I thumbnails nella cronologia delle ricerche sono visibili. - I thumbnails nella cronologia delle ricerche sono nascosti. - Nascondi i thumbnails nella cronologia delle ricerche - Il messaggio di scorrimento è visibile. - Il messaggio di scorrimento è nascosto. - Nascondi il messaggio di scorrimento - Il messaggio di annullamento è visibile. - Il messaggio di annullamento è nascosto. - Nascondi il messaggio di annullamento - Le etichette del capitolo accanto al timestamp sono nascoste. - Le etichette del capitolo accanto al timestamp sono nascoste. - Nascondi le etichette dei capitoli della barra di avanzamento - La barra di avanzamento è visibile. - La barra di avanzamento è nascosta. - La barra di avanzamento nei thumbnails dei video è visibile. - La barra di avanzamento nei thumbnails dei video è nascosta. - Nascondi la barra di avanzamento nei thumbnails dei video - Nascondi la barra di avanzamento - Le schede autopromozionali sono visibili. - Le schede autopromozionali sono nascoste. - Nascondi le schede autopromozionali - Il menù Informazioni è visibile. - Il menù Informazioni è nascosto. - Nascondi il menù Informazioni - Il menù Accessibilità è visibile. - Il menù Accessibilità è nascosto. - Nascondi il menù Accessibilità - Il menù Account è visibile. - Il menù Account è nascosto. - Nascondi il menù Account - Il menù Riproduzione Automatica è visibile. - Il menù Riproduzione Automatica è nascosto. - Nascondi il menù Riproduzione Automatica - Il menù Fatturazione e Pagamenti è visibile. - Il menù Fatturazione e Pagamenti è nascosto. - Nascondi il menù Fatturazione e Pagamenti - Il menù Sottotitoli è visibile. - Il menù Sottotitoli è nascosto. - Nascondi il menù Sottotitoli - Il menù App Collegate è visibile. - Il menù App Collegate è nascosto. - Nascondi il menù App Collegate - Il menù Risparmio Dati è visibile. - Il menù Risparmio Dati è nascosto. - Nascondi il menù Risparmio Dati - Il menù Generali è visibile. - Il menù Generali è nascosto. - Nascondi il menù Generali - Il menù Gestisci Tutta la Cronologia è visibile. - Il menù Gestisci Tutta la Cronologia è nascosto. - Nascondi il menù Gestisci Tutta la Cronologia - Il menù Chat Live è visibile. - Il menù Chat Live è nascosto. - Nascondi il menù Chat Live - Il menù Notifiche è visibile. - Il menù Notifiche è nascosto. - Nascondi il menù Notifiche - Il menù Sfondo è visibile. - Il menù Sfondo è nascosto. - Nascondi il menù Sfondo - Il menù Guarda sulla TV è visibile. - Il menù Guarda sulla TV è nascosto. - Nascondi il menù Guarda sulla TV - Il menù Centro Famiglia è visibile. - Il menù Centro Famiglia è nascosto. - Nascondi il menù Centro Famiglia - Il menù Prova le Nuove Funzionalità Sperimentali è visibile. - Il menù Prova le Nuove Funzionalità Sperimentali è nascosto. - Nascondi il menù Prova le Nuove Funzionalità Sperimentali - Il menù Privacy è visibile. - Il menù Privacy è nascosto. - Nascondi il menù Privacy - Il menù Acquisti e Abbonamenti è visibile. - Il menù Acquisti e Abbonamenti è nascosto. - Nascondi il menù Acquisti e Abbonamenti - Nascondi elementi nel menu delle impostazioni di YouTube. - Nascondi menu impostazioni di YouTube - Il menù Preferenze Relative alla Qualità Video è visibile. - Il menù Preferenze Relative alla Qualità Video è nascosto. - Nascondi il menù Preferenze Relative alla Qualità Video - Il menù I Tuoi Dati su YouTube è visibile. - Il menù I Tuoi Dati su YouTube è nascosto. - Nascondi il menù I Tuoi Dati su YouTube - Il pulsante Condividi è visibile. - Il pulsante Condividi è nascosto. - Nascondi il pulsante Condividi - Il pulsante Negozio è visibile. - Il pulsante Negozio è nascosto. - Nascondi il pulsante Negozio - La sezione Prodotti è visibile. - La sezione Prodotti è nascosta. - Nascondi la sezione Prodotti - La barra del canale è visibile. - La barra del canale è nascosta. - Nascondi la barra del canale - Il pulsante Commenti è visibile. - Il pulsante Commenti è nascosto. - Nascondi il pulsante Commenti - Il pulsante dei commenti disabilitato o con l\'etichetta \"0\" sono mostrati. - Il pulsante dei commenti disabilitato o con l\'etichetta \"0\" sono nascosti. - Nascondi il pulsante dei commenti disabilitati - Il pulsante Non Mi Piace è visibile. - Il pulsante Non Mi Piace è nascosto. - Nascondi il pulsante Non Mi Piace - "I pulsanti fluttuanti come Usa Questa Traccia Audio sono visibili." - "I pulsanti fluttuanti come Usa Questa Traccia Audio sono nascosti." - Nascondi i pulsanti fluttuanti - L\'etichetta del link del video è visibile. - L\'etichetta del link del video è nascosta. - Nascondi l\'etichetta del link del video - Il pulsante Green Screen è visibile. - Il pulsante Green Screen è nascosto. - Nascondi il pulsante Green Screen - I pannelli informativi sono visibili. - I pannelli informativi sono nascosti. - Nascondi i pannelli informativi - Il pulsante Abbonati è visibile. - Il pulsante Abbonati è nascosto. - Nascondi il pulsante Abbonati - Il pulsante Mi Piace è visibile. - Il pulsante Mi Piace è nascosto. - Nascondi il pulsante Mi Piace - L\'intestazione della chat dal vivo è visibile.\n\nNota: il pulsante Indietro dell\'intestazione non verrà nascosto. - L\'intestazione della chat dal vivo è nascosta.\n\nNota: il pulsante Indietro dell\'intestazione non verrà nascosto. - Nascondi l\'intestazione della chat dal vivo - Il pulsante Posizione è visibile. - Il pulsante Posizione è nascosto. - Nascondi il pulsante Posizione - La barra di navigazione è visibile. - La barra di navigazione è nascosta. - Nascondi la barra di navigazione - L\'etichetta della promozione a pagamento è visibile. - L\'etichetta della promozione a pagamento è nascosta. - Nascondi l\'etichetta della promozione a pagamento - L\'intestazione in pausa è visibile. - L\'intestazione in pausa è nascosta. - Nascondi l\'intestazione in pausa - I pulsanti di sovrapposizione in pausa sono visibili. - I pulsanti di sovrapposizione in pausa sono nascosti. - Nascondi i pulsanti di sovrapposizione in pausa - Lo sfondo dei pulsanti Riproduci e Pausa è visibile. - Lo sfondo dei pulsanti Riproduci e Pausa è nascosto. - Nascondi lo sfondo dei pulsanti Riproduci e Pausa - Il pulsante Remix è visibile. - Il pulsante Remix è nascosto. - Nascondi il pulsante Remix - Il pulsante Salva Musica è visibile. - Il pulsante Salva Musica è nascosto. - Nascondi il pulsante Salva Musica - Il pulsante Suggerimenti di Ricerca è visibile. - Il pulsante Suggerimenti di Ricerca è nascosto. - Nascondi il pulsante Suggerimenti di Ricerca - Il pulsante Condividi è visibile. - Il pulsante Condividi è nascosto. - Nascondi il pulsante Condividi - Sono visibili nel canale. - "Sono nascosti nel canale. - -Nota: solo gli scaffali con l'intestazione Shorts nella scheda Home sono nascosti." - Nascondi nel canale - Sono visibili nella cronologia delle visualizzazioni. - Sono nascosti nella cronologia delle visualizzazioni. - Nascondi nella cronologia delle visualizzazioni - Sono visibili nella scheda Home e nei video correlati. - Sono nascosti nella scheda Home e nei video correlati. - Nascondi nella scheda Home e nei video correlati - Sono visibili nei risultati di ricerca. - Sono nascosti nei risultati di ricerca. - Nascondi nei risultati di ricerca - Sono visibili nella scheda Iscrizioni. - Sono nascosti nella scheda Iscrizioni. - Nascondi nella scheda Iscrizioni - "Nota: l'intestazione ufficiale nei risultati di ricerca sarà nascosta." - Nascondi gli scaffali degli Shorts - Il pulsante Negozio è visibile. - Il pulsante Negozio è nascosto. - Nascondi il pulsante Negozio - Il pulsante Prodotti è visibile. - Il pulsante Prodotti è nascosto. - Nascondi il pulsante Prodotti - Il pulsante Suono è visibile. - Il pulsante Suono è nascosto. - Nascondi il pulsante Suono - L\'etichetta dei metadati dell\'audio è visibile. - L\'etichetta dei metadati dell\'audio è nascosta. - Nascondi l\'etichetta dei metadati dell\'audio - Gli adesivi sono visibili. - Gli adesivi sono nascosti. - Nascondi gli adesivi - Il pulsante Iscriviti è visibile. - Il pulsante Iscriviti è nascosto. - Nascondi il pulsante Iscriviti - Il pulsante Super Grazie è visibile. - Il pulsante Super Grazie è nascosto. - Nascondi il pulsante Super Grazie - I prodotti taggati sono visibili. - I prodotti taggati sono nascosti. - Nascondi i prodotti taggati - La barra degli strumenti è visibile. - La barra degli strumenti è nascosta. - Nascondi la barra degli strumenti - Il pulsante Tendenze è visibile. - Il pulsante Tendenze è nascosto. - Nascondi il pulsante Tendenze - Il pulsante Usa Template è visibile. - Il pulsante Usa Template è nascosto. - Nascondi il pulsante Usa Template - Il pulsante Usa Questa Traccia è visibile. - Il pulsante Usa Questa Traccia è nascosto. - Nascondi il pulsante Usa Questa Traccia - Il titolo è visibile. - Il titolo è nascosto. - Nascondi il titolo - Il pulsante Mostra Altro è visibile. - Il pulsante Mostra Altro è nascosto. - Nascondi il pulsante Mostra Altro - Le notifiche snackbar sono visibili. - Le notifiche snackbar sono nascoste. - Nascondi le notifiche snackbar - Il pulsante Avvia Prova è visibile. - Il pulsante Avvia Prova è nascosto. - Nascondi il pulsante Avvia Prova - Il carosello delle iscrizioni è visibile. - Il carosello delle iscrizioni è nascosto. - Nascondi il carosello delle iscrizioni - Le azioni consigliate sono visibili. - Le azioni consigliate sono nascoste. - Nascondi le azioni consigliate - "This setting has been deprecated. - -Instead, use the 'Settings → Autoplay → Autoplay next video' setting. - -Note: -• If you have any issues with 'Suggested video end screen', try restarting the app." - Il video suggerito della schermata finale è visibile. - "Il video suggerito della schermata finale è nascosto quando la riproduzione automatica è disattivata. - -La riproduzione automatica può essere modificata nelle impostazioni di YouTube: -Impostazioni → Riproduzione automatica → Telefono cellulare/tablet" - Nascondi il video suggerito nella schermata finale - Il pulsante Grazie è visibile. - Il pulsante Grazie è nascosto. - Nascondi pulsante Grazie - Lo scaffale degli eventi è visibile. - Lo scaffale degli eventi è nascosto. - Nascondi lo scaffale degli eventi - Il timestamp è visibile - Il timestamp è nascosto - Nascondi il timestamp - Le reazioni temporizzate sono visibili. - Le reazioni temporizzate sono nascoste. - Nascondi le reazioni temporizzate - Il pulsante Trasmetti è visibile. - Il pulsante Trasmetti è nascosto. - Nascondi il pulsante Trasmetti - Il pulsante Crea è visibile. - Il pulsante Crea è nascosto. - Nascondi il pulsante Crea - Il pulsante Notifiche è visibile. - Il pulsante Notifiche è nascosto. - Nascondi il pulsante Notifiche - La sezione Trascrizione è visibile - La sezione Trascrizione è nascosta - Nascondi la sezione Trascrizione - Gli annunci video sono visibili. - Gli annunci video sono nascosti. - Nascondi gli annunci video - "Le schede Home e Iscrizioni e i risultati di ricerca sono filtrati per nascondere i video con visualizzazioni inferiori o superiori ad un numero specificato. - -Note: -• Gli Shorts non possono essere nascosti. -• I video con 0 visualizzazioni non sono filtrati." - Informazioni sul filtro delle visualizzazioni - I video della scheda Home non sono filtrati. - I video della scheda Home sono filtrati. - Nascondi i video della scheda Home per visualizzazioni - I risultati di ricerca non sono filtrati. - I risultati di ricerca sono filtrati. - Nascondi i risultati di ricerca per visualizzazioni - I video della scheda Iscrizioni feed non sono filtrati. - I video della scheda Iscrizioni feed sono filtrati. - Nascondi i video della scheda Iscrizioni per visualizzazioni - Nascondi i video consigliati con meno di un numero specifico di visualizzazioni.\n\nProblema noto: I video con 0 visualizzazioni non vengono filtrati. - Nascondi i video consigliati per il numero di visualizzazioni - I video con più visualizzazioni di questo numero saranno nascosti. - Visualizzazioni superiori - I video con meno visualizzazioni di questo numero saranno nascosti. - Visualizzazioni inferiori - Mln di -> 1000000\nMld -> 1000000000\nvisualizzazioni -> views - Specifica il tuo modello linguistico per il numero di visualizzazioni mostrate sotto ogni video nell\'interfaccia. Ogni chiave (una lettera o una parola nella tua lingua) -> valore (significato della chiave) deve essere su una nuova riga. Le chiavi vanno prima del \"->\" segno. Se cambi la lingua dell\'app o di sistema, devi reimpostare questa impostazione.\n\nEsempi:\nInglese: 10K views = K -> 1000, views -> visualizzazioni\nSpagnolo: 10 K vistas = K -> 1000, vistas -> views - Visualizza le chiavi - Il banner dei prodotti è visibile. - Il banner dei prodotti è nascosto. - Nascondi il banner dei prodotti - Il pulsante Microfono è visibile. - Il pulsante Microfono è nascosto. - Nascondi il pulsante Microfono - I risultati di ricerca del web sono visibili. - I risultati di ricerca del web sono nascosti. - Nascondi i risultati di ricerca del web - Il Doodle è visibile. - Il Doodle è nascosto. - Nascondi il Doodle - "Gli YouTube Doodle compaiono alcuni giorni all'anno. - -Se nella tua regione è attualmente visibile un Doodle e questa impostazione è attiva, anche la barra dei filtri nei risultati di ricerca verrà nascosta." - La sovrapposizione dello zoom è visibile. - La sovrapposizione dello zoom è nascosta. - Nascondi la sovrapposizione dello zoom - Afn Blu - Afn Rosso - Personalizzato - Inventario - MMT - MMT Blu - MMT Verde - MMT Giallo - Revancify Blu - Revancify Rosso - Revancify Giallo - Vanced Scuro - Vanced Chiaro - Xisr Giallo - YouTube - Mantiene la modalità orizzontale quando lo schermo viene spento e acceso a schermo intero. - Il tempo in millisecondi in cui viene forzata la modalità orizzontale. - Il time-out della modalità orizzontale forzata - Mantieni la modalità orizzontale - Inventario - Il gesto Doppio Tocco è disattivato. - "Il gesto Doppio Tocco è attivato. - -• Tocca due volte per aumentare la dimensione del video minimizzato. -• Tocca di nuovo due volte per tornare alle dimensioni originali." - Attiva il gesto Doppio Tocco - Il gesto Trascina-e-Rilascia è disattivato. - Il gesto Trascina-e-Rilascia è attivato. - Attiva il gesto Trascina-e-Rilascia - I pulsanti Espandi e Chiudi sono visibili. - I pulsanti Espandi e Chiudi sono nascosti.\n(Trascina il riproduttore minimizzato per espandere o chiudere) - Nascondi i pulsanti Espandi e Chiudi - I pulsanti Salta Avanti e Salta Indietro sono visibili. - I pulsanti Salta Avanti e Salta Indietro sono nascosti. - Nascondi i pulsanti Salta Avanti e Salta Indietro - I sottotesti sono visibili. - I sottotesti sono nascosti. - Nascondi i sottotesti - L\'opacità della sovrapposizione del riproduttore minimizzato deve essere tra 0 e 100 - Il valore dell\'opacità è tra 0 e 100, dove 0 è trasparente. - Opacità della sovrapposizione - Originale - Telefono - Tablet - Moderno 1 - Moderno 2 - Moderno 3 - Cambia il tipo di riproduttore minimizzato - Pulsanti in sovrapposizione - "Tocca per attivare le ripetizioni dei video. -Tocca e tieni premuto per attivare la pausa dopo le ripetizione." - Mostra il pulsante Ripeti Sempre - "Tocca per copiare l'URL del video. -Tocca e tieni premuto per copiare l'URL del video con il timestamp." - "Tocca per copiare l'URL del video con il timestamp. -Tocca e tieni premuto per copiare il timestamp del video." - Mostra il pulsante Copia URL Video con Timestamp - Mostra il pulsante Copia URL Video - Tocca per avviare il downloader esterno. - Mostra il pulsante Downloader Esterno - Tocca per silenziare il volume del video corrente. Tocca di nuovo per disattivarlo. - Mostra pulsante volume silenziato - Tocca e tieni premuto per cambiare lo stato del pulsante. - Velocità di riproduzione ripristinata al predefinito - "Tocca per aprire la finestra di dialogo della velocità. -Tocca e tieni premuto per impostare la velocità di riproduzione a 1.0x. -Tocca e tieni premuto di nuovo per ripristinare la velocità a quella predefinita" - Mostra la finestra di dialogo della velocità - "Tocca per generare una playlist di tutti i video del canale dal più vecchio al più recente. -Tocca e tieni premuto per annullare." - Mostra il pulsante playlist ordinato in tempo - \"Tocca per aprire la finestra della whitelist. -Tocca e tieni premuto per aprire la finestra delle impostazioni della whitelist. - Mostra il pulsante whitelist - Se è visibile, il pulsante Download Playlist nativo apre il downloader nativo. - Il pulsante Download Playlist nativo è sempre visibile e nelle playlist pubbliche apre il downloader esterno. - Sovrascrivi il pulsante Download Playlist - Il pulsante Download Video nativo apre il downloader nativo. - Il pulsante Download Video nativo apre il downloader esterno. - Sovrascrivi il pulsante Download Video - L\'app YouTube Music è necessario per sovrascrivere l\'azione del pulsante. Tocca qui per scaricare YouTube Music. - Prerequisito - Il pulsante YouTube Music apre l\'app originale. - Il pulsante YouTube Music apre l\'app RVX Music. - Sovrascrivi il pulsante YouTube Music - Escluso - Incluso - Normale - Pulsanti di azione - Impostazioni aggiuntive - Animazione e feedback - Pulsanti Download - Opzioni sperimentali - Restrizioni regionali delle immagini - Esporta / Importa come file - Esporta / Importa come testo - Filtro per parola chiave - Altri - Pulsanti in sovrapposizione - Informazioni patch - Azioni rapide - Video consigliati - Scaffali degli Shorts - Azioni suggerite - Strumenti utilizzati - Filtro sul numero di visualizzazioni - Personalizza i componenti dei menù dell\'account e della scheda Tu. - Menù dell\'account - Personalizza i pulsanti di azione sotto i video. - Pulsanti di azione - Annunci - Thumbnails alternativi - Disattiva la modalità Ambient o bypassa le sue restrizioni. - Modalità Ambient - Nascondi o mostra la barra dei filtri nelle schede, nei risultati di ricerca e nei video correlati. - Barra dei filtri - Personalizza i componenti della barra del canale sotto i video. - Barra del canale - Personalizza i componenti del profilo del canale. - Profilo del canale - Personalizza i componenti della sezione Commenti. - Commenti - Nascondi o mostra i post della community nelle schede e nel canale. - Post della community - Nascondi i componenti usando il filtro personalizzato. - Filtro personalizzato - Personalizza i componenti dei menù a comparsa. - Menù a comparsa - Schede - Personalizza i componenti dello schermo intero. - Schermo intero - Generale - Disattiva o attiva le vibrazioni tattili. - Vibrazioni tattili - Sovrascrive l\'azione dei pulsanti. - Pulsanti Download - Esporta o importa le impostazioni. - Esporta / Importa impostazioni - Cambia lo stile del riproduttore minimizzato. - Riproduttore minimizzato - Varie - Personalizza i componenti della barra di navigazione. - Barra di navigazione - Informazioni sulle patch applicate. - Informazioni patch - Personalizza i pulsanti del riproduttore. - Pulsanti del riproduttore - Personalizza i componenti dei menù a comparsa nel riproduttore. - Menù a comparsa - Riproduttore - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Personalizza i componenti della barra di avanzamento. - Barra di avanzamento - Personalizza i componenti del menù delle impostazioni di YouTube. - Menù delle impostazioni - Personalizza i componenti del riproduttore degli Shorts. - Riproduttore degli Shorts - Shorts - Camuffa i dati in streaming per prevenire problemi di riproduzione. - Camuffa i dati in streaming - Gesti di trascinamento - Personalizza i componenti della barra degli strumenti, come la barra di ricerca, i pulsanti e l\'intestazione. - Barra degli strumenti - Personalizza i componenti della descrizione dei video. - Descrizione dei video - Nascondi i video in base a parole chiave o visualizzazioni. - Filtro dei video - Video - Cambia le impostazioni della cronologia. - Cronologia - Il margine superiore delle azioni rapide deve essere compreso tra 0-32. Reimposta ai valori predefiniti. - Configura la spaziatura dalla barra di ricerca al contenitore di azione rapida, tra 0-32. - Margine superiore delle azioni rapide - "Rifiuta forzatamente la risposta del codec video AV1. -Verrà applicato un codec video diverso dopo circa 20 secondi di buffering." - Rifiuta la risposta del codec video AV1 - Il processo di fallback provoca circa 20 secondi di buffering - Numero massimo di interfacce - I cambiamenti alla velocità di riproduzione si applicano solo al video corrente. - I cambiamenti alla velocità di riproduzione si applicano a tutti i video. - Ricorda i cambiamenti della velocità di riproduzione - Una notifica toast non verrà mostrato quando si cambierà la velocità di riproduzione predefinita. - Una notifica toast verrà mostrato quando si cambierà la velocità di riproduzione predefinita. - Mostra una notifica toast al cambio della velocità di riproduzione - Cambiando la velocità predefinita a %s - I cambiamenti alla qualità video si applicano solo al video corrente. - I cambiamenti alla qualità video si applicano a tutti i video. - Ricorda i cambiamenti di qualità video - Una notifica toast non verrà mostrato quando si cambierà la qualità video predefinita. - Una notifica toast verrà mostrato quando si cambierà la qualità video predefinita. - Mostra una notifica toast al cambio della qualità video - Cambiando la qualità video predefinita con connessione dati a %s - Impostazione della qualità video non riuscita - Cambiando la qualità video predefinita con Wi-Fi a %s - "Rimuove la finestra sulla discrezione dello spettatore. -Nota: questo non bypassa la restrizione di età, ma la accetta automaticamente." - Rimuovi la finestra sulla discrezione dello spettatore - Sostituisce il codec video AV1 con il codec video VP9. - Sostituisci il codec video AV1 - L\'handle è visibile. - Il nome del canale è visibile. - Sostituisci l\'handle con il nome del canale - Tocca per mostrare il tempo restante. - Tocca per aprire la velocità di riproduzione o il menu a comparsa di qualità video. - Sostituire l\'azione timestamp - Sostituisce il pulsante Crea con il pulsante Impostazioni. - Sostituisci il pulsante Crea con il pulsante Impostazioni - "Tocca per aprire le impostazioni di YouTube. -Tocca e tieni premuto per aprire le impostazioni di RVX." - "Tocca per aprire le impostazioni di RVX. -Tocca e tieni premuto per aprire le impostazioni di YouTube." - Cambia il tipo di azione da assegnare al pulsante - Le miniature della barra di avanzamento appariranno a schermo intero. - Le miniature della barra di ricerca appariranno sopra la barra di avanzamento. - Ripristina vecchie miniature della barra di avanzamento - Il vecchio menu di qualità video non è visibile. - Vecchio menu di qualità video è visibile. - Ripristina il vecchio menu di qualità video - \@handle (Nome utente) - Formato di visualizzazione - Nome utente (@handle) - Nome utente - L\'handle è visible. - Il nome utente è visibile. - Attiva Return YouTube Username - "Una chiave sviluppatore di YouTube Data API v3 è necessaria per sostituire gli handle con i nomi utente. - -La quota giornaliera per le chiavi API nel piano gratuito è di 10.000 e 1 quota viene utilizzata per sostituire un handle con un nome utente per 1 commento. - -Tocca per vedere come emettere una chiave API." - Informazioni sulla chiave API di dati di YouTube - La chiave sviluppatore per utilizzare YouTube Data API v3. - Chiave API di dati di YouTube - 1. Vai su <a href=%1$s>Crea un nuovo progetto</a>.<br>. Clicca il pulsante <b>CREA</b>.<br>3. Vai su <a href=%2$s>dati YouTube API v3</a>.<br>4. Fare clic sul pulsante <b>ABILITA</b>.<br>5. Fare clic sul pulsante <b>CREA CREDENZIALI</b>.<br>6. Selezionare l\'opzione <b>Dati pubblici</b>.<br>7. Fare clic sul pulsante <b>AVANTI</b><br>8. Copia la chiave API.<br><br>※ La chiave API non dovrebbe mai essere condivisa con gli altri, quindi non è inclusa nelle impostazioni di Esportazione / Importazione. - Inserimento della chiave sviluppatore YouTube Data API v3 - Informazioni - I dati relativi ai non mi piace sono forniti dall\'API di Return YouTube Dislike. Tocca qui per saperne di più. - ReturnYouTubeDislike.com - Riadattato per la migliore visualizzazione. - Riadattato per una dimensione minima. - Attiva il pulsante Mi Piace compatto - Numero. - Percentuale. - Cambia il tipo di non mi piace - I non mi piace sono nascosti. - I non mi piace sono visibili. - Attiva Return YouTube Dislike - I mi piace stimati sono nascosti. - I mi piace stimati sono visibili. - Mostra i mi piace stimati - Non mi piace non disponibili (È stato raggiunto il limite del client API) - Non mi piace non disponibili (Stato: %d) - Non mi piace temporaneamente non disponibili (API scaduto) - Non mi piace non disponibili (%s) - Ricarica il video per votare usando Return YouTube Dislike - I non mi piace degli Shorts sono nascosti. - I non mi piace degli Shorts sono visibili. - "I Non mi piace degli Shorts sono visibili. - -Nota: i non mi piace potrebbero non apparire se l'utente non ha effettuato l'accesso o in navigazione in incognito." - Mostra i non mi piace degli Shorts - La notifica toast è nascosta se l\'API di ReturnYouTubeDislike non è disponibile. - La notifica toast è visibile se l\'API di ReturnYouTubeDislike non è disponibile. - Mostra una notifica toast se l\'API non è disponibile - Nascosti - Rimuove i parametri di tracciamento dagli URL durante la condivisione dei link. - Sanitizza i link di condivisione - "Frasi come '#', 'Raccolta fondi', 'Negozio' e 'Prodotti' sono state mostrate nei sottotitoli video." - "Frasi come '#', 'Raccolta fondi', 'Negozio' e 'Prodotti' sono state nascoste nei sottotitoli video." - Sanitizza sottotitoli video - Informazioni - sponsor.ajay.app - I dati sono forniti dall\'API di SponsorBlock. Tocca qui per saperne di più e vedere i download per altre piattaforme. - URL API cambiato - URL API non valido - URL API ripristinato - Aspetto - Colore cambiato - Colore: - Codice colore non valido - Colore ripristinato - Creazione di nuovi segmenti - Cambia il comportamento dei segmenti - Nascondi automaticamente il pulsante Salta - Il pulsante Salta è visibile per l\'intero segmento. - Il pulsante Salta è nascosto dopo alcuni secondi. - Attiva il pulsante Salta compatto - Riadattato per la migliore visualizzazione. - Riadattato per una larghezza minima. - Mostra il pulsante Crea Nuovo Segmento - Il pulsante Crea Nuovo Segmento è nascosto. - Il pulsante Crea Nuovo Segmento è visibile. - Attiva SponsorBlock - SponsorBlock è un sistema di crowdsourcing per saltare parti fastidiose dei video di YouTube. - Mostra il pulsante Voto - Il pulsante Voto è nascosto. - Il pulsante Voto è visibile. - Generale - Regola il nuovo passo del segmento - Il valore deve essere un numero positivo - Il numero di millisecondi di cui si spostano i pulsanti del tempo quando si creano nuovi segmenti. - Modifica l\'URL API - L\'indirizzo SponsorBlock usato per contattare il server. - Durata minima del segmento - Durata non valida - I segmenti più brevi di questo valore (in secondi) non verranno mostrati o saltati. - Attiva il monitoraggio del conteggio dei salti - Il monitoraggio del contatore dei salti è disattivato. - Permette alla classifica di SponsorBlock di sapere quanto tempo è stato risparmiato. Viene inviato un messaggio alla classifica ogni volta che un segmento viene saltato. - Mostra una notifica toast quando un segmento è stato saltato automaticamente - La notifica toast è nascosta. - La notifica toast è visibile quando un segmento è saltato automaticamente. Tocca qui per vedere un esempio. - Mostra la durata dei video senza segmenti - La durata completa del video è mostrata. - La durata del video senza la durata complessiva dei segmenti è mostrata tra parentesi accanto alla durata completa del video. - Il tuo ID utente privato - L\'ID utente privato deve essere lungo almeno 30 caratteri - Questo dovrebbe essere tenuto privato perchè come una password e non dovrebbe essere condiviso con nessuno. Se qualcuno dovesse ottenerlo, potrebbe impersonarti. - Già lette - Leggi le linee guida di SponsorBlock prima di creare nuovi segmenti. - Mostramelo - Segui le linee guida - Le linee guida contengono regole e suggerimenti per la creazione dei nuovi segmenti. - Visualizza le linee guida - Regolazione: Segna l\'ora di inizio e di fine del segmento. - Scegli la categoria del segmento - Verifica il Segmento - The segment lasts from %1$02d:%2$02d to %3$02d:%4$02d (%5$d minutes %6$02d seconds)\nIs it ready to submit? - Il segmento è da\n\n%1$s\na\n%2$s\n\n(%3$s)\n\nPronto per l\'invio? - I valori sono corretti? - La categoria è disattivata nelle impostazioni. Attiva la categoria da inviare. - Modifica il Segmento - Vuoi modificare i tempi di inizio o fine del segmento? - I tempi forniti non sono validi - Modifica manualmente i tempi del segmento - Avanzamento in base al Tempo Specificato (Predefinito: 150 ms) - Impostare %s come inizio o fine di un nuovo segmento? - fine - Segna prima due posizioni sulla barra di avanzamento - inizio - adesso - Guarda l\'anteprima del segmento e assicurati che lo salti senza problemi - Pubblica Segmento Creato - Riavvolgimento in base al Tempo Specificato (Predefinito: 150 ms) - L\'inizio deve essere prima della fine - Il segmento finisce a - Il segmento inizia a - Nuovo segmento SponsorBlock - Ripristina - Ripristina colore - Tangente di riempimento e battute - Le scene tangenziali aggiunte solo a scopo riempitivo o per umorismo, non necessarie per comprendere il contenuto principale del video. Non dovrebbe includere sezioni con informazioni. - Momento saliente - La parte del video che la maggior parte delle persone sta guardando. - Promemoria di interazione (Iscrizione) - Una breve promemoria per mettere mi piace, iscriversi o seguirli su altre piattaforme durante la visione. Se è lungo o riguarda qualcosa di specifico, dovrebbe essere considerato autopromozione. - Intermezzo e introduzione animata - Un intervallo senza contenuto effettivo. Potrebbe essere una pausa, un fotogramma statico o un\'animazione ripetuta. Non dovrebbe includere transizioni con informazioni. - Sezione non musicale - Le sezioni di video musicali senza musica che non sono già incluse in un\'altra categoria. Solo per i video musicali. - Schede finali e crediti - I crediti o quando appaiono le schede finali. Non dovrebbe includere le conclusioni con informazioni. - Anteprima, riepilogo e pretesto - Una breve raccolta di anteprime che mostrano ciò che è in programma o ciò che è successo nel video o in altri video di una serie, dove tutte le informazioni sono ripetute altrove. - Promozione non Pagata e autopromozione - Simili agli sponsor, ma sono promozioni non pagate o autopromozioni. Include sezioni sul merchandising, donazioni e informazioni dei collaboratori del video. - Sponsor - Le promozioni a pagamento, referral a pagamento e pubblicità diretta. Non dovrebbe includere autopromozione e ringraziamenti gratis a cause, creatori, siti web e prodotti di loro gradimento. - Copia - Esportazione non riuscita (%s) - Esporta / Importa impostazioni - La tua configurazione di SponsorBlock in JSON può essere esportata e importata su RVX e su altre piattaforme SponsorBlock. - La tua configurazione di SponsorBlock in JSON può essere esportata e importata su RVX e su altre piattaforme SponsorBlock. Questo include il tuo ID utente privato, quindi assicurati di condividerlo con cautela. - Importazione non riuscita (%s) - Impostazioni importate con successo - Le tue impostazioni contengono un ID utente privato di SponsorBlock.\n\nIl tuo ID utente è come una password e non dovrebbe mai essere condiviso.\n - Non mostrare di nuovo - Impostazioni copiate negli appunti - Salta automaticamente - Salta automaticamente una volta - Salta - Momento saliente - Salta riempitivo - Salta al momento saliente - Salta promemoria - Salta introduzione - Salta intermezzo - Salta intermezzo - Salta sezione non musicale - Salta conclusione - Salta anteprima - Salta riepilogo - Salta anteprima - Salta promozione - Salta sponsor - Salta segmento - Disabilita - Mostra nella barra di avanzamento - Mostra un pulsante salta - Riempitivo saltato - Saltato al momento saliente - Promemoria saltato - Introduzione saltata - Intermezzo saltato - Intermezzo saltato - Segmenti multipli saltati - Sezione non musicale saltata - Conclusione saltato - Anteprima saltata - Riepilogo saltato - Anteprima saltata - Autopromozione saltata - Sponsor saltato - Segmento non inviato saltato - SponsorBlock temporaneamente non disponibile - SponsorBlock temporaneamente non disponibile (Stato: %d) - SponsorBlock temporaneamente non disponibile (API scaduta) - Statistiche - Statistiche temporaneamente non disponibili (API è inattiva) - Caricamento... - La tua reputazione è <b>%.2f</b> - Hai salvato le persone da <b>%s</b> segmenti - %1$s ore %2$s minuti - %1$s minuti %2$s secondi - %s secondi - È <b>%s</b> della loro vita.<br>Tocca qui per vedere la classifica. - Tocca qui per vedere le statistiche globali e i migliori contributori. - Classifica di SponsorBlock - SponsorBlock è disattivato. - Hai saltato <b>%s</b> segmenti - Ripristinare il contatore dei segmenti saltati? - È <b>%s</b>. - Hai creato <b>%s</b> segmenti - Tocca qui per vedere i tuoi segmenti. - Il tuo nome utente: <b>%s</b> - Tocca qui per cambiare il tuo nome utente - Modifica del nome utente non riuscita (Stato: %1$d %2$s) - Nome utente cambiato con successo - Invio del segmento non riuscito\nEsiste già - Invio del segmento non riuscito (%s) - Invio del segmento non riuscito (%s) - Invio del segmento non riuscito\nFrequenza limitata (troppi dello stesso utente o IP) - SponsorBlock è temporaneamente inattivo. - Invio del segmento non riuscito (Stato: %1$d %2$s) - Segmento inviato con successo. - La notifica toast non è visibile se SponsorBlock non è disponibile. - La notifica toast è visibile se SponsorBlock non è disponibile. - Mostra una notifica toast se l\'API non è disponibile - Cambia la categoria - Voto negativo - Impossibile votare per il segmento (%s) - Impossibile votare per il segmento (API scaduto) - Impossibile votare per il segmento (Stato: %1$d %2$s) - Non ci sono segmenti per cui votare. - Voto positivo - Impostazioni copiate negli appunti - Timestamp copiato negli appunti (%s) - URL copiato negli appunti - URL con timestamp copiato negli appunti - Originale - Pollice in su - Pollice in su (Cairo) - Cuore - Cuore (Tinta) - Nascosta - Animazione del doppio tocco - Il margine inferiore del pannello Meta deve essere tra 0 e 64 - Configura lo spazio dalla barra di ricerca al pannello Meta tra 0 e 64. - Margine inferiore del pannello Meta - La percentuale di altezza deve essere tra 0 e 100 (%) - Configura la percentuale di altezza dello spazio vuoto rimasto quando la barra di navigazione è nascosta, tra 0 e 100 (%). - Altezza in percentuale dello spazio vuoto - Tocca e tieni premuto il timestamp per cambiare lo stato di ripetizione degli Shorts. - Azione della pressione prolungata della barra di avanzamento - "Mostra la sezione del titolo del video a schermo intero. - -Limitazione: il titolo del video scompare quando si fa clic." - Mostra sezione titolo video - Se la riproduzione automatica è attivata, il video successivo verrà riprodotto al termine del conto alla rovescia. - Se la riproduzione automatica è attivata, il video successivo verrà riprodotto immediatamente. - Salta il conto alla rovescia della riproduzione automatica - "Salta il buffer precaricato all'avvio dei video per bypassare il ritardo della qualità video predefinita forzata. - -Note: -• Quando il video inizia, c'è un ritardo di circa 0.7 secondi, ma la qualità video predefinita viene applicata immediatamente. -• Non si applica ai video HDR, dal vivo e più brevi di 10 secondi." - Salta il buffer precaricato - La notifica toast è nascosta. - La notifica toast è visibile. - Mostra una notifica toast quando un segmento è saltato - Attivare questa impostazione può causare problemi di riproduzione video. - Buffer precaricato saltato - Il valore di sovrapposizione della velocità deve essere tra 0 e 8.0 - Valore di sovrapposizione della velocità tra 0 e 8.0. - Valore di sovrapposizione della velocità - "Simula la versione del client a una versione precedente. - -Note: -• Questo cambierà l'aspetto dell'app, ma potrebbero verificarsi degli effetti collaterali sconosciuti -• Se in seguito verrà disattivato, la vecchia interfaccia potrebbe rimanere fino a quando i dati dell'app non verranno cancellati" - Il camuffamento della versione dell\'app è disattivato. - Il camuffamento della versione dell\'app è attivato. - 17.33.42 - Ripristina la vecchia interfaccia - 17.41.37 - Ripristina il vecchio scaffale delle playlist - 18.05.40 - Ripristina la vecchia casella di inserimento dei commenti - 18.17.43 - Ripristina il vecchio pannello a comparsa del riproduttore - 18.33.40 - Ripristina la vecchia barra di azione degli Shorts - 18.38.45 - Ripristina il comportamento della vecchia qualità video predefinita - 18.48.39 - Disattiva l\'aggiornamento in tempo reale delle visualizzazioni e dei mi piace - 19.13.37 - Ripristina il vecchio effetto contatore dei numeri - Versione dell\'app da camuffare - Digita la versione dell\'app da camuffare. - Modifica la versione dell\'app da camuffare - Attiva il camuffamento della versione dell\'app - "La versione dell'app sarà camuffata ad una versione precedente di YouTube. - -Questo cambierà l'aspetto e le caratteristiche dell'app, ma potrebbero verificarsi effetti collaterali sconosciuti. - -Se in seguito questa impostazione verrà disattivata, si consiglia di cancellare i dati dell'app per evitare bug dell'interfaccia." - "Camuffa le dimensioni del dispositivo portandole al valore massimo. -L'alta qualità potrebbe essere sbloccata per alcuni video che richiedono dimensioni del dispositivo elevate, ma non per tutti i video. -" - Camuffa le dimensioni del dispositivo - Il codec video è AVC (H.264), VP9 o AV1. - Il codec video è AVC (H.264). - Forza iOS AVC (H.264) - "L'attivazione di questa impostazione potrebbe migliorare la durata della batteria e risolvere il problema della riproduzione a scatti. - -Nota: AVC (H.264) ha una risoluzione massima di 1080p e la riproduzione utilizzerà più dati internet rispetto a VP9 o AV1." - "• Il menù Traccia Audio è mancante. -• Il menù Volume Stabile è mancante." - "• Il menù Traccia Audio è mancante. -• Il menù Volume Stabile è mancante." - "• I film o i video a pagamento potrebbero non essere riprodotti. -• I video dal vivo iniziano dall'inizio. -• I video potrebbero terminare 1 secondo prima. -• Nessun codec audio Opus." - Effetti collaterali del camuffamento - • Il video potrebbe non essere riprodotto. - Il client usato per recuperare i dati in streaming è nascosto nelle statistiche per nerd. - Il client usato per recuperare i dati in streaming è visibile nelle statistiche per nerd. - Mostra nelle statistiche per nerd - "I dati in streaming non sono camuffati. La riproduzione potrebbe non funzionare." - I dati in streaming sono camuffati. - Camuffa i dati in streaming - Android - Android TV - Android VR - iOS - Client predefinito - La disattivazione di questa impostazione potrebbe causare problemi di riproduzione. - La sensibilità del gesto della luminosità deve essere tra 1 e 1000 (%) - Configura la distanza minima per scorrere la luminosità tra 1 e 1000 (%).\nPiù breve è la distanza minima, più velocemente cambia la luminosità. - La sensibilità del gesto della luminosità - I gesti di trascinamento in modalità Blocca Schermo sono disattivati. - I gesti di trascinamento in modalità Blocca Schermo sono attivati. - Attiva i gesti di trascinamento in modalità Blocca Schermo - Automatico - Il limite di ampiezza entro cui deve avvenire il trascinamento. - Il limite di ampiezza del trascinamento - La visibilità dello sfondo in sovrapposizione durante il trascinamento. - La visibilità dello sfondo del trascinamento - La dimensione dell\'area trascinabile non può essere superiore a 50 - La percentuale dell\'area trascinabile.\n\nNota: questo modificherà anche la dimensione dell\'area dello schermo del gesto Doppio Tocco. - La dimensione dello schermo in sovrapposizione del trascinamento - La dimensione del testo in sovrapposizione durante il trascinamento. - La dimensione del testo del trascinamento - Il tempo in millisecondi per cui la sovrapposizione del trascinamento è visibile. - La durata della sovrapposizione del trascinamento - La sensibilità del gesto del volume deve essere tra 1 e 1000 (%) - Configura la distanza minima per scorrere il volume tra 1 e 1000 (%).\n\nPiù breve è la distanza minima, più velocemente cambia il volume.\n\nLa sensibilità consigliata è del 100% a incrementi di 15 volumi e del 10% a incrementi di 150 volumi. - La sensibilità del gesto del volume - "Scambia le posizioni del pulsante Crea con il pulsante notifiche camuffando informazioni del dispositivo. - -• Potrebbe essere necessario riavviare il dispositivo per rendere effettiva la modifica di questa impostazione. -• Disabilitare questa impostazione carica più annunci dal lato server. -• Dovresti disabilitare questa impostazione per rendere visibili gli annunci nei video." - Il pulsante Crea non è sostituito dal pulsante Notifiche. - "Il pulsante Crea è sostituito dal pulsante Notifiche. - -Nota: l'attivazione di questa impostazione nasconderà anche gli annunci video." - Sostituisci il pulsante Crea con il pulsante Notifiche - "La disattivazione di questa impostazione potrebbe caricare più annunci dal server. - -Inoltre, gli annunci degli Shorts non saranno più bloccati. - -Se questa impostazione non ha effetto, prova a passare alla navigazione in incognito." - Inventario - RVX Music - %s non è installato. Per favore installalo. - Il nome del pacchetto dell\'app RVX Music installata. - Nome del pacchetto dell\'app RVX Music - La cronologia è bloccata. - "• Segue le impostazioni di cronologia dell'account Google. -• La cronologia potrebbe non funzionare a causa di DNS o VPN." - Segue le impostazioni di cronologia dell\'account Google. - Stato della cronologia - Tocca per aprire la gestione della cronologia di YouTube. - Gestisci tutta la cronologia - Originale - Sostituisci dominio - Blocca cronologia - Cambia il tipo di cronologia - Aggiunta del canale \"%1$s\" alla whitelist %2$s non riuscita - Il canale \"%1$s\" è stato aggiunto alla whitelist %2$s - Non ci sono canali nella whitelist - Non aggiunto alla whitelist - Caricamento delle informazioni del canale non riuscito - Aggiunto alla whitelist - Velocità di riproduzione - Rimuovere il canale \"%1$s\" dalla whitelist %2$s? - Rimozione del canale \"%1$s\" dalla whitelist %2$s non riuscita - Il canale \"%1$s\" è stato rimosso dalla whitelist %2$s - Controlla o rimuovi l\'elenco dei canali aggiunti alla whitelist. - Whitelist dei canali - SponsorBlock - diff --git a/src/main/resources/youtube/translations/ja-rJP/missing_strings.xml b/src/main/resources/youtube/translations/ja-rJP/missing_strings.xml deleted file mode 100644 index c5cd0e333..000000000 --- a/src/main/resources/youtube/translations/ja-rJP/missing_strings.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - AI-generated video summary section is shown. - AI-generated video summary section is hidden. - Hide AI-generated video summary section - MMT Orange - MMT Pink - MMT Turquoise - diff --git a/src/main/resources/youtube/translations/ja-rJP/strings.xml b/src/main/resources/youtube/translations/ja-rJP/strings.xml deleted file mode 100644 index 308ba5edd..000000000 --- a/src/main/resources/youtube/translations/ja-rJP/strings.xml +++ /dev/null @@ -1,1721 +0,0 @@ - - - 動画プレーヤーのアクセシビリティコントロールをオンにしますか? - ユーザー補助サービスがオンになっているため、操作方法が変わります。 - 続行 - 今後表示しない - "MicroG GmsCore はバックグラウンドで実行する権限がありません。 - -あなたの端末の \"Don't kill my app\"のガイドに従って、MicroG のインストールに適用してください。 - -これはアプリが動作するために必要です。" - "問題を防ぐために GmsCore のバッテリーの最適化を無効にする必要があります。 - -「続行」をタップし、バッテリーの最適化を無効にします。" - Web サイトを開く - アクションが必要です - 通知を受け取るには、Cloud Messaging の設定を有効にしてください。 - GmsCore を開く - GmsCore がインストールされていません。インストールしてください。 - "DeArrow は、YouTube 動画のサムネイルをクラウドソーシングで提供する機能です。DeArrow のサムネイルは、YouTube が提供するサムネイルよりも適切なことが多いです。 これを有効にすると、動画のURLが API サーバーに送信されますが、他のデータは送信されません。 動画に DeArrow サムネイルがない場合は、オリジナルまたは静止画サムネイルが表示されます。 - -DeArrow の詳細については、ここをタップしてください。" - DeArrow - 無効な DeArrow API の URL です。 - DeArrow サムネイルキャッシュエンドポイントの URLです。 - DeArrow API エンドポイント - DeArrow が利用できない場合、トーストを表示します。 - DeArrow が利用できない場合、トーストを表示します。 - API が利用できない場合にトーストを表示 - DeArrow は一時的に利用できません。(ステータスコード: %s) - DeArrow は一時的に利用できません。 - [ホーム]タブ - [マイページ]タブ - オリジナルサムネイル - DeArrow & オリジナルサムネイル - DeArrow & 静止画サムネイル - 静止画サムネイル - 再生リスト、おすすめ - 検索結果 - 静止画サムネイル - 静止画サムネイルは、各動画の最初/中間/最後の部分から取得されます。これらの画像は YouTube に直接組み込まれており、外部 API は使用されません。 - 静止画サムネイルについて - 現在の設定: 高画質の静止画サムネイルを使用します。 - 現在の設定: 中画質の静止画サムネイルを使用します。\n\n注意: 読み込みは速くなりますが、ライブ、公開予定、非常に古い動画は、サムネイルが空白になることがあります。 - 高速な静止画サムネイル - 動画の最初 - 動画の中間 - 動画の最後 - 取得する静止画のサムネイルの位置 - [登録チャンネル]タブ - タイムスタンプの横に情報を表示します。 - "タイムスタンプの横に情報を表示します。" - タイムスタンプの横に情報を追加 - 現在の設定: 再生速度を表示します。 - 現在の設定: 画質を表示します。 - 表示する情報の種類 - バッテリーセーバーモードでもアンビエントモードを有効にします。 - バッテリーセーバーモードでもアンビエントモードを有効にします。 - アンビエントモードの制限を回避 - 画像を取得するためのドメインです。\n注意: 「https://」のような接頭辞を付けずにドメイン名のみを入力してください。 - 代替ドメイン - 画像表示の地域制限を回避するために yt4.ggpht.com から画像を取得します。 - 画像表示の地域制限を回避するために yt4.ggpht.com から画像を取得します。 - 画像表示の地域制限を回避 - オリジナル - スマホ - スマホ(最大 480 dp) - タブレット - タブレット(最小 600 dp) - レイアウトを変更 - 現在の設定: スイッチのトグル - 現在の設定: テキストのトグル - トグルの種類を変更 - 「共有」ボタンをタップした際に表示される共有メニューを、システムの共有メニューに置き換えます。 - 「共有」ボタンをタップした際に表示される共有メニューを、システムの共有メニューに置き換えます。 - 共有メニューを変更 - 次の動画を自動再生 - デフォルト - 一時停止 - リピート再生 - リピート状態を変更 - チャンネルを探す - コース / 学び - デフォルト - 探索 - ゲーム - 履歴 - ライブラリ - 高評価した動画 - ライブ - 映画 - 音楽 - 検索 - ショート - スポーツ - 登録チャンネル - トレンド - 後で見る - 起動時のページを変更 - 現在の設定: 起動時のページを一度のみ変更します。 - "現在の設定: 起動時のページを常に変更します。 - -注意: ツールバーの戻るボタンが機能しない場合があります。" - 「起動時のページを変更」の種類 - 左上のヘッダーを「Premium」に変更します。 - 左上のヘッダーを「Premium」に変更します。 - YouTube ヘッダーを変更 - フィルタリングするコンポーネントパスビルダーの文字列を並べたリスト。(改行区切り) - カスタムフィルター - カスタムフィルターを有効化します。 - カスタムフィルターを有効化します。 - カスタムフィルターを有効化 - 無効なカスタムフィルターです: %s。 - 現在の設定: 古いスタイルのフライアウトパネルが使用されます。 - 現在の設定: カスタムダイアログが使用されます。 - カスタム再生速度メニューの種類を選択 - カスタム再生速度は %s 倍速未満である必要があります。デフォルト値にリセットします。 - 無効なカスタム再生速度です。デフォルトの値を使用します。 - 利用可能な再生速度を追加または変更します。 - カスタム再生速度の編集 - プレーヤーのオーバーレイの不透明度は 0 ~ 100 の間でなければなりません。デフォルト値にリセットします。 - 透明度の値は 0 〜 100 の範囲で、 0 が透明です。 - プレーヤーのオーバーレイのカスタム不透明度 - シークバーの色を16進数カラーコードで入力してください。 - シークバーの色 - 外部ブラウザから ReVanced Extended を開くには、「デフォルトで開く」→「対応リンクを開く」→「対応リンクを開くことをアプリに許可する」を選択してください。 - 「デフォルトで開く」の設定を開く - デフォルトの再生速度 - モバイルネットワーク使用時のデフォルト画質 - Wi-Fi 使用時のデフォルト画質 - 全画面表示時のアンビエントモードを無効化します。 - 全画面表示時にアンビエントモードを無効化します。 - 全画面表示時にアンビエントモードを無効化します。 - 全画面表示時のアンビエントモードを無効化 - アンビエントモードを無効化します。 - アンビエントモードを無効化します。 - アンビエントモードを無効化します。 - アンビエントモードを無効化 - 音声トラックが自動で選択されるのを無効化します。 - -注意: この設定はショート動画には適用されません。 - 音声トラックが自動で選択されるのを無効化します。 - -注意: この設定はショートには適用されません。 - 音声トラックの強制を無効化 - 投稿者が設定している字幕の強制を無効にします。 - 投稿者が設定している字幕の強制を無効にします。 - 字幕の強制を無効化 - 再生リストとライブチャットのパネルが自動で開くのを無効化します。 - 再生リストとライブチャットのパネルが自動で開くのを無効化します。 - プレーヤーのポップアップパネルを無効化 - "自動再生がオフの場合、音楽を再生した際にミックスプレイリストへの自動切り替えを無効化できます。\n\n自動再生は YouTube の設定で変更できます: 「設定 → 自動再生 → 次の動画を自動再生」" - 自動再生がオフの場合、音楽を再生した際に自動的にミックスプレイリストに切り替わるのを無効化できます。\n\n自動再生は YouTube の設定で変更できます: 「設定 → 自動再生 → 次の動画を自動再生」 - ミックスプレイリストの切り替えを無効化 - 自動再生がオフの場合、音楽を再生した際にミックスプレイリストへの自動切り替えを無効化できます。\n\n自動再生は YouTube の設定で変更できます: 「設定 → 自動再生 → 次の動画を自動再生」 - ライブ配信ではデフォルトに設定された再生速度を無効化します。 - ライブ配信ではデフォルトに設定された再生速度を無効化します。 - ライブ配信でデフォルトの再生速度を無効化 - 音楽を再生する際に、「デフォルトの再生速度」で設定した再生速度を無効化します。\n\n注意: この設定は、「YouTube Music で聴く」バナーが表示されている動画にのみ適用されます。 - "音楽を再生する際に、「デフォルトの再生速度」で設定した再生速度を無効化します。 - -注意: この設定は、「YouTube Music で聴く」バナーが表示されている動画にのみ適用されます。" - 音楽再生時にデフォルトの再生速度を無効化 - エンゲージメントパネルを無効化します。 - エンゲージメントパネルを無効化します。 - エンゲージメントパネルを無効化 - チャプターの触覚フィードバックを無効化します。 - チャプターの触覚フィードバックを無効化します。 - チャプターの触覚フィードバックを無効化 - フィルムストリップの触覚フィードバックを無効化します。 - フィルムストリップの触覚フィードバックを無効化します。 - フィルムストリップの触覚フィードバックを無効化 - 動画長押し時の触覚フィードバックを無効化します。 - 動画長押し時の触覚フィードバックを無効化します。 - 動画長押し時の触覚フィードバックを無効化 - シーク取り消しの触覚フィードバックを無効化します。 - シーク取り消しの触覚フィードバックを無効化します。 - シーク取り消しの触覚フィードバックを無効化 - 動画ズーム時の触覚フィードバックを無効化します。 - 動画ズーム時の触覚フィードバックを無効化します。 - 動画ズーム時の触覚フィードバックを無効化 - HDR 動画の明るさの自動調節を無効にします。 - HDR 動画の明るさの自動調節を無効にします。 - HDR 動画の明るさ自動調節を無効化 - HDR 動画を無効化します。 - HDR 動画を無効化します。 - HDR 動画を無効化 - 縦向きの全画面表示を有効化します。 - 縦向きの全画面表示を有効化します。 - 横画面モードを無効化 - 「高評価」と「低評価」ボタンを押した時のアニメーションを無効化します。 - 「高評価」と「低評価」ボタンを押した時のアニメーションを無効化します。 - 高評価と低評価ボタンのアニメーションを無効化 - "CronetEngine の QUIC プロトコルを無効化します。これにより動画の読み込み速度が多少改善されます。" - QUIC プロトコルを無効化 - アプリを閉じる前に視聴していたショート動画をアプリ起動時に再開しないようにします。 - アプリを閉じる前に視聴していたショート動画をアプリ起動時に再開しないようにします。 - プレーヤーの再開を無効化 - 高評価数と視聴回数の回転アニメーションを無効にします。 - 高評価数と視聴回数の回転アニメーションを無効にします。 - 数字の回転アニメーションを無効化 - シークバーからチャプターを非表示にします。 - シークバーからチャプターを非表示にします。 - シークバーのチャプターを非表示 - 「高評価」ボタンの上部に表示されるアニメーションを無効化します。 - 「高評価」ボタンの上部に表示されるアニメーションを無効化します。 - 高評価ボタンのアニメーションを無効化 - "画面を長押しして2倍速で再生する機能を無効にします。 - -注意: -・倍速再生オーバーレイを無効にすると、古いレイアウトの「スライドしてシーク」動作が復元されます。 -・この設定は、スピードオーバーレイを強制的に有効にするものではありません。" - 再生速度のオーバーレイを無効化 - YouTube 起動時のスプラッシュアニメーションを無効にします。 - YouTube 起動時のスプラッシュアニメーションを無効にします。 - スプラッシュアニメーションを無効化 - "概要欄が展開されている場合、以下のインタラクションを無効にします: - -• タップしてスクロールする。 -• タップしたままテキストを選択する。" - 概要欄の操作を無効化 - VP9 コーデックを無効化します。\n\n注意: \n -• 最大解像度は 1080p です。\n• 動画を再生する際には VP9 コーデックよりも多くの通信量を消費します。\n • HDR 再生を可能にするために、HDR 動画では引き続き VP9 コーデックが使用されます。 - "VP9 コーデックを無効化します。 - -注意: -• 最大解像度は 1080p です。 -• 動画を再生する際には VP9 コーデックよりも多くの通信量を消費します。 -• HDR 再生を可能にするために、HDR 動画では引き続き VP9 コーデックが使用されます。" - VP9 コーデックを無効化 - Cairo シークバー (シークバーの色が動画に応じて変更される機能) を有効化します。\n\n注意: 通知ドットにも Cairo テーマが適用されるという副作用があります。 - "Cairo シークバー (シークバーの色が動画に応じて変更される機能) を有効化します。 - -注意: 通知ドットにも Cairo テーマが適用されるという副作用があります。" - Cairo シークバーを有効化 - コントロールのオーバーレイを全画面に表示せず、コンパクトに表示します。 - コントロールのオーバーレイを全画面に表示せず、コンパクトに表示します。 - コンパクトなコントロールオーバーレイを有効化 - 再生速度のカスタムを有効化します。 - 再生速度のカスタムを有効化します。 - カスタム再生速度を有効化 - シークバーの色のカスタマイズを有効化します。 - シークバーの色のカスタマイズを有効化します。 - シークバーの色のカスタマイズを有効化 - バッファログをデバッグログに含めます。 - バッファログをデバッグログに含めます。 - デバッグバッファログを有効化 - デバッグログを有効化します。 - デバッグログを有効化します。 - デバッグログを有効化 - デフォルトの再生速度をショートに適用します。 - デフォルトの再生速度をショートに適用します。 - ショートのデフォルト再生速度を有効化 - 外部ブラウザを有効化します。 - 外部ブラウザを有効化します。 - 外部ブラウザを有効化 - アプリ起動時や動画の読み込み画面などでグラデーションを有効化します。 - アプリ起動時や動画の読み込み画面などでグラデーションを有効化します。 - グラデーションの読み込み画面を有効化 - ナビゲーション ボタンの間隔を狭くします。 - ナビゲーション ボタンの間隔は狭くなります。 - 幅の狭いナビゲーションボタンを有効化 - URL リダイレクト (youtube.com/redirect) をバイパスします。 - URL リダイレクト (youtube.com/redirect) をバイパスします。 - 直リンクを有効化 - プレーヤーの応答に OPUS コーデックが含まれている場合、OPUS コーデックを有効化します。 - OPUS コーデックを有効化 - 全画面表示を終了 / 開始した際に明るさを保存/復元します。 - 全画面表示を終了 / 開始した際に明るさを保存/復元します。 - 明るさの保存と復元を有効化 - シークバーのタップを有効化します。 - シークバーのタップを有効化します。 - シークバーのタップを有効化 - "この機能を有効化することにより、シークバーサムネイルがないライブのアーカイブにサムネイルが復元されます。 - -注意: -・通信量が増える可能性があります。 -・通信速度が遅いネットワークに接続している場合、シークバーサムネイルが表示されるまでに時間がかかることがあります。" - シークバーサムネイルを高画質化します。 - シークバーサムネイルを高画質化します。 - 高画質のサムネイルを有効化 - プレーヤーの左下にタイムスタンプを表示します。\n\n注意: \n・プレーヤーの背景をタップすると UI が非表示 (画面クリアモード) になります。\n -・開発段階の機能であるため、レイアウトが崩れる可能性があります。 - "プレーヤーの左下にタイムスタンプを表示します。 - -注意: -・プレーヤーの背景をタップすると UI が非表示 (画面クリアモード) になります。 -・開発段階の機能であるため、レイアウトが崩れる可能性があります。" - タイムスタンプを表示 - 明るさのスワイプコントロールを有効化します。 - 明るさのスワイプコントロールを有効化します。 - 明るさのジェスチャーを有効化 - シークバーをスワイプした際の触覚フィードバックを有効化します。 - シークバーをスワイプした際の触覚フィードバックを有効化します。 - 触覚フィードバックを有効化 - スワイプして明るさを 0 にして、明るさの自動調節を有効化します。 - スワイプして明るさを 0 にして、明るさの自動調節を有効化します。 - スワイプして明るさの自動調節を有効化 - 全画面表示時に、長押し時のみスワイプジェスチャーを有効化します。 - 全画面表示時に、長押し時のみスワイプジェスチャーを有効化します。 - 「長押ししてスワイプ」ジェスチャーを有効化 - 全画面表示時に、上/下にスワイプして次の動画/前の動画に切り替えられるようにします。 - 全画面表示時に、上/下にスワイプして次の動画/前の動画に切り替えられるようにします。 - スワイプして動画の切り替えを有効化 - 音量のスワイプコントロールを有効化します。 - 音量のスワイプコントロールを有効化します。 - 音量ジェスチャーを有効化 - ナビゲーションバー(ホーム、登録チャンネルなどのボタン)を半透明にします。\n\n注意: この機能は Android 12 以降のみ利用可能です。 - ナビゲーションバー(ホーム、登録チャンネルなどのボタン)を半透明にします。\n\n注意: この機能は Android 12 以降のみ利用可能です。 - 半透明のナビゲーションバーを有効化 - プレーヤーの一番下の領域をスワイプして全画面に切り替えれるようにします。 - プレーヤーの一番下の領域をスワイプして全画面に切り替えれるようにします。 - スワイプして全画面に切り替えを有効化 - "この設定を有効にすると、[マイページ] タブ内の設定ボタンが無効になります。 - -この場合、以下の手順に従ってください: -[マイページ] タブ > チャンネルを表示 > メニュー > 設定" - マイページ タブで幅広い検索バーを有効化 - 検索バーの幅を広くします。 - 検索バーの幅を広くします。 - 幅広い検索バーを有効化 - ヘッダー付きの幅広い検索バーを有効化します。 - ヘッダー付きの幅広い検索バーを有効化します。 - ヘッダー付きの幅広い検索バーを有効化 - 概要 - "動画の概要欄パネルのタイトルを入力してください。 -概要欄パネルのタイトルは各言語によって異なるため不正確な文字列を保存した場合、「概要欄の自動展開」が機能しないことがあります。" - 動画の概要欄を自動で開く - 概要欄を自動で展開します。 - 概要欄を自動で展開します。 - 概要欄を自動で展開 - 続行しますか? - デフォルト値にリセットしました。 - 再起動してレイアウトを正常に読み込みます - "YouTube サーバー側のバグにより、高評価数、再生回数、アップロード日などの数字の回転アニメーションが一部のユーザーに対して非表示になります。 - -この問題は、アプリのバージョンを 19.13.37 に偽装することで回避できます。(一時的な回避策) - -アプリを再起動する前にアプリのバージョンを偽装しますか?" - 再起動して設定を適用します - 設定のエクスポートに失敗しました。 - 設定は正常にエクスポートされました。 - 設定をファイルにエクスポートします。 - 設定をエクスポート - インポート - コピー - 設定をテキストとしてインポートまたはエクスポートします。 - テキストとしてインポート/エクスポート - 設定のインポートに失敗しました。 - 設定をデフォルトにリセットしました。 - 設定は正常にインポートされました。 - 設定をファイルからインポートします。 - 設定をインポート - リセット - 設定を検索 - ReVanced Extended - 外部ダウンローダーを選択 - 未インストール - "%1$s はインストールされていません。 -ウェブサイトから %2$s をダウンロードしてください。" - 注意 - %s はインストールされていません。インストールしてください。 - NewPipe や YTDLnis などの、インストールされている外部ダウンローダーアプリのパッケージ名です。 - プレイリストの外部ダウンローダーのパッケージ名 - 長押し時に使用する NewPipe や YTDLnis などの、インストールされている外部ダウンローダーアプリのパッケージ名です。 - 長押し時の外部ダウンローダのパッケージ名 - NewPipe や YTDLnis などの、インストールされている外部ダウンローダーアプリのパッケージ名です。 - 外部ダウンローダーのパッケージ名 - "以下の状況で全画面に切り替わります: - -• コメントのタイムスタンプをタップしたとき -• 動画開始時" - 強制的に全画面表示 - アプリの起動時に、GMSCore の最適化ダイアログを表示します。 - GMSCore の最適化ダイアログを表示 - フィルタリングするアカウントメニュー名のリスト。(改行区切り) - アカウントメニューフィルター - "アカウントメニューとマイページタブの要素を非表示にします。 -一部のコンポーネントは非表示にならない可能性があります。" - アカウントメニューを非表示 - アーティストの概要欄の下部に表示されるアルバムカードを非表示にします。 - アーティストの概要欄の下部に表示されるアルバムカードを非表示にします。 - アルバムカードを非表示 - 注目の場所、ゲーム、音楽セクションを非表示にします。 - 注目の場所、ゲーム、音楽セクションを非表示にします。 - 提案セクションを非表示 - 「次の動画」コンテナーを非表示にします。 - 「次の動画」コンテナーを非表示にします。 - 自動再生プレビューコンテナーを非表示 - 「ストアを見る」ボタンを非表示にします。 - 「ストアを見る」ボタンを非表示にします。 - 「ストアを見る」ボタンを非表示 - "以下の欄を非表示にします: -• ニュース速報 -• 続きを見る -• もう一度見る -• もう一度聴く -• 他のチャンネルを探す -• ショッピング" - おすすめ欄を非表示 - フィードから非表示にします。 - フィードから非表示にします。 - フィードから非表示 - 関連動画から非表示にします。 - 関連動画から非表示にします。 - 関連動画から非表示 - 検索結果から非表示にします。 - 検索結果から非表示にします。 - 検索結果から非表示 - チャンネルのガイドラインを非表示にします。 - チャンネルのガイドラインを非表示にします。 - コミュニティガイドラインを非表示 - チャンネルページからメンバー欄を非表示にします。 - チャンネルページからメンバー欄を非表示にします。 - メンバー欄を非表示 - チャンネルプロフィール上部のリンクを非表示にします。 - チャンネルプロフィール上部のリンクを非表示にします。 - チャンネルプロフィールのリンクを非表示 - "ショート -プレイリスト -ストア" - フィルタリングする「チャンネルタブ名」のリスト(改行区切り) - チャンネルタブのフィルター - チャンネルタブのフィルターを有効化します。 - チャンネルタブのフィルターを有効化します。 - チャンネルタブのフィルターを有効化 - 全画面表示時に、右下に表示されるチャンネルの透かしを非表示にします。 - 全画面表示時に、右下に表示されるチャンネルの透かしを非表示にします。 - チャンネルの透かしを非表示 - 概要欄のチャプターセクションを非表示にします。 - 概要欄のチャプターセクションを非表示にします。 - チャプターセクションを非表示 - フィードに表示される、似ている動画のチップ欄を非表示にします。 - フィードに表示される、似ている動画のチップ欄を非表示にします。 - チップ欄を非表示 - 「クリップ」ボタンを非表示にします。 - 「クリップ」ボタンを非表示にします。 - 「クリップ」ボタンを非表示 - 「ショートの作成」ボタンを非表示にします。 - 「ショートの作成」ボタンを非表示にします。 - ショートの作成ボタンを非表示 - コメント欄のハイライト表示された検索リンク(虫眼鏡マークが付いている水色の文字)を非表示にします。 - コメント欄のハイライト表示された検索リンク(虫眼鏡マークが付いている水色の文字)を非表示にします。 - ハイライト表示された検索リンクを非表示 - 「Thanks」ボタンを非表示にします。 - 「Thanks」ボタンを非表示にします。 - 「Thanks」ボタンを非表示 - 「タイムスタンプと絵文字」ボタンを非表示にします。 - 「タイムスタンプと絵文字」ボタンを非表示にします。 - タイムスタンプと絵文字ボタンを非表示 - 「メンバーからのコメント」バナーを非表示にします。 - 「メンバーからのコメント」バナーを非表示にします。 - 「メンバーからのコメント」バナーを非表示 - ホームフィードのコメント欄を非表示にします。 - ホームフィードのコメント欄を非表示にします。 - ホームフィードのコメント欄を非表示 - コメント欄を非表示にします。 - コメント欄を非表示にします。 - コメント欄を非表示 - チャンネル内から非表示にします。 - チャンネル内から非表示にします。 - チャンネルから非表示 - ホームフィードや関連動画から非表示にします。 - ホームフィードや関連動画から非表示にします。 - ホームフィードや関連動画から非表示 - 登録チャンネルフィードから非表示にします。 - 登録チャンネルフィードから非表示にします。 - 登録チャンネルフィードから非表示 - 概要欄下部に表示される「このコンテンツの作成手段」を非表示にします。 - 概要欄下部に表示される「このコンテンツの作成手段」を非表示にします。 - コンテンツ欄を非表示 - プレーヤーと概要欄の間のクラウドファンディングボックスを非表示にします。 - プレーヤーと概要欄の間のクラウドファンディングボックスを非表示にします。 - クラウドファンディング欄を非表示 - ダブルタップオーバーレイフィルタを非表示にします。 - ダブルタップオーバーレイフィルタを非表示にします。 - ダブルタップオーバーレイフィルタを非表示 - 「オフライン」ボタンを非表示にします。 - 「オフライン」ボタンを非表示にします。 - 「オフライン」ボタンを非表示 - 動画の最後に表示される作成者が追加したエンドカードを非表示にします。 - 動画の最後に表示される作成者が追加したエンドカードを非表示にします。 - エンドカードを非表示 - 検索結果の動画の下に表示される、展開可能なチップを非表示にします。 - 検索結果の動画の下に表示される、展開可能なチップを非表示にします。 - 展開可能なチップを非表示 - 検索結果に表示される、展開可能な棚を非表示にします。 - 検索結果に表示される、展開可能な棚を非表示にします。 - 展開可能な棚を非表示 - フィードから字幕ボタンを非表示にします。 - フィードから字幕ボタンを非表示にします。 - 字幕ボタンを非表示 - フィルタリングするフライアウトメニューの項目名をリストしてください 。(改行区切り) - フィードのフライアウトメニューのフィルタ - フィードのフライアアウトメニューのフィルタを有効化します。 - フィードのフライアアウトメニューのフィルタを有効化します。 - フィードのフライアアウトメニューのフィルタを有効化 - フィードから検索バーを非表示にします。 - フィードから検索バーを非表示にします。 - 検索バーを非表示 - ホームページと登録チャンネルフィードからアンケートを非表示にします。 - ホームページと登録チャンネルフィードからアンケートを非表示にします。 - アンケートを非表示 - シークバーを上スワイプで表示されるフィルムストリップを非表示にします。 - シークバーを上スワイプで表示されるフィルムストリップを非表示にします。 - フィルムストリップオーバーレイを非表示 - ホームフィードの右下にある「フィードを調整する」ボタンを非表示にします。 - ホームフィードの右下にある「フィードを調整する」ボタンを非表示にします。 - フローティングボタンを非表示 - 動画を検索する際、右下に表示される音声入力のフローティングボタンを非表示にします。 - 動画を検索する際、右下に表示される音声入力のフローティングボタンを非表示にします。 - 音声入力のフローティングボタンを非表示 - おすすめ欄を非表示にします。 - おすすめ欄を非表示にします。 - おすすめ欄を非表示 - アプリ起動時に表示される全画面広告を非表示にします。 - アプリ起動時に表示される全画面広告を非表示にします。 - 全画面広告を非表示 - "現在の設定: 全ての全画面広告を非表示にします。 - -注意: 全画面のコミュニティ投稿画像も非表示になるという副作用があります。" - 現在の設定: 全画面広告の右上の「‪✕‬」ボタンを自動的に押します。 - 全画面広告を非表示にする方法を変更 - 動画以外の広告を非表示にします。 - 動画以外の広告を非表示にします。 - 全般的な広告を非表示 - プレーヤーと動画の説明の間に表示される YouTube Premium のバナーを非表示にします。 - プレーヤーと動画の説明の間に表示される YouTube Premium のバナーを非表示にします。 - YouTube Premium のプロモーションを非表示 - ホームフィードの動画の間に表示される、グレーのセパレーターを非表示にします。 - ホームフィードの動画の間に表示される、グレーのセパレーターを非表示にします。 - グレーのセパレーターを非表示 - ハンドル (例: @youtubecreators) を非表示にします。 - ハンドル (例: @youtubecreators) を非表示にします。 - ハンドルを非表示 - 画像検索ボタンを非表示にします。 - 画像検索ボタンを非表示にします。 - 画像検索ボタンを非表示 - 画像欄を非表示にします。 - 画像欄を非表示にします。 - 画像欄を非表示 - 概要欄のチャンネル情報カードを非表示にします。 - 概要欄のチャンネル情報カードを非表示にします。 - チャンネル情報カードを非表示 - プレーヤーの右上に表示される情報カードを非表示にします。 - プレーヤーの右上に表示される情報カードを非表示にします。 - 情報カードを非表示 - フィード、検索、動画に表示される重要な情報(医療関係等)のパネルを非表示にします。 - フィード、検索、動画に表示される重要な情報(医療関係等)のパネルを非表示にします。 - 情報パネルを非表示 - 「メンバーになる」ボタンを非表示にします。 - 「メンバーになる」ボタンを非表示にします。 - 「メンバーになる」ボタンを非表示 - 概要欄下部に表示されるキーコンセプトセクションを非表示にします。 - 概要欄下部に表示されるキーコンセプトセクションを非表示にします。 - キーコンセプトセクションを非表示 - "ホーム / 登録チャンネル / 検索結果 は、キーワードやフレーズに一致するコンテンツを非表示にするようにフィルタリングされます。 -注意: -• 一部のショート動画は非表示にならない場合があります。 -• 一部の UI コンポーネントは非表示にならない場合があります。 -• キーワードで検索しても結果が表示されない場合があります。" - キーワードフィルタリングについて - キーワードやフレーズを二重引用符 (\" \") で囲むことで、動画のタイトルやチャンネル名の部分一致を防ぐことができます。<br><br>例えば、<br><b>\"ai\"</b> とすると、動画 <b>「AI はどのように機能するのか?」</b><br> は表示されなくなりますが、<b>「フェアユースとは何か? (What does f“ai”r use mean?) 」</b> は表示されます。 - 単語全体を一致させる - キーワードでコメントをフィルタリングします。 - キーワードでコメントをフィルタリングします。 - コメントをフィルタリング - キーワードでホームフィード内の動画をフィルタリングします。 - キーワードでホームフィード内の動画をフィルタリングします。 - ホームフィードをフィルタリング - "非表示にするキーワードやフレーズを入力します。(改行区切り) - -キーワードには、チャンネル名や動画のタイトルに表示されるテキストを使用できます。 - -真ん中に大文字がある単語は、大文字と小文字を区別して入力する必要があります。(例: iPhone、TikTok、LeBlanc)" - キーワードでフィルタリング - キーワードで検索結果をフィルタリングします。 - キーワードで検索結果をフィルタリングします。 - 検索結果をフィルタリング - キーワードで登録チャンネルの動画をフィルタリングします。 - キーワードで登録チャンネルの動画をフィルタリングします。 - 登録チャンネルをフィルタリング - キーワード「%1$s」は範囲が広すぎるため、すべての動画を非表示にします。 - 無効なキーワードです。\'%s\' はフィルターとして使用できません - キーワードを使用するには、引用符を追加してください: %s - キーワードに矛盾する宣言があります: %s - キーワードが短すぎるため、引用符が必要です: %s - 最新の投稿を非表示にします。 - 最新の投稿を非表示にします。 - 最新の投稿を非表示 - 「最新の動画」ボタンを非表示にします。 - 「最新の動画」ボタンを非表示にします。 - 「最新の動画」ボタンを非表示 - 高評価と低評価ボタンを非表示にします。 - 高評価と低評価ボタンを非表示にします。 - 高評価と低評価ボタンを非表示 - ライブチャットのコメントを非表示にします。\n\nこの設定は縦型のライブ配信にも適用されます。 - ライブチャットのコメントを非表示にします。\n\nこの設定は縦型のライブ配信にも適用されます。 - ライブチャットのコメントを非表示 - ライブで全画面表示時に右下に表示される「チャットのリプレイ」ボタンを非表示にします。\n\nライブチャットを終了すると全画面表示になります。 - ライブで全画面表示時に右下に表示される「チャットのリプレイ」ボタンを非表示にします。\n\nライブチャットを終了すると全画面表示になります。 - チャットのリプレイボタンを非表示 - 登録していないチャンネルからアップロードされた、再生回数が 1,000 回未満の動画をホームフィードから非表示にします。 - 再生回数が少ない動画を非表示 - 医療情報パネルを非表示にします。 - 医療情報パネルを非表示にします。 - 医療情報パネルを非表示 - フィードに表示される商品広告を非表示にします。 - フィードに表示される商品広告を非表示にします。 - 商品欄を非表示 - ミックスプレイリストを非表示にします。 - ミックスプレイリストを非表示にします。 - ミックスプレイリストを非表示 - 有料の映画、テレビ番組を非表示にします。 - 有料の映画、テレビ番組を非表示にします。 - 映画欄を非表示 - ナビゲーションバー(ホーム、登録チャンネルなどのボタン)を非表示にします。 - ナビゲーションバー(ホーム、登録チャンネルなどのボタン)を非表示にします。 - ナビゲーションバーを非表示 - 「作成」ボタンを非表示にします。 - 「作成」ボタンを非表示にします。 - 作成ボタンを非表示 - 「ホーム」ボタンを非表示にします。 - 「ホーム」ボタンを非表示にします。 - ホームボタンを非表示 - ナビゲーションバーのラベルを非表示にします。 - ナビゲーションバーのラベルを非表示にします。 - ナビゲーションバーのラベルを非表示 - 「ライブラリ」ボタンを非表示にします。 - 「ライブラリ」ボタンを非表示にします。 - ライブラリボタンを非表示 - 「通知」ボタンを非表示にします。 - 「通知」ボタンを非表示にします。 - 通知ボタンを非表示 - 「ショート」ボタンを非表示にします。 - 「ショート」ボタンを非表示にします。 - ショートボタンを非表示 - 「登録チャンネル」ボタンを非表示にします。 - 「登録チャンネル」ボタンを非表示にします。 - 登録チャンネルボタンを非表示 - 配信予定の動画の「通知する」ボタンを非表示にします。 - 配信予定の動画の「通知する」ボタンを非表示にします。 - 「通知する」ボタンを非表示 - プレーヤー上に表示される「プロモーションを含みます」の文章を非表示にします。 - プレーヤー上に表示される「プロモーションを含みます」の文章を非表示にします。 - 有料プロモーションラベルを非表示 - Playables (アプリやウェブ版ですぐにゲームが遊べる機能) を非表示にします。 - Playables (アプリやウェブ版ですぐにゲームが遊べる機能) を非表示にします。 - Playables を非表示 - 「自動再生」ボタンを非表示にします。 - 「自動再生」ボタンを非表示にします。 - 自動再生ボタンを非表示 - 「字幕」ボタンを非表示にします。 - 「字幕」ボタンを非表示にします。 - 字幕ボタンを非表示 - 「キャスト」ボタンを非表示にします。 - 「キャスト」ボタンを非表示にします。 - キャストボタンを非表示 - 「プレーヤーの最小化」ボタンを非表示にします。 - 「プレーヤーの最小化」ボタンを非表示にします。 - 最小化ボタンを非表示 - 「アンビエントモード」メニューを非表示にします。 - 「アンビエントモード」メニューを非表示にします。 - 「アンビエントモード」を非表示 - 「音声トラック」メニューを非表示にします。 - 「音声トラック」メニューを非表示にします。 - 「音声トラック」を非表示 - 「字幕」メニューの下にあるスペースを削除します。 - 「字幕」メニューの下にあるスペースを削除します。 - 字幕メニュー下のスペースを削除 - 「字幕」メニューを非表示にします。 - 「字幕」メニューを非表示にします。 - 「字幕」を非表示 - 画質設定メニューから「1080p Premium」を非表示にします。 - 画質設定メニューから「1080p Premium」を非表示にします。 - 1080p Premium メニューを非表示 - 「ヘルプとフィードバック」メニューを非表示にします。 - 「ヘルプとフィードバック」メニューを非表示にします。 - 「ヘルプとフィードバック」を非表示 - 「YouTube Music で聴く」メニューを非表示にします。 - 「YouTube Music で聴く」メニューを非表示にします。 - 「YouTube Music で聴く」を非表示 - 「画面のロック」メニューを非表示にします。 - 「画面のロック」メニューを非表示にします。 - 「画面のロック」を非表示 - 「ループ再生」メニューを非表示にします。 - 「ループ再生」メニューを非表示にします。 - 「ループ再生」を非表示 - 「詳細情報」メニューを非表示にします。 - 「詳細情報」メニューを非表示にします。 - 「詳細情報」を非表示 - 「ピクチャーインピクチャー」メニューを非表示にします。 - 「ピクチャーインピクチャー」メニューを非表示にします。 - 「ピクチャーインピクチャー」を非表示 - 「再生速度」メニューを非表示にします。 - 「再生速度」メニューを非表示にします。 - 「再生速度」を非表示 - 「Premium のコントロール」メニューを非表示にします。 - 「Premium のコントロール」メニューを非表示にします。 - 「Premium のコントロール」を非表示 - 画質メニューの下にあるスペースを削除します。 - 画質メニューの下にあるスペースを削除します。 - 画質メニューの下のスペースを削除 - 画質メニューの上にあるスペースを削除します。 - 画質メニューの上にあるスペースを削除します。 - 画質メニューの上のスペースを削除 - 「報告」メニューを非表示にします。 - 「報告」メニューを非表示にします。 - 「報告」を非表示 - 「スリープタイマー」メニューを非表示にします。 - 「スリープタイマー」メニューを非表示にします。 - 「スリープタイマー」を非表示 - 「一定音量」メニューを非表示にします。 - 「一定音量」メニューを非表示にします。 - 「一定音量」を非表示 - 「統計情報」メニューを非表示にします。 - 「統計情報」メニューを非表示にします。 - 「統計情報」を非表示 - 「VR で見る」メニューを非表示にします。 - 「VR で見る」メニューを非表示にします。 - 「VR で見る」を非表示 - 全画面表示のボタンを非表示にします。 - 全画面表示のボタンを非表示にします。 - 全画面表示のボタンを非表示 - 「前の動画に戻る」「次の動画に進む」ボタンを非表示にします。 - 「前の動画に戻る」「次の動画に進む」ボタンを非表示にします。 - 前の動画に戻る/次の動画に進むボタンを非表示 - 動画のタイトルの下部にある「○○ストア」欄を非表示にします。 - 動画のタイトルの下部にある「○○ストア」欄を非表示にします。 - ストア欄を非表示 - 「YouTube Music」ボタンを非表示にします。 - 「YouTube Music」ボタンを非表示にします。 - YouTube Music ボタンを非表示 - 「再生リストに保存」ボタンを非表示にします。 - 「再生リストに保存」ボタンを非表示にします。 - 「保存」ボタンを非表示 - 概要欄のポッドキャストセクションを非表示にします。 - 概要欄のポッドキャストセクションを非表示にします。 - ポッドキャストセクションを非表示 - コメントのプレビューを非表示にします。 - コメントのプレビューを非表示にします。 - コメントのプレビューを非表示 - 現在の設定: コメント欄のサイズをコンパクトに変更します。\n\n注意: コメント欄からライブチャットのリプレイを開くことができなくなります。 - 現在の設定: コメント欄を元のサイズに変更します。 - コメントのプレビューを非表示にする方法を設定 - YouTube Premium の価格の値上げなどのプロモーションバナーを非表示にします。 - YouTube Premium の価格の値上げなどのプロモーションバナーを非表示にします。 - プロモーションバナーを非表示 - 「コメント」ボタンを非表示にします。 - 「コメント」ボタンを非表示にします。 - コメントボタンを非表示 - 「低評価」ボタンを非表示にします。 - 「低評価」ボタンを非表示にします。 - 低評価ボタンを非表示 - 「高評価」ボタンを非表示にします。 - 「高評価」ボタンを非表示にします。 - 高評価ボタンを非表示 - 「チャット」ボタンを非表示にします。 - 「チャット」ボタンを非表示にします。 - チャットボタンを非表示 - 「さらに表示」ボタンを非表示にします。 - 「さらに表示」ボタンを非表示にします。 - 「さらに表示」ボタンを非表示 - ミックスリストを非表示にします。 - ミックスリストを非表示にします。 - ミックスリストを非表示 - プレイリストを非表示にします。 - プレイリストを非表示にします。 - 再生リストを開くボタンを非表示 - 「プレイリストに追加する」ボタンを非表示にします。 - 「プレイリストに追加する」ボタンを非表示にします。 - プレイリストボタンを非表示 - 「共有」ボタンを非表示にします。 - 「共有」ボタンを非表示にします。 - 共有ボタンを非表示 - 「クイック操作」コンテナーを非表示にします。 - 「クイック操作」コンテナーを非表示にします。 - クイック操作コンテナーを非表示 - "以下のおすすめ動画を非表示にします: - -• 「メンバーシップ限定」タグの付いた動画 -• 動画の下部に「他のユーザーも視聴しています」などのフレーズがある動画 -• 登録していないチャンネルからアップロードされた、再生回数が 1,000 回未満の動画" - おすすめ動画を非表示 - 関連動画のオーバーレイを無効化します。 - 関連動画のオーバーレイを無効化します。 - 関連動画を非表示 - ホームフィードから関連動画を非表示にします。 - ホームフィードから関連動画を非表示にします。 - 関連動画を非表示 - "これを有効にすると、プレーヤー画面に読み込まれるレイアウトの最大数を制限します。 - -注意: サーバー側の変更によりプレーヤー画面のレイアウトが変更された場合、意図しないレイアウトがプレーヤー画面上から非表示になる可能性があります。" - 「リミックス」ボタンを非表示にします。 - 「リミックス」ボタンを非表示にします。 - 「リミックス」ボタンを非表示 - 「報告」ボタンを非表示にします。 - 「報告」ボタンを非表示にします。 - 「報告」ボタンを非表示 - 「特典」ボタンを非表示にします。 - 「特典」ボタンを非表示にします。 - 「特典」ボタンを非表示 - 検索履歴のサムネイルを非表示にします。 - 検索履歴のサムネイルを非表示にします。 - 検索履歴のサムネイルを非表示 - 動画長押しシーク時に表示される「移動するには左右にスライドします」などのメッセージを非表示にします。 - 動画長押しシーク時に表示される「移動するには左右にスライドします」などのメッセージを非表示にします。 - シーク時のメッセージを非表示 - シーク取り消しのメッセージを非表示にします。 - シーク取り消しのメッセージを非表示にします。 - シーク取り消しメッセージを非表示 - タイムスタンプの横に表示されるチャプターのラベルを非表示にします。 - タイムスタンプの横に表示されるチャプターのラベルを非表示にします。 - チャプターのラベルを非表示 - プレーヤーのシークバーを非表示にします。 - プレーヤーのシークバーを非表示にします。 - 動画のサムネイルのシークバーを非表示にします。 - 動画のサムネイルのシークバーを非表示にします。 - 動画サムネイルのシークバーを非表示 - プレーヤーのシークバーを非表示 - 概要欄下部に表示されるセルフスポンサーカードを非表示にします。 - 概要欄下部に表示されるセルフスポンサーカードを非表示にします。 - 自己スポンサーカードを非表示 - 「アプリに関する情報」メニューを非表示にします。 - 「アプリに関する情報」メニューを非表示にします。 - アプリに関する情報メニューを非表示 - 「ユーザー補助」メニューを非表示にします。 - 「ユーザー補助」メニューを非表示にします。 - ユーザー補助メニューを非表示 - 「アカウント」メニューを非表示にします。 - 「アカウント」メニューを非表示にします。 - アカウントメニューを非表示 - 「自動再生」メニューを非表示にします。 - 「自動再生」メニューを非表示にします。 - 自動再生メニューを非表示 - 「請求とお支払い」メニューを非表示にします。 - 「請求とお支払い」メニューを非表示にします。 - 請求とお支払いメニューを非表示 - 「字幕」メニューを非表示にします。 - 「字幕」メニューを非表示にします。 - 字幕メニューを非表示 - 「接続済みのアプリ」メニューを非表示にします。 - 「接続済みのアプリ」メニューを非表示にします。 - 接続済みのアプリメニューを非表示 - 「データの節約」メニューを非表示にします。 - 「データの節約」メニューを非表示にします。 - データの節約メニューを非表示 - 「全般」メニューを非表示にします。 - 「全般」メニューを非表示にします。 - 全般メニューを非表示 - 「すべての履歴を管理」メニューを非表示にします。 - 「すべての履歴を管理」メニューを非表示にします。 - すべての履歴を管理メニューを非表示 - 「チャット」メニューを非表示にします。 - 「チャット」メニューを非表示にします。 - チャットメニューを非表示 - 「通知」メニューを非表示にします。 - 「通知」メニューを非表示にします。 - 通知メニューを非表示 - 「バックグラウンド」メニューを非表示にします。 - 「バックグラウンド」メニューを非表示にします。 - バックグラウンドメニューを非表示 - 「テレビで見る」メニューを非表示にします。 - 「テレビで見る」メニューを非表示にします。 - テレビで見るメニューを非表示 - 「ファミリーセンター」メニューを非表示にします。 - 「ファミリーセンター」メニューを非表示にします。 - ファミリーセンターメニューを非表示 - 「試験運用版の新機能を試す」メニューを非表示にします。 - 「試験運用版の新機能を試す」メニューを非表示にします。 - 試験運用版の新機能を試すメニューを非表示 - 「プライバシー」メニューを非表示にします。 - 「プライバシー」メニューを非表示にします。 - プライバシーメニューを非表示 - 「購入とメンバーシップ」メニューを非表示にします。 - 「購入とメンバーシップ」メニューを非表示にします。 - 購入とメンバーシップメニューを非表示 - 「YouTube 設定」メニューの要素を非表示にします。 - YouTube 設定メニューを非表示 - 「動画の画質設定」メニューを非表示にします。 - 「動画の画質設定」メニューを非表示にします。 - 動画の画質設定メニューを非表示 - 「YouTube でのデータ」メニューを非表示にします。 - 「YouTube でのデータ」メニューを非表示にします。 - YouTube でのデータメニューを非表示 - 「共有」ボタンを非表示にします。 - 「共有」ボタンを非表示にします。 - 「共有」ボタンを非表示 - 「ショップ」ボタンを非表示にします。 - 「ショップ」ボタンを非表示にします。 - 「ショップ」ボタンを非表示 - 概要欄のショッピングのリンクを非表示にします。 - 概要欄のショッピングのリンクを非表示にします。 - ショッピングリンクを非表示 - プレーヤー下部に表示されるチャンネルのバーを非表示にします。 - プレーヤー下部に表示されるチャンネルのバーを非表示にします。 - チャンネルバーを非表示 - 「コメント」ボタンを非表示にします。 - 「コメント」ボタンを非表示にします。 - コメントボタンを非表示 - コメント数が「0」または「無効」になっているコメントボタンを非表示にします。 - コメント数が「0」または「無効」になっている「コメント」ボタンを非表示にします。 - 無効な「コメント」ボタンを非表示 - 「低評価」ボタンを非表示にします。 - 「低評価」ボタンを非表示にします。 - 「低評価」ボタンを非表示 - "「このサウンドを使用する」のようなフローティングボタンを、ショートタブから非表示にします。" - "「このサウンドを使用する」のようなフローティングボタンを、ショートタブから非表示にします。" - フローティングボタンを非表示 - フルの動画のリンクのラベルを非表示にします。 - フルの動画のリンクのラベルを非表示にします。 - フルの動画のリンクラベルを非表示 - プレーヤーの下部に表示される「グリーンスクリーン」ボタンを非表示にします。 - プレーヤーの下部に表示される「グリーンスクリーン」ボタンを非表示にします。 - グリーンスクリーンボタンを非表示 - 情報パネルを非表示にします。 - 情報パネルを非表示にします。 - 情報パネルを非表示 - 「メンバーになる」ボタンを非表示にします。 - 「メンバーになる」ボタンを非表示にします。 - 「メンバーになる」ボタンを非表示 - 「高評価」ボタンを非表示にします。 - 「高評価」ボタンを非表示にします。 - 高評価ボタンを非表示 - 縦型のライブ配信のプレーヤー内で上部に表示されるチャンネル名などを非表示にします。\n\nヘッダーの戻るボタンは非表示になりません。 - 縦型のライブ配信のプレーヤー内で上部に表示されるチャンネル名などを非表示にします。\n\nヘッダーの戻るボタンは非表示になりません。 - ライブチャットのヘッダーを非表示 - 位置情報のボタンを非表示にします。 - 位置情報のボタンを非表示にします。 - 位置情報のボタンを非表示 - ナビゲーションバー(ホーム、登録チャンネルなどのボタン)を非表示にします。 - ナビゲーションバー(ホーム、登録チャンネルなどのボタン)を非表示にします。 - ナビゲーションバーを非表示 - プレーヤー左上の「プロモーションを含みます」を非表示にします。 - プレーヤー左上の「プロモーションを含みます」を非表示にします。 - 有料プロモーションラベルを非表示 - 一時停止中に左上に表示される「ショート」を非表示にします。 - 一時停止中に左上に表示される「ショート」を非表示にします。 - 一時停止中のヘッダーを非表示 - 一時停止中に表示されるオーバーレイボタンを非表示にします。 - 一時停止中に表示されるオーバーレイボタンを非表示にします。 - 一時停止中のオーバーレイボタンを非表示 - 再生 / 一時停止ボタンの背景を非表示にします。 - 再生 / 一時停止ボタンの背景を非表示にします。 - ボタンの背景を非表示 - 「リミックス」ボタンを非表示にします。 - 「リミックス」ボタンを非表示にします。 - 「リミックス」ボタンを非表示 - 楽曲の「保存」ボタンを非表示にします。 - 楽曲の「保存」ボタンを非表示にします。 - 保存ボタンを非表示 - 検索候補のボタンを非表示にします。 - 検索候補のボタンを非表示にします。 - 検索候補のボタンを非表示 - 「共有」ボタンを非表示にします。 - 「共有」ボタンを非表示にします。 - 「共有」ボタンを非表示 - チャンネルページの「ホーム」からショート欄を非表示にします。\n\n注意: 「ショート」ヘッダーがあるショート欄のみが非表示になります。 - "チャンネルページの「ホーム」からショート欄を非表示にします。 - -注意: 「ショート」ヘッダーがあるショート欄のみが非表示になります。" - チャンネルページから非表示 - 再生履歴から非表示にします。 - 再生履歴から非表示にします。 - 再生履歴から非表示 - ホームフィードや関連動画から非表示にします。 - ホームフィードや関連動画から非表示にします。 - ホームフィードや関連動画から非表示 - 検索結果から非表示にします。 - 検索結果から非表示にします。 - 検索結果から非表示 - 登録チャンネルフィードから非表示にします。 - 登録チャンネルフィードから非表示にします。 - 登録チャンネルフィードから非表示 - "ショート欄を非表示にします。 - -注意: 検索結果の公式ヘッダーが非表示になります。" - ショート欄を非表示 - 「ショップ」ボタンを非表示にします。 - 「ショップ」ボタンを非表示にします。 - 「ショップ」ボタンを非表示 - ショートで左下に表示される「購入」ボタンを非表示にします。 - ショートで左下に表示される「購入」ボタンを非表示にします。 - 「購入」ボタンを非表示 - プレーヤー右下に表示される楽曲のボタンを非表示にします。 - プレーヤー右下に表示される楽曲のボタンを非表示にします。 - 楽曲ボタンを非表示 - プレーヤー下部に表示される楽曲のラベルを非表示にします。 - プレーヤー下部に表示される楽曲のラベルを非表示にします。 - 楽曲のラベルを非表示 - ステッカーを非表示にします。 - ステッカーを非表示にします。 - ステッカーを非表示 - 「チャンネル登録」ボタンを非表示にします。 - 「チャンネル登録」ボタンを非表示にします。 - 「チャンネル登録」ボタンを非表示 - 「Super Thanks」ボタンを非表示にします。 - 「Super Thanks」ボタンを非表示にします。 - 「Super Thanks」ボタンを非表示 - タグ付けされている商品を非表示にします。 - タグ付けされている商品を非表示にします。 - タグ付き商品を非表示 - ツールバー(カメラ、検索などのボタン)を非表示にします。 - ツールバー(カメラ、検索などのボタン)を非表示にします。 - ツールバーを非表示 - 「トレンド」ボタンを非表示にします。 - 「トレンド」ボタンを非表示にします。 - 「トレンド」ボタンを非表示 - 「テンプレートを使用する」ボタンを非表示にします。 - 「テンプレートを使用する」ボタンを非表示にします。 - 「テンプレートを使用する」を非表示 - ショートで楽曲ボタンを押した際に表示される「このサウンドを使用する」ボタンを非表示にします。 - ショートで楽曲ボタンを押した際に表示される「このサウンドを使用する」ボタンを非表示にします。 - 「このサウンドを使用する」を非表示 - プレーヤー下部に表示される動画のタイトルを非表示にします。 - プレーヤー下部に表示される動画のタイトル名を非表示にします。 - 動画のタイトルを非表示 - 動画を検索した際に、検索結果の動画の下に表示される「もっと表示」のボタンを非表示にします。 - 動画を検索した際に、検索結果の動画の下に表示される「もっと表示」のボタンを非表示にします。 - 「もっと表示」ボタンを非表示 - 共有からリンクをコピーした際などに画面下部に表示されるポップアップを非表示にします。 - 共有からリンクをコピーした際などに画面下部に表示されるポップアップを非表示にします。 - スナックバーを非表示 - 「トライアル開始」ボタンを非表示にします。 - 「トライアル開始」ボタンを非表示にします。 - 「トライアル開始」ボタンを非表示 - 「登録チャンネル」タブの上部に表示される登録チャンネル一覧を非表示にします。 - 「登録チャンネル」タブの上部に表示される登録チャンネル一覧を非表示にします。 - 登録チャンネルのカルーセルを非表示 - プレーヤー上の「Premium のコントロール」などの提案を非表示にします。 - プレーヤー上の「Premium のコントロール」などの提案を非表示にします。 - 提案されるアクションを非表示 - "この設定は非推奨になりました。 - -代わりに、「設定 → 自動再生 → 次の動画の自動再生」設定を使用してください。" - 自動再生がオフの場合、おすすめの動画は動画の終了画面で表示されません。\n\n自動再生は YouTube の設定で変更できます: 「設定 → 自動再生 → 次の動画を自動再生」 - "自動再生がオフの場合、おすすめの動画は動画の終了画面で表示されません。\n\n自動再生は YouTube の設定で変更できます: 「設定 → 自動再生 → 次の動画を自動再生」" - おすすめされる動画の終了画面を非表示 - 「Thanks」ボタンを非表示にします。 - 「Thanks」ボタンを非表示にします。 - 「Thanks」ボタンを非表示 - 検索結果/関連動画からチケット欄を非表示にします。 - 検索結果/関連動画からチケット欄を非表示にします。 - チケット欄を非表示 - タイムスタンプを非表示にします。 - タイムスタンプを非表示にします。 - タイムスタンプを非表示 - Timed Reactions を非表示にします。 - Timed Reactions を非表示にします。 - リアクションを非表示 - 「キャスト」ボタンを非表示にします。 - 「キャスト」ボタンを非表示にします。 - キャストボタンを非表示 - 「作成」ボタンを非表示にします。 - 「作成」ボタンを非表示にします。 - 作成ボタンを非表示 - 「通知」ボタンを非表示にします。 - 「通知」ボタンを非表示にします。 - 通知ボタンを非表示 - 概要欄の文字起こしセクションを非表示にします。 - 概要欄の文字起こしセクションを非表示にします。 - 文字起こし欄を非表示 - プレーヤー内の広告を非表示にします。 - プレーヤー内の広告を非表示にします。 - 動画広告を非表示 - "ホーム / 登録チャンネル / 検索結果はフィルタリングされ、設定した値よりも少ない再生回数の動画を非表示にします。 - -注意: -• ショート動画は非表示にできません。 -• 再生回数が 0 の動画はフィルタリングされません。" - 再生回数のフィルタリングについて - ホームフィード内の動画を再生回数でフィルタリングします。 - ホームフィード内の動画を再生回数でフィルタリングします。 - ホームフィードをフィルタリング - 検索結果を再生回数でフィルタリングします。 - 検索結果を再生回数でフィルタリングします。 - 検索結果をフィルタリング - 登録チャンネルの動画を再生回数でフィルタリングします。 - 登録チャンネルの動画を再生回数でフィルタリングします。 - 登録チャンネルをフィルタリング - 設定した再生回数未満のおすすめ動画を非表示にします。 - おすすめ動画を再生回数でフィルタリング - この数値より多い再生回数の動画を非表示にします。 - 再生回数が多い動画を非表示 - この数値より少ない再生回数の動画を非表示にします。 - 再生回数が少ない動画を非表示 - 万 -> 10 000\n億 -> 100 000 000\n回視聴 -> views - UIの各動画の下に表示される再生回数の言語テンプレートを設定します。各キー (言語の文字/単語) -> 値 (キーの意味) は、改行して記述する必要があります。キーは「->」記号の前に記述します。言語設定を更新した場合は、この設定をリセットする必要があります。\n\n例:\n英語: 10K views = K -> 1000、views -> 回\nスペイン語: 10 K vistas = K -> 1000、vistas -> 回 - キーを表示 - プレーヤー内に表示される商品バナーを非表示にします。 - プレーヤー内に表示される商品バナーを非表示にします。 - 商品バナーを非表示 - 「音声検索」ボタンを非表示にします。 - 「音声検索」ボタンを非表示にします。 - 音声検索ボタンを非表示 - 検索フィードから Web 検索結果を非表示にします。 - 検索フィードから Web 検索結果を非表示にします。 - ウェブの検索結果を非表示 - YouTube Doodle を非表示にします。 - YouTube Doodle を非表示にします。 - YouTube Doodle を非表示 - "YouTube Doodle とは、左上の YouTube ロゴが祝日や記念日などにその日にあわせたデザインに変更されるロゴのことです。 - -注意: 現在お住まいの地域で YouTube Doodle が表示されていて、この設定がオンの場合、検索欄の下のフィルターバーも非表示になります。" - ズーム時のオーバーレイを非表示にします。 - ズーム時のオーバーレイを非表示にします。 - ズームオーバーレイを非表示 - Afn Blue - Afn Red - カスタム - オリジナル - MMT - MMT ブルー - MMT グリーン - MMT イエロー - Revancify Blue - Revancify Red - Revancify Yellow - Vanced Black - Vanced Light - Xisr Yellow - YouTube - 全画面表示時に、画面をオン/オフしても、横向きモードを維持します。 - 画面がオンになってから横画面モードが強制されるまでのミリ秒数です。 - 横画面モードのタイムアウトを維持 - 横画面モードを維持 - オリジナル - ダブルタップアクションを有効化します。\n\n・モダン1: ダブルタップで、最小化された動画を大きなサイズに変更します。\n・モダン2, 3: ダブルタップで、最小化された動画を閉じます。 - "ダブルタップアクションを有効化します。 - -・モダン1: ダブルタップで、最小化された動画を大きなサイズに変更します。 -・モダン2, 3: ダブルタップで、最小化された動画を閉じます。" - ダブルタップアクションを有効化 - ドラッグ&ドロップを有効化します。 - ドラッグ&ドロップを有効化します。 - ドラッグ&ドロップを有効化 - 拡大/縮小のボタンを非表示にします。\n(ミニプレーヤーをスワイプして拡大/縮小できます) - 拡大/縮小のボタンを非表示にします。\n(ミニプレーヤーをスワイプして拡大/縮小できます) - 拡大/縮小ボタンを非表示 - 次の動画/前の動画へスキップするボタンを非表示にします。 - 次の動画/前の動画へスキップするボタンを非表示にします。 - スキップボタンを非表示 - ミニプレーヤーに表示される「プロモーションを含みます」などの文章を非表示にします。 - ミニプレーヤーに表示される「プロモーションを含みます」などの文章を非表示にします。 - サブテキストを非表示 - ミニプレーヤーのオーバーレイの不透明度は0 ~ 100の間でなければなりません。デフォルト値にリセットします。 - 不透明度の値は 0 ~ 100 の間で、 0 が透明です。 - オーバーレイの不透明度 - オリジナル - スマホ - タブレット - モダン1 - モダン2 - モダン3 - ミニプレーヤーの種類 - オーバーレイボタン - "タップして常にリピート -長押ししてリピート後に一時停止" - 自動リピートボタンを表示 - "タップして動画 URL をコピー -長押ししてタイムスタンプ付き URL をコピー" - "タップしてタイムスタンプ付き URL をコピー -長押ししてタイムスタンプをコピー" - タイムスタンプ付き URL のコピーボタンを表示 - 動画 URL のコピーボタンを表示 - タップして外部の動画ダウンローダーを起動します。 - 外部ダウンロードボタンを表示 - 現在の動画の音声をミュートするには、タップします。 ミュートを解除するには、もう一度タップします。 - 音声ミュートボタンを表示 - ボタンの状態を変更するには、長押ししてください。 - 再生速度を %s 倍速にリセットしました。 - "タップして再生速度ダイアログをを開きます。 -長押しして再生速度を 1.0 倍速にします。" - 再生速度のダイアログボタンを表示 - "タップすると、チャンネルの古いものから新しいものまですべての動画の再生リストが生成されます。 -元に戻すには、長押しします。" - 時間順のプレイリストボタンを表示 - タップするとホワイトリストのダイアログが開きます。 -長押しするとホワイトリストの設定のダイアログが開きます。 - ホワイトリストボタンを表示 - 「ダウンロード」ボタンで外部ダウンローダーを開きます。 - 「ダウンロード」ボタンで外部ダウンローダーを開きます。 - プレイリストにダウンロードボタンを追加 - 「オフライン」ボタンで外部ダウンローダーを開きます。 - 「オフライン」ボタンで外部ダウンローダーを開きます。 - 「オフライン」ボタンを置換 - ボタンを置換するには YouTube Music が必要です。ここをタップして YouTube Music をダウンロードします。 - 前提条件 - 「YouTube Music」ボタンで RVX Music を開けるようにします。 - 「YouTube Music」ボタンで RVX Music を開けるようにします。 - YouTube Music ボタンを置換 - 除外されています - 適用されています - 通常 - プレーヤー下部(共有、クリップなど)のボタン - その他の設定 - アニメーション / フィードバック - ダウンロードボタン - 実験的な機能 - 画像表示の地域制限 - ファイルとしてインポート/エクスポート - テキストとしてインポート/エクスポート - キーワードフィルター - その他 - オーバーレイボタン - パッチ情報 - クイック操作 - おすすめ動画 - ショート欄 - 推奨されるアクション - 使用されたツール - 再生回数フィルター - 「アカウント」メニューと「マイページ」タブで要素を非表示または表示します。 - 「アカウント」メニュー - 動画下のアクションボタンを非表示または表示します。 - アクションボタン - 広告 - 代替サムネイル - アンビエントモードの制限を回避またはアンビエントモードを無効化します。 - アンビエントモード - フィード、検索、関連動画からカテゴリバーを非表示または表示します。 - カテゴリーバー - 動画の下のチャンネルバーコンポーネント(「参加」ボタン、「トライアルを開始」ボタン等)を非表示または表示します。 - チャンネルバー - チャンネルプロフィールのコンポーネントを非表示または表示します。 - チャンネルプロフィール - コメントセクションのコンポーネントを非表示または表示します。 - コメント - フィードとチャンネルのコミュニティの投稿を非表示または表示します。 - コミュニティ投稿 - カスタムフィルターを使用してコンポーネントを非表示にします。 - カスタムフィルター - フィード内のドロップダウンメニューを表示または非表示にします。 - フライアウトメニュー - フィード - 全画面表示に関連するコンポーネントを非表示または変更します。 - 全画面表示 - 全般 - 触覚フィードバックを無効化または有効化します。 - 触覚フィードバック - YouTube アプリ内の「YouTube Music」ボタンを置換します。 - ボタンをフック - 設定をインポートまたはエクスポートします。 - 設定のインポート/エクスポート - ミニプレーヤーのスタイルを変更します。 - ミニプレーヤー - その他 - ナビゲーションバーセクションのコンポーネントを非表示または表示します。 - ナビゲーションバー - 適用されたパッチに関する情報です。 - パッチ情報 - プレーヤー内のボタンを非表示または表示します。 - プレーヤーボタン - プレーヤーのフライアウトメニューを非表示または変更します。 - フライアウトメニュー - プレーヤー - Return YouTube Username - Return YouTube Dislike - SponsorBlock - シークバーのコンポーネントをカスタマイズします。 - シークバー - 「YouTube 設定」メニューの要素を非表示にします。 - 設定メニュー - ショートのプレーヤー内のコンポーネントを非表示または表示します。 - プレーヤー - ショート - バッファリングの問題を防ぐためにストリーミングデータを偽装します。 - ストリーミングデータを偽装 - スワイプコントロール - ツールバーのボタン、検索バー、ヘッダーなどのツールバーにあるコンポーネントを非表示または変更できます。 - ツールバー - 概要欄のコンポーネントを非表示または表示 - 概要欄 - キーワードや再生回数で動画をフィルタリングします。 - 動画フィルター - 動画 - 再生履歴に関連する設定を変更します。 - 再生履歴 - クイックアクションの上部の余白は 0 ~ 32 の間でなければなりません。デフォルト値にリセットします。 - シークバーからクイックアクション コンテナーまでの間隔を 0 ~ 32 の間で設定してください。 - クイック操作上部の余白 - "AV1 コーデック応答を強制的に拒否します。 -約20秒間のバッファリングの後、異なるコーデックに切り替わります。" - AV1 コーデックの応答を拒否 - フォールバック処理で約20秒のバッファリングが発生します。 - オフセット - 現在の設定: 再生速度の変更は現在の動画にのみ適用されます。 - 現在の設定: 再生速度の変更はすべての動画に適用されます。 - 再生速度の変更を保存 - デフォルトの再生速度を変更した際にトーストが表示されるようにします。 - デフォルトの再生速度を変更した際にトーストが表示されるようにします。 - トーストを表示 - デフォルトの再生速度を %s に変更しました。 - 現在の設定: 画質の変更は現在の動画にのみ適用されます。 - 現在の設定: 画質の変更はすべての動画に適用されます。 - 画質の変更を保存 - デフォルトの画質を変更した際にトーストが表示されるようにします。 - デフォルトの画質を変更した際にトーストが表示されるようにします。 - トーストを表示 - モバイルネットワーク使用時のデフォルト画質を %s に変更しました。 - 画質を設定できませんでした。 - Wi-Fi 使用時のデフォルト画質を %s に変更しました。 - "年齢制限ダイアログを削除します。 -これにより年齢制限を回避することはできませんが、自動的に同意します。" - 年齢制限ダイアログを削除 - AV1 コーデックを VP9 コーデックに置き換えます。 - AV1 コーデックを置換 - 現在の設定: チャンネルのハンドル名が表示されます。 - 現在の設定: チャンネル名が表示されます。 - ショートのチャンネル名を復元 - 現在の設定: プレーヤー左下のタイムスタンプをタップすると、残り時間が表示されます。 - 現在の設定: プレーヤー左下のタイムスタンプをタップすると、再生速度または画質のフライアウトメニューが開きます。 - タイムスタンプを置換 - 「作成」ボタンを「設定」ボタンに置き換えます。 - 「作成」ボタンを置換 - "タップすると YouTube の設定が開きます。 -長押しすると RVX 設定が開きます。" - "タップすると RVX 設定が開きます。 -長押しすると YouTube 設定が開きます。" - ボタンに割り当てるアクションの種類 - 現在の設定: 全画面表示時に、シークバーの上にサムネイルを表示します。 - 現在の設定: シークバーの上にサムネイルを表示します。 - 古いシークバーのサムネイルを復元 - 古いスタイルの画質設定メニューを復活させます。 - 古いスタイルの画質設定メニューを復活させます。 - 古いスタイルの画質メニューを復元 - \@ハンドル名 + (ユーザー名) - 表示形式 - ユーザー名 + (@ハンドル名) - ユーザー名のみ - ユーザー名の表示を復活させます。 - ユーザー名の表示を復活させます。 - Return YouTube Username を有効化 - "ハンドル名をユーザー名に置き換えるには、YouTube Data API v3 の開発者キーが必要です。 - -無料プランの API キーの 1 日あたりの割り当ては 10,000 で、コメント 1 件につきハンドル名をユーザー名に置き換えるのに 1 つの割り当てが使用されます。 - -API キーの発行方法については、ここをタップしてください。" - YouTube Data API キーについて - YouTube Data API v3 を使用するための開発者キーです。 - YouTube Data API キーを入力 - 1. 「<a href=%1$s>新しいプロジェクトの作成</a>」に移動します。<br>2. 「<b>作成</b>」をタップします。<br>3. 「<a href=%2$s>YouTube Data API v3</a>」に移動します。<br>4. 「<b>有効にする</b>」をタップします。<br>5. 「<b>認証情報を作成</b>」をタップします。<br>6. 「<b>一般公開データ</b>」オプションを選択します。<br>7. 「<b>次へ</b>」をタップします。<br>8. API キーをコピーします。<br><br>※API キーは他人と共有してはならないため、インポート/エクスポート設定には含まれません。 - YouTube Data API v3 の開発者キーを発行 - Return YouTube Dislike について - 低評価のデータは、Return YouTube Dislike API によって提供されています。詳細はここをタップしてください。 - ReturnYouTubeDislike.com - 「高評価」ボタンをコンパクトに表示します。 - 「高評価」ボタンをコンパクトに表示します。 - コンパクトな高評価ボタン - 低評価数をパーセントで表示します。 - 低評価数をパーセントで表示します。 - 低評価数をパーセントで表示 - 低評価数の表示を復活させます。\n\n注意: 正確な値ではありません。 - 低評価数の表示を復活させます。\n\n注意: 正確な値ではありません。 - Return YouTube Dislike を有効化 - 高評価数が非公開の動画で高評価数を推定して表示します。 - 高評価数が非公開の動画で高評価数を推定して表示します。 - 推定の高評価数を表示 - 低評価数は一時的に利用できません。 (クライアント API が制限に達しました) - 低評価数は一時的に利用できません。(ステータス: %d) - 低評価数は一時的に利用できません。(API がタイムアウトしました) - 低評価数は一時的に利用できません。(%s) - 投票するために Return YouTube Dislike を使用するため、動画を再読み込みします - ショートで低評価数を表示します。\n\n注意: シークレットモードでは低評価数が表示されないことがあります。 - ショートで低評価数を表示します。\n\n注意: シークレットモードでは低評価数が表示されないことがあります。 - "ショートで低評価数を表示します。 - -注意: シークレットモードでは低評価数が表示されないことがあります。" - ショートで低評価数を表示 - Return YouTube Dislike が利用できない場合にトーストを表示します。 - Return YouTube Dislike が利用できない場合にトーストを表示します。 - API が利用できない場合にトーストを表示 - 非表示 - リンクを共有する際に、URL からトラッキングクエリパラメーターを削除します。 - 共有リンクのクリーンアップ - "動画のタイトルの横にある「#」、「寄付」、「ショップ」、「商品」のようなサブタイトルを非表示にします。" - "動画のタイトルの横にある「#」、「寄付」、「ショップ」、「商品」のようなサブタイトルを非表示にします。" - 動画のサブタイトルをサニタイズ - SponsorBlock について - spon.ajay.app - データは SponsorBlock API によって提供されています。他のプラットフォームのダウンロードや詳細については、ここをタップしてください。 - API の URL を変更しました。 - API の URl が無効です。 - API の URL をリセットしました。 - 外観 - 色を変更しました。 - 色: - 無効なカラーコードです。 - 色をリセットしました。 - 新しいセグメントの作成 - セグメントの設定 - スキップボタンを自動的に非表示 - スキップボタンを数秒後に非表示にします。 - スキップボタンを数秒後に非表示にします。 - コンパクトなスキップボタンを使用 - 「スキップ」ボタンをコンパクトに表示します。 - 「スキップ」ボタンをコンパクトに表示します。 - 新しいセグメントの作成ボタンを表示 - プレーヤーの右上に「新しいセグメントの作成ボタン」を表示します。 - プレーヤーの右上に「新しいセグメントの作成ボタン」を表示します。 - SponsorBlock を有効化 - SponsorBlock は、YouTube の動画の迷惑な部分をスキップするためのクラウドソーシングシステムです。 - 評価ボタン - セグメントの「評価」ボタンを表示します。 - セグメントの「評価」ボタンを表示します。 - 全般 - 新しいセグメントのステップの調節 - 値は正の数でなければなりません。 - 新しいセグメントを作成する際の時間調節ボタンの移動時間 (ミリ秒) - API の URL を変更 - SponsorBlockがサーバーとの通信で使うアドレスです。自分が何をしているのか理解していない場合は、変更しないでください。 - 最小のセグメントの長さ - 無効な時間の値です。 - この値 (秒) より短いセグメントは、表示もスキップもされません。 - スキップ回数の集計を有効化 - スキップ回数の集計を有効化します。 - SponsorBlock リーダーボードに、どれだけの時間を節約したかを知らせます。セグメントがスキップされるたびに、リーダーボードにメッセージが送信されます。 - 自動的にスキップする時にトーストを表示 - セグメントが自動的にスキップされたときに、トーストを表示します。ここをタップすると、サンプルのトーストが表示されます。 - セグメントが自動的にスキップされたときに、トーストを表示します。ここをタップすると、サンプルのトーストが表示されます。 - セグメントを除いた動画の時間を表示 - セグメントを除いた時間をタイムスタンプの横の括弧内に表示します。 - セグメントを除いた時間をタイムスタンプの横の括弧内に表示します。 - プライベートユーザー ID - 「プライベート ユーザー ID」は 30 字以上でなければなりません。 - 「プライベートユーザー ID」は公開しないようにしてください。この ID はパスワードのようなものなので、誰とも共有しないでください。誰かが ID を知っている場合、あなたになりすますことができます。 - 既に読みました - 新しいセグメントを作成する前に、SponsorBlock のガイドラインをお読みください。 - 表示 - ガイドラインに従ってください - ガイドラインには、セグメントの作成に関するルールとヒントが含まれています。 - ガイドラインを表示 - 調整: セグメントの開始時間と終了時間をマークする - セグメントのカテゴリを選択してください - セグメントを確認 - The segment lasts from %1$02d:%2$02d to %3$02d:%4$02d (%5$d minutes %6$02d seconds)\nIs it ready to submit? - セグメントは\n\n%1$s\nから\n%2$s\n\n(%3$s)です。\n\n送信してもよろしいですか? - これらの変更は正しいですか? - カテゴリーは設定で無効になっています。送信するにはカテゴリーを有効にしてください。 - セグメントを編集 - セグメントの開始または終了のタイミングを編集しますか? - 時間の値が無効です。 - セグメントのタイミングを手動で編集 - 指定された時間までスキップ (デフォルト: 150ms) - %s を新しいセグメントの開始または終了として設定しますか? - 終了 - タイムバーの2か所をマークしてください - 開始 - 現在 - セグメントをプレビューして、スムーズにスキップできるようにしてください。 - 作成したセグメントを送信 - 指定された時間まで巻き戻す (デフォルト: 150ms) - 開始は終了より前である必要があります - セグメントの終了時刻 - セグメントの開始時刻 - 新しい SponsorBlock セグメント - リセット - 色のリセット - 繋ぎの話 / 冗談 - 動画の本編を理解するのに必要のない繋ぎの話やユーモアなどの逸脱したシーン。コンテクストや背景情報の詳細は含まれません。 - ハイライト - 動画の中で多くの人々が見たい部分 - 行動を促すメッセージ (チャンネル登録等) - 動画の途中に挿入される高評価、チャンネル登録、フォローなどを促す短いリマインダーは、長いものや何か具体的なものは「セルフプロモーション」に分類するべきです。 - 休憩 / イントロアニメーション - 本編ではない部分。一時停止、静止画面、アニメーションの繰り返しが含まれます。情報を含んだ転換画面は含まれません。 - 音楽ではない区間 - ミュージックビデオでのみ使用できます。他のカテゴリーに含まれていない、ミュージックビデオの音楽のない区間。 - エンドカード / クレジット - クレジットや動画のエンドカードが表示されている場面。情報を含む結論は含まれません。 - 予告 / 要約 / フック - この動画やシリーズの他の動画で起きた、または今後起きる内容などをまとめたクリップのコレクション。すべての情報は、別の場所で繰り返し表示されます。 - 無報酬 / セルフプロモーション - 無報酬のプロモーションあるいはセルフプロモーションであるという点を除いては「スポンサー」と同様です。商品、寄付、コラボ情報に関する内容を含みます。 - スポンサー - 有料プロモーション、有料紹介、直接広告が含まれます。セルフプロモーションや、個人の好きなクリエイター/ウェブサイト/商品に対する無償の活動は含まれません。 - コピー - エクスポート失敗: %s - 設定のインポート/エクスポート - SponsorBlock 設定の JSON は、ReVanced Extended やその他の SponsorBlock プラットフォームにインポート/エクスポートできます。 - ReVanced Extended や他のプラットフォームの SponsorBlock にインポート/エクスポート可能な SponsorBlock 設定の JSON です。これにはプライベートユーザー ID が含まれています。共有する際は十分注意してください。 - インポート失敗: %s - 設定は正常にインポートされました。 - この設定には SponsorBlock のプライベーユーザー ID が含まれています。\n\nユーザー ID はパスワードのようなもののため、誰とも共有しないようにしてください。\n - 今後表示しない - 設定をクリップボードにコピーしました。 - 自動的にスキップ - 一度だけ自動的にスキップ - スキップ - ハイライト - 繋ぎの話をスキップ - ハイライトにスキップ - 対話をスキップ - イントロをスキップ - 休憩をスキップ - 休憩をスキップ - 音楽以外をスキップ - アウトロをスキップ - 予告をスキップ - 要約をスキップ - 予告をスキップ - プロモーションをスキップ - スポンサーをスキップ - セグメントをスキップ - 無効 - シークバーに表示 - スキップボタンを表示 - 繋ぎの話をスキップしました - ハイライトにスキップしました - リマインダーをスキップしました - イントロをスキップしました - 休憩をスキップしました - 休憩をスキップしました - 複数のセグメントをスキップしました - 音楽ではない区間をスキップしました - アウトロをスキップしました - 予告をスキップしました - 要約をスキップしました - 予告をスキップしました - セルフプロモーションをスキップしました - スポンサーをスキップしました - 未送信のセグメントをスキップしました - SponsorBlock は一時的に利用できません。 - SponsorBlock は一時的に利用できません。(ステータス %d) - SponsorBlock は一時的に利用できません。(API がタイムアウトしました) - 統計情報 - 統計は一時的に利用できません (API がダウンしています) - 読み込み中... - あなたの評価は <b>%.2f</b> です。 - 今までに <b>%s</b> 個のセグメントから人々から守りました - %1$s 時間 %2$s 分 - %1$s 分 %2$s 秒 - %s 秒 - 合計で人々の人生を <b>%s</b> 無駄にせずに済みました。<br>リーダーボードを表示するには、ここをタップしてください。 - グローバルの統計およびトップの貢献者を表示するには、ここをタップしてください。 - SponsorBlock リーダーボード - SponsorBlock は、YouTube の動画の迷惑な部分をスキップするためのクラウドソーシングシステムです。 - 今まで <b>%s</b> 個のセグメントをスキップしました。 - スキップしたセグメントの回数をリセットしますか? - 合計 <b>%s</b> です。 - 今までに <b>%s</b> 個のセグメントを作成しました。 - セグメントを表示するにはここをタップしてください。 - あなたのユーザー名: <b>%s</b> - ここをタップしてユーザー名を変更 - ユーザー名を変更できませんでした。 ステータス: %1$d %2$s - ユーザー名は正常に変更されました。 - セグメントを送信できません。\n既に存在します。 - セグメントを送信できません: %s - セグメントを送信できません: %s - セグメントを送信できません\nレート制限 (同じユーザー / IP からの送信が多すぎます) - SponsorBlock は一時的に停止しています。 - セグメントを送信できません (ステータス: %1$d %2$s) - セグメントは正常に送信されました - SponsorBlock が利用できない場合、トーストを表示します。 - SponsorBlock が利用できない場合、トーストを表示します。 - API が利用できない場合にトーストを表示 - カテゴリーを変更 - 反対 - セグメントの評価を送信できません: %s - セグメントを評価できません (API がタイムアウトしました) - セグメントの評価を送信できません (ステータス: %1$d %2$s) - 評価できるセグメントがありません - 賛成 - 設定をクリップボードにコピーしました。 - タイムスタンプをクリップボードにコピーしました。 (%s) - URL をクリップボードにコピーしました。 - タイムスタンプ付きの URL をクリップボードにコピーしました。 - オリジナル - いいね! - いいね!(Cairo) - ハート(白色) - ハート(赤色) - 非表示 - ダブルタップ時のアニメーション - メタパネルの下部の余白は 0 ~ 64 の間でなければなりません。デフォルト値にリセットします。 - シークバーからメタパネルまでの間隔を 0 ~ 64 の間で設定できます。 - メタパネルの下部の余白 - 高さは 0~100 (%) の間でなければなりません。 - ナビゲーションバーが非表示になっている際に残るスペースの高さを 0~100 (%) の間で設定します。 - スペースの高さを調整 - タイムスタンプを長押しすると、ショートのリピート状態を変更できます。 - タイムスタンプ長押し時の動作 - "全画面表示時にタイトルを表示します。 - -注意: 動画のタイトルをタップすると消えます。" - タイトル欄を表示 - 自動再生がオンの場合、次の動画をカウントダウンなしで再生します。 - 自動再生がオンの場合、次の動画をカウントダウンなしで再生します。 - 自動再生カウントダウンをスキップ - "動画開始時に予め読み込まれたバッファをスキップして、デフォルトの画質強制の遅延を回避します。 - -• 動画開始時に約 0.3 秒の遅延が発生しますが、デフォルトの画質はすぐに適用されます。 -• HDR 動画、ライブ配信、15 秒未満の動画には適用されません。" - プリロードバッファをスキップ - スキップ時にトーストを表示します。 - スキップ時にトーストを表示します。 - スキップ時にトーストを表示 - この設定をオンにした場合、バッファリングの問題が発生する可能性があります。 - プリロードバッファをスキップしました。 - 再生速度のオーバーレイの値は 0 ~ 8.0 の間でなければなりません。デフォルト値にリセットします。 - 再生速度のオーバーレイの値は 0 ~ 8.0 の間です。 - 再生速度のオーバーレイの値 - "YouTube のバージョンを古いバージョンに偽装します。 - -• アプリの外観が変わりますが、未知の問題が発生する場合があります。 -• 後からこの機能を無効にしても、アプリのデータを消去するまで古い UI のままになる場合があります。" - アプリのバージョンは偽装されていません。 - アプリのバージョンは偽装されています。 - 17.33.42 - 古い UI レイアウトを復元 - 17.41.37 - 古い再生リスト欄を復元 - 18.05.40 - 古いコメント入力欄を復元 - 18.17.43 - 古いプレーヤーフライアウトパネルを復元 - 18.33.40 - ショートの古いアクションバーを復元 - 18.38.45 - 以前のデフォルトの画質の動作を復元 - 18.48.39 - リアルタイムで更新される再生回数と高評価数を無効化 - 19.13.37 - 古いスタイルの数字の回転アニメーションを復元 - 偽装するバージョン - 偽装するバージョンを入力してください。 - 偽装するアプリのバージョンを編集 - アプリのバージョンを偽装 - "アプリのバージョンを以前のバージョンに偽装します。 - -これによりアプリの外観や機能が変更されますが、予期しない副作用が発生する可能性があります。 - -後でこの機能をオフにする場合は、UIのバグを防ぐためにアプリのデータを消去することをお勧めします。" - "デバイスの解像度を最大値に偽装します。 -高画質は、高いデバイスの解像度を必要とする一部の動画でアンロックされる可能性がありますが、すべての動画でアンロックされるわけではありません。" - デバイスの解像度を偽装 - iOS クライアントで AVC コーデック (H.264) を強制します。 - iOS クライアントで AVC コーデック (H.264) を強制します。 - iOS クライアントで AVC (H.264) を強制 - "これを有効にすると、バッテリーの持ちが改善され、再生時のカクつきが修正される可能性があります。 - -注意: \n・AVC コーデック (H.264) の最大解像度は 1080p です。\n・動画の再生には VP9 や AV1 よりも多くの通信量を消費します。" - "・「音声トラック」メニューは表示されません。 -・「一定音量」は使用できません。" - "•「音声トラック」メニューは表示されません。 -•「一定音量」は使用できません。" - "• 映画や有料動画は再生できない場合があります。 -•ライブは最初から再生されます。 -• 動画が 1 秒早く終了する場合があります。 -• Opus オーディオ コーデックは使用できません。" - ストリーミングデータを偽装することによる副作用 - • 動画が再生できない可能性があります。 - 統計情報に偽装したストリーミングデータを表示します。 - 統計情報に偽装したストリーミングデータを表示します。 - 統計情報に偽装したクライアントを表示 - "ストリーミングデータを偽装していない場合、動画の再生ができない可能性があります。" - ストリーミングデータを偽装していない場合、動画の再生ができない可能性があります。 - ストリーミングデータを偽装 - Android - Android TV - Android VR - iOS - 偽装するクライアントの種類 - この設定をオフにした場合、バッファリングの問題が発生する可能性があります。 - 感度は 1 ~ 1000 (%) の間でなければなりません。 - スワイプして明るさを調整する際の感度を 1 ~ 1000 (%) の間で設定できます。 - 明るさのスワイプ感度 - スワイプジェスチャーを「画面のロック」モードで有効化します。 - スワイプジェスチャーを「画面のロック」モードで有効化します。 - 「画面のロック」時のスワイプジェスチャーを有効化 - 自動 - スワイプとして検出する量のしきい値です。 - スワイプ可能な領域のしきい値 - 背景の不透明度の値は 0 ~255 の間で、0 が透明です。 - スワイプオーバーレイの背景の透明度 - スワイプ可能な領域は 50 を超えることはできません。デフォルト値にリセットします。 - スワイプ可能な画面領域の割合です。\n\n注意: ダブルタップでシークするジェスチャーの画面領域のサイズも変化します。 - スワイプオーバーレイの画面サイズ - スワイプオーバーレイのテキストサイズです。 - スワイプオーバーレイのテキストサイズ - スワイプオーバーレイが表示される時間 (単位: ミリ秒) - スワイプオーバーレイのタイムアウト - 感度は 1 ~ 1000 (%) の間でなければなりません。 - スワイプして音量を調整する際の感度を 1 ~ 1000 (%) の間で設定できます。\n\n推奨される音量スワイプ感度は、15 音量ステップで 100%、150 音量ステップで 10% です。 - 音量のスワイプ感度 - "デバイスの情報を偽装して、作成ボタンと通知ボタンの位置を入れ替えます。 -• この設定を変更しても、デバイスを再起動するまで有効にならない場合があります。 -• この設定を無効にすると、サーバーからさらに多くの広告が読み込まれます。 -• 動画広告を表示するには、この設定を無効にする必要があります。" - 「作成」ボタンと「通知」ボタンを入れ替えます。\n\n注意: これを有効にすると、動画広告が強制的に非表示になります。 - "「作成」ボタンと「通知」ボタンを入れ替えます。 - -注意: これを有効にすると、動画広告が強制的に非表示になります。" - 「作成」を「通知」と入れ替え - "これを無効にすると、サーバーからさらに多くの広告が読み込まれる可能性があります。 - -また、ショートで広告がブロックされなくなります。 - -この設定が有効にならない場合は、シークレットモードに切り替えてみてください。" - オリジナル - RVX Music - %s はインストールされていません。インストールしてください。 - インストールされている RVX Music のパッケージ名です。 - RVX Music のパッケージ名 - • 再生履歴をブロックします。 - "• Google アカウントの再生履歴の設定に従います。 -• DNS や VPN が原因で再生履歴が動作しない可能性があります。" - • Google アカウントの再生履歴の設定に従います。 - 再生履歴のステータス - タップして YouTube 再生履歴の管理画面を開きます。 - すべての履歴を管理 - オリジナル - ドメインを置換 - 再生履歴をブロック - 再生履歴の種類 - チャンネル「%1$s」を %2$s ホワイトリストに追加できませんでした。 - チャンネル「%1$s」を %2$s ホワイトリストに登録しました。 - ホワイトリストに登録されているチャンネルはありません。 - ホワイトリストに登録されていません。 - チャンネル情報の読み込みに失敗しました。 - ホワイトリストに登録されています。 - 再生速度 - チャンネル「%1$s」を %2$s ホワイトリストから削除しますか? - チャンネル「%1$s」を %2$s ホワイトリストから削除できませんでした。 - チャンネル「%1$s」を %2$s ホワイトリストから削除しました。 - ホワイトリストに登録したチャンネルのリストを確認/削除します。 - チャンネルのホワイトリスト - SponsorBlock - diff --git a/src/main/resources/youtube/translations/ko-rKR/strings.xml b/src/main/resources/youtube/translations/ko-rKR/strings.xml deleted file mode 100644 index 1a29cf800..000000000 --- a/src/main/resources/youtube/translations/ko-rKR/strings.xml +++ /dev/null @@ -1,1732 +0,0 @@ - - - 플레이어에 접근성 컨트롤을 표시하시겠습니까? - 접근성 서비스가 켜져있기 때문에 플레이어 컨트롤을 변경합니다. - 계속하기 - 다시 보지 않기 - "GmsCore에 백그라운드에서 실행할 수 있는 권한이 없습니다. - -이 기기에 대한 \"Don't kill my app\" 가이드를 읽어보고, GmsCore 설치 지침을 적용하세요. - -앱을 실행하려면 이 과정이 필요합니다." - "GmsCore를 배터리 최적화 목록에서 제외하여 앱 문제를 방지할 수 있습니다. - -배터리 최적화 목록에서 제외하려면 '계속하기' 버튼을 누르세요." - 웹사이트 열기 - 필수 조치 - 알림 수신을 위한 클라우드 메시징 설정을 할 수 있습니다. - GmsCore 열기 - GmsCore가 설치되어 있지 않습니다. 설치하세요. - "DeArrow는 YouTube 동영상에 크라우드 소싱된 썸네일을 제공합니다. 이러한 썸네일은 YouTube에서 제공하는 썸네일보다 관련성이 높은 경우가 많습니다. - -이 설정을 활성화하면 동영상 URL이 API 서버로 전송되며 다른 데이터는 전송되지 않습니다. 동영상에 DeArrow 썸네일이 없는 경우에는 기본 썸네일 또는 스틸 컷 썸네일이 표시됩니다. - -DeArrow에 대해 자세히 알아보려면 여기를 누르세요." - DeArrow에 대한 정보 - 잘못된 DeArrow API URL 입니다. - DeArrow 썸네일 캐시 엔드포인트 URL입니다. 이것이 무슨 역할을 하는지 모르는 경우에는 이 URL을 변경하지 마세요. - DeArrow API 엔드포인트 - DeArrow를 사용할 수 없을 때, 팝업 메시지를 표시하지 않습니다. - DeArrow를 사용할 수 없을 때, 팝업 메시지를 표시합니다. - API를 사용할 수 없을 때, 팝업 메시지 표시하기 - DeArrow를 일시적으로 사용할 수 없습니다. (상태 코드: %s) - DeArrow를 일시적으로 사용할 수 없습니다. - 홈 탭 - 내 페이지 탭 - 기본 썸네일 - DeArrow & 기본 썸네일 - DeArrow & 스틸 컷 썸네일 - 스틸 컷 썸네일 - 플레이어: 재생목록, 관련 동영상 ... - 검색 결과 - 동영상 스틸 컷 썸네일 - 스틸 컷 썸네일은 각 동영상의 시작 / 중간 / 끝 부분에서 캡쳐된 이미지입니다. 이러한 이미지는 YouTube에 내장되어 있으며 외부 API는 사용되지 않습니다. - 스틸 컷 썸네일에 대한 정보 - 고화질 스틸 컷 썸네일을 표시합니다. - 일반화질 스틸 컷 썸네일을 표시합니다. 썸네일을 빠르게 불러오지만 실시간 스트림, 비공개, 오래된 동영상에서는 아무것도 표시되지 않은 썸네일이 표시될 수 있습니다. - 일반화질 스틸 컷 썸네일 표시하기 - 동영상의 시작 부분 이미지 - 동영상의 중간 부분 이미지 - 동영상의 끝 부분 이미지 - 스틸 컷 썸네일에서 표시되는 이미지 - 구독 탭 - 타임스탬프에서 정보를 표시하지 않습니다.\n• 정보: 동영상 화질, 동영상 재생 속도 - "타임스탬프에서 정보를 표시합니다.\n• 정보: 동영상 화질, 동영상 재생 속도" - 타임스탬프에서 정보 표시하기 - 현재 동영상 재생 속도 값을 표시합니다.\n\n동영상을 재생하는 동안에 타임스탬프 정보를 길게 누르면 다른 정보로 빠르게 전환할 수 있습니다. - 현재 동영상 화질 값을 표시합니다.\n\n동영상을 재생하는 동안에 타임스탬프 정보를 길게 누르면 다른 정보로 빠르게 전환할 수 있습니다. - 타임스탬프에서 표시할 정보 설정 - 배터리 절전 모드에서 앰비언트 모드를 비활성화합니다. - 배터리 절전 모드에서 앰비언트 모드를 활성화합니다. - 앰비언트 모드 제한 우회하기 - 이미지를 가져올 대체 도메인을 입력하세요.\n알림: \'https\:\/\/\' 없이 도메인 이름만 입력해야 합니다. - 대체 이미지 도메인 - 기본 이미지 도메인을 사용합니다.\n\n이 설정을 활성화하면 일부 국가에서 차단된 이미지를 수신할 수 있습니다. (채널 프로필 사진, 커뮤니티 게시물 이미지 ...) - 대체 이미지 도메인을 사용합니다.\n\n대체 이미지 도메인 기본값: yt4.ggpht.com - 이미지 표시 제한 국가 우회하기 - 기기 기본값 사용 - - 폰 (최대 너비: 480 dp) - 태블릿 - 태블릿 (최소 너비: 600 dp) - 레이아웃 변경하기 - 스위치 토글으로 표시합니다. - 텍스트 토글으로 표시합니다. - 토글 유형 변경하기 - YouTube 기본 공유 시트를 사용합니다. - Android 기본 공유 시트를 사용합니다.\n\n• 공유 버튼으로 바로 Android 기본 공유 메뉴를 실행할 수 있습니다. - 공유 시트 변경하기 - 자동넘김 - 기본값 - 일시정지 - 반복하기 - Shorts 반복 상태 변경하기 - 채널 둘러보기 - 학습 프로그램 - 홈 (기본값) - 탐색 - 게임 - 기록 - 내 페이지 - 좋아요 표시한 동영상 - 실시간 - 영화 - 음악 - 검색 - Shorts - 스포츠 - 구독 - 인기 급상승 - 나중에 볼 동영상 - 앱 시작 페이지 변경하기 - 앱 시작 페이지가 한 번만 변경됩니다. - "앱 시작 페이지가 항상 변경됩니다. - -알려진 문제점: 툴바에서 '뒤로 가기' 버튼이 작동되지 않을 수 있습니다." - 앱 시작 페이지 유형 변경하기 - 일반 헤더를 활성화합니다. - Premium 헤더를 활성화합니다. - YouTube 헤더 변경하기 - 필터링할 컴포넌트 패스 빌더 문자열을 줄바꿈으로 구분하여 설정합니다.\n• 컴포넌트 패스 빌더 문자열은 숨겨질 레이아웃 구성요소를 식별하는 기술적인 이름입니다. - 사용자 정의 필터 편집하기 - 사용자 정의 필터를 비활성화합니다. - 사용자 정의 필터를 활성화합니다. - 사용자 정의 필터 활성화하기 - 잘못된 필터 값입니다: %s - 이전 메뉴 구성요소를 활성화합니다. - 사용자 정의 다이얼로그를 활성화합니다. - 사용자 정의 동영상 재생 속도 메뉴 유형 설정 - 사용자 정의 재생 속도는 %s 배속보다 작아야 합니다. - 잘못된 재생 속도 값입니다. - 사용하고 싶은 동영상 재생 속도 값을 추가 또는 변경할 수 있습니다. - 사용자 정의 동영상 재생 속도 편집하기 - 플레이어 오버레이 불투명도 값은 0-100 사이어야 합니다. - 불투명도 값은 0-100 사이이며, 0은 투명입니다. - 사용자 정의 플레이어 오버레이 불투명도 - 재생바 색상의 헥스 코드를 입력하세요. - 사용자 정의 재생바 색상 설정 - 다른 앱에서 YouTube 링크를 ReVanced Extended로 열려면 \'지원되는 링크 열기\'를 활성화하고 지원되는 링크를 추가하세요. 링크 추가가 잠겨있다면 순정 YouTube 앱 정보 → \'기본적으로 열기\'에서 \'지원되는 링크 열기\'를 비활성화한 후에 추가할 수 있습니다. - 기본 앱 설정 열기 - 기본 동영상 재생 속도 - 모바일 네트워크 이용 시 기본 동영상 화질 - Wi-Fi 이용 시 기본 동영상 화질 - 전체 화면에서만 앰비언트 모드를 비활성화합니다. - 전체 화면에서 앰비언트 모드를 활성화합니다. - 전체 화면에서 앰비언트 모드를 비활성화합니다. - 전체 화면에서 앰비언트 모드 비활성화하기 - 앰비언트 모드를 비활성화합니다. - 앰비언트 모드를 활성화합니다. - 앰비언트 모드를 비활성화합니다. - 앱비언트 모드 비활성화하기 - 오디오 트랙 사용이 강제된 동영상에서 오디오 트랙을 활성화합니다. - 오디오 트랙 사용이 강제된 동영상에서 오디오 트랙을 비활성화합니다. - 자동 오디오 트랙 비활성화하기 - 자막 사용이 강제된 동영상에서 자막을 활성화합니다. - 자막 사용이 강제된 동영상에서 자막을 비활성화합니다. - 자동 자막 비활성화하기 - 자동 플레이어 팝업 패널을 활성화합니다.\n• 재생목록, 실시간 채팅, 제품 패널 ... - 자동 플레이어 팝업 패널을 비활성화합니다.\n• 재생목록, 실시간 채팅, 제품 패널 ... - 플레이어 팝업 패널 비활성화하기 - "자동재생이 켜져 있으면 믹스 재생목록 자동전환을 활성화합니다. - -자동재생은 YouTube 설정에서 변경할 수 있습니다: -설정 → 자동재생 → 다음 동영상 자동재생" - 믹스 재생목록 자동전환을 비활성화합니다. - 믹스 재생목록 전환 비활성화하기 - 이 설정을 활성화하면 자동재생이 켜져 있는 동안에 음악 동영상을 재생하면 YouTube 믹스 재생목록으로 자동전환되지 않습니다. - 실시간 스트림에서 기본 동영상 재생 속도를 활성화합니다. - 실시간 스트림에서 기본 동영상 재생 속도를 비활성화합니다. - 실시간 스트림에서 기본 동영상 재생 속도 비활성화하기 - 음악 동영상에서 기본 동영상 재생 속도를 활성화합니다. - "음악 동영상에서 기본 동영상 재생 속도를 비활성화합니다. - -알려진 문제점: 이 설정은 'YouTube Music에서 감상하기' 배너가 포함되지 않은 동영상에는 적용되지 않을 수 있습니다." - 음악에서 기본 동영상 재생 속도 비활성화하기 - 참여 패널을 활성화합니다. - 참여 패널을 비활성화합니다. - 참여 패널 비활성화하기 - 진동 피드백을 활성화합니다. - 진동 피드백을 비활성화합니다. - 챕터를 탐색할 때, 진동 피드백 비활성화하기 - 진동 피드백을 활성화합니다. - 진동 피드백을 비활성화합니다. - 위로 스와이프할 때, 진동 피드백 비활성화하기 - 진동 피드백을 활성화합니다. - 진동 피드백을 비활성화합니다. - 탐색할 때, 진동 피드백 비활성화하기 - 진동 피드백을 활성화합니다. - 진동 피드백을 비활성화합니다. - 탐색을 취소할 때, 진동 피드백 비활성화하기 - 진동 피드백을 활성화합니다. - 진동 피드백을 비활성화합니다. - 동영상을 확대할 때, 진동 피드백 비활성화하기 - HDR 자동 밝기를 활성화합니다. - HDR 자동 밝기를 비활성화합니다. - HDR 자동 밝기 비활성화하기 - HDR 동영상을 활성화합니다. - HDR 동영상을 비활성화합니다. - HDR 동영상 비활성화하기 - 전체 화면에서 동영상의 방향을 기기의 설정에 따라 활성화합니다. - 전체 화면에서 동영상의 방향을 세로 모드로 활성화합니다. - 전체 화면에서 세로 모드 활성화하기 - 동영상에서 \'Like (좋아요)\' 버튼이 언급되었을 때, 해당 버튼에 빛나는 애니메이션을 적용합니다.\n• 일부 언어는 아직 지원되지 않습니다. - 동영상에서 \'Like (좋아요)\' 버튼이 언급되었을 때, 해당 버튼에 빛나는 애니메이션을 적용하지 않습니다.\n• 일부 언어는 아직 지원되지 않습니다. - 빛나는 \'좋아요 & 싫어요\' 버튼 비활성화하기 - "QUIC 프로토콜을 비활성화해서 동영상을 불러올 때 발생하는 동영상 압축과 푸는 과정을 제거하여 동영상 로딩 속도를 향상시킵니다. 더 많은 모바일 데이터가 사용되오니 주의하세요." - QUIC 프로토콜 비활성화하기 - 앱을 시작할 때, Shorts 플레이어를 다시 실행합니다. - 앱을 시작할 때, Shorts 플레이어를 다시 실행하지 않습니다. - 앱을 시작할 때, Shorts 플레이어 비활성화하기 - 다음 롤링 넘버 애니메이션을 활성화합니다.\n• 조회수, 시청자 수 롤링 애니메이션 (플레이어 하단)\n• 좋아요 수, 조회수 롤링 애니메이션 (동영상 설명) - 다음 롤링 넘버 애니메이션을 비활성화합니다.\n• 조회수, 시청자 수 롤링 애니메이션 (플레이어 하단)\n• 좋아요 수, 조회수 롤링 애니메이션 (동영상 설명) - 롤링 넘버 애니메이션 비활성화하기 - 재생바에서 챕터를 활성화합니다. - 재생바에서 챕터를 비활성화합니다. - 재생바 챕터 비활성화하기 - 좋아요 버튼 상단에 표시되는 애니메이션을 활성화합니다. - 좋아요 버튼 상단에 표시되는 애니메이션을 비활성화합니다. - 좋아요 상단 애니메이션 비활성화하기 - "화면을 길게 눌러서 '2배속 >>'을 비활성화합니다. - -알림: -• 동영상 재생 속도 오버레이를 비활성화하면 이전 레이아웃의 '왼쪽이나 오른쪽으로 슬라이드하여 탐색' 동작이 복원됩니다. -• 이 설정을 비활성화해도 동영상 재생 속도 오버레이가 강제로 활성화되지는 않습니다." - 동영상 재생 속도 오버레이 비활성화하기 - 앱을 시작할 때, 스플래시 애니메이션을 활성화합니다. - 앱을 시작할 때, 스플래시 애니메이션을 비활성화합니다. - 스플래시 애니메이션 비활성화하기 - "동영상 설명이 펼쳐질 때, 다음 상호 작용을 비활성화합니다: - -• 눌러서 스크롤하기 -• 길게 눌러서 텍스트 선택하기" - 동영상 설명 상호 작용 비활성화하기 - VP9 코덱을 활성화합니다.\n• 예전에 업로드된 동영상에서 일부 화질 값들이 제거되어 360p와 1080p(Premium 기능)만 선택할 수 있거나 화질 메뉴를 선택할 수 없을 수 있습니다. - "VP9 코덱을 비활성화합니다. -• 재생 문제가 없는 계정이거나 iOS 클라이언트만 AV1 코덱을 지원하고 나머지 클라이언트는 VP9 코덱까지만 지원하기 때문에 iOS만 4K 동영상까지 재생될 수 있고, 나머지는 1080p까지 재생될 수 있습니다. -• AVC 코덱 동영상을 재생했을 경우에는 VP9보다 더 많은 데이터가 사용됩니다. -• HDR 동영상을 재생하기 위해 HDR 동영상에서는 VP9 또는 AV1 코덱이 사용됩니다." - VP9 코덱 비활성화하기 - Cairo 재생바를 비활성화합니다.\n• 그라데이션 색상 재생바 - "Cairo 재생바를 활성화합니다. -• 그라데이션 색상 재생바 - -알려진 문제점: -• Cairo 테마가 알림 표시 점에도 적용됩니다." - Cairo 재생바 활성화하기 - 전체 화면에서 컨트롤 오버레이를 작게 표시하지 않습니다. - 전체 화면에서 컨트롤 오버레이를 작게 표시합니다. - 컴팩트 컨트롤 오버레이 활성화하기 - 사용자 정의 동영상 재생 속도를 비활성화합니다. - 사용자 정의 동영상 재생 속도를 활성화합니다. - 사용자 정의 동영상 재생 속도 활성화하기 - 사용자 정의 재생바 색상을 비활성화합니다. - 사용자 정의 재생바 색상을 활성화합니다. - 사용자 정의 재생바 색상 활성화하기 - 디버그 로그에 버퍼를 포함하지 않습니다. - 디버그 로그에 버퍼를 포함합니다. - 디버그 버퍼 로깅 활성화하기 - 디버그 로그를 출력하지 않습니다. - 디버그 로그를 출력합니다. - 디버그 로깅 활성화하기 - Shorts에서 기본 동영상 재생 속도를 비활성화합니다. - Shorts에서 기본 동영상 재생 속도를 활성화합니다. - Shorts에서 기본 동영상 재생 속도 활성화하기 - 앱 내에서 외부 링크를 열 때, 내부 브라우저를 사용합니다. - 앱 내에서 외부 링크를 열 때, 외부 브라우저를 사용합니다. - 외부 브라우저 사용하기 - 그라데이션 색상 로딩 화면을 비활성화합니다. - 그라데이션 색상 로딩 화면을 활성화합니다. - 그라데이션 색상 로딩 화면 활성화하기 - 하단바 버튼 사이의 간격이 좁아지지 않습니다. - 하단바 버튼 사이의 간격이 좁아집니다. - 좁은 하단바 버튼 활성화하기 - 앱 내에서 외부 링크를 열 때, URL 리다이렉션(youtube.com/redirect)을 거쳐서 연결됩니다. - 앱 내에서 외부 링크를 열 때, URL 리다이렉션(youtube.com/redirect)을 거치지 않고 다이렉트로 연결됩니다. - 리다이렉션 없이 링크 바로 열기 - 플레이어 응답에 OPUS 코덱이 포함된 경우에는 OPUS 코덱을 활성화합니다. - OPUS 코덱 활성화하기 - 전체 화면에서 나가거나 들어갈 때마다 화면 밝기 값을 저장 및 복원하지 않습니다. - 전체 화면에서 나가거나 들어갈 때마다 화면 밝기 값을 저장 및 복원합니다. - 화면 밝기 값 저장 및 복원 활성화하기 - 재생바 터치 조작을 비활성화합니다. - 재생바 터치 조작을 활성화합니다. - 재생바 터치 조작 활성화하기 - "이 설정을 활성화하면 재생바 썸네일이 없는 실시간 스트림에서 썸네일이 복원됩니다. - -재생바 썸네일이 표시되기 전에 약간의 지연이 발생하고, 더 많은 모바일 데이터가 사용되오니 주의하세요. - -이 설정은 인터넷 연결이 매우 좋을 때 가장 잘 작동합니다." - 재생바 썸네일이 일반화질입니다, - 재생바 썸네일이 고화질입니다. - 고화질 썸네일 활성화하기 - 타임스탬프를 비활성화합니다. - "타임스탬프를 활성화합니다. - -알려진 문제점: -• Shorts 플레이어 배경을 누르면 모든 구성요소들이 숨겨졌다가 다시 누르면 다시 표시됩니다. -• 이 기능은 Google에서 개발 단계에 있는 기능이므로 레이아웃이 깨질 수 있습니다." - 타임스탬프 활성화하기 - 스와이프 제스처로 밝기 조절을 비활성화합니다. - 스와이프 제스처로 밝기 조절을 활성화합니다. - 스와이프 제스처로 밝기 조절 활성화하기 - 진동 피드백을 비활성화합니다. - 진동 피드백을 활성화합니다. - 길게 눌러서 스와이프 제스처를 사용할 때, 진동 피드백 활성화하기 - 스와이프 제스처로 밝기가 0이 되면 자동 밝기를 활성화하지 않습니다. - 스와이프 제스처로 밝기가 0이 되면 자동 밝기를 활성화합니다. - 스와이프 제스처로 자동 밝기 활성화하기 - 화면을 짧게 눌러서 스와이프 제스처를 사용합니다. - 화면을 길게 눌러서 스와이프 제스처를 사용합니다. - 길게 눌러서 스와이프 제스처 사용하기 - 전체 화면에서 스와이프 제스처로 다음/이전 동영상으로 전환하지 않습니다. - 전체 화면에서 스와이프 제스처로 다음/이전 동영상으로 전환합니다. - 스와이프 제스처로 동영상 전환 활성화하기 - 스와이프 제스처로 볼륨 조절을 비활성화합니다. - 스와이프 제스처로 볼륨 조절을 활성화합니다. - 스와이프 제스처로 볼륨 조절 활성화하기 - 불투명 하단바를 활성화합니다. - 반투명 하단바를 활성화합니다. - 반투명 하단바 활성화하기 - 동영상 플레이어 하단에서 아래로 스와이프하여 전체 화면으로 전환하지 않습니다. - 동영상 플레이어 하단에서 아래로 스와이프하여 전체 화면으로 전환합니다. - 시청 패널 제스처 활성화하기 - "이 설정을 활성화하면 내 페이지에서 설정 버튼이 비활성화됩니다. - -이 경우에 설정 메뉴를 보려면 다음 경로를 사용하세요: -내 페이지 → 채널 보기 → 메뉴 더보기 (툴바) → 설정" - 내 페이지에서 넓은 검색창 활성화하기 - 넓은 검색창을 비활성화합니다. - 넓은 검색창을 활성화합니다. - 넓은 검색창 활성화하기 - YouTube 헤더가 없는 넓은 검색창 - YouTube 헤더가 있는 넓은 검색창 - 헤더가 있는 넓은 검색창 활성화하기 - 설명 - "동영상 설명 패널의 제목을 사용자의 언어로 입력하세요. -입력한 문자열이 동영상 설명 패널 제목과 일치하지 않으면 '동영상 설명 펼치기'가 작동되지 않을 수 있습니다. " - 동영상 설명 패널 제목 - 동영상 설명이 수동으로 펼쳐집니다. - 동영상 설명이 자동으로 펼쳐집니다. - 동영상 설명 펼치기 - 계속하시겠습니까? - 기본값으로 초기화합니다. - 레이아웃을 정상적으로 불러오기 위해 다시 시작합니다. - "일부 사용자에게 좋아요, 조회수 및 업로드 날짜와 같은 롤링 넘버 텍스트가 숨겨지는 YouTube 서버 측 문제가 있습니다. - -이 문제에 대한 임시 해결 방법은 앱 버전을 19.13.37로 변경하는 것입니다. - -앱을 다시 시작하기 전에 앱 버전을 변경하시겠습니까?" - 새로고침 및 다시 시작 - 설정을 내보낼 수 없습니다. - 설정을 성공적으로 내보냈습니다. - 설정을 파일로 내보낼 수 있습니다. - 설정 내보내기 - 가져오기 - 복사하기 - 설정을 텍스트로 가져오거나 내보낼 수 있습니다. - 텍스트로 가져오기 / 내보내기 - 설정을 가져올 수 없습니다. - 설정을 기본값으로 초기화합니다. - 설정을 성공적으로 가져왔습니다. - 설정을 저장된 파일에서 가져올 수 있습니다. - 설정 가져오기 - 초기화 - %s 검색 - ReVanced Extended 설정 - 외부 다운로더 앱 - 설치되어 있지 않습니다. - "%1$s 가 설치되어 있지 않습니다. -웹사이트에서 %2$s 를 다운로드하세요." - 경고 - %s가 설치되지 않았습니다. 설치하세요. - YTDLnis와 같은 설치된 외부 다운로더 앱 패키지명을 설정하세요. - 재생목록 외부 다운로더 앱 패키지명 - 길게 눌러서 실행할 NewPipe 또는 YTDLnis와 같은 설치된 외부 다운로더 앱 패키지명을 설정하세요. - 길게 눌러서 동영상 외부 다운로더 앱 패키지명 - NewPipe 또는 YTDLnis와 같은 설치된 외부 다운로더 앱 패키지명을 설정하세요. - 동영상 외부 다운로더 앱 패키지명 - " -다음과 같은 상황에서 동영상이 전체 화면으로 전환됩니다: - -• 동영상이 시작되었을 경우. -• 동영상 설명에서 챕터를 선택했을 경우. -• 댓글 내용에서 타임스탬프를 눌렀을 경우." - 전체 화면 강제 전환하기 - 앱을 시작할 때마다 GmsCore에 대한 배터리 최적화 다이얼로그를 표시합니다. - GmsCore 배터리 최적화 다이얼로그 표시하기 - 필터링할 계정 메뉴 이름을 줄바꿈으로 구분하여 설정합니다. - 계정 메뉴 필터 편집하기 - "계정 메뉴 및 내 페이지에서 구성요소가 숨겨집니다. -일부 구성요소는 숨겨지지 않을 수 있습니다." - 계정 메뉴 숨기기 - AI-generated video summary 섹션이 표시됩니다. - AI-generated video summary 섹션이 숨겨집니다. - AI-generated video summary 섹션 숨기기 - 검색 결과에서 음악 앨범 카드가 표시됩니다. - 검색 결과에서 음악 앨범 카드가 숨겨집니다. - 음악 앨범 카드 숨기기 - 게임 섹션, 음악 섹션 그리고 동영상 속 장소 섹션이 표시됩니다. - 게임 섹션, 음악 섹션 그리고 동영상 속 장소 섹션이 숨겨집니다. - 속성 섹션 숨기기 - 자동재생 미리보기 컨테이너가 표시됩니다. - 자동재생 미리보기 컨테이너가 숨겨집니다. - 자동재생 미리보기 컨테이너 숨기기 - 스토어 방문 버튼이 표시됩니다. - 스토어 방문 버튼이 숨겨집니다. - 스토어 방문 버튼 숨기기 - "다음 선반들이 숨겨집니다: -• 다시 듣기 -• 다시 시청하기 -• 이어서 시청하기 -• 채널 더보기 -• 이 게임 더보기 -• 주요 뉴스, 뉴스 속보 -• 맞춤 실시간 스트림 -• 라이브 쇼핑 -• 보건 정보 출처 ..." - 좌우 슬라이드형 선반 숨기기 - 피드에서 카테고리 바가 표시됩니다. - 피드에서 카테고리 바가 숨겨집니다. - 피드에서 카테고리 바 숨기기 - 관련 동영상에서 카테고리 바가 표시됩니다. - 관련 동영상에서 카테고리 바가 숨겨집니다. - 관련 동영상에서 카테고리 바 숨기기 - 검색 결과에서 카테고리 바가 표시됩니다. - 검색 결과에서 카테고리 바가 숨겨집니다. - 검색 결과에서 카테고리 바 숨기기 - 커뮤니티 가이드라인이 표시됩니다. - 커뮤니티 가이드라인이 숨겨집니다. - 커뮤니티 가이드라인 숨기기 - 채널 회원 선반이 표시됩니다. - 채널 회원 선반이 숨겨집니다. - 채널 회원 선반 숨기기 - 채널 프로필 상단에서 링크가 표시됩니다. - 채널 프로필 상단에서 링크가 숨겨집니다. - 채널 프로필 상단에서 링크 숨기기 - "Shorts -재생목록 -스토어" - 필터링할 채널 탭 이름을 줄바꿈으로 구분하여 설정합니다. - 채널 탭 필터 - 채널 탭 필터를 비활성화합니다. - 채널 탭 필터를 활성화합니다. - 채널 탭 필터 활성화하기 - 동영상 하단에서 채널 워터마크가 표시됩니다. - 동영상 하단에서 채널 워터마크가 숨겨집니다. - 동영상 하단에서 채널 워터마크 숨기기 - 챕터 섹션이 표시됩니다. - 챕터 섹션이 숨겨집니다. - 챕터 섹션 숨기기 - 더 많은 주제 탐색 선반이 표시됩니다. - 더 많은 주제 탐색 선반이 숨겨집니다. - 더 많은 주제 탐색 선반 숨기기 - 클립 버튼이 표시됩니다. - 클립 버튼이 숨겨집니다. - 클립 버튼 숨기기 - Shorts 만들기 버튼이 표시됩니다. - Shorts 만들기 버튼이 숨겨집니다. - Shorts 만들기 버튼 숨기기 - 강조 표시된 검색 링크가 표시됩니다.\n• 돋보기 마크가 있는 파란색 강조 글씨 - 강조 표시된 검색 링크가 숨겨집니다.\n• 돋보기 마크가 있는 파란색 강조 글씨 - 강조 표시된 검색 링크 숨기기 - Thanks 버튼이 표시됩니다. - Thanks 버튼이 숨겨집니다. - Thanks 버튼 숨기기 - 타임스탬프 & 이모지 버튼이 표시됩니다. - 타임스탬프 & 이모지 버튼이 숨겨집니다. - 타임스탬프 & 이모지 버튼 숨기기 - 회원별 댓글 배너가 표시됩니다. - 회원별 댓글 배너가 숨겨집니다. - 회원별 댓글 배너 숨기기 - 홈 피드에서 댓글 섹션이 표시됩니다. - 홈 피드에서 댓글 섹션이 숨겨집니다. - 홈 피드에서 댓글 섹션 숨기기 - 댓글 섹션이 표시됩니다. - 댓글 섹션이 숨겨집니다. - 댓글 섹션 숨기기 - 채널에서 커뮤니티 게시물이 표시됩니다. - 채널에서 커뮤니티 게시물이 숨겨집니다. - 채널에서 커뮤니티 게시물 숨기기 - 홈 피드 및 관련 동영상에서 커뮤니티 게시물이 표시됩니다. - 홈 피드 및 관련 동영상에서 커뮤니티 게시물이 숨겨집니다. - 홈 피드 및 관련 동영상에서 커뮤니티 게시물 숨기기 - 구독 피드에서 커뮤니티 게시물이 표시됩니다. - 구독 피드에서 커뮤니티 게시물이 숨겨집니다. - 구독 피드에서 커뮤니티 게시물 숨기기 - 콘텐츠 생성 방식 섹션이 표시됩니다. - 콘텐츠 생성 방식 섹션이 숨겨집니다. - 콘텐츠 생성 방식 섹션 숨기기 - 플레이어 하단에서 크라우드 펀딩 박스가 표시됩니다. - 플레이어 하단에서 크라우드 펀딩 박스가 숨겨집니다. - 크라우드 펀딩 박스 숨기기 - 두 번 누르기 오버레이 필터가 표시됩니다. - 두 번 누르기 오버레이 필터가 숨겨집니다. - 두 번 누르기 오버레이 필터 숨기기 - 오프라인 저장 버튼이 표시됩니다. - 오프라인 저장 버튼이 숨겨집니다. - 오프라인 저장 버튼 숨기기 - 최종 화면 카드가 표시됩니다. - 최종 화면 카드가 숨겨집니다. - 최종 화면 카드 숨기기 - 썸네일 하단에서 다음 정보들이 표시됩니다:\n동영상 설명, 챕터, 주요 순간, 스크립트,\n재생목록의 동영상, 이 동영상에 나온 제품 ... - 썸네일 하단에서 다음 정보들이 숨겨집니다:\n동영상 설명, 챕터, 주요 순간, 스크립트,\n재생목록의 동영상, 이 동영상에 나온 제품 ... - 펼쳐볼 수 있는 정보 숨기기 - 다음 선반이 표시됩니다:\n좋아하는 장르 선택 선반 - 다음 선반이 숨겨집니다:\n좋아하는 장르 선택 선반 - 펼쳐볼 수 있는 선반 숨기기 - 자막 버튼이 표시됩니다. - 자막 버튼이 숨겨집니다. - 피드 자막 버튼 숨기기 - 필터링할 메뉴 구성요소 이름을 줄바꿈으로 구분하여 설정합니다. - 피드 메뉴 구성요소 필터 - 피드 메뉴 구성요소 필터를 비활성화합니다. - 피드 메뉴 구성요소 필터를 활성화합니다. - 피드 메뉴 구성요소 필터 활성화하기 - 피드 검색창이 표시됩니다. - 피드 검색창이 숨겨집니다. - 피드 검색창 숨기기 - 피드 설문 조사가 표시됩니다. - 피드 설문 조사가 숨겨집니다. - 피드 설문 조사 숨기기 - 필름 스트립 오버레이가 표시됩니다.\n• -세밀하게 보면서 탐색 제스처 - 필름 스트립 오버레이가 숨겨집니다.\n• 세밀하게 보면서 탐색 제스처 - 필름 스트립 오버레이 숨기기 - 플로팅 버튼이 표시됩니다. - 플로팅 버튼이 숨겨집니다. - 플로팅 버튼 숨기기 - 검색할 때, 오른쪽 하단에서 플로팅 마이크 버튼이 표시됩니다. - 검색할 때, 오른쪽 하단에서 플로팅 마이크 버튼이 숨겨집니다. - 플로팅 마이크 버튼 숨기기 - 추천 선반이 표시됩니다. - 추천 선반이 숨겨집니다. - 추천 선반 숨기기 - 전체 화면 광고가 표시됩니다. - 전체 화면 광고가 숨겨집니다. - 전체 화면 광고 숨기기 - "전체 화면 광고가 차단됩니다. - -알려진 문제점: 전체 화면에서 커뮤니티 게시물 이미지가 차단될 수 있습니다." - 닫기 버튼을 누르면 전체 화면 광고가 닫혀집니다. - 전체 화면 광고 닫기 - 일반 레이아웃 광고가 표시됩니다. - 일반 레이아웃 광고가 숨겨집니다. - 일반 레이아웃 광고 숨기기 - YouTube Premium 프로모션이 표시됩니다. - YouTube Premium 광고가 숨겨집니다. - YouTube Premium 프로모션 숨기기 - 동영상들 사이에서 회색 구분선이 표시됩니다. - 동영상들 사이에서 회색 구분선이 숨겨집니다. - 회색 구분선 숨기기 - 핸들(@사용자 아이디)이 표시됩니다. - 핸들(@사용자 아이디)이 숨겨집니다. - 핸들(@사용자 아이디) 숨기기 - Google 렌즈 버튼이 표시됩니다. - Google 렌즈 버튼이 숨겨집니다. - Google 렌즈 버튼 숨기기 - 이미지 선반이 표시됩니다. - 이미지 선반이 숨겨집니다. - 이미지 선반 숨기기 - 크리에이터 정보 카드 섹션이 표시됩니다. - 크리에이터 정보 카드 섹션이 숨겨집니다. - 크리에이터 정보 카드 섹션 숨기기 - 정보 카드가 표시됩니다. - 정보 카드가 숨겨집니다. - 정보 카드 숨기기 - 정보 패널이 표시됩니다. - 정보 패널이 숨겨집니다. - 정보 패널 숨기기 - 가입 버튼이 표시됩니다. - 가입 버튼이 숨겨집니다. - 가입 버튼 숨기기 - 주요 개념 섹션이 표시됩니다. - 주요 개념 섹션이 숨겨집니다. - 주요 개념 섹션 숨기기 - "홈 / 구독 / 검색 결과가 필터링되어 키워드 구문과 일치하는 콘텐츠가 숨겨집니다. - -알려진 문제점: -• 채널 이름으로 Shorts 동영상은 숨길 수 없습니다. -• 일부 화면 구성요소는 숨겨지지 않을 수 있습니다. -• 필터링 키워드를 검색하면 검색 결과가 표시되지 않을 수 있습니다." - 키워드 필터링에 대한 정보 - 필터링할 키워드 및 구문을 큰따옴표로 묶으면 동영상 제목과 채널 이름이 부분적으로 일치하지 않도록 방지할 수 있습니다.<br><br>• 예를 들어, <b>\"ai\"</b>라는 키워드로 <b>AI 커리어 완벽 가이드</b>라는 동영상을 숨길 수 있지만, <b>생성형AI가 바꿔놓은 세계</b> 또는 <b>What does fair use mean?</b>라는 동영상은 숨길 수 없습니다.<br>• 그리고 구두점을 단어의 경계로 간주하기 때문에 <b>인공지능(AI)의 원리</b>라는 동영상은 숨길 수 있습니다. 큰따옴표는 다른 단어 내부의 하위 문자열만 무시합니다 (예: <b>fair</b>는 숨길 수 없지만, <b>f(ai)r</b>는 숨김). - 전체 단어 일치시키기 - 댓글 섹션에서 키워드 필터를 비활성화합니다. - 댓글 섹션에서 키워드 필터를 활성화합니다. - 댓글 섹션에서 키워드 필터 활성화하기 - 홈 피드에서 키워드 필터를 비활성화합니다. - 홈 피드에서 키워드 필터를 활성화합니다. - 홈 피드에서 키워드 필터 활성화하기 - "필터링할 키워드 및 구문을 줄바꿈으로 구분하여 설정합니다. - -• 필터링 키워드는 채널 이름 또는 동영상 제목에 표시되는 모든 텍스트가 될 수 있습니다. -• 가운데 대문자가 있는 단어는 대소문자를 함께 입력해야 합니다 (예: iPhone, TikTok, LeBlanc)." - 키워드 필터 - 검색 결과에서 키워드 필터를 비활성화합니다. - 검색 결과에서 키워드 필터를 활성화합니다. - 검색 결과에서 키워드 필터 활성화하기 - 구독 피드에서 키워드 필터를 비활성화합니다. - 구독 피드에서 키워드 필터를 활성화합니다. - 구독 피드에서 키워드 필터 활성화하기 - 키워드가 모든 동영상을 숨깁니다: %s - 키워드를 사용할 수 없습니다: %s - 따옴표를 추가하여 키워드를 사용합니다: %s - 키워드에 충돌하는 선언이 있습니다: %s - 키워드가 너무 짧아서 따옴표가 필요합니다: %s - 최신 게시물이 표시됩니다. - 최신 게시물이 숨겨집니다. - 최신 게시물 숨기기 - 피드 상단에서 최신 동영상 버튼이 표시됩니다. - 피드 상단에서 최신 동영상 버튼이 숨겨집니다. - 최신 동영상 버튼 숨기기 - 좋아요 & 싫어요 버튼이 표시됩니다. - 좋아요 & 싫어요 버튼이 숨겨집니다. - 좋아요 & 싫어요 버튼 숨기기 - 실시간 채팅 메시지가 표시됩니다.\n\n이 설정은 Shorts 실시간 스트림에도 적용됩니다. - 실시간 채팅 메시지가 숨겨집니다.\n\n이 설정은 Shorts 실시간 스트림에도 적용됩니다. - 실시간 채팅 메시지 숨기기 - 실시간 채팅 다시보기 버튼이 표시됩니다.\n\n이 버튼은 전체 화면에서 실시간 채팅이 종료되었을 때, 오른쪽 하단에서 표시됩니다. - 실시간 채팅 다시보기 버튼이 숨겨집니다.\n\n이 버튼은 전체 화면에서 실시간 채팅이 종료되었을 때, 오른쪽 하단에서 표시됩니다. - 실시간 채팅 다시보기 버튼 숨기기 - 구독하지 않는 채널에서 업로드한 동영상 중 조회수가 1,000회 미만인 동영상이 홈 피드에서 숨겨집니다. - 조회수가 낮은 추천 동영상 숨기기 - 보건 정보 패널이 표시됩니다. - 보건 정보 패널이 숨겨집니다. - 보건 정보 패널 숨기기 - 매장 쇼핑 선반이 표시됩니다.\n• 크리에이터명 매장 쇼핑 선반 - 매장 쇼핑 선반이 숨겨집니다.\n• 크리에이터명 매장 쇼핑 선반 - 매장 쇼핑 선반 숨기기 - 믹스 재생목록이 표시됩니다. - 믹스 재생목록이 숨겨집니다. - 믹스 재생목록 숨기기 - 영화 선반이 표시됩니다. - 영화 선반이 숨겨집니다. - 영화 선반 숨기기 - 하단바가 표시됩니다. - 하단바가 숨겨집니다. - 하단바 숨기기 - 만들기 버튼이 표시됩니다. - 만들기 버튼이 숨겨집니다. - 만들기 버튼 숨기기 - 홈 버튼이 표시됩니다. - 홈 버튼이 숨겨집니다. - 홈 버튼 숨기기 - 하단바 버튼 라벨이 표시됩니다. - 하단바 버튼 라벨이 숨겨집니다. - 하단바 버튼 라벨 숨기기 - 내 페이지 버튼이 표시됩니다. - 내 페이지 버튼이 숨겨집니다. - 내 페이지 버튼 숨기기 - 알림 버튼이 표시됩니다. - 알림 버튼이 숨겨집니다. - 알림 버튼 숨기기 - Shorts 버튼이 표시됩니다. - Shorts 버튼이 숨겨집니다. - Shorts 버튼 숨기기 - 구독 버튼이 표시됩니다. - 구독 버튼이 숨겨집니다. - 구독 버튼 숨기기 - (게시) 예정 동영상 하단에서 \'알림 받기\' 버튼이 표시됩니다. - (게시) 예정 동영상 하단에서 \'알림 받기\' 버튼이 숨겨집니다. - \'알림 받기\' 버튼 숨기기 - 유료 광고 포함 라벨이 표시됩니다. - 유료 광고 포함 라벨이 숨겨집니다. - 유료 광고 포함 라벨 숨기기 - Playables(게임 룸) 선반이 표시됩니다.\n• YouTube 앱에 내장된 미니 게임\n• 일부 국가에서는 아직 서비스가 제공되지 않습니다. - Playables(게임 룸) 선반이 숨겨집니다.\n• YouTube 앱에 내장된 미니 게임\n• 일부 국가에서는 아직 서비스가 제공되지 않습니다. - Playables(게임 룸) 선반 숨기기 - 자동재생 버튼이 표시됩니다. - 자동재생 버튼이 숨겨집니다. - 자동재생 버튼 숨기기 - 자막 버튼이 표시됩니다. - 자막 버튼이 숨겨집니다. - 자막 버튼 숨기기 - 크롬캐스트 버튼이 표시됩니다. - 크롬캐스트 버튼이 숨겨집니다. - 크롬캐스트 버튼 숨기기 - 플레이어 접기 버튼이 표시됩니다. - 플레이어 접기 버튼이 숨겨집니다. - 플레이어 접기 버튼 숨기기 - 앰비언트 모드 메뉴가 표시됩니다. - 앰비언트 모드 메뉴가 숨겨집니다. - 앰비언트 모드 메뉴 숨기기 - 오디오 트랙 메뉴가 표시됩니다. - 오디오 트랙 메뉴가 숨겨집니다. - 오디오 트랙 메뉴 숨기기 - 자막 설정 메뉴에서 하단 설명이 표시됩니다. - 자막 설정 메뉴에서 하단 설명이 숨겨집니다. - 자막 설정 메뉴에서 하단 설명 숨기기 - 자막 메뉴가 표시됩니다. - 자막 메뉴가 숨겨집니다. - 자막 메뉴 숨기기 - 1080p Premium 메뉴가 표시됩니다. - 1080p Premium 메뉴가 숨겨집니다. - 1080p Premium 메뉴 숨기기 - 고객센터 메뉴가 표시됩니다. - 고객센터 메뉴가 숨겨집니다. - 고객센터 메뉴 숨기기 - YouTube Music으로 음악 감상 메뉴가 표시됩니다. - YouTube Music으로 음악 감상 메뉴가 숨겨집니다. - YouTube Music으로 음악 감상 메뉴 숨기기 - 잠금 화면 메뉴가 표시됩니다. - 잠금 화면 메뉴가 숨겨집니다. - 잠금 화면 메뉴 숨기기 - 동영상 연속 재생 메뉴가 표시됩니다. - 동영상 연속 재생 메뉴가 숨겨집니다. - 동영상 연속 재생 메뉴 숨기기 - 콘텐츠 더보기 메뉴가 표시됩니다. - 콘텐츠 더보기 메뉴가 숨겨집니다. - 콘텐츠 더보기 메뉴 숨기기 - PIP 모드 메뉴가 표시됩니다. - PIP 모드 메뉴가 숨겨집니다. - PIP 모드 메뉴 숨기기 - 재생 속도 메뉴가 표시됩니다. - 재생 속도 메뉴가 숨겨집니다. - 재생 속도 메뉴 숨기기 - Premium 설정 메뉴가 표시됩니다. - Premium 설정 메뉴가 숨겨집니다. - Premium 설정 메뉴 숨기기 - 화질 설정 메뉴에서 하단 설명이 표시됩니다. - 화질 설정 메뉴에서 하단 설명이 숨겨집니다. - 화질 설정 메뉴에서 하단 설명 숨기기 - 화질 설정 메뉴에서 헤더가 표시됩니다. - 화질 설정 메뉴에서 헤더가 숨겨집니다. - 화질 설정 메뉴에서 헤더 숨기기 - 신고 메뉴가 표시됩니다. - 신고 메뉴가 숨겨집니다. - 신고 메뉴 숨기기 - 취침 타이머 메뉴가 표시됩니다. - 취침 타이머 메뉴가 숨겨집니다. - 취침 타이머 메뉴 숨기기 - 안정적인 볼륨 메뉴가 표시됩니다. - 안정적인 볼륨 메뉴가 숨겨집니다. - 안정적인 볼륨 메뉴 숨기기 - 전문 통계 메뉴가 표시됩니다. - 전문 통계 메뉴가 숨겨집니다. - 전문 통계 메뉴 숨기기 - VR로 보기 메뉴가 표시됩니다. - VR로 보기 메뉴가 숨겨집니다. - VR로 보기 메뉴 숨기기 - 전체 화면 버튼이 표시됩니다. - 전체 화면 버튼이 숨겨집니다. - 전체 화면 버튼 숨기기 - 버튼이 표시됩니다. - 버튼이 숨겨집니다. - 이전 & 다음 동영상 버튼 숨기기 - 판매자 쇼핑 선반이 표시됩니다.\n• 판매자(크리에이터명) 선반 - 판매자 쇼핑 선반이 숨겨집니다.\n• 판매자(크리에이터명) 선반 - 판매자 쇼핑 선반 숨기기 - YouTube Music 버튼이 표시됩니다. - YouTube Music 버튼이 숨겨집니다. - YouTube Music 버튼 숨기기 - (재생목록에) 저장 버튼이 표시됩니다. - (재생목록에) 저장 버튼이 숨겨집니다. - (재생목록에) 저장 버튼 숨기기 - 팟캐스트 살펴보기 섹션이 표시됩니다. - 팟캐스트 살펴보기 섹션이 숨겨집니다. - 팟캐스트 살펴보기 섹션 숨기기 - 댓글 미리보기가 표시됩니다. - 댓글 미리보기가 숨겨집니다. - 댓글 미리보기 숨기기 - 댓글 섹션의 크기가 변경되므로 댓글 섹션에서 \'실시간 채팅 다시보기\' 및 \'YouTube Music에서 감상하기\'를 사용할 수 없습니다. - 댓글 섹션의 크기가 변경되지 않으므로 댓글 섹션에서 \'실시간 채팅 다시보기\' 및 \'YouTube Music에서 감상하기\'를 사용할 수 있습니다. - 댓글 미리보기 유형 숨기기 - 프로모션 알림 배너가 표시됩니다. - 프로모션 알림 배너가 숨겨집니다. - 프로모션 알림 배너 숨기기 - 댓글 버튼이 표시됩니다. - 댓글 버튼이 숨겨집니다. - 댓글 버튼 숨기기 - 싫어요 버튼이 표시됩니다. - 싫어요 버튼이 숨겨집니다. - 싫어요 버튼 숨기기 - 좋아요 버튼이 표시됩니다. - 좋아요 버튼이 숨겨집니다. - 좋아요 버튼 숨기기 - 실시간 채팅 버튼이 표시됩니다. - 실시간 채팅 버튼이 숨겨집니다. - 실시간 채팅 버튼 숨기기 - 메뉴 더보기 버튼이 표시됩니다. - 메뉴 더보기 버튼이 숨겨집니다. - 메뉴 더보기 버튼 숨기기 - 믹스 재생목록 열기 버튼이 표시됩니다. - 믹스 재생목록 열기 버튼이 숨겨집니다. - 믹스 재생목록 열기 버튼 숨기기 - 재생목록 열기 버튼이 표시됩니다. - 재생목록 열기 버튼이 숨겨집니다. - 재생목록 열기 버튼 숨기기 - 재생목록에 저장 버튼이 표시됩니다. - 재생목록에 저장 버튼이 숨겨집니다. - 재생목록에 저장 버튼 숨기기 - 공유 버튼이 표시됩니다. - 공유 버튼이 숨겨집니다. - 공유 버튼 숨기기 - 재생바 하단에서 빠른 작업 컨테이너가 표시됩니다. - 재생바 하단에서 빠른 작업 컨테이너가 숨겨집니다. - 빠른 작업 컨테이너 숨기기 - "다음 추천 동영상이 숨겨집니다: -• '회원 전용' 태그가 있는 동영상 -• 썸네일 하단에 '시청자가 이 동영상도 시청함'와 같은 문구가 있는 동영상" - 추천 동영상 숨기기 - 빠른 작업 컨테이너의 \'동영상 더보기\' 섹션과 관련 동영상 오버레이가 표시됩니다. - 빠른 작업 컨테이너의 \'동영상 더보기\' 섹션과 관련 동영상 오버레이가 숨겨집니다. - 관련 동영상 오버레이 숨기기 - 관련 동영상이 표시됩니다. - 관련 동영상이 숨겨집니다. - 관련 동영상 숨기기 - "이 설정은 플레이어 화면에서 로드할 수 있는 최대 레이아웃 수를 제한합니다. - -서버 측 변경으로 인해 플레이어 화면의 레이아웃이 변경되면 플레이어 화면에서 의도하지 않은 레이아웃이 숨겨질 수 있습니다." - 리믹스 버튼이 표시됩니다. - 리믹스 버튼이 숨겨집니다. - 리믹스 버튼 숨기기 - 신고 버튼이 표시됩니다. - 신고 버튼이 숨겨집니다. - 신고 버튼 숨기기 - 리워드 버튼이 표시됩니다. - 리워드 버튼이 숨겨집니다. - 리워드 버튼 숨기기 - 검색어 기록에서 썸네일이 표시됩니다. - 검색어 기록에서 썸네일이 숨겨집니다. - 검색어 썸네일 숨기기 - 다음 탐색 메시지가 표시됩니다.\n• \'왼쪽이나 오른쪽으로 슬라이드하여 탐색\' - 다음 탐색 메시지가 숨겨집니다.\n• \'왼쪽이나 오른쪽으로 슬라이드하여 탐색\' - 탐색 메시지 숨기기 - 탐색 취소 메시지가 표시됩니다.\n• \'손가락을 떼어 취소\' - 탐색 취소 메시지가 숨겨집니다.\n• \'손가락을 떼어 취소\' - 탐색 취소 메시지 숨기기 - 타임스탬프 옆에 표시되는 챕터 라벨이 표시됩니다. - 타임스탬프 옆에 표시되는 챕터 라벨이 숨겨집니다. - 재생바 챕터 라벨 숨기기 - 동영상 플레이어 재생바가 표시됩니다. - 동영상 플레이어 재생바가 숨겨집니다. - 썸네일 재생바가 표시됩니다. - 썸네일 재생바가 숨겨집니다. - 동영상 썸네일 재생바 숨기기 - 동영상 플레이어 재생바 숨기기 - 셀프 스폰서 카드가 표시됩니다. - 셀프 스폰서 카드가 숨겨집니다. - 셀프 스폰서 카드 숨기기 - 정보 메뉴가 표시됩니다. - 정보 메뉴가 숨겨집니다. - 정보 메뉴 숨기기 - 접근성 메뉴가 표시됩니다. - 접근성 메뉴가 숨겨집니다. - 접근성 메뉴 숨기기 - 계정 메뉴가 표시됩니다. - 계정 메뉴가 숨겨집니다. - 계정 메뉴 숨기기 - 자동재생 메뉴가 표시됩니다. - 자동재생 메뉴가 숨겨집니다. - 자동재생 메뉴 숨기기 - 청구 및 결제 메뉴가 표시됩니다. - 청구 및 결제 메뉴가 숨겨집니다. - 청구 및 결제 메뉴 숨기기 - 자막 메뉴가 표시됩니다. - 자막 메뉴가 숨겨집니다. - 자막 메뉴 숨기기 - 연결된 앱 메뉴가 표시됩니다. - 연결된 앱 메뉴가 숨겨집니다. - 연결된 앱 메뉴 숨기기 - 데이터 절약 메뉴가 표시됩니다. - 데이터 절약 메뉴가 숨겨집니다. - 데이터 절약 메뉴 숨기기 - 일반 메뉴가 표시됩니다. - 일반 메뉴가 숨겨집니다. - 일반 메뉴 숨기기 - 전체 기록 관리 메뉴가 표시됩니다. - 전체 기록 관리 메뉴가 숨겨집니다. - 전체 기록 관리 메뉴 숨기기 - 실시간 채팅 메뉴가 표시됩니다. - 실시간 채팅 메뉴가 숨겨집니다. - 실시간 채팅 메뉴 숨기기 - 알림 메뉴가 표시됩니다. - 알림 메뉴가 숨겨집니다. - 알림 메뉴 숨기기 - 백그라운드 메뉴가 표시됩니다. - 백그라운드 메뉴가 숨겨집니다. - 백그라운드 메뉴 숨기기 - \'TV로 시청하기\' 메뉴가 표시됩니다. - \'TV로 시청하기\' 메뉴가 숨겨집니다. - \'TV로 시청하기\' 메뉴 숨기기 - 가족 센터 메뉴가 표시됩니다. - 가족 센터 메뉴가 숨겨집니다. - 가족 센터 메뉴 숨기기 - \'새 실험 기능 사용해 보기\' 메뉴가 표시됩니다. - \'새 실험 기능 사용해 보기\' 메뉴가 숨겨집니다. - \'새 실험 기능 사용해 보기\' 메뉴 숨기기 - 공개 설정 메뉴가 표시됩니다. - 공개 설정 메뉴가 숨겨집니다. - 공개 설정 메뉴 숨기기 - 구매 항목 및 멤버십 메뉴가 표시됩니다. - 구매 항목 및 멤버십 메뉴가 숨겨집니다. - 구매 항목 및 멤버십 메뉴 숨기기 - YouTube 설정 메뉴에서 구성요소를 숨깁니다. - YouTube 설정 메뉴 숨기기 - 동영상 화질 환경설정 메뉴가 표시됩니다. - 동영상 화질 환경설정 메뉴가 숨겨집니다. - 동영상 화질 환경설정 메뉴 숨기기 - YouTube의 내 데이터 메뉴가 표시됩니다. - YouTube의 내 데이터 메뉴가 숨겨집니다. - YouTube의 내 데이터 메뉴 숨기기 - 공유 버튼이 표시됩니다. - 공유 버튼이 숨겨집니다. - 공유 버튼 숨기기 - 쇼핑 버튼이 표시됩니다. - 쇼핑 버튼이 숨겨집니다. - 쇼핑 버튼 숨기기 - 제품 섹션 & 쇼핑 링크가 표시됩니다. - 제품 섹션 & 쇼핑 링크가 숨겨집니다. - 제품 섹션 & 쇼핑 링크 숨기기 - 채널바가 표시됩니다. - 채널바가 숨겨집니다. - 채널바 숨기기 - 댓글 버튼이 표시됩니다. - 댓글 버튼이 숨겨집니다. - 댓글 버튼 숨기기 - \'사용 중지됨\' 또는 \'0\'으로 표시된 댓글 버튼이 표시됩니다. - \'사용 중지됨\' 또는 \'0\'으로 표시된 댓글 버튼이 숨겨집니다. - \'사용 중지됨\' 또는 \'0\'으로 표시된 댓글 버튼 숨기기 - 싫어요 버튼이 표시됩니다. - 싫어요 버튼이 숨겨집니다. - 싫어요 버튼 숨기기 - "'이 사운드 사용'과 같은 플로팅 버튼이 Shorts 채널 탭에서 표시됩니다." - "'이 사운드 사용'과 같은 플로팅 버튼이 Shorts 채널 탭에서 숨겨집니다." - 플로팅 버튼 숨기기 - 동영상 링크 라벨이 표시됩니다. - 동영상 링크 라벨이 숨겨집니다. - FULL 또는 관련 동영상 링크 라벨 숨기기 - 그린 스크린 버튼이 표시됩니다. - 그린 스크린 버튼이 숨겨집니다. - 그린 스크린 버튼 숨기기 - 정보 패널이 표시됩니다. - 정보 패널이 숨겨집니다. - 정보 패널 숨기기 - 가입 버튼이 표시됩니다. - 가입 버튼이 숨겨집니다. - 가입 버튼 숨기기 - 좋아요 버튼이 표시됩니다. - 좋아요 버튼이 숨겨집니다. - 좋아요 버튼 숨기기 - 실시간 채팅 헤더(상단 채널바)가 표시됩니다.\n\n헤더에서 뒤로 가기 버튼은 숨길 수 없습니다. - 실시간 채팅 헤더(상단 채널바)가 숨겨집니다.\n\n헤더에서 뒤로 가기 버튼은 숨길 수 없습니다. - 실시간 채팅 헤더 숨기기 - 위치 버튼이 표시됩니다. - 위치 버튼이 숨겨집니다. - 위치 버튼 숨기기 - 하단바가 표시됩니다. - 하단바가 숨겨집니다. - 하단바 숨기기 - 유료 광고 포함 라벨이 표시됩니다. - 유료 광고 포함 라벨이 숨겨집니다. - 유료 광고 포함 라벨 숨기기 - 일시 정지 헤더(Shorts 헤더)가 표시됩니다. - 일시 정지 헤더(Shorts 헤더)가 숨겨집니다. - 일시 정지 헤더 숨기기 - 플레이어 왼쪽 상단에서 다음 버튼들이 표시됩니다.\n• 구독 & 라이브 & 트렌드 & 쇼핑 - 플레이어 왼쪽 상단에서 다음 버튼들이 숨겨집니다.\n• 구독 & 라이브 & 트렌드 & 쇼핑 - 일시 정지 오버레이 버튼 숨기기 - 버튼 배경이 표시됩니다. - 버튼 배경이 숨겨집니다. - 재생 & 일시 정지 버튼 배경 숨기기 - 리믹스 버튼이 표시됩니다. - 리믹스 버튼이 숨겨집니다. - 리믹스 버튼 숨기기 - (재생목록에) 음악 저장 버튼이 표시됩니다. - (재생목록에) 음악 저장 버튼이 숨겨집니다. - (재생목록에) 음악 저장 버튼 숨기기 - 검색 추천 버튼이 표시됩니다. - 검색 추천 버튼이 숨겨집니다. - 검색 추천 버튼 숨기기 - 공유 버튼이 표시됩니다. - 공유 버튼이 숨겨집니다. - 공유 버튼 숨기기 - 채널에서 표시됩니다. - "채널에서 숨겨집니다. - -알림: -• 홈 탭에 있는 Shorts 헤더 선반만 숨겨집니다." - 채널에서 숨기기 - 시청 기록에서 Shorts 선반이 표시됩니다. - 시청 기록에서 Shorts 선반이 숨겨집니다. - 시청 기록에서 Shorts 선반이 숨기기 - 홈 피드 및 관련 동영상에서 Shorts 선반이 표시됩니다. - 홈 피드 및 관련 동영상에서 Shorts 선반이 숨겨집니다. - 홈 피드 및 관련 동영상에서 Shorts 선반 숨기기 - 검색 결과에서 Shorts 선반이 표시됩니다. - 검색 결과에서 Shorts 선반이 숨겨집니다. - 검색 결과에서 Shorts 선반 숨기기 - 구독 피드에서 Shorts 선반이 표시됩니다. - 구독 피드에서 Shorts 선반이 숨겨집니다. - 구독 피드에서 Shorts 선반 숨기기 - "Shorts 선반이 숨겨집니다. - -알려진 문제점: 검색 결과에서 다음 헤더가 숨겨집니다. -• 아티스트 또는 크리에이터의 최신 동영상 -• 이전에 시청한 동영상 -• 관련 검색어의 검색결과 -• 새로운 맞춤 채널 ..." - Shorts 선반 숨기기 - 플레이어 하단 정보에서 쇼핑 버튼이 표시됩니다. - 플레이어 하단 정보에서 쇼핑 버튼이 숨겨집니다. - 쇼핑 버튼 숨기기 - 일시 정지 오버레이에서 쇼핑 버튼이 표시됩니다. - 일시 정지 오버레이에서 쇼핑 버튼이 숨겨집니다. - 쇼핑 버튼 숨기기 - 사운드 버튼이 표시됩니다. - 사운드 버튼이 숨겨집니다. - 사운드 버튼 숨기기 - 메타데이터 라벨이 표시됩니다. - 메타데이터 라벨이 숨겨집니다. - 사운드 메타데이터 라벨 숨기기 - 스티커가 표시됩니다. - 스티커가 숨겨집니다. - 스티커 숨기기 - 구독 버튼이 표시됩니다. - 구독 버튼이 숨겨집니다. - 구독 버튼 숨기기 - Super Thanks 구매 버튼이 표시됩니다. - Super Thanks 구매 버튼이 숨겨집니다. - Super Thanks 구매 버튼 숨기기 - 태그된 제품이 표시됩니다. - 태그된 제품이 숨겨집니다. - 태그된 제품 숨기기 - 툴바가 표시됩니다. - 툴바가 숨겨집니다. - 툴바 숨기기 - 트렌드 버튼이 숨겨집니다. - 트렌드 버튼이 숨겨집니다. - 트렌드 버튼 숨기기 - 템플릿 사용 버튼이 표시됩니다. - 템플릿 사용 버튼이 숨겨집니다. - 템플릿 사용 버튼 숨기기 - \'이 사운드 사용\' 버튼이 표시됩니다. - \'이 사운드 사용\' 버튼이 숨겨집니다. - \'이 사운드 사용\' 버튼 숨기기 - 제목이 표시됩니다. - 제목이 숨겨집니다. - 동영상 제목 숨기기 - \'자세히 보기\' 버튼이 표시됩니다. - \'자세히 보기\' 버튼이 숨겨집니다. - \'자세히 보기\' 버튼 숨기기 - 스낵바(팝업 메시지바)가 표시됩니다. - 스낵바(팝업 메시지바)가 숨겨집니다. - 스낵바(팝업 메시지바) 숨기기 - \'(Entertainment Plus) 무료 체험하기\' 버튼이 표시됩니다.\n• Max, STARZ, and Paramount Plus\n• 일부 국가에서는 아직 서비스가 제공되지 않습니다. - \'(Entertainment Plus) 무료 체험하기\' 버튼이 숨겨집니다.\n• Max, STARZ, and Paramount Plus\n• 일부 국가에서는 아직 서비스가 제공되지 않습니다. - \'무료 체험하기\' 버튼 숨기기 - 구독 피드 상단에서 구독 채널 섹션이 표시됩니다. - 구독 피드 상단에서 구독 채널 섹션이 숨겨집니다. - 구독 채널 섹션 숨기기 - 동작 추천바가 표시됩니다. - 동작 추천바가 숨겨집니다. - 동작 추천바 숨기기 - "이 설정은 더 이상 사용되지 않습니다. - -'설정 → 자동재생 → 다음 동영상 자동재생'에서 설정을 사용할 수 있습니다. - -알림: -• '최종 화면 추천 동영상'에서 문제가 발생하는 경우 앱을 다시 시작해보세요." - 최종 화면에서 \'다음 재생 추천 동영상\'이 표시됩니다. - "자동재생을 비활성화하면 최종 화면에서 '다음 재생 추천 동영상'이 숨겨집니다. - -자동재생은 YouTube 설정에서 변경할 수 있습니다: -설정 → 자동재생 → 다음 동영상 자동재생" - 최종 화면에서 \'다음 재생 추천 동영상\' 숨기기 - Thanks 버튼이 표시됩니다. - Thanks 버튼이 숨겨집니다. - Thanks 버튼 숨기기 - 콘서트 티켓 선반이 표시됩니다.\n• 일부 국가에서는 아직 서비스가 제공되지 않습니다. - 콘서트 티켓 선반이 숨겨집니다.\n• 일부 국가에서는 아직 서비스가 제공되지 않습니다. - 콘서트 티켓 선반 숨기기 - 타임스탬프가 표시됩니다. - 타임스탬프가 숨겨집니다. - 타임스탬프 숨기기 - 실시간 이모티콘 리액션이 표시됩니다. - 실시간 이모티콘 리액션이 숨겨집니다. - 실시간 이모티콘 리액션 숨기기 - 크롬캐스트 버튼이 표시됩니다. - 크롬캐스트 버튼이 숨겨집니다. - 크롬캐스트 버튼 숨기기 - 만들기 버튼이 표시됩니다. - 만들기 버튼이 숨겨집니다. - 만들기 버튼 숨기기 - 알림 버튼이 표시됩니다. - 알림 버튼이 숨겨집니다. - 알림 버튼 숨기기 - 스크립트 섹션이 표시됩니다. - 스크립트 섹션이 숨겨집니다. - 스크립트 섹션 숨기기 - 동영상 광고가 표시됩니다. - 동영상 광고가 숨겨집니다. - 동영상 광고 숨기기 - "홈 / 구독 / 검색 결과가 필터링되어 조회수가 지정한 수보다 적거나 많은 동영상이 숨겨집니다. - -알려진 문제점: -• Shorts 동영상은 숨길 수 없습니다. -• 실시간 스트림과 '조회수 없음' 동영상은 숨길 수 없습니다." - 조회수 필터링에 대한 정보 - 홈 피드에서 조회수 필터를 비활성화합니다. - 홈 피드에서 조회수 필터를 활성화합니다. - 홈 피드에서 조회수 필터 활성화하기 - 검색 결과에서 조회수 필터를 비활성화합니다. - 검색 결과에서 조회수 필터를 활성화합니다. - 검색 결과에서 조회수 필터 활성화하기 - 구독 피드에서 조회수 필터를 비활성화합니다. - 구독 피드에서 조회수 필터를 활성화합니다. - 구독 피드에서 조회수 필터 활성화하기 - 조회수가 높거나 낮은 추천 동영상을 숨길 수 있습니다.\n\n알려진 문제점: 실시간 스트림과 \'조회수 없음\' 동영상은 숨겨지지 않습니다. - 조회수를 기준으로 동영상 숨기기 - 입력한 숫자보다 높은 조회수를 가진 동영상을 숨길 수 있습니다. - 조회수가 높은 동영상 숨기기 - 입력한 숫자보다 낮은 조회수를 가진 동영상을 숨길 수 있습니다. - 조회수가 낮은 동영상 숨기기 - 천회 -> 1000\n만회 -> 10000\n억회 -> 100000000\n조회수 -> views - 레이아웃에서 표시되는 동영상의 조회수에 대한 언어 템플릿을 설정할 수 있습니다. \n• \'키(해당 언어의 문자/단어) -> 값(키의 의미)\'은 줄바꿈으로 구분하여 설정해야 하고, 키는 \'->\' 기호 앞에 와야 합니다. \n• 편집창은 \'하나의 언어에 대한 키만 입력\' & \'앞에는 숫자 관련 키, 마지막에는 조회수 단어 키 순으로 입력\' 해야 합니다. \n• 앱 언어 또는 시스템 언어를 변경하는 경우 이 설정을 다시 설정해야 합니다.\n예) 한국어: 조회수 10만회 = 만회 -> 10000, 조회수 -> views\n영어: 10K views = K -> 1000, views -> views - 조회수 키 - 플레이어에서 제품 보기 배너가 표시됩니다. - 플레이어에서 제품 보기 배너가 숨겨집니다. - 제품 보기 배너 숨기기 - 음성 검색 버튼이 표시됩니다. - 음성 검색 버튼이 숨겨집니다. - 음성 검색 버튼 숨기기 - 웹 검색 결과가 표시됩니다. - 웹 검색 결과가 숨겨집니다. - 웹 검색 결과 숨기기 - YouTube Doodles가 표시됩니다.\n• Doodles: 기념일 로고 헤더 - YouTube Doodles가 숨겨집니다.\n• Doodles: 기념일 로고 헤더 - YouTube Doodles 숨기기 - "YouTube Doodles는 공휴일이나 기념일 등, 그날에 맞춘 디자인으로 변경되는 왼쪽 상단의 YouTube 헤더를 말합니다. - -현재 거주하는 지역에서 YouTube Doodles가 표시되어 있는데 이 설정이 활성화되어 있는 경우에는 검색창 아래에 표시되는 카테고리 바도 숨겨집니다." - 줌 오버레이가 표시됩니다. - 줌 오버레이가 숨겨집니다. - 줌 오버레이 숨기기 - Afn Blue - Afn Red - 사용자 정의 - YouTube - MMT - MMT Blue - MMT Green - MMT Orange - MMT Pink - MMT Turquoise - MMT Yellow - Revancify Blue - Revancify Red - Revancify Yellow - Vanced Black - Vanced Light - Xisr Yellow - YouTube - 자동 회전 모드를 켜지 않고, 전체 화면으로 시청 중 화면을 껐다 켰을 때, 가로 모드를 유지합니다. - 가로 모드로 강제로 유지되는 시간을 지정할 수 있습니다. (밀리초)\n\n화면을 끄고 이 시간이 지나고 다시 켰을 때는 세로 모드로 적용됩니다. - 가로 모드 유지하기 제한 시간 - 가로 모드 유지하기 - YouTube - 두 번 누르기 동작을 비활성화합니다 - "두 번 누르기 동작을 활성화합니다. - -• 미니 플레이어를 두 번 눌러서 더 큰 사이즈로 변경할 수 있습니다. -• 다시 두 번 누르면 원래 사이즈로 변경됩니다." - 두 번 누르기 동작 활성화하기 - 드래그 앤드 드롭을 비활성화합니다. - 드래그 앤드 드롭을 활성화합니다. - 드래그 앤드 드롭 활성화하기 - \'펼치기\' & \'닫기\' 버튼이 표시됩니다.\n• YouTube v19.20.35+에서는 위로 스와이프하여 미니 플레이어를 펼칠 수 없으므로 \'펼치기\' 버튼을 사용해야 합니다. - \'펼치기\' & \'닫기\' 버튼이 숨겨집니다.\n• 스와이프하여 미니 플레이어를 펼치거나 닫을 수 있습니다.\n• YouTube v19.20.35+에서는 위로 스와이프하여 미니 플레이어를 펼칠 수 없으므로 \'펼치기\' 버튼을 사용해야 합니다. - \'펼치기\' & \'닫기\' 버튼 숨기기 - \'되감기\' & \'빨리 감기\' 버튼이 표시됩니다. - \'되감기\' & \'빨리 감기\' 버튼이 숨겨집니다. - \'되감기\' & \'빨리 감기\' 버튼 숨기기 - 서브텍스트가 표시됩니다.\n• 왼쪽 하단에서 표시되는 \'유료 광고 포함\'과 같은 라벨 - 서브텍스트가 숨겨집니다.\n• 왼쪽 하단에서 표시되는 \'유료 광고 포함\'과 같은 라벨 - 서브텍스트 숨기기 - 미니 플레이어 오버레이 불투명도 값은 0-100 사이어야 합니다. - 불투명도 값은 0-100 사이이며, 0은 투명입니다. - 미니 플레이어 오버레이 불투명도 - 기기 기본값 사용 - - 태블릿 - 최신 스타일 1 - 최신 스타일 2 - 최신 스타일 3 - 미니 플레이어 유형 설정 - 오버레이 버튼 - "버튼을 눌러서 동영상 반복재생으로 전환할 수 있습니다. -길게 누르면 동영상이 끝나면 일시 정지로 전환됩니다." - 동영상 반복재생 버튼 표시하기 - "버튼을 눌러서 동영상 URL을 복사할 수 있습니다. -길게 누르면 타임스탬프를 표기한 동영상 URL이 복사됩니다." - "버튼을 눌러서 타임스탬프를 표기한 동영상 URL을 복사할 수 있습니다. -길게 누르면 타임스탬프가 복사됩니다." - 타임스탬프를 표기한 URL 복사 버튼 표시하기 - 동영상 URL 복사 버튼 표시하기 - 버튼을 눌러서 외부 다운로더를 실행할 수 있습니다. - 외부 다운로드 버튼 표시하기 - 버튼을 눌러서 재생 중인 동영상을 음소거할 수 있습니다.\n다시 누르면 음소거가 해제됩니다. - 음소거 버튼 표시하기 - 버튼을 길게 눌러서 버튼 상태를 변경할 수 있습니다. - 동영상 재생 속도 값 설정: %s배속 - "버튼을 눌러서 동영상 재생 속도 다이얼로그를 열 수 있습니다. -길게 누르면 동영상 재생 속도 값이 1.0배속으로 설정되고, 다시 길게 누르면 기본 동영상 재생 속도 값으로 설정됩니다." - 동영상 재생 속도 다이얼로그 버튼 표시하기 - "버튼을 눌러서 채널에서 가장 오래된 동영상부터 최신 동영상까지 모든 동영상의 재생목록을 생성할 수 있습니다. -길게 누르면 생성이 취소됩니다." - 시간순 재생목록 버튼 표시하기 - 버튼을 눌러서 화이트리스트 다이얼로그를 열 수 있습니다. -길게 누르면 화이트리스트 설정 다이얼로그가 열립니다. - 화이트리스트 버튼 표시하기 - 기본 재생목록 오프라인 저장 버튼이 표시되어 있으면, 그 버튼으로 기본 다운로더를 실행할 수 있습니다. (YouTube Premium 기능) - 기본 재생목록 오프라인 저장 버튼이 항상 표시되어 있으며, 공개 재생목록에서는 그 버튼으로 외부 다운로더를 실행할 수 있습니다. - 재생목록 오프라인 저장 버튼 재정의하기 - 동영상 오프라인 저장 버튼으로 기본 다운로더를 실행할 수 있습니다. (YouTube Premium 기능) - 동영상 오프라인 저장 버튼으로 외부 다운로더를 실행할 수 있습니다. - 동영상 오프라인 저장 버튼 재정의하기 - 버튼 동작을 재정의하려면 YouTube Music이 필요합니다. 여기를 눌러서 YouTube Music을 다운로드하세요. - 전제 조건 - YouTube Music 버튼으로 순정 YouTube Music을 실행할 수 있습니다. - YouTube Music 버튼으로 RVX Music을 실행할 수 있습니다. - YouTube Music 버튼 재정의하기 - 제외된 패치 - 포함된 패치 - 일반 - 액션 버튼 - 추가 설정 - 애니메이션 / 피드백 - 오프라인 저장 버튼 - 실험 기능 - 이미지 표시 제한 국가 - 파일로 가져오기 / 내보내기 - 텍스트로 가져오기 / 내보내기 - 키워드 필터 - 기타 - 오버레이 버튼 - 패치 정보 - 빠른 작업 - 추천 동영상 - Shorts 선반 - 추천 동작 - 사용된 도구 - 조회수 필터 - 계정 메뉴 및 내 페이지에서 구성요소를 숨기거나 표시할 수 있습니다. - 계정 메뉴 - 플레이어 하단에 있는 액션 버튼을 숨기거나 표시할 수 있습니다. - 액션 버튼 - 광고 - 대체 썸네일 - 앰비언트 모드 제한을 우회하거나 앰비언트 모드를 비활성화할 수 있습니다. - 앰비언트 모드 - 피드, 검색 결과 그리고 관련 동영상에서 카테고리 바를 숨기거나 표시할 수 있습니다. - 카테고리 바 - 플레이어에서 채널바 구성요소를 숨기거나 표시할 수 있습니다. - 채널바 - 채널 프로필에서 구성요소를 숨기거나 표시할 수 있습니다. - 채널 프로필 - 댓글 섹션에서 구성요소를 숨기거나 표시할 수 있습니다. - 댓글 - 피드 및 채널에서 커뮤니티 게시물을 숨기거나 표시할 수 있습니다. - 커뮤니티 게시물 - 사용자 정의 필터를 사용하여 구성요소를 숨길 수 있습니다. - 사용자 정의 필터 - 피드에서 메뉴 구성요소를 숨기거나 표시할 수 있습니다. - 메뉴 구성요소 - 피드 - 전체 화면과 관련된 구성요소를 숨기거나 변경할 수 있습니다. - 전체 화면 - 일반 - 진동 피드백을 활성화하거나 비활성화할 수 있습니다. - 진동 피드백 - \'앱 내에 있는 버튼을 터치 시 실행 동작\'을 재정의할 수 있습니다. - 버튼 재정의 - 설정을 가져오거나 내보낼 수 있습니다. - 설정 가져오기 / 내보내기 - 앱 내에서 최소화된 플레이어의 스타일을 변경할 수 있습니다. - 미니 플레이어 - 기타 - 하단바에서 섹션 구성요소를 숨기거나 표시할 수 있습니다. - 하단바 - 적용된 패치에 대한 정보입니다. - 패치 정보 - 플레이어에서 버튼을 숨기거나 표시할 수 있습니다. - 플레이어 버튼 - 플레이어에서 메뉴 구성요소를 숨기거나 변경할 수 있습니다. - 메뉴 구성요소 - 플레이어 - Return YouTube Username - Return YouTube Dislike - SponsorBlock - 재생바 구성요소를 사용자 정의할 수 있습니다. - 재생바 - YouTube 설정 메뉴에서 구성요소를 숨길 수 있습니다. - 설정 메뉴 - Shorts 플레이어에서 구성요소를 숨기거나 표시할 수 있습니다. - Shorts 플레이어 - Shorts - 스트리밍 데이터를 변경하여 재생 문제를 방지할 수 있습니다. - 스트리밍 데이터 변경하기 - 스와이프 제스처 - 툴바에서 버튼, 검색창, 헤더와 같은 구성요소를 숨기거나 변경할 수 있습니다. - 툴바 - 동영상 설명에서 구성요소를 숨기거나 표시할 수 있습니다. - 동영상 설명 - 키워드 또는 조회수로 동영상을 숨길 수 있습니다. - 동영상 필터 - 동영상 - 시청 기록과 관련된 설정을 변경할 수 있습니다. - 시청 기록 - 빠른 작업 상단 여백 값은 0-32 사이어야 합니다. - 재생바에서 빠른 작업 컨테이너까지의 간격을 0-32 사이에서 지정할 수 있습니다. - 빠른 작업 상단 여백 - "AV1 코덱 응답을 강제로 거부합니다. -약 20초정도의 버퍼링 후에 다른 코덱으로 전환됩니다." - AV1 코덱 응답 거부하기 - Fallback 프로세스로 인해 약 20초정도의 버퍼링이 발생합니다. - 오프셋 - 동영상 재생 속도 값을 변경할 때마다 저장하지 않습니다. - 동영상 재생 속도 값을 변경할 때마다 저장합니다. - 동영상 재생 속도 저장 활성화하기 - 기본 동영상 재생 속도 값으로 변경되었을 때, 팝업 메시지를 표시하지 않습니다. - 기본 동영상 재생 속도 값으로 변경되었을 때, 팝업 메시지를 표시합니다. - 팝업 메시지 표시하기 - 기본 동영상 재생 속도 값을 %s으로 변경합니다. - 동영상 화질 값을 변경할 때마다 저장하지 않습니다. - 동영상 화질 값을 변경할 때마다 저장합니다. - 동영상 화질 저장 활성화하기 - 기본 동영상 화질 값으로 변경되었을 때, 팝업 메시지를 표시하지 않습니다. - 기본 동영상 화질 값으로 변경되었을 때, 팝업 메시지를 표시합니다. - 팝업 메시지 표시하기 - 모바일 네트워크 이용 시 기본 동영상 화질 값을 %s로 변경합니다. - 동영상 화질을 설정할 수 없습니다. - Wi-Fi 이용 시 기본 동영상 화질 값을 %s로 변경합니다. - "시청 경고 다이얼로그를 제거합니다.\n\n• 이 설정은 다이얼로그를 자동으로 허용하기만 하며 연령 제한(성인인증 절차)을 우회할 수 없습니다.\n• 즉, 성인인증이 필요한 동영상에서 인증을 하려 할 때, 휴대폰 번호가 필요하다고 알려주는 소형 팝업창(다이얼로그) 없이 바로 휴대폰 번호 인증 페이지가 표시됩니다." - 시청 경고 다이얼로그 제거하기 - AV1 코덱을 VP9 코덱으로 변경합니다. - AV1 코덱 변경하기 - 채널 핸들(@채널 아이디)을 사용합니다. - 채널 이름을 사용합니다. - 채널 핸들 변경하기 - 타임스탬프를 누르면 남은 시간을 표시할 수 있습니다. - 타임스탬프를 누르면 동영상 재생 속도 또는 동영상 화질 설정 메뉴 구성요소를 열 수 있습니다. - 타임스탬프 액션 변경하기 - 만들기 버튼을 설정 버튼으로 변경합니다. - 만들기 버튼 변경하기 - "버튼을 눌러서 YouTube 설정을 열 수 있습니다. -길게 누르면 RVX 설정이 열립니다." - "버튼을 눌러서 RVX 설정을 열 수 있습니다. -길게 누르면 YouTube 설정이 열립니다." - 버튼에 설정할 동작 유형 - 플레이어에서 전체 화면으로 된 썸네일을 표시합니다. - 재생바 상단에서 최소화된 썸네일을 표시합니다. - 이전 재생바 썸네일 복원하기 - 이전 동영상 화질 설정을 비활성화합니다. - 이전 동영상 화질 설정 메뉴를 활성화합니다. - 이전 동영상 화질 설정 메뉴 활성화하기 - 핸들 (사용자 이름) - 표시 형식 - 사용자 이름 (핸들) - 사용자 이름 - 핸들(@사용자 아이디)을 사용합니다. - 사용자 이름을 사용합니다. - Return YouTube Username 활성화하기 - "핸들을 사용자 이름으로 변경하려면 YouTube Data API v3 Developer Key가 필요합니다. - -무료 요금제에서 API Key의 일일 할당량은 10,000개이며, 1개의 할당량은 댓글 1개에 대해 핸들을 사용자 이름으로 변경하는 데 사용됩니다. - -API Key를 발급받는 방법을 보려면 여기를 누르세요." - YouTube Data API Key에 대한 정보 - YouTube Data API v3를 사용하기 위한 Developer Key입니다. - YouTube Data API Key - 1. <a href=%1$s>새 프로젝트 만들기</a> 로 이동합니다.<br>2. <b>만들기</b> 버튼을 터치합니다.<br>3. <a href=%2$s>YouTube Data API v3</a> 로 이동합니다.<br>4. <b>사용</b> 버튼을 터치합니다.<br>5. <b>사용자 인증 정보 만들기</b> 버튼을 터치합니다.<br>6. <b>공개 데이터</b> 옵션을 선택합니다.<br>7. <b>다음</b> 버튼을 터치합니다.<br>8. API Key를 복사합니다.<br><br>※ API Key는 다른 사람과 공유해서는 안 되므로 가져오기 / 내보내기 설정에 포함되지 않습니다. - YouTube Data API v3 Developer Key 발급받기 - 정보 - 싫어요 수의 데이터는 Return YouTube Dislike API에 의해 제공됩니다. 자세한 내용을 보려면 여기를 누르세요. - ReturnYouTubeDislike.com - 좋아요 버튼에서 구분선을 표시합니다. - 좋아요 버튼에서 구분선을 표시하지 않습니다. - 좋아요 버튼에서 구분선 숨기기 - 싫어요 수를 숫자로 표시합니다. - 싫어요 수를 퍼센트로 표시합니다. - 싫어요 수를 퍼센트로 표시하기 - 싫어요 수를 표시하지 않습니다. - 싫어요 수를 표시합니다. - Return YouTube Dislike 활성화하기 - 좋아요 수가 숨겨진 동영상에서 추정되는 좋아요 수를 표시하지 않습니다. - 좋아요 수가 숨겨진 동영상에서 추정되는 좋아요 수를 표시합니다. - 추정되는 좋아요 수 표시하기 - 싫어요 수를 표시할 수 없습니다 (클라이언트 API 제한 도달). - 싫어요 수를 표시할 수 없습니다 (상태 코드: %d). - 싫어요 수를 일시적으로 표시할 수 없습니다 (응답 시간 초과). - 싫어요 수를 표시할 수 없습니다 (%s). - Return YouTube Dislike를 사용하여 투표하려면 동영상을 다시 로드하세요. - Shorts에서 싫어요 수를 표시하지 않습니다. - Shorts에서 싫어요 수를 표시합니다. - "Shorts에서 싫어요 수를 표시합니다. - -알려진 문제점: 사용자가 로그인을 하지 않았거나 시크릿 모드에서는 싫어요 수가 표시되지 않을 수 있습니다." - Shorts에서 싫어요 수 표시하기 - ReturnYouTubeDislike를 사용할 수 없을 때, 팝업 메시지를 표시하지 않습니다. - ReturnYouTubeDislike를 사용할 수 없을 때, 팝업 메시지를 표시합니다. - API를 사용할 수 없을 때, 팝업 메시지 표시하기 - 숨겨짐 - 링크를 공유할 때, URL에서 추적 쿼리 매개변수를 제거합니다. (URL의 뒷부분 \'?si=...\' 이 제거됨.) - 추적 쿼리를 제거한 링크 공유하기 - "동영상 자막에서 '#', '모금 행사', '쇼핑', '제품'과 같은 문구가 표시됩니다." - "동영상 자막에서 '#', '모금 행사', '쇼핑', '제품'과 같은 문구가 숨겨집니다." - 불필요한 동영상 자막 문구 숨기기 - 정보 - sponsor.ajay.app - 건너뛸 구간의 데이터는 SponsorBlock API에 의해 제공됩니다. 자세한 내용을 보려면 여기를 누르세요. - API URL을 변경하였습니다. - 잘못된 API URL입니다. - API URL을 초기화하였습니다. - 레이아웃 - 설정한 색상을 적용하였습니다. - 색상: - 잘못된 헥스 코드입니다. - 색상을 초기화하였습니다. - 새로운 구간 추가하기 - 각 구간에 설정할 동작 - 자동으로 건너뛰기 버튼 숨기기 - 건너뛰기 버튼을 해당 구간이 끝날 때까지 표시합니다. - 건너뛰기 버튼이 몇 초 후에 사라집니다. - 최소화된 건너뛰기 버튼 표시하기 - 일반적인 건너뛰기 버튼을 표시합니다. - 최소화된 건너뛰기 버튼을 표시합니다. - 구간 추가 버튼 표시하기 - 플레이어에서 구간 추가 버튼을 표시하지 않습니다. - 플레이어에서 구간 추가 버튼을 표시합니다. - SponsorBlock 활성화하기 - SponsorBlock은 YouTube 동영상 내 성가신 구간을 건너뛰게 해주는 크라우드소싱 시스템입니다. - 투표 버튼 표시하기 - 구간 투표 버튼을 표시하지 않습니다. - 구간 투표 버튼을 표시합니다. - 일반 - 구간 추가 시 최소 슬라이더 단위 설정 - 값은 양수여야 합니다. - 새로운 구간 추가 시에 시간 앞으로 버튼 또는 뒤로 버튼을 눌렀을 때 이동하는 최소 시간으로, 단위는 밀리초입니다. - API URL 변경하기 - SponsorBlock이 요청을 보낼 서버 URL입니다. 이것이 무슨 역할을 하는지 모르는 경우에는 이 URL을 변경하지 마세요. - 건너뛸 최소 구간 길이 - 잘못된 지속 시간입니다. - 설정한 값(초)보다 작은 구간은 건너뛰지 않으며, 재생바에도 표시되지 않습니다. - 건너뛴 횟수 기록 활성화하기 - 건너뛴 횟수 기록을 비활성화합니다. - 구간 건너뛰기를 통해 절약한 시간을 SponsorBlock의 리더보드 시스템에 알려줍니다. 건너뛴 구간에 대한 정보가 서버에 전송됩니다. - 자동으로 구간을 건너뛸 때, 팝업 메시지 표시하기 - 자동으로 구간을 건너뛸 때, 팝업 메시지를 표시하지 않습니다. 여기를 누르면 예시를 볼 수 있습니다. - 자동으로 구간을 건너뛸 때, 팝업 메시지를 표시합니다. 여기를 누르면 예시를 볼 수 있습니다. - 건너뛸 구간을 제외한 시간 표시하기 - 건너뛸 구간을 포함한 전체 동영상 길이를 타임스탬프에 표시합니다. - 건너뛸 구간을 제외한 전체 동영상 길이를 타임스탬프에 표시합니다. - 비공개 사용자 아이디 - 비공개 사용자 아이디는 30자 이상이어야 합니다. - 비공개 사용자 아이디는 SponsorBlock 서버에서 구간을 제출하거나 건너뛴 구간 정보를 기록하는데 사용되는 고유 아이디입니다. 절대 다른 이에게 공개하지 마세요. - 이미 읽음 - 광고 구간을 제출하기 전에 SponsorBlock 가이드라인을 읽어보시는 것을 추천합니다. - 보러가기 - 가이드라인 - 구간 제출 시의 주의사항에 대한 내용을 포함하고 있습니다. - 가이드라인 보기 - 조정: 구간의 시작과 끝 선택하기 - 구간 카테고리를 선택하세요. - 구간 확인하기 - 선택된 구간은 %1$02d:%2$02d부터 %3$02d:%4$02d까지 입니다 (%5$d분 %6$02d초).\n이대로 제출하시겠습니까? - 선택한 구간이\n\n%1$s\n부터\n%2$s\n\n(%3$s) 까지 입니다.\n\n이렇게 제출하시겠습니까? - 설정한 구간이 정확합니까? - 이 카테고리는 비활성화되어 있습니다. 제출하려면 설정에서 활성화해야 합니다. - 구간 편집하기 - 구간의 시작이나 끝을 편집하시겠습니까? - 잘못된 시간 형식입니다. - 직접 시간 구간 편집하기 - 지정한 시간만큼 빨리감기 (기본값: 150밀리초) - %s 을 구간의 시작 또는 끝으로 설정하시겠습니까? - - 먼저 재생바에서 시작 지점과 끝 지점을 표시하세요. - 시작 - 현재 - 구간 미리 보기 버튼을 눌러서 설정한 구간이 정상적으로 건너뛰기가 되는지 확인하세요. - 생성한 구간 제출하기 - 지정한 시간만큼 되감기 (기본값: 150밀리초) - 구간의 시작 또는 끝을 잘못 설정하였습니다. - 구간의 끝 - 구간의 시작 - 새로운 SponsorBlock 구간 - 초기화 - 색상 초기화 - 주제와 관련 없는 구간 - 전반적인 동영상의 주제를 이해하는 데 필요 없는 내용을 포함하고 있습니다. - 하이라이트 - 사람들이 동영상에서 가장 많이 찾는 구간입니다. - 상호 작용 요청 - 좋아요, 구독, 알림 설정을 요청하는 내용에 관한 구간입니다. - 무음 구간 / 인트로 - 아무 내용도 없는 구간입니다. 애니메이션이나 정적 프레임과 같은 내용을 포함하고 있습니다. - 음악이 아닌 구간 - 뮤직 비디오에서 음악이 아닌 구간이 해당됩니다. - 최종 화면 / 크레딧 - 엔딩 크레딧이나 최종 화면이 나타나는 구간입니다. - 미리 보기 / 요약 / 흥미 유발 - 이전 에피소드를 간략히 요약하거나 현재 동영상의 하이라이트를 미리 보여줍니다. - 자체 홍보 구간 - \'스폰서 광고\' 구간과 비슷하지만, 자발적으로 홍보하는 내용을 포함하는 구간입니다. 채널 굿즈 광고, 기부 광고와 동영상에 참여한 사람들을 홍보하는 광고가 해당됩니다. - 스폰서 광고 - 유료 광고, 협찬과 같은 직/간접적인 광고 구간입니다. - 복사 - 설정을 내보낼 수 없습니다: %s - 설정 가져오기 / 내보내기 - ReVanced Extended 및 다른 SponsorBlock 플랫폼에서 가져오거나 내보낼 수 있는 SponsorBlock JSON 구성입니다. - ReVanced Extended 및 다른 SponsorBlock 플랫폼으로부터 가져오거나 내보낼 수 있는 SponserBlock JSON의 전체 구성 파일입니다. 비공개 사용자 아이디를 포함하고 있으므로 주의하세요. - 설정을 가져올 수 없습니다: %s - 설정을 성공적으로 가져왔습니다. - 설정에는 비공개 SponsorBlock 사용자 아이디가 포함되어 있습니다.\n\n절대 다른 이에게 공개하지 마세요.\n - 다시 보지 않기 - 설정을 클립보드에 복사하였습니다. - 자동으로 건너뛰기 - 한 번만 자동으로 건너뛰기 - 건너뛰기 - 하이라이트 - 주제와 관련 없는 구간 건너뛰기 - 하이라이트로 건너뛰기 - 상호 작용 요청 건너뛰기 - 인트로 건너뛰기 - 무음 구간 건너뛰기 - 무음 구간 건너뛰기 - 음악이 아닌 구간 건너뛰기 - 최종 화면 건너뛰기 - 미리 보기 건너뛰기 - 요약 건너뛰기 - 미리 보기 건너뛰기 - 자체 홍보 구간 건너뛰기 - 스폰서 광고 건너뛰기 - 미제출한 구간 건너뛰기 - 사용 안 함 - 재생바에만 표시하기 - 건너뛰기 버튼 표시하기 - 주제와 관련 없는 구간을 건너뛰었습니다. - 하이라이트로 건너뛰었습니다. - 상호 작용 요청을 건너뛰었습니다. - 인트로를 건너뛰었습니다. - 무음 구간을 건너뛰었습니다. - 무음 구간을 건너뛰었습니다. - 여러 구간을 건너뛰었습니다. - 음악이 아닌 구간을 건너뛰었습니다. - 최종 화면을 건너뛰었습니다. - 미리 보기를 건너뛰었습니다. - 요약을 건너뛰었습니다. - 미리 보기를 건너뛰었습니다. - 자체 홍보 구간을 건너뛰었습니다. - 스폰서 광고를 건너뛰었습니다. - 미제출한 구간을 건너뛰었습니다. - SponsorBlock을 일시적으로 사용할 수 없습니다. - SponsorBlock을 일시적으로 사용할 수 없습니다 (상태 코드: %d). - SponsorBlock을 일시적으로 사용할 수 없습니다 (응답 시간 초과). - 통계 - 기록을 일시적으로 가져올 수 없습니다 (응답 시간 초과). - 불러오는 중 ... - 사용자의 평판: <b>%.2f</b> - 다른 분들이 <b>%s</b>개의 구간을 건너뛸 수 있게 해주셨습니다. - %1$s 시간 %2$s 분 - %1$s 분 %2$s 초 - %s 초 - 이는 <b>%s</b>에 해당됩니다.<br>리더보드를 보려면 여기를 누르세요. - 글로벌 기록 또는 상위 기여자를 확인하려면 여기를 누르세요. - SponsorBlock 리더보드 - SponsorBlock을 비활성화합니다. - 구간 <b>%s</b>개를 건너뛰었습니다. - 건너뛴 횟수 기록을 초기화하시겠습니까? - 이는 <b>%s</b>에 해당됩니다. - 제출 횟수: <b>%s</b> - 구간을 보려면 여기를 누르세요. - 사용자 이름: <b>%s</b> - 사용자 이름을 변경하려면 여기를 누르세요. - 사용자 이름을 변경할 수 없습니다. 상태 코드: %1$d %2$s - 사용자 이름을 성공적으로 변경하였습니다. - 구간을 제출할 수 없습니다.\n이미 존재하는 구간입니다. - 구간을 제출할 수 없습니다: %s - 구간을 제출할 수 없습니다: %s - 구간을 제출할 수 없습니다.\n동일 사용자 또는 동일 IP로 부터 제출된 요청이 너무 많습니다. - SponsorBlock을 일시적으로 사용할 수 없습니다. - 구간을 제출할 수 없습니다 (상태 코드: %1$d %2$s). - 구간을 성공적으로 제출하였습니다. - SponsorBlock을 사용할 수 없을 때, 팝업 메시지를 표시하지 않습니다. - SponsorBlock을 사용할 수 없을 때, 팝업 메시지를 표시합니다. - API를 사용할 수 없을 때, 팝업 메시지 표시하기 - 카테고리 변경 - 싫어요 - 구간에 투표할 수 없습니다: %s - 구간에 투표할 수 없습니다 (응답 시간 초과). - 구간에 투표할 수 없습니다 (상태 코드: %1$d %2$s). - 투표할 구간이 없습니다. - 좋아요 - 설정을 클립보드에 복사하였습니다. - 타임스탬프를 클립보드에 복사하였습니다. (%s) - URL을 클립보드에 복사하였습니다. - 타임스탬프를 표기한 URL을 클립보드에 복사하였습니다. - 기본값 - 엄지척 - 엄지척 (Cairo) - 하트 - 하트 (엷은색) - 숨겨짐 - 두 번 누르기 애니메이션 - 메타 패널 하단 여백은 0-64 사이여야 합니다. - 재생바에서 메타 패널까지의 간격을 0-64 사이에서 지정할 수 있습니다. - 메타 패널 하단 여백 - 높이 비율은 0-100 사이어야 합니다 (백분율). - 하단바가 숨겨졌을 때, 남는 빈 공간의 높이 비율을 0-100 사이에서 지정할 수 있습니다. (백분율) - 빈 공간의 높이 비율 - 타임스탬프를 길게 눌러서 Shorts 반복 상태를 변경할 수 있습니다. - 타임스탬프 길게 누르기 동작 - "전체 화면에서 동영상 제목 섹션을 표시합니다. - -알려진 문제점: 동영상 제목을 누르면 사라집니다." - 동영상 제목 섹션 표시하기 - 자동재생이 활성화되어 있으면, 카운트다운 종료 후에 다음 동영상을 재생합니다 - 자동재생이 활성화되어 있으면, 카운트다운 없이 다음 동영상을 재생합니다. - 자동재생 카운트다운 건너뛰기 - "동영상을 시작할 때, 미리 로드된 버퍼를 건너뛰어 기본 동영상 화질 적용 지연을 우회합니다. - -• 동영상이 시작되면 약 0.3초정도의 지연이 발생하지만 기본 동영상 화질이 즉시 적용됩니다. -• HDR 동영상, 실시간 스트림, 15초 미만의 동영상에는 적용되지 않습니다." - 미리 로드된 버퍼 건너뛰기 - 팝업 메시지를 표시하지 않습니다. - 팝업 메시지를 표시합니다. - 건너뛸 때, 팝업 메시지 표시하기 - 이 설정을 활성화하면 동영상 재생 문제가 발생할 수 있습니다. - 미리 로드된 버퍼를 건너뛰었습니다. - 재생 속도 오버레이 값은 0-8.0 사이어야 합니다. - 동영상 재생 속도 오버레이 값은 0-8.0 사이어야 합니다. - 동영상 재생 속도 오버레이 값 - "서버에 연결할 때 사용되는 앱 버전을 지정하여 앱 레이아웃을 변경합니다. - -• 이 설정을 활성화하면 앱 레이아웃이 변경되지만 알려지지 않은 문제점이 발생할 수 있습니다. -• 나중에 이 설정을 비활성화하면 앱 데이터를 지우기 전까지 이전 레이아웃이 유지될 수 있습니다." - 앱 버전을 변경하지 않습니다. - 앱 버전을 변경합니다. - 17.33.42 - 이전 레이아웃으로 복원합니다. - 17.41.37 - 이전 재생목록 선반으로 복원합니다. - 18.05.40 - 이전 댓글 입력 상자로 복원합니다. - 18.17.43 - 이전 플레이어 패널 구성요소로 복원합니다. - 18.33.40 - 이전 Shorts 액션바로 복원합니다. - 18.38.45 - 이전 기본 동영상 화질 적용 방식을 복원합니다. - 18.48.39 - \'조회수\' & \'좋아요\'의 실시간 업데이트를 비활성화합니다. - 19.13.37 - 이전 롤링 넘버 애니메이션으로 복원합니다. - 변경할 앱 버전 설정 - 변경할 앱 버전을 입력하세요. - 변경할 앱 버전 편집하기 - 앱 버전 변경하기 - "앱 버전을 이전 YouTube 앱 버전으로 변경합니다.\n\n변경하면 앱 레이아웃과 기능이 변경되지만 알려지지 않은 문제점이 발생할 수 있습니다.\n\n나중에 이 기능을 비활성화하게 되면 앱 레이아웃 문제점을 방지하기 위해 앱 데이터를 지우는 것이 좋습니다." - "기기 크기 정보를 최대값으로 변경합니다. -높은 기기 크기 정보가 필요한 일부 동영상에서는 고화질 동영상 값이 잠금 해제될 수 있지만 모든 동영상에는 적용되지 않습니다." - 기기 크기 정보 변경하기 - iOS 동영상 코덱을 AVC (H.264), VP9 또는 AV1으로 활성화합니다.\n\n• 예전에 업로드된 동영상을 재생했는데 VP9 코덱 응답을 받았을 경우, 일부 화질 값들이 제거되어 360p와 1080p(Premium 기능)만 선택할 수 있거나 화질 메뉴를 선택할 수 없을 수 있습니다. - iOS 동영상 코덱을 AVC (H.264)로 활성화합니다.\n\n• 일부 VP9 코덱 동영상에서 제거되었던 화질 값들이 표시될 수 있습니다.\n• 최대 화질 값이 1080p이므로 초고화질 동영상을 재생할 수 없습니다.\n• HDR 동영상을 재생할 수 없습니다 - iOS AVC (H.264) 강제로 활성화하기 - "이 설정을 활성화하면 배터리 수명이 향상되고 재생 끊김 현상이 해결될 수 있습니다. - -AVC (H.264)의 최대 화질 값은 1080p이며 동영상을 재생하면 VP9 또는 AV1보다 더 많은 모바일 데이터가 사용되오니 주의하세요." - "• 오디오 트랙 메뉴가 표시되지 않습니다.\n• 안정적인 볼륨 메뉴가 비활성화된 채로 잠겨있습니다." - "• 오디오 트랙 메뉴가 표시되지 않습니다.\n• 안정적인 볼륨 메뉴가 비활성화된 채로 잠겨있습니다." - "• 영화 또는 회원 전용 동영상과 같은 유료 동영상이 재생되지 않을 수 있습니다.\n• 되감기가 가능한 실시간 스트림이 라이브 중인 시점이 아닌 처음부터 재생될 수 있습니다.\n• 동영상이 1초 일찍 종료될 수 있습니다.\n• OPUS 오디오 코덱이 지원되지 않습니다." - 알려진 문제점 - • 동영상이 재생되지 않을 수 있습니다. - \'스트리밍 데이터를 가져오는 데 사용되는 클라이언트\'가 전문 통계에서 숨겨집니다. - \'스트리밍 데이터를 가져오는 데 사용되는 클라이언트\'가 전문 통계에서 표시됩니다. - 전문 통계에서 표시하기 - "스트리밍 데이터를 변경하지 않습니다.\n동영상 재생 문제가 발생할 수 있습니다." - 스트리밍 데이터를 변경합니다. - 스트리밍 데이터 변경하기 - Android - Android TV - Android VR - iOS - 기본 클라이언트 - 이 설정을 비활성화하면 동영상 재생 문제가 발생할 수 있습니다. - 밝기 스와이프 감도는 1-1000 사이어야 합니다. (퍼센트) - 밝기 스와이프의 최소 거리를 1-1000 사이에서 지정할 수 있습니다. (퍼센트)\n\n최소 거리가 짧을수록 밝기 레벨이 더 빠르게 변경됩니다. - 밝기 스와이프 감도 - 잠금 화면 모드에서 스와이프 제스처를 비활성화합니다. - 잠금 화면 모드에서 스와이프 제스처를 활성화합니다. - 잠금 화면 모드에서 스와이프 제스처 활성화하기 - 자동 - 제스처 인식을 위해 얼마나 스와이프를 해야 할지를 지정할 수 있으며, 원하지 않은 제스처 인식을 방지합니다. - 스와이프 한계치 - 오버레이 투명도 값을 지정할 수 있습니다. (0–255) - 오버레이 투명도 - 스와이프 영역 크기는 50 보다 클 수 없습니다. - 스와이프를 할 수 있는 화면 영역 크기를 지정할 수 있습니다. (백분율)\n\n알림: \'두 번 탭하여 탐색\' 제스처의 화면 영역 크기도 변경됩니다. - 스와이프 오버레이 화면 크기 - 오버레이 텍스트 크기를 지정할 수 있습니다. - 오버레이 텍스트 크기 - 오버레이가 표시되는 시간을 지정할 수 있습니다. (밀리초) - 오버레이 타임아웃 - 볼륨 스와이프 감도는 1-1000 사이어야 합니다. (퍼센트) - 볼륨 스와이프의 최소 거리를 1-1000 사이에서 지정할 수 있습니다. (퍼센트)\n\n최소 거리가 짧을수록 볼륨 레벨이 더 빠르게 변경됩니다.\n\n권장 볼륨 스와이프 감도는 15단계 볼륨에서 100%, 150단계 볼륨에서 10%입니다. - 볼륨 스와이프 감도 - "기기 정보를 변경하여 만들기 버튼과 알림 버튼의 위치를 교환합니다. -• 이 설정을 변경하더라도 기기를 다시 시작할 때까지 적용되지 않을 수 있습니다. -• 이 설정을 비활성화하면 서버에서 광고 필터에 등록되지 않은 광고(Shorts 광고)가 로드됩니다. -• 이 설정을 활성화하면 일부 광고가 강제로 숨겨집니다. (동영상 광고, 일반 레이아웃 광고) -• 광고 설정에 있는 일부 설정들을 비활성화하려면 이 설정도 비활성화해야 합니다." - 만들기 버튼과 알림 버튼의 위치를 교환하지 않습니다. - "만들기 버튼과 알림 버튼의 위치를 교환합니다. - -알림: 이 설정을 활성화하면 동영상 광고도 강제로 숨겨집니다." - 만들기 버튼과 알림 버튼 위치 교환하기 - "이 설정을 비활성화하면 서버에서 더 많은 광고가 로드될 수 있습니다. - -또한, Shorts에서 광고가 더 이상 숨겨지지 않습니다. - -이 설정이 적용되지 않는 경우에는 시크릿 모드로 전환해 보세요." - YouTube - RVX Music - %s 이 설치되어 있지 않습니다. 설치하세요. - 이 기기에 설치된 RVX Music 앱 패키지명을 설정하세요. - RVX Music 앱 패키지명 - • 시청 기록이 차단됩니다. - "• Google 계정의 시청 기록 설정을 따릅니다. -• '광고 차단기', '광고 & 추적 차단 기능이 내장된 DNS 또는 VPN'으로 인해 시청 기록이 작동되지 않을 수 있습니다." - • Google 계정의 시청 기록 설정을 따릅니다. - 시청 기록 상태 - YouTube 시청 기록을 관리하려면 여기를 누르세요. - 전체 기록 관리 - 기본값 - 도메인 변경 - 시청 기록 차단 - 시청 기록 유형 설정 - \'%1$s\' 채널을 %2$s 화이트리스트에 추가할 수 없습니다. - \'%1$s\' 채널을 %2$s 화이트리스트에 추가하였습니다. - 화이트리스트 채널이 없습니다. - 화이트리스트에 추가하지 못하였습니다. - 채널 정보를 불러오지 못하였습니다. - 화이트리스트에 추가하였습니다. - 재생 속도 - \'%1$s\' 채널을 %2$s 화이트리스트에서 제거하시겠습니까? - \'%1$s\' 채널을 %2$s 화이트리스트에서 제거할 수 없습니다. - \'%1$s\' 채널을 %2$s 화이트리스트에서 제거하였습니다. - 화이트리스트에 추가된 채널을 확인 또는 제거할 수 있습니다. - 채널 화이트리스트 - SponsorBlock - diff --git a/src/main/resources/youtube/translations/pl-rPL/strings.xml b/src/main/resources/youtube/translations/pl-rPL/strings.xml deleted file mode 100644 index e68b32d5d..000000000 --- a/src/main/resources/youtube/translations/pl-rPL/strings.xml +++ /dev/null @@ -1,1730 +0,0 @@ - - - Włączyć gesty ułatwień dostępu dla odtwarzacza filmów? - Twoje ustawienia są zmienione, ponieważ serwis ułatwień dostępu jest włączony. - Kontynuuj - Nie pokazuj ponownie - "GmsCore nie ma uprawnień do działania w tle. - -Postępuj zgodnie z przewodnikiem 'Don't kill my app!' dla twojego urządzenia i zastosuj instrukcje dla swojej instalacji GmsCore. - -Jest to wymagane do działania aplikacji." - "Optymalizacja baterii GmsCore musi być wyłączona, aby zapobiec problemom. - -Kontynuuj i wyłącz optymalizację baterii." - Otwórz stronę - Wymagana akcja - Włącz cloud messaging, by otrzymywać powiadomienia. - Otwórz GmsCore - GmsCore nie jest zainstalowany. Zainstaluj go. - "DeArrow dostarcza miniaturki filmów z YouTube pochodzące z procesu crowdsourcingu. Miniaturki te są często bardziej trafne niż te dostarczane przez YouTube. Jeśli opcja ta jest włączona, adresy URL filmów będą wysyłane do serwera API i nie będą wysyłane żadne inne dane. Jeśli film nie posiada miniaturki od DeArrow, to wyświetla się oryginalna lub przechwycona z filmu. - -Stuknij tutaj, aby dowiedzieć się więcej o DeArrow." - DeArrow - Nieprawidłowy adres URL API DeArrow. - Adres URL punktu końcowego do miniaturek DeArrow - Punkt końcowy API DeArrow - Ukryty - Widoczny - Komunikat, jeśli API jest niedostępne - DeArrow jest tymczasowo niedostępne. (kod statusu: %s) - DeArrow jest tymczasowo niedostępne. - Na stronie głównej - Na stronie Ty - Oryginalne miniaturki - DeArrow i oryginalne miniaturki - DeArrow i miniaturki przechwycone z filmu - Miniaturki przechwycone z filmu - W playlistach, rekomendacjach - W wynikach wyszukiwania - Miniaturki przechwycone z wideo - Miniaturki przechwytywane z filmu są brane z początku, środka lub końca filmu. Obrazy te są wbudowane w YouTube, więc żadne zewnętrzne API nie jest używane. - Miniaturki przechwycone z filmu - Używasz przechwytywania wysokiej jakości. - Używasz przechwytywania średniej jakości. Miniaturki będą ładować się szybciej, lecz mogą pozostać puste w przypadku transmisji na żywo, niewydanych i bardzo starych filmów. - Szybkie przechwytywanie miniaturek z filmu - Początek filmu - Środek filmu - Koniec filmu - Czas, z którego ma być przechwycona miniaturka z filmu - Na stronie subskrypcji - Niewidoczna - "Widoczna" - Dodatkowa informacja obok czasu - Prędkość odtwarzania - Jakość filmu - Typ informacji - Oświetlenie kinowe w trybie oszczędzania baterii jest wyłączone - Oświetlenie kinowe w trybie oszczędzania baterii jest włączone - Obejdź ograniczenia oświetlenia kinowego - Domena, z której mają być pobierane obrazy.\nNotka: Wprowadź tylko nazwę domeny, tzn. bez prefiksu \"https\:\/\/\". - Alternatywna domena - Oryginalny\n\nWłączenie tej opcji może naprawić brakujące obrazy, które są blokowane w niektórych regionach. - yt4.ggpht.com - Host dla obrazów - Oryginalny - Telefonowy - Telefonowy (max. 480 dpi) - Tabletowy - Tabletowy (max. 600 dpi) - Układ aplikacji - Przełączniki - Tekst - Wygląd przełączników - Aplikacji - Systemowy - Wygląd panelu udostępniania - Automatyczne odtwarzanie - Domyślny - Wstrzymywanie - Powtarzanie - Zmień stan powtarzania Shortsów - Przeglądaj kanały - Kursy / Nauka - Domyślna - Odkrywanie - Gry - Historia - Biblioteka - Polubione filmy - Na żywo - Filmy - Muzyka - Wyszukiwanie - Shortsy - Sport - Subskrypcje - Na czasie - Do obejrzenia - Strona startowa - Strona główna zmienia się tylko raz - "Strona główna zawsze się zmienia - -Ograniczenie: Przycisk wstecz na pasku narzędzi może nie działać." - Działanie zmieniania strony głównej - Domyślny - Premium - Zmień nagłówek YouTube - Lista tekstów tworzących ścieżkę komponentów do filtrowania, która musi być oddzielona nowymi liniami. - Własny filtr - Wyłączony - Włączony - Własny filtr - Nieprawidłowy własny filtr: %s. - Stary - Własny - Wygląd menu od prędkości odtwarzania - Niestandardowe prędkości muszą by mniejsze niż %sx. - Nieprawidłowe niestandardowe prędkości odtwarzania. - Dodaj lub zmień dostępne prędkości odtwarzania - Edytuj niestandardowe prędkości odtwarzania - Przezroczystość nakładki odtwarzacza musi wynosić między 0 a 100. - Wartość przezroczystości musi wynosić między 0 a 100, gdzie 0 oznacza przezroczystość - Niestandardowa przezroczystość nakładki - Wpisz kod hex koloru paska postępu filmu - Niestandardowy kolor paska postępu filmu - Aby otwierać linki YouTube w RVX, przejdź do opcji obsługiwanych linków w ustawieniach i włącz obsługiwane adresy internetowe dla RVX. - Otwórz systemowe ustawienia aplikacji - Domyślna prędkość odtwarzania - Domyślna jakość filmu podczas używania sieci mobilnej - Domyślna jakość filmu podczas używania Wi-Fi - Wyłącza oświetlenie kinowe tylko w trybie pełnoekranowym - Włączone - Wyłączone - Oświetlenie kinowe w trybie pełnoekranowym - Wyłącza oświetlenie kinowe - Włączone - Wyłączone - Oświetlenie kinowe - Włączone - Wyłączone - Wymuszone ścieżki dźwiękowe - Włączone - Wyłączone - Wymuszone napisy - Ukryte - Widoczne - Wyskakujące panele w odtwarzaczu - "Włączone, gdy autoodtwarzanie jest włączone. - -Autoodtwarzanie może być zmienione w ustawieniach YouTube: -Ustawienia → Autoodtwarzanie → Autoodtwarzanie następnego filmu" - Wyłączone - Automatyczne przełączanie na playlisty mix - Włączenie tej funkcji wyłączy automatyczne zmienianie na YouTube Mix, podczas odtwarzania muzyki z włączonym autoodtwarzaniem. - Włączona - Wyłączona - Domyślna prędkość odtwarzania podczas transmisji na żywo - Włączona - "Wyłączona - -Ograniczenie: To ustawienie może nie działać dla filmów bez baneru 'Słuchaj w YouTube Music'." - Domyślna prędkość odtwarzania dla muzyki - Widoczny - Ukryty - Panel zaangażowania - Włączone - Wyłączone - Wibracje podczas zmieniania rozdziału - Włączone - Wyłączone - Wibracje podczas przeciągania - Włączone - Wyłączone - Wibracje podczas przewijania - Włączone - Wyłączone - Wibracje podczas cofania przewijania - Włączone - Wyłączone - Wibracje podczas przybliżania filmu - Włączona - Wyłączona - Automatyczna jasność w filmach z HDR - Włączone - Wyłączone - Filmy z HDR - Orientacja filmu jest zgodna z ustawieniami urządzenia w trybie pełnoekranowym. - Orientacja filmu jest w trybie pionowym w trybie pełnoekranowym. - Tryb poziomy - Widoczna - Ukryta - Poświata przycisków od łapkowania w górę i dół - "Wyłącz protokół QUIC w CronetEngine" - Wyłącz protokół QUIC - Shortsy będą włączane podczas startu aplikacji - Shortsy nie będą włączane podczas startu aplikacji - Start od Shortsów - Włączone - Wyłączone - Animacje liczb - Włączone - Wyłączone - Rozdziały w pasku postępu filmu - Włączona - Wyłączona - Animacja przycisku polubienia - "Wyłącz 'Odtwarzam 2x szybciej' podczas przytrzymywania. - -Notki: -• Wyłączenie nakładki prędkości odtwarzania przywraca zachowanie 'Przesuń, by przewinąć' ze starego układu -• Wyłączenie tego ustawienia nie powoduje wymuszonego włączenia nakładki prędkości odtwarzania" - Wyłącz nakładkę prędkości odtwarzania - Włączona - Wyłączona - Animacja uruchamiania aplikacji - "Wyłącza następujące interakcje, gdy opis filmu jest otworzony: - -• Stuknij, by przewijać -• Stuknij i przytrzymaj, by zaznaczyć tekst" - Wyłącz interakcje z opisami filmów - Włączone - "Wyłączone - -• Maksymalną rozdzielczością filmów jest 1080p -• Odtwarzanie filmów wykorzystuje więcej danych internetowych niż VP9 -• Kodek VP9 jest używany do filmów z HDR" - Wyłącz kodek VP9 - Wyłączony - "Włączony - -Efekt uboczny: motyw Cairo powiązany m.in. z paskiem postępu filmu, nakłada się również na kropki przy powiadomieniach" - Motyw Cairo paska postępu filmu - Przyciski w odtwarzaczu zajmują cały ekran - Przyciski w odtwarzaczu nie zajmują całego ekranu - Przyciski w odtwarzaczu bliżej środka - Wyłączona - Włączona - Niestandardowa prędkość odtwarzania - Niewidoczny - Widoczny - Niestandardowy kolor paska postępu filmu - Bez bufora - Z buforem - Logi do debugowania buforu - Niezapisywane - Zapisywane - Logi do debugowania - Wyłączona - Włączona - Domyślna prędkość odtwarzania w Shortsach - Wyłączona - Włączona - Zewnętrzna przeglądarka - Wyłączony - Włączony - Kolorowy ekran ładowania - Odstępy między przyciskami nawigacji są normalne - Odstępy między przyciskami nawigacji są mniejsze - Wąskie przyciski nawigacji - Nie - Tak - Bezpośrednie otwieranie linków - Włącza kodek OPUS, jeśli odpowiedź odtwarzacza zawiera ten kodek. - Włącz kodek OPUS - Nie - Tak - Zapisuj i przywracaj jasność podczas zamykania lub wchodzenia w tryb pełnoekranowy - Wyłączone - Włączone - Stukanie w pasek postępu filmu - "Przywróci to miniaturki w transmisjach na żywo, które ich nie mają. - -Zużycie danych internetowych może być większe, a miniaturki w pasku postępu filmu mogą mieć drobne opóźnienie przed wyświetleniem. - -Funkcja działa najlepiej przy bardzo szybkim połączeniu internetowym." - Średnia - Wysoka - Rozdzielczość miniaturek - Wyłączony - "Włączony - -Ograniczenia: -• Ustawienie nie tylko włącza czas, lecz także pozwala użytkownikom ukryć interfejs po kliknięciu tła odtwarzacza -• Jako, że jest to funkcja w fazie rozwoju przez Google, układ aplikacji może być uszkodzony" - Czas Shortsa - Wyłączone - Włączone - Zmienianie jasności przesuwaniem - Wyłączone - Włączone - Wibracje - Najniższa wartość jasności aktywowana przesuwaniem nie włącza automatycznej jasności - Najniższa wartość jasności aktywowana przesuwaniem włącza automatyczną jasność - Automatyczna jasność przesuwaniem - Stuknij, aby aktywować przesuwanie - Stuknij i przytrzymaj, aby aktywować przesuwanie - Naciśnij, by przesunąć - Przesuwanie góra/dół nie będzie odtwarzać następnego/poprzedniego filmu. - Przesuwanie góra/dół będzie odtwarzać następny/poprzedni film. - Gest zmiany filmu w trybie pełnoekranowym - Wyłączone - Włączone - Zmienianie głośności przesuwaniem - Nieprzezroczysty - Półprzezroczysty - Wygląd paska nawigacji - Wejście w tryb pełnoekranowy przesuwaniem w dół poniżej odtwarzacza filmu jest wyłączone - Wejście w tryb pełnoekranowy przesuwaniem w dół poniżej odtwarzacza filmu jest włączone - Pełny ekran przesuwaniem - "Włączenie tej opcji ukryje przycisk ustawień w zakładce Ty. - -W tym przypadku, proszę użyć następującej ścieżki dostania się do ustawień: -Zakładka Ty → Zobacz kanał → Menu → Ustawienia" - Szeroki pasek wyszukiwania na stronie Ty - Wyłączony - Włączony - Szeroki pasek wyszukiwania - Wyłączony - Włączony - Szeroki pasek wyszukiwania z nagłówkiem YouTube - Opis - "Wprowadź tytuł panelu opisu filmu w twoim języku. -Opcja rozwijania opisu filmu może nie działać, jeśli wprowadzony ciąg znaków nie zgadza się z tytułem panelu opisu filmu." - Tytuł w panelu opisu filmu - Ręcznie - Automatycznie - Otwieraj opisy filmów - Czy chcesz kontynuować? - Przywrócono domyślne wartości. - Uruchom ponownie, aby wczytać poprawnie układ aplikacji - "Istnieje błąd po stronie serwera YouTube, który u niektórych użytkowników powoduje ukrywanie się tekstu liczb posiadających animację, takich jak polubienia, wyświetlenia, daty opublikowania filmów. - -Tymczasowym obejściem dla tego błędu jest oszukanie wersji aplikacji do 19.13.37. - -Czy chcesz oszukać wersję aplikacji przed ponownym uruchomieniem?" - Odśwież i uruchom ponownie - Nie udało się wyeksportować ustawień. - Ustawienia zostały pomyślnie wyeksportowane. - Wyeksportuj ustawienia do pliku - Wyeksportuj ustawienia - Zaimportuj - Skopiuj - Importuje bądź eksportuje ustawienia w formie tekstu - Zaimportuj / Wyeksportuj jako tekst - Nie udało się zaimportować ustawień. - Zresetowano ustawienia do domyślnych. - Ustawienia zostały pomyślnie zaimportowane. - Zaimportuj ustawienia z zapisanego pliku - Zaimportuj ustawienia - Zresetuj - Wyszukaj w %s - ReVanced Extended - Zewnętrzna aplikacja od pobierania - Nie zainstalowany - "%1$s nie jest zainstalowany. -Pobierz %2$s ze strony internetowej." - Ostrzeżenie - %s nie jest zainstalowany. Proszę go zainstalować. - Nazwa pakietu zainstalowanej zewnętrznej aplikacji od pobierania, takiej jak YTDLnis. - Nazwa pakietu aplikacji od pobierania (playlisty) - Nazwa pakietu zainstalowanej zewnętrznej aplikacji od pobierania, takiej jak NewPipe lub YTDLnis przy długim przytrzymaniu. - Nazwa pakietu aplikacji od pobierania (filmy) przy długim przytrzymaniu - Nazwa pakietu zainstalowanej zewnętrznej aplikacji od pobierania, takiej jak NewPipe lub YTDLnis. - Nazwa pakietu aplikacji od pobierania (filmy) - "Filmy zostaną przełączone w tryb pełnoekranowy w następujących sytuacjach: - -• Po rozpoczęciu filmu -• Po stuknięciu w czas filmu w komentarzach" - Wymuś tryb pełnoekranowy - Wyświetla okno o optymalizacji GMSCore przy każdym uruchomieniu aplikacji. - Pokaż okno o optymalizacji dla GMSCore - Lista nazw menu konta do filtrowania, która musi być oddzielona nowymi liniami. - Filtr menu konta - "Ukrywa elementy menu konta i zakładki Ty. -Niektóre komponenty mogą nie być ukryte." - Menu konta - Widoczne - Ukryte - Karty albumów - Sekcje wyróżnionych miejsc, gier i muzyki są widoczne - Sekcje wyróżnionych miejsc, gier i muzyki są ukryte - Sekcja atrybutów - Widoczne - Ukryte - Ramki z następnym filmem - Widoczny - Ukryty - Przycisk do sklepu - "Ukrywa półki: -• Z najnowszymi informacjami -• Z filmami, które nie zostały dokończone -• Od odkrywania kanałów -• Z muzyką do ponownego odsłuchania -• Od kupowania -• Z filmami, które nie zostały obejrzane" - Półki karuzelowe - Widoczny - Ukryty - Na stronie głównej - Widoczny - Ukryty - Nad powiązanymi filmami - Widoczny - Ukryty - Nad wynikami wyszukiwania - Widoczne - Ukryte - Wytyczne - Widoczne - Ukryte - Półki ze sponsorami - Widoczne - Ukryte - Linki na stronie kanału - "Shortsy -Playlisty -Sklep" - Lista sekcji na stronie kanałów do filtrowania, która musi być oddzielona nowymi liniami. - Filtr sekcji na stronie kanałów - Wyłączony - Włączony - Filtr sekcji na stronie kanałów - Widoczne - Ukryte - Znaki wodne kanałów - Widoczne - Ukryte - Rozdziały nad opisami - Widoczne - Ukryte - Paski z kategoriami - Widoczny - Ukryty - Przycisk od tworzenia klipów - Widoczny - Ukryty - Przycisk od tworzenia Shortsów - Widoczne - Ukryte - Najciekawsze wyniki wyszukiwania (linki) - Widoczny - Ukryty - Przycisk od dziękowania - Widoczne - Ukryte - Czas i przyciski od emotikon - Widoczne - Ukryte - Banery z komentarzami sponsorów - Widoczne - Ukryte - Komentarze na stronie głównej - Widoczne - Ukryte - Komentarze - Widoczne - Ukryte - Na stronie kanałów - Widoczne - Ukryte - Na stronie głównej i między powiązanymi filmami - Widoczne - Ukryte - Na stronie subskrypcji - Widoczna - Ukryta - Sekcja \'Jak powstały te treści\' - Widoczne - Ukryte - Ramki ze zbiórkami - Widoczna - Ukryta - Poświata po dwukrotnym kliknięciu - Widoczny - Ukryty - Przycisk od pobierania - Widoczne - Ukryte - Karty końcowe w filmach - Widoczne - Ukryte - Produkty i rozdziały pod filmami - Widoczne - Ukryte - Rozszeralne półki - Widoczny - Ukryty - Przycisk od napisów na stronie głównej - Lista nazw menu do filtrowania, która musi być oddzielona nowymi liniami. - Filtr menu ze strony głównej - Wyłączony - Włączony - Filtr menu ze strony głównej - Widoczny - Ukryty - Pasek wyszukiwania na stronie głównej - Widoczne - Ukryte - Ankiety na stronie głównej - Włączone - Wyłączone - Precyzyjne przewijanie - Widoczny - Ukryty - Pływający przycisk - Widoczny - Ukryty - Dolny przycisk od mikrofonu - Widoczna - Ukryta - Półka \'Dla Ciebie\' - Widoczne - Ukryte - Pełnoekranowe reklamy - "Blokowane - -Ograniczenie: obrazy postów społeczności w trybie pełnoekranowym mogą być zablokowane" - Zamykane poprzez przycisk zamknięcia - Pełnoekranowe reklamy - Widoczne - Ukryte - Ogólne reklamy - Widoczne - Ukryte - Promocje YouTube Premium - Widoczne - Ukryte - Szare separatory - Widoczne - Ukryte - Nicki - Widoczny - Ukryty - Przycisk od wyszukiwania po obrazie - Widoczne - Ukryte - Półki z obrazkami - Widoczne - Ukryte - Nazwy kanałów pod opisami - Widoczne - Ukryte - Karty z informacjami - Widoczne - Ukryte - Panele z informacjami - Widoczny - Ukryty - Przycisk od sponsorowania - Widoczna - Ukryta - Sekcja kluczowych pojęć - "Strona główna / subskrypcji / wyniki wyszukiwania są filtrowane, by ukryć kontent, który zawiera słowa z filtra. - -Ograniczenia: -• Shortsy nie mogą być ukryte poprzez nazwę kanału -• Niektóre elementy interfejsu użytkownika mogą nie być ukryte -• Wyszukiwanie słowa z filtru może nie pokazywać żadnych wyników" - O filtrowaniu słów - Otoczenie słowa/frazy podwójnym cudzysłowem może zapobiec częściowemu dopasowywaniu tytułów filmów i nazw kanałów.<br><br>Dla przykładu,<br><b>\"ai\"</b> ukryje film: Jak działa AI?</b><br>, lecz nie ukryje: Co oznacza uczciwy użytek?</b> - Uwzględnij całe wyrazy - Wyłączone - Włączone - W komentarzach - Wyłączone - Włączone - Na stronie głównej - "Słowa i frazy, które mają być ukryte, oddzielone nowymi liniami. - -Słowami mogą być nazwy kanałów, jak też jakikolwiek tekst z tytułu filmu. - -Słowa z wielkimi literami w środku muszą być wpisane z odpowiednią wielkością liter (np. iPhone, TikTok, LeBlanc)." - Słowa, które mają być ukryte - Wyłączone - Włączone - W wynikach wyszukiwania - Wyłączone - Włączone - Na stronie subskrypcji - Słowo ukryje wszystkie filmy: %s. - Nie można użyć słowa: %s. - Dodaj cudzysłowy, by użyć słowa: %s. - Słowo zawiera sprzeczne deklaracje: %s. - Słowo jest za krótkie i wymaga cudzysłowu: %s. - Widoczne - Ukryte - Najnowsze posty - Widoczny - Ukryty - Przycisk od najnowszych filmów - Widoczne - Ukryte - Przyciski od łapkowania w górę i dół - Widoczne\n\nUstawienie te dotyczy również transmisji na żywo za pośrednictwem Shortsów. - Ukryte\n\nUstawienie te dotyczy również transmisji na żywo za pośrednictwem Shortsów. - Wiadomości z czatu na żywo - Widoczny\n\nPojawia się również w trybie pełnoekranowym po zamknięciu czatu na żywo. - Ukryty\n\nPojawia się również w trybie pełnoekranowym po zamknięciu czatu na żywo. - Przycisk od ponownego odtwarzania czatu na żywo - Ukrywa filmy poniżej 1000 wyświetleń ze strony głównej, które zostały przesłane z niesubskrybowanych kanałów. - Ukryj filmy z małą ilością wyświetleń - Widoczne - Ukryte - Panele medyczne - Widoczne - Ukryte - Półki z towarami - Widoczne - Ukryte - Playlisty mix - Widoczne - Ukryte - Półki z filmami kinowymi - Widoczny - Ukryty - Pasek nawigacji - Widoczny - Ukryty - Przycisk od przesyłania - Widoczny - Ukryty - Przycisk do strony głównej - Widoczne - Ukryte - Nazwy w pasku nawigacji - Widoczny - Ukryty - Przycisk do biblioteki - Widoczny - Ukryty - Przycisk do powiadomień - Widoczny - Ukryty - Przycisk do Shortsów - Widoczny - Ukryty - Przycisk do strony subskrypcji - Widoczny - Ukryty - Przycisk \'Powiadom mnie\' - Widoczne - Ukryte - Etykiety oznaczające płatne promocje - Widoczne - Ukryte - Pokój gier - Widoczny - Ukryty - Przycisk od automatycznego odtwarzania - Widoczny - Ukryty - Przycisk od napisów - Widoczny - Ukryty - Przycisk od castowania - Widoczny - Ukryty - Przycisk od minimalizowania filmu - Widoczne - Ukryte - Menu od oświetlenia kinowego - Widoczne - Ukryte - Menu od ścieżki dźwiękowej - Widoczny - Ukryty - Opis menu od napisów - Widoczne - Ukryte - Menu od napisów - Widoczne - Ukryte - Menu jakości 1080p Premium - Widoczne - Ukryte - Menu od pomocy i opinii - Widoczne - Ukryte - Menu od słuchania w YouTube Music - Widoczne - Ukryte - Menu od blokady ekranu - Widoczne - Ukryte - Menu od pętli filmu - Widoczne - Ukryte - Menu do większej ilości informacji - Widoczne - Ukryte - Menu od obrazu w obrazie - Widoczne - Ukryte - Menu od prędkości odtwarzania - Widoczne - Ukryte - Menu od elementów sterujących Premium - Widoczny - Ukryty - Opis menu od jakości - Widoczny - Ukryty - Nagłówek menu jakości - Widoczne - Ukryte - Menu od zgłaszania - Widoczne - Ukryte - Menu od wyłącznika czasowego - Widoczne - Ukryte - Menu od stabilnej głośności - Widoczne - Ukryte - Menu od statystyk dla nerdów - Widoczne - Ukryte - Menu od oglądania w VR - Widoczny - Ukryty - Przycisk od pełnego ekranu - Widoczne - Ukryte - Przyciski do poprzedniego i następnego filmu - Widoczne - Ukryte - Półki sklepowe w odtwarzaczu - Widoczny - Ukryty - Przycisk od odtwarzania w YouTube Music - Widoczny - Ukryty - Przycisk od zapisywania do playlisty - Widoczne - Ukryte - Sekcje eksplorowania podcastów - Widoczne - Ukryte - Wyróżnione komentarze - Zmienia wielkość sekcji komentarzy, przez co nie ma możliwości włączenia ponownego odtwarzania czatu na żywo w sekcji komentarzy. - Nie zmienia wielkości sekcji komentarzy, dzięki czemu jest możliwość włączenia ponownego odtwarzania czatu na żywo w sekcji komentarzy. - Sposób ukrywania wyróżnionych komentarzy - Widoczne - Ukryte - Banery z alertami promocyjnymi - Widoczny - Ukryty - Przycisk od komentarzy - Widoczny - Ukryty - Przycisk od łapkowania w dół - Widoczny - Ukryty - Przycisk od łapkowania w górę - Widoczny - Ukryty - Przycisk od czatu na żywo - Widoczny - Ukryty - Przycisk do reszty przycisków - Widoczny - Ukryty - Przycisk od otwierania playlist mix - Widoczny - Ukryty - Przycisk od otwierania playlisty - Widoczny - Ukryty - Przycisk od zapisywania do playlisty - Widoczny - Ukryty - Przycisk od udostępniania - Widoczne - Ukryte - Przyciski na dole odtwarzacza - "Ukrywa następujące rekomendowane filmy: - -• Filmy z tagiem 'Dla wspierających' -• Filmy z takimi frazami jak 'Inne osoby również obejrzały' u dołu filmu" - Ukryj rekomendowane filmy - Widoczne - Ukryte - Rekomendacje YouTube na końcu filmu - Widoczne - Ukryte - Powiązane filmy - "Ustawienie te ogranicza maksymalną ilość układów aplikacji, jakie mogą być załadowane na ekranie odtwarzacza. - -Jeśli układ ekranu odtwarzacza zmieni się w skutek zmian po stronie serwera, niepożądane układy mogą być ukryte na ekranie odtwarzacza." - Widoczny - Ukryty - Przycisk od remiksowania - Widoczny - Ukryty - Przycisk od zgłaszania - Widoczny - Ukryty - Przycisk od nagród - Widoczne - Ukryte - Miniaturki sugestii wyszukiwań - Widoczna - Ukryta - Wiadomość o przewijaniu - Widoczna - Ukryta - Wiadomość o cofnięciu przewijania - Widoczne - Ukryte - Nazwy rozdziałów obok czasu filmu - Widoczny - Ukryty - Widoczne - Ukryte - Paski postępu filmu na stronie głównej - Pasek postępu filmu w odtwarzaczu - Widoczne - Ukryte - Karty z autopromocją - Widoczne - Ukryte - Informacje - Widoczne - Ukryte - Ułatwienia dostępu - Widoczne - Ukryte - Konto - Widoczne - Ukryte - Autoodtwarzanie - Widoczne - Ukryte - Rozliczenia i płatności - Widoczne - Ukryte - Napisy - Widoczne - Ukryte - Połączone aplikacje - Widoczne - Ukryte - Oszczędzanie danych - Widoczne - Ukryte - Ogólne - Widoczne - Ukryte - Zarządzaj całą historią - Widoczny - Ukryty - Czat na żywo - Widoczne - Ukryte - Powiadomienia - Widoczne - Ukryte - Tło - Widoczne - Ukryte - Oglądaj na telewizorze - Widoczne - Ukryte - Centrum rodziny - Widoczne - Ukryte - Wypróbuj eksperymentalne funkcje - Widoczna - Ukryta - Prywatność - Widoczne - Ukryte - Zakupy i subskrypcje - Ukryj elementy menu ustawień YouTube - Menu ustawień YouTube - Widoczne - Ukryte - Preferencje dotyczące jakości filmu - Widoczne - Ukryte - Twoje dane w YouTube - Widoczny - Ukryty - Przycisk od udostępniania - Widoczny - Ukryty - Przycisk do sklepu - Widoczne - Ukryte - Linki do sklepów - Widoczny - Ukryty - Pasek kanału - Widoczny - Ukryty - Przycisk do komentarzy - Wyłączone komentarze lub te z etykietą \"0\" są widoczne. - Wyłączone komentarze lub te z etykietą \"0\" są ukryte. - Przycisk wyłączonych komentarzy - Widoczny - Ukryty - Przycisk od łapkowania w dół - "Widoczne" - "Ukryte" - Pływające przyciski nad tytułami - Widoczne - Ukryte - Etykiety z linkami do całych filmów - Widoczny - Ukryty - Przycisk od greenscreena - Widoczne - Ukryte - Panele z informacjami - Widoczny - Ukryty - Przycisk od sponsorowania - Widoczny - Ukryty - Przycisk od łapkowania w górę - Widoczny\n\nPrzycisk cofania w nagłówku nie będzie ukryty. - Ukryty\n\nPrzycisk cofania w nagłówku nie będzie ukryty. - Nagłówek czatu transmisji na żywo - Widoczny - Ukryty - Przycisk od lokalizacji - Widoczny - Ukryty - Pasek nawigacji - Widoczne - Ukryte - Etykiety oznaczające płatne promocje - Widoczny - Ukryty - Nagłówek po spauzowaniu - Widoczne - Ukryte - Zatrzymane przyciski w odtwarzaczu - Widoczne - Ukryte - Tło przycisku odtwarzania i pauzy - Widoczny - Ukryty - Przycisk od remiksowania - Widoczny - Ukryty - Przycisk od zapisywania muzyki - Widoczny - Ukryty - Przycisk od sugestii wyszukiwań - Widoczny - Ukryty - Przycisk od udostępniania - Widoczne - "Ukryte - -Informacja: -• Tylko półki z nagłówkiem Shorts będą ukryte na stronie głównej kanału" - Na stronie kanału - Widoczne - Ukryte - W historii oglądania - Widoczne - Ukryte - Na stronie głównej i między powiązanymi filmami - Widoczne - Ukryte - W wynikach wyszukiwania - Widoczne - Ukryte - Na stronie subskrypcji - "Ukrywa półki z Shortsami. - -Ograniczenie: Nagłówki z tytułami będą ukryte w wynikach wyszukiwania." - Półki z Shortsami - Widoczny - Ukryty - Przycisk do sklepu - Widoczny - Ukryty - Przycisk od kupowania - Widoczny - Ukryty - Przycisk do dźwięku - Widoczne - Ukryte - Etykiety z metadanymi dźwięku - Widoczne - Ukryte - Naklejki - Widoczny - Ukryty - Przycisk od subskrybowania - Widoczny - Ukryty - Przycisk od superpodziękowania - Widoczne - Ukryte - Oznaczone produkty - Widoczny - Ukryty - Pasek z narzędziami - Widoczny - Ukryty - Przycisk od trendów - Widoczny - Ukryty - Przycisk \'Użyj tego szablonu\' - Widoczny - Ukryty - Przycisk \'Użyj tego dźwięku\' - Widoczne - Ukryte - Tytuły filmów - Widoczny - Ukryty - Przycisk \'Pokaż więcej\' - Widoczne - Ukryte - Komunikaty - Widoczny - Ukryty - Przycisk od rozpoczęcia okresu próbnego - Widoczne - Ukryte - Półki karuzelowe z subskrypcjami - Widoczne - Ukryte - Sugerowane działania - "To ustawienie nie jest już wspierane. - -Zamiast tego, użyj ustawienia 'Ustawienia → Automatyczne odtwarzanie → Automatycznie odtwarzaj następne wideo'." - Widoczne - "Ukryte, lecz wyłącznie, gdy autoodtwarzanie jest wyłączone. - -Autoodtwarzanie można zmienić w ustawieniach YouTube: -'Ustawienia → Autoodtwarzanie → Autoodtwarzanie następnego filmu'" - Sugerowane filmy na końcu trwania filmu - Widoczny - Ukryty - Przycisk od dziękowania - Widoczne - Ukryte - Półki z biletami - Widoczny - Ukryty - Czas filmu - Widoczne - Ukryte - Reakcje czasowe - Widoczny - Ukryty - Przycisk od castowania - Widoczny - Ukryty - Przycisk od przesyłania - Widoczny - Ukryty - Przycisk do powiadomień - Widoczne - Ukryte - Transkrypcje - Widoczne - Ukryte - Reklamy w filmach - "Strona główna / subskrypcje / wyniki wyszukiwania są filtrowane, by ukrywać filmy z ilością wyświetleń mniejszą bądź większą od określonej liczby. - -Ograniczenia: -• Shortsy nie mogą zostać ukryte -• Filmy z 0 wyświetleń nie są filtrowane" - O filtrowaniu filmów po wyświetleniach - Wyłączone - Włączone - Na stronie głównej - Wyłączone - Włączone - W wynikach wyszukiwania - Wyłączone - Włączone - Na stronie subskrypcji - Ukrywa rekomendowane filmy, które mają mniejszą ilość wyświetleń niż zadeklarowana.\n\nZnany problem: Filmy z zerem wyświetleń nie są filtrowane. - Ukryj rekomendowane filmy według ilości wyświetleń - Filmy z wyświetleniami większymi niż ta liczba zostaną ukryte - Popularniejsze - Filmy z wyświetleniami mniejszymi niż ta liczba zostaną ukryte - Mniej popularne - tys. -> 1 000\nmln -> 1 000 000\nmld - > 1 000 000 000\n wyświetleń-> views - Określ swój szablon językowy dla liczby wyświetleń pod każdym filmem w interfejsie użytkownika. Każdy klucz (litera/słowo w twoim języku) - > wartość (znaczenie klucza) musi znajdować się w nowej linii. Klucze muszą znajdować się przed znakiem \"->\". Jeśli zmienisz język aplikacji, musisz zresetować to ustawienie.\n\nPrzykłady:\nAngielski: 10K views = K -> 1000, views -> views\nPolski: 10 tys. wyświetleń = tys -> 1000, wyświetleń -> views - Wyświetl klucze - Widoczne - Ukryte - Banery produktów - Widoczny - Ukryty - Przycisk od wyszukiwania głosowego - Widoczne - Ukryte - Wyniki wyszukiwania stron internetowych - Widoczne - Ukryte - YouTube Doodles - "YouTube Doodles pojawiają się przez kilka dni w ciągu roku. - -Jeśli YouTube Doodles pojawiają się obecnie w Twoim regionie, a opcja ukrywania jest włączona, to pasek filtrowania poniżej paska wyszukiwania również zostanie ukryty." - Widoczna - Ukryta - Poświata przy powiększaniu filmu - Niebieska od Afn - Czerwona od Afn - Własna - Domyślna - MMT - Niebieska MMT - Zielona MMT - Pomarańczowa od MMT - Różowa od MMT - Turkusowa od MMT - Żółta MMT - Niebieska od Revancify - Czerwona od Revancify - Żółta Revancify - Czarna Vanced - Jasna Vanced - Żółta Xisr - YouTube - Zachowuje tryb poziomy, gdy wyłączysz i włączysz ekran w trybie pełnoekranowym. - Ilość milisekund, podczas których tryb poziomy jest wymuszony. - Limit czasu zachowania trybu poziomego - Zachowaj tryb poziomy - Domyślne - Wyłączone - "Włączone - -• Podwójne kliknięcie zmienia zminimalizowany film do większego rozmiaru -• Ponowne podwójne kliknięcie przywraca film do oryginalnego rozmiaru" - Akcje po podwójnym kliknięciu - Wyłączony - Włączony - Gest przeciągnięcia i upuszczenia - Widoczne - Ukryte\n(przesuń miniodtwarzacz, aby rozwinąć lub zamknąć) - Przyciski rozwijania i zamykania - Widoczne - Ukryte - Przyciski przewijania do przodu i wstecz - Widoczne - Ukryte - Podteksty - Przezroczystość nakładki miniodtwarzacza musi wynosić między 0 a 100. - Wartość przezroczystości musi wynosić między 0 a 100, gdzie 0 oznacza przezroczystość - Przezroczystość nakładki - Oryginalny - Telefonowy - Tabletowy - Nowoczesny 1 - Nowoczesny 2 - Nowoczesny 3 - Typ miniodtwarzacza - Przyciski w odtwarzaczu - "Stuknij, by zmienić stan pętli. -Stuknij i przytrzymaj, by zmienić stan pętli z pauzą." - Przycisk od pętli - "Stuknij, by skopiować URL filmu. -Stuknij i przytrzymaj, by skopiować URL filmu z czasem." - "Stuknij, by skopiować URL filmu z czasem. -Stuknij i przytrzymaj, by skopiować czas filmu." - Przycisk od kopiowania URL filmu z czasem - Przycisk od kopiowania URL filmu - Stuknij, by otworzyć aplikacje od pobierania. - Przycisk do pobierania - Stuknij, by wyciszyć bieżący film. Stuknij ponownie, by odciszyć film. - Przycisk od wyciszania - Stuknij i przytrzymaj, by zmienić działanie przycisku - Prędkość odtwarzania została zresetowana: %sx. - "Stuknij, by zmienić prędkość odtwarzania. -Stuknij i przytrzymaj, by zmienić prędkość odtwarzania na 1.0x. Stuknij i przytrzymaj ponownie, by zresetować do domyślnej prędkości." - Przycisk od prędkości - "Stuknij, by wygenerować playlistę ze wszystkimi filmami z kanału od najstarszego do najnowszego. -Stuknij i przytrzymaj, by cofnąć generowanie playlisty." - Przycisk od czasowo uporządkowanych playlist - Stuknij, by otworzyć okno białej listy. -Stuknij i przytrzymaj, by otworzyć okno ustawień białej listy. - Przycisk od białej listy - Natywne pobieranie - Zewnętrzna aplikacja - Metoda pobierania playlist - Natywne pobieranie - Zewnętrzna aplikacja - Metoda pobierania filmów - YouTube Music jest wymagane do zmiany tego ustawienia. -Stuknij tutaj, by pobrać YouTube Music. - Wymaganie wstępne - YouTube Music - RVX Music - Aplikacja otwierana przyciskiem do YouTube Music - Wykluczone - Zawarte - Normalna - Przyciski akcji - Dodatkowe ustawienia - Animacje / Przesyłanie opinii - Przycisk od pobierania - Ustawienia Eksperymentalne - Ograniczenia regionu dla obrazów - Zaimportuj / Wyeksportuj jako plik - Zaimportuj / Wyeksportuj jako tekst - Filtr słów - Inne - Przyciski w odtwarzaczu - Informacje o łatkach - Pod paskiem postępu filmu - Rekomendowane filmy - Półki z Shortsami - Sugerowane akcje - Użyte narzędzie - Filtr ilości wyświetleń - Ukryj bądź pokaż elementy w menu konta i zakładki Ty - Menu konta - Ukryj lub pokazuj przyciski akcji pod odtwarzaczem - Przyciski akcji - Reklamy - Alternatywne miniaturki - Wyłącz oświetlenie kinowe lub obejdź jego ograniczenia - Oświetlenie kinowe - Ukryj lub pokazuj pasek z kategoriami na stronie głównej, nad wynikami wyszukiwania i powiązanymi filmami - Pasek kategorii - Ukryj lub pokazuj pasek kanału pod filmami - Pasek kanału - Ukryj lub pokazuj sekcje na stronie kanałów - Strona kanału - Ukryj lub pokazuj komentarze - Komentarze - Ukryj lub pokazuj posty na stronie głównej i kanałów - Posty - Ukryj komponenty używając własnego filtru - Własny filtr - Ukryj lub pokazuj menu na stronie głównej - Menu ustawień ze strony głównej - Strona główna - Ukryj lub zmień układ trybu pełnoekranowego - Pełny ekran - Ogólne - Wyłącz lub włącz wibracje - Wibracje - Nadpisuje działanie przycisków w aplikacji - Zmień działanie przycisków - Ustawienia importu oraz eksportu - Zaimportuj / Wyeksportuj ustawienia - Zmień styl miniodtwarzacza w aplikacji - Miniodtwarzacz - Pozostałe - Ukryj lub pokazuj przyciski nawigacji - Pasek nawigacji - Informacje na temat zastosowanych łatek. - Informacje o łatkach - Ukryj lub pokazuj przyciski w odtwarzaczu - Przyciski w odtwarzaczu - Ukryj lub zmień elementy menu ustawień filmu - Menu ustawień filmu - Odtwarzacz - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Zmień wygląd paska postępu filmu - Pasek postępu filmu - Ukryj elementy menu ustawień YouTube - Menu ustawień - Ukryj lub pokazuj przyciski w odtwarzaczu Shortsów - Odtwarzacz Shortsów - Shortsy - Oszukuj strumień danych, by zapobiec problemom z odtwarzaniem - Oszukuj strumień danych - Sterowanie przesuwaniem - Ukryj lub pokazuj przyciski na pasku narzędzi, takie jak pasek wyszukiwania, nagłówek i inne - Pasek narzędzi - Ukryj lub pokazuj dodatkowe informacje pod opisami - Opis filmu - Ukryj filmy, używając słów lub wyświetleń - Filtr filmów - Filmy - Zmień ustawienia związane z historią oglądania - Historia oglądania - Górny margines przycisków na dole odtwarzacza musi wynosić między 0 a 32. - Skonfiguruj odstęp od paska postępu filmu do kontenera szybkich akcji, w zakresie od 0 do 32. - Górny margines przycisków na dole odtwarzacza - "Odrzucaj kodek AV1 programu. -Po około 20 sekundach ładowania kodek zostanie zmieniony." - Odrzucaj kodek AV1 programu - Ten sposób powoduje około 20 sekund ładowania. - Przesunięcie - Zmiany prędkości odtwarzania dotyczą tylko bieżącego filmu - Zmiany prędkości odtwarzania dotyczą wszystkich filmów - Zapamiętuj zmiany prędkości odtwarzania - Wyłączone - Włączone - Komunikaty o zmianie domyślnej prędkości odtwarzania - Zmieniono domyślną prędkość odtwarzania na %s. - Zmiany jakości dotyczą tylko bieżącego filmu - Zmiany jakości dotyczą wszystkich filmów - Zapamiętuj zmiany jakości filmu - Wyłączone - Włączone - Komunikaty o zmianie domyślnej jakości filmów - Zmieniono domyślną jakość podczas używania sieci mobilnej na %s. - Nie udało się zmienić jakości obrazu. - Zmieniono domyślną jakość podczas używania Wi-Fi na %s. - "Usuwa okno dialogowe treści ograniczonej do oglądania. -Nie pomija to ograniczeń wiekowych, lecz akceptuje je automatycznie." - Usuń okno dialogowe treści ograniczonej do oglądania - Zastąp kodek AV1 programu kodekiem VP9 - Zmień kodek AV1 programu - Po nicku - Po nazwie - Wyświetlanie tytułu kanału - Stuknij, aby wyświetlić pozostały czas. - Stuknij, aby otworzyć menu od prędkości odtwarzania lub jakości. - Działanie informacji obok czasu - Zamienia przycisk od przesyłania z przyciskiem do ustawień - Zastąp przycisk od przesyłania - "Stuknij, by otworzyć ustawienia YouTube. -Stuknij i przytrzymaj, by otworzyć ustawienia RVX." - "Stuknij, by otworzyć ustawienia RVX. -Stuknij i przytrzymaj, by otworzyć ustawienia YouTube." - Działanie przycisku - Podgląd filmu pojawia się w trybie pełnoekranowym - Podgląd filmu pojawia się nad paskiem postępu filmów - Stary wygląd podglądu filmów - Niewidoczne - Widoczne - Stare menu od jakości filmu - \@nick (Nazwa użytkownika) - Format wyświetlania - Nazwa użytkownika (@nick) - Nazwa użytkownika - Wyłączone - Włączone - Return YouTube Username - "Klucz deweloperski YouTube Data API v3 jest wymagany do zastępowania nicków nazwami użytkownika. - -Dzienny limit kluczy API w planie darmowym wynosi 10 000, a 1 limit służy do zastąpienia nicku nazwą użytkownika dla 1 komentarza. - -Kliknij, by zobaczyć, jak zgłosić klucz API." - O kluczu YouTube Data API - Klucz deweloperski używany do korzystania z API YouTube Data V3. - Klucz YouTube Data API - 1. Przejdź do <a href=%1$s>Utwórz nowy projekt</a>.<br>2. Kliknij przycisk <b>UTWÓRZ</b><br>3. Przejdź do <a href=%2$s>YouTube Data API v3</a>.<br>4. Kliknij przycisk <b>WŁĄCZ</b><br>5. Kliknij przycisk <b>UTWÓRZ DANE LOGOWANIA</b><br>6. Wybierz opcję <b>Dane publiczne</b><br>7. Kliknij przycisk <b>DALEJ</b><br>8. Skopiuj klucz API<br><br>※ Klucz API nie powinien być współdzielony z innymi, dlatego nie jest zawarty w ustawieniach importu/eksportu - Zgłoś klucz deweloperski YouTube Data API - O integracji - Dane o łapkach w dół są dostarczane przez API Return YouTube Dislike. Stuknij tutaj by dowiedzieć się więcej. - ReturnYouTubeDislike.com - Przycisk łapki w górę ładnie wygląda - Przycisk łapki w górę zajmuje mało miejsca - Kompaktowy przycisk łapki w górę - Liczba - Tak - Łapki w dół wyświetlane jako procent - Łapki w dół są niewidoczne - Łapki w dół są widoczne - Return YouTube Dislike - Ukryta - Widoczna - Szacowana ilość polubień - Liczba łapek w dół nie jest dostępna (limit API użytkownika został osiągnięty). - Liczba łapek w dół nie jest dostępna (status %d). - Łapki w dół są tymczasowo niedostępne (API nie reaguje). - Liczba łapek w dół nie jest dostępna (%s). - Odśwież film, aby zagłosować używając Return YouTube Dislike - Niewidoczna - Widoczna - "Widoczna - -Ograniczenie: Liczba łapek w dół może nie być widoczna, gdy użytkownik nie jest zalogowany, bądź jest w trybie incognito." - Liczba łapek w dół w odtwarzaczu Shortsów - Ukryty - Widoczny - Komunikat, jeśli API jest niedostępne - Ukryte - Usuwa śledzące parametry z adresów URL podczas udostępniania linków - Oczyść udostępniane linki - "Frazy takie jak '#', 'Zbiórka', 'Sklep' i 'Produkty' są widoczne" - "Frazy takie jak '#', 'Zbiórka', 'Sklep' i 'Produkty' są ukryte" - Oczyść napisy w filmach - O integracji - sponsor.ajay.app - Dane są dostarczane przez API SponsorBlock. Stuknij tutaj, aby dowiedzieć się więcej i pobrać na inne platformy. - Adres API został zmieniony. - Adres API jest nieprawidłowy. - Adres API został zresetowany. - Wygląd - Kolor został zmieniony. - Kolor: - Nieprawidłowy kod koloru. - Kolor został zresetowany. - Tworzenie nowych segmentów - Zmień sposoby pomijania segmentów - Automatyczne ukrywanie przycisku od pomijania - Przycisk od pomijania jest wyświetlany podczas całego segmentu - Przycisk od pomijania znika po kilku sekundach - Styl przycisku od pomijania - Najlepszy wygląd - Minimalna szerokość - Przycisk od tworzenia nowych segmentów - Ukryty - Widoczny - SponsorBlock - SponsorBlock to system pomijania denerwujących fragmentów w filmach na YouTube. - Przycisk od głosowania - Ukryty - Widoczny - Ogólne - Dokładność tworzenia nowego segmentu - Wartość musi być liczbą dodatnią. - Ilość milisekund, o którą przeskakuje czas podczas używania przycisków od tworzenia segmentów. - Zmień Adres API - Adres SponsorBlock używany do wykonywania połączeń z serwerem. - Minimalny czas segmentu - Nieprawidłowy czas trwania. - Segmenty krótsze niż ta wartość (w sekundach) nie będą pokazywane ani pomijane. - Śledzenie liczby pominięć - Wyłączone - To pozwala systemowi wyników SponsorBlock wiedzieć, ile czasu zostało zaoszczędzone. Rozszerzenie wysyła wiadomość do serwera za każdym razem, gdy pominięty jest segment. - Komunikaty po automatycznych pominięciach - Komunikaty nie są pokazywane. Stuknij tutaj, aby zobaczyć przykład. - Komunikaty są pokazywane, gdy segmenty są automatycznie pomijane. Stuknij tutaj, aby zobaczyć przykład. - Czas bez segmentów - Nie - Pokazuje czas trwania filmu, odejmując czas trwania zsumowanej długości segmentów, pojawia się w nawiasach obok czasu trwania filmu. - Twój prywatny identyfikator użytkownika - Prywatny identyfikator użytkownika musi mieć przynajmniej 30 znaków. - Nie powinno się go dzielić, działa jak hasło. Jeżeli ktoś go posiada, może korzystać z SponsorBlock jako ty np. może wysyłać segmenty. - Przeczytano - Przeczytaj wytyczne SponsorBlock przed wysłaniem jakiegokolwiek segmentu. - Pokaż mi - Postępuj zgodnie z wytycznymi - Wytyczne zawierają zasady i wskazówki dotyczące wysyłania segmentów - Wyświetl wytyczne - Dostosuj: Zaznacz czas rozpoczęcia i zakończenia segmentu - Wybierz kategorię segmentu - Zweryfikuj segment - Segment trwa od %1$02d:%2$02d do %3$02d:%4$02d (%5$d minut i %6$02d sekund)\nCzy jest gotowy do wysłania? - Segment jest od \n\n%1$s\ndo\n%2$s\n\n(%3$s)\n\nGotowe do przesłania? - Czy te czasy są poprawne? - Ta kategoria jest wyłączona w ustawieniach. Włącz kategorię, aby móc wysłać ten segment. - Edytuj segment - Chcesz edytować czas rozpoczęcia czy zakończenia segmentu? - Wprowadzono czas w złym formacie. - Edytuj ręcznie czas segmentu - Przewiń o określony czas (Domyślnie: 150 ms) - Ustawić %s jako początek, bądź koniec nowego segmentu? - koniec - Najpierw zaznacz dwa miejsca na pasku postępu filmu. - początek - obecny - Zobacz i dopilnuj, aby segment pomijał się płynnie. - Opublikuj utworzony segment - Przewiń wstecz o określony czas (Domyślnie: 150 ms) - Początek musi być przed końcem. - Czas zakończenia segmentu - Czas rozpoczęcia segmentu - Nowy segment SponsorBlock - Zresetuj - Zresetuj kolor - Nietematyczny Wypełniacz / Żarty - Segmenty nietematyczne dodawane tylko dla wypełnienia lub humor, który nie jest wymagany do zrozumienia głównej treści filmu. Nie dotyczy segmentów zawierających informacje kontekstowe lub szczegółowe. - Najważniejsze Informacje - Część filmu, która interesuje większość osób. - Przypomnienie O Interakcji (Zasubskrybuj) - Krótkie przypomnienie o łapce w górę, subskrypcji lub obserwowaniu. Jeśli trwa długo lub dotyczy czegoś konkretnego, powinno być oznaczone jako autopromocja. - Przerywnik / Animowane Intro - Fragment bez faktycznej treści. Może to być pauza, statyczna klatka lub powtarzająca się animacja. Nie dotyczy przejść zawierających informacje. - Muzyka: Sekcja Bez Muzyki - Do użytku jedynie w teledyskach. Sekcje teledysków, które nie są uwzględnione w innej kategorii. - Karty / Napisy Końcowe - Napisy końcowe lub gdy pojawia się ekran końcowy. Nie dotyczy zakończeń zawierających informacje. - Zapowiedź / Podsumowanie / Haczyk - Zbiór klipów pokazujących to, co pojawi się lub co pojawiło się w tym filmie, oraz innych fiilmach z tej serii, w którym wszystkie informacje są gdzieś powtarzane. - Nieopłacona / Auto Reklama - Podobne do treści sponsorowanych, z wyjątkiem nieopłaconych lub auto reklam. Obejmuje to sekcje o własnych towarach, darowiznach czy informacjach o tym, z kim współpracowali. - Treści Sponsorowane - Płatna promocja, płatne rekomendacje oraz bezpośrednie reklamy. Nie do autopromocji ani darmowych wyrazów uznania dla kwestii / twórców / stron / produktów, które im się podobają. - Skopiuj - Nie udało się wyeksportować: %s. - Zaimportuj / Wyeksportuj ustawienia - Twoja konfiguracja JSON SponsorBlock może zostać zaimportowana / wyeksportowana do ReVacned Extended i innych platform SponsorBlock. - Twoja konfiguracja JSON SponsorBlock, która może zostać zaimportowana / wyeksportowana do ReVanced Extended i innych platform SponsorBlocka. Zawiera twój prywatny identyfikator użytkownika. Dziel się nią mądrze. - Nie udało się zaimportować: %s. - Ustawienia zostały pomyślnie zaimportowane. - Twoje ustawienia zawierają prywatny identyfikator użytkownika SponsorBlock.\n\nTwój identyfikator użytkownika jest jak hasło i nie powinno być nigdy udostępniane.\n - Nie pokazuj ponownie - Skopiowano ustawienia do schowka. - Pomiń automatycznie - Pomiń automatycznie jeden raz - Pomiń - Najważniejsze Informacje - Pomiń wypełniacz - Przejdź do najważniejszych informacji - Pomiń przypomnienie o interakcji - Pomiń wstęp - Pomiń przerywnik - Pomiń przerywnik - Pomiń fragment bez muzyki - Pomiń zakończenie - Pomiń zapowiedź - Pomiń podsumowanie - Pomiń zapowiedź - Pomiń autoreklamę - Pomiń treści sponsorowane - Pomiń segment - Wyłącz - Pokazuj w pasku postępu filmu - Pokaż przycisk od pomijania - Pominięto wypełniacz. - Pominięto do najważniejszych informacji. - Pominięto irytujące przypomnienie. - Pominięto wstęp. - Pominięto przerywnik. - Pominięto przerywnik. - Pominięto kilka segmentów. - Pominięto fragment bez muzyki. - Pominięto zakończenie. - Pominięto zapowiedź. - Pominięto podsumowanie. - Pominięto zapowiedź. - Pominięto autoreklamę. - Pominięto treści sponsorowane. - Pominięto niewysłany segment. - SponsorBlock jest tymczasowo niedostępny. - SponsorBlock jest tymczasowo niedostępny (status %d). - SponsorBlock jest tymczasowo niedostępny (API nie reaguje). - Statystyki - Statystyki są tymczasowo niedostępne (API nie reaguje). - Ładowanie... - Twoja reputacja to <b>%.2f</b> - Uchroniłeś ludzi przed <b>%s</b> segmentami - %1$s godzin %2$s minut - %1$s minut %2$s sekund - %s sekund - To <b>%s</b> ich życia.<br>Stuknij tutaj, aby zobaczyć tabelę wyników. - Stuknij tutaj, aby zobaczyć globalne statystyki i najlepszych użytkowników. - Tablica wyników SponsorBlock - SponsorBlock jest wyłączony - Pominąłeś <b>%s</b> segmentów - Czy chcesz zresetować ilość pominiętych segmentów? - To <b>%s</b> - Stworzyłeś <b>%s</b> segmentów - Kliknij, by zobaczyć swoje segmenty - Twoja nazwa użytkownika: <b>%s</b> - Stuknij tutaj, aby zmienić nazwę użytkownika - Nie można zmienić nazwy użytkownika: Status: %1$d %2$s. - Nazwa użytkownika została zmieniona. - Nie można wysłać segmentu.\nJuż istnieje. - Nie można wysłać segmentu: %s. - Nie można wysłać segmentu: %s. - Nie można wysłać segmentu (zbyt wiele od tego samego użytkownika lub IP). - SponsorBlock jest tymczasowo niedostępny. - Nie można wysłać segmentu (status: %1$d %2$s). - Segment został wysłany pomyślnie. - Widoczny - Ukryty - Komunikat, jeśli API jest niedostępne - Zmień kategorię - Głos przeciw - Nie można zagłosować na segment: %s. - Nie można ocenić segmentu (API nie reaguje). - Nie można zagłosować na segment (status: %1$d %2$s). - Brak segmentów, na które można zagłosować. - Głos za - Skopiowano ustawienia do schowka. - Skopiowano czas do schowka. (%s) - Skopiowano URL do schowka. - Skopiowano URL z czasem do schowka. - Oryginalna - Łapka w górę - Łapka w górę (Cairo) - Serce - Serce (jaśniejsze) - Ukryta - Animacja po dwukrotnym kliknięciu - Dolny margines panelu meta musi wynosić między 0 a 64. - Skonfiguruj odstęp od paska postępu filmu do panelu meta, w zakresie od 0 do 64. - Dolny margines panelu meta - Wysokość musi wynosić od 0 do 100 (%). - Zmienia wysokość pustej przestrzeni po ukryciu paska nawigacyjnego, od 0 do 100 (%) - Wysokość pustego miejsca (procentowa) - Naciśnij i przytrzymaj czas, aby zmienić status powtarzania Shortsów - Akcja po długim naciśnięciu czasu - "Tytuły filmów w trybie pełnoekranowym są widoczne. - -Ograniczenie: tytuły filmów znikają po stuknięciu w nie." - Tytuły filmów - Jeżeli automatyczne odtwarzanie jest włączone, następny film zostanie odtworzony po ukończeniu odliczania. - Jeżeli automatyczne odtwarzanie jest włączone, następny film zostanie odtworzony bez odliczania. - Pomiń odliczanie do automatycznego odtwarzania - "Pomija wstępnie załadowany bufor na początku filmu, aby natychmiastowo ustawić domyślną jakość filmu. - -Informacje: -• Po uruchomieniu filmu występuje opóźnienie około 0.3 sekundy -• Nie działa w przypadku filmów z HDR, transmisji na żywo i filmów krótszych niż 15 sekund." - Wstępnie załadowany bufor - Ukryte - Widoczne - Komunikaty o pominięciu - Włączenie tej opcji może spowodować problemy z odtwarzaniem filmów. - Pominięto wstępnie załadowany bufor. - Przezroczystość nakładki prędkości odtwarzania musi wynosić między 0 a 8.0. - Wartość nakładki prędkości odtwarzania między 0, a 8. - Wartość nakładki prędkości odtwarzania - "Oszukuje wersję aplikacji do starszej wersji. - -• Zmieni to wygląd aplikacji, lecz mogą wystąpić nieznane efekty uboczne -• Jeśli później opcja ta zostanie wyłączona, stare UI może pozostać do czasu usunięcia danych aplikacji" - Wyłączone - Włączone - 17.33.42 - Przywróć stary wygląd interfejsu użytkownika - 17.41.37 - Przywraca starą półkę do playlist - 18.05.40 - Przywraca stare pole od pisania komentarzy - 18.17.43 - Przywraca stary wygląd menu ustawień filmu - 18.33.40 - Przywraca stary pasek akcji Shortsów - 18.38.45 - Przywraca stare zachowanie domyślnej jakości filmu - 18.48.39 - Wyłącza aktualizowanie wyświetleń i łapek w górę w czasie rzeczywistym - 19.13.37 - Przywraca stary styl animacji liczb - Oszukiwana wersja aplikacji - Wpisz wersję, którą chcesz oszukiwać - Zmień oszukiwaną wersję aplikacji - Oszukiwanie wersji aplikacji - "Wersja aplikacji zostanie oszukana do starszej wersji YouTube. - -Zmieni to wygląd i rzeczy aplikacji, lecz mogą wystąpić nieznane efekty uboczne. - -Jeśli później zostanie to wyłączone, rekomendowane jest usunięcie danych aplikacji, aby zapobiec błędom w interfejsie." - "Oszukuje rozdzielczość urządzenia do maksymalnej wartości. -Wysoka jakość może być odblokowana na niektórych filmach, które wymagają wysokiej rozdzielczości urządzenia, lecz nie na wszystkich." - Oszukaj rozdzielczość urządzenia - Wyłączone - Włączone - Wymuś kodek iOS AVC (H.264) - "Włączenie tego ustawienia może poprawić żywotność baterii i naprawić zacinanie się filmów. - -Kodek AVC (H.264) obsługuje maksymalnie rozdzielczość 1080p, a odtwarzanie filmów wykorzystuje więcej danych internetowych niż VP9 i AV1." - "• Brakuje menu od ścieżki dźwiękowej -• Stabilna głośność jest niedostępna" - "• Brakuje menu od ścieżki dźwiękowej -• Stabilna głośność jest niedostępna" - "• Filmy kinowe lub płatne filmy mogą się nie odtwarzać -• Transmisje na żywo rozpoczynają się od początku -• Filmy mogą kończyć się o 1 sekundę wcześniej -• Kodek opus jest niedostępny" - Efekty uboczne oszukiwania - • Filmy mogą się nie odtwarzać - Ukryta - Widoczna - Informacja w statystykach dla nerdów - "Wyłączone. Odtwarzanie filmów może nie działać" - Włączone - Oszukuj strumień danych - Android - Android TV - Android VR - iOS - Domyślny klient - Wyłączenie tej opcji może spowodować problemy z odtwarzaniem filmów. - Czułość przesuwania gestu jasności musi być pomiędzy 1 a 1000 (%). - Skonfiguruj minimalną odległość dla przesuwania jasności, pomiędzy 1 a 1000 (%).\nIm mniejsza minimalna odległość, tym szybciej zmienia się poziom jasności. - Czułość przesuwania jasności - Wyłączone - Włączone - Przesuwanie podczas trybu blokady ekranu - Automatyczna - Minimalna długość przesunięcia - Minimalna długość przesunięcia - Widoczność tła nakładki przesuwania - Widoczność tła przesuwania - Rozmiar obszaru przesuwania nie może być większy niż 50. - Procentowa wartość obszaru ekranu, gdzie można przesuwać.\n\nNotka: Zmieni to także rozmiar obszaru ekranu dla gestu podwójnego kliknięcia, aby przewinąć film. - Rozmiar obszaru przesuwania - Rozmiar tekstu nakładki przesuwania - Rozmiar tekstu nakładki przesuwania - Ilość milisekund, przez które nakładka jest widoczna - Limit czasu widoczności nakładki - Czułość przesuwania gestu głośności musi być pomiędzy 1 a 1000 (%). - Skonfiguruj minimalną odległość dla przesuwania głośności, pomiędzy 1 a 1000 (%).\n\nIm mniejsza minimalna odległość, tym szybciej zmienia się poziom głośności.\n\nZalecana czułość przesuwania głośności wynosi 100% przy 15 krokach głośności i 10% przy 150 krokach głośności. - Czułość przesuwania głośności - "Zamienia pozycję przycisku od przesyłania z przyciskiem do powiadomień oszukując informacje o urządzeniu. - -• Nawet jeśli zmienisz to ustawienie, może się nic nie zmienić dopóki nie zrestartujesz urządzenia. -• Wyłączenie tego ustawienia powoduje zwiększenie ilości reklam. -• Powinieneś wyłączyć to ustawienie jeśli chcesz, by reklamy w filmach były widoczne." - Nie zamienone - "Zamienione - -Notka: włączenie tej opcji wymusza ukrywanie reklam w filmach." - Zamień przyciski przesyłania i powiadomień - "Wyłączenie tej opcji może spowodować załadowanie się większej ilości reklam z serwera. - -Dodatkowo, reklamy nie będą już blokowane w Shortsach. - -Jeśli opcja nie przynosi skutku, spróbuj przełączyć się na tryb incognito." - Domyślny - RVX Music - %s nie jest zainstalowany. Proszę go zainstalować. - Nazwa pakietu zainstalowanego RVX Music - Nazwa pakietu RVX Music - • Historia oglądania nie działa - "• Stosuje się do ustawień historii oglądania konta Google -• Historia oglądania może nie działać przy używaniu DNS lub VPN" - • Stosuje się do ustawień historii oglądania konta Google - O historii oglądania - Stuknij, aby otworzyć zarządzanie historią oglądania YouTube. - Zarządzaj całą historią - s.youtube.com - www.youtube.com - Wyłączona - Historia - Nie udało się dodać kanału \'%1$s\' do białej listy %2$s. - Kanał \'%1$s\' został dodany do białej listy %2$s. - Brak kanałów na białej liście. - Nie dodano do białej listy. - Nie udało się załadować informacji o kanale. - Dodano do białej listy. - Prędkość odtwarzania - Usunąć kanał \'%1$s\' z białej listy %2$s? - Nie udało się usunąć kanału \'%1$s\' z białej listy %2$s. - Kanał \'%1$s\' został usunięty z białej listy %2$s. - Sprawdź lub usuń listę kanałów dodanych do białej listy - Biała lista kanałów - SponsorBlock - Widoczne - Ukryte - Podsumowanie filmów wygenerowane przez AI - diff --git a/src/main/resources/youtube/translations/pt-rBR/missing_strings.xml b/src/main/resources/youtube/translations/pt-rBR/missing_strings.xml deleted file mode 100644 index 43788e232..000000000 --- a/src/main/resources/youtube/translations/pt-rBR/missing_strings.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - Don\'t show again - Courses / Learning - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - AI-generated video summary section is shown. - AI-generated video summary section is hidden. - Hide AI-generated video summary section - MMT Blue - MMT Green - MMT Orange - MMT Pink - MMT Turquoise - MMT Yellow - Revancify Yellow - Vanced Black - Vanced Light - Xisr Yellow - Adjust: Mark Start and End Time for segment - Verify the Segment - Edit the Segment - Forward by Specified Time (Default: 150ms) - Publish Created Segment - Rewind by Specified Time (Default: 150ms) - diff --git a/src/main/resources/youtube/translations/pt-rBR/strings.xml b/src/main/resources/youtube/translations/pt-rBR/strings.xml deleted file mode 100644 index b8731d662..000000000 --- a/src/main/resources/youtube/translations/pt-rBR/strings.xml +++ /dev/null @@ -1,1698 +0,0 @@ - - - Ativar os controles de acessibilidade para o reprodutor de vídeo? - Seus controles foram modificados porque um serviço de acessibilidade está ativado. - Continuar - "O GmsCore não tem permissão para executar em segundo plano. - -Siga o guia 'Não mate o meu aplicativo' para o seu telefone e aplique as instruções para a sua instalação do MicroG. - -Isto é necessário para o aplicativo funcionar." - "As otimizações da bateria GmsCore devem ser desativadas para evitar problemas. - -Toque no botão continuar e desative as otimizações da bateria." - Abrir site - Ação necessária - Ative as mensagens na nuvem para receber notificações. - Abrir GmsCore - O GmsCore não está instalado. Instale-o. - "DeArrow fornece miniaturas coletivas para vídeos do YouTube. Essas miniaturas costumam ser mais relevantes do que as fornecidas pelo YouTube. - -Se ativado, os URLs dos vídeos serão enviados ao servidor API e nenhum outro dado será enviado. Se um vídeo não tiver miniaturas DeArrow, as capturas originais ou estáticas serão mostradas. - -Toque aqui para saber mais sobre o DeArrow." - DeArrow - URL da API DeArrow inválida. - A URL do ponto final do cache de miniaturas DeArrow. - DeArrow API endpoint - Notificação flutuante não exibida se Derow não estiver disponível. - Notificação flutuante exibida se DeArrow não estiver disponível. - Mostrar notificação flutuante se a API não estiver disponível - DeArrow temporariamente indisponível. (código de status: %s) - DeArrow temporariamente indisponível. - Aba Início - Aba Você - Miniaturas originais - DeArrow & Miniaturas originais - DeArrow & Capturas estátícas - Capturas estáticas - Playlists do reprodutor, recomendações - Resultados de pesquisas - Capturas de vídeo estáticas - As capturas estáticas são feitas do início / meio / fim de cada vídeo. Essas imagens são integradas ao YouTube e nenhuma API externa é usada. - Capturas de vídeo estáticas - Usando capturas estáticas de alta qualidade. - Usando capturas estáticas de qualidade média. As miniaturas carregam mais rápido, mas transmissões ao vivo, vídeos inéditos e muito antigos podem mostrar miniaturas em branco. - Usar capturas estáticas rápidas - Início do vídeo - Meio do vídeo - Fim do vídeo - Hora do vídeo para tirar a foto - Aba de inscrições - Adicionar informação de registro de tempo está desativado. - "Adicionar informação de registro de tempo está ativado." - Adicionar informações de registro de tempo - Adicionar velocidade de reprodução. - Adicionar qualidade do vídeo. - Tipo de informação adicionada - O modo ambiente está desativado no modo de economia de bateria - O modo ambiente está ativado no modo de economia de bateria - Ignorar restrições do modo ambiente - O domínio para buscar imagens de.\nNota: Digite apenas o nome do domínio, ou seja, sem o prefixo \"https\:\/\/\". - Domínio alternativo - Usando o host de imagem original.\n\nAtivar isso pode corrigir imagens ausentes que estão bloqueadas em algumas regiões. - Usando o host de imagem yt4.ggpht.com. - Ignorar restrições de imagem por região - Original - Telefone - Telefone (Máximo 480 dp) - Tablet - Tablet (Min 600 dp) - Alterar layout - Alternadores serão usados. - Alternadores de texto são usados. - Modificar tipo de alternância - O menu de compartilhamento do aplicativo é utilizado. - O menu de compartilhamento do sistema é utilizado. - Alterar menu de compartilhamento - Reprodução automática - Padrão - Pausar - Repetir - Alterar estado de repetição do shorts - Explorar canais - Padrão - Explorar - Jogos - Histórico - Biblioteca - Vídeos curtidos - Ao Vivo - Filmes - Música - Buscar - Shorts - Esportes - Inscrições - Em alta - Assistir mais tarde - Alterar a página inicial - A página inicial muda apenas uma vez. - "Página inicial sempre muda. - -Limitação: O botão voltar na barra de ferramentas pode não funcionar." - Alterar tipo de página inicial - Cabeçalhos genéricos estão ativados. - O cabeçalho premium está ativado. - Alterar cabeçalho do YouTube - Lista de componentes a serem filtradas separadas por uma nova linha. - Filtro personalizado - Filtro personalizado está desativado - Filtro personalizado está ativado - Ativar filtro personalizado - Filtro personalizado inválido: %s - O menu flutuante antigo é usado. - O diálogo personalizado é usado. - Tipo de menu de velocidade de reprodução personalizado - As velocidades personalizadas devem ser inferiores a %sx. Usando valores padrão. - Velocidade personalizada de reprodução inválida. Usando valores padrão. - Adicionar ou alterar as velocidades de reprodução disponíveis - Editar velocidades de reprodução personalizadas - A opacidade do reprodutor deve ser entre 0-100. Redefinir aos valores padrão. - Valor de opacidade entre 0-100, onde 0 é transparente - Opacidade personalizada da sobreposição do reprodutor - Digite o código hexadecimal da cor da barra de progresso. - Valor da cor personalizada da barra de progresso - Para abrir o RVX em um navegador externo, ative \'Abrir links suportados\' e habilite os endereços web suportados. - Abrir configurações padrão do aplicativo - Velocidade de reprodução padrão - Qualidade de vídeo padrão nos dados móveis - Qualidade de vídeo padrão no Wi-Fi - Desativa o modo ambiente apenas para tela cheia. - O modo ambiente está ativado em tela cheia. - O modo ambiente está desativado em tela cheia. - Desativar o modo ambiente em tela cheia - Desativa o modo ambiente. - O modo ambiente está ativado. - O modo ambiente está desativado. - Desativar o modo ambiente - As faixas de áudio automáticas forçadas estão ativadas. - As faixas de áudio automáticas forçadas estão desativadas. - Desativar faixas de áudio automáticas forçadas - As legendas automáticas forçadas estão ativadas. - As legendas automáticas forçadas estão desativadas. - Desativar legendas automáticas forçadas - Os painéis popup do reprodutor automático estão desativados. - Os painéis popup do reprodutor automático estão ativados. - Desativar painéis popup do reprodutor - "A troca automática de playlists de mix está ativada quando a reprodução automática está ativada. - -A reprodução automática pode ser alterada nas configurações do YouTube: -Configurações → Reprodução automática → Reprodução automática do próximo vídeo" - A troca automática de playlists mix está desativada. - Desativar troca de playlists mix - Ativar este recurso irá desativar a mudança automática para o YouTube Mix quando a reprodução automática estiver ativada. - A velocidade de reprodução padrão está ativada na transmissão ao vivo. - A velocidade de reprodução padrão está desativada na transmissão ao vivo. - Desativar a velocidade de reprodução na transmissão ao vivo - A velocidade de reprodução padrão está ativada para música. - "A velocidade de reprodução padrão é desabilitada para música. - -Limitação: esta configuração pode não se aplicar a vídeos que não incluem o banner 'Ouvir no YouTube Music'." - Desativar a velocidade de reprodução para música - O painel de engajamento está ativado. - O painel de engajamento está desativado. - Desativar painel de engajamento - O retorno tátil está ativado. - O retorno tátil está desativado. - Desativar retorno tátil de capítulos - O retorno tátil está ativado. - O retorno tátil está desativado. - Desativar retorno tátil ao deslizar - O retorno tátil está ativado. - O retorno tátil está desativado. - Desativar retorno tátil de busca - O retorno tátil está ativado. - O retorno tátil está desativado. - Desativar retorno tátil ao desfazer - O retorno tátil está ativado. - O retorno tátil está desativado. - Desativar retorno tátil de zoom - O brilho automático HDR está ativado. - O brilho automático HDR está desativado. - Desativar brilho automático HDR - O vídeo HDR está ativado. - O vídeo HDR está desativado. - Desativar vídeo HDR - A orientação do vídeo segue as configurações do dispositivo em tela cheia. - A orientação do vídeo é o modo retrato em tela cheia. - Desativar modo paisagem - Os botões Like e Dislike brilharão quando pressionados. - Os botões Like e Dislike não brilharão quando pressionados. - Desativar brilho do botão Like e Dislike - "Desativar o protocolo QUIC do CronetEngine." - Desativar protocolo QUIC - O shorts irá continuar reproduzindo ao iniciar o aplicativo - O shorts não irá continuar reproduzindo ao iniciar o aplicativo - Desativar continuar a reproduzir Shorts - As rolagens números são animadas. - As rolagens números não são animadas. - Desativar animações de rolagem de números - Os capítulos na barra de progresso estão ativados. - Os capítulos na barra de progresso estão desativados. - Desativar capítulos na barra de progresso - A animação da fonte está ativada acima do botão Curtir. - A animação da fonte está desativada acima do botão Curtir. - Desativar animação do botão Curtir - "Desativar o 'Reproduzindo na velocidade 2x' enquanto segurar. - -Nota: -• Desativar a sobreposição de velocidade restaura o comportamento de 'Deslizar para buscar' do layout antigo. -• Esta configuração não força a ativação da sobreposição de velocidade." - Desativar sobreposição de velocidade - A animação do inicial está ativada. - A animação inicial está desativada. - Desativar animação inicial - "Desabilita as seguintes interações quando a descrição do vídeo é expandida: - -• Toque para rolar. -• Toque e segure para selecionar texto." - Desativar interação com a descrição do vídeo - O codec VP9 está ativado. - "O codec VP9 está desativado. - -• A resolução máxima é 1080p. -• A reprodução de vídeos usará mais dados na internet do que VP9. -• Para fazer com que o HDR reproduza, o vídeo HDR ainda usa o codec VP9." - Desativar codec VP9 - A barra de busca do Cairo está desativada. - "A barra de busca do Cairo está ativada. - -Efeito colateral: o tema Cairo também é aplicado aos pontos de notificação." - Ativar barra de busca do Cairo - A sobreposição de controles preenche a tela inteira. - A sobreposição de controles não preenche a tela inteira. - Ativar sobreposição de controles compactos - A velocidade de reprodução personalizada está desativada. - A velocidade de reprodução personalizada está ativada. - Ativar velocidade de reprodução personalizada - A cor personalizada da barra de progresso está desativada. - A cor personalizada da barra de progresso está ativada. - Ativar cor personalizada da barra de progresso - Os registros de depuração não incluem o buffer. - Os registros de depuração incluem o buffer. - Ativar o registro de depuração do buffer - O registro de depuração está desativado. - O registro de depuração está ativado. - Ativar o registro de depuração - A velocidade de reprodução padrão não se aplica ao Shorts. - A velocidade de reprodução padrão se aplica ao Shorts. - Ativar velocidade de reprodução padrão no Shorts - O navegador externo está desativado. - O navegador externo está ativado. - Ativar navegador externo - A tela de carregamento gradiente está desativada. - A tela de carregamento gradiente está ativada. - Ativar tela de carregamento gradiente - O espaçamento entre os botões de navegação não se torna mais reduzido. - O espaçamento entre os botões de navegação se torna reduzido. - Ativar botões estreitos de navegação - Seguindo a política de redirecionamento padrão - Ignorando redirecionamentos de URL - Ativar abrir links diretamente - Ative o codec OPUS se a resposta do reprodutor incluir o codec OPUS. - Ativar codec OPUS - Não salvar e restaurar o brilho ao sair ou entrar em tela cheia. - Salvar e restaurar o brilho ao sair ou entrar em tela cheia. - Ativar salvar e restaurar brilho - O toque na barra de progresso desativado. - O toque na barra de progresso está ativado. - Ativar toque na barra de progresso - "Isso irá restaurar miniaturas para transmissões ao vivo que não têm miniaturas de barra de progresso. - -O uso de dados da Internet pode ser maior, e as miniaturas de barra de progresso terão um pequeno atraso antes de serem exibidas. - -Este recurso funciona melhor com uma conexão de Internet muito rápida." - As miniaturas na barra de progresso são de qualidade média. - As miniaturas na barra de progresso são de alta qualidade. - Ativar miniaturas de alta qualidade - A marcação de tempo está desativada. - "A marcação de tempo está ativada. - -Problema conhecido: como se trata de um recurso em fase de desenvolvimento pelo Google, o layout pode estar corrompido." - Ativar marcação de tempo - O gesto de brilho está desativado. - O gesto de brilho está ativado. - Ativar gesto de brilho - O retorno tátil está desativado. - O retorno tátil está ativado. - Ativar retorno tátil - O menor valor do gesto de brilho não ativa o brilho automático. - O menor valor do gesto de brilho ativa o brilho automático. - Ativar gesto de brilho automático - Toque para ativar o gesto de deslizar. - Toque e segure para ativar o gesto de deslizar. - Ativar gesto de pressionar para deslizar - Deslizar para cima / para baixo não reproduzirá o vídeo seguinte / anterior. - Deslizar para cima / para baixo reproduzirá o vídeo seguinte / anterior. - Ativar deslizar para alterar o vídeo em tela cheia - O gesto de volume está desativado. - O gesto de volume está ativado. - Ativar gesto de volume - A barra de navegação está opaca. - A barra de navegação está transparente. - Ativar barra de navegação transparente - Entrar em tela cheia quando deslizar para baixo do reprodutor de vídeo está desativado. - Entrar em tela cheia quando deslizar para baixo do reprodutor de vídeo está ativado. - Ativar gestos no painel de exibição - "Ativar essa configuração desativará o botão de configurações na aba Você. - -Nesse caso, por favor, use o seguinte caminho: -Aba Você > Visualizar canal > Menu > Configurações." - Ativar a barra de pesquisa ampla na aba Você - A barra de pesquisa ampla está desativada. - A barra de pesquisa ampla está ativada. - Ativar barra de pesquisa ampla - A barra de pesquisa ampla não inclui o cabeçalho do YouTube. - A barra de pesquisa ampla inclui o cabeçalho do YouTube. - Ativar barra de pesquisa ampla com cabeçalho - Descrição - "Insira um título no painel de descrição do vídeo. -Estes caracteres variam dependendo do seu idioma. -'Expandir descrição do vídeo' pode não funcionar se você salvar uma string incorreta." - Título no painel de descrição do vídeo - A descrição do vídeo é expandida manualmente. - A descrição do vídeo é expandida automaticamente. - Expandir descrição do vídeo - Você deseja continuar? - Redefinir para os valores padrão. - Reinicie para carregar o layout normalmente - "Há um bug do lado do servidor do YouTube que faz com que o texto de números contínuos, como curtidas, visualizações e datas de upload, fique oculto para alguns usuários. - -Uma solução temporária para esse problema é falsificar a versão do aplicativo para 19.13.37. - -Você quer falsificar a versão do aplicativo antes de reiniciá-lo?" - Atualizar e reiniciar - Falha ao exportar configurações. - As configurações foram exportadas com sucesso. - Exportar configurações para um arquivo. - Exportar configurações - Importar - Copiar - Importar ou exportar as configurações como texto. - Importar / Exportar como texto - Falha ao importar as configurações. - Configurações redefinidas para o padrão. - As configurações foram importadas com sucesso. - Importar configurações de um arquivo salvo. - Importar configurações - Redefinir - Pesquisar %s - ReVanced Extended - Aplicativo de download externo - Não instalado - "%1$s não está instalado. -Por favor, baixe %2$s do site." - Aviso - %s não está instalado. Por favor, instale-o. - Nome do pacote do seu aplicativo de download externo instalado, como YTDLnis. - Nome do pacote do aplicativo de download de playlist - Nome do pacote do seu aplicativo de download externo instalado, como NewPipe ou YTDLnis. - Nome do pacote do aplicativo de download de vídeo - "O vídeo será alternado para tela cheia nas seguintes situações: - -• Quando uma marcação de tempo nos comentários é clicada. -• Quando um vídeo é iniciado." - Forçar tela cheia - Lista de nomes do menu de conta a serem filtrados separados por uma nova linha. - Filtro do menu da conta - "Ocultar elementos do menu de contas e aba Você -Alguns componentes podem não ser ocultos" - Ocultar menu de contas - Os cartões de álbum serão exibidos. - Os cartões de álbum estão ocultos. - Ocultar cartões de álbum - As seções de lugares em destaque, jogos e música serão exibidas. - As seções de lugares em destaque, jogos e música estão ocultas. - Ocultar seção de Atributos - O contêiner de visualização da reprodução automática será exibido. - O contêiner de visualização da reprodução automática está oculto. - Ocultar contêiner de visualização de reprodução automática - O botão navegar da loja será exibido. - O botão navegar na loja está oculto. - Ocultar botão de navegar na loja - "Oculta os seguintes painéis: -• Últimas notícias -• Continue assistindo -• Explore mais canais -• Ouça novamente -• Compras -• Assista novamente" - Ocultar painel de carrossel - Exibido no feed. - Oculto no feed. - Ocultar no feed - Exibido em vídeos relacionados. - Oculto em vídeos relacionados. - Ocultar nos vídeos relacionados - Exibido nos resultados de pesquisa. - Oculto nos resultados de pesquisa. - Ocultar nos resultados de pesquisa - As diretrizes do canal serão exibidas. - As diretrizes do canal estão ocultas. - Ocultar diretrizes do canal - O painel de membros do canal será exibido. - O painel de membros do canal está oculto. - Ocultar painel de membros do canal - Os links no topo do perfil do canal serão exibidos. - Os links no topo do perfil do canal estão ocultos. - Ocultar links do perfil do canal - "Shorts -Playlists -Loja" - Lista de nomes de abas de canal para filtrar separados por uma nova linha. - Filtro de aba do canal - O filtro de abas do canal está desativado. - O filtro de abas do canal está ativado. - Ativar filtro de abas do canal - A marca d\'água do canal será exibida. - A marca d\'água do canal está oculta. - Ocultar marca d\'água do canal - As seções de capítulos serão exibidas. - As seções de capítulos estão ocultas. - Ocultar seções de capítulos - O painel de chips será exibido. - O painel de chips está oculto. - Ocultar painel de chips - O botão clipe será exibido. - O botão clipe está oculto. - Ocultar botão clipe - O botão de criação de shorts será exibido. - O botão de criação de shorts está oculto. - Ocultar botão de criação de shorts - Os links de pesquisa em destaque serão exibidos. - Os links de pesquisa em destaque estão ocultos. - Ocultar links de pesquisa em destaque - O botão valeu será exibido. - O botão valeu está oculto. - Ocultar botão valeu - Os botões de marcação de tempo e emoji serão exibidos. - Os botões de marcação de tempo e emoji estão ocultos. - Ocultar botões de marcação de tempo e emoji - O banner de comentários de membros serão exibidos. - O banner de comentários de membros estão ocultos. - Ocultar banner de comentários de membros - A seção de comentários será exibida no feed inicial. - A seção de comentários está oculta no feed inicial. - Ocultar seção de comentários no feed inicial - A seção de comentários será exibida. - A seção de comentários está oculta. - Ocultar seção de comentários - Exibindo no canal. - Oculto no canal. - Ocultar no canal - Exibindo no feed de início e em vídeos relacionados. - Oculto no feed de início e em vídeos relacionados. - Ocultar no feed de início e em vídeos relacionados - Exibido no feed de inscrições. - Oculto no feed de inscrições. - Ocultar no feed de inscrições - A seção Como este conteúdo foi feito será exibida. - A seção Como este conteúdo foi feito está oculta. - Ocultar seção de Conteúdo - A caixa de financiamento coletivo será exibida. - A caixa de financiamento coletivo está oculta. - Ocultar caixa de financiamento coletivo - O filtro de sobreposição de toque duplo será exibido. - O filtro de sobreposição de toque duplo está oculto. - Ocultar filtro de sobreposição de toque duplo - O botão download será exibido. - O botão download está oculto. - Ocultar botão download - Os cartões de tela final serão exibidos. - Os cartões de fim de tela estão ocultos. - Ocultar cartões de fim de tela - Os chips expansíveis serão exibidos. - Os chips expansíveis estão ocultos. - Ocultar chip expansível nos vídeos - Os painéis expansíveis serão exibidos. - Os painéis expansíveis estão ocultos. - Ocultar painéis expansíveis - O botão de legendas será exibido. - O botão de legendas está oculto. - Ocultar botão de legendas no feed - Lista de nomes do menu flutuante para filtrar separados por uma nova linha. - Filtro do menu flutuante do feed - O filtro do menu flutuante do feed está desativado. - O filtro do menu flutuante do feed está ativado. - Ativar filtro do menu flutuante do feed - A barra de pesquisa no feed será exibida. - A barra de pesquisa no feed está oculta. - Ocultar barra de pesquisa no feed - As pesquisas no feed serão exibidas. - As pesquisas no feed estão ocultas. - Ocultar pesquisas no feed - A sobreposição da tira de filme será exibida. - A sobreposição da tira de filme está oculta. - Ocultar sobreposição de tira de filme - O botão flutuante será exibido. - O botão flutuante está oculto. - Ocultar botão flutuante - O botão do microfone flutuante será exibido. - O botão do microfone flutuante está oculto. - Ocultar botão de microfone flutuante - O painel \'Para Você\' será exibido. - O painel \'Para Você\' está oculto. - Ocultar painel \'Para Você\' - Os anúncios em tela cheia serão exibidos. - Os anúncios em tela cheia estão ocultos. - Ocultar anúncios em tela cheia - "Os anúncios em tela cheia estão bloqueados. - -Limitação: A imagem do post da comunidade em tela cheia pode ser bloqueada." - Os anúncios de tela cheia são fechados através do botão Fechar. - Fechar anúncios em tela cheia - Os anúncios gerais serão exibidos. - Os anúncios gerais estão ocultos. - Ocultar anúncios gerais - A promoção do YouTube Premium será exibida. - A promoção do YouTube Premium está oculta. - Ocultar promoção do YouTube Premium - Os separadores cinza serão exibidos. - Os separadores cinza estão ocultos. - Ocultar separador cinza - O identificador será exibido. - O identificador está oculto. - Ocultar identificador - O botão de busca de imagens será exibido. - O botão de busca de imagens está oculto. - Ocultar botão de busca de imagem - Os painéis de imagens serão exibidos. - Os painéis de imagens estão ocultos. - Ocultar painel de imagens - As seções de cartões de informação serão exibidas. - As seções de cartões de informações estão ocultas. - Ocultar seções de cartões de informação - Os cartões de informações serão exibidos. - Os cartões de informações estão ocultos. - Ocultar cartões de informações - Os painéis de informações serão exibidos. - Os painéis de informações estão ocultos. - Ocultar painéis de informações - O botão seja membro será exibido. - O botão seja membro está oculto. - Ocultar botão seja membro - A seção de conceitos principais será exibida. - A seção de conceitos principais está oculta. - Ocultar seção de conceitos principais - "Início / Inscrições / Resultados de pesquisas são filtrados para ocultar o conteúdo que corresponde as palavras-chave. - -Limitações: -• Alguns Shorts podem não ser ocultos. -• Alguns componentes da UI podem não ser ocultos. -• A pesquisa por uma palavra-chave pode não apresentar resultados." - Sobre a filtragem por palavra-chave - Colocar uma palavra-chave/frase entre aspas duplas evitará correspondências parciais de títulos de vídeo e nomes de canais.<br><br>Por exemplo,<br><b>\"ia\"</b> ocultará o vídeo: <b>Como funciona a IA?</b><br>mas não ocultará: <b>O que significa uso justo?</b> - Corresponder palavras inteiras - Os comentários não são filtrados. - Os comentários são filtrados. - Ocultar comentários por palavras-chave - Os vídeos no feed de início não são filtrados. - Os vídeos no feed de início são filtrados. - Ocultar vídeos no feed de início por palavras-chave - "Palavras-chave e frases a serem ocultadas, separadas por novas linhas. -Palavras com letras maiúsculas no meio devem ser inseridas com maiúsculas (ou seja: iPhone, TikTok, LeBlanc)." - Palavras-chave para ocultar - Os resultados da pesquisa não são filtrados. - Os resultados da pesquisa são filtrados. - Ocultar resultados de pesquisa por palavras-chave - Os vídeos no feed de inscrições não são filtrados. - Os vídeos no feed de inscrições são filtrados. - Ocultar vídeos no feed de inscrições por palavras-chave - Palavra-chave \'%1$s\' irá ocultar todos os vídeos. - Palavra-chave inválida. Não pode usar: \'%s\' como um filtro - Adicione aspas para usar a palavra-chave: %s. - A palavra-chave tem declarações conflitantes: %s. - A palavra-chave é muito curta e requer aspas: %s. - As últimas postagens serão exibidas. - As últimas postagens estão ocultas. - Ocultar últimas postagens - O botão \'Vídeos recentes\' será exibido. - O botão \'Vídeo recentes\' está oculto. - Ocultar o botão \'Vídeos recentes\' - Os botões de like e deslike serão exibidos. - Os botões de like e deslike estão ocultos. - Ocultar botões de like e deslike - As mensagens de bate-papo ao vivo serão exibidas.\n\nEssa configuração se aplica a vídeos do Shorts também. - As mensagens de bate-papo ao vivo estão ocultas.\n\nEssa configuração se aplica a vídeos do Shorts também. - Ocultar mensagens de bate-papo ao vivo - O botão de repetição do chat ao vivo será exbido.\n\nEle aparece em tela cheia ao fechar o chat ao vivo. - O botão de repetição do chat ao vivo está oculto.\n\nEle aparece em tela cheia ao fechar o chat ao vivo. - Ocultar botão de repetição do chat ao vivo - Ocultar vídeos com menos de 1.000 visualizações do feed de início que foram enviadas de canais não inscritos. - Ocultar vídeos com baixas visualizações - Os painéis médicos serão exibidos. - Os painéis médicos estão ocultos. - Ocultar painéis médicos - Os painéis de mercadoria serão exibidos. - Os painéis de mercadoria estão ocultos. - Ocultar painel de mercadoria - A playlist mix será exibida. - A playlist mix está oculta. - Ocultar playlists mix - Os painéis de filmes serão exibidos. - Os painéis de filmes estão ocultos. - Ocultar painel de filmes - A barra de navegação será exibida. - A barra de navegação está oculta. - Ocultar barra de navegação - O botão de criação será exibido. - O botão de criação está oculto. - Ocultar botão de criação - O botão início será exibido. - O botão início está oculto. - Ocultar botão Início - O rótulo de navegação será exibido. - O rótulo de navegação está oculto. - Ocultar rótulo de navegação - O botão biblioteca será exibido. - O botão biblioteca está oculto. - Ocultar botão Biblioteca - O botão de notificações será exibido. - O botão de notificações está oculto. - Ocultar botão de notificações - O botão Shorts será exibido. - O botão Shorts está oculto. - Ocultar botão Shorts - O botão de inscrições será exibido. - O botão de inscrições está oculto. - Ocultar botão Inscrições - O botão \'Notifique-me\' será exibido. - O botão \'Notifique-me\' está oculto. - Ocultar botão \'Notifique-me\' - O rótulo de promoção pago será exibido. - O rótulo de promoção pago está oculto. - Ocultar rótulo de promoção paga - As reproduções serão exibidas. - As reproduções estão ocultas. - Ocultar Reproduções - O botão de reprodução automática será exibido. - O botão de reprodução automática está oculto. - Ocultar botão de reprodução automática - O botão de legendas será exibido. - O botão de legendas está oculto. - Ocultar botão de legendas - O botão de transmissão será exibido. - O botão de transmissão está oculto. - Ocultar botão de transmissão - O botão minimizar será exibido. - O botão minimizar está oculto. - Ocultar botão minimizar - O menu modo ambiente será exibido. - O menu modo ambiente está oculto. - Ocultar menu do Modo Ambiente - O menu faixa de áudio será exibido. - O menu faixa de áudio está oculto. - Ocultar menu faixa de áudio - O rodapé do menu de legendas será exibido. - O rodapé do menu de legendas está oculto. - Ocultar rodapé do menu de legendas - O menu legendas será exibido. - O menu legendas está oculto. - Ocultar menu legendas - O menu 1080p Premium será exibido. - O menu 1080p Premium está oculto. - Ocultar o menu 1080p Premium - O menu ajuda & feedback será exibido. - O menu ajuda & feedback está oculto. - Ocultar menu ajuda & feedback - O menu ouvir com o YouTube Music será exibido. - O menu ouvir com o YouTube Music está oculto. - Ocultar menu ouvir com o YouTube Music - O menu tela de bloqueio será exibido. - O menu tela de bloqueio está oculto. - Ocultar menu tela de bloqueio - O menu exibir o vídeo no modo de repetição será exibido. - O menu exibir o vídeo no modo de repetição está oculto. - Ocultar menu exibir o vídeo no modo de repetição - O menu mais informações será exibido. - O menu mais informações está oculto. - Ocultar menu mais informações - O menu picture-in-picture será exibido. - O menu picture-in-picture está oculto. - Ocultar menu picture-in-picture - O menu velocidade de reprodução será exibido. - O menu velocidade de reprodução está oculto. - Ocultar menu velocidade de reprodução - O menu de controles premium será exibido. - O menu de controles premium está oculto. - Ocultar menu de controles premium - O rodapé do menu de qualidade será exibido. - O rodapé do menu de qualidade está oculto. - Ocultar rodapé do menu de qualidade - O cabeçalho do menu de qualidade será exibido. - O cabeçalho do menu de qualidade está oculto. - Ocultar cabeçalho do menu de qualidade - O menu denunciar será exibido. - O menu denunciar está oculto. - Ocultar menu denunciar - O menu Timer de suspensão será exibido. - O menu Timer de suspensão está oculto. - Ocultar menu de Timer de suspensão - O menu volume estável será exibido. - O menu volume estável está oculto. - Ocultar menu volume estável - O menu estatísticas para nerds será exibido. - O menu estatísticas para nerds está oculto. - Ocultar menu estatísticas para nerds - O menu assistir em VR será exibido. - O menu assistir em VR está oculto. - Ocultar menu assistir em VR - O botão de tela cheia será exibido. - O botão de tela cheia está oculto. - Ocultar botão de tela cheia - Os botões serão exibidos. - Os botões estão ocultos. - Ocultar o botão anterior & próximo - O painel de compras do reprodutor será exibido. - O painel de compras do reprodutor está oculto. - Ocultar painel de compras do reprodutor - O botão YouTube Music será exibido. - O botão YouTube Music está oculto. - Ocultar botão do YouTube Music - O botão salvar para lista de reprodução será exibido. - O botão salvar para lista de reprodução está oculto. - Ocultar botão salvar para lista de reprodução - As seções de podcast serão exibidas. - As seções de podcast estão ocultas. - Ocultar seções de podcast - A prévia do comentário será exibida. - A prévia do comentário está oculta. - Ocultar prévia de comentário - Isto altera o tamanho da seção de comentários, por isso é impossível abrir um replay ao vivo do chat na seção de comentários. - Isto não altera o tamanho da seção de comentários, por isso é possível abrir o replay do chat ao vivo na seção de comentários. - Ocultar tipo de comentário de visualização - O banner de alerta de promoção será exibido. - O banner de alerta de promoção está oculto. - Ocultar banner de alerta de promoção - O botão comentários será exibido. - O botão comentários está oculto. - Ocultar botão comentários - O botão dislike será exibido. - O botão dislike está oculto. - Ocultar botão dislike - O botão curtir será exibido. - O botão curtir está oculto. - Ocultar botão curtir - O botão chat ao vivo será exibido. - O botão chat ao vivo está oculto. - Ocultar botão chat ao vivo - O botão mais será exibido. - O botão mais está oculto. - Ocultar botão mais - O botão de abrir a playlist mix será exibido. - O botão de abrir a playlist mix está oculto. - Ocultar botão abrir playlist mix - O botão de abrir lista de reprodução será exibido. - O botão de abrir lista de reprodução está oculto. - Ocultar botão abrir lista de reprodução - O botão salvar será exibido. - O botão salvar está oculto. - Ocultar botão salvar - O botão compartilhar será exibido. - O botão compartilhar está oculto. - Ocultar botão compartilhar - O contêiner de ações rápidas será exibido. - O contêiner de ações rápidas está oculto. - Ocultar contêiner de ações rápidas - "Oculta os seguintes vídeos recomendados: - -• Vídeos com a tag 'Somente para membros' -• Vídeos com frases como 'Pessoas também assistiram' na parte inferior do vídeo -• Vídeos enviados de canais não inscritos com menos de 1,000 visualizações" - Ocultar vídeos recomendados - A sobreposição de vídeo relacionado será exibida. - A sobreposição de vídeo relacionado está oculta. - Ocultar sobreposição de vídeo relacionado - Os vídeos relacionados serão exibidos. - Os vídeos relacionados estão ocultos. - Ocultar vídeos relacionados - "Esta configuração limita o número máximo de layouts que podem ser carregados na tela do reprodutor. - -Se o layout da tela do reprodutor mudar devido a alterações no lado do servidor, layouts não intencionais podem ficar ocultos na tela do reprodutor." - O botão remix será exibido. - O botão remix está oculto. - Ocultar botão remix - O botão denunciar será exibido. - O botão denunciar está oculto. - Ocultar botão denunciar - O botão recompensas será exibido. - O botão recompensas está oculto. - Ocultar botão recompensas - As miniaturas do histórico de pesquisa serão exibidas. - As miniaturas do histórico de pesquisa estão ocultas. - Ocultar miniatura do termo de pesquisa - A mensagem de busca será exibida. - A mensagem de busca está oculta. - Ocultar mensagem de busca - A mensagem de desfazer busca será exibida. - A mensagem de desfazer busca está oculta. - Ocultar mensagem de desfazer busca - Os rótulos dos capítulos ao lado da marcação de tempo serão exibidos. - Os rótulos dos capítulos ao lado da marcação de tempo estão ocultos. - Ocultar rótulos dos capítulos na barra de progresso - A barra de progresso no reprodutor de vídeo será exibida. - A barra de progresso no reprodutor de vídeo está oculta. - A barra de progresso em miniaturas será exibida. - A barra de progresso em miniaturas está oculta. - Ocultar barra de progresso nas miniaturas do vídeo - Ocultar barra de progresso no reprodutor de vídeo - Os cartões auto-patrocinados serão exibidos. - Os cartões auto-patrocinados estão ocultos. - Ocultar cartões auto-patrocinados - O menu Sobre será exibido. - O menu Sobre está oculto. - Ocultar menu Sobre - O menu Acessibilidade será exibido. - O menu Acessibilidade está oculto. - Ocultar menu Acessibilidade - O menu Conta será exibido. - O menu Conta está oculto. - Ocultar menu Conta - O menu Reprodução automática será exibido. - O menu Reprodução automática está oculto. - Ocultar menu Reprodução automática - O menu de Faturamento e pagamentos será exibido. - O menu de Faturamento e pagamentos está oculto. - Ocultar menu Faturamento e pagamentos - O menu Legendas será exibido. - O menu Legendas está oculto. - Ocultar menu Legendas - O menu Apps conectados será exibido. - O menu Apps conectados está oculto. - Ocultar menu Apps conectados - O menu Economia de dados será exibido. - O menu Economia de dados está oculto. - Ocultar menu Economia de dados - O menu Geral será exibido. - O menu Geral está oculto. - Ocultar menu Geral - O menu Gerencie todo o histórico será exibido. - O menu Gerencie todo o histórico está oculto. - Ocultar menu Gerencie todo o histórico - O menu Chat o vivo será exibido. - O menu Chat o vivo está oculto. - Ocultar menu Chat ao vivo - O menu Notificações será exibido. - O menu Notificações está oculto. - Ocultar Menu Notificações - O menu Segundo plano será exibido. - O menu Segundo plano está oculto. - Ocultar menu Segundo plano - O menu Assistir na TV será exibido. - O menu Assistir na TV está oculto. - Ocultar menu Assistir na TV - O menu do Central da família será exibido. - O menu do Central da família está oculto. - Ocultar menu Central da família - O menu Testar os novos recursos experimentais será exibido. - O menu Testar os novos recursos experimentais está oculto. - Ocultar menu Testar os novos recursos experimentais - O menu Privacidade será exibido. - O menu Privacidade está oculto. - Ocultar menu Privacidade - O menu Compras e assinaturas será exibido. - O menu Compras e assinaturas está oculto. - Ocultar menu Compras e assinaturas - Ocultar elementos no menu de configurações do YouTube. - Ocultar menu de configurações do YouTube - O menu Preferências de qualidade de vídeo será exibido. - O menu Preferências de qualidade de vídeo está oculto. - Ocultar menu Preferências de qualidade de vídeo - O menu Seus dados no YouTube será exibido. - O menu Seus dados no YouTube está oculto. - Ocultar menu Seus dados no YouTube - O botão compartilhar será exibido. - O botão compartilhar está oculto. - Ocultar botão compartilhar - O botão comprar será exibido. - O botão comprar está oculto. - Ocultar botão comprar - Os links de compras serão exibidos. - Os links de compras estão ocultos. - Ocultar links de compras - A barra do canal será exibida. - A barra do canal está oculta. - Ocultar barra de canal - O botão comentários será exibido. - O botão comentários está oculto. - Ocultar botão comentários - O botão de comentários desativado ou com rótulo \"0\" será exibido. - O botão de comentários desativado ou com rótulo \"0\" está oculto. - Ocultar o botão de comentários desativados - O botão dislike será exibido. - O botão dislike está oculto. - Ocultar botão dislike - "Os botões flutuantes como \"Usar este som\" serão exibidos na aba do canal do Shorts." - "Os botões flutuantes como \"Usar este som\" estão ocultos na aba do canal do Shorts." - Ocultar botão flutuante - O rótulo de link de vídeo será exibido. - O rótulo de link de vídeo está oculto. - Ocultar rótulo completo do link do vídeo - O botão de tela verde será exibido. - O botão de tela verde está oculto. - Ocultar botão de tela verde - Os painéis de informação serão exibidos. - Os painéis de informação estão ocultos. - Ocultar painéis de informações - O botão seja membro será exibido. - O botão seja membro está oculto. - Ocultar botão seja membro - O botão curtir será exibido. - O botão curtir está oculto. - Ocultar botão curtir - O cabeçalho do chat ao vivo será exibido.\n\nO botão Voltar no cabeçalho não será ocultado. - O cabeçalho do chat ao vivo está oculto.\n\nO botão Voltar no cabeçalho não será ocultado. - Ocultar o cabeçalho do chat ao vivo - O botão de localização será exibido. - O botão localização está oculto. - Ocultar botão localização - A barra de navegação será exibida. - A barra de navegação está oculta. - Ocultar barra de navegação - O rótulo de promoção pago será exibido. - O rótulo de promoção pago está oculto. - Ocultar rótulo de promoção paga - O cabeçalho pausado será exibido. - O cabeçalho pausado está oculto. - Ocultar cabeçalho pausado - Os botões de sobreposição pausados serão exibidos. - Os botões de sobreposição pausados estão ocultos. - Ocultar botões de sobreposição pausados - O fundo do botão será exibido. - O fundo do botão está oculto. - Ocultar fundo do botão Play & Pause - O botão remix será exibido. - O botão remix está oculto. - Ocultar botão remix - O botão Salvar música será exibido. - O botão Salvar música está oculto. - Ocultar botão Salvar música - O botão de sugestões de pesquisa será exibido. - O botão de sugestões de pesquisa está oculto. - Ocultar botão de sugestões de pesquisa - O botão compartilhar será exibido. - O botão compartilhar está oculto. - Ocultar botão compartilhar - Exibindo no canal. - "Oculto no canal. - -info: -• Somente painéis com o cabeçalho Shorts na aba home são ocultadas." - Ocultar no canal - Exibindo no histórico de exibição. - Oculto no histórico de exibição. - Ocultar no histórico de exibição - Exibindo no feed de início e em vídeos relacionados. - Oculto no feed de início e em vídeos relacionados. - Ocultar no feed de início e em vídeos relacionados - Exibido nos resultados de pesquisa. - Oculto nos resultados de pesquisa. - Ocultar nos resultados de pesquisas - Exibido no feed de inscrições. - Oculto no feed de inscrições. - Ocultar no feed de inscrições - "Oculta o painel de shorts. - -Limitação: Os cabeçalhos oficiais nos resultados da pesquisa serão ocultados." - Ocultar painel de Shorts - O botão comprar será exibido. - O botão comprar está oculto. - Ocultar botão comprar - O botão de compras será exibido. - O botão de compras está oculto. - Ocultar botão de Compras - O botão som será exibido. - O botão som está oculto. - Ocultar botão som - O rótulo de metadados será exibido. - O rótulo de metadados está oculto. - Ocultar rótulo de metadados de som - Os stickers serão exibidos. - Os stickers estão ocultos. - Ocultar stickers - O botão de inscrição será exibido. - O botão de inscrição está oculto. - Ocultar botão de inscrição - O botão de Super Valeu será exibido. - O botão de Super Valeu está oculto. - Ocultar botão de Super Valeu - Os produtos marcados serão exibidos. - Os produtos marcados estão ocultos. - Ocultar produtos marcados - A barra de ferramentas será exibida. - A barra de ferramentas está oculta. - Ocultar barra de ferramentas - O botão de tendências será exibido. - O botão de tendências está oculto. - Ocultar botão Tendências - O botão Usar template será exibido. - O botão Usar template está oculto. - Ocultar botão Usar template - O botão Usar este som será exibido. - O botão Usar este som está oculto. - Ocultar botão Usar este som - O título será exibido. - O título está oculto. - Ocultar título do vídeo - O botão \'Mostrar mais\' será exibido. - O botão \'Mostrar mais\' está oculto. - Ocultar botão \'Mostrar mais\' - A barra de busca será exibida. - A barra de busca está oculta. - Ocultar barra de busca - O botão iniciar teste será exibido. - O botão iniciar teste está oculto. - Ocultar botão iniciar teste - As inscrições em carrossel serão exibidas. - As inscrições em carrossel estão ocultas. - Ocultar inscrições em carrossel - As ações sugeridas serão exibidas. - As ações sugeridas estão ocultas. - Ocultar ações sugeridas - "Esta configuração foi depreciada. - -Ao invés disso, use a configuração 'Configurações → Reprodução automática → Reprodução automática do próximo vídeo'." - A tela de vídeo sugerido no fim é exibida. - "A tela de fim de vídeo sugerido fica oculta quando a reprodução automática é desativada. - -A reprodução automática pode ser alterada nas configurações do YouTube: -'Configurações → Reprodução automática → Reprodução automática do próximo vídeo'" - Ocultar a tela final de vídeo sugerido - O botão valeu será exibido. - O botão valeu está oculto. - Ocultar botão valeu - Os painéis de ingressos serão exibidos. - Os painéis de ingressos estão ocultos. - Ocultar painel de ingressos - A marcação de tempo será exibida. - A marcação de tempo está oculta. - Ocultar marcação de tempo - As reações temporizadas são exibidas. - As reações temporizadas estão ocultas. - Ocultar reações temporizadas - O botão de transmissão será exibido. - O botão de transmissão está oculto. - Ocultar botão de transmissão - O botão de criação será exibido. - O botão de criação está oculto. - Ocultar botão de criação - O botão de notificação será exibido. - O botão de notificação está oculto. - Ocultar botão de notificação - As seções de transcrição serão exibidas. - As seções de transcrição estão ocultas. - Ocultar seções de transcrição - Os anúncios de vídeo serão exibidos. - Os anúncios de vídeo estão ocultos. - Ocultar anúncios de vídeo - "Início / Inscrições / Resultados de pesquisa são filtrados para ocultar vídeos com visualização menor ou maior que um número especificado. - -Limitações: -• Shorts não podem ser ocultos. -• Vídeos com 0 visualizações não são filtrados." - Sobre a filtragem da contagem de visualizações - Os vídeos no feed de início não são filtrados. - Os vídeos no feed de início são filtrados. - Ocultar vídeos no feed de início por visualizações - Os resultados da pesquisa não são filtrados. - Os resultados da pesquisa são filtrados. - Ocultar resultados de pesquisa por visualizações - Os vídeos no feed de inscrições não são filtrados. - Os vídeos no feed de inscrições são filtrados. - Ocultar vídeos no feed de inscrições por visualizações - Oculte vídeos recomendados com menos de um determinado número de visualizações. - Ocultar vídeos recomendados por visualizações - Vídeos com visualizações maiores que este número serão ocultados. - Maior que as visualizações - Vídeos com visualizações menores do que este número serão ocultados. - Menos que as visualizações - K -> 1 000\nM -> 1 000 000\nB -> 1 000 000 000\nvisualizações -> visualizações - Especifique seu modelo de idioma para o número de visualizações mostradas em cada vídeo na interface do usuário. Cada chave (uma letra/palavra no seu idioma) sinal -> (significado da chave) deve estar em uma nova linha. Chaves vão antes do sinal \"->\". Se você alternar o idioma do aplicativo ou do sistema, você terá que redefinir esta configuração.\n\nExemplos:\nInglês: 10K views = K -> 1000, views -> views\nEspanhol: 10K vistas = K -> 1000, vistas -> views - Chaves de visualização - O banner de produtos será exibido. - O banner de produtos está oculto. - Ocultar visualização de banner de produtos - O botão de pesquisa por voz será exibido. - O botão de pesquisa por voz está oculto. - Ocultar botão de pesquisa por voz - Os resultados de pesquisa web serão exibidos. - Os resultados de pesquisa web estão ocultos. - Ocultar resultados da pesquisa web - Os Doodles do YouTube serão exibidos. - Os Doodles do YouTube estão ocultos. - Ocultar Doodles do YouTube - "Os Doodles do YouTube aparecem alguns dias por ano. - -Se um Doodle do YouTube estiver sendo exibido na sua região e essa configuração de ocultação estiver ativada, a barra de filtro abaixo da barra de pesquisa também ficará oculta." - A sobreposição de zoom será exibida. - A sobreposição de zoom está oculta. - Ocultar sobreposição de zoom - Afn Blue - Afn Red - Personalizado - Padrão - MMT - Revancify Blue - Revancify Red - YouTube - Mantenha o modo paisagem ao desligar e ligar em tela cheia. - A quantidade de milissegundos que o modo paisagem é forçado. - Tempo limite para manter o modo paisagem - Manter o modo paisagem - Padrão - A ação de toque duplo está desativada. - "A ação de toque duplo está ativada. - -• Moderno 1: Toque duas vezes para alterar o vídeo minimizado para um tamanho maior. -• Moderno 2, 3: Toque duas vezes para fechar o vídeo minimizado." - Ação de toque duplo - Arrastar e soltar está desativado. - Arrastar e soltar está ativado. - Ativar arrastar e soltar - Os botões de expansão e fechamento serão exibidos. - Os botões estão ocultos.\n(deslize o mini reprodutor para expandir ou fechar) - Ocultar botões de expansão e fechamento - Os botões avançar e retroceder serão exibidos. - Os botões avançar e retroceder estão ocultos. - Ocultar os botões avançar e retroceder - Os subtextos serão exibidos. - Os subtextos estão ocultos. - Ocultar subtextos - A opacidade da sobreposição do mini reprodutor deve ser entre 0-100. Redefinir aos valores padrão. - Valor da opacidade entre 0-100, onde 0 é transparente. - Opacidade da sobreposição - Original - Telefone - Tablet - Moderno 1 - Moderno 2 - Moderno 3 - Tipo de mini reprodutor - Botão de sobreposição - "Toque para alternar entre estados sempre repetidos. -Toque e segure para alternar a pausa após estados repetidos." - Exibir botão de repetição automática - "Toque para copiar a URL do vídeo. -Toque e segure para copiar a URL do vídeo com marcação de tempo." - "Toque para copiar a URL do vídeo com marcação de tempo. -Toque e segure para copiar a marcação de tempo do vídeo." - Exibir botão copiar URL com marcação de tempo - Exibir botão copiar URL do vídeo - Toque para iniciar o aplicativo de download externo. - Exibir botão de download externo - Toque para silenciar o volume do vídeo atual. Toque novamente para desativar. - Exibir botão de volume mudo - Toque e segure para alterar o estado do botão. - Velocidade de reprodução redefinida (1,0x). - "Toque para abrir a caixa de diálogo de velocidade. -Toque e segure para redefinir a velocidade do vídeo para 1.0x. -Toque e segure novamente para redefinir para a velocidade padrão." - Exibir botão de velocidade - "Toque para gerar uma lista de reprodução de todos os vídeos do canal, do mais antigo para o mais recente. -Toque e segure para desfazer." - Exibir botão de lista de reprodução ordenada por tempo - Toque para abrir a caixa de diálogo da lista branca. -Toque e segure para abrir a caixa de diálogo de configuração da lista branca. - Exibir botão de lista branca - O botão de download de playlist nativo abre o download nativo do aplicativo. - O botão de download de playlist nativo abre seu aplicativo de download externo. - Substituir o botão de download da playlist - O botão de download de vídeo nativo abre o download nativo do aplicativo. - O botão de download de vídeo nativo abre seu aplicativo de download externo. - Substituir o botão de download de vídeo - O YouTube Music é necessário para substituir a ação do botão. Toque aqui para baixar o YouTube Music. - Pré-requisito - O botão do YouTube Music abre o aplicativo nativo. - O botão do YouTube Music abre o RVX Music. - Substituir botão do YouTube Music - Excluído - Incluído - Normal - Botões de ação - Configurações adicionais - Animação / Feedback - Botão de download - Sinalizadores experimentais - Restrições de imagem por região - Importar / Exportar como arquivo - Importar / Exportar como texto - Filtro por palavras-chave - Outros - Botões de ação rápida - Informações do patch - Ações rápidas - Vídeo recomendado - Painel de shorts - Ações sugeridas - Ferramenta usada - Filtro por contagem de visualização - Ocultar ou mostrar elementos no menu de contas e na aba Você. - Menu da Conta - Ocultar ou mostrar botões de ação sob os vídeos. - Botões de ação - Anúncios - Miniaturas alternativas - Ignorar restrições do modo ambiente ou desativar o modo ambiente. - Modo ambiente - Ocultar ou mostrar a barra de categorias no feed, pesquisa e vídeos relacionados. - Barra de categoria - Ocultar ou mostrar componentes da barra de canal sob os vídeos. - Barra do canal - Ocultar ou mostrar componentes no perfil do canal. - Perfil do canal - Ocultar ou mostrar componentes da seção de comentários. - Comentários - Ocultar ou exibir postagens na comunidade no feed e no canal. - Publicações da comunidade - Ocultar componentes usando filtros personalizados. - Filtro personalizado - Ocultar ou mostrar menu flutuante no feed. - Menu flutuante - Feed - Ocultar ou alterar componentes relacionados a tela cheia. - Tela cheia - Geral - Desativar ou ativar o retorno tátil. - Retorno tátil - Substitui a ação de clique dos botões do aplicativo. - Botões hook - Importar ou exportar configurações. - Importar / Exportar configurações - Alterar o estilo do reprodutor minimizado no aplicativo. - Mini reprodutor - Diversos - Ocultar ou mostrar componentes da seção de navegação. - Barra de Navegação - Informações sobre as modificações aplicadas. - Informações do patch - Oculte ou mostre botões em vídeos. - Botões do reprodutor - Ocultar ou alterar o menu flutuante no reprodutor de vídeo. - Menu flutuante - Reprodutor - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Personalize os componentes da barra de progresso. - Barra de progresso - Ocultar elementos no menu de configurações do YouTube. - Menu de configurações - Ocultar ou mostrar componentes no reprodutor de shorts. - Reprodutor de shorts - Shorts - Falsifique os dados de streaming para evitar problemas de reprodução. - Dados de streaming falsos - Controles deslizantes - Ocultar ou alterar componentes localizados na barra de ferramentas, como botões da barra de ferramentas, barra de pesquisa, cabeçalho. - Barra de ferramentas - Ocultar ou mostrar componentes de descrição de vídeo. - Descrição do vídeo - Ocultar vídeos por palavras-chave ou visualizações. - Filtro de vídeo - Vídeo - Altere as configurações relacionadas ao histórico de exibição. - Histórico de exibição - A margem superior das ações rápidas deve ser entre 0-32. Redefinir para os valores padrão. - Configure o espaçamento da barra de progresso para o contêiner de ação rápida, entre 0-32. - Margem superior das ações rápidas - "Rejeita à força a resposta do codec AV1 do software. -Após cerca de 20 segundos de buffer, muda para um codec diferente." - Rejeitar resposta do codec AV1 do software - O processo de fallback causa cerca de 20 segundos de buffer. - Desvio - As alterações de velocidade de reprodução só se aplicam ao vídeo atual. - As alterações de velocidade de reprodução aplicam-se a todos os vídeos. - Lembrar alterações na velocidade de reprodução - Uma notificação flutuante não será exibida quando mudar a velocidade padrão de reprodução. - Uma notificação flutuante será exibida quando mudar a velocidade padrão de reprodução. - Mostrar uma notificação flutuante - Alterando a velocidade padrão para %s. - As alterações de qualidade só se aplicam ao vídeo atual. - As alterações de qualidade aplicam-se a todos os vídeos. - Lembrar alterações na qualidade do vídeo - Uma notificação flutuante não será exibida quando mudar a qualidade padrão de vídeo. - Uma notificação flutuante será exibida quando mudar a qualidade padrão de vídeo. - Mostrar uma notificação flutuante - Alterando a qualidade padrão de dados móveis para %s. - Falha ao definir a qualidade de vídeo. - Alterando a qualidade padrão do Wi-Fi para %s. - "Remover o diálogo discricionário de visualização. -Isso não ignora a restrição de idade, apenas aceita isso automaticamente." - Remover o diálogo discricionário do visualizador - Substitua o codec AV1 do software com o codec VP9. - Substituir codec AV1 do software - O identificador do canal é usado. - O nome do canal é usado. - Substitua o identificador do canal - Toque para mostrar o tempo restante. - Toque para abrir o menu flutuante de velocidade de reprodução ou de qualidade de vídeo. - Substituir ação da marcação de tempo - Substitui o botão criar com o botão de configurações. - Substituir botão criar - "Toque para abrir as Configurações do YouTube. -Toque e segure para abrir as Configurações RVX." - "Toque para abrir as Configurações RVX. -Toque e segure para abrir as Configurações do YouTube." - Tipo de ação a ser atribuída ao botão - As miniaturas da barra de progresso aparecerão em tela cheia. - As miniaturas da barra de progresso aparecerão acima da barra de progresso. - Restaurar as miniaturas antigas da barra de progresso - O menu antigo de qualidade de vídeo não está sendo exibido. - O menu antigo de qualidade de vídeo está sendo exibido. - Restaurar menu antigo de qualidade de vídeo - \@identificador (Nome de usuário) - Formato de exibição - Nome de usuário (@identificador) - Nome de usuário - O identificador é utilizado. - Username é utilizado. - Ativar Return YouTube Username - "A Chave de desenvolvedor da API de dados do YouTube v3 é necessária para substituir o identificador por nome de usuário. - -A cota diária para chaves de API no plano gratuito é de 10.000, e 1 cota é usada para substituir o identificador por nome de usuário para 1 comentário. - -Clique para ver como emitir uma chave de API." - Sobre a chave API de dados do YouTube - A chave de desenvolvedor para usar a API de Dados do YouTube v3. - Chave API dos Dados do YouTube - 1. Vá para <a href=%1$s>Criar um novo projeto</a>.<br>2. Clique no botão <b>CRIAR</b>.<br>3. Vá para <a href=%2$s>API de dados do YouTube v3</a>.<br>4. Clique no botão <b>ATIVAR</b>.<br>5. Clique no botão <b>CRIAR CREDENCIAIS</b>.<br>6. Selecione a opção <b>Dados públicos</b>.<br>7. Clique no botão <b>PRÓXIMO</b>.<br>8. Copie a chave da API.<br><br>※ A chave da API nunca deve ser compartilhada com outras pessoas, portanto, ela não é incluída nas configurações de Importação/Exportação. - Emitir chave de desenvolvedor da API de dados do YouTube v3 - Sobre - Os dados de dislikes são fornecidos pela API do Return YouTube Dislike. Toque aqui para saber mais. - ReturnYouTubeDislike.com - O botão curtir estilizado para melhor aparência. - O botão curtir estilizado para largura mínima. - Botão de curtir compacto - Dislikes exibidos como número. - Dislikes exibidos como porcentagem. - Dislikes como porcentagem - Os dislikes não serão exibidos. - Os dislikes serão exibidos. - Ativar Return YouTube Dislike - As curtidas estimadas estão ocultas. - As curtidas estimadas serão exibidas. - Exibir curtidas estimadas - Dislikes indisponível (limite da API do cliente atingido). - Deslikes indisponível (status %d). - Dislikes temporariamente indisponível (API expirou). - Deslikes indisponível (%s). - Recarregue o vídeo para votar usando o Return YouTube Dislike - Dislikes ocultos no Shorts. - Dislikes exibidos no Shorts. %s - "Exibindo dislikes em Shorts. - -Limitação: Dislikes pode não aparecer no modo incógnito." - Exibir dislikes no Shorts - Uma notificação flutuante não é exibida se o Retorn YouTube Dislike não estiver disponível. - Uma notificação flutuante é exibida se o Retorn YouTube Dislike não estiver disponível. - Exibir uma notificação flutuante se a API não estiver disponível - Oculto - Remove os parâmetros de consulta de rastreamento das URLs ao compartilhar os links. - Limpar links compartilhados - "Frases como '#', 'Arrecadação de fundos', 'Loja' e 'produtos' serão exibidos na legenda do vídeo." - "Frases como '#', 'Arrecadação de fundos', 'Loja' e 'produtos' serão ocultadas da legenda do vídeo." - Limpar legendas de vídeo - Sobre - sponsor.ajay.app - Os dados são fornecidos pela API do SponsorBlock. Toque aqui para aprender mais e ver downloads para outras plataformas. - URL da API alterada. - URL da API é inválida. - URL da API redefinida. - Aparência - Cor alterada. - Cor: - Código de cor inválido. - Redefinir cor. - Criando novos segmentos - Alterar comportamento do segmento - Ocultar automaticamente o botão pular - O botão pular é exibido para todo o segmento. - O botão pular se oculta após alguns segundos. - Usar botão pular compacto - Botão de pular estilizado para melhor aparência. - Botão de pular estilizado para largura mínima. - Exibir botão de criar novo segmento - O botão criar novo segmento não será exibido. - O botão criar novo segmento será exibido. - Ativar SponsorBlock - SponsorBlock é um sistema coletivo para pular partes irritantes de vídeos do YouTube. - Exibir botão votar - O botão de votação do segmento não será exibido. - O botão de votação do segmento será exibido. - Geral - Ajustar nova etapa de segmento - Valor deve ser um número positivo. - Número de milissegundos que os botões de ajuste de tempo se movem ao criar novos segmentos. - Alterar URL da API - O endereço que o SponsorBlock usa para fazer chamadas ao servidor. - Duração mínima do segmento - Duração de tempo inválida. - Segmentos menores que este valor (em segundos) não serão mostrados ou ignorados. - Ativar rastreamento de contagem de pulos - O rastreamento de contagem de pulos não está ativado. - Permite que a classificação do SponsorBlock saiba quanto tempo é economizado. Uma mensagem é enviada para a tabela de classificação sempre que um segmento é ignorado. - Exibir uma notificação flutuante quando pular automaticamente - Uma notificação flutuante não é exibida. Toque aqui para ver um exemplo. - Uma notificação flutuante é exibida quando um segmento é automaticamente pulado. Toque aqui para ver um exemplo. - Exibir duração do vídeo sem segmentos - Exibindo duração total do vídeo. - Duração do vídeo sem os segmentos, exibido entre parênteses ao lado da duração total do vídeo. - Seu id privado de usuário - Id privado do usuário deve ter pelo menos 30 caracteres. - Isso deve ser mantido em segredo. É como se fosse uma senha e não deve ser compartilhado com ninguém. Se alguém tiver isso, poderá se passar por você. - Já lido - Leia as diretrizes do SponsorBlock antes de criar novos segmentos. - Mostre-me - Siga as diretrizes - As diretrizes contêm regras e dicas para a criação de novos segmentos. - Ver diretrizes - Escolha a categoria do segmento - O segmento dura de %1$02d:%2$02d a %3$02d:%4$02d (%5$d minutos %6$02d segundos)\nEle está pronto para enviar? - O segmento é de\n\n%1$s\na\n%2$s\n\n(%3$s)\n\nPronto para enviar? - Os tempos estão corretos? - A categoria está desativada nas configurações. Ative a categoria para enviar. - Você quer editar o tempo para o início ou o fim do segmento? - Tempo inserido inválido. - Editar tempo do segmento manualmente - Definir %s como início ou fim de um novo segmento? - fim - Marque dois locais na barra de tempo primeiro. - início - agora - Pré-visualizar o segmento e garantir que ele pule sem problemas. - O início deve ser antes do fim. - Tempo que o segmento termina - Tempo que o segmento começa - Novo segmento SponsorBlock - Redefinir - Redefinir cor - Enrolação / Piadas - Cenas tangenciais inseridas apenas por enrolação ou humor que não são necessárias para compreender o tópico principal do vídeo. Isto não deve incluir segmentos que fornecem contexto ou detalhes de segundo plano. - Destaque - A parte do vídeo que a maioria das pessoas está procurando. - Lembrete de Interação (Inscrição) - Um breve lembrete para curtir, se inscrever ou segui-los no meio do conteúdo. Se for longo ou sobre algo específico, deve estar sob autopromoção. - Intervalo / Introdução animada - Um intervalo sem conteúdo real. Pode ser uma pausa, um quadro estático ou uma animação repetida. Não inclui transições contendo informações. - Música: Seção sem música - Apenas para uso em vídeos musicais. Deve ser usado exclusivamente para seções de vídeos musicais que já não pertençam à outra categoria. - Finalização / Créditos - Créditos ou quando os cartões finais do YouTube aparecem. Não deve ser usado para conclusões informativas. - Pré-visualização / Recapitulação / Hook - Coleção de clipes que mostram o que está por vir ou o que aconteceu no vídeo ou em outros vídeos de uma série, onde todas as informações são repetidas em outro lugar. - Não pago / Autopromoção - Semelhante ao \'Patrocinador\' exceto pela promoção não paga ou autopromoção. Inclui seções sobre mercadorias, doações ou informações sobre com quem eles colaboraram. - Patrocinador - Promoção paga, referências pagas e anúncios diretos. Não deve ser usado para auto-promoção ou mensagens grátis para causas / criadores / sites / produtos que eles gostam. - Copiar - Falha ao exportar: %s. - Importar / Exportar configurações - Sua configuração JSON do SponsorBlock que pode ser importada / exportada para ReVanced Extended e outras plataformas do SponsorBlock. - Sua configuração SponsorBlock JSON que pode ser importada / exportada para o ReVanced Extended e outras plataformas SponsorBlock. Isso inclui seu ID de usuário privado. Certifique-se de compartilhar isso com sabedoria. - Falha ao importar: %s. - Configurações importadas com sucesso. - Suas configurações contêm um id de usuário SponsorBlock particular.\n\nSeu id de usuário é como uma senha e nunca deve ser compartilhada.\n - Não exibir novamente - Configurações copiadas para a área de transferência. - Pular automaticamente - Pular automaticamente uma vez - Pular - Destaque - Pular enrolação - Pular para destaque - Pular interação - Pular introdução - Pular intervalo - Pular intervalo - Pular sem música - Pular outro - Pular pré-visualização - Pular recapitulação - Pular pré-visualização - Pular promoção - Pular patrocinador - Pular segmento - Desativar - Exibir na barra de progresso - Exibir botão de pular - Enrolação pulada. - Pulado para destaque. - Lembrete irritante pulado. - Introdução pulada. - Intervalo pulado. - Intervalo pulado. - Pulou vários segmentos. - Seção sem música pulada. - Outro pulado. - Pré-visualização pulada. - Recapitulação pulada. - Pré-visualização pulada. - Autopromoção pulada. - Patrocinador pulado. - Segmento não-enviado pulado. - SponsorBlock temporariamente indisponível. - SponsorBlock temporariamente indisponível (status %d). - SponsorBlock temporariamente indisponível (API expirou). - Estatísticas - Estatísticas temporariamente indisponíveis (a API está inativa). - Carregando... - Sua reputação é <b>%.2f</b> - Você salvou pessoas de <b>%s</b> segmentos - %1$s horas %2$s minutos - %1$s minutos %2$s segundos - %s segundos - Isso é <b>%s</b> de suas vidas.<br> Toque aqui para ver a tabela de classificação. - Toque aqui para ver as estatísticas globais e os principais colaboradores. - Tabela de classificação SponsorBlock - O SponsorBlock está desativado. - Você pulou <b>%s</b> segmentos - Redefinir o contador de segmentos pulados? - Isso é <b>%s</b>. - Você criou <b>%s</b> segmentos - Toque aqui para ver seus segmentos. - Seu nome de usuário: <b>%s</b> - Toque aqui para alterar seu nome de usuário - Não foi possível alterar o nome de usuário: Status: %1$d %2$s. - Nome de usuário alterado com sucesso. - Não é possível enviar o segmento.\nJá existe. - Não é possível enviar o segmento: %s. - Não é possível enviar o segmento: %s. - Não foi possível enviar o segmento.\nTaxa limitada (muitos do mesmo usuário ou IP). - SponsorBlock está temporariamente fora do ar. - Não é possível enviar o segmento (estado: %1$d %2$s). - Segmento enviado com sucesso. - Uma notificação flutuante não é exibida se o SponsorBlock não está disponível. - Uma notificação flutuante é exibida se o SponsorBlock não está disponível. - Exibir uma notificação flutuante se a API não estiver disponível - Alterar categoria - Voto negativo - Não foi possível votar no segmento: %s. - Não foi possível votar para o segmento (API expirou). - Não foi possível votar para o segmento (status: %1$d %2$s). - Não há segmentos para votar. - Voto positivo - Configurações copiadas para a área de transferência. - Marcador de Tempo copiado para área de transferência. (%s) - URL copiada para a área de transferência. - URL com marcador de Tempo copiado para área de transferência. - Original - Gostei - Gostei (Cairo) - Coração - Coração (Matiz) - Oculto - Animação de toque duplo - Margem inferior do painel meta deve estar entre 0-64. Voltar aos valores padrão. - Configure o espaçamento da barra de busca para o painel meta, entre 0-64. - Margem inferior do painel meta - A porcentagem de altura deve estar entre 0-100 (%). - Configura a porcentagem de altura do espaço vazio esquerdo quando a barra de navegação está oculta, entre 0 e 100 (%). - Porcentagem de altura do espaço vazio - Pressione e segure a marcação de tempo para alterar o status de repetição do Shorts. - Ação de toque longo na marcação de tempo - "Exibe a seção de título do vídeo em tela cheia. - -Limitação: Título do vídeo desaparece quando clicado." - Exibir seção de título do vídeo - Se a reprodução automática estiver ativada, o próximo vídeo será reproduzido após a contagem regressiva terminar. - Se a reprodução automática estiver ativada, o próximo vídeo será reproduzido sem uma contagem regressiva. - Pular contagem regressiva de reprodução automática - "Ignore o buffer pré-carregado no início do vídeo para ignorar o atraso padrão na aplicação da qualidade do vídeo. - -• Quando o vídeo é iniciado, há um atraso de aproximadamente 0,3 segundos, mas a qualidade de vídeo padrão é aplicada imediatamente. -• Não se aplica a vídeos HDR, vídeos transmitidos ao vivo e vídeos com menos de 15 segundos." - Ignorar buffer pré-carregado - Uma notificação flutuante não será exibida. - Uma notificação flutuante será exibida. - Mostrar uma notificação flutuante quando ignorado - Ativar essa configuração pode causar problemas de reprodução de vídeo. - O buffer pré-carregado é ignorado. - O valor da sobreposição de velocidade deve estar entre 0-8.0. Redefinir aos valores padrão. - Valor da sobreposição de velocidade entre 0-8.0. - Valor da sobreposição de velocidade - "Falsifica a versão cliente para a versão antiga - -• Isto alterará a aparência do aplicativo, mas poderão ocorrer efeitos colaterais desconhecidos. -• Se for desativada posteriormente, a interface antiga poderá permanecer até que os dados do aplicativo sejam apagados." - Versão não falsificada - Versão falsificada - 17.33.42 - Restaurar layout antigo da interface - 17.41.37 - Restaurar o painel de playlist antiga - 18.05.40 - Restaurar caixa de entrada de comentários antiga - 18.17.43 - Restaurar menu suspenso do reprodutor ao antigo estilo - 18.33.40 - Restaurar barra de ação antiga do shorts - 18.38.45 - Restaurar antigo comportamento padrão de qualidade de vídeo - 18.48.39 - Desativa a atualização em tempo real de \'visualizações\' e \'curtidas\' - 19.13.37 - Restaura as animações de números de rolagem do antigo estilo - Versão da falsificação do aplicativo - Digite a versão do app para falsificação - Editar versão de falsificação do app - Falsificar versão do aplicativo - "A versão do aplicativo será falsificada para uma versão mais antiga do YouTube. - -Isso mudará a aparência e os recursos do aplicativo, mas poderão ocorrer efeitos colaterais desconhecidos. - -Se for desativado posteriormente, é recomendável limpar os dados do aplicativo para evitar bugs na interface do usuário." - "Falsifica as dimensões do dispositivo para o valor máximo. -A alta qualidade pode ser desbloqueada em alguns vídeos que exigem dimensões elevadas do dispositivo, mas não em todos os vídeos." - Falsificar dimensões do dispositivo - O codec de vídeo do iOS é AVC (H.264), VP9 ou AV1. - O codec de vídeo do iOS é AVC (H.264). - Forçar iOS AVC (H.264) - "Ativar isto pode melhorar a duração da bateria e corrigir travamentos na reprodução. - -AVC (H. 64) tem uma resolução máxima de 1080p, e a reprodução de vídeo usará mais dados de internet do que VP9 ou AV1." - "• O menu de faixa de áudio está faltando." - "• O menu de faixa de áudio está faltando." - "• Filmes ou vídeos pagos podem não reproduzir." - Efeitos colaterais da falsificação - • O vídeo pode não reproduzir. - O cliente usado para buscar dados de streaming está oculto em Estatísticas para nerds. - O cliente usado para buscar dados de streaming é mostrado em Estatísticas para nerds. - Exibir em Estatísticas para nerds - "Os dados de streaming não são falsificados. A reprodução de vídeo pode não funcionar." - Os dados de streaming são falsificados. - Dados de streaming falsos - Android - Android TV - Android VR - iOS - Cliente padrão - Desativar esta configuração pode causar problemas de reprodução de vídeo. - A sensibilidade de deslizamento de brilho deve estar entre 1-1000 (%). - Configure a sensibilidade do deslize para o brilho entre 1 e 1000 (%). - Sensibilidade de deslizar brilho - Os gestos de deslize estão desativados no modo \'Tela de bloqueio\'. - Os gestos de deslize estão ativados no modo \'Tela de bloqueio\'. - Gestos de deslize no modo \'Tela de bloqueio\' - Automático - A quantidade de limite para que o deslize ocorra. - Limite de magnitude de deslize - A visibilidade do fundo da sobreposição de gestos. - Visibilidade do fundo de gestos - O tamanho da área deslizável não pode ser maior que 50. Redefinir ao valor padrão. - Porcentagem de área deslizável da tela.\n\nNota: isto também altera o tamanho da área da tela para o gesto de toque duplo na tela. - Tamanho da área deslizável da tela de sobreposição - O tamanho do texto para a sobreposição de gestos - Tamanho do texto da sobreposição de gestos - A quantidade de milissegundos em que a sobreposição é visível. - Tempo limite da sobreposição de gestos - A sensibilidade do deslizamento de volume deve estar entre 1-1000 (%). - Configure a sensibilidade do movimento de deslizar o volume entre 1 e 1000 (%).\nA sensibilidade do movimento de deslizar o volume recomendada é de 100% em etapas de 15 volumes e 10% em etapas de 150 volumes. - Sensibilidade de deslizar volume - "Alterne as posições do botão de criação e do botão de notificação falsificando as informações do dispositivo. - -• Mesmo que você altere esta configuração, ela poderá não entrar em vigor até que você reinicie o dispositivo. -• Desativar esta configuração carrega mais anúncios do lado do servidor. -• Você deve desativar esta configuração para tornar os anúncios em vídeo visíveis." - O botão criar não está alternado com o botão Notificações. - "O botão criar é alternado com o botão notificações. - -Nota: Ativar isso também oculta anúncios de vídeo à força." - Alternar criar com notificações - "Desativar isso pode carregar mais anúncios do servidor. - -Além disso, os anúncios não serão mais bloqueados no Shorts. - -Se essa configuração não surtir efeito, tente alternar para o modo anônimo." - Padrão - RVX Music - %s não está instalado. Por favor, instale-o. - Nome do pacote do RVX Music instalado. - Nome do pacote do RVX Music - • O histórico de exibição não funciona. - "• Segue as configurações do histórico de exibição da conta do Google. -• O histórico de exibição pode não funcionar devido ao DNS ou à VPN." - • Segue as configurações do histórico de exibição da conta do Google. - Sobre o histórico de exibição - Clique para abrir o gerenciamento do histórico de exibição do YouTube. - Gerenciar todo o histórico - Original - Substituir domínio - Bloquear histórico de exibição - Tipo de histórico de exibição - Falha ao adicionar o canal \'%1$s\' à lista branca de %2$s. - O canal \'%1$s\' foi adicionado à lista branca de %2$s. - Não há canais na lista branca. - Não adicionado à lista branca. - Falha ao carregar informações do canal. - Adicionado à lista branca. - Velocidade de reprodução - Remover canal \'%1$s\' da lista branca de %2$s? - Falha ao remover o canal \'%1$s\' da lista branca de %2$s. - O canal \'%1$s\' foi removido da lista branca de %2$s. - Verifique ou remova a lista de canais adicionados à lista branca. - Lista branca de canais - SponsorBlock - diff --git a/src/main/resources/youtube/translations/ru-rRU/missing_strings.xml b/src/main/resources/youtube/translations/ru-rRU/missing_strings.xml deleted file mode 100644 index a820effd6..000000000 --- a/src/main/resources/youtube/translations/ru-rRU/missing_strings.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - AI-generated video summary section is shown. - AI-generated video summary section is hidden. - Hide AI-generated video summary section - MMT Orange - MMT Pink - MMT Turquoise - Xisr Yellow - Adjust: Mark Start and End Time for segment - Verify the Segment - Edit the Segment - Forward by Specified Time (Default: 150ms) - Publish Created Segment - Rewind by Specified Time (Default: 150ms) - diff --git a/src/main/resources/youtube/translations/ru-rRU/strings.xml b/src/main/resources/youtube/translations/ru-rRU/strings.xml deleted file mode 100644 index 7d521ac00..000000000 --- a/src/main/resources/youtube/translations/ru-rRU/strings.xml +++ /dev/null @@ -1,1734 +0,0 @@ - - - Включить специальные возможности в плеере? - Служба специальных возможностей включена. Управление изменено. - Продолжить - Больше не показывать - "GmsCore не имеет разрешения на запуск в фоновом режиме. - -Следуйте инструкции \"Don't kill my app!\" для вашего устройства и примените её к вашему GmsCore. - -Это необходимо для работы приложения." - "Во избежание проблем необходимо отключить оптимизацию батареи для GmsCore. - -Нажмите кнопку \"Продолжить\" и отключите оптимизацию батареи." - Открыть сайт - Требуется действие - Включите \"Облачные уведомления\" для получения уведомлений. - Открыть GmsCore - GmsCore не установлен. Установите его. - "DeArrow предоставляет краудсорсинговые миниатюры для видео на YouTube. Они часто более актуальны, чем те, что предоставлены YouTube. - -При активации URL-адрес видео будут отправлен на API-сервер. Если у видео нет миниатюр DeArrow, то будет показан оригинал или захваченный кадр. - -Нажмите здесь, чтобы узнать больше о DeArrow." - О \"DeArrow\" - Неверный URL API для миниатюр DeArrow. - URL кэша миниатюр DeArrow. - DeArrow API - Уведомление при недоступности DeArrow API отключено. - Уведомление при недоступности DeArrow API включено. - Уведомление при недоступности DeArrow API - DeArrow временно недоступен. (код состояния: %s) - DeArrow временно недоступен. - Главная - Вы - Оригинальные миниатюры - DeArrow & Оригинальные миниатюры - DeArrow & Захват кадров - Захват кадров - Плейлисты, рекомендации - Результаты поиска - Захват изображения из видео - Кадры захватываются из начала/середины/конца каждого видео. Эти кадры встроены в YouTube и внешний API не используется. - О захвате кадров из видео - Кадры высокого качества. - Кадры среднего качества. Загружаются быстрее, но в прямых трансляциях, премьерах или старых видео могут быть пустыми. - Выбор качества кадров - Начало видео - Середина видео - Конец видео - Время захвата миниатюры - Подписки - Информация метки времени отключена. - "Информация метки времени включена." - Информация метки времени - Показывать скорость воспроизведения. - Добавить качество видео. - Добавить тип информации - Окружающая подсветка в режиме экономии заряда батареи отключена. - Окружающая подсветка в режиме экономии заряда батареи включена. - Окружающая подсветка в режиме экономии заряда батареи - Домен для получения картинок.\nВажно: Вводите только название домена без префикса \"https\:\/\/\". - Альтернативный домен - Использовать оригинальный хост изображений. -Включение может исправить недостающие изображения, в некоторых регионах. - Использовать для изображений host yt4.ggpht.com. - Обойти ограничения изображений по региону - Оригинал - Телефон - Телефон (Макс. 480 dip) - Планшет - Планшет (Мин. 600 dip) - Изменить макет - Текущий переключатель - Визуальный. - Текущий переключатель - Текстовый. - Изменить тип переключателя - Список приложений общего доступа - встроенный. - Список приложений общего доступа - системный. - Список приложений общего доступа - Автопереход - По умолчанию - Остановить - Повторять - Состояние повторов в Shorts - Обзор каналов - Курсы / Обучение - По умолчанию - Навигатор - Игры - История - Библиотека - Понравившиеся - Трансляции - Фильмы - Музыка - Поиск - Shorts - Спорт - Подписки - В тренде - Смотреть позже - Начальная страница - Начальная страница изменится один раз. - "Начальная страница постоянно изменяется. -Ограничение: Кнопка возврат может не работать." - Тип начальной страницы - Включен обычный логотип. - Включен логотип Premium. - Логотип YouTube - Список компонентов для скрытия\nРазделять новой строкой. - Пользовательский фильтр - Пользовательский фильтр отключен. - Пользовательский фильтр включен. - Пользовательский фильтр - Недопустимый фильтр: %s. - Старый стиль всплывающего меню скорости воспроизведения. - Пользовательское меню скорости воспроизведения. - Тип меню скорости воспроизведения - Значения пользовательской скорости не должны превышать %sx. Сброс к значениям по умолчанию. - Недопустимые значения скорости. Сброс к значениям по умолчанию. - Добавить или изменить скорости воспроизведения. - Пользовательская скорость воспроизведения - Значение должно быть в диапазоне от 0 до 100. Сброс по умолчанию. - Диапазон от 0 (прозрачный) до 100. - Видимость затемнения плеера - Введите HEX код цвета шкалы воспроизведения. - HEX код цвета шкалы воспроизведения - Чтобы открывать ссылки YouTube с помощью RVX, настройте \"Открытие поддерживаемых ссылок\" и включите нужные поддерживаемые веб-адреса. - Использование по умолчанию - Скорость по умолчанию - Качество видео для мобильной сети - Качество видео для Wi-Fi сети - Фоновая подсветка в полном экране отключена. - Окружающая подсветка в полном экране включена. - Окружающая подсветка в полном экране отключена. - Отключить окружающую подсветку в полноэкранном режиме - Фоновая подсветка отключена. - Окружающая подсветка включена. - Окружающая подсветка отключена. - Отключить окружающую подсветку - Принудительные автоматические звуковые дорожки включены. - Принудительные автоматические звуковые дорожки отключены. - Принудительные автоматические звуковые дорожки - Принудительные автоматические субтитры включены. - Принудительные автоматические субтитры отключены. - Принудительные автоматические субтитры - Всплывающие панели плеера включены. - Всплывающие панели плеера отключены. - Всплывающие панели плеера - "Авто-переключение списков \"Джем\", при активном автовоспроизведении, включено. - -Автовоспроизведение настраивается в: -Настройки YouTube → Автовоспроизведение → Автовоспроизведение следующего видео" - Авто-переключатель списков \"Джем\" отключен. - Переключатель списков \"Джем\" - Авто-переключение списков \"Джем\", при активном автовоспроизведении, отключено. - Скорость воспроизведения в трансляциях включена. - Скорость воспроизведения в трансляциях отключена. - Скорость воспроизведения в трансляциях - Скорость воспроизведения по умолчанию для музыки включена. - "Скорость воспроизведения по умолчанию для музыки отключена. - -Ограничение: -Этот параметр не может применяться к видео, которые не содержат баннер 'Слушать в YouTube Music'." - Скорость воспроизведения для музыки - Панель взаимодействия включена. - Панель взаимодействия отключена. - Панель взаимодействия - Виброотклик включен. - Виброотклик отключен. - Виброотклик при смене главы - Виброотклик включен. - Виброотклик отключен. - Виброотклик при перемотке жестом - Виброотклик включен. - Виброотклик отключен. - Виброотклик при перемотке нажатием - Виброотклик включен. - Виброотклик отключен. - Виброотклик при отмотке нажатием - Виброотклик включен. - Виброотклик отключен. - Виброотклик при жесте увеличения экрана - Автояркость HDR включена. - Автояркость HDR отключена. - Автояркость HDR - HDR видео включены. - HDR видео отключены. - HDR видео - Ориентация в полноэкранном режиме по настройкам устройства. - Портретная ориентация в полноэкранном режиме включена. - Альбомный режим - Кнопки \"Лайк\" и \"Дизлайк\" при упоминании подсвечиваются. - Кнопки \"Лайк\" и \"Дизлайк\" при упоминании не подсвечиваются. - Отключить свечение кнопки \"Лайк\" и \"Дизлайк\" - "Отключает протокол, построенный поверх UDP, переходя на TCP." - Отключить протокол QUIC - Возобновление Shorts включено. - Возобновление Shorts отключено. - Возобновление Shorts при запуске приложения - Анимация прокручивания чисел включена. - Анимация прокручивания чисел отключена. - Анимация прокручивания чисел - Главы в прогрессе отображены. - Главы в прогрессе скрыты. - Главы в прогрессе - Анимация кнопки \"Лайк\" включена. - Анимация кнопки \"Лайк\" отключена. - Анимация кнопки \"Лайк\" - "Отключить наложение скорости при нажатии и удержании. - -Примечание: -• Отключение восстанавливает поведение старого интерфейса. -\"Проведите, чтобы перемотать.\"" - Наложение скорости - Анимированная заставка включена. - Анимированная заставка отключена. - Анимированная заставка - "Отключает следующее взаимодействие при расширении описания видео: - -• Нажать - прокрутка. -• Нажать и удерживать - выбор текста." - Взаимодействие с описанием видео - VP9 кодек включен. - "VP9 кодек отключен. - -• Максимальное разрешение 1080p. -• Воспроизведение видео будет использовать больше сетевых данных, чем с VP9. -• HDR воспроизведения нет, HDR видео все еще использует VP9 кодек." - VP9 кодек - Тема Каир прогресса отключена. - "Тема Каир прогресса включена. - -Дополнительно: -Тема также применяется к точкам уведомлений." - Тема Каир прогресса - Наложение элементов управления полноэкранное. - Наложение элементов управления не полноэкранное. - Компактные элементы управления - Пользовательская скорость воспроизведения отключена. - Пользовательская скорость воспроизведения включена. - Пользовательская скорость воспроизведения - Используется оригинальный цвет шкалы воспроизведения. - Используется пользовательский цвет шкалы воспроизведения. - Цвет шкалы воспроизведения - Журнал отладки буфера отключен. - Журнал отладки буфера включен. - Журнал отладки буфера - Журнал отладки отключен. - Журнал отладки включен. - Журнал отладки - Скорость воспроизведения по умолчанию в Shorts отключена. - Скорость воспроизведения по умолчанию в Shorts включена. - Скорость воспроизведения по умолчанию в Shorts - Внешние ссылки открываются в приложении. - Внешние ссылки открываются в браузере. - Внешние ссылки - Переливающийся экран загрузки отключен. - Переливающийся экран загрузки включен. - Переливающийся экран загрузки - Обычное расстояние между кнопками. - Уменьшенное расстояние между кнопками. - Узкие кнопки навигации - Обход youtube.com/redirect при открытии ссылок отключен. - Обход youtube.com/redirect при открытии ссылок включен. - Перенаправление ссылок - Включить кодек OPUS, если содержимое в плеере подходит для кодека. - Включить кодек OPUS - Не сохранять и не восстанавливать яркость при переключениях полного экрана. - Сохранять и восстанавливать яркость при переключениях полного экрана. - Сохранение и восстановление яркости - Перемотка нажатием отключена. - Перемотка нажатием включена. - Перемотка нажатием - "Будут восстановлены эскизы прямых трансляций, у которых отсутствовали миниатюры в прогрессе. - -Поток данных повышается, и миниатюры будут подгружаться с задержкой. - -Рекомендуется интернет соединение с хорошей пропускной способностью." - Миниатюры высокого качества в прогрессе отключены. - Миниатюры высокого качества в прогрессе включены. - Миниатюры высокого качества - Метка времени отключена. - "Метка времени включена. - -Проблема: функция на этапе разработки Google, внешний вид может быть нарушен." - Метка времени - Управление яркостью жестом отключено. - Управление яркостью жестом включено. - Управление яркостью жестом - Виброотклик отключен. - Виброотклик включен. - Виброотклик - При снижении яркости жестом до минимума автояркость не активируется. - При снижении яркости жестом до минимума активируется автояркость. - Управление автояркостью жестом - Прикосновение для активации жеста. - Прикосновение и удержание для активации жеста. - Нажатие для жестов - Жест вверх - следующее видео и жест вниз - предыдущее видео отключены. - Жест вверх - следующее видео и жест вниз - предыдущее видео включены. - Жест для изменения видео в полном экране - Громкость жестом отключено. - Громкость жестом включено. - Управление громкостью жестом - Полупрозрачность отключена. - Полупрозрачность включена. - Полупрозрачная панель навигации - Переход в полноэкранный режим жестом вниз в нижней части плеера отключен. - Переход в полноэкранный режим жестом вниз в нижней части плеера включен. - Жест под плеером - "Эта опция отключит кнопку \"Настройки\" во вкладке \"Вы\". - -Используйте такую последовательность: Вкладка \"Вы\" -> Страница канала -> Меню -> Настройки." - Широкая строка поиска во вкладке \"Вы\" - Широкая строка поиска отключена. - Широкая строка поиска включена. - Широкая строка поиска - Широкая строка поиска замещает логотип YouTube. - Логотип YouTube отображается рядом с широкой строкой поиска. - Широкая строка поиска с логотипом - Описание - "Введите название на панели описания видео. -Символы различаются в зависимости от языка. -«Развернуть описание видео» может не работать, если вы сохраните неверную строку." - Название в панели описания видео - Описание видео разворачивается вручную. - Описание видео автоматически развернуто. - Развернуть описание видео - Подтверждаете действие? - К значениям по умолчанию. - Перезапустить для правильной загрузки интерфейса? - "Существует ошибка на стороне YouTube, которую вызывает анимация прокручивания чисел - лайки, просмотры и даты загрузок будут скрыты для некоторых пользователей. - -Временный обход для этой проблемы - использовать версию приложения до 19.13.37. - -Вы хотите подменить версию приложения перед перезагрузкой?" - Перезапустить для применения? - Не удалось экспортировать настройки. - Настройки успешно экспортированы. - Экспорт настроек в файл. - Экспорт настроек - Импорт - Копировать - Импорт/экспорт настроек в виде текста. - Импорт/экспорт в виде текста - Не удалось импортировать настройки. - Настройки сброшены к значениям по умолчанию. - Настройки успешно импортированы. - Импорт настроек из файла. - Импорт настроек - Сброс - Поиск %s - ReVanced Extended - Внешний загрузчик - Не установлен - "%1$s не установлен. -Пожалуйста, скачайте %2$s с сайта." - Предупреждение - %s не установлен. Установите его. - Например: NewPipe или YTDLnis, или др. (Для плейлиста). - Внешний загрузчик для плейлиста, название пакета - Например: NewPipe или YTDLnis, или др. - Внешний загрузчик для видео, название пакета - "Видео будут переведены в полноэкранный режим при: - -• Нажатии на временную метку в комментариях. -• Запуске видео." - Принудительный полноэкранный режим - Показывает диалоговое окно оптимизации для GMSCore при каждом запуске приложения. - Показать диалог оптимизации для GMSCore - Список элементов меню учетной записи для скрытия.\nРазделять новой строкой. - Фильтр элементов меню аккаунта - "Скрыть элементы меню аккаунта и вкладки \"Вы\". -Некоторые компоненты не могут быть скрыты." - Скрыть меню аккаунта - Карточки альбомов отображены. - Карточки альбомов скрыты. - Карточки альбомов - Секция атрибутов (Особенные места, Игры, Музыка) отображена. - Секция атрибутов (Особенные места, Игры, Музыка) скрыта. - Секция атрибутов - Превью автовоспроизведения отображено. - Превью автовоспроизведения скрыто. - Превью автовоспроизведения - Кнопка \"Магазин\" отображена. - Кнопка \"Магазин\" скрыта. - Кнопка \"Магазин\" - "Скрывает следующие полки: -• Срочные новости -• Продолжить просмотр -• Больше каналов -• Слушать ещё раз -• Покупки -• Смотреть ещё раз" - Скрыть рекомендуемые секции - В ленте отображена. - В ленте скрыта. - Панель категорий в ленте - В похожих видео отображена. - В похожих видео скрыта. - Панель категорий в похожих видео - В результатах поиска отображена. - В результатах поиска скрыта. - Панель категорий в результатах поиска - Правила канала отображены. - Правила канала скрыты. - Правила канала - Полка участников канала отображена. - Полка участников канала скрыта. - Полка участников канала - Ссылки в верхней части профиля канала отображены. - Ссылки в верхней части профиля канала скрыты. - Ссылки в верхней части профиля канала - "Примеры: -Shorts -Плейлисты -Магазин" - Список имён вкладок для скрытия в профиле канала.\nРазделять новой строкой. - Фильтр профиля канала - Фильтр профиля канала отключен. - Фильтр профиля канала включен. - Фильтр профиля канала - Водяной знак канала отображен. - Водяной знак канала скрыт. - Водяной знак канала - Главы отображены. - Главы скрыты. - Главы - Полка эпизодов отображена. - Полка эпизодов скрыта. - Полка эпизодов - Кнопка \"Клип\" отображена. - Кнопка \"Клип\" скрыта. - Кнопка \"Клип\" - Кнопка \"Создать Shorts\" отображена. - Кнопка \"Создать Shorts\" скрыта. - Кнопка Создать \"Shorts\" - Подсвеченные ссылки поиска отображены. - Подсвеченные ссылки поиска скрыты. - Подсвеченные ссылки поиска - Кнопка \"Спасибо\" отображена. - Кнопка \"Спасибо\" скрыта. - Кнопка \"Спасибо\" - Кнопки метки времени и эмодзи отображены. - Кнопки метки времени и эмодзи скрыты. - Метка времени и кнопки эмодзи - Баннер \"Комментарии участников\" отображен. - Баннер \"Комментарии участников\" скрыт. - Баннер \"Комментарии участников\" - Секция комментариев ленте отображена. - Секция комментариев в ленте скрыта. - Секция комментариев в ленте - Секция комментариев отображена. - Секция комментариев скрыта. - Секция комментариев - В канале отображены. - В канале скрыты. - Посты сообщества в канале - В ленте и похожих видео отображены. - В ленте и похожих видео скрыты. - Посты сообщества в ленте и похожих видео - В подписках отображены. - В подписках скрыты. - Посты сообщества в подписках - Секция содержимого отображена. - Секция содержимого скрыта. - Секция содержимого - \"Пожертвования\" отображено. - \"Пожертвования\" скрыто. - \"Пожертвования\" - Фильтр двойного нажатия отображен. - Фильтр двойного нажатия скрыт. - Фильтр двойного нажатия - Кнопка \"Скачать\" отображена. - Кнопка \"Скачать\" скрыта. - Кнопка \"Скачать\" - Заставки следующих видео отображены. - Заставки следующих видео скрыты. - Заставки следующих видео - Расширенные эпизоды отображены. - Расширенные эпизоды скрыты. - Расширенные эпизоды под видео - Расширяемые полки отображены. - Расширяемые полки скрыты. - Расширяемые полки - Кнопка \"Субтитры\" в ленте отображена. - Кнопка \"Субтитры\" в ленте скрыта. - Кнопка \"Субтитры\" в ленте - Список имен всплывающего меню ленты для скрытия.\nРазделять новой строкой. - Фильтр всплывающего меню ленты - Фильтр всплывающего меню ленты отключен. - Фильтр всплывающего меню ленты включен. - Фильтр всплывающего меню ленты - Панель поиска в ленте отображена. - Панель поиска в ленте скрыта. - Панель поиска в ленте - Опросы в ленте отображены. - Опросы в ленте скрыты. - Опросы в ленте - Покадровая лента при перемотке отображена. - Покадровая лента при перемотке скрыта. - Покадровая лента при перемотке - Плавающая кнопка отображена. - Плавающая кнопка скрыта. - Плавающая кнопка - Плавающая кнопка микрофона отображена. - Плавающая кнопка микрофона скрыта. - Плавающая кнопка микрофона - Полка \"Для вас\" отображена. - Полка \"Для вас\" скрыта. - Полка \"Для вас\" - Полноэкранная реклама отображена. - Полноэкранная реклама скрыта. - Полноэкранная реклама - "Реклама в полном экране заблокирована. - -Ограничение: -Посты сообщества, в полном экране, могут быть заблокированы." - Реклама в полном экране закрыта кнопкой. - Способ закрытия рекламы в полном экране - Реклама общего формата отображена. - Реклама общего формата скрыта. - Реклама общего формата - Реклама YouTube Premium отображена. - Реклама YouTube Premium скрыта. - Реклама YouTube Premium - Серые разделители отображены. - Серые разделители скрыты. - Серые разделители - Псевдоним отображен. - Псевдоним скрыт. - Скрыть псевдоним - Кнопка поиска изображений отображена. - Кнопка поиска изображений скрыта. - Кнопка поиска изображений - Секция изображений отображена. - Секция изображений скрыта. - Секция изображений - Секции информационных карт отображены. - Секции информационных карт скрыты. - Секции информационных карт - Информационные карточки отображены. - Информационные карточки скрыты. - Информационные карточки - Информационные панели отображены. - Информационные панели скрыты. - Информационные панели - Кнопка \"Стать спонсором\" отображена. - Кнопка \"Стать спонсором\" скрыта. - Кнопка \"Стать спонсором\" - Секция \"Ключевые понятия\" отображена. - Секция \"Ключевые понятия\" скрыта. - Секция \"Ключевые понятия\" - "Фильтры используются для скрытия контента/комментариев с помощью ключевых фраз. - -Ограничения: -• Некоторые Shorts могут быть не скрыты. -• Некоторые компоненты интерфейса могут быть не скрыты. -• Поиск по ключевому слову может не дать результатов." - О фильтрах - Ключевые слова/фразы с двойными кавычками, принудительно, используют набор символов между кавычками <br><br>, как слово. -Например:<br><b>\"ai\"</b> скроет видео: <b>How does AI work?</b><br>но не будет скрывать: <b>What does fair use mean?</b> - Ключевые слова обрабатываются целиком - Фильтр комментариев отключен. - Фильтр комментариев включен. - Фильтр комментариев - Фильтр видео в \"Главная\" отключен. - Фильтр видео в \"Главная\" включен. - Фильтр видео в \"Главная\" - "Скрывать по ключевым словам и фразам. -Разделять новой строкой. -Для слов с заглавными буквами в середине важен регистр (пример: iPhone, TikTok, LeBlanc)." - Ключевые слова для скрытия - Фильтр результатов поиска отключен. - Фильтр результатов поиска включен. - Фильтр результатов поиска - Фильтр видео в подписках отключен. - Фильтр видео в подписках включен. - Фильтр видео в подписках - Ключевое слово \'%1$s\' не точное и скроет все видео - Недопустимо. \'%s\' не подходит в качестве фильтра. - Добавьте кавычки для использования ключевого слова: %s. - У ключевого слова %s есть конфликт. - Ключевое слово короткое и необходимо взятие в кавычки: %s. - Последние публикации отображены. - Последние публикации скрыты. - Последние публикации - Кнопка \"Последние видео\" отображена. - Кнопка \"Последние видео\" скрыта. - Кнопка \"Последние видео\" - Кнопки \"Лайк\" и \"Дизлайк\" отображены. - Кнопки \"Лайк\" и \"Дизлайк\" скрыты. - Кнопки \"Лайк\" и \"Дизлайк\" - Сообщения онлайн чата отображены.\n\nТакже применимо к трансляциям в Shorts. - Сообщения онлайн чата скрыты.\n\nТакже применимо к трансляциям в Shorts. - Скрыть сообщения онлайн чата - Кнопка \"Повтор\" в онлайн чате отображена.\n\nПоявляется в полноэкранном режиме при закрытии онлайн чата. - Кнопка \"Повтор\" в онлайн чате скрыта.\n\nПоявляется в полноэкранном режиме при закрытии онлайн чата. - Скрыть кнопку \"Повтор\" в онлайн чате - Скрыть видео с менее 1000 просмотров из ленты, из каналов от которых Вы отписались. - Скрыть видео с низкими просмотрами - Медицинские панели отображены. - Медицинские панели скрыты. - Медицинские панели - Полки товаров отображены. - Полки товаров скрыты. - Полки товаров - Плейлист \"Джем\" отображен. - Плейлист \"Джем\" скрыт. - Плейлист \"Джем\" - Полки фильмов отображены. - Полки фильмов скрыты. - Полки фильмов - Панель навигации отображена. - Панель навигации скрыта. - Панель навигации - Кнопка \"Создать\" отображена. - Кнопка \"Создать\" скрыта. - Кнопка \"Создать\" - Кнопка \"Главная\" отображена. - Кнопка \"Главная\" скрыта. - Кнопка \"Главная\" - Подписи кнопок навигации отображены. - Подписи кнопок навигации скрыты. - Подписи кнопок навигации - Кнопка \"Библиотека\" отображена. - Кнопка \"Библиотека\" скрыта. - Кнопка \"Библиотека\" - Кнопка \"Уведомления\" отображена. - Кнопка \"Уведомления\" скрыта. - Кнопка \"Уведомления\" - Кнопка \"Shorts\" отображена. - Кнопка \"Shorts\" скрыта. - Кнопка \"Shorts\" - Кнопка \"Подписки\" отображена. - Кнопка \"Подписки\" скрыта. - Кнопка \"Подписки\" - Кнопка \"Уведомить\" отображена. - Кнопка \"Уведомить\" скрыта. - Кнопка \"Уведомить\" - Метка \"Прямая реклама\" отображена. - Метка \"Прямая реклама\" скрыта. - Метка \"Прямая реклама\" - Встроенные игры отображены. - Встроенные игры скрыты. - Встроенные игры - Кнопка \"Автовоспроизведение\" отображена. - Кнопка \"Автовоспроизведение\" скрыта. - Кнопка \"Автовоспроизведение\" - Кнопка \"Субтитры\" отображена. - Кнопка \"Субтитры\" скрыта. - Кнопка \"Субтитры\" - Кнопка \"Трансляция\" отображена. - Кнопка \"Трансляция\" скрыта. - Кнопка \"Трансляция\" - Кнопка \"Свернуть\" отображена. - Кнопка \"Свернуть\" скрыта. - Кнопка \"Свернуть\" - Меню отображено. - Меню скрыто. - Меню режима окружающей подсветки - Меню \"Звуковая дорожка\" отображено. - Меню \"Звуковая дорожка\" скрыто. - Меню \"Звуковая дорожка\" - Колонтитул меню субтитров отображен. - Колонтитул меню субтитров скрыт. - Колонтитул меню субтитров - Меню \"Субтитры\" отображено. - Меню \"Субтитры\" скрыто. - Меню \"Субтитры\" - Меню 1080p Premium отображено. - Меню 1080p Premium скрыто. - Меню 1080p Premium - Меню \"Справка и Отзывы\" отображено. - Меню \"Справка и Отзывы\" скрыто. - Меню \"Справка и Отзывы\" - Меню \"Открыть в YouTube Music\" отображено. - Меню \"Открыть в YouTube Music\" скрыто. - Меню \"Открыть в YouTube Music\" - Меню \"Блокировка экрана\" отображено. - Меню \"Блокировка экрана\" скрыто. - Меню \"Блокировка экрана\" - Меню \"Повтор\" отображено. - Меню \"Повтор\" скрыто. - Меню \"Повтор\" - Меню \"Дополнительная информация\" отображено. - Меню \"Дополнительная информация\" скрыто. - Меню \"Дополнительная информация\" - Меню \"Картинка в картинке\" отображено. - Меню \"Картинка в картинке\" скрыто. - Меню \"Картинка в картинке\" - Меню \"Скорость воспроизведения\" отображено. - Меню \"Скорость воспроизведения\" скрыто. - Меню \"Скорость воспроизведения\" - Меню \"Настройки Premium\" отображено. - Меню \"Настройки Premium\" скрыто. - Меню \"Настройки Premium\" - Колонтитул меню качества отображен. - Колонтитул меню качества скрыт. - Колонтитул меню качества - Заголовок меню выбора качества отображен. - Заголовок меню выбора качества скрыт. - Заголовок меню выбора качества - Меню \"Пожаловаться\" отображено. - Меню \"Пожаловаться\" скрыто. - Меню \"Пожаловаться\" - Меню таймера сна отображено. - Меню таймера сна скрыто. - Меню таймера сна - Меню \"Постоянный уровень громкости\" отображено. - Меню \"Постоянный уровень громкости\" скрыто. - Меню \"Постоянный уровень громкости\" - Меню \"Статистика для сисадминов\" отображено. - Меню \"Статистика для сисадминов\" скрыто. - Меню \"Статистика для сисадминов\" - Меню \"Смотреть в VR\" отображено. - Меню \"Смотреть в VR\" скрыто. - Меню \"Смотреть в VR\" - Кнопка полноэкранного режима отображена. - Кнопка полноэкранного режима скрыта. - Кнопка полноэкранного режима - Кнопки отображены. - Кнопки скрыты. - Кнопки предыдущего и следующего видео - Полка покупок в плеере отображена. - Полка покупок в плеере скрыта. - Полка покупок в плеере - Кнопка \"YouTube Music\" отображена. - Кнопка \"YouTube Music\" скрыта. - Кнопка YouTube Music - Кнопка \"Сохранить в плейлист\" отображена. - Кнопка \"Сохранить в плейлист\" скрыта. - Кнопка \"Сохранить в плейлист\" - Секции подкастов отображены. - Секции подкастов скрыты. - Секции подкастов - Предпросмотр комментария отображен. - Предпросмотр комментария скрыт. - Предпросмотр комментария - Изменяет размер секции комментариев, повтор онлайн чата будет недоступен. - Не изменяет размер секции комментариев, повтор онлайн чата будет доступен. - Тип скрытия предпросмотра комментария - Баннер отображен. - Баннер скрыт. - Баннер оповещения о промо акциях - Кнопка \"Комментарии\" отображена. - Кнопка \"Комментарии\" скрыта. - Кнопка \"Комментарии\" - Кнопка \"Дизлайк\" отображена. - Кнопка \"Дизлайк\" скрыта. - Кнопка \"Дизлайк\" - Кнопка \"Лайк\" отображена. - Кнопка \"Лайк\" скрыта. - Кнопка \"Лайк\" - Кнопка \"Чат\" отображена. - Кнопка \"Чат\" скрыта. - Кнопка \"Чат\" - Кнопка \"Еще\" отображена. - Кнопка \"Еще\" скрыта. - Кнопка \"Еще\" - Кнопка \"Джем\" отображена. - Кнопка \"Джем\" скрыта. - Кнопка \"Джем\" - Кнопка \"Открыть плейлист\" отображена. - Кнопка \"Открыть плейлист\" скрыта. - Кнопка \"Открыть плейлист\" - Кнопка \"Сохранить в плейлист\" отображена. - Кнопка \"Сохранить в плейлист\" скрыта. - Кнопка \"Сохранить в плейлист\" - Кнопка \"Поделиться\" отображена. - Кнопка \"Поделиться\" скрыта. - Кнопка \"Поделиться\" - Быстрые действия отображены. - Быстрые действия скрыты. - Быстрые действия - "Скрывает следующие рекомендованные видео: - -• С тегом \"Только для участников\". -• С фразами по типу \"Люди также смотрели\" под видео. -• С каналов, на которые вы не подписаны (менее 1000 просмотров)." - Скрыть рекомендованные видео - Предложение о похожих видео отображено. - Предложение о похожих видео скрыто. - Предложение о похожих видео - Похожие видео отображены. - Похожие видео скрыты. - Похожие видео - "Этот параметр ограничивает количество контейнеров по умолчанию, которые влияют на отображение похожих видео. - -Если параметр изменен на стороне сервера, не запрошенные, контейнеры могут быть не доступны." - Кнопка \"Ремикс\" отображена. - Кнопка \"Ремикс\" скрыта. - Кнопка \"Ремикс\" - Кнопка \"Пожаловаться\" отображена. - Кнопка \"Пожаловаться\" скрыта. - Кнопка \"Пожаловаться\" - Кнопка \"Награды\" отображена. - Кнопка \"Награды\" скрыта. - Кнопка \"Награды\" - Миниатюры поискового запроса отображены. - Миниатюры поискового запроса скрыты. - Миниатюра поискового запроса - Сообщение при перемотке отображено. - Сообщение при перемотке скрыто. - Сообщение при перемотке - Сообщение при отмотке назад отображено. - Сообщение при отмотке назад скрыто. - Сообщение при отмотке назад - Метки глав в прогрессе отображены. - Метки глав в прогрессе скрыты. - Метки глав в прогрессе - Шкала воспроизведения отображена. - Шкала воспроизведения скрыта. - Миниатюры шкалы воспроизведения отображены. - Миниатюры шкалы воспроизведения скрыты. - Миниатюры шкалы воспроизведения - Скрыть шкалу воспроизведения - Карточки саморекламы отображены. - Карточки саморекламы скрыты. - Карточки саморекламы - О приложении отображено. - О приложении скрыто. - О приложении - Специальные возможности отображены. - Специальные возможности скрыты. - Специальные возможности - Аккаунт отображен. - Аккаунт скрыт. - Аккаунт - Автовоспроизведение отображено. - Автовоспроизведение скрыто. - Автовоспроизведение - Счета и платежи отображены. - Счета и платежи скрыты. - Счета и платежи - Субтитры отображены. - Субтитры скрыты. - Субтитры - Связанные приложения отображены. - Связанные приложения скрыты. - Связанные приложения - Экономия трафика отображена. - Экономия трафика скрыта. - Экономия трафика - Общее отображено. - Общее скрыто. - Общее - Управлять историей просмотра отображено. - Управлять историей просмотра скрыто. - Управлять историей просмотра - Чат отображен. - Чат скрыт. - Чат - Уведомления отображены. - Уведомления скрыты. - Уведомления - Фоновый и офлайн режим отображен. - Фоновый и офлайн режим скрыт. - Фоновый и офлайн режим - Просмотр на телевизоре отображен. - Просмотр на телевизоре скрыт. - Просмотр на телевизоре - Семейный центр отображен. - Семейный центр скрыт. - Семейный центр - Экспериментальные функции отображены. - Экспериментальные функции скрыты. - Экспериментальные функции - Конфиденциальность отображена. - Конфиденциальность скрыта. - Конфиденциальность - Покупки и платные подписки отображены. - Покупки и платные подписки скрыты. - Покупки и платные подписки - Скрытие элементов в меню настроек YouTube. - Меню настроек YouTube - Качество видео отображено. - Качество видео скрыто. - Качество видео - Ваши данные на YouTube отображено. - Ваши данные на YouTube скрыто. - Ваши данные на YouTube - Кнопка \"Поделиться\" отображена. - Кнопка \"Поделиться\" скрыта. - Кнопка \"Поделиться\" - Кнопка \"Магазин\" отображена. - Кнопка \"Магазин\" скрыта. - Кнопка \"Магазин\" - Ссылки на покупки отображены. - Ссылки на покупки скрыты. - Ссылки покупок - Панель канала отображена. - Панель канала скрыта. - Панель канала - Кнопка \"Комментарии\" отображена. - Кнопка \"Комментарии\" скрыта. - Кнопка \"Комментарии\" - Кнопка отключенных комментариев отображена. - Кнопка отключенных комментариев скрыта. - Скрыть кнопку с отключенными комментариями - Кнопка \"Дизлайк\" отображена. - Кнопка \"Дизлайк\" скрыта. - Кнопка \"Дизлайк\" - "Всплывающие кнопки, такие как «Использовать этот звук», во вкладке Shorts отображены." - "Всплывающие кнопки, такие как «Использовать этот звук», во вкладке Shorts скрыты." - Всплывающая кнопка - Метка видео ссылки отображена. - Метка видео ссылки скрыта. - Метка ссылки на полное видео - Кнопка зеленого экрана отображена. - Кнопка зеленого экрана скрыта. - Кнопка зеленого экрана - Информационные панели отображены. - Информационные панели скрыты. - Информационные панели - Кнопка \"Спонсор\" отображена. - Кнопка \"Спонсор\" скрыта. - Кнопка \"Спонсор\" - Кнопка \"Лайк\" отображена. - Кнопка \"Лайк\" скрыта. - Кнопка \"Лайк\" - Заголовок онлайн чата отображен. - -Кнопка возврата в заголовке не будет скрыта. - Заголовок онлайн чата скрыт. - -Кнопка возврата в заголовке не будет скрыта. - Заголовок онлайн чата - Кнопка \"Местоположение\" отображена. - Кнопка \"Местоположение\" скрыта. - Кнопка \"Местоположение\" - Панель навигации отображена. - Панель навигации скрыта. - Панель навигации - Метка \"Содержит прямую рекламу\" отображена. - Метка \"Содержит прямую рекламу\" скрыта. - Метка \"Содержит прямую рекламу\" - Надпись паузы отображена. - Надпись паузы скрыта. - Надпись паузы - Фоновые кнопки во время паузы отображены. - Фоновые кнопки во время паузы скрыты. - Фоновые кнопки во время паузы - Фон отображен. - Фон скрыт. - Фон кнопки \"Воспроизведение\" - \"Пауза\" - Кнопка \"Ремикс\" отображена. - Кнопка \"Ремикс\" скрыта. - Кнопка \"Ремикс\" - Кнопка \"Сохранить музыку\" отображена. - Кнопка \"Сохранить музыку\" скрыта. - Кнопка \"Сохранить музыку\" - Кнопка \"Подсказки поиска\" отображена. - Кнопка \"Подсказки поиска\" скрыта. - Кнопка \"Подсказки поиска\" - Кнопка \"Поделиться\" отображена. - Кнопка \"Поделиться\" скрыта. - Кнопка \"Поделиться\" - Показано в канале. - "Скрыто в канале. - -Информация: -• Скрыты только полки с заголовком Shorts на домашней вкладке." - Скрывать в канале - В истории просмотра отображены. - В истории просмотра скрыты. - Shorts в истории просмотра - В ленте и похожих видео отображены. - В ленте и похожих видео скрыты. - Shorts в ленте и похожих видео - В результатах поиска отображены. - В результатах поиска скрыты. - Shorts в результатах поиска - В ленте подписок отображены. - В ленте подписок скрыты. - Shorts в ленте подписок - "Скрывает полки Shorts. - -Ограничения: -Официальные заголовки в результатах поиска будут скрыты." - Полки Shorts - Кнопка \"Магазин\" отображена. - Кнопка \"Магазин\" скрыта. - Кнопка \"Магазин\" - Кнопка \"Покупки\" отображена. - Кнопка \"Покупки\" скрыта. - Кнопка \"Покупки\" - Кнопка \"Трек\" отображена. - Кнопка \"Трек\" скрыта. - Кнопка \"Трек\" - Метка метаданных отображена. - Метка метаданных скрыта. - Метка звуковых метаданных - Стикеры отображены. - Стикеры скрыты. - Стикеры - Кнопка \"Подписаться\" отображена. - Кнопка \"Подписаться\" скрыта. - Кнопка \"Подписаться\" - Кнопка \"Особая благодарность\" отображена. - Кнопка \"Особая благодарность\" скрыта. - Кнопка \"Особая благодарность\" - Товары с тегом отображены. - Товары с тегом скрыты. - Товары с тегом - Панель инструментов отображена. - Панель инструментов скрыта. - Панель инструментов - Кнопка \"В тренде\" отображена. - Кнопка \"В тренде\" скрыта. - Кнопка \"В тренде\" - Кнопка \"Использовать шаблон\" отображена. - Кнопка \"Использовать шаблон\" скрыта. - Кнопка \"Использовать шаблон\" - Кнопка \"Использовать этот звук\" отображена. - Кнопка \"Использовать этот звук\" скрыта. - Кнопка \"Использовать этот звук\" - Заголовок отображен. - Заголовок скрыт. - Заголовок видео - Кнопка \"Показать еще\" отображена. - Кнопка \"Показать еще\" скрыта. - Кнопка \"Показать еще\" - Виджет коротких уведомлений отображен. - Виджет коротких уведомлений скрыт. - Виджет коротких уведомлений - Кнопка \"Попробовать\" отображена. - Кнопка \"Попробовать\" скрыта. - Кнопка \"Попробовать\" - Строка иконок с подписками отображена. - Строка иконок с подписками скрыта. - Строка иконок с подписками - Предлагаемые действия отображены. - Предлагаемые действия скрыты. - Предлагаемые действия - "Этот параметр устарел. - -Вместо этого используйте настройку «Настройки → Автозапуск → Автовоспроизведение следующего видео»." - Рекомендуемое видео в конце воспроизведения отображено. - "Рекомендуемое видео в конце воспроизведения скрыто при выключенном автовоспроизведении. - -Автовоспроизведение можно изменить в настройках YouTube: -Настройки -> Автовоспроизведение -> Автовоспроизведение следующего видео" - Рекомендуемое видео в конце воспроизведения - Кнопка \"Спасибо\" отображена. - Кнопка \"Спасибо\" скрыта. - Кнопка \"Спасибо\" - Полки билетов отображены. - Полки билетов скрыты. - Полки билетов - Метка времени отображена. - Метка времени скрыта. - Метка времени - Реакции по времени отображены. - Реакции по времени скрыты. - Реакции по времени - Кнопка \"Трансляция\" отображена. - Кнопка \"Трансляция\" скрыта. - Кнопка \"Трансляция\" - Кнопка \"Создать\" отображена. - Кнопка \"Создать\" скрыта. - Кнопка \"Создать\" - Кнопка \"Уведомления\" отображена. - Кнопка \"Уведомления\" скрыта. - Кнопка \"Уведомления\" - Секция \"Расшифровка видео\" отображена. - Секция \"Расшифровка видео\" скрыта. - Секция \"Расшифровка видео\" - Реклама в видео отображена. - Реклама в видео скрыта. - Реклама в видео - "Главная / Подписки / Результаты поиска, фильтруются, чтобы скрыть видео с просмотрами отличающиеся от введенного числа. - -Ограничения: -• Shorts нельзя скрыть. -• Видео без просмотров не обрабатываются." - О фильтре подсчета просмотров - Фильтр видео в \"Главная\", по просмотрам, отключен. - Фильтр видео в \"Главная\", по просмотрам, включен. - Фильтр видео в \"Главная\" по просмотрам - Фильтр результатов поиска, по просмотрам, отключен. - Фильтр результатов поиска, по просмотрам, включен. - Фильтр результатов поиска по просмотрам - Фильтр видео в подписках, по просмотрам, отключен. - Фильтр видео в подписках, по просмотрам, включен. - Фильтр видео в подписках по просмотрам - Скрывает рекомендованные видео с заданным количеством просмотров. - Скрыть рекомендованные видео по количеству просмотров - Видео с большим количеством просмотров будут скрыты. - Больше по просмотрам - Видео с меньшим количеством просмотров будут скрыты. - Меньше по просмотрам - Тыс. -> 1 000 -Млн -> 1 000 000 -Млрд -> 1 000 000 000 -просмотров -> views - Языковой шаблон для количества просмотров. -Каждый ключ (буквы/слово на вашем языке) -> значение (значение ключа) должно быть на новой строке. -Ключ идет перед знаком \"->\". Если вы переключите язык приложения или системы, вам необходимо сбросить этот параметр. - -Примеры: -Английский: 10K views = K -> 1000, views -> views -Русский: 10 тыс. просмотров = Тыс. -> 1000, просмотров -> views - Ключи просмотров - Баннер просмотра товаров отображен. - Баннер просмотра товаров скрыт. - Баннер просмотра товаров - Кнопка \"Голосовой поиск\" отображена. - Кнопка \"Голосовой поиск\" скрыта. - Кнопка \"Голосовой поиск\" - Результаты веб поиска отображены. - Результаты веб поиска скрыты. - Результаты веб поиска - YouTube Doodles отображены. - YouTube Doodles скрыты. - YouTube Doodles - "YouTube Doodles появляются несколько дней в году. - -Если YouTube Doodles отображаются в вашем регионе и они скрыты, то панель фильтров под строкой поиска будет также скрыта." - Увеличение наложения включено. - Увеличение наложения отключено. - Увеличение наложения - Иконка Afn синяя - Иконка Afn красная - Пользовательский - По умолчанию - MMT - MMT Blue - MMT Green - MMT Yellow - Revancify синяя - Revancify красная - Revancify Yellow - Vanced Black - Vanced Light - Иконка YouTube - Удерживает альбомный режим в полноэкранном режиме. - Количество миллисекунд, в течение которых принудительно работает альбомный режим. - Сохранять таймаут альбомного режима - Удерживать альбомный режим - По умолчанию - Действие двойного нажатия отключено. - "Действие двойного нажатия включено. - -Плеер \"Современный 1\" - изменяет размер уменьшенного видео. -Плеер \"Современный 2, 3\" - закрывает уменьшенное видео." - Действие двойного нажатия - Перетаскивание мини-плеера отключено. - Перетаскивание мини-плеера включено. - Перетаскивание мини-плеера - Кнопки \"Развернуть\" и \"Закрыть\" отображены. - Кнопки скрыты. -(развернуть или закрыть мини-плеер жестом) - Кнопки \"Развернуть\" и \"Закрыть\" - Кнопки перемотки отображены. - Кнопки перемотки скрыты. - Кнопки перемотки - Подстрочный текст отображен. - Подстрочный текст скрыт. - Подстрочный текст - Непрозрачность должна быть в диапазоне 0-100. Сброс по умолчанию. - Значение непрозрачности в промежутке 0-100, где 0 это прозрачный. - Непрозрачность мини-плеера - Оригинал - Телефон - Планшет - Современный 1 - Современный 2 - Современный 3 - Тип мини-плеера - Компоненты в плеере - "Нажать - все время повтор. -Нажать и удерживать - пауза после повтора." - Кнопка \"Постоянный повтор\" - "Нажать - скопировать URL видео. -Нажать и удерживать - скопировать URL с меткой времени." - "Нажать - скопировать URL видео с меткой времени. -Нажать и удерживать - скопировать метку времени." - Кнопка копирования URL с меткой времени - Кнопка копировать URL видео - Запустить внешний загрузчик. - Кнопка внешний загрузчик - Нажатия кнопки, отключает/включает звук текущего видео. - Кнопка отключения звука - Нажать и удерживать - изменить состояние кнопки. - Скорость сброшена на: %sx. - "Нажать - открытие окна скорости. -Нажать и удерживать - скорость воспроизведения в 1.0x." - Кнопка скорости воспроизведения - "Нажмите, чтобы создать плейлист всех видео канала. -Нажмите и удерживайте, чтобы отменить действие." - Кнопка создания плейлиста канала - Нажать - Открыть \"Белый список\". -Нажать и удерживать - Открыть настройки \"Белый список\". - Кнопка \"Белый список\" - Кнопка \"Скачать\" для плейлиста, использует внутренний загрузчик. - Кнопка \"Скачать\" для плейлиста, использует внешний загрузчик. - Действие кнопки \"Скачать\" для плейлиста - Кнопка \"Скачать\" использует внутренний загрузчик. - Кнопка \"Скачать\" использует внешний загрузчик. - Действие кнопки \"Скачать\" для видео - Для переопределения кнопки требуется YouTube Music. -Нажмите здесь, чтобы загрузить YouTube Music. - Обязательное условие - Кнопка YouTube Music открывает встроенное приложение. - Кнопка YouTube Music открывает RVX Music. - Переопределить кнопку YouTube Music - Не применён - Применён - Обычная - Кнопки действий - Дополнительные настройки - Анимация / Обратная связь - Кнопка \"Скачать\" - Экспериментальные опции - Ограничения изображений по региону - Импорт/экспорт в виде файла - Импорт/экспорт настроек в виде текста - Фильтр ключевых слов - Другие - Наложение кнопок - Информация о патчах - Быстрые действия - Рекомендованное видео - Настройки скрытия Shorts - Предложенные действия - Используемые инструменты - Фильтр по количеству просмотров - Настройка меню аккаунта и вкладки Вы. - Меню аккаунта - Настройка кнопок действий под видео. - Кнопки действий под видео - Реклама - Альтернативные миниатюры - Отключить окружающую подсветку или обойти ограничение в режиме экономии заряда батареи. - Окружающая подсветка - Скрытие или отображение панели категорий в ленте, поиске и похожих видео. - Панель категорий - Настройка компонентов в панели канала под видео. - Панель канала - Скрытие или отображение компонентов в профиле канала. - Профиль канала - Настройка компонентов секции комментариев. - Комментарии - Скрытие или отображение постов сообщества в ленте, подписках и канале. - Посты сообщества - Скрыть компоненты используя пользовательский фильтр. - Пользовательский фильтр - Скрытие или отображение компонентов всплывающего меню в ленте с помощью фильтра. - Всплывающее меню - Лента - Скрыть или изменить компоненты полноэкранного режима. - Полноэкранный режим - Основные настройки - Отключить или включить виброотклик по событиям. - Виброотклик - Переопределяет действие нажатия кнопок в приложении. - Настройки действий кнопок - Настройки импорта/экспорта. - Импорт/экспорт настроек - Стиль мини-плеера. - Мини-плеер - Разное - Настроить компоненты панели навигации. - Панель навигации - Информация о примененных патчах. - Информация о патчах - Скрыть или показать кнопки в видео. - Кнопки плеера - Скрыть или изменить \"Выдвижное меню\" в плеере. - Выдвижное меню плеера - Плеер - Имя пользователя YouTube - \"RYU\" - Вернуть YouTube Dislike - SponsorBlock - Настроить компоненты шкалы воспроизведения. - Шкала воспроизведения - Настройка меню YouTube. - Меню настроек - Компоненты в плеере Shorts. - Плеер Shorts - Shorts - Подменяет потоковые данные при проблемах с воспроизведением видео. - Подмена потоковых данных - Управление жестами - Скрыть или изменить компоненты на панели инструментов - строка поиска, кнопки и заголовок. - Панель инструментов - Скрыть или отобразить компоненты описания видео. - Описание видео - Скрытие видео по ключевым словам, просмотрам или продолжительности. - Фильтрация видео - Видео - Изменить настройки истории просмотра. - История просмотра - Значение должно быть в диапазоне от 0 до 32. Сброс по умолчанию. - Значение отступа поля быстрых действий от шкалы воспроизведения\nДиапазон от 0 до 32. - Отступ над быстрыми действиями - "Принудительный отказ от программного кодека AV1. -После примерно 20 секунд буферизации будет применён другой кодек." - Отказ от программного кодека AV1 - Буферизация из-за программного кодека AV1 (примерно 20 сек.) - Оффсет - Алгоритм выдачи похожих видео - Изменяется скорость воспроизведения у текущего видео. - Изменяется скорость воспроизведения у всех видео. - Запомнить скорость воспроизведения - Всплывающее уведомление скрыто. - Всплывающее уведомление отображено. - Уведомление о выбранной скорости - Скорость воспроизведения по умолчанию изменена на %s. - Изменяется качество у текущего видео. - Изменяется качество у всех видео. - Запомнить изменения качества видео - Всплывающее уведомление скрыто. - Всплывающее уведомление отображено. - Уведомление о выбранном качестве - Качество видео в моб. сети изменено на %s. - Ошибка установки качества видео. - Качество видео в Wi-Fi сети изменено на %s. - "Возрастные ограничения будут приниматься автоматически." - Скрыть диалог возрастного ограничения - Замена программного кодека AV1 на VP9. - Заменить программный кодек AV1 - Используется псевдоним канала. - Используется имя канала. - Заменить псевдоним канала - Нажмите, чтобы показать оставшееся время. - Нажмите, чтобы открыть меню скорости воспроизведения или качества видео. - Заменить действие метки времени - Заменить \"Создать\" кнопкой настройки. - Заменить кнопку \"Создать\" - "Нажать - Настройки YouTube. -Нажать и удерживать - Настройки RVX." - "Нажать - Настройки RVX. -Нажать и удерживать - Настройки YouTube." - Тип действия для назначения кнопке - Миниатюры отображаются в полноэкранном режиме. - Миниатюры отображаются над шкалой воспроизведения. - Старые миниатюры шкалы воспроизведения - Старое меню качества скрыто. - Старое меню качества отображено. - Старое меню качества - \@псевдоним (Имя пользователя) - Вид отображения - Имя пользователя (@псевдоним) - Имя пользователя - Имя пользователя YouTube отключено. - Имя пользователя YouTube включено. - Включить Имя пользователя YouTube - \"RYU\" - "Ключ разработчика \"Return YouTube Username\" (RYU) API Data v3 необходим для замены псевдонима на имя пользователя. - -Ежедневная, бесплатная, квота ключей API составляет 10 000, и 1 квота используется для замены псевдонима на имя пользователя для комментария. - -Нажмите, чтобы увидеть, как выпустить API ключ." - О ключе \"RYU\" Data API - Ключ разработчика для \"RYU\" Data API v3. - Ключ \"RYU\" Data API - 1. Перейдите <a href=\"%1$s\">Новый проект</a>.<br>2. Нажмите <b>Создать</b>.<br>3. Перейдите <a href=\"%2$s\">YouTube Data API v3</a>.<br>4. Нажмите <b>Включить</b>.<br>5. Нажмите <b>Создать учетные данные</b>.<br>6. Выберите <b>Публичные данные</b>.<br>7. Нажмите <b>Далее</b>.<br>8. Скопируйте ключ API.<br><br>※ Ключом API нельзя поделиться, поэтому он не доступен в настройках Импорт/Экспорт. - Проблема с ключом разработчика \"RYU\" Data API v3 - О \"Вернуть YouTube Dislike\" - Данные об отметках \"Не нравится\" предоставлены Return YouTube Dislike API.\nНажмите здесь, чтобы узнать больше. - ReturnYouTubeDislike.com - Компактная кнопка \"Лайк\" отключена. - Компактная кнопка \"Лайк\" включена. - Компактная кнопка \"Лайк\" - \"Дизлайк\" отображается как число. - \"Дизлайк\" отображается в процентах. - Отображение кнопки \"Дизлайк\" - Кнопка \"Дизлайк\" скрыта. - Кнопка \"Дизлайк\" отображена. - Включить Return YouTube Dislike - Соотношение лайков скрыто. - Соотношение лайков отображено. - Соотношение лайков - Return YouTube Dislike недоступен (достигнут лимит клиента API). - Return YouTube Dislike недоступен (статус %d). - Return YouTube Dislike недоступен (время ожидания API истекло). - Return YouTube Dislike недоступен (%s). - Обновите видео для использования Return YouTube Dislike - Кнопка \"Дизлайк\" в Shorts скрыта. - Кнопка \"Дизлайк\" в Shorts отображена. - "Кнопка \"Дизлайк\" в Shorts отображена. - -Ограничение: -Кнопка \"Дизлайк\" может отсутствовать в режиме инкогнито." - Кнопка \"Дизлайк\" в Shorts - Уведомление при недоступности Return YouTube Dislike API отключено. - Уведомление при недоступности Return YouTube Dislike API включено. - Уведомление при недоступности Return YouTube Dislike API - Скрыто владельцем - Убирает параметры отслеживания запросов из URL при отправке ссылки. - Очистить ссылки при отправке - "Фильтр фраз типа \"#\", \"Магазин\" и \"N продуктов\" в субтитрах включен." - "Фильтр фраз типа \"#\", \"Магазин\" и \"N продуктов\" в субтитрах отключен." - Фильтр фраз в субтитрах - О \"SponsorBlock\" - Об API (sponsor.ajay.app) - Данные предоставлены SponsorBlock API. Нажмите здесь, чтобы узнать больше и скачать версии для других платформ. - API адрес сервера изменен. - API адрес сервера недействителен. - API адрес сервера сброшен. - Внешний вид - Цвет изменен. - Цвет: - Неверный код цвета. - Цвет сброшен. - Создание новых сегментов - Изменить поведение сегмента - Скрытие кнопки \"Пропуск\" - Кнопка \"Пропуск\" отображается до конца сегмента. - Кнопка \"Пропуск\" скрывается через несколько секунд. - Компактная кнопка \"Пропуск\" - Компактная кнопка \"Пропуск\" отключена. - Компактная кнопка \"Пропуск\" включена. - Кнопка \"Новый сегмент\" - Кнопка \"Новый сегмент\" скрыта. - Кнопка \"Новый сегмент\" отображена. - Включить SponsorBlock - SponsorBlock - коллективная система для пропуска раздражающих частей в видео. - Кнопка \"Голосовать\" - Кнопка \"Голосовать\" за сегмент скрыта. - Кнопка \"Голосовать\" за сегмент отображена. - Общее - Настройки шага нового сегмента - Значение должно быть положительным числом. - Количество миллисекунд, на которое можно смещаться, используя кнопки перемотки при добавлении нового сегмента. - Изменить API адреса сервера - Адрес, используемый SponsorBlock для связи с сервером. - Минимальная длительность сегмента - Недопустимая, минимальная, длительность времени. - Сегменты, продолжительность которых короче, чем установленное значение (в секундах), не будут пропущены или показаны в плеере. - Подсчет количества пропусков - Отслеживание количества пропусков не включено. - Позволяет системе доски лидеров SponsorBlock знать, сколько времени было сэкономлено. Отправляет сообщение на сервер каждый раз при пропуске сегмента. - Уведомление при автоматическом пропуске сегмента - Уведомление при автоматическом пропуске сегмента отключено.\nНажмите для примера. - Уведомление при автоматическом пропуске сегмента включено.\nНажмите для примера. - Продолжительность без сегментов - Отображается общая продолжительность видео. - Продолжительность видео без всех сегментов отображена в скобках рядом с общей продолжительностью видео. - Ваш приватный ID - Приватный ID должен иметь длину не менее 30 символов. - Его нужно держать в секрете. Это как пароль, не стоит им ни с кем делиться. Если он у кого-то есть, он сможет выдать себя за вас. - Уже прочитано - Перед отправкой любого сегмента рекомендуется прочитать рекомендации от SponsorBlock. - Показать - Следуйте инструкциям - Руководство содержит правила и советы по созданию новых сегментов. - Посмотреть руководство - Выберите категорию сегмента - Сегмент с %1$02d:%2$02d до %3$02d:%4$02d (%5$d минут %6$02d секунд)\nГотов ли к одобрению? - Сегмент длится с\n\n%1$s\nдо\n%2$s\n\n(%3$s)\n\nОн готов для отправки? - Время начала и конца сегмента верно? - Эта категория отключена в настройках. Включите ее для отправки сегмента. - Вы хотите изменить время начала или окончания сегмента? - Указано неверное время. - Изменить время сегмента вручную - Установить %s как начало или конец нового сегмента? - конец - Сначала отметьте два места на шкале воспроизведения. - начало - сейчас - Предварительный просмотр сегмента и обеспечение его плавного пропуска. - Начало сегмента должно быть перед его окончанием. - Время окончания сегмента: - Время начала сегмента: - Новый сегмент SponsorBlock - Сброс - Сбросить цвет - Отвлеченные темы/шутки - Сегменты, которые увеличивают длительность видео за счет отвлеченных тем или шуток, но не требуются для понимания основного содержания. Не должно иметь сегментов, объясняющие контекст или предысторию. - Часто просматриваемое - Часть видео, которую ищут большинство людей. - Напоминание о взаимодействии - Короткое напоминание поставить лайк, подписаться или подписаться посреди контента. Если оно длинное или о чем-то конкретном, оно должно быть саморекламой. - Анимация концовки/вступления - Интервал без фактического содержания. Это может быть пауза, статический кадр или повторяющаяся анимация. Не включает переходы, содержащие информацию. - Музыка: сегмент без музыки - Только для использования в музыкальных видеороликах. Разделы музыкальных видео без музыки, которые еще не охвачены другой категорией. - Конечные заставки/титры - Титры или появление конечных заставок YouTube. Не для подведения итогов сказанного в видео. - Предпросмотр/краткое содержание/отсылка - Краткое содержание предыдущих эпизодов или препросмотр того, что будет в данном видео. Предназначено для сегментов, смонтированных из кусков видео, а не для дополнительной информации. - Самореклама/рекомендация - Похоже на спонсорскую рекламу, но для бесплатной рекламы и саморекламы. Включает в себя вставки про мерчендайз, пожертвования или информацию о тех, вместе с кем было сделано видео. - Спонсорская реклама - Рекламные интеграции, реферальные ссылки и прямая реклама. - Копировать - Не удалось экспортировать: %s. - Импорт/экспорт настроек - Ваша JSON конфигурация SponsorBlock может быть импортирована/экспортирована в ReVanced Extended и другие платформы SponsorBlock. - Ваша JSON конфигурация SponsorBlock может быть импортирована/экспортирована в ReVanced Extended и другие платформы SponsorBlock. Она содержит ваш приватный ID. Будьте осторожны при его передаче другим лицам. - Не удалось импортировать: %s. - Настройки успешно импортированы. - Ваши настройки SponsorBlock содержат приватный ID.\n\nВаш ID как пароль, им не стоит ни с кем делиться. - Не показывать снова - Настройки скопированы в буфер обмена. - Автоматически пропускать - Автоматически пропустить единожды - Пропустить - Основные моменты - Пропустить наполнитель контента - Ключевой момент - Пропустить напоминание о взаимодействии - Пропустить вступление - Пропустить перерыв - Пропустить перерыв - Пропустить не музыкальный сегмент - Пропустить концовку - Пропустить предпросмотр - Пропустить краткое повторение - Пропустить предпросмотр - Пропустить саморекламу - Пропустить спонсорскую рекламу - Пропустить неотправленный сегмент - Ничего не делать - Показывать в шкале воспроизведения - Показывать кнопку \"Пропуск\" - Пропущен наполнитель контента. - Пропущены основные моменты. - Пропущено напоминание о взаимодействии. - Пропущено вступление. - Пропущен перерыв. - Пропущен перерыв. - Пропущено несколько сегментов. - Пропущен не музыкальный сегмент. - Пропущена концовка. - Пропущено превью. - Пропущено краткое повторение. - Пропущено превью. - Пропущена самореклама. - Пропущена спонсорская реклама. - Пропущен неотправленный сегмент. - SponsorBlock временно недоступен. - SponsorBlock временно недоступен (статус %d). - SponsorBlock временно недоступен (время ожидания API истекло). - Статистика - Статистика временно недоступна (API не работает). - Загрузка... - Ваша репутация <b>%.2f</b> - Вы избавили людей от <b>%s</b> сегментов - %1$s часов %2$s минут - %1$s минут %2$s секунд - %s секунд - Это <b>%s</b> их жизни.<br> Нажмите здесь, чтобы увидеть таблицу лидеров. - Нажмите здесь, чтобы увидеть глобальную статистику и ведущих участников. - Список лидеров SponsorBlock - SponsorBlock отключен. - Вы пропустили <b>%s</b> сегментов - Сбросить счетчик пропущенных сегментов? - Это <b>%s</b>. - Вы создали <b>%s</b> сегментов - Нажмите здесь для просмотра Ваших сегментов. - Ваше имя пользователя: <b>%s</b> - Нажмите, чтобы изменить имя пользователя - Невозможно изменить имя пользователя: Статус: %1$d %2$s. - Имя пользователя успешно изменено. - Не удалось отправить сегмент.\nСегмент уже существует. - Не удалось отправить сегмент: %s. - Невозможно отправить сегмент: %s. - Невозможно отправить сегмент.\nЛимит запросов достигнут (слишком много запросов от данного пользователя или IP). - SponsorBlock временно не работает. - Невозможно отправить сегмент (статус: %1$d %2$s). - Сегмент успешно отправлен. - Уведомление при недоступности SponsorBlock API отключено. - Уведомление при недоступности SponsorBlock API включено. - Уведомление при недоступности SponsorBlock API - Изменить категорию - Проголосовать против - Невозможно проголосовать за сегмент: %s. - Невозможно проголосовать за сегмент (время ожидания API истекло). - Невозможно проголосовать за сегмент (статус: %1$d %2$s). - Нет сегментов для голосования. - Голосовать за - Настройки скопированы в буфер обмена. - Метка времени скопирована в буфер. (%s) - Ссылка скопирована в буфер обмена. - Ссылка с меткой времени скопирована в буфер. - Обычная - Палец вверх - Каир - Сердечко - Сердце (Оттенок) - Скрыта - Анимация двойного нажатия - Нижнее поле отступа \"мета\" панели может быть только в диапазоне от 0 до 64. -Сброс по умолчанию. - Отступ от прогресса до \"мета\" панели. -Диапазон от 0 до 64. - Нижнее поле отступа \"мета\" панели - Высота должна быть от 0 до 100 (%). - Настраивает высоту отступа, когда панель навигации скрыта, от 0 до 100 (%). - Процент высоты отступа - Нажать и удерживать метку времени - изменить статус повтора Shorts. - Длительное нажатие метки времени - "Показывает секцию заголовка видео в полноэкранном режиме. - -Ограничение: заголовок видео исчезает при нажатии." - Секция заголовка видео - Задержка автовоспроизведения следующего видео включена. - Задержка автовоспроизведения следующего видео отключена. - Задержка автовоспроизведения - "Пропустить предварительно загруженный буфер при запуске видео, чтобы избежать задержек обеспечения качества видео. - -Ограничения: -• При запуске видео возникает задержка примерно в 0.3 секунды. -• Не распространяется на HDR-видео, трансляции и видео длительностью менее 15 секунд." - Пропустить предварительно загруженный буфер - Уведомление отключено. - Уведомление включено. - Уведомление при пропуске буфера - Включение приведет к проблемам при воспроизведении видео. -(Связанно с буфером предзагрузки). - Пропущен предварительно загруженный буфер. - Значение должно быть в диапазоне от 0 до 8.0. Сброс по умолчанию. - Значение от 0 до 8.0. - Значение скорости - "Подменяет версию клиента на старую. - -• Это изменит внешний вид приложения, но могут возникнуть неизвестные проблемы. -• Если отключить данную опцию после её активации, старый интерфейс может оставаться до тех пор, пока данные приложения не будут очищены." - Версия приложения не подменена - Версия приложения подменена - 17.33.42 - Восстановление старого стиля пользовательского интерфейса - 17.41.37 - Восстановление старого стиля отображения секции плейлистов - 18.05.40 - Восстановление старого поля ввода комментариев - 18.17.43 - Восстановление старого стиля отображения всплывающей панели плеера - 18.33.40 - Восстановление старой панели действий Shorts - 18.38.45 - Восстановление старого поведения качества видео по умолчанию - 18.48.39 - Отключение обновления \"просмотров\" и \"лайков\" в реальном времени - 19.13.37 - Старый стиль анимаций - прокручивание чисел - Целевая версия приложения при подмене - Введите целевую версию приложения для подмены. - Целевая версия подмены - Подмена версии приложения - "Подмена на более раннюю версию YouTube. - -Это изменит вид и функции приложения, но могут быть побочные эффекты. - -При отключении подмены, рекомендуется очистить данные приложения для предотвращения проблем с пользовательским интерфейсом." - "Подменяет размеры устройства, для разблокировки более высокого качества видео, которое может быть недоступно на вашем устройстве." - Подмена размеров устройства - Видео кодек подмены как iOS - VP9 или AV1. - Видео кодек подмены как iOS - AVC (H.264). - Принудительно подмена как iOS, AVC (H.264) - "Включение - может улучшить время работы батареи и исправить задержки воспроизведения. - -AVC (H.264) имеет максимальное разрешение 1080p, и будет использовать больше интернет данных, чем VP9 или AV1." - "• Меню \"Звуковая дорожка\" не доступно." - "• Меню \"Звуковая дорожка\" VR не доступно." - "• Фильмы или платные видео могут не проигрываться." - Эффекты от подмены - • Видео может не воспроизводиться. - Клиент, используемый для получения данных потока, скрыт в Статистике для сисадминов. - Клиент, используемый для получения данных потока, отображается в Статистике для сисадминов. - Показывать в Статистике для сисадминов - "Подмена потоковых данных отключена. -Воспроизведение видео может не работать." - Подмена потоковых данных включена. - Подмена потоковых данных - Android - Android TV - Android VR - iOS - Клиент по умолчанию - Отключение этой настройки вызовет проблемы с воспроизведением видео. - Значение должно быть от 1 до 1000 (%). - Настройка чувствительности жеста яркости от 1 до 1000 (%). - Чувствительность жеста яркости - Жесты в режиме \"Блокировка экрана\" отключены. - Жесты в режиме \"Блокировка экрана\" включены. - Жесты в режиме \"Блокировка экрана\" - Авто - Амплитуда движения распознаваемая как жест. - Порог величины жеста - Видимость фона наложения при жесте. - Видимость фона жестов - Размер области для жеста больше 50 %. Сброс по умолчанию. - Процент изменяемой области экрана для жестов.\n\nПримечание:\nЭто также изменит размер области экрана для жеста двойного нажатия (перемотка). - Размер области экрана для жестов - Размер текста для наложения жестов. - Размер текста при жесте - Количество миллисекунд отображения наложения. - Таймаут наложения при жесте - Значение должно быть от 1 до 1000 (%). - Настройка чувствительности жеста громкости от 1 до 1000 (%). - -Рекомендованная чувствительность жеста 100% при шаге 15 громкости и 10% при шаге 150 громкости. - Чувствительность жеста громкости - "Подмена осуществляется с помощью подмены габаритов устройства. - -• Если подмена кнопки не происходит, перезагрузите устройство. -• Отключение этого параметра загружает больше рекламы со стороны сервера. -• Чтобы видеореклама была видна, следует отключить этот параметр." - Кнопки \"Создать\" и \"Уведомления\" не поменяны местами. - "Кнопки \"Создать\" и \"Уведомления\" поменяны местами. - -Примечание: Включение опции принудительно скрывает видеорекламу." - Подмена кнопки \"Создать\" на \"Уведомления\" - "Отключение этого параметра может привести к загрузке большего количества рекламы с сервера. - -Кроме того, реклама больше не будет блокироваться в Shorts. - -Если эта настройка не вступила в силу, попробуйте перейти в режим инкогнито." - По умолчанию - RVX Music - %s не установлен. Установите его. - Название пакета установленной RVX Music. - Имя пакета RVX Music - • История просмотра не работает. - "• Статус истории просмотров обычный. -• История просмотров может не работать с DNS или VPN." - • Статус истории просмотров изменен. - Об истории просмотра - Управление истории просмотра YouTube. - Управление всей историей - Обычный - Замена домена - Блокировать историю просмотра - Режим истории просмотра - Ошибка добавления канала \'%1$s\' в белый список %2$s. - Канал \'%1$s\' добавлен в белый список %2$s. - Каналы в белом списке отсутствуют. - Не добавлен в белый список. - Ошибка загрузки данных канала. - Добавлен в белый список. - Скорость воспроизведения - Удалить канал \'%1$s\' из белого списка %2$s? - Ошибка удаления канала \'%1$s\' из белого списка %2$s. - Канал \'%1$s\' удален из белого списка %2$s. - Управление каналами в белом списке. - \"Белый список\" канала - SponsorBlock - diff --git a/src/main/resources/youtube/translations/tr-rTR/missing_strings.xml b/src/main/resources/youtube/translations/tr-rTR/missing_strings.xml deleted file mode 100644 index fe4890375..000000000 --- a/src/main/resources/youtube/translations/tr-rTR/missing_strings.xml +++ /dev/null @@ -1,324 +0,0 @@ - - - Don\'t show again - Original - Phone - Phone (Max 480 dp) - Tablet - Tablet (Min 600 dp) - Change layout - In-app share sheet is used. - System share sheet is used. - Change share sheet - Courses / Learning - Start page changes only once. - "Start page always changes. - -Limitation: Back button on the toolbar may not work." - Change start page type - "Auto switch mix playlists is enabled when autoplay is turned on. - -Autoplay can be changed in YouTube settings: -Settings → Autoplay → Autoplay next video" - Auto switch mix playlists is disabled. - Disable switch mix playlists - Enabling this feature will disable automatic switching to YouTube Mix when playing music while autoplay is turned on. - Default playback speed is enabled for music. - "Default playback speed is disabled for music. - -Limitation: This setting may not apply to videos that do not include the 'Listen on YouTube Music' banner." - Disable playback speed for music - Chapters are enabled in the seekbar. - Chapters are disabled in the seekbar. - Disable seekbar chapters - Fountain animation is enabled above the Like button. - Fountain animation is disabled above the Like button. - Disable Like button animation - VP9 codec is enabled. - "VP9 codec is disabled. - -• Maximum resolution is 1080p. -• Video playback will use more internet data than VP9. -• VP9 codec is still used for HDR video." - Disable VP9 codec - "This will restore thumbnails to livestreams that do not have seekbar thumbnails. - -Internet data usage may be higher, and seekbar thumbnails will have a slight delay before showing. - -This feature works best with a very fast internet connection." - Seekbar thumbnails are medium quality. - Seekbar thumbnails are high quality. - Enable high quality thumbnails - Package name of your installed external downloader app, such as YTDLnis. - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - AI-generated video summary section is shown. - AI-generated video summary section is hidden. - Hide AI-generated video summary section - Highlighted search links are shown. - Highlighted search links are hidden. - Hide highlighted search links - Expandable shelves are shown. - Expandable shelves are hidden. - Hide expandable shelves - Floating button is shown. - Floating button is hidden. - Hide floating button - Surrounding a keyword/phrase with double-quotes will prevent partial matches of video titles and channel names.<br><br>For example,<br><b>\"ai\"</b> will hide the video: <b>How does AI work?</b><br>but will not hide: <b>What does fair use mean?</b> - Add quotes to use keyword: %s. - Keyword has conflicting declarations: %s. - Keyword is too short and requires quotes: %s. - Navigation bar is shown. - Navigation bar is hidden. - Hide navigation bar - 1080p Premium menu is shown. - 1080p Premium menu is hidden. - Hide 1080p Premium menu - Shopping shelf is shown. - Shopping shelf is hidden. - Related videos are shown. - Related videos are hidden. - Hide related videos - "This setting limits the maximum number of layouts that can be loaded on the player screen. - -If the layout of the player screen changes due to server-side changes, unintended layouts may be hidden on the player screen." - Chapter labels next to the timestamp are shown. - Chapter labels next to the timestamp are hidden. - Hide seekbar chapter labels - About menu is shown. - About menu is hidden. - Hide About menu - Accessibility menu is shown. - Accessibility menu is hidden. - Hide Accessibility menu - Account menu is shown. - Account menu is hidden. - Hide Account menu - Autoplay menu is shown. - Autoplay menu is hidden. - Hide Autoplay menu - Billing and payments menu is shown. - Billing and payments menu is hidden. - Hide Billing and payments menu - Captions menu is shown. - Captions menu is hidden. - Hide Captions menu - Connected apps menu is shown. - Connected apps menu is hidden. - Hide Connected apps menu - Data saving menu is shown. - Data saving menu is hidden. - Hide Data saving menu - General menu is shown. - General menu is hidden. - Hide General menu - Manage all history menu is shown. - Manage all history menu is hidden. - Hide Manage all history menu - Live chat menu is shown. - Live chat menu is hidden. - Hide Live chat menu - Notifications menu is shown. - Notifications menu is hidden. - Hide Notifications menu - Background menu is shown. - Background menu is hidden. - Hide Background menu - Watch on TV menu is shown. - Watch on TV menu is hidden. - Hide Watch on TV menu - Family Center menu is shown. - Family Center menu is hidden. - Hide Family Center menu - Try experimental new features menu is shown. - Try experimental new features menu is hidden. - Hide Try experimental new features menu - Privacy menu is shown. - Privacy menu is hidden. - Hide Privacy menu - Purchases and memberships menu is shown. - Purchases and memberships menu is hidden. - Hide Purchases and memberships menu - Video quality preferences menu is shown. - Video quality preferences menu is hidden. - Hide Video quality preferences menu - Your data in YouTube menu is shown. - Your data in YouTube menu is hidden. - Hide Your data in YouTube menu - Disabled comments button or with label \"0\" is shown. - Disabled comments button or with label \"0\" is hidden. - Hide disabled comments button - "Floating buttons like 'Use this sound' are shown in the Shorts channel tab." - "Floating buttons like 'Use this sound' are hidden in the Shorts channel tab." - Green screen button is shown. - Green screen button is hidden. - Hide Green screen button - Location button is shown. - Location button is hidden. - Hide location button - Save music button is shown. - Save music button is hidden. - Hide Save music button - Search suggestions button is shown. - Search suggestions button is hidden. - Hide search suggestions button - Shown in channel. - "Hidden in channel. - -Info: -• Only shelves with the Shorts header on the home tab are hidden." - Hide in channel - Shopping button is shown. - Stickers are shown. - Stickers are hidden. - Hide stickers - Use template button is shown. - Use template button is hidden. - Hide Use template button - Use this sound button is shown. - Use this sound button is hidden. - Hide Use this sound button - "Home / Subscription / Search results are filtered to hide videos with views less or greater than a specified number. - -Limitations: -• Shorts cannot be hidden. -• Videos with 0 views are not filtered." - About view count filtering - Videos in home feed are not filtered. - Videos in home feed are filtered. - Hide home videos by views - Search results are not filtered. - Search results are filtered. - Hide search results by views - Videos in subscriptions feed are not filtered. - Videos in subscriptions feed are filtered. - Hide subscription videos by views - YouTube Doodles are shown. - YouTube Doodles are hidden. - Hide YouTube Doodles - "YouTube Doodles show up a few days each year. - -If a YouTube Doodle is currently showing in your region and this setting is on, the filter bar below the search bar will also be hidden." - MMT Blue - MMT Green - MMT Orange - MMT Pink - MMT Turquoise - MMT Yellow - Revancify Yellow - Vanced Black - Vanced Light - Xisr Yellow - If shown, the native playlist download button opens the native in-app downloader. - Native playlist download button is always shown, and in public playlists, it opens your external downloader. - Native video download button opens the native in-app downloader. - YouTube Music is required to override button action. Tap here to download YouTube Music. - Prerequisite - YouTube Music button opens the native app. - YouTube Music button opens the RVX Music. - Override YouTube Music button - Download button - Suggested actions - Overrides the click action of in-app buttons. - Hook buttons - Hide or show navigation bar section components. - Navigation bar - Return YouTube Username - Spoof the streaming data to prevent playback issues. - Spoof streaming data - Offset - A toast will not be shown when changing the default playback speed. - A toast will be shown when changing the default playback speed. - Show a toast - A toast will not be shown when changing the default video quality. - A toast will be shown when changing the default video quality. - Show a toast - @handle (Username) - Display format - Username (@handle) - Username - Handle is used. - Username is used. - Enable Return YouTube Username - "A YouTube Data API v3 Developer Key is required to replace handles with usernames. - -The daily quota for API keys on the free plan is 10,000, and 1 quota is used to replace a handle with a username for 1 comment. - -Click to see how to issue a API key." - About YouTube Data API key - The developer key for using the YouTube Data API v3. - YouTube Data API key - 1. Go to <a href=%1$s>Create a new project</a>.<br>2. Click the <b>CREATE</b> button.<br>3. Go to <a href=%2$s>YouTube Data API v3</a>.<br>4. Click the <b>ENABLE</b> button.<br>5. Click the <b>CREATE CREDENTIALS</b> button.<br>6. Select the <b>Public data</b> option.<br>7. Click the <b>NEXT</b> button.<br>8. Copy the API key.<br><br>※ API key should never be shared with others, so it is not included in Import / Export settings. - Issue YouTube Data API v3 developer key - Estimated likes are hidden. - Estimated likes are shown. - Show estimated likes - Hidden - "Phrases like '#', 'Fundraiser', 'Shop' and 'products' were shown from the video subtitles." - "Phrases like '#', 'Fundraiser', 'Shop' and 'products' were hidden from the video subtitles." - Sanitize video subtitle - Invalid time duration. - Adjust: Mark Start and End Time for segment - Verify the Segment - Edit the Segment - Forward by Specified Time (Default: 150ms) - Publish Created Segment - Rewind by Specified Time (Default: 150ms) - Tap here to view your segments. - Height percentage must be between 0-100 (%). - Configure the height percentage of the empty space left when the navigation bar is hidden, between 0 and 100 (%). - Height percentage of empty space - 19.13.37 - Restore old style Rolling number animations - iOS video codec is AVC (H.264), VP9, or AV1. - iOS video codec is AVC (H.264). - Force iOS AVC (H.264) - "Enabling this might improve battery life and fix playback stuttering. - -AVC (H.264) has a maximum resolution of 1080p, and video playback will use more internet data than VP9 or AV1." - "• Audio track menu is missing. -• Stable volume is not available." - "• Audio track menu is missing. -• Stable volume is not available." - "• Movies or paid videos may not play. -• Livestreams start from the beginning. -• Videos may end 1 second early. -• No opus audio codec." - Spoofing side effects - • Video may not play. - Client used to fetch streaming data is hidden in Stats for nerds. - Client used to fetch streaming data is shown in Stats for nerds. - Show in Stats for nerds - "Streaming data is not spoofed. Video playback may not work." - Streaming data is spoofed. - Spoof streaming data - Android - Android TV - Android VR - iOS - Default client - Turning off this setting may cause video playback issues. - Brightness swipe sensitivity must be between 1-1000 (%). - Configure the minimum distance for brightness swiping between 1 and 1000 (%).\nThe shorter the minimum distance, the faster the brightness level changes. - Brightness swipe sensitivity - Volume swipe sensitivity must be between 1-1000 (%). - Configure the minimum distance for volume swiping between 1 and 1000 (%).\n\nThe shorter the minimum distance, the faster the volume level changes.\n\nRecommended volume swipe sensitivity is 100% at 15-volume steps and 10% at 150-volume steps. - Volume swipe sensitivity - Create button is not switched with Notifications button. - "Create button is switched with Notifications button. - -Note: Enabling this also forcibly hides video ads." - "Disabling this might load more ads from the server. - -Also, ads will no longer be blocked in Shorts. - -If this setting do not take effect, try switching to Incognito mode." - RVX Music - %s is not installed. Please install it. - Package name of installed RVX Music. - RVX Music package name - "• Follows the watch history settings of Google account. -• Watch history may not work due to DNS or VPN." - • Follows the watch history settings of Google account. - diff --git a/src/main/resources/youtube/translations/tr-rTR/strings.xml b/src/main/resources/youtube/translations/tr-rTR/strings.xml deleted file mode 100644 index a0c06ea98..000000000 --- a/src/main/resources/youtube/translations/tr-rTR/strings.xml +++ /dev/null @@ -1,1390 +0,0 @@ - - - Video oynatıcı için erişilebilirlik kontrolleri açılsın mı? - Bir erişilebilirlik hizmeti açık olduğundan kontrolleriniz değiştirildi. - Devam Et - "GmsCore'un arka planda çalışma izni yoktur. - -Telefonunuz için 'Uygulamamı öldürme' kılavuzunu izleyin ve talimatları MicroG kurulumunuza uygulayın. - -Bu uygulamanın çalışması için gereklidir." - "Sorunları önlemek için GmsCore pil optimizasyonları devre dışı bırakılmalıdır. - -Devam düğmesine dokunun ve pil optimizasyonlarını devre dışı bırakın." - Web sitesini aç - Eylem gerekiyor - Bildirimleri alabilmek için bulut mesajlaşmayı etkinleştirin. - GmsCore\'yi aç - GmsCore yüklü değil. Yükleyin. - "DeArrow, YouTube videoları için kitle kaynaklı kapak fotoğrafları sağlar. Bu kapak fotoğrafları genellikle YouTube tarafından sağlananlardan daha alakalıdır. Etkinleştirilirse video URL'leri API sunucusuna gönderilir ve başka veri gönderilmez. - -DeArrow hakkında daha fazla bilgi edinmek için buraya dokunun." - DeArrow Hakkında - Geçersiz DeArrow API URL. - DeArrow kapak fotoğraf önbellek uç noktasının URL\'si. Ne yaptığınızı bilmiyorsanız bunu değiştirmeyin. - DeArrow API uç noktası - DeArrow\'un mevcut olmaması durumunda uyarı gösterilmez. - DeArrow\'un mevcut olmaması durumunda uyarı gösterilir. - API mevcut olmadığında bir uyarı göster - DeArrow geçici olarak kullanılamıyor (durum kodu: %s) - DeArrow geçici olarak kullanılamıyor. - Ana Sayfa Sekmesi - \'Siz\' sekmesi - Orijinal küçük resimler - DeArrow & Orijinal küçük resimleri - DeArrow & Hareketsiz Yakalamalar - Hareketsiz yakalamalar - Oynatıcı çalma listeleri, öneriler - Arama sonuçları - Hareketsiz video çekimleri - Her videonun başlangıcı, ortası, ve sonundan fotoğraflar alınır. Bu fotoğraflar, YouTube içindedir ve dışarıdan bir API kullanılmamaktadır. - Hareketsiz video çekimleri hakkında - Hızlı hareketsiz çekimler kullanılıyor. - Orta kalitede çekimler kullanılmaktadır. Kapak fotoğrafları daha hızlı yüklenir, ama canlı yayınlar, paylaşılmayan veya eski videolar boş bir kapak fotoğrafı gösterebilirler. - Hızlı hareketsiz çekimler kullan - Videonun başlangıcı - Videonun ortası - Videonun sonu - Fotoğrafın çekileceği video süresi - Abonelikler sekmesi - Süre sayacının yanına gösterge eklenmiyor. - "Süre sayacının yanına gösterge ekleniyor." - Süre sayacının yanına gösterge ekle - Videonun mevcut oynatma hızı gösteriliyor. - Videonun mevcut kalitesi gösteriliyor. - Gösterilecek bilgi - Güç tasarrufu modundayken ambiyans modu kullanılamıyor. - Güç tasarrufu modundayken ambiyans modu kullanılabilir. - Ambiyans modu kısıtlamalarını atlayın - Resimlerin alınacağı alan adı.\nNot: Yalnızca alan adını girin, yani \"https\:\/\/\" öneki olmadan. - Alternatif alan adı - Orijinal resim barındırıcısı kullanılıyor.\n\nBunun etkinleştirilmesi bazı bölgelerde engellenen eksik resimleri düzeltebilir. - Yt4.ggpht.com resim sunucusu kullanılıyor. - Resimlerin bölge kısıtlamalarını atla - Anahtar geçişleri kullanılıyor. - Yazı geçişleri kullanılıryor. - Geçiş anahtarı tipini değiştir - Otomatik Oynat - Varsayılan - Duraklat - Tekrarla - Shorts tekrarlama durumunu değiştirme - Kanalları Tara - Varsayılan - Keşfet - Gaming - Geçmiş - Kitaplık - Beğenilen videolar - Canlı - Filmler - Müzik - Ara - Shorts - Sporlar - Abonelikler - Trendler - Daha sonra izlenecekler - Başlangıç ​​sayfasını değiştir - Genel başlıklar etkin. - Premium başlık etkin. - YouTube başlığını değiştir - Yeni satırlarla ayrılmış olarak hangi bileşenlerin filtreleneceğini yapılandırın. - Özel filtreyi düzenle - Özel filtre devre dışı - Özel filtre etkin - Özel filtreyi etkinleştir - Geçersiz özel filtre: %s - Eski stil açılır menü kullanıldı. - Özel diyalog kullanıldı. - Oynatma hızı panel türü - Geçersiz özel oynatma hızları. Hızlar varsayılana sıfırlandı. - Geçersiz özel oynatma hızları. Varsayılanlar seçildi. - Mevcut oynatma hızlarını değiştirin. - Özel oynatma hızlarını düzenle - Oynatıcının siyah arkaplan opaklığı 0 ila 100 arasında olmalıdır. Varsayılan değere sıfırlandı. - 0-100 arası opaklık değeri, 0 şeffaftır. - Oynatıcının siyah arkaplan opaklığı - Zaman çubuğunun olmasını istediğiniz rengin kodunu (6\'lı/hex) girin - Zaman çubuğu renk kodu - Bir tarayıcıda, bir YouTube videosuna tıkladığınızda videoyu RVX\'te açmak için, \"Desteklenen bağlantıları aç\"ı açın ve desteklenen web adreslerini etkinleştirin. - Varsayılan uygulama ayarlarını aç - Varsayılan oynatma hızı - Mobil ağda varsayılan video kalitesi - Wi̇-Fi ağında varsayılan video kalitesi - Video tam ekrandayken ambiyans modunu devre dışı bırak - Ambiyans modu tam ekranda etkin. - Ambiyans modu tam ekranda devre dışı. - Tam ekranda ambiyans modunu devre dışı bırak - Ambiyans modu hep kapat. - Ambiyans modu etkin. - Ambiyans modu devre dışı. - Ambiyans modunu devre dışı bırak - Ses parçalarının kendiliğinden açılması etkin. - Ses parçalarının kendiliğinden açılması kapalı. - Ses parçalarının kendiliğinden açılmasını kapat - Altyazılar kendiliğinden açılabiliyor - Altyazıların kendiliğinden açılması kapalı - Altyazıların kendiliğinden açılmasını kapat - Otomatik oynatıcı açılır panelleri devre dışı bırakıldı. - Otomatik oynatıcı açılır panelleri etkinleştirildi. - Oynatıcı açılır panellerini devre dışı bırak - Canlı yayınlarda varsayılan oynatma hızı etkin - Canlı yayınlarda varsayılan oynatma hızı devre dışı - Canlı yayınlarda varsayılan oynatma hızını devre dışı bırak - Etkileşim paneli açık. - Etkileşim paneli devre dışı. - Etkileşim panelini devre dışı bırak - Titreşim açık. - Titreşim kapalı. - Bölümlerde geçiş yaparkenki titreşimi kapat - Titreşim açık. - Titreşim kapalı. - Videoyu sararkenki titreşimi kapat - Titreşim açık - Titreşim kapalı. - Zaman çubuğunu kaydırırkenki titreşimi kapat - Titreşim açık. - Titreşim kapalı - \"İptal etmek için bırakın\" titreşimini kapat - Titreşim açık. - Titreşim kapalı. - Videoyu yakınlaştırırkenki titreşimi kapat - Otomatik HDR parlaklığı etkin - Otomatik HDR parlaklığı devre dışı - Otomatik HDR parlaklığını devre dışı bırak - HDR etkin - HDR devre dışı - HDR\'lı videolarda, HDR\'ı devre dışı bırak - Videoyu tam ekrana alırken otomatik olarak yatay moda geçme etkin - Videoyu tam ekrana alırken otomatik olarak yatay moda geçme devre dışı - Videoyu otomatik yatay moda geçirmeyi kapat - Beğen Ve beğenme butonları bahsedildiğinde parıldayacak. - Beğen Ve beğenme butonları bahsedildiğinde parıldamayacak. - Beğen ve Beğenme düğmesinin parlamasını devre dışı bırak - "CronetEngine'in QUIC protokolünü devre dışı bırak" - QUIC protokolünü devre dışı bırak - Shorts oynatıcı açılışta devam edecek - Shorts oynatıcı açılışta devam etmeyecek - Shorts oynatıcıya devam edilmesini devre dışı bırak - Kayan numaralar animasyonlu - Kayan numaralar animasyonlu değil. - Yuvarlanan numara animasyonlarını devre dışı bırak - "Videoya basılı tutarken '2x>>' devre dışı bırakın. - -Not: Bu özelliği devre dışı bırakmak, eski arayüzdeki \"Videoyu sarmak için çubuğu sağa sola kaydırma\" özelliğini geri getirecektir." - Videoya basılı tutarak hızlandırmayı kapat - Açılış animasyonu etkin. - Açılış animasyonu etkin değil. - Açılış animasyonunu devre dışı bırak - "Video açıklaması genişletildiğinde aşağıdaki etkileşimleri devre dışı bırakır: - -• Kaydırmak için dokunun. -• Metni seçmek için dokunun ve basılı tutun." - Video açıklama etkileşimini devre dışı bırak - Kahire arama çubuğu devre dışı. - "Kahire arama çubuğu etkin. - -Yan etki: Kahire teması bildirim noktalarına da uygulanır." - Kahire zaman çubuğunu etkinleştir - Tam ekran videoda sıkışık oynatıcı etkin değil - Tam ekran videoda sıkışık oynatıcı etkin. - Tam ekranda sıkışık oynatıcıyı etkinleştir - Özel oynatma hızları kapalı - Özel oynatma hızları etkin - Özel oynatma hızlarını etkinleştir - Özel zaman çubuğu rengi kapalı - Özel zaman çubuğu rengi etkin - Özel zaman çubuğu rengini etkinleştir - Hata ayıklama günlükleri arabellek içermiyor - Hata ayıklama günlükleri arabellek içeriyor - Hata ayıklama günlüklerine arabelleği kaydet - Hata ayıklama günlükleri devre dışı - Hata ayıklama günlükleri etkin - Hata ayıklama günlüğünü etkinleştir - Varsayılan oynatma hızı Shorts videolara uygulanmaz. - Varsayılan oynatma hızı Shorts videolara uygulanır. - Shorts videolarında varsayılan oynatma hızını etkinleştir - Harici tarayıcı devre dışı - Harici tarayıcı etkin - Harici tarayıcıyı kullan - Gradyan yükleme ekranı devre dışı - Gradyan yükleme ekranı etkin - Gradyan yükleme ekranını etkinleştir - Gezinme düğmeleri arasındaki boşluk daralır. - Gezinme düğmeleri arasındaki boşluk daralır. - Dar gezinme düğmelerini etkinleştir - Varsayılan yönlendirme politikası takip ediliyor. - URL yönlendirmeleri atlanıyor. - Bağlantıları direkt açmayı etkinleştir - Oynatıcı yanıtı OPUS codec bileşenini içeriyorsa OPUS codec bileşenini etkinleştirin. - OPUS kodek bileşenini etkinleştir - Tam ekrana girip çıkarken parlaklığı kaydetme ve geri yükleme. - Tam ekrandan çıkarken veya tam ekrana girerken parlaklığı kaydedin ve geri yükleyin. - Kaydetmeyi ve parlaklığı geri yüklemeyi etkinleştir - Zaman çubuğuna dokunma kapalı - Zaman çubuğuna dokunma etkin - Zaman çubuğuna dokunmayı etkinleştir - Zaman damgası gizleniyor. - "Zaman damgası etkin. - -Bilinen sorunlar: -•Bu, Google'ın geliştirme aşamasındaki bir özellik olduğundan düzen bozuk olabilir. -•Bu ayar yalnızca zaman damgalarını etkinleştirmekle kalmaz, aynı zamanda kullanıcıların oynatıcı arka planına tıklayarak kullanıcı arayüzünü gizlemesine de olanak tanır." - Zaman damgalarını etkinleştir - Video tam ekrandayken, ekranın solunu kaydırarak ekran parlaklığını ayarlama kapalı - Video tam ekrandayken, ekranın solunu kaydırarak ekran parlaklığını ayarlama açık - Kaydırarak parlaklığı ayarla - Titreşim kapalı. - Titreşim açık. - Titreşimli geribildirimi etkinleştir - Parlaklık hareketinin en düşük değeri otomatik parlaklığı etkinleştirmez. - Parlaklık hareketinin en düşük değeri otomatik parlaklığı etkinleştirir. - Kaydırarak otomatik parlaklığı etkinleştir - Kaydırma hareketini etkinleştirmek için dokunun. - Kaydırma hareketini etkinleştirmek için dokunun ve basılı tutun. - Basılı tut ve kaydır hareketini etkinleştir - Yukarı kaydırma / aşağı bir dahaki videoyu oynatmayacaktır / önceki video. - Yukarı kaydırma / aşağı bir dahaki videoyu oynatacaktır / önceki video. - Kaydırarak videoyu değiştirmeyi etkinleştir - Video tam ekrandayken, ekranın sağını kaydırarak sesi ayarlama kapalı - Video tam ekrandayken, ekranın sağını kaydırarak sesi ayarlama açık - Kaydırarak sesi ayarla - Gezinme çubuğu opaktır. - Gezinme çubuğu yarı saydam. - Yarı saydam gezinme çubuğunu etkinleştir - Oynatıcının altına doğru kaydırırken tam ekrana giriş devre dışı bırakıldı. - Oynatıcının altından kaydırarak tam ekrana geçme etkin. - İzleme paneli hareketlerini etkinleştir - "Bu ayarın etkinleştirilmesi, Siz sekmesindeki Ayarlar butonunu kaldıracaktır. - -Bu durumda lütfen şu yolu kullanın: -Siz sekmesi→ Kanalı görüntüle→ Menü→ Ayarlar" - \"Siz\" sekmesinde de etkinleştir - Geniş arama çubuğu devre dışı - Geniş arama çubuğu etkin - Geniş arama çubuğunu etkinleştir - Geniş arama çubuğu YouTube başlığını içermez. - Geniş arama çubuğu YouTube başlığını içerir. - Geniş araba çubuğunu başlıkla etkinleştir - Açıklama - "Video açıklama panelinin başlığını kendi dilinizde girin. Girilen dize video açıklama paneli başlığıyla eşleşmiyorsa Video açıklamasını genişlet seçeneği çalışmayabilir." - Video açıklama panelindeki başlık - Video açıklaması elle genişletilir. - Video açıklaması otomatikman genişletilir. - Açıklamayı genişlet - Devam etmek istiyor musunuz? - Varsayılan değerlere sıfırla. - Düzeni normal şekilde yüklemek için yeniden başlatın - "\?" - Yenile ve yeniden başlat - Ayarlar dışa aktarılamadı. - Ayarlar başarıyla dışa aktarıldı. - Ayarlarınızı bir dosyaya kaydedin. - Ayarları dışa aktarın - İçe aktar - Kopyala - Ayarları yazı olarak içe veya dışa aktarın. - İçe / Dışa yazı olarak aktar - İçe aktarma başarısız oldu: %s. - Ayarlar varsayılana sıfırlandı. - %d ayar içe aktarıldı. - Ayarları kaydedilen dosyadan içe aktarın. - Ayarları içe aktar - Sıfırla - Arama %s - ReVanced Extended - Harici indirici - Yüklü değil - "%1$s yüklü değil. -Lütfen web sitesinden %2$s dosyasını indirin." - Dikkat - %s kurulmamış. Lütfen önce indiriniz. - Oynatma listesi indirici paket ismi - Yüklü olan harici indirme uygulamanızın paket adı, örneğin NewPipe veya YTDLnis gibi. - Video indirici paket adı - "Aşağıdaki durumlarda video tam ekrana geçecektir: - -• Yorumlardaki zaman damgasına tıklandığında. -• Video başladığında." - Tam ekrana zorla - Yeni bir satırla ayrılmış olarak filtrelenecek hesap menüsü adlarının listesi. - Hesap menüsü filtresini düzenle - "Özel filtredeki hesap menüsü elementlerini gizle." - Hesap menüsünü gizle - Albüm kartları gösteriliyor - Albüm kartları gizleniyor - Albüm kartlarını gizle - Öne çıkan yerler, Oyunlar ve Müzik bölümleri gizlenmez. - Öne çıkan yerler, Oyunlar ve Müzik bölümleri gizlenir. - Nitelikler bölümünü gizle - Video sonunda sol üstteki \"Sıradaki\" video gösteriliyor - Video sonunda sol üstteki \"Sıradaki\" video gizleniyor - Video sonunda sol üstteki \"Sıradaki\"ni gizle - Mağazaya göz at butonu gösteriliyor. - Mağazaya göz at butonu gizleniyor. - Mağazaya göz at butonunu gizle - "Döner rafı ana sayfa ve keşfet sekmesinden gizler." - Atlıkarınca rafını gizle - Akışta görünür. - Akışta gizli. - Akışta üstteki kategori çubuğunu gizle - İlgili videolarda görünür. - İlgili videolarda gizli. - İlgili videolarda gizle - Arama sonuçlarında gizlenmiyor. - Arama sonuçlarında gizli. - Arama sonuçlarında gizle - Yorumların en üstündeki Topluluk Kuralları hatırlatıcısı gizlenmiyor - Yorumların en üstündeki Topluluk Kuralları hatırlatıcısı gizleniyor - Kanal yönergelerini gizle - Kanal üyeleri menüsü gösteriliyor - Kanal üyeleri menüsü gizleniyor - Kanal profilindeki \"Üyelerimiz\" bölümünü gizle - Kanal profilinin en üstündeki linkler gösteriliyor. - Kanal profilinin en üstündeki linkler gizleniyor. - Kanal profilindeki linkleri gizle - "Shorts -Oynatma listeleri -Mağaza" - Yeni bir satırla ayrılmış olarak filtrelenecek kanal sekmesi adlarının listesi. - Kanal sekmesi filtresini - Kanal sekmesi filtresi devre dışı. - Kanal sekmesi filtresi etkin. - Kanal sekmesi filtresini etkinleştir - Kanal filigranı gösteriliyor - Kanal filigranı gizleniyor - Videonun sağ altındaki kanal filigranını gizle - Bölümler kısmı gizlenmiyor. - Bölümler kısmı gizleniyor. - \"Öne çıkan yerler\"i gizle - Silindirik öneri butonları ve menüleri gösteriliyor - Silindirik öneri butonları ve menüleri gizleniyor - Buna benzer daha çok video çipini gizle - \"Klip\" butonu gösteriliyor. - \"Klip\" butonu gizleniyor. - Klip butonunu gizle - Short oluştur butonu gösterilir. - Short oluştur butonu gizlenir. - Short oluştur butonunu gizle - \"Teşekkürler\" butonu gösteriliyor. - \"Teşekkürler\" butonu gizleniyor. - Teşekkürler butonunu gizle - Zaman damgası ve emoji düğmelerini gizlenmiyor. - Zaman damgası ve emoji düğmelerini gizleniyor - Zaman damgası ve emoji düğmelerini gizle - Üyeler tarafından yapılan yorumlar afişi gizlenmiyor. - \'Üyeler tarafından yapılan yorumlar\' afişi gizleniyor. - \'Üyeler tarafından yapılan yorumlar\' afişini gizle - Akışta yorumlar kısmı gösteriliyor. - Akışta yorumlar kısmı gizleniyor. - Akışta yorumlar bölümünü gizle - Yorumlar kısmı gizlenmiyor. - Yorumlar kısmı gizleniyor. - Yorumlar bölümünü gizle - Kanalda gösterilir. - Kanalda gizli. - Kanalda gizle - Akışta ve ilgili videolarda gösterilir. - Akışta ve ilgili videolarda gizlidir. - Akışta ve ilgili videolarda gizle - Abonelikler sayfasında topluluk gönderileri gösteriliyor - Abonelikler sayfasında topluluk gönderileri gizleniyor - Abonelikler kısmında topluluk gönderilerini gizle - Bu içeriğin nasıl yapıldığı bölümü gizlenmiyor. - Bu içeriğin nasıl yapıldığı bölümü gizlidir. - İçerikler bölümünü gizle - Bağış etkinliği menüleri gösteriliyor - Bağış etkinliği menüleri gizleniyor - Video altındaki bağış etkinliklerini gizle - Çift tıklama arayüz filtresi gizlenmiyor. - Çift tıklama arayüz filtresi gizli. - Çift tıklama arayüz filtresini gizle - \"İndir\" butonu gösteriliyor. - \"İndir\" butonu gizleniyor - İndir düğmesini gizle - Bitiş ekranı kartları gösteriliyor - Bitiş ekranı kartları gizleniyor - Videoların sonundaki kartları gizle - Genişletilebilir çipler gizlenmiyor. - Genişletilebilir çipler gizleniyor. - Vdeoların altındaki genişletilebilir çipi gizle - Altyazılar butonu gizlenmiyor. - Altyazılar butonu gizleniyor. - Akış altyazı butonunu gizle - Yeni bir satırla ayrılmış olarak filtrelenecek Açılır pencere menüsü adlarının listesi. - Akış açılır menü filtresi - Akış açılır menü filtresi devre dışı. - Akış açılır menü filtresi etkin. - Akış açılır menü filtresini etkinleştir - Akış arama çubuğu gizlenmiyor. - Akış arama çubuğu gizleniyor. - Akışta çıkan arama çubuğunu gizle - YouTube\'un anketleri gösteriliyor - YouTube\'un anketleri gizleniyor - Akışta çıkan anketleri gizle - Zaman çubuğunu yukarı çekince çıkan film şeridi / hassas sarma etkin. - Zaman çubuğunu yukarı çekince çıkan film şeridi / hassas sarma etkin devre dışı. - Film şeridini / hassas sarmayı devre dışı bırak - Mikrofon butonu etkin - Mikrofon butonu devre dışı - Aramada sağ alttaki mikrofon butonunu gizle - \'Sizin için\' rafları gizlenmiyor. - \'Sizin için\' rafları gizleniyor. - \'Sizin için\' rafını gizle - Tam ekran reklamları görünür. - Tam ekran reklamları gizli. - Tam ekran reklamlarını gizle - "Tam ekran reklamları engellendi. - -Yan etki: Tam ekrandaki topluluk gönderisi resimleri engellenebilir." - Tam ekran reklamlar Kapat düğmesiyle kapatılır. - Tam ekran reklamlarını kapat - Genel reklamlar gösteriliyor - Genel reklamlar gizleniyor - Genel reklamları gizle - YouTube Premium promosyonu gösteriliyor. - YouTube Premium promosyonu gizleniyor. - YouTube Premium reklamlarını gizle - Video ve topluluk gönderisi arasındaki gri çubuk gizlenmiyor. - Video ve topluluk gönderisi arasındaki gri çubuk gizleniyor. - Gri ayırıcıyı gizle - Etiket gizlenmiyor. - Etiket gizleniyor. - Etiketi gizle - Fotoğraf arama butonu gösteriliyor. - Fotoğraf arama butonu gizlendi. - Fotoğraf arama düğmesini gizle - Aramalarda çıkan resimler gizlenmiyor. - Aramalarda çıkan resimler gizleniyor. - Görüntü rafını gizle - Bilgi kartları bölümü görünür. - Video açıklamasındaki kanal bilgileri gizleniyor. - Video açıklamasındaki kanal bilgileri kısmını gizle - Videonun sağ üstünde çıkan bilgi kartları gizlenmiyor. - Videonun sağ üstünde çıkan bilgi kartları gizleniyor. - Bilgi kartlarını gizle - Bilgi panelleri gizlenmiyor. - Bilgi panelleri gizleniyor. - Bilgi panellerini gizle - \"Katıl\" butonu gösteriliyor - \"Katıl\" butonu gizleniyor - Katıl butonunu gizle - Anahtar kavramlar bölümünü gizlenmiyor. - Anahtar kavramlar bölümünü gizli. - Anahtar kavramlar bölümünü gizle - "Ana Sayfa/Abonelikler/Arama sonuçları anahtar kelimelerle eşleşen videoları gizlemek için filtrelenir - -Kısıtlamalar: -• Bazı Short videolar gizlenemeyebilir -• Bazı arayüz bileşenleri gizlenemeyebilir -• Anahtar kelime aratmak hiçbir sonuç göstermeyebilir" - Anahtar kelime filtreleme hakkında - Bütün kelimeyi eşle - Yorumlar anahtar kelimelerle filtrelenmiyor. - Yorumlar anahtar kelimelerle filtreleniyor. - Anahtar kelimeler ile yorumları gizle - Ana sayfa videoları anahtar kelimeler tarafından filtrelenmiyor. - Ana sayfa videoları anahtar kelimeler tarafından filtreleniyor. - Ana ekran videolarını anahtar kelimelerle gizle. - "Yeni satırla ayrılmış, gizlenecek anahtar kelimeler ve söz öbekleri - -Ortasında büyük harf bulunan kelimeler büyük harfle birlikte girilmelidir (örn: iPhone, TikTok, LeBlanc)" - Anahtar kelime filtresini düzenle - Arama sonuçları anahtar kelimelerle filtrelenmiyor. - Arama sonuçları anahtar kelimelerle filtreleniyor. - Arama sonuçlarını anahtar kelimelerle filtrele - Abonelik videoları anahtar kelimeler tarafından filtrelenmiyor. - Abonelik videoları anahtar kelimeler tarafından filtreleniyor. - Abonelik videolarını anahtar kelimelerle gizle - \'%1$s\' anahtar kelimesi tüm videoları gizleyecek. - Geçersiz anahtar kelime. \'%s\' kullanılamaz - Son gönderiler gizlenmiyor. - Son gönderiler gizleniyor. - \"Son yayınlar\"ı gizle - Son videolar butonu gizlenmiyor. - Son videolar butonu gizleniyor. - Son videolar butonunu gizle - Beğen ve Beğenmeme düğmeleri görünür. - Beğen ve beğenmeme butonları gizleniyor. - Beğen ve Beğenmeme butonlarını gizle - Canlı sohbet mesajları gösterilir.\n\nBu ayar, Shorts canlı videoları için de geçerlidir. - Canlı sohbet mesajları gizlenir.\n\nBu ayar, Shorts canlı videoları için de geçerlidir. - Canlı sohbet mesajlarını gizle - Canlı sohbeti tekrar oynat düğmesi görünür.\n\nCanlı sohbet kapatıldığında tam ekranda görünür. - Canlı sohbeti tekrar oynat düğmesi gizlidir.\n\nCanlı sohbet kapatıldığında tam ekranda görünür. - Canlı sohbet tekrarı butonunu gizle - Abone olunmayan kanallardan yüklenen ana sayfa yayınlarında 1.000\'den az izlenen videoları gizleyin. - Az izlenen videoları gizle - Tıbbi bilgi içeren paneller gizlenmiyor. - Tıbbi bilgi içeren paneller gizleniyor. - Tıbbi panelleri gizle - Ürünler rafı gizlenmiyor. - Ürünler rafı gizleniyor. - Ürünler kısmını gizle - \"Mix\" oynatma listeleri gösteriliyor - \"Mix\" oynatma listeleri gizleniyor - \"Mix\" oynatma listelerini gizle - Filmler rafı gizlenmiyor. - Filmler rafı gizleniyor. - Filmler rafını gizle - Oluştur butonu gösteriliyor. - Oluştur butonu gizleniyor. - Oluştur butonunu gizle - Ana sayfa düğmesi görünür. - Ana sayfa düğmesi gizli. - Ana Sayfa butonunu gizle - Alt gezinme çubuğundaki butonların yazıları gizlenmiyor. - Alt gezinme çubuğundaki butonların yazıları gizleniyor. - Navigasyon paneli altyazılarını gizle - Kitaplık butonu gösteriliyor. - Kitaplık butonu gizleniyor. - Kitaplık butonunu gizle - Bildirimler butonu gizlenmiyor. - Bildirimler butonu gizleniyor. - \"Bildirimler\" butonunu gizle - Shorts düğmesi görünür durumda. - Shorts düğmesi gizli durumda. - Shorts butonunu gizle - Abonelikler butonu gizlenmiyor. - Abonelikleri butonu gizli. - Abonelikler butonunu gizle - \"Beni bilgilendir\" butonu gizlenmiyor. - \"Beni bilgilendir\" butonu gizleniyor. - \"Beni bilgilendir\" butonunu gizle - Ücretli tanıtım etiketi gösteriliyor. - Ücretli tanıtım etiketi gizli. - Ücretli tanıtım etiketini gizle - Oynanabilir öğeler görünür. - Oynanabilir öğeler gizli. - Oynanabilirleri gizle - Otomatik oynatma butonu gösteriliyor. - Otomatik oynatma butonu gizleniyor. - Otomatik oynatma butonunu gizle - Altyazılar butonu gizlenmiyor. - Altyazılar butonu gizleniyor. - Altyazı butonunu gizle - Yansıtma düğmesi görünür. - \"Yayınla\" butonu gizli durumda. - Yayınla butonunu gizle - Videoyu küçült butonu gösteriliyor - Videoyu küçült butonu devre dışı. - Sol üstteki videoyu küçültme butonunu gizle - Ambiyans modu menüsü görünür. - Ambiyans modu menüsü gizli. - Ambiyans mod menüsünü gizle - \"Ses Parçası\" menüsü gösteriliyor. - \"Ses Parçası\" menüsü gizleniyor. - Ses parçası menüsünü gizle - Altyazı menüsünün altındaki bilgilendirme gizlenmiyor. - Altyazı menüsünün altındaki bilgilendirme gizleniyor. - Altyazılar menüsünün altındaki yazıyı gizle - \"Altyazılar\" butonu gösteriliyor. - \"Altyazılar\" butonu gizleniyor. - Altyazılar butonunu gizle - Yardım & geri bildirim menüsü gösteriliyor. - Yardım & geri bildirim menüsü gizleniyor. - Yardım & geri bildirim menüsünü gizle - \"YouTube Müzik ile dinle\" butonu gizlenmiyor. - \"YouTube Müzik ile dinle\" butonu gizleniyor. - YouTube Music ile dinle menüsünü gizle - \"Ekranı kilitle\" butonu gösteriliyor. - \"Ekranı kilitle\" butonu gizleniyor. - \"Ekranı kilitle\" butonunu gizle - \"Videoyu döngüye al\" butonu gösteriliyor. - \"Videoyu döngüye al\" butonu gizleniyor. - \"Videoyu döngüye al\" butonunu gizle - Daha fazla bilgi menüsü gösterilir. - Daha fazla bilgi menüsü gizli. - Daha fazla bilgi menüsünü gizle - Pencere içinde pencere menüsü gizlenmiyor. - Pencere içinde pencere menüsü gizli. - Pencere içinde pencere menüsünü gizle - \"Oynatma hızı\" butonu gösteriliyor. - \"Oynatma hızı\" butonu gizleniyor. - \"Oynatma hızı\" butonunu gizle - Premium kontrolleri menüsü gizlenmiyor. - Premium kontrolleri menüsü gizli. - Premium kontrolleri menüsünü gizle - Video kalitesi menüsünün altındaki yazı gizlenmiyor. - Video kalitesi menüsünün altındaki yazı gizleniyor. - Video kalitesi menüsünün altındaki yazıyı gizle - Video kalitesi menüsünün başlığındaki yazı gösteriliyor. - Video kalitesi menüsünün başlığındaki yazı gizlendi. - Video kalitesi menüsünün başlığındaki yazıyı gizle - \"Bildir\" butonu gösteriliyor. - \"Bildir\" butonu gizleniyor. - Rapor Menüsünü Sakla - Uyku zamanlayıcısı menüsü görünür. - Uyku zamanlayıcısı menüsü gizli. - Uyku zamanlayıcısı menüsünü gizle - \"Sabit ses\" butonu gösteriliyor. - \"Sabit ses\" butonu gizleniyor. - \"Sabit ses\" butonunu gizle - \"Meraklısı için istatikler\" butonu gösteriliyor. - \"Meraklısı için istatikler\" butonu gizleniyor. - \"Meraklısı için istatikler\" butonunu gizle - \"VR modunda izle\" butonu gösteriliyor. - \"VR modunda izle\" butonu gizleniyor. - \"VR modunda izle\" butonunu gizle - Tam ekran butonu gizlenmiyor. - Tam ekran butonu gizli. - Tam ekran butonunu gizle - Düğmeler görünür. - Düğmeler gizli. - Önceki & sonraki düğmelerini gizle - \? - YouTube Müzik butonu gösteriliyor. - YouTube Müzik butonu gizleniyor. - YouTube Music butonunu gizle - Kaydet düğmesi görünür. - Kaydet düğmesi gizli. - Kaydet butonunu gizle - Podcast bölümlerini keşfet bölümleri gizlenmiyor. - Podcast bölümleri gizleniyor. - \"Podcast\'i keşfedin\" kısmını gizle - Videonun altındaki Yorumlar yazısının altında gösterilen yorum gizlenmiyor. - Videonun altındaki Yorumlar yazısının altında gösterilen yorum gizleniyor. - Önizlenen yorumu gizle - Bu, yorum bölümünün boyutunu değiştirir, dolayısıyla yorum bölümünde canlı sohbet tekrarını açmak imkansızdır. - Bu, yorum bölümünün boyutunu değiştirmez, dolayısıyla yorum bölümünde canlı sohbet tekrarını açmak mümkündür. - Önizleme yorumu tipini gizle - Promosyon uyarı afişi gösteriliyor. - Promosyon uyarı afişi gizli. - Promosyon uyarı afişini gizle - Yorumlar butonu gösteriliyor. - Yorumlar butonu gizleniyor. - \"Yorumlar\" butonunu gizle - \"Beğenme\" butonu gösteriliyor. - \"Beğenme\" butonu gizleniyor. - Beğenmeme butonunu gizle - \"Beğen\" butonu gösteriliyor. - \"Beğen\" butonu gizleniyor. - Beğen butonunu gizle - \"Canlı sohbet\" butonu gösteriliyor. - \"Canlı sohbet\" butonu gizleniyor. - Canlı sohbet butonunu gizle - Daha fazla butonu gizlenmiyor. - Daha fazla butonu izleniyor. - Daha fazla butonunu gizle - \"Mix\" oynatma listesini aç butonu gösteriliyor. - \"Mix\" oynatma listesini aç butonu gizleniyor. - Mix oynatma listesini aç butonunu gizle - Oynatma listesini aç butonu gösteriliyor. - Tam ekranda oynatma listesini aç butonu gizleniyor. - Oynatma listesini aç butonunu gizle - Kaydet düğmesi görünür. - Kaydet düğmesi gizli. - Kaydet butonunu gizle - \"Paylaş\" butonu gösteriliyor. - \"Paylaş\" butonu gizleniyor. - Paylaş butonunu gizle - Tam ekranda zaman çubuğunun altındaki butonlar gizlenmiyor. - Tam ekranda zaman çubuğunun altındaki butonlar gizleniyor. - Zaman çubuğunun altındaki butonları gizle - "Aşağıdaki önerilen videoları gizler: - -• 'Yalnızca Üyeler İçin' etiketine sahip videolar -• Videonun altında 'Kullanıcılar da izledi' gibi ibarelerin yer aldığı videolar." - Önerilen videoları gizle - Hızlı işlemler kapsayıcısındaki \'Diğer videolar\' bölümü ve ilgili video katmanı gizlenmiyor. - Hızlı işlemler kapsayıcısındaki daha fazla video bölümü ve ilgili video katmanı gizlenir. - \"Önerilen video\" arayüzünü gizle - \"Remix\" butonu gösteriliyor. - \"Remix\" butonu gizleniyor. - Remix düğmesini gizle - \"Bildir\" butonu gösteriliyor. - \"Bildir\" butonu gizleniyor. - Rapor butonunu gizle - \"Ödüller\" butonu gösteriliyor. - \"Ödüller\" butonu gizleniyor. - Ödüller butonunu gizle - Arama geçmişindeki kapak fotoğrafları gösteriliyor. - Arama geçmişindeki kapak fotoğrafları gizleniyor. - Arama terimlerinde kapak fotoğraflarını gizle - \"Videoyu sarmak için çubuğu sağa veya sola hareket ettirin\" gibi yazılar gizlenmiyor. - \"Videoyu sarmak için çubuğu sağa veya sola hareket ettirin\" gibi yazılar gizleniyor. - Zaman çubuğunu kaydırırken çıkan yazıları gizle - \"İptal etmek için bırakın\" gibi yazılar gizlenmiyor. - \"İptal etmek için bırakın\" gibi yazılar gizleniyor. - \"İptal etmek için bırakın\" yazısını gizle - Gizlenmiyor - Gizleniyor - Gizlenmiyor - Gizleniyor - Kapak fotoğraflarındaki zaman çubuğunu gizle - Zaman çubuğunu gizle - Kanalın kendine sponsor kartları gösteriliyor. - Kanalın kendine sponsor kartları gizli. - Kanalın kendi sponsorlu kartlarını gizle - YouTube ayarlar menüsünde elementleri gizle. - YouTube ayarlar menüsünü gizle - \"Paylaş\" butonu gösteriliyor. - \"Paylaş\" butonu gizleniyor. - Paylaş butonunu gizle - Mağaza düğmesi görünür. - Mağaza düğmesi gizli. - Mağaza düğmesini gizle - Alışveriş linkleri gizlenmiyor. - Alışveriş linkleri gizleniyor. - Alışveriş bağlantılarını gizle - Kanal barı görünür - Kanal barı gizli. - Kanal barını gizle - Yorumlar butonu gösteriliyor. - Yorumlar butonu gizleniyor. - Yorumlar butonunu gizle - \"Beğenme\" butonu gösteriliyor. - \"Beğenme\" butonu gizleniyor. - Beğenmeme butonunu gizle - Butonları gizle - Video bağlantı etiketi gösteriliyor - Video bağlantı etiketi gizlendi. - Tüm video bağlantısı etiketini gizle - Bilgi panelleri gizlenmiyor. - Bilgi panelleri gizleniyor. - Bilgi panellerini gizle - \"Katıl\" butonu gösteriliyor. - \"Katıl\" butonu gizleniyor. - Katıl butonunu gizle - \"Beğen\" butonu gösteriliyor - \"Beğen\" butonu gizleniyor - Beğen butonunu gizle - Canlı sohbet başlığı gizlenmedi.\n\nBaşlıktaki geri düğmesi gizlenmeyecek. - Canlı sohbet başlığı gizlendi.\n\nBaşlıktaki geri düğmesi gizlenmeyecek. - Canlı sohbet başlığını gizle - Gezinme çubuğu gösteriliyor - Gezinme çubuğu gizli. - Gezinme çubuğunu gizle - Ücretli tanıtım etiketi gösteriliyor. - Ücretli tanıtım etiketi gizli. - Ücretli tanıtım etiketini gizle - Durdurulmuş başlık gizlenmiyor. - Durdurulmuş başlık gizli - Durdurulmuş başlığı gizle - Duraklama katmanı düğmeleri görünür. - Duraklama katmanı düğmeleri gizli. - Duraklama katmanı düğmelerini gizle - Buton arka planı gizlenmiyor. - Buton arka planı gizli. - Oynatma & buton arka planı duraklat - \"Remix\" butonu gösteriliyor. - \"Remix\" butonu gizleniyor. - Remix düğmesini gizle - \"Paylaş\" butonu gösteriliyor. - \"Paylaş\" butonu gizleniyor. - Paylaş butonunu gizle - İzleme geçmişinde gizlenmiyor. - İzleme geçmişinde gizli. - İzleme geçmişinde gizle - Akışta ve ilgili videolarda gösterilir. - Akışta ve ilgili videolarda gizlidir. - Akışta ve ilgili videolarda gizle - Arama sonuçlarındaki Shorts rafları gizlenmiyor - Arama sonuçlarındaki Shorts rafları gizli - Arama sonuçlarında Shorts\'u gizle - Abonelikler akışındaki Shorts rafı gizlenmiyor - Abonelikler akışındaki Shorts rafı gizli - Abonelikler akışında Shorts\'u gizle - "Shorts raflarını gizler. - -Bilinen sorun: Arama sonuçlarındaki resmi başlıklar da gizlenebiliyor." - Shorts raflarını gizle - Mağaza düğmesi görünür. - Mağaza düğmesi gizli. - Mağaza düğmesini gizle - Alışveriş butonu gizli. - Alışveriş butonunu gizle - Ses düğmesi görünür. - Ses düğmesi gizli. - Ses düğmesini gizle - Bilgi etiketi gösteriliyor - Bilgi etiketi gizlendi - Ses bilgisi etiketini gizle - Abone ol düğmesi görünür. - Abone ol düğmesi gizli - Abone ol düğmesini gizle - Süper Teşekkürler düğmesi gizlenmiyor. - Süper Teşekkürler düğmesi gizli. - Süper Teşekkürler butonunu gizle - Etiketli ürünler görünür. - Etiketli ürünler gizli. - Etiketli ürünleri gizle - Araç çubuğu gösteriliyor. - Araç çubuğu gizli. - Araç çubuğunu göster - Trend butonu görünür. - Trend butonu gizli. - Trends butonunu gizle - Başlık görünür - Başlık gizli - Video başlığını gizle - \"Daha fazla göster\" butonu gizlenmiyor. - \"Daha fazla göster\" butonu gizleniyor. - \'Daha fazla göster\' düğmesini gizle - Gizlenmiyor - Gizleniyor - Yapılan eylemi bildiren çubuğu gizle - Gizlenmiyor - Gizleniyor - Denemeyi başlat butonunu gizle - Abonelikler karosel rafı gizlenmiyor. - Abonelikler karosel rafı gizleniyor. - Abonelikler karosel rafını gizle - Önerilen eylemler gizlenmiyor. - Önerilen eylemler gizlendi. - Önerilen eylemleri gizle - "Bu ayar kullanımdan kaldırıldı. - -Bunun yerine 'Ayarlar → Otomatik oynat → Sonraki videoyu otomatik oynat' ayarını kullanın." - Önerilen video bitiş ekranı gizlenmiyor. - "Otomatik oynatma kapatıldığında önerilen video bitiş ekranı gizlenir. - -Otomatik oynatma YouTube ayarlarından değiştirilebilir: -'Ayarlar → Otomatik oynat → Sonraki videoyu otomatik oynat'" - Önerilen video bitiş ekranını devre dışı bırak - \"Teşekkürler\" butonu gösteriliyor. - \"Teşekkürler\" butonu gizleniyor. - Teşekkürler butonunu gizle - Gizlenmiyor - Gizleniyor - Biletler kısmını gizle - Süre sayacı gösteriliyor - Gizleniyor - Video süre sayacını gizle - Gizlenmiyor - Gizleniyor - Zamanlı tepkileri gizle - Yansıtma düğmesi görünür. - \"Yayınla\" butonu gizli durumda. - Yayınla butonunu gizle - Oluştur butonu gösteriliyor. - Oluştur butonu gizleniyor. - Oluştur butonunu gizle - Bildirimler butonu gizlenmiyor. - Bildirimler butonu gizleniyor. - Bildirimler butonunu gizle - \"Transkripti göster\" butonu gizlenmiyor - \"Transkripti göster\" butonu gizleniyor - \"Transkripti göster\" butonunu gizle - Video reklamları gösteriliyor - Video reklamları gizleniyor - Video reklamlarını gizle - Belirtilen görüntüleme sayısından daha az olan önerilen videoları gizleyin.\n\nBilinen sorun: 0 izleme alan videolar filtrelenmez. - Önerilen videoları izlenmeye göre gizle - Görüntüleme sayısı bu sayıdan fazla olan videolar gizlenecektir. - Görüntülemelerden büyük - Görüntüleme sayısı bu sayıdan az olan videolar gizlenecektir. - Görüntülemelerden az - K -> 1 000\nM -> 1 000 000\nB -> 1 000 000 000\ngörüntüleme -> Görüntüleme - Kullanıcı arayüzünde her videonun altında gösterilen görüntüleme sayısı için dil şablonunuzu belirtin. Her tuş (dilinizde bir harf/kelime) -> değer (anahtarın anlamı) yeni bir satırda olmalıdır. Tuşlar \"->\"den önce gelir imza. Uygulamayı veya sistem dilini değiştirirseniz bu ayarı sıfırlamanız gerekir.\n\nÖrnekler:\nTürkçe: 10K görüntüleme = K -> 1000, görüntülemeler -> görüntüleme\nİspanyolca: 10 K manzara = K -> 1000, manzaralar -> Görüntüleme - Anahtarları göster - Gizlenmiyor - Gizleniyor - Ürünleri görüntüle yazısını gizle - Sesli arama butonu gösteriliyor. - Sesli arama butonu gizlendi. - Sesli arama düğmesini gizle - Gizlenmiyor - Gizleniyor - Web arama sonuçlarını gizle - Yakınlaştırma arayüzü gizlenmiyor. - Yakınlaştırma arayüzü gizli. - Yakınlaştırma arayüzünü gizle - Afn Mavi - Afn Kırmızı - Özel - Stok - MMT - Revancify Mavi - Revancify Kırmızı - YouTube - Ekranı tam ekranda kapatıp açarken yatay modu korur. - Manzara modunun zorlandığı milisaniye miktarı. - Manzara modunu tutma zaman aşımı - Manzara modunu tut - Stok - Çift tıklama eylemi devre dışı. - "Çift dokunma eylemi etkin. - -• Küçültülmüş videoyu daha büyük bir boyuta değiştirmek için iki kez dokunun. -• Orijinal boyuta geçmek için bir kez daha iki kez dokunun." - Çift tıklama eylemini etkinleştir - Sürükle ve Burak devre dışı. - Sürükle ve Bırak etkin. - Sürükle ve Bırak\'ı etkinleştir - Genişletme ve kapatma butonları gösteriliyor. - Butonlar gizlendi. \n(mini oynatıcıyı kaydırarak genişletin veya kapatın) - Genişlet ve kapat butonlarını gizle - Atla ileri ve geri butonlarını gizlenmiyor. - Atla ileri ve geri butonlarını gizli. - Atla ileri ve geri butonlarını gizle - Alt metinler gizlenmiyor. - Alt metinler gizli. - Alt metinleri gizle - Mini oynatıcının siyah arkaplan opaklığı 0 ila 100 arasında olmalıdır. Varsayılan değere sıfırlandı. - 0-100 arası opaklık değeri, 0 şeffaftır. - Kaplama opaklığı - Orjinal - Telefon - Tablet - Modern 1 - Modern 2 - Modern 3 - Miniplayer tipi - Bindirme düğmesi - "Her zaman tekrarlama ayarı için dokunun. -Tekrarlama durumlarından sonra duraklamayı etkinleştirmek için basılı tutun." - Videoyu döngüye alma butonunu ekle - "Video URL'sini kopyalamak için dokunun. -Video URL'sini zaman damgasıyla kopyalamak için basılı tutun." - "Video URL'sini zaman damgasıyla kopyalamak için dokunun. -Videoyu zaman damgasıyla kopyalamak için basılı tutun." - Mevcut süreli linki kopyala butonunu ekle - Linki kopyala butonunu ekle - Harici indirme uygulamasını açmak için tıklayın - Harici indirici butonunu ekle - Geçerli videonun sesini kapatmak için dokunun. Sesi açmak için tekrar dokunun. - Sesi sustur butonunu göster - Buton durumunu değiştirmek için buraya dokunup basılı tutun. - Oynatma hızı sıfırlandı: %sx. - "Hız iletişim kutusunu açmak için dokunun. -Oynatma hızını 1,0x'e sıfırlamak için dokunun ve basılı tutun. Varsayılan hıza sıfırlamak için tekrar dokunun ve basılı tutun." - Oynatma hızını ayarlama butonunu ekle - "Kanaldaki en eskiden en yeniye tüm videolardan oluşan bir oynatma listesi oluşturmak için dokunun. -Geri almak için dokunun ve basılı tutun." - Zaman Sıralı Çalma Listesi Düğmesini göster - \"Beyaz liste iletişim kutusunu açmak için dokunun. -Beyaz liste ayarı iletişim kutusunu açmak için dokunun ve basılı tutun. - Beyaz liste düğmesini göster - Oynatma listesi indirme butonunu geçersiz kıl - İndirme düğmesi harici indiricinizi açar. - Video indirme butonunu geçersiz kıl - Hariç tutulan - Dahil - Normal - Eylem düğmeleri - Ek ayarlar - Animasyon / geri bildirim - Deneysel Parametreler - Resim bölgesi kısıtlamaları - İçe / Dışa dosya olarak aktar - İçe / Dışa yazı olarak aktar - Anahtar kelime filtresi - Diğerleri - Arayüz düğmeleri - Yama Bilgileri - Hızlı eylemler - Önerilen video - Shorts rafları - Araç kullanıldı - İzlenme sayısı filtresi - Hesap menüsünde ve siz sekmesinde gizli veya göster. - Hesap Menüsü - Videonun altındaki aksiyon düğmeleri gizle veya göster. - Eylem düğmeleri - Reklamlar - Alternatif kapak fotoğrafları - Ambiyans modu kısıtlamalarını atlayın veya ortam modunu devre dışı bırakın. - Ortam modu - Akışta, arama sonuçlarında ve ilgili videolardaki kategori barını gizle veya göster. - Kategori çubuğu - Videoların altında kanal çubuğu bileşenlerini gizleyin veya gösterin. - Kanal çubuğu - Kanal profilindeki bileşenleri gizleyin veya gösterin. - Kanal profili - Yorumlar kısmındaki öğeleri gizle veya göster. - Yorumlar - Akıştaki ve kanaldaki topluluk gönderilerini gizleyin veya gösterin. - Topluluk gönderileri - Özel filtre ile bileşenleri gizle. - Özel filtre - Akıştaki açılır menüyü gizleyin veya gösterin. - Açılır menü - Akış - Tam ekranla ilgili bileşenleri gizleyin veya değiştirin. - Tam Ekran - Genel - Zaman çubuğunu kaydırırkenki titreşimi kapat. - Titreşimli geri bildirim - Ayarları içe veya dışa aktarın. - Ayarları içe / dışa aktar - Uygulama içi simge durumuna küçültülmüş oynatıcının stilini değiştirin. - Miniplayer - Diğer Ayarlar - Uygulanmış Yamalar Hakkında Bilgi. - Yama Bilgileri - Videolardaki düğmeleri gizle veya göster. - Oynatıcı butonları - Video oynatıcıdaki açılır menüyü gizleyin veya değiştirin. - Açılır menü - Oynatıcı - Return YouTube Dislike - SponsorBlock - Zaman çubuğu bileşenlerini özelleştir. - Zaman çubuğu - YouTube ayarlar menüsünde elementleri gizle. - Ayarlar menüsü - Shorts oynatıcıdaki bileşenleri gizleyin veya gösterin. - Shorts oynatıcı - Shorts - Kaydırma kontrolleri - Araç çubuğu düğmeleri, arama çubuğu, başlık gibi araç çubuğunda bulunan bileşenleri gizleyin veya değiştirin. - Araç Çubuğu - Video açıklaması bileşenlerini gizle veya göster. - Video açıklaması - Videoları anahtar kelimelere veya görüntülemelere göre gizleyin. - Video filtresi - Video - İzlemek geçmişi ile alakalı ayarları değiştirir. - İzleme geçmişi - Hızlı eylemler üst kenar boşluğu 0-32 arasında olmalıdır. Varsayılan değerlere sıfırlayın. - Zaman çubuğundan hızlı işlem kapsayıcısına kadar olan aralığı 0-32 arasında yapılandırın. - Hızlı ayarlar çubuğunun yerden yüksekliği - "AV1 codec yazılımı yanıtı zorla reddeder. -Yaklaşık 20 saniyelik ara belleğe alma işleminin ardından farklı codec bileşenine geçiş yapılır." - AV1 codec yazılımı yanıtını reddet - Geri çekilme işlemi yaklaşık 20 saniyelik ara belleğe alma işlemine neden olur. - Oynatma hızı değişimleri sadece oynatılan videoya uygulanıyor - Oynatma hızı değişimleri bütün videolara uygulanıyor. - Oynatma hızı değişimlerini hatırla - Varsayılan hız %s olarak değiştiriliyor. - Kalite değişimleri sadece oynatılan videoya uygulanıyor - Kalite değişimleri bütün videolara uygulanıyor - Video kalitesi değişimlerini hatırla - Mobil ağda varsayılan video kalitesi %s olarak değiştiriliyor. - Video kalitesi ayarlanamadı. - Wi-Fi\'da varsayılan video kalitesi %s olarak değiştiriliyor. - "Görüntüleyicinin takdirine ilişkin iletişim kutusunu kaldırır. Bu yaş sınırlamasını atlamaz. Sadece otomatik olarak kabul ediyor." - İzleyicinin takdirine bağlı iletişim kutusunu kaldır - AV1 codec yazılımı bileşenini VP9 kodeği ile değiştirin. - AV1 codec yazılımı bileşenini değiştirin - Kanal etiketi kullanılıyor. - Kanal ismi kullanılıyor. - Kanal etiketini değiştirin - Kalan süreyi görüntülemek için dokunun - Oynatma hızı veya video kalitesi açılır menüsünü açmak için dokunun. - Zaman damgası eylemini değiştir - Oluştur butonunu ayarlar butonu ile değiştir. - Oluştur butonunu değiştir - "Tıklayarak YouTube ayarlarını aç. -Tıklayıp basılı tutarak RVX ayarlarını aç." - "Tıklayarak RVX ayarlarını aç. -Tıklayıp basılı tutarak YouTube ayarlarını aç." - Düğmeye atanacak eylem türü - Zaman çubuğu küçük resimleri tam ekran olarak gösterilecek. - Zaman çubuğu küçük resimleri zaman çubuğunun üzerinde gözükecek. - Eski zaman çubuğu küçük resimlerini kullan - Eski video kalite menüsü gösterilmiyor. - Eski video kalite menüsü gösteriliyor. - Eski video kalite menüsünü geri getir - Hakkında - Veriler True RYD Worker API tarafından sağlanır. Daha fazlasını öğrenmek için buraya dokunun. - ReturnYouTubeDislike.com - Beğen düğmesi en iyi görünüm için tasarlandı. - Beğen düğmesi minimum genişlik için tasarlandı. - Kompakt beğenme düğmesi - Beğenmemeler sayı olarak gösterilir. - Beğenmemeler yüzde olarak gösterilir. - Yüzde olarak beğenmemeler - Beğenmemeler gösterilmiyor. - Beğenmemeler gösteriliyor - Return YouTube Dislike\'ı etkinleştir - Beğenmeme sayısı mevcut değil (istemci API sınırına ulaşıldı). - Beğenmemeler mevcut değil (durum %d). - Beğenmemeler geçici olarak kullanılamıyor (API zaman aşımına uğradı). - Beğenmemeler mevcut değil (%s). - ReturnYouTubeDislike\'ı kullanarak oy vermek için videoyu yeniden yükleyin - Shorts videolarda beğenmemeler gösterilmiyor - Shorts videolarda beğenmemeler gösteriliyor. %s - "Shorts'da beğenmeme sayıları gösteriliyor. - -Kısıtlama: Gizli modda beğenmeme sayıları görünmeyebilir." - Shorts videolarda beğenmemeleri göster - ReturnYouTubeDislike API\'si mevcut değilse uyarı gösterilmiyor. - ReturnYouTubeDislike API\'si mevcut değilse uyarı gösteriliyor. - API mevcut değilse bir uyarı göster - Bağlantıları paylaşırken, tracking query parametrelerini URL\'lerden kaldırır. - Paylaşılan bağlantıları sterilize edin - Hakkında - sponsor.ajay.app - Veriler, SponsorBlock API tarafından sağlanmaktadır. Daha fazla bilgi edinmek ve diğer platformlar için indirmeleri görmek için buraya dokunun. - API URL\'si değiştirildi. - API bağlantısı geçersiz. - API URL\'si sıfırlandı. - Görünüm - Renk değiştirildi. - Renk: - Renk kodu geçersiz. - Renk sıfırlandı. - Yeni segmentler oluşturma - Segment davranışını değiştir - Atla düğmesini otomatik olarak gizle - Atlama düğmesi tüm segmentler için görüntülenir. - Atla düğmesi birkaç saniye sonra gizlenir. - Kompakt atlama düğmesini kullanın - En iyi görünüm için tasarlanmış atla düğmesi. - Minimum genişlik için tasarlanmış atlama düğmesi. - Yeni segment oluştur düğmesini göster - Yeni bölüm oluştur butonu gizleniyor. - Yeni bölüm oluştur butonu gizlenmiyor. - SponsorBlock\'u etkinleştir - SponsorBlock, YouTube videolarındaki sıkıcı kısımları atlamaya yarayan kitle kaynaklı bir sistemdir. - Oylama Düğmesini Göster - Gizleniyor. - Gizlenmiyor. - Genel - Yeni kısım adımını ayarla - Değer pozitif bir sayı olmalıdır. - Yeni segmentler oluşturulurken adım düğmelerinin hareket ettiği milisaniye sayısı. - API URL\'sini Değiştir - SponsorBlock\'un sunucuya çağrı yapmak için kullandığı adres. - En az bölüm süresi - Bu değerden (saniye olarak) daha kısa bölümler gösterilmeyecek veya atlanmayacaktır - Atlama sayısı izlemeyi etkinleştir - Atlama sayısı izleme etkin değil - SponsorBlock liderlik tablosunun ne kadar zaman kazanıldığını bilmesini sağlar. Bir segment her atlandığında skor tablosuna bir mesaj gönderilir. - Bir kısım otomatikman atlanırken uyarı ver - Uyarı gizleniyor. Bir örnek görmek için buraya dokunun. - Uyarı, bir bölüm otomatik olarak atlandığında gösterilir. -Bir örnek görmek için buraya dokunun. - Video uzunluğunu bölümler olmadan göster - Tam video uzunluğu gizlenmiyor. - Video uzunluğu eksi tüm segmentler, tam video uzunluğunun yanında parantez içinde gösterilir. - Benzersiz kullanıcı kimliğiniz - Özel kullanıcı kimliğiniz en az 30 karakter olmalıdır. - Bu gizli tutulmalıdır. Bu bir şifre gibidir ve kimseyle paylaşılmamalıdır. Birisi buna sahipse, sizi taklit edebilir. - Zaten okundu - Herhangi bir kısmı göndermeden önce SponsorBlock kurallarını okumalısınız. - Göster bana - Yönergeleri takip et - Yönergeler yeni segmentler oluşturmak için kurallar ve ipuçları içerir. - Yönergeleri görüntüle - Kategori seçin - Bu kısmın başlangıcı %1$02d.%2$02d, bitişi %3$02d.%4$02d, süresi %5$d dakika %6$02d saniye.\nGönderilmeye hazır mı? - Segment\n\n%1$s\niçin\n%2$s\n\n(%3$s)\n\nGöndermeye hazır mısınız? - Zaman aralığı doğru mu? - Ayarlarda kategori devre dışı bırakıldı. Göndermek için kategoriyi etkinleştir. - Bu segmentin başlangıcını mı, bitişini mi düzenlemek istiyorsunuz? - Girilen süre geçersiz. - Segmentin süresini elle düzenleyin - %s bu kısmın başlangıcı mı, bitişi mi olarak ayarlansın? - bitiş - Öncelikle zaman çizgisinde iki konum işaretleyin. - başlangıç - şimdi - Segmenti önizleyin ve sorunsuz bir şekilde atladığından emin olun. - Başlangıç, bitişten önce olmalıdır - Kısmın bitişi - Kısmın başlangıcı - Yeni SponsorBlock segmenti - Sıfırla - Rengi sıfırla - Konuyla Alakasız / Şaka - Sadece videoyu doldurmak ya da mizah için eklenmiş, videonun ana içeriğini anlamak için gerekli olmayan alakasız sahneler. Bu, içerik veya arka plan detayları hakkında bilgi veren kısımları içermemelidir - Vurgula - Videonun çoğu kişinin aradığı bölümü. - Etkileşim Anımsatıcısı (Abonelik) - Videonun ortasında beğenmek, abone olmak veya takip etmek için kısa bir hatırlatma olan kısımdır. Eğer süresi uzunsa veya belirli bir şey hakkındaysa, kendi reklamını yapan kategorisi seçilmelidir. - Aralık/Giriş Animasyonu - Gerçek içeriği olmayan bir aralık. Bir duraklama, statik çerçeve veya yinelenen animasyon olabilir. Bilgi içeren geçişleri içermez. - Müzik: Müzik Olmayan Bölüm - Yalnızca müzik videolarında kullanım içindir. Henüz başka bir kategoride yer almayan müzik videolarının müziksiz bölümleri - Kapanış Sahneleri/Jenerik - Videoda emeği geçenlerin veya video sonunda çıkan kartların gösterildiği kısımlar. Bilgilendirici sona sahip videolar için değil - Ön İzleme/Özet - Videoda veya bir dizinin diğer videolarında neler olduğunu veya neler olduğunu gösteren, tüm bilgilerin başka bir yerde tekrarlandığı klip koleksiyonu. - Karşılıksız / Kendi Reklamı - Ücretsiz veya kendi kendine tanıtım haricinde Sponsora benzer. Ürünler, bağışlar veya kiminle işbirliği yaptıklarına ilişkin bilgilerle ilgili bölümler içerir. - Sponsor - Ücretli tanıtım, ücretli yönlendirmeler ve doğrudan reklamlar. Kendini pazarlayan veya beğendiği içerik üreticilerine / sitelere / ürünlere atıfta bulunanlar için değil. - Kopyala - %s dışa aktarılamadı. - Ayarları içe / dışa aktar - ReVanced Extended ve diğer SponsorBlock platformlarına içe / dışa aktarılabilen SponsorBlock JSON yapılandırmanız. - ReVanced Extended ve diğer SponsorBlock platformlarına aktarılabilen SponsorBlock JSON yapılandırmanız. Bu, özel kullanıcı kimliğinizi içerir. Bunu dikkatli paylaştığınızdan emin olun. - %s içe aktarılamadı. - Ayarlar başarıyla içe aktarıldı. - Ayarlarınız özel bir SponsorBlock kullanıcı kimliği içeriyor.\n\nKullanıcı kimliğiniz bir şifre gibidir ve asla paylaşılmamalıdır.\n - Tekrar gösterme - Ayarlar panoya kopyalandı. - Otomatik atla - Bir kez otomatik atla - Atla - Vurgula - Dolguyu atla - Vurguya atla - Etkileşimi atla - Tanıtımı geç - Aralığı atla - Aralığı atla - Müzik olmayan kısımları geç - Kapanış ekranını atla - Önizlemeyi atla - Özeti atla - Önizlemeyi atla - Promosyonu atla - Sponsoru atla - Kısımları atla - Devre dışı bırak - Zaman çubuğunda Göster - Atlamak için buton göster - Alakasız konu atlandı. - Vurguya atlandı. - Etkileşim anımsatıcısı atlandı - Giriş ekranı atlandı. - Ara atlandı. - Ara atlandı. - Birden çok segment atlandı. - Müzik olmayan kısım atlandı. - Kapanış ekranı atlandı. - Önizleme atlandı. - Seans atlandı. - Önizleme atlandı. - Kendi reklamı atlandı - Sponsor atlandı. - Gönderilmemiş kısım atlandı. - SponsorBlock geçici olarak kullanılamıyor. - SponsorBlock geçici olarak kullanılamıyor (durum %d). - SponsorBlock geçici olarak kullanılamıyor (API zaman aşımına uğradı). - İstatistikler - İstatikler geçici olarak kullanılamıyor (API zaman aşımına uğradı) - Yükleniyor... - İtibarınız: <b>%.2f</b> - İnsanları <b>%s</b> segmentten kurtardın - %1$s saat %2$s dakika - %1$s dakika %2$s saniye - %s saniye - Bu, hayatlarının <b>%s</b> kadarı.<br>Skor tablosunu görmek için buraya dokunun. - Küresel istatistikleri ve en çok katkıda bulunanları görmek için buraya dokunun. - SponsorBlock liderlik tablosu - SponsorBlock devre dışı. - <b>%s</b> segmenti atladın - Atlanan segment sayacı sıfırlansın mı? - Bu da <b>%s</b> demek. - <b>%s</b> segment oluşturdun - Kullanıcı adın: <b>%s</b> - Kullanıcı adını değiştirmek için buraya dokun - Kullanıcı adı değiştirilemiyor: Durum: %1$d %2$s. - Kullanıcı adı başarıyla değiştirildi. - Kısım gönderilemedi.\nAynısı mevcut. - Kısım gönderilemiyor: %s. - Kısım gönderilemedi: %s. - Kısım gönderilemedi.\nKısıtlanmış (bir kullanıcıdan veya IPden çok fazla istek). - SponsorBlock geçici olarak kullanılamıyor. - Bölüm gönderilemedi (durum: %1$d %2$s). - Düzenlenen kısım gönderimi tamamlandı. - SponsorBlock\'un mevcut olmaması durumunda uyarı gösterilmez. - SponsorBlock\'un mevcut olmaması durumunda uyarı gösterilir. - API mevcut değilse bir uyarı göster - Kategoriyi değiştir - Olumsuz oy ver - Kısım oylanamadı: %s. - Segmente oy verilemiyor (API zaman aşımına uğradı). - Kısım oylanamadı (durum: %1$d %2$s). - Oylanabilecek bir kısım yok. - Olumlu Oy Ver - Ayarlar panoya kopyalandı - Süre Sayacı panoya kopyalandı. (%s) - URL panoya kopyalandı - Videonun şuanki saniyeli URL\'si panoya kopyalandı - Orjinal - Beğen - Beğen (Kahire) - Kalp - Kalp (Renk Tonlu) - Gizlendi - Çift tıklama animasyonu - Meta panel alt kenar boşluğu 0-64 arasında olmalıdır. Varsayılan değerlere sıfırlayın. - Arama çubuğundan meta panele kadar olan aralığı 0-64 arasında yapılandırın. - Meta panel alt kenar boşluğu - Kısa videoların tekrarlanma durumunu değiştirmek için zaman damgasını basılı tutun. - Zaman damgası uzun basma aksiyonu - "Video başlığı bölümünü tam ekranda gösterir. - -Sınırlama: Tıklandığında video başlığı kayboluyor." - Video başlığı bölümünü göster - Otomatik oynatma açıksa sonraki video geri sayım bitmeden oynatılır. - Otomatik oynatma açıksa sonraki video geri sayım olmadan oynatılır. - Otomatik oynatma geri sayımını atla - "Varsayılan video kalitesi uygulama gecikmesini atlamak için video başlangıcında önceden yüklenmiş arabelleği atlayın. - - • Video başladığında yaklaşık 0.7 saniyelik bir gecikme olur, ancak varsayılan video kalitesi hemen uygulanır. -• HDR videolar veya 10 saniyeden kısa videolar için geçerli değildir." - Önceden yüklenmiş arabelleği atla - Gizleniyor - Gizlenmiyor - Bir kısım atlanırken uyarı ver - Bu ayarı etkinleştirmek video oynatma sorunlarına yol açabilir. - Önceden yüklenmiş arabellek atlandı. - Hız arayüzü değeri 0 ila 8.0 arasında olmalıdır. Varsayılan değere sıfırlandı. - 0-8.0 arası hız kaydırma değeri. - Hız kaydırma değeri - "İstemciyi sürümünün eski sürümle sahteleştir - -• Bu, uygulamanın görünümünü değiştirir ancak bilinmeyen yan etkiler ortaya çıkabilir. -• Daha sonra kapatılırsa, uygulama verileri temizlenene kadar eski kullanıcı arayüzü kalabilir." - Sürüm taklit edilmiyor - Sürüm taklit ediliyor - 17.33.42 - Eski kullanıcı arayüzü düzenini geri yükle - 17.41.37 - Eski oynatma listesi raf düzenini geri yükle - 18.05.40 - Eski yorum girme düzenini geri yükle - 18.17.43 - Eski tarz oynatıcı açılır panelini geri yükleyin - 18.33.40 - Eski shorts aksiyon çubuğunu geri yükle - 18.38.45 - Eski varsayılan video kalitesi davranışını geri getir - 18.48.39 - Görüntülemelerin ve beğenilerin gerçek zamanlı olarak güncellenmesini devre dışı bırakır - Taklit edilecek sürüm - Sahte uygulama sürümü hedefini seçin. - Uygulama hedef sürümünü kandırma hedefi - Uygulama Versiyonunu taklit et - "Uygulama sürümü, YouTube'un eski bir sürümüyle taklit edilecek. - -Bu, uygulamanın görünümünü ve özelliklerini değiştirecektir ancak bilinmeyen yan etkiler ortaya çıkabilir. - -Daha sonra kapatılırsa kullanıcı arayüzü hatalarını önlemek için uygulama verilerinin temizlenmesi önerilir." - "Cihaz boyutlarını maksimum değere kadar taklit eder. Yüksek cihaz boyutları gerektiren bazı videolarda yüksek kalitenin kilidi açılabilir ancak tüm videolarda bu durum geçerli olmayabilir." - Farklı cihaz boyutlarını taklit et - Kaydırma hareketleri \'Kilit ekranı\' modunda devre dışıdır. - Kaydırma hareketleri \'Kilit ekranı\' modunda etkindir. - \'Kilit ekranı\' modundaki kaydırma hareketleri - Otomatik - Kaydırma işleminin gerçekleşmesi için eşik miktarı - Kaydırma büyüklük eşiği - Kaydırma arka planının şeffaflık değeri - Kaydırma arka plan şeffaflığı - Kaydırılabilir alan boyutu 50\'den fazla olamaz. Varsayılan değere sıfırlayın. - Kaydırılabilir ekran alanının yüzdesi.\n\nNot: Bu aynı zamanda aramak için iki kez dokunma hareketi için ekran alanının boyutunu da değiştirecektir. - Kaydırma arayüzü boyutu - Kaydırma animasyonu için metin boyutu - Kaydırma metin boyutu - Kaplamanın görünür olduğu milisaniye miktarı - Kaydırma zaman aşımı - "Cihazın bilgilerini taklit ederek oluştur düğmesinin ve bildirim düğmesinin konumlarını değiştirin. - -• Bu ayarı değiştirseniz bile, cihaz yeniden başlatılıncaya kadar geçerli olmayabilir. -• Bu ayarın devre dışı bırakılması, sunucu tarafından daha fazla reklamın yüklenmesine neden olur. -• Video reklamların görünür olması için bu ayarı devre dışı bırakmalısınız." - Oluştur düğmesini Bildirimler düğmesi ile yer değiş - Stok - • İzleme geçmişi çalışmaz. - İzleme geçmişi hakkında - YouTube izleme geçmişi yönetimini açmak için tıklayın. - Tüm geçmişi yönet - Orjinal - Alan adını değiştir - İzleme geçmişini durdur - İzleme geçmişi tipi - Kanal \'%1$s\', \'%2$s\' beyaz listesine eklenemedi. - \'%1$s\' kanalı \'%2$s\' beyaz listesine eklendi. - Beyaz listeye alınmış kanal yok. - Beyaz listeye eklenmedi. - Kanal bilgileri yüklenemedi. - Beyaz listeye eklendi. - Oynatma hızı - Kanalı %1$s\', \'%2$s\' beyaz listesinden kaldır? - Kanal \'%1$s\', \'%2$s\' beyaz listesinden kaldırılamadı. - \'%1$s\' kanalı \'%2$s\' beyaz listesinden kaldırıldı. - Beyaz listeye eklenen kanalların listesini kontrol edin veya kaldırın. - Kanal beyaz listesi - SponsorBlock - diff --git a/src/main/resources/youtube/translations/uk-rUA/missing_strings.xml b/src/main/resources/youtube/translations/uk-rUA/missing_strings.xml deleted file mode 100644 index ada29f025..000000000 --- a/src/main/resources/youtube/translations/uk-rUA/missing_strings.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - AI-generated video summary section is shown. - AI-generated video summary section is hidden. - Hide AI-generated video summary section - diff --git a/src/main/resources/youtube/translations/uk-rUA/strings.xml b/src/main/resources/youtube/translations/uk-rUA/strings.xml deleted file mode 100644 index 714cf9963..000000000 --- a/src/main/resources/youtube/translations/uk-rUA/strings.xml +++ /dev/null @@ -1,1723 +0,0 @@ - - - Увімкнути спеціальні можливості для відеоплеєра? - Керування змінено, оскільки служба спеціальних можливостей увімкнена. - Продовжити - Не показувати знову - "GmsCore не дозволено працювати у фоні.\n\nДотримуйтесь посібника \"Don't kill my app\" для вашого телефону і застосуйте інструкції для встановлення GmsCore.\n\nЦе необхідно для того, щоб програма працювала." - "Оптимізацію акумулятора GmsCore слід вимкнути, щоб запобігти виникненню проблем.\n\nНатисніть кнопку продовжити й вимкніть оптимізацію акумулятора." - Відкрити сайт - Потрібна дія - Увімкніть налаштування спливаючих повідомлень, щоб отримувати сповіщення. - Відкрити GmsCore - GmsCore не встановлено. Встановіть. - "DeArrow надає краудсорсингові мініатюри для відео з YouTube. Ці мініатюри часто більш релевантні, ніж ті, що надаються YouTube. Якщо увімкнено, URL-адреси відео надсилатиметься на сервер API, а інші дані не надсилатиметься. Якщо для відео немає мініатюр DeArrow, зображається оригінал або кадр. - -Натисніть тут, щоб дізнатися більше про DeArrow." - DeArrow - Некоректна URL API DeArrow. - URL кінцевої точки кешу мініатюр DeArrow - Кінцева точка API DeArrow - Тост не показується, якщо DeArrow не доступний. - Тост показується, якщо DeArrow не доступний. - Показувати тост, якщо API не доступний - DeArrow тимчасово недоступний. (код статусу: %s) - DeArrow тимчасово недоступний. - Вкладка Головна - Вкладка \'Ви\' - Оригінальні мініатюри - DeArrow та Оригінальні мініатюри - DeArrow та кадри - Кадри - Списки відтворення, рекомендації - Результати пошуку - Кадри з відео - Кадри береться з початку/середини/кінця кожного відео. Ці зображення вбудовані в YouTube і зовнішній API не використовується. - Кадри з відео - Використовується кадри високої якості. - Використовується кадри середньої якості. Мініатюри завантажуватиметься швидше, але прямі трансляції, неопубліковані та дуже старі відео можуть зображатися порожніми мініатюрами. - Використовувати неточні кадри - Початок відео - Середина відео - Кінець відео - Час відео з якого береться кадр - Вкладка Підписки - Додавання інформації біля мітки часу вимкнено. - "Додавання інформації біля мітки часу увімкнено." - Додавати інформацію біля мітки часу - Додається швидкість відтворення. - Додається якість відео. - Тип додаваної інформації - Кінематографічне освітлення вимкнено в енергозберігаючому режимі. - Кінематографічне освітлення увімкнено в енергозберігаючому режимі. - Обхід обмежень кінематографічного освітлення - Домен для отримання зображень.\nЗауваження: Вводьте лише ім\'я домену, тобто без префікса \"https\:\/\/\". - Альтернативний домен - Використовується оригінальний хост зображень\n\nУвімкнення може виправити відсутність зображень, які заблоковані в деяких регіонах. - Використовується хост зображень yt4.ggpht.com. - Обхід регіональних обмежень зображень - Оригінал - Телефонний - Телефонний (Макс 480 dp) - Планшетний - Планшетний (Мін 600 dp) - Змінити макет - Використовується змінні перемикачі. - Використовується текстові перемикачі. - Тип перемикача змін - Використовується діалог Поділитися додатка. - Використовується системний діалог поширення. - Змінити діалог Поділитися - Автовідворення - Стандартно - Призупинити - Повторювати - Змінити стан повторення Shorts - Перегляд каналів - Навчання - Стандартна - Що нового - Ігри - Історія - Бібліотека - Відео, які сподобалися - Наживо - Фільми - Музика - Пошук - YouTube Shorts - Спорт - Підписки - Тенденції - Переглянути пізніше - Змінити початкову сторінку - Початкова сторінка змінюється лише один раз. - "Початкова сторінка змінюється завжди. - -Застереження: Кнопка Назад на панелі інструментів не працює." - Тип зміни початкової сторінки - Звичний заголовок увімкнено. - Заголовок Premium увімкнено. - Змінити заголовок YouTube - Список рядків конструктора шляхів компонентів для фільтрування, розділених новим рядком - Користувацький фільтр - Користувацький фільтр вимкнено. - Користувацький фільтр увімкнено. - Увімкнути користувацький фільтр - Недопустимий користувацький фільтр: %s - Використовується висувне меню старого стилю. - Використовується специфічний діалог. - Тип меню користувацької швидкості відтворення - Користувацькі швидкості повинні бути менше ніж %sx. - Неправильні користувацькі швидкості відтворення. - Додати або змінити доступні швидкості відтворення - Редагувати користувацькі швидкості відтворення - Непрозорість затемнення плеєра має бути в межах 0-100. - Значення непрозорості в межах 0-100, де 0 це прозорий - Користувацька непрозорість затемнення плеєра - Введіть hex код кольору смуги прогресу. - Користувацьке значення кольору смуги прогресу - Щоб відкривати додаток у зовнішньому перегляді, увімкніть \"Відкривати підтримувані посилання\" та підтримувані вебадреси - Відкрити налаштування - Типова швидкість відтворення - Типова якість відео в мобільній мережі - Типова якість відео в Wi-Fi мережі - Вимикається кінематографічне освітлення в повноекранному режимі - Кінематографічне освітлення увімкнено у повноекранному режимі. - Кінематографічне освітлення вимкнено у повноекранному режимі. - Вимкнути кінематографічне освітлення в повноекранному режимі - Вимикається кінематографічне освітлення. - Кінематографічне освітлення увімкнено. - Кінематографічне освітлення вимкнено. - Вимкнути кінематографічне освітлення - Примусові автоматичні звукові доріжки увімкнено. - Примусові автоматичні звукові доріжки вимкнено. - Вимкнути примусові автоматичні звукові доріжки - Примусові авто субтитри увімкнено. - Примусові авто субтитри вимкнено. - Вимкнути примусові авто субтитри - Автовисувні панелі плеєра увімкнено. - Автовисувні панелі плеєра вимкнено. - Вимкнути висувні панелі плеєра - "Автоперемикання списків створення Мікс увімкнено коли автовідтворення увімкнене. - -Автовідтворення можна змінити у налаштуваннях YouTube: -Налаштування → Автоматичне відтворення → Автовідтворення наступного відео" - Автоперемикання списків відтворення Мікс вимкнено. - Вимкнути перемикання списків відтворення Мікс - Вмикання цієї функції вимкне автоматичне перемикання на YouTube Mix коли музика відтворюється якщо автовідтворення увімкнене. - Типову швидкість відтворення увімкнено в прямих трансляціях. - Типову швидкість відтворення вимкнено в прямих трансляціях. - Вимкнути швидкість відтворення в прямих трансляціях - Типову швидкість відтворення увімкнено для музики. - "Типову швидкість відтворення вимкнено для музики. - -Застереження: Це налаштування може не застосовуватись до відео, які не містять напис 'Слухати через YouTube Music'." - Вимкнути швидкість відтворення для музики - Панель залучення увімкнено. - Панель залучення вимкнено. - Вимкнути панель залучення - Вібрацію увімкнено. - Вібрацію вимкнено. - Вимкнути вібрацію при зміні розділу - Вібрацію увімкнено. - Вібрацію вимкнено. - Вимкнути вібрацію при перемотуванні жестом вгору по рядку прогресу - Вібрацію увімкнено. - Вібрацію вимкнено. - Вимкнути вібрацію при перемотуванні - Вібрацію увімкнено. - Вібрацію вимкнено. - Вимкнути вібрацію при скасуванні перемотки - Вібрацію увімкнено. - Вібрацію вимкнено. - Вимкнути вібрацію при зумі - Авто яскравість HDR увімкнено. - Авто яскравість HDR вимкнено. - Вимкнути авто яскравість HDR - HDR відео увімкнено. - HDR відео вимкнено. - Вимкнути HDR відео - Орієнтація відео згідно налаштувань пристрою в повноекранному режимі. - Орієнтація відео портретна в повноекранному режимі. - Вимкнути ландшафтний режим - Кнопки Подобається та Не подобається відблискуватимуть при згадуванні. - Кнопки Подобається та Не подобається не відблискуватимуть при згадуванні. - Вимкнути відблиск кнопки Подобається та Не подобається - "Вимкнути протокол CronetEngine's QUIC" - Вимкнути протокол QUIC - Плеєр Shorts відновлюватиметься при запуску додатка. - Плеєр Shorts не відновлюватиметься при запуску додатка. - Вимкнути відновлення плеєра Shorts - Лічильники анімовані. - Лічильники не анімовані. - Вимкнути анімації лічильників - Розділи ввімкнено у панелі прогресу. - Розділи вимкнено у панелі прогресу. - Вимкнути розділи панелі прогресу - Фонтанну анімацію увімкнено над кнопкою Подобається. - Фонтанну анімацію вимкнено над кнопкою Подобається. - Вимкнути анімацію кнопки Подобається - "Вимикається 'Відтворення зі швидкістю 2x' під час утримання - -Зауваження: -• Вимкнення накладання швидкості відновлює поведінку 'Перемотки пересуванням' старого макету. -• Це налаштування не вмикає примусове накладання швидкості." - Вимкнути накладання швидкості - Сплеш анімацію увімкнено. - Сплеш анімацію вимкнено. - Вимкнути сплеш анімацію - "Вимикається такі взаємодії під час розгортання опису відео: - -• Натисніть, щоб прокрутити. -• Натисніть і утримуйте, щоб вибрати текст." - Вимкнути взаємодію з описом відео - Кодек VP9 увімкнено. - "Кодек VP9 вимкнено. - -• Максимальна роздільна здатність - 1080p. -• Відтворення відео використовуватиме більше інтернет даних ніж VP9. -• Кодек VP9 все ще використовується для HDR відео." - Вимкнути кодек VP9 - Смугу прогресу Каїр вимкнено. - "Смугу прогресу Каїр увімкнено. - -Побічний ефект: Тема Каїр також застосовується до цяток сповіщення." - Увімкнути смугу прогресу Каїр - Компактне керування заповнює весь екран. - Компактне керування не заповнює весь екран. - Увімкнути компактне керування - Користувацьку швидкість відтворення вимкнено. - Користувацьку швидкість відтворення увімкнено. - Увімкнути користувацьку швидкість відтворення - Користувацький колір смуги прогресу вимкнено. - Користувацький колір смуги прогресу увімкнено. - Увімкнути користувацький колір смуги прогресу - Журнали налагодження не містять буфер. - Журнали налагодження містять буфер. - Увімкнути ведення журналу буфера налагодження - Журнали налагодження вимкнено. - Журнали налагодження увімкнено. - Увімкнути журнал налагодження - Типову швидкість відтворення не застосовується для Shorts. - Типову швидкість відтворення застосовується для Shorts. - Увімкнути типову швидкість відтворення Shorts - Зовнішній браузер вимкнено. - Зовнішній браузер увімкнено. - Увімкнути зовнішній браузер - Градієнт екрану завантаження вимкнено. - Градієнт екрану завантаження увімкнено. - Увімкнути градієнт екрану завантаження - Відстань між кнопками навігації не зменшується. - Відстань між кнопками навігації зменшується. - Увімкнути вузькі кнопки навігації - Політика перенаправлення. - Обхід URL переадресацій. - Увімкнути безпосереднє відкриття посилань - Вмикає кодек OPUS, якщо відповідь плеєра включає кодек OPUS. - Увімкнути кодек OPUS - Не зберігається і не відновлюється яскравість при переході з/до повноекранного режиму. - Зберігається та відновлюється яскравість при переході з/до повноекранного режиму. - Увімкнути збереження та відновлення яскравості - Натискання на смугу прогресу вимкнено. - Натискання на смугу прогресу увімкнено. - Увімкнути натискання на смугу прогресу - "Це відновить мініатюри для прямих трансляцій, але не мініатюри панелі прогресу. - -Інтернет даних може використовуватися більше, та мініатюри панелі прогресу матимуть невелику затримку перед показом. - -Ця функція працює найкраще з дуже швидким інтернетом." - Мініатюри панелі прогресу середньої якості. - Мініатюри панелі прогресу високої якості. - Увімкнути високоякісні мініатюри - Мітку часу вимкнено. - "Мітку часу увімкнено. - -Застереження: -• Це налаштування вмикає не тільки мітку часу, але й дозволяє користувачам приховати інтерфейс, натиснувши на тло плеєра. -• Оскільки ця функція на стадії розробки Google, макет може бути порушений." - Увімкнути мітку часу - Зміну яскравості жестом вимкнено. - Зміну яскравості жестом увімкнено. - Увімкнути зміну яскравості жестом - Вібрацію вимкнено. - Вібрацію увімкнено. - Увімкнути вібрацію - Нижнє значення жесту яскравості не активує автояскравість. - Нижнє значення жесту яскравості активує автояскравість. - Увімкнути автояскравість жестом - Натисніть, щоб активувати жест переміщення. - Натисніть і утримуйте, щоб активувати жест переміщення. - Увімкнути жест натискання для переміщення - Проведення вгору / вниз не відтворюватиме наступне / попереднє відео. - Проведення вгору / вниз відтворюватиме наступне / попереднє відео. - Увімкнути зміну відео проведенням у повноекранному режимі - Зміну гучності жестом вимкнено. - Зміну гучності жестом увімкнено. - Увімкнути зміну гучності жестом - Панель навігації непрозора. - Панель навігації напівпрозора. - Увімкнути напівпрозорість панелі навігації - Перехід на повний екран при проведенні вниз під відеоплеєром вимкнено. - Перехід на повний екран при проведенні вниз під відеоплеєром увімкнено. - Увімкнути жести панелі перегляду - "Увімкнення цього налаштування вимкне кнопку налаштувань у вкладці Ви. - -У цьому випадку, будь ласка, використовуйте такий шлях для доступу до налаштувань: -'Вкладка Ви → Перегляд каналу → Меню → Налаштування'" - Увімкнути широку панель пошуку у вкладці Ви - Широку панель пошуку вимкнено. - Широку панель пошуку увімкнено. - Увімкнути широку панель пошуку - Широка панель пошуку не включає заголовок YouTube. - Широка панель пошуку включає заголовок YouTube. - Увімкнути широку панель пошуку з заголовком - Опис - "Введіть назву в панелі опису відео. -Ці символи варіюються залежно від вашої мови. -'Розгортати опис відео' може не працювати, якщо збережете неправильний рядок." - Назва в панелі опису відео - Опис відео розгортається вручну. - Опис відео розгортається автоматично. - Розгортати опис відео - Бажаєте продовжити? - Скинуто. - Перезапустіть, щоб нормально завантажився макет - "Помилка зі сторони сервера YouTube спричиняє приховування тексту лічильників таких як вподобайки, перегляди, та дати завантаження для деяких користувачів. - -Тимчасовим вирішенням цієї проблеми є підміна версії програми на 19.13.37. - -Бажаєте підмінити версію програми перед перезапуском програми?" - Поновити й перезапустити - Не вдалося експортувати налаштування. - Налаштування було вдало експортовано. - Експорт налаштувань у файл. - Експортувати налаштування - Імпортувати - Копіювати - Імпортувати або експортувати налаштування у вигляді тексту. - Імпорт / Експорт як текст - Не вдалося імпортувати налаштування. - Налаштування скинуто до стандартних. - Налаштування було вдало імпортовано. - Імпорт налаштувань зі збереженого файлу. - Імпортувати налаштування - Скинути - Пошук %s - Розширені - Зовнішній завантажувач - Не встановлено - "%1$s не встановлено. -Будь ласка, завантажте %2$s з сайту." - Попередження - %s не встановлено. Будь ласка, встановіть його. - Ім\'я пакета встановленого зовнішнього завантажувача, наприклад YTDLnis. - Ім\'я пакета завантажувача списку відтворення - Ім\'я пакета встановленого зовнішнього завантажувача, наприклад NewPipe або YTDLnis, при довгому натисканні. - Ім\'я пакета завантажувача відео при довгому натисканні - Ім\'я пакета встановленого зовнішнього завантажувача, наприклад NewPipe або YTDLnis. - Ім\'я пакета завантажувача відео - "Відео перемикатиметься на повний екран в таких ситуаціях: - -• Натиснуто на мітку часу у коментарях. -• Відео розпочалося." - Примусово на повний екран - Показ діалога оптимізації для GMSCore при кожному запуску програми. - Показувати діалог оптимізації для GMSCore - Список назв меню облікового запису для фільтрування, розділених новим рядком. - Фільтр меню облікового запису - "Приховуються елементи меню облікового запису і вкладки Ви. -Деякі компоненти не можуть бути приховані." - Приховати меню акаунту - Картки альбому показується. - Картки альбомів приховано. - Приховати картки альбому - Секції Місця на відео, Ігри та Музика показується. - Секції Місця на відео, Ігри та Музика приховано. - Приховати секції атрибутів - Контейнер автовідтворення перед перегляду показується. - Контейнер автовідтворення перед перегляду приховано. - Приховати контейнер автовідтворення перед перегляду - Кнопку огляду магазину показується. - Кнопку огляду магазину приховано. - Приховати кнопку огляду магазину - "Приховати наступні полиці: -• Важливі новини -• Продовжити перегляд -• Переглянути більше каналів -• Прослухати знову -• Покупки -• Переглянути ще раз" - Приховати карусельну полицю - Показується у стрічці. - Приховано у стрічці. - Приховати у стрічці - Показується у пов\'язаних відео. - Приховується у пов\'язаних відео. - Приховати у пов\'язаних відео - Показується в пошуку. - Приховано в пошуку. - Приховати в пошуку - Рекомендації каналу показується. - Рекомендації каналу приховано. - Приховати рекомендації каналу - Полицю спонсорів каналу показується. - Полицю спонсорів каналу приховано. - Приховати полицю спонсорів каналу - Посилання вверху опису каналу показується. - Посилання вверху опису каналу приховано. - Приховати посилання в описі каналу - "YouTube Shorts -Списки відтворення -Магазин" - Список назв вкладок каналу для фільтрування, розділених новим рядком. - Фільтр вкладок каналу - Фільтр вкладок каналу вимкнено. - Фільтр вкладок каналу увімкнено. - Увімкнути фільтр вкладок каналу - Водяний знак показується. - Водяний знак приховано. - Приховати водяний знак каналу - Секції розділів показується. - Секції розділів приховано. - Приховати секції розділів - Полицю фішок показується - Полицю фішок приховано - Приховати полицю фішок - Кнопку Створити кліп показується. - Кнопку Створити кліп приховано. - Приховати Створити кліп - Кнопку створення Shorts показується. - Кнопку створення Shorts приховано. - Приховати кнопку створення Shorts - Виділені пошукові посилання показується. - Виділені пошукові посилання приховано. - Приховати виділені пошукові посилання - Кнопку подяки показується. - Кнопку подяки приховано. - Приховати кнопку подяки - Кнопку з міткою часу та емодзі показується. - Кнопку з міткою часу та емодзі приховано. - Приховати кнопку з міткою часу та емодзі - Банер \'Коментарі спонсорів\' показується. - Банер \'Коментарі спонсорів\' приховано. - Приховати банер \'Коментарі спонсорів\' - Секцію коментарів показується у головній стрічці. - Секцію коментарів приховано у головній стрічці. - Приховати секцію Коментарі у головній стрічці - Секцію коментарів показується. - Секцію коментарів приховано. - Приховати секцію Коментарі - Показується в каналі. - Приховано в каналі. - Приховати у каналі - Показується у головній стрічці та пов\'язаних відео. - Приховано у головній стрічці та пов\'язаних відео. - Приховати у головній стрічці та пов\'язаних відео - Показується в стрічці підписок. - Приховано в стрічці підписок. - Приховати у стрічці підписок - Секцію Як створювався цей контент показується. - Секцію Як створювався цей контент приховано. - Приховати секцію Контент - Скриньку фінансування показується. - Скриньку фінансування приховано. - Приховати скриню фінансування - Фільтр подвійного натискання показується. - Фільтр подвійного натискання приховано. - Приховати фільтр подвійного натискання - Кнопку Завантажити показується. - Кнопку Завантажити приховано. - Приховати кнопку Завантажити - Картки кінцевого екрану показується. - Картки кінцевого екрану приховано. - Приховати картки кінцевого екрану - Розширювані фішки показується. - Розширювані фішки приховано. - Приховати розширювану фішку під відео - Висувні полиці показується. - Висувні полиці приховано. - Приховати висувні полиці - Кнопку субтитрів показується. - Кнопку субтитрів приховано. - Приховати кнопку субтитрів в стрічці - Список назв висувного меню для фільтрування, розділених новим рядком. - Фільтр висувного меню у стрічці - Фільтр висувного меню у стрічці вимкнено. - Фільтр висувного меню у стрічці увімкнено. - Увімкнути фільтр висувного меню у стрічці - Панель пошуку в стрічці показується. - Панель пошуку в стрічці приховано. - Приховати панель пошуку в стрічці - Опитування в стрічці показується. - Опитування в стрічці приховано. - Приховати опитування в стрічці - Покадрову перемотку показується. - Покадрову перемотку приховано. - Приховати покадрову перемотку - Плавучу кнопку показується. - Плавучу кнопку приховано. - Приховати плавучу кнопку - Плавучу кнопку мікрофону показується. - Плавучу кнопку мікрофону приховано. - Приховати плавучу кнопку мікрофону - Полицю \'Для вас\' показується. - Полицю \'Для вас\' приховано. - Приховати полицю \'Для вас\' - Повноекранну рекламу показується. - Повноекранну рекламу приховано. - Приховати повноекранну рекламу - "Повноекранну рекламу блокується. - -Застереження: Зображення публікації спільноти в повноекранному режимі може бути блоковано." - Повноекранну рекламу закривається за допомогою кнопки Закрити. - Закривати повноекранну рекламу - Загальну рекламу показується. - Загальну рекламу приховано. - Приховати загальну рекламу - Рекламу YouTube Premium під відеоплеєром показується. - Рекламу YouTube Premium під відеоплеєром приховано. - Приховати рекламу YouTube Premium - Сірі роздільники показується. - Сірі роздільники приховано. - Приховати сірий роздільник - Ідентифікатор показується. - Ідентифікатор приховано. - Приховати ідентифікатор - Кнопку пошуку за зображенням показується. - Кнопку пошуку за зображенням приховано. - Приховати кнопку пошуку за зображенням - Полицю зображень показується. - Полицю зображень приховано. - Приховати полицю зображень - Секції інформаційних карток показується. - Секції інформаційних карток приховано. - Приховати секції інформаційних карток - Інформаційні картки показується. - Інформаційні картки приховано. - Приховати інформаційні картки - Інформаційні панелі показується. - Інформаційні панелі приховано. - Приховати інформаційні панелі - Кнопку Спонсорувати показується. - Кнопку Спонсорувати приховано. - Приховати кнопку Спонсорувати - Секцію Ключові концепції показується. - Секцію Ключові концепції приховано. - Приховати секцію Ключові концепції - "Головна/Підписки/Результати пошуку фільтрується, щоб приховати контент, який відповідає ключовим фразам. - -Застереження: -• YouTube Shorts не можливо приховати за назвою каналу. -• Деякі компоненти інтерфейсу не можливо приховати. -• Шукання за ключовим словом може не давати результатів." - Про фільтрування ключових слів - Взяття ключового слова/фрази в подвійні лапки запобігатиме частковим збігам назв відео та каналів.<br><br>Наприклад,<br><b>\"ші\"</b> приховає відео: <b>Як працює ШІ?</b><br>але не приховає: <b>Що означає цифра шість?</b> - Лише цілі слова - Коментарі не фільтрується. - Коментарі фільтрується. - Приховати коментарі за ключовими словами - Відео у головній стрічці не фільтруються. - Відео у головній стрічці фільтруються. - Приховати відео на головній за ключовими словами. - "Ключові слова та фрази для приховування, відокремлені новими рядками. - -Ключовими словами можуть бути назви каналів чи будь-який текст у заголовках відео. - -Слова з великими літерами в середині повинні вводитися відповідно регістру (тобто: iPhone, TikTok, LeBlanc)." - Ключові слова для приховування - Результати пошуку не фільтрується. - Результати пошуку фільтрується. - Приховати результати пошуку за ключовими словами - Відео у стрічці підписок не фільтруються. - Відео у стрічці підписок фільтруються. - Приховати відео підписок за ключовими словами - Ключове слово приховає всі відео: %s. - Неможливо використати ключове слово: %s. - Додайте лапки, щоб використовувати ключове слово: %s. - Ключове слово комплектує з: %s. - Ключове слово занадто коротке і потребує лапок: %s. - Останні публікації показується. - Останні публікації приховано. - Приховати останні публікації - Кнопку \'Останні відео\' показується. - Кнопку \'Останні відео\' приховано. - Приховати кнопку \'Останні відео\' - Кнопки Подобається та Не подобається показується. - Кнопки Подобається та Не подобається приховано. - Приховати кнопки Подобається і Не подобається - Повідомлення онлайн чату показується.\n\nЦе налаштування також застосовується до прямих трансляцій Shorts. - Повідомлення онлайн чату приховано.\n\nЦе налаштування також застосовується до прямих трансляцій Shorts. - Приховати повідомлення онлайн чату - Кнопку повтору онлайн чату показується.\n\nЗ\'являється в повноекранному режимі при закритті онлайн чату. - Кнопку повтору онлайн чату приховано.\n\nЗ\'являється в повноекранному режимі при закритті онлайн чату. - Приховати кнопку повтору онлайн чату - Приховати відео з менш ніж 1,000 переглядів з головної стрічки, вантажені з каналів, на які не підписані. - Приховати відео з малою кількістю переглядів - Панелі про медицину показується. - Панелі про медицину приховано. - Приховати медичні панелі - Товарні полиці показується. - Товарні полиці приховано. - Приховати товарну полицю - Мікс плейлист показується. - Мікс плейлист приховано. - Приховати Мікс плейлист - Полиці фільмів показується. - Полиці фільмів приховано. - Приховати полицю фільмів - Панель навігації показується. - Панель навігації приховано. - Приховати панель навігації - Кнопку Створити показується. - Кнопку Створити приховано. - Приховати кнопку Створити - Кнопку Головна показується. - Кнопку Головна приховано. - Приховати кнопку Головна - Навігаційну мітку показується. - Навігаційну мітку приховано. - Приховати мітки на панелі навігації - Кнопку Бібліотека показується. - Кнопку Бібліотека приховано. - Приховати кнопку Бібліотека - Кнопку сповіщень показується. - Кнопку сповіщень приховано. - Приховати кнопку сповіщень - Кнопку YouTube Shorts показується. - Кнопку YouTube Shorts приховано. - Приховати кнопку YouTube Shorts - Кнопку Підписки показується. - Кнопку Підписки приховано. - Приховати кнопку Підписки - Кнопку \'Сповістити\' показується. - Кнопку \'Сповістити\' приховано. - Приховати кнопку \'Сповістити\' - Мітку Містить пряму рекламу показується. - Мітку Містить пряму рекламу приховано. - Приховати мітку Містить пряму рекламу - Ігрову кімнату показується. - Ігрову кімнату приховано. - Приховати Ігрову кімнату - Кнопку автовідтворення показується. - Кнопку автовідтворення приховано. - Приховати кнопку автовідтворення - Кнопку субтитрів показується. - Кнопку субтитрів приховано. - Приховати кнопку субтитрів - Кнопку трансляції показується. - Кнопку трансляції приховано. - Приховати кнопку трансляції - Кнопку згортування показується. - Кнопку згортування приховано. - Приховати кнопку згортування - Меню Кінематографічне освітлення показується. - Меню Кінематографічне освітлення приховано. - Приховати меню Кінематографічне освітлення - Меню звукової доріжки показується. - Меню звукової доріжки приховано. - Приховати меню звукової доріжки - Колонтитул меню субтитрів показується. - Колонтитул меню субтитрів приховано. - Приховати колонтитул меню субтитрів - Меню субтитрів показується. - Меню субтитрів приховано. - Приховати меню субтитрів - Меню Premium 1080p показується. - Меню Premium 1080p приховано. - Приховати меню Premium 1080p - Меню допомоги та підтримки показується. - Меню допомоги та підтримки приховано. - Приховати меню допомоги та підтримки - Меню Слухати через YouTube Music показується. - Меню Слухати через YouTube Music приховано. - Приховати меню Слухати через YouTube Music - Меню Блокування екрану показується. - Меню Блокування екрану приховано. - Приховати меню Блокування екрану - Меню Повторювати відео показується. - Меню Повторювати відео приховано. - Приховати меню Повторювати відео - Меню додаткової інформації показується. - Меню додаткової інформації приховано. - Приховати меню додаткової інформації - Меню картинки в картинці показується. - Меню картинки в картинці приховано. - Приховати меню картинки в картинці - Меню швидкості відтворення показується. - Меню швидкості відтворення приховано. - Приховати меню швидкості відтворення - Меню керування преміумом показується. - Меню керування преміумом приховано. - Приховати меню керування преміумом - Колонтитул меню якості показується. - Колонтитул меню якості приховано. - Приховати колонтитул меню якості - Заголовок меню якості показується. - Заголовок меню якості приховано. - Приховувати заголовок меню якості - Меню Поскаржитися показується. - Меню Поскаржитися приховано. - Приховати меню Поскаржитися - Меню Таймер сну показується. - Меню Таймер сну приховано. - Приховати меню Таймер сну - Меню стабілізації гучності показується. - Меню стабілізації гучності приховано. - Приховати меню стабілізації гучності - Меню статистики для сисадмінів показується. - Меню статистики для сисадмінів приховано. - Приховати меню статистики для сисадмінів - Меню Дивитись у VR показується. - Меню Дивитись у VR приховано. - Приховати меню Дивитись у VR - Кнопку повноекранного режиму показується. - Кнопку повного екранного режиму приховано. - Приховати кнопку повноекранного режиму - Кнопки показується. - Кнопки приховано. - Приховати кнопки Попереднє та Наступне - Полицю покупок показується. - Полицю покупок приховано. - Приховати полицю покупок в плеєрі - Кнопку YouTube Music показується. - Кнопку YouTube Music приховано. - Приховати кнопку Youtube Music - Кнопка Зберегти до списку відтворення показується. - Кнопку Зберегти в списку відтворення приховано. - Приховати кнопку Зберегти в списку відтворення - Секції подкастів показується. - Секції подкастів приховано. - Приховати секції подкастів - Закріплений коментар показується - Закріплений коментар приховано - Приховати Закріплений коментар - Це змінює розмір секції коментарів, тому неможливо відкрити чат у секції коментарів. - Це не змінює розмір секції коментарів, тому можливо відкрити чат у секції коментарів. - Тип приховування закріпленого коментаря - Рекламні сповіщення показується. - Рекламні сповіщення приховано. - Приховати рекламні сповіщення - Кнопку коментарів показується. - Кнопку коментарів приховано. - Приховати кнопку коментарів - Кнопку Не подобається показується. - Кнопку Не подобається приховано. - Приховати кнопку Не подобається - Кнопку Подобається показується. - Кнопку Подобається приховано. - Приховати кнопку Подобається - Кнопку онлайн чату показується. - Кнопку онлайн чату приховано. - Приховати кнопку онлайн чату - Кнопку Детальніше показується. - Кнопку Детальніше приховано. - Приховати кнопку Детальніше - Кнопку відкриття мікс плейлиста показується. - Кнопку відкриття мікс плейлиста приховано. - Приховати кнопку відкриття мікс плейлиста - Кнопку відкриття списку відтворення показується. - Кнопку відкриття списку відтворення приховано. - Приховати кнопку відкриття списку відтворення - Кнопка Зберегти до списку відтворення показується. - Кнопку Зберегти в списку відтворення приховано. - Приховати кнопку Зберегти в списку відтворення - Кнопку Поділитися показується. - Кнопку Поділитися приховано. - Приховати кнопку Поділитися - Контейнер швидких дій показується. - Контейнер швидких дій приховано. - Приховати контейнер швидких дій - "Приховуються такі рекомендовані відео: - -• Відео з тегом 'Тільки для спонсорів'. -• Відео з фразами на кшталт 'Людей також дивилися' внизу." - Приховати рекомендовані відео - Секцію \'Більше відео\' у контейнері швидких дій та пов\'язаних відео показується. - Секцію \'Більше відео\' у контейнері швидких дій та пов\'язаних відео приховано. - Приховати пов’язані відео - Пов\'язані відео показується. - Пов\'язані відео приховано. - Приховати пов’язані відео - "Це налаштування обмежує максимальну кількість, які можуть вантажитися на екран плеєра. - -Якщо екран плеєра змінюється через зміни на стороні сервера, можуть бути приховані ненавмисно на екрані плеєра." - Кнопку Ремікс показується. - Кнопку Ремікс приховано. - Приховати кнопку Ремікс - Кнопку Поскаржитися показується. - Кнопку Поскаржитися приховано. - Приховати кнопку Поскаржитися - Кнопку винагород показується. - Кнопку винагород приховано. - Приховати кнопку винагород - Мініатюри в історії пошукового запиту показується. - Мініатюри в історії пошукового запиту приховано. - Приховати мініатюру пошукового запиту - Сповіщення перемотки показується. - Сповіщення перемотки приховано. - Приховати сповіщення перемотки - Сповіщення скасування перемотки показується. - Сповіщення скасування перемотки приховано. - Приховати сповіщення скасування перемотки - Мітки розділів біля мітки часу показується. - Мітки розділів біля мітки часу приховано. - Приховати мітки розділів панелі прогресу - Панель прогресу у відеоплеєрі показується. - Панель прогресу у відеоплеєрі приховано. - Мініатюру панелі прогресу показується. - Мініатюру панелі прогресу приховано. - Приховати мініатюри панелі прогресу у відео - Приховати панель прогресу у відеоплеєрі - Картки само спонсорства показується. - Картки само спонсорства приховано. - Приховати картки само спонсорства - Меню Про додаток показується. - Меню Про додаток приховано. - Приховати меню Про додаток - Меню Доступність показується. - Меню Доступність приховано. - Приховати меню Доступність - Меню Акаунт показується. - Меню Акаунт приховано. - Приховати меню Акаунт - Меню Автоматичне відтворення показується. - Меню Автоматичне відтворення приховано. - Приховати меню Автоматичне відтворення - Меню Платежі показується. - Меню Платежі приховано. - Приховати меню Платежі - Меню Субтитри показується. - Меню Субтитри приховано. - Приховати меню Субтитри - Меню Підключені додатки показується. - Меню Підключені додатки приховано. - Приховати меню Підключені додатки - Меню Заощадження трафіку показується. - Меню Заощадження трафіку приховано. - Приховати меню Заощадження трафіку - Меню Загальні показується. - Меню Загальні приховано. - Приховати меню Загальні - Меню Керувати всією історією пошуків показується. - Меню Керувати всією історією пошуків приховано. - Приховати меню Керувати всією історією пошуків - Меню Чат показується. - Меню Чат приховано. - Приховати меню Чат - Меню Сповіщення показується. - Меню Сповіщення приховано. - Приховати меню Сповіщення - Меню Фоновий і офлайн-режим показується. - Меню Фоновий і офлайн-режим приховано. - Приховати меню Фоновий і офлайн-режим - Меню Дивитись на телевізорі показується. - Меню Дивитись на телевізорі приховано. - Приховати меню Дивитись на телевізорі - Меню Сімейний Центр показується. - Меню Сімейний Центр приховано. - Приховати меню Сімейний Центр - Меню Спробуйте нові експериментальні функції показується. - Меню Спробуйте нові експериментальні функції приховано. - Приховати меню Спробуйте нові експериментальні функції - Меню Конфіденційність показується. - Меню Конфіденційність приховано. - Приховати меню Конфіденційність - Меню Покупки й платні підписки показується. - Меню Покупки й платні підписки приховано. - Приховати меню Покупки й платні підписки - Приховати елементи в меню налаштувань YouTube. - Приховати меню налаштувань YouTube - Меню Параметри якості відео показується. - Меню Параметри якості відео приховано. - Приховати меню Параметри якості відео - Меню Ваші дані на YouTube показується. - Меню Ваші дані на YouTube приховано. - Приховати меню Ваші дані на YouTube - Кнопку Поділитися показується. - Кнопку Поділитися приховано. - Приховати кнопку Поділитися - Кнопку магазину показується. - Кнопку магазину приховано. - Приховати кнопку магазину - Посилання покупки показується. - Посилання покупки приховано. - Приховати посилання покупки - Панель каналу показується. - Панель каналу приховано. - Приховати панель каналу - Кнопку Коментарі показується. - Кнопку Коментарі приховано. - Приховати кнопку Коментарі - Кнопку вимкнених коментарів або з позначкою \"0\" показується. - Кнопку вимкнених коментарів або з позначкою \"0\" приховано. - Приховати кнопку вимкнених коментарів - Кнопку Не подобається показується. - Кнопку Не подобається приховано. - Приховати кнопку Не подобається - "Плавучі кнопки, такі як 'Використати цей звук' показується у вкладці YouTube Shorts каналу." - "Плавучі кнопки, такі як 'Використати цей звук' приховано у вкладці YouTube Shorts каналу." - Приховати плавучу кнопку - Мітку посилання відео показується. - Мітку посилання відео приховано. - Приховати мітку посилання на повне відео - Кнопку Зелений екран показується. - Кнопку Зелений екран приховано. - Приховати кнопку Зелений екран - Інформаційні панелі показується. - Інформаційні панелі приховано. - Приховати інформаційні панелі - Кнопку Спонсорувати показується. - Кнопку Спонсорувати приховано. - Приховати кнопку Спонсорувати - Кнопку Подобається показується. - Кнопку Подобається приховано. - Приховати кнопку Подобається - Заголовок онлайн чату показується.\n\nКнопку назад у заголовку не приховуватиметься. - Заголовок онлайн чату приховано.\n\nКнопку назад у заголовку не приховуватиметься. - Приховати заголовок онлайн чату - Кнопку місцезнаходження показується. - Кнопку місцезнаходження приховано. - Приховати кнопку місцезнаходження - Панель навігації показується. - Панель навігації приховано. - Приховати панель навігації - Мітку Містить пряму рекламу показується. - Мітку Містить пряму рекламу приховано. - Приховати мітку Містить пряму рекламу - Заголовок при призупиненні показується. - Заголовок при призупиненні приховано. - Приховати заголовок при призупиненні - Кнопки накладені при паузі показується. - Кнопки накладені при паузі приховано. - Приховати кнопки накладені при паузі - Фон кнопки показується. - Фон кнопки приховано. - Приховати фон кнопки Відтворити - Призупинити - Кнопку Ремікс показується. - Кнопку Ремікс приховано. - Приховати кнопку Ремікс - Кнопку Зберегти звук показується. - Кнопку Зберегти звук приховано. - Приховати кнопку Зберегти звук - Кнопку пропозицій пошуку показується. - Кнопку пропозицій пошуку приховано. - Приховати кнопку пропозицій пошуку - Кнопку Поділитися показується. - Кнопку Поділитися приховано. - Приховати кнопку Поділитися - Показується в каналі. - "Приховується в каналі. - -Інформація: -• Лише полиці з заголовком Shorts на головній вкладці приховано." - Приховати у каналі - Показується в історії перегляду. - Приховано в історії перегляду. - Приховати в історії перегляду - Показується у головній стрічці та пов\'язаних відео. - Приховано у головній стрічці та пов\'язаних відео. - Приховати у головній стрічці та пов\'язаних відео - Показується в результатах пошуку. - Приховано в результатах пошуку. - Приховати в результатах пошуку - Показується в стрічці підписок. - Приховано в стрічці підписок. - Приховати у стрічці підписок - "Приховується полиці Shorts. - -Відома проблема: Офіційні заголовки в результатах пошуку приховуватиметься." - Приховати полицю Shorts - Кнопка Магазин показується. - Кнопку Магазин приховано. - Приховати кнопку Магазин - Кнопку Магазин показується. - Кнопку Магазин приховано. - Приховати кнопку Магазин - Кнопку Зі звуком показується. - Кнопку Зі звуком приховано. - Приховати кнопку Зі звуком - Мітку метаданих показується. - Мітку метаданих приховано. - Приховати мітку метаданих звуку - Стікери показується. - Стікери приховано. - Приховати стікери - Кнопку Підписатися показується. - Кнопку Підписатися приховано. - Приховати кнопку Підписатися - Кнопку супер подяки показується. - Кнопку супер подяки приховано. - Приховати кнопку супер подяки - Товари з тегами показується. - Товари з тегами приховано. - Приховати товари з тегами - Панель інструментів показується. - Панель інструментів приховано. - Приховати панель інструментів - Кнопку Тренди показується. - Кнопку Тренди приховано. - Приховати кнопку Тренди - Кнопку Використати шаблон показується. - Кнопку Використати шаблон приховано. - Приховати кнопку Використати шаблон - Кнопку Використати цей звук показується. - Кнопку Використати цей звук приховано. - Приховати кнопку Використати цей звук - Назву показується. - Назву приховано. - Приховати назву відео - Кнопку \'Показати більше\' показується. - Кнопку \'Показати більше\' приховано. - Приховати кнопку \'Показати більше\' - Панель взаємодії показується. - Панель взаємодії приховано. - Приховати панель взаємодії - Кнопку Спробувати показується. - Кнопку Спробувати приховано. - Приховати кнопку Спробувати - Карусель підписок показується. - Карусель підписок приховано. - Приховати карусель підписок - Пропоновані дії показується. - Пропоновані дії приховано. - Приховати пропоновані дії - "Це налаштування застаріле. - -Натомість використовуйте 'Налаштування → Автоматичне відтворення → Автовідтворення наступного відео'. - -Зауваження: -• Якщо виникли проблеми з 'Кінцевим екраном з пропонованими відео', спробуйте перезапустити додаток." - Кінцевий екран з пропонованими відео показується. - "Кінцевий екран з пропонованими відео приховано коли автовідтворення вимкнене. - -Автовідтворення можна змінити у налаштуваннях YouTube: -'Налаштування → Автоматичне відтворення → Автовідтворення наступного відео'" - Приховати кінцевий екран з пропонованими відео - Кнопку Дякую показується. - Кнопку Дякую приховано. - Приховати кнопку Дякую - Полиці квитків показується. - Полиці квитків приховано. - Приховати полицю квитків - Мітку часу показується. - Мітку часу приховано. - Приховати мітку часу - Тимчасові реакції показується. - Тимчасові реакції приховано. - Приховати тимчасові реакції - Кнопку трансляції показується. - Кнопку трансляції приховано. - Приховати кнопку трансляції - Кнопку Створити показується. - Кнопку Створити приховано. - Приховати кнопку Створити - Кнопку сповіщень показується. - Кнопку сповіщень приховано. - Приховати кнопку сповіщень - Секції Текст відео показується. - Секції Текст відео приховано. - Приховати секції Текст відео - Відеорекламу показується. - Відеорекламу приховано. - Приховати відеорекламу - "Головна/Підписки/Результати пошуку фільтрується, щоб приховати відео з переглядами менше або більше вказаної кількості. - -Застереження: -• YouTube Shorts неможливо приховати. -• Відео з 0 переглядів не фільтруються." - Про фільтрацію за переглядами - Відео у головній стрічці не фільтруються. - Відео у головній стрічці фільтруються. - Приховати відео на головній за переглядами - Результати пошуку не фільтрується. - Результати пошуку фільтрується. - Приховати результати пошуку за переглядами - Відео у стрічці підписок не фільтруються. - Відео у стрічці підписок фільтруються. - Приховати відео підписок за переглядами - Приховати рекомендовані відео з меншою за вказану кількість переглядів.\n\nВідома проблема: Відео з 0 переглядами не фільтруються. - Приховати рекомендовані відео за переглядами - Відео з більше ніж ця кількість переглядів приховуватимуться. - Більші за переглядами - Відео з менше ніж ця кількість переглядів приховуватимуться. - Менші за переглядами - тис. -> 1 000\nмлн -> 1 000 000\nмлрд -> 1 000 000 000\nпереглядів -> views - Вкажіть шаблон вашою мовою для кількості переглядів як показується під будь-яким відео в інтерфейсі користувача. Будь-який ключ (літера/слово на вашій мові) -> значення (значення ключа) повинно бути з нового рядка. Ключі йдуть перед позначкою \"->\". Якщо змінили мову програми або системи, потрібно скинути це налаштування.\n\nЗразок:\nАнглійською: 10K views = K -> 1000, views -> views\nУкраїнською: 10 тис. переглядів = тис. -> 1000, переглядів -> views - Ключі переглядів - Банер перегляду товарів показується. - Банер перегляду товарів приховано. - Приховати банер перегляду товарів - Кнопку голосового пошуку показується. - Кнопку голосового пошуку приховано. - Приховати кнопку голосового пошуку - Результати вебпошуку показується. - Результати вебпошуку приховано. - Приховати результати вебпошуку - Yoodles показується. - Yoodles приховано. - Приховати Yoodles - "Yoodles з'являються на декілька днів щороку. - -Якщо Yoodles наразі зображаються у вашому регіоні й це налаштування приховування увімкнено, то панель фільтрів під рядком пошуку також приховуватиметься." - Накладання при масштабуванні показується. - Накладання при масштабуванні приховано. - Приховати накладання при масштабуванні - AFN синя - AFN червона - Користувацька - Стандартна - ММТ - MMT синя - MMT зелена - MMT Помаранчева - MMT Рожева - MMT Бірюзова - MMT жовта - Revancify синя - Revancify червона - Revancify жовта - Vanced чорна - Vanced світла - Xisr жовта - YouTube - Зберігає ландшафтний режим під час вимкнення та ввімкнення екрана у повноекранному режимі. - Кількість мілісекунд примусового ландшафтного режиму. - Скільки зберігати ландшафтний режим - Зберігати ландшафтний режим - Стандартна - Дію подвійного натискання вимкнено. - "Дію подвійного натискання увімкнено. - -• Подвійне натискання для зміни мінімізованого відео до більшого розміру. -• Подвійне натискання ще раз для зміни до первісного розміру." - Увімкнути дію подвійного натискання - Перетягування вимкнено. - Перетягування увімкнено. - Увімкнути перетягування - Кнопки розгортання і закриття показується. - Кнопки приховано.\n(проведіть по мініплеєру, щоб розгорнути чи закрити) - Приховати кнопки розгортання і закриття - Перемотування вперед та назад показується. - Перемотування вперед та назад приховано. - Приховати кнопки перемотування вперед та назад - Підтексти показується. - Підтексти приховано. - Приховати підтексти - Непрозорість затемнення мініплеєра має бути в межах 0-100. - Значення непрозорості в межах 0-100, де 0 це прозоро. - Непрозорість затемнення - Оригінал - Телефонний - Планшетний - Новітній 1 - Новітній 2 - Новітній 3 - Тип мініплеєра - Накладання кнопок - "Натискайте для перемикання станів постійного повторення. -Натисніть й утримуйте для призупинення перемикання станів повторення." - Показувати кнопку постійного повторення - "Натисніть, щоб скопіювати URL відео. -Натисніть і утримуйте, щоб скопіювати URL відео з міткою часу." - "Натисніть, щоб скопіювати URL відео із міткою часу. -Натисніть і утримуйте, щоб скопіювати мітку часу відео." - Показувати кнопку копіювання URL із міткою часу - Показувати кнопку копіювання URL відео - Натисніть для запуску зовнішнього завантажувача - Показувати кнопку зовнішнього завантаження - Натисніть, щоб вимкнути звук поточного відео. Натисніть знову, щоб увімкнути. - Показувати кнопку вимкнення звуку - Натисніть і утримуйте, щоб змінити стан кнопки. - Швидкість відтворення скинуто: %sx. - "Натисніть, щоб відкрити діалог швидкості. -Натисніть і утримуйте, щоб скинути швидкість відтворення до 1.0x. Натисніть і утримуйте ще раз, щоб скинути швидкість назад до типової." - Показувати кнопку Діалог швидкості - "Натисніть, щоб згенерувати список відтворення з усіх відео з каналу від найстарішого до найновішого. -Натисніть і утримуйте, щоб скасувати." - Показувати кнопку впорядкованого за часом списку відтворення - Натисніть, щоб відкрити діалог білого списку. -Натисніть і утримуйте, щоб відкрити діалог налаштування білого списку. - Показувати кнопку Білого списку - Кнопка вбудованого завантаження списку відтворення відкриває вбудований завантажувач, якщо показується. - Кнопка вбудованого завантаження списку відтворення завжди показується, і в публічних списках відтворення відкриває зовнішній завантажувач. - Перевизначити кнопку завантаження списку відтворення - Кнопка завантаження відео відкриває вбудований завантажувач. - Кнопка завантаження відео відкриває зовнішній завантажувач. - Перевизначити кнопку завантаження відео - Для перевизначення дії кнопки потрібно YouTube Music. Натисніть тут, щоб завантажити YouTube Music. - Передумова - Кнопка YouTube Music відкриває стандартний додаток. - Кнопка YouTube Music відкриває RVX Music. - Перевизначити кнопку YouTube Music - Виключено - Включено - Стандартна - Кнопки дії - Додаткові налаштування - Анімація / Відгук - Кнопка завантаження - Експериментальні опції - Регіональні обмеження зображень - Імпорт / Експорт як файл - Імпорт / Експорт як текст - Фільтр ключових слів - Інше - Накладання кнопок - Інформація про патчі - Швидкі дії - Рекомендовані відео - Полиця Shorts - Пропоновані дії - Використано інструменти - Фільтр за кількістю переглядів - Приховувати чи показувати елементи меню облікового запису і вкладки Ви. - Меню облікового запису - Приховувати чи показувати кнопки дії під відео. - Кнопки дії - Реклама - Альтернативні мініатюри - Обходити обмеження кінематографічного освітлення чи вимкнути кінематографічне освітлення. - Кінематографічне освітлення - Приховувати чи показувати панель категорій у стрічці, пошуку та пов\'язаних відео. - Панель категорій - Приховувати чи показувати компоненти панелі каналу під відео. - Панель каналу - Приховувати чи показувати компоненти в профілі каналу. - Профіль каналу - Приховувати чи показувати компоненти секції коментарів. - Коментарі - Приховувати чи показувати публікації спільноти у стрічці та каналі. - Публікації спільноти - Приховати компоненти за допомогою користувацьких фільтрів. - Користувацький фільтр - Приховувати чи показувати висувне меню у стрічці. - Висувне меню - Стрічка - Приховати або змінити компоненти, пов\'язані з повноекранним режимом. - Повноекранний режим - Загальне - Вимкнути чи увімкнути вібрацію. - Вібрація - Перевизначення дії натискання кнопок додатка. - Заміна кнопок - Імпортувати або експортувати налаштування. - Імпорт/Експорт налаштувань - Зміна стилю мінімізованого плеєра в додатку. - Мініплеєр - Різне - Приховувати чи показувати секцію компонентів панелі навігації. - Панель навігації - Інформація про застосовані патчі. - Інформація про патчі - Приховувати чи показувати кнопки у відео. - Кнопки плеєра - Приховати або змінити висувне меню у відеоплеєрі. - Висувне меню - Плеєр - Повернення назви користувача YouTube - Повернення Дизлайків - Спонсорблок - Налаштуйте компоненти панелі прогресу. - Панель прогресу - Приховати елементи в меню налаштувань YouTube. - Меню налаштувань - Приховувати чи показувати компоненти у плеєрі Shorts. - Плеєр Shorts - YouTube Shorts - Підробка даних трансляції для вирішення проблем відтворення. - Підробка даних трансляції - Керування жестами - Приховати або змінити компоненти, розташовані на панелі інструментів, такі як кнопки панелі інструментів, панель пошуку, заголовок. - Панель інструментів - Приховувати чи показувати компоненти опису відео. - Опис відео - Приховати відео за ключовими словами або переглядами. - Фільтр відео - Відео - Змінити налаштування пов\'язані з історією перегляду. - Історія перегляду - Верхній відступ швидких дій повинен бути в межах 0-32. - Налаштуйте відстань від панелі прогресу до контейнера швидких дій, діапазон 0-32. - Верхній відступ швидких дій - "Примусово відкидається відповідь програмного кодека AV1. -Приблизно через 20 секунд буферизації перемикається на інший кодек." - Відкинути відповідь програмного кодека AV1 - Процес спричиняє приблизно 20 секунд буферизації. - Зміщення - Зміни швидкості відтворення застосовуються лише до поточного відео. - Зміни швидкості відтворення застосовуються до всіх відео. - Запам\'ятовувати зміни швидкості відтворення - Тост не показуватиметься при зміні типової швидкості відтворення. - Тост показуватиметься при зміні типової швидкості відтворення. - Показувати тост - Зміна типової швидкості на %s. - Зміни якості застосовуються лише до поточного відео. - Зміни якості застосовуються до всіх відео. - Запам\'ятовувати зміни якості відео - Тост не показуватиметься при зміні типової якості відео. - Тост показуватиметься при зміні типової якості відео. - Показувати тост - Зміна типової якості при мобільному з\'єднанні на %s. - Не вдалося встановити якість відео. - Зміна типової якості при Wi-Fi на %s. - "Вилучається діалогове вікно. -Це не обходить вікові обмеження, а просто приймається автоматично." - Вилучати діалогове вікно - Замінити програмний кодек AV1 кодеком VP9. - Замінити програмний кодек AV1 - Використовується ідентифікатор каналу. - Використовується назву каналу. - Замінити ідентифікатор каналу - Натисніть, щоб показувався залишок часу. - Натисніть, щоб відкрити висувне меню швидкості відтворення або якості відео. - Замінити дію мітки часу - Замінює кнопку створення кнопкою налаштувань. - Замінити кнопку створення - "Натисніть, щоб відкрити налаштування YouTube. -Натисніть і отримуйте, щоб відкрити Розширені налаштування." - "Натисніть, щоб відкрити Розширені налаштування. -Натисніть і отримуйте, щоб відкрити налаштування YouTube." - Тип дії для призначення кнопці - Мініатюри панелі прогресу зображатиметься в повноекранному режимі. - Мініатюри панелі прогресу зображатиметься над панеллю прогресу. - Відновити старі мініатюри панелі прогресу - Старе меню якості відео не показується. - Старе меню якості відео показується. - Відновити старе меню якості відео - \@ідентифікатор (Назва користувача) - Формат відображення - Назва користувача (@ідентифікатор) - Назва користувача - Використовується ідентифікатор. - Використовується назву користувача. - Ввімкнути Повернення назви користувача YouTube - "Ключ розробника YouTube Data API v3 потрібен для заміни Ідентифікатора на Назву користувача. - -Щоденна квота для ключів API на безкоштовному тарифі становить 10,000, і 1 квота використовується для заміни Ідентифікатора на Назву користувача для 1 коментаря. - -Натисніть щоб побачити як створити ключ API." - Про ключ YouTube Data API - Ключ розробника для використання YouTube Data API v3. - Ключ YouTube Data API - 1. Перейдіть до <a href=%1$s>Створення нового проекту</a>.<br>2. Натисніть кнопку <b>CREATE</b>.<br>3. Перейдіть до <a href=%2$s>YouTube Data API v3</a>.<br>4. Натисніть кнопку <b>ENABLE</b>.<br>5. Натисніть кнопку <b>CREATE CREDENTIALS</b>.<br>6. Виберіть опцію <b>Public data</b>.<br>7. Натисніть кнопку <b>NEXT</b>.<br>8. Скопіюйте ключ API.<br><br>※ Ключ API не можна поширювати, тому він не включений у Імпорт / Експорт налаштувань. - Створення ключа розробника YouTube Data API v3 - Про Повернення дизлайків - Дані про Дизлайки надаються за допомогою API Повернення дизлайків YouTube. Натисніть тут, щоб дізнатися більше. - ReturnYouTubeDislike.com - Кнопку Подобається стилізовано для кращого вигляду. - Кнопку Подобається стилізовано під мінімальну ширину. - Компактна кнопка Подобається - Дизлайки показується числом. - Дизлайки показується у відсотках. - Дизлайки у відсотках - Дизлайки не показується. - Дизлайки показується. - Ввімкнути повернення дизлайків YouTube - Розрахункові лайки приховано. - Розрахункові лайки показується. - Показувати розрахункові лайки - Дизлайки недоступні (досягнутий ліміт клієнта API) - Дизлайки недоступні (статус %d). - Дизлайки тимчасово недоступні (закінчився час API). - Дизлайки недоступні (%s). - Перезавантажте відео, щоб голосувати використовуючи Повернення дизлайків YouTube - Дизлайки приховано на Shorts. - Дизлайки показується на Shorts. - "Дизлайки показується на Shorts. - -Застереження: Дизлайки не можуть відображатися якщо користувач не увійшов чи в анонімному режимі." - Показувати Дизлайки на Shorts - Тост не показується, якщо Повернення дизлайків YouTube не доступне. - Тост показується, якщо Повернення дизлайків YouTube не доступне. - Показувати тост, якщо не доступний API - Приховано - Вилучає параметри запиту відстеження з посилань під час поширення посилань. - Обробляти поширення посилань - "Фрази типу '#', 'Збір коштів', 'Магазин' та 'товари' показується в субтитрах відео." - "Фрази типу '#', 'Збір коштів', 'Магазин' та 'товари' приховано з субтитрів відео." - Обробляти субтитри відео - Про Спонсорблок - sponsor.ajay.app - Дані надаються Спонсорблок API. Натисніть тут, щоб дізнатися більше та побачити завантаження для інших платформ - URL API змінено. - URL API недійсне. - URL API скинуто. - Вигляд - Колір змінено - Колір: - Невірний код кольору - Колір скинуто - Створити нові сегменти - Змінити поведінку сегмента - Автоматично приховувати кнопку пропуску - Кнопку пропуску показується для всього сегменту - Кнопку пропуску приховується після декількох секунд - Використовувати компактну кнопку Пропустити - Кнопку Пропустити стилізовано для кращого вигляду - Кнопку Пропустити стилізовано під мінімальну ширину - Показувати кнопку створення нового сегмента - Кнопку створення нового сегменту не показується - Кнопку створення нового сегменту показується - Увімкнути Спонсорблок - Спонсорблок - це краудсорсингова система для пропускання дратівливих частин відео на YouTube - Показувати кнопку голосування - Кнопку голосування за сегмент не показується - Кнопку голосування за сегмент показується - Загальне - Відрегулювати новий крок сегмента - Значення має бути додатнім числом - Кількість мілісекунд, на яку переміщуються кнопки регулювання часу при створенні нових сегментів - Змінити API URL - Адреса, яку Спонсорблок використовує для звернень до сервера - Мінімальна тривалість сегменту - Недопустима тривалість часу. - Сегменти, коротші за це значення (в секундах), не будуть показані або пропущені - Увімкнути відстеження кількості пропусків - Відстеження кількості пропусків не ввімкнено - Дозволяє таблиці лідерів Спонсорблок дізнатися, скільки часу заощаджено. Повідомлення надсилається в таблицю лідерів щоразу, коли сегмент пропущено - Показати тост коли пропущено сегмент автоматично - Тост не показується. Натисніть тут, щоб побачити приклад. - Тост показується, коли сегмент автоматично пропущено. Натисніть тут, щоб побачити приклад. - Показати тривалість відео без сегментів - Тривалість повного відео показується - Тривалість відео мінус всі сегменти, вказано в дужках поруч з повною тривалістю відео - Ваш приватний id користувача - Особистий id користувача повинен бути довжиною не менше 30 символів - Це повинно залишатися конфіденційним. Це як пароль, і його не можна нікому передавати. Якщо хтось має його, він може видавати себе за вас - Прочитано - Перед створенням нових сегментів прочитайте інструкції Спонсорблок - Показати - Дотримуйтесь інструкцій - Інструкція містить правила та поради щодо створення нових сегментів - Переглянути інструкцію - Відрегулювати: Позначте час початку та закінчення сегмента - Вибрати категорію сегмента - Перевірити сегмент - Сегмент триває від %1$02d:%2$02d до %3$02d:%4$02d (%5$d хвилин(-и) %6$02d секунд(-и))\nНадіслати? - Сегмент від \n\n%1$s\nдо\n%2$s\n\n(%3$s)\n\nНадіслати? - Час правильний? - Категорія вимкнена у налаштуваннях. Увімкніть категорію, щоб надіслати. - Редагувати сегмент - Ви хочете змінити час початку чи кінця сегмента? - Вказано неправильний час - Редагувати час сегмента вручну - Перемотати вперед на вказаний час (Стандартно: 150мс) - Встановити %s як початок чи кінець нового сегмента? - кінець - Спочатку позначте дві позиції на панелі часу - початок - зараз - Попередній перегляд сегменту для забезпечення плавного пропуску - Опублікувати створений сегмент - Перемотати назад на вказаний час (Стандартно: 150мс) - Початок має бути перед кінцем - Час сегменту закінчується на - Час сегменту починається з - Новий сегмент Спонсорблок - Скинути - Скинути колір - Дотичне наповнення/Жарти - Дотичні сцени, додані лише для наповнення або гумору, які не є необхідними для розуміння основного змісту відео. Не включає сегменти, що надають контекст або фонові деталі - Основний момент - Частина відео, яку шукає більшість людей - Нагадування про взаємодію (Підписка) - Коротке нагадування про вподобання, підписку або підписку посеред контенту. Якщо воно довге або про щось конкретне, його слід розмістити в розділі самореклами - Перерва/Вступна Анімація - Інтервал без фактичного контенту. Може бути паузою, статичним кадром або повторюваною анімацією. Не включає переходи, що містять інформацію - Музика: Немузична секція - Тільки для використання в музичних відео. Секції музичних відео без музики, які не підпадають під іншу категорію - Кінцеві картки/Титри - Титри або коли з\'являються кінцеві картки YouTube. Не для підсумків з інформацією - Попередній перегляд/Підсумок/Зачіпка - Колекція кліпів, які показують, що відбувається або що сталося у відео чи в інших відео серій, де вся інформація повторюється в іншому місці - Неоплачувана/Самореклама - Подібно до \"Спонсор\", за винятком неоплачуваної або самореклами. Включає секції про товари, пожертви або інформацію про те, з ким вони співпрацювали - Спонсор - Рекламні інтеграції, реферальні посилання і пряма реклама. Не для самореклами або рекомендацій різних подій/творців/сайтів/продуктів, які подобаються автору відео - Копіювати - Не вдалося експортувати: %s - Імпорт/Експорт налаштувань - Ваші конфігурації Спонсорблок JSON, які можуть бути імпортовані/експортовані у різні платформи Спонсорблок - Ваші конфігурації Спонсорблок JSON, які можуть бути імпортовані/експортовані у різні платформи Спонсорблок. Це включає Ваш особистий id користувача. Поширюйте його розумно - Не вдалося імпортувати: %s - Налаштування успішно імпортовано - Ваші налаштування містять особистий ID користувача Спонсорблок.\n\nВаш ID користувача це як пароль і його не можна поширювати.\n - Не показувати знову - Налаштування скопійовано до буфера обміну. - Пропустити автоматично - Пропустити автоматично раз - Пропустити - Основний момент - Пропустити вставку - Перейти до Основного моменту - Пропустити взаємодію - Пропустити вступ - Пропустити перерву - Пропустити перерву - Пропустити без музики - Пропустити закінчення - Пропустити перед перегляд - Пропустити підсумок - Пропустити перед перегляд - Пропустити промо-ролик - Пропустити спонсорську вставку - Пропустити сегмент - Вимкнути - Показувати в панелі прогресу - Показувати кнопку пропуску - Пропущено вставку - Перейдено до Основного моменту. - Пропущено дратівливе нагадування - Пропущено вступ - Пропущено перерву - Пропущено перерву - Пропущено декілька сегментів - Пропущено секцію без музики - Пропущено закінчення - Пропущено перед перегляд - Пропущено підсумок - Пропущено перед перегляд - Пропущено саморекламу - Пропущено спонсорську вставку - Пропущено не надісланий сегмент - Спонсорблок тимчасово недоступний. - Спонсорблок тимчасово недоступний (статус %d). - Спонсорблок тимчасово недоступний (закінчився час API). - Статистика - Статистика тимчасово недоступна (API не працює) - Вантажиться... - Ваша репутація — <b>%.2f</b> - Ви врятували людей від <b>%s</b> сегментів - %1$s годин %2$s хвилин - %1$s хвилин %2$s секунд - %s секунд - Це <b>%s</b> за весь час. <br> Натисніть тут, щоб побачити лідерську таблицю. - Натисніть тут, щоб побачити глобальну статистику та найкращих учасників - Таблиця лідерів Спонсорблок - Спонсорблок вимкнено - Ви пропустили <b>%s</b> сегментів - Скинути лічильник пропущених сегментів? - Це <b>%s</b>. - Ви створили <b>%s</b> сегментів - Натисніть тут для перегляду Ваших сегментів. - Ваше ім\'я користувача: <b>%s</b> - Натисніть тут, щоб змінити ім\'я користувача - Не вдалося змінити ім\'я користувача: Статус: %1$d %2$s - Ім\'я користувача успішно змінено - Неможливо надіслати сегмент.\nВже існує - Неможливо надіслати сегмент: %s - Неможливо відправити сегмент: %s - Неможливо відправити сегмент.\nЧастота обмежена (занадто багато від одного користувача або IP) - Спонсорблок тимчасово не працює - Не вдалося надіслати сегмент (статус: %1$d %2$s) - Сегмент успішно надіслано - Тост не показується, якщо Спонсорблок не доступний. - Тост показується, якщо Спонсорблок не доступний. - Показувати тост, якщо не доступний API - Змінити категорію - Проголосувати «проти» - Не вдалося проголосувати за сегмент: %s - Не вдалося проголосувати за сегмент (закінчився час API). - Не вдалося проголосувати за сегмент (статус: %1$d %2$s) - Немає сегментів для голосування - Проголосувати «за» - Налаштування скопійовано до буфера обміну. - Мітку часу скопійовано в буфер обміну. (%s) - URL скопійовано до буфера обміну - URL з міткою часу скопійовано в буфер обміну. - Оригінал - Палець вгору - Пальці вгору (Каїр) - Серце - Серце (Тоноване) - Прихована - Анімація подвійного натискання - Нижній відступ метапанелі повинен бути в межах 0-64. - Налаштуйте відстань від панелі прогресу до метапанелі, діапазон 0-64. - Нижній відступ метапанелі - Відсоток висоти повинен бути в межах 0-100 (%). - Налаштуйте відсоток висоти порожнього простору, що залишається, коли приховано панель навігації, між 0 і 100 (%). - Відсоток висоти порожнього простору - Натисніть і утримуйте мітку часу для зміни станів повторення Shorts. - Дія довго натискання на мітку часу - "Показується секцію назви відео в повноекранному режимі. - -Застереження: Назва відео зникає при натисканні." - Показувати секцію назви відео - Якщо автовідтворення увімкнено, наступне відео відтворюватиметься після закінчення відліку. - Якщо автовідтворення увімкнено, наступне відео відтворюватиметься без відліку. - Пропустити відлік автовідтворення - "Пропуск перед вантаженого буфера під час запуску відео для обходу затримки примусового застосування типової якості відео. - -• Під час запуску відео є затримка приблизно 0.3 секунди, але типова якість відео застосовується відразу. -• Не застосовується на відео HDR, прямі трансляції, менші ніж 15 секунд." - Пропустити перед вантажений буфер - Тост не показується. - Тост показується. - Показувати тост, коли пропущено - Вмикання цього налаштування може спричинити проблеми відтворення відео. - Пропущено перед вантажений буфер - Значення накладання швидкості повинно бути в межах 0-8.0. - Значення накладання швидкості в межах 0-8.0. - Значення накладання швидкості - "Підміна версії клієнта на стару версію - -• Це змінить зовнішній вигляд програми, але можуть виникнути невідомі побічні ефекти -• Якщо пізніше вимкнути, старий інтерфейс може залишитися, доки не буде очищено дані програми" - Версію не підроблено - Версію підроблено - 17.33.42 - Відновлення старого інтерфейсу - 17.41.37 - Відновлення старої полиці плейлистів - 18.05.40 - Відновлення старого поля введення коментаря - 18.17.43 - Відновлення старої висувної панелі плеєра - 18.33.40 - відновлення старої панелі дій Shorts - 18.38.45 - Відновлення старої поведінки типової якості відео - 18.48.39 - Виключає \'переглянуті\' та \'вподобані\' з оновлення в режимі реального часу - 19.13.37 - Відновлення старого стилю анімації лічильників - Підробити цільову версію програми - Введіть цільову версію підробки програми - Підробити цільову версію програми - Підробити версію програми - "Версію додатка підробиться на старішу версію YouTube. - -Це змінить вигляд і функції додатка, але можуть трапитися невідомі побічні ефекти. - -Якщо пізніше вимкнути, рекомендується очистити дані додатка, щоб запобігти помилкам інтерфейсу." - "Підробка розмірів пристрою до максимального значення. -Високу якість може бути розблоковано для деяких відео, які вимагають великих розмірів пристрою, але не для всіх відео." - Підробити розміри пристрою - AVC (H.264), VP9, чи AV1 кодек відео iOS. - AVC (H.264) кодек відео iOS. - Примусово AVC (H.264) iOS - "Увімкнення може зменшити споживання акумулятора та усунути затинання при відтворенні. - -AVC (H.264) має максимальну роздільну здатність 1080p, а для відтворення відео використовується більше інтернет-даних, ніж VP9 або AV1." - "• Меню звукової доріжки відсутнє. -• Стабілізація гучності недоступна." - "• Меню звукової доріжки відсутнє. -• Стабілізація гучності недоступна." - "• Фільми чи платні відео можуть не відтворюватися. -• Прямі трансляції починаються з початку. -• Відео можуть закінчуватися на 1 секунду раніше. -• Немає аудіокодека opus." - Побічні ефекти імітування - • Відео може не відтворюватися. - Клієнт, що використовується для отримання даних трансляції приховано у Статистика для сисадмінів. - Клієнт, що використовується для отримання даних трансляції показується у Статистика для сисадмінів. - Показувати в Статистика для сисадмінів - "Дані трансляції не підроблено. Відтворення відео може не працювати." - Дані трансляції підроблено. - Підробити дані трансляції - Android - Android TV - Android VR - iOS - Основний клієнт - Вимикання цього налаштування може призвести до проблем відтворення відео. - Чутливість жесту яскравості повинна бути в межах 1-1000 (%). - Налаштуйте мінімальну відстань жесту яскравості від 1 до 1000 (%).\nЧим менша мінімальна відстань, тим швидше змінюється рівень яскравості. - Чутливість жесту яскравості - Жести переміщення вимкнено в режимі \'Блокування екрана\'. - Жести переміщення увімкнено в режимі \'Блокування екрана\'. - Жести переміщення в режимі \'Блокування екрана\' - Авто - Мінімальна амплітуда руху, що розпізнається як жест - Поріг величини жесту - Видимість фону панелі при жесті - Видимість фону при жесті - Розмір площі для проведення не може бути більшим ніж 50. - Відсоток площі екрана для проведення.\n\nЗауваження: це також змінить розмір площі екрану для жесту перемотування подвійним натисканням. - Розмір екрана накладки проведення - Розмір шрифту в панелі при жесті - Розмір шрифту панелі - Скільки мілісекунд панель буде показуватися - Час показу панелі - Чутливість жесту гучності повинна бути в межах 1-1000 (%). - Налаштуйте мінімальну відстань жесту гучності від 1 до 1000 (%).\n\nЧим менша мінімальна відстань, тим швидше змінюється рівень гучності.\n\nРекомендована чутливість жесту гучності 100% при 15 поділках і 10% при 150 поділках. - Чутливість жесту гучності - "Поміняти місцями кнопку створення та кнопку сповіщення підробленням інформації про пристрій. - -• Навіть якщо ви зміните це налаштування, воно може не набути чинності, доки ви не перезапустите пристрій. -• Вимкнувши це налаштування, вантажиться більше реклами з боку сервера. -• Ви повинні вимкнути це налаштування, щоб зробити відеорекламу видимою." - Кнопку Створити не міняється з кнопкою Сповіщення. - "Кнопку Створити міняється з кнопкою Сповіщення. - -Примітка: Увімкнення також примусово приховує відеорекламу." - Поміняти Створити зі Сповіщення - "Вимкнення може спричинити вантаження більше реклами з сервера. - -А також, рекламу більше не блокуватиметься в Shorts. - -Якщо це налаштування не діє, спробуйте перемкнути Анонімний режим." - Стандартна - RVX Music - %s не встановлено. Будь ласка, встановіть. - Назва пакету встановленого RVX Music. - Назва пакету RVX Music - • Історію перегляду заблоковано. - "• Дотримується налаштувань історії перегляду облікового запису Google. -• Історія перегляду може не працювати через DNS або VPN." - • Дотримується налаштувань історії перегляду облікового запису Google. - Стан історії перегляду - Натисніть, щоб відкрити керування історією перегляду YouTube. - Керувати всією історією - Оригінал - Замінити домен - Блокувати історію перегляду - Тип історії перегляду - Не вдалося додати канал \'%1$s\' до білого списку %2$s. - Канал \'%1$s\' додано до білого списку %2$s. - Нема каналів у білому списку. - Не додано в білий список. - Не вдалося завантажити інформацію про канал. - Додано в білий список. - Швидкість відтворення - Вилучити канал \'%1$s\' з білого списку %2$s? - Не вдалося вилучити канал \'%1$s\' з білого списку %2$s. - Канал \'%1$s\' вилучено з білого списку %2$s. - Перевірити чи вилучити список каналів, доданих до білого списку. - Білий список каналів - Спонсорблок - diff --git a/src/main/resources/youtube/translations/vi-rVN/missing_strings.xml b/src/main/resources/youtube/translations/vi-rVN/missing_strings.xml deleted file mode 100644 index 43788e232..000000000 --- a/src/main/resources/youtube/translations/vi-rVN/missing_strings.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - Don\'t show again - Courses / Learning - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - Displays the optimization dialog for GMSCore at each application startup. - Show optimization dialog for GMSCore - AI-generated video summary section is shown. - AI-generated video summary section is hidden. - Hide AI-generated video summary section - MMT Blue - MMT Green - MMT Orange - MMT Pink - MMT Turquoise - MMT Yellow - Revancify Yellow - Vanced Black - Vanced Light - Xisr Yellow - Adjust: Mark Start and End Time for segment - Verify the Segment - Edit the Segment - Forward by Specified Time (Default: 150ms) - Publish Created Segment - Rewind by Specified Time (Default: 150ms) - diff --git a/src/main/resources/youtube/translations/vi-rVN/strings.xml b/src/main/resources/youtube/translations/vi-rVN/strings.xml deleted file mode 100644 index 3fbffb04a..000000000 --- a/src/main/resources/youtube/translations/vi-rVN/strings.xml +++ /dev/null @@ -1,1710 +0,0 @@ - - - Bật các điều khiển trợ năng cho trình phát video? - Các điều khiển của bạn đã được sửa đổi vì dịch vụ trợ năng đang bật. - Tiếp tục - "Hiện GmsCore không có quyền chạy nền. - -Hãy làm theo hướng dẫn của 'Don't kill my app!' và tiến hành cài đặt GmsCore đúng cách. - -Để ứng dụng hoạt động hiệu quả nhất." - "Vui lòng tắt tối ưu hoá pin cho GmsCore để tránh phát sinh lỗi. - -Nhấn vào Tiếp tục và tắt tối ưu hóa pin." - Mở trang web - Hành động cần thiết - Kích hoạt Cloud Messaging để nhận thông báo đẩy và các cài đặt khác. - Mở GmsCore - GmsCore chưa được cài đặt. Hãy cài đặt nó đi nào. - "DeArrow là một tiện ích được đóng góp bởi cồng đồng nhằm thay thế hình thu nhỏ mặc định của video YouTube bằng những hình thu nhỏ phù hợp hơn, giúp hạn chế clickbait. - -Nếu được bật, chỉ có URL video được gửi đến máy chủ API, ngoài ra không có bất kì dữ liệu nào khác được gửi đi. Nếu video không có hình thu nhỏ DeArrow, hình thu nhỏ gốc hoặc hình thu nhỏ tự động sẽ được hiển thị. - -Nhấn vào đây để tìm hiểu thêm về DeArrow." - Giới thiệu về DeArrow - URL của API DeArrow không hợp lệ. - URL của điểm cuối của bộ nhớ đệm hình thu nhỏ DeArrow. - Điểm cuối API DeArrow - Không hiện thông báo ngắn nếu API DeArrow không khả dụng. - Hiển thị thông báo ngắn nếu API DeArrow không khả dụng. - Thông báo ngắn nếu API không khả dụng - DeArrow tạm thời không khả dụng (Mã trạng thái: %s). - DeArrow tạm thời không khả dụng. - Thẻ Trang chủ - Thẻ Bạn - Hình thu nhỏ gốc - DeArrow & Hình thu nhỏ gốc - DeArrow & Hình thu nhỏ tự động - Hình thu nhỏ tự động - Danh sách phát của trình phát, đề xuất - Kết quả tìm kiếm - Hình thu nhỏ tự động - Hình thu nhỏ tự động là ảnh tĩnh ở đầu, giữa hoặc cuối video, được tạo tự động bởi YouTube và không sử dụng bất kỳ API bên ngoài nào. - Giới thiệu về Hình thu nhỏ tự động - Đang sử dụng ảnh tĩnh chất lượng cao làm hình thu nhỏ video. - Đang sử dụng ảnh tĩnh chất lượng trung bình làm hình thu nhỏ video. Hình thu nhỏ sẽ tải nhanh hơn, tuy nhiên các sự kiện trực tiếp, sắp diễn ra và video đã rất cũ có thể hiển thị hình thu nhỏ trống. - Hình thu nhỏ nhanh - Đầu video - Giữa video - Cuối video - Thời điểm để lấy ảnh tĩnh từ video - Thẻ Kênh đăng ký - Thông tin không còn được thêm vào dấu thời gian. - "Thông tin được thêm vào dấu thời gian." - Thêm thông tin vào dấu thời gian - Thêm Tốc độ phát. - Thêm Chất lượng video. - Loại thông tin cần thêm - Chế độ môi trường xung quanh sẽ bị vô hiệu hoá trong chế độ tiết kiệm pin. - Chế độ môi trường xung quanh vẫn được kích hoạt trong Chế độ tiết kiệm pin. - Không giới hạn Chế độ môi trường xung quanh - Tên miền để tìm nạp hình ảnh.\nLưu ý: Chỉ nhập tên miền, không có tiền tố \"https://\" - Tên miền thay thế - Sử dụng máy chủ lưu trữ hình ảnh gốc.\n\nBật tính năng này có thể khắc phục tình trạng hình ảnh bị chặn ở một số khu vực. - Sử dụng máy chủ hình ảnh yt4.ggpht.com. - Vượt qua hạn chế - Gốc - Điện thoại - Điện thoại -(Tối đa 480 dpi) - Máy tính bảng - Máy tính bảng -(Tối thiểu 600 dpi) - Thay đổi giao diện - Đang sử dụng kiểu bật/tắt tuỳ chọn dạng công tắc. - Đang sử dụng kiểu bật/tắt tuỳ chọn dạng văn bản. - Đổi kiểu bật/tắt tuỳ chọn - Sử dụng giao diện chia sẻ của ứng dụng. - Sử dụng giao diện chia sẻ của hệ thống. - Thay đổi giao diện chia sẻ - Tự động phát - Mặc định - Dừng - Lặp lại - Thay đổi trạng thái lặp lại video ngắn - Duyệt kênh - Mặc định - Khám phá - Trò chơi - Video đã xem - Bạn - Video đã thích - Trực tiếp - Phim - Âm nhạc - Tìm kiếm - Shorts - Thể thao - Kênh đăng ký - Thịnh hành - Xem sau - Thay đổi trang khởi động - Trang khởi động chỉ thay đổi một lần. - "Trang khởi động sẽ liên tục thay đổi. - -Hạn chế: Nút Quay lại trên thanh công cụ có thể không hoạt động." - Thay đổi kiểu trang khởi động - Tiêu đề Youtube mặc định. - Tiêu đề Premium được kích hoạt. - Thay đổi tiêu đề YouTube - Nhập tên các mục mà bạn muốn lọc được phân cách bằng dòng. - Cài đặt bộ lọc - Bộ lọc tuỳ chỉnh đã tắt. - Bộ lọc tuỳ chỉnh đã bật. - Bộ lọc tuỳ chỉnh - Bộ lọc tuỳ chỉnh không hợp lệ: %s. - Mục tốc độ phát kiểu cũ được sử dụng. - Mục tốc độ phát dạng hộp thoại được sử dụng. - Kiểu mục tốc độ phát tùy chỉnh - Tốc độ tùy chỉnh phải nhỏ hơn %sx. - Tốc độ phát tùy chỉnh không hợp lệ. - Thêm hoặc thay đổi tốc độ phát lại có sẵn. - Chỉnh sửa tốc độ phát - Độ mờ của lớp phủ trình phát phải nằm trong khoảng 0 - 100. - Giá trị độ mờ của lớp phủ trình phát trong khoảng từ 0 đến 100, trong đó 0 là trong suốt. - Độ mờ lớp phủ trình phát - Nhập mã màu hex của thanh tiến trình video mà bạn muốn thay đổi. - Thay đổi màu thanh tiến trình - Để mở liên kết YouTube trong RVX, hãy kích hoạt \'Mở các đường liên kết được hỗ trợ\' và thêm các đường liên kết được hỗ trợ. - Mở theo mặc định - Tốc độ phát mặc định - Chất lượng video mặc định trên mạng di động - Chất lượng video mặc định trên mạng Wi-Fi - Tắt chế độ môi trường xung quanh khi xem video ở chế độ toàn màn hình. - Chế độ môi trường xung quanh đã được kích hoạt ở chế độ toàn màn hình. - Chế độ môi trường xung quanh sẽ bị vô hiệu hoá ở chế độ toàn màn hình. - Tắt chế độ môi trường khi toàn màn hình - Luôn tắt chế độ môi trường xung quanh. - Chế độ môi trường xung quanh đã được kích hoạt. - Chế độ môi trường xung quanh đã được vô hiệu hoá. - Tắt chế độ môi trường xung quanh - Tự động phát bản âm thanh khi phát video có bản âm thanh đã bật. - Tự động phát bản âm thanh khi phát video có bản âm thanh đã tắt. - Tắt tự động phát bản âm thanh - Tự động hiển thị phụ đề khi phát video có phụ đề đã bật. - Tự động hiển thị phụ đề khi phát video có phụ đề đã tắt. - Tắt tự động hiển thị phụ đề - Bảng tự động bật lên khi phát video (Danh sách phát, Trò chuyện trực tiếp,...) đã bật. - Bảng tự động bật lên khi phát video (Danh sách phát, Trò chuyện trực tiếp,...) đã tắt. - Tắt bảng tự động bật lên khi phát - "Tự động chuyển sang danh sách kết hợp đã được kích hoạt khi bật tính năng Tự động phát. - -Tính năng Tự động phát có thể thay đổi trong Cài đặt YouTube: -Cài đặt → Tự động phát → Tự động phát video tiếp theo" - Tự động chuyển sang danh sách phát kết hợp đã bị vô hiệu hóa. - Vô hiệu hoá chuyển sang danh sách kết hợp - Việc bật tính năng này sẽ vô hiệu hóa việc tự động chuyển sang YouTube Mix khi phát nhạc đồng thời chế độ phát tự động cũng được bật. - Tốc độ phát mặc định được bật khi xem sự kiện trực tiếp và buổi công chiếu. - Tốc độ phát mặc định bị tắt khi xem sự kiện trực tiếp và buổi công chiếu. - Tắt tùy chọn tốc độ phát khi xem trực tiếp - Tốc độ phát mặc định đã được kích hoạt khi phát nhạc. - "Tốc độ phát mặc định đã bị vô hiệu hoá khi phát nhạc. - -Hạn chế: Cài đặt này có thể sẽ không áp dụng cho các video không bao gồm biểu ngữ 'Nghe nhạc trên YouTube Music'." - Tắt tùy chọn tốc độ phát khi phát nhạc - Bảng điều khiển tương tác đã được bật. - Bảng tương tác đã vô hiệu hóa. - Vô hiệu hóa bảng tương tác - Phản hồi xúc giác đã bật. - Phản hồi xúc giác đã tắt. - Tắt phản hồi xúc giác khi vuốt phân cảnh - Phản hồi xúc giác được bật. - Phản hồi xúc giác đã tắt. - Tắt phản hồi xúc giác khi đăng ký kênh - Phản hồi xúc giác được bật. - Phản hồi xúc giác đã tắt. - Tắt phản hồi xúc giác khi trượt để tua - Phản hồi xúc giác được bật. - Phản hồi xúc giác đã tắt. - Tắt phản hồi xúc giác khi huỷ tua - Phản hồi xúc giác được bật. - Phản hồi xúc giác đã tắt. - Tắt phản hồi xúc giác khi chụm để thu phóng - Độ sáng HDR tự động đã bật. - Độ sáng HDR tự động đã tắt. - Tắt độ sáng HDR tự động - Video HDR đã bật. - Video HDR đã tắt. - Tắt video HDR - Đang phát video ở chế độ toàn màn hình mặc định. - Đang phát video ở chế độ toàn màn hình dọc. - Xem ở chế độ toàn màn hình dọc - Các nút Thích và Không thích sẽ sáng lên khi được nhắc đến. - Các nút Thích và Không thích sẽ không sáng lên khi được nhắc đến. - Tắt hoạt ảnh các nút Thích và Không thích - "Vô hiệu hoá giao thức QUIC của CronetEngine để giảm độ trễ khi phát video." - Vô hiệu hoá giao thức QUIC - Trinh phát Shorts sẽ tiếp tục khi ứng dụng khởi chạy. - Trinh phát Shorts sẽ không tiếp tục khi ứng dụng khởi chạy. - Tắt tính năng tiếp tục phát video Shorts - Đã kích hoạt hoạt ảnh Số cuộn. - Đã vô hiệu hoá hoạt ảnh Số cuộn. - Vô hiệu hoá hoạt ảnh Số cuộn - Các phân cảnh đã được hiển thị trên thanh tiến trình. - Các phân cảnh đã bị ẩn trên thanh tiến trình. - Ẩn các phân cảnh trên thanh tiến trình - Đã kích hoạt hiệu ứng phun nước trên nút Thích. - Đã vô hiệu hoá hiệu ứng phun nước trên nút Thích. - Vô hiệu hoá hiệu ứng nút Thích - "Tắt tính năng nhấn và giữ để \"2x>>\". - -Lưu ý: -• Bật tùy chọn này sẽ khôi phục lại thao tác trượt để tua ở bố cục cũ. -• Tắt tùy chọn này cũng không ép buộc tính năng nhấn và giữ để tua nhanh 2x được bật trở lại. Hãy xoá dữ liệu ứng dụng." - Tắt nhấn và giữ để phát 2x>> - Đã kích hoạt hoạt ảnh khởi động. - Đã vô hiệu hoá hoạt ảnh khởi động. - Vô hiệu hoá hoạt ảnh khởi động - "Tắt các tương tác sau khi mô tả video được mở rộng: - - • Nhấn để cuộn. - • Nhấn và giữ để chọn văn bản." - Tắt tương tác mô tả video - Codec VP9 đã được kích hoạt. - "Codec VP9 đã bị vô hiệu hoá. - -• Độ phân giải tối đa là 1080p. -• Việc phát video sẽ sử dụng nhiều dữ liệu di động hơn so với VP9. -• Codec VP9 vẫn được sử dụng cho video HDR." - Vô hiệu hoá codec VP9 - Thanh tiến trình kiểu Cairo đã bị vô hiệu hoá. - "Thanh tiến trình kiểu Cairo đã được kích hoạt. - -Hạn chế: Chủ đề Cairo cũng sẽ áp dụng cho dấu chấm thông báo của ứng dụng." - Thanh tiến trình kiểu Cairo - Lớp phủ điều khiển trình phát thu gọn đã tắt. - Lớp phủ điều khiển trình phát thu gọn đã bật. - Lớp phủ điều khiển trình phát thu gọn - Đang áp dụng các giá trị tốc độ phát video mặc định. - Đang áp dụng các giá trị tốc độ phát video tùy chỉnh. - Tốc độ phát tùy chỉnh - Đang sử dụng màu thanh tiến trình video mặc định. - Đang sử dụng màu thanh tiến trình video tùy chỉnh. - Màu thanh tiến trình tùy chỉnh - Đang ghi nhật ký gỡ lỗi mà không có thông tin bộ đệm. - Đang ghi nhật ký gỡ lỗi bao gồm thông tin bộ đệm. - Nhật ký gỡ lỗi bộ đệm - Nhật ký gỡ lỗi đã tắt. - Nhật ký gỡ lỗi đã bật. - Nhật ký gỡ lỗi - Tốc độ phát mặc định không áp dụng cho Shorts. - Đang áp dụng tốc độ phát mặc định (bạn đã đặt) khi xem Shorts. - Tốc độ phát mặc định cho video ngắn - Đang mở các liên kết xuất hiện trên YouTube trong ứng dụng. - Đang mở các liên kết xuất hiện trên YouTube bằng trình duyệt bên ngoài. - Mở liên kết bằng trình duyệt bên ngoài - Đã vô hiệu hoá màn hình tải màu Gradient. - Đã kích hoạt màn hình tải màu Gradient. - Màn hình tải màu gradient - Khoảng cách giữa các nút trên thanh điều hướng về mặc định. - Khoảng cách giữa các nút trên thanh điều hướng sẽ hẹp hơn. - Thanh điều hướng thu gọn - Đang chuyển hướng URL khi mở các liên kết xuất hiện trên YouTube. - Đang bỏ qua chuyển hướng URL khi mở các liên kết xuất hiện trên YouTube. - Mở liên kết trực tiếp - Kích hoạt codec OPUS nếu phản hồi của trình phát bao gồm codec OPUS. - Kích hoạt Codec OPUS - Không lưu độ sáng khi thoát ra hoặc vào chế độ toàn màn hình. - Lưu độ sáng khi thoát ra hoặc vào chế độ toàn màn hình. - Lưu độ sáng - Chạm thanh tiến trình video để tua đã tắt. - Chạm thanh tiến trình video để tua đã bật. - Chạm thanh tiến trình để tua - "Tính năng này sẽ khôi phục hình thu nhỏ cho các video phát trực tiếp không có hình thu nhỏ trên thanh tiến trình. - -Nhưng điều này cũng sẽ làm tiêu tốn nhiều dữ liệu di động hơn, và hình thu nhỏ trên thanh tiến trình sẽ được hiển thị với một độ trễ nhất định. - -Vì vậy bạn nên bật tính năng này khi có kết nối mạng ổn định." - Hình thu nhỏ trên thanh tiến trình có chất lượng trung bình. - Hình thu nhỏ trên thanh tiến trình có chất lượng cao. - Hình thu nhỏ chất lượng cao - Dấu thời gian đã bị tắt. - "Dấu thời gian đã được bật. - -Hạn chế: -• Cài đặt này không chỉ bật Dấu thời gian mà còn cho phép ẩn giao diện người dùng bằng cách nhấn vào nền trình phát. -• Vì đây là tính năng đang trong giai đoạn phát triển của Google nên bố cục có thể bị lỗi." - Dấu thời gian - Đã vô hiệu hoá cử chỉ vuốt điều chỉnh độ sáng. - Đã kích hoạt cử chỉ vuốt điều chỉnh độ sáng. - Vuốt điều chỉnh độ sáng - Phản hồi xúc giác đã tắt. - Phản hồi xúc giác đã bật. - Phản hồi xúc giác - Chế độ độ sáng tự động sẽ không được bật khi vuốt độ sáng về mức tổi thiểu. - Chế độ độ sáng tự động sẽ được bật khi vuốt độ sáng về mức tổi thiểu. - Cử chỉ điều chỉnh độ sáng tự động - Nhấn và giữ để vuốt đã bị vô hiệu hoá. - Nhấn và giữ để vuốt đã được kích hoạt. - Nhấn và giữ để vuốt - Vuốt lên/xuống sẽ không phát video tiếp theo hoặc trước đó. - Vuốt lên/xuống sẽ phát video tiếp theo hoặc trước đó. - Vuốt để chuyển video - Đã vô hiệu hoá cử chỉ vuốt điều chỉnh âm lượng. - Đã kích hoạt cử chỉ vuốt điều chỉnh âm lượng. - Vuốt điều chỉnh âm lượng - Thanh điều hướng đã được hiển thị. - Thanh điều hướng đã được làm trong suốt. - Thanh điều hướng trong suốt - Đã vô hiệu hoá cử chỉ vuốt xuống từ khu vực bên dưới trình phát để xem ở chế độ toàn màn hình dọc. - Đã kích hoạt cử chỉ vuốt xuống từ khu vực bên dưới trình phát để xem ở chế độ toàn màn hình dọc. - Cử chỉ bên dưới trình phát - "Bật cài đặt này sẽ làm vô hiệu hoá nút Cài đặt trong thẻ Bạn. - -Trong trường hợp đó, vui lòng làm theo các bước sau để truy cập Cài đặt: -Thẻ Bạn → Xem kênh → Trình đơn → Cài đặt" - Thanh tìm kiếm rộng trên thẻ Bạn - Đang áp dụng thanh tìm kiếm mặc định. - Đang áp dụng thanh tìm kiếm rộng. - Thanh tìm kiếm rộng - Thanh tìm kiếm rộng sẽ ẩn tiêu đề YouTube. - Thanh tìm kiếm rộng đồng thời với tiêu đề YouTube. - Thanh tìm kiếm rộng với tiêu đề YouTube - Mô tả - "Nhập tiêu đề vào bảng mô tả video. -Mở rộng mô tả video có thể không hoạt động nếu bạn nhập nội dung không khớp với tiêu đề thực tế của bảng mô tả video." - Tiêu đề trong bảng mô tả video - Mô tả video được mở rộng thủ công. - Mô tả video được mở rộng tự động. - Mở rộng mô tả video - Bạn có muốn tiếp tục không? - Đặt lại về giá trị mặc định. - Vui lòng khởi động lại ứng dụng trong lần đầu khởi chạy để các tính năng hoạt động bình thường - "Hiện có một lỗi phía máy chủ YouTube khiến các văn bản dạng số cuộn như số lượt thích, số lượt xem và ngày tải lên bị ẩn đối với một số người dùng. - -Giải pháp tạm thời cho sự cố này là giả mạo phiên bản ứng dụng thành 19.13.37. - -Bạn có muốn giả mạo phiên bản ứng dụng trước khi khởi động lại ứng dụng không?" - Làm mới và khởi động lại - Xuất cài đặt thất bại. - Cài đặt đã được xuất thành công. - Xuất toàn bộ cài đặt của bạn dưới dạng tệp. - Xuất cài đặt - Nhập - Sao chép - Nhập hoặc xuất toàn bộ Cài đặt nâng cao của bạn dưới dạng văn bản. - Nhập/Xuất dưới dạng văn bản - Nhập cài đặt thất bại. - Đã đặt lại cài đặt về mặc định. - Cài đặt đã được nhập thành công. - Nhập toàn bộ cài đặt của bạn từ tệp đã lưu trước đó. - Nhập cài đặt - Đặt lại - Tìm kiếm trong %s - ReVanced Extended - Trình tải xuống bên ngoài - Chưa được cài đặt - "Có vẻ như %1$s chưa được cài đặt. - Vui lòng tải xuống %2$s từ trang web." - Chú ý - Hiện %s chưa được cài đặt. Hãy cài đặt và thử lại. - Nhập tên gói ứng dụng trình tải xuống đã cài đặt trên thiết bị của bạn, chẳng hạn như YTDLnis. - Tên gói trình tải xuống danh sách phát - Nhập tên gói ứng dụng trình tải xuống đã cài đặt trên thiết bị của bạn, chẳng hạn như NewPipe hoặc YTDLnis. - Tên gói trình tải xuống video - "Video sẽ chuyển sang chế độ toàn màn hình trong các trường hợp sau: - -• Khi video bắt đầu. -• Khi nhấn vào dấu thời gian trong phần bình luận." - Buộc áp dụng chế độ toàn màn hình - Nhập tên các mục thành phần của trình đơn Tài khoản mà bạn muốn lọc được phân cách bằng dòng. - Cài đặt bộ lọc - "Ẩn các thành phần của trình đơn Tài khoản và thẻ Bạn. -Một số thành phần có thể không bị ẩn." - Bộ lọc trình đơn Tài khoản - Đĩa nhạc được hiển thị trong kết quả tìm kiếm. - Đĩa nhạc đã ẩn khỏi kết quả tìm kiếm. - Ẩn thẻ album - Phần Địa điểm nổi bật, Trò chơi và Âm nhạc được hiển thị. - Phần Địa điểm nổi bật, Trò chơi và Âm nhạc đã ẩn. - Ẩn phần Thuộc tính - Bảng video tiếp theo được hiển thị ở màn hình kết thúc. - Bảng video tiếp theo đã ẩn khỏi màn hình kết thúc. - Ẩn bảng video tiếp theo - Đã hiện nút Chuyển đến cửa hàng. - Đã ẩn nút Chuyển đến cửa hàng. - Ẩn nút Chuyển đến cửa hàng - "Ẩn các kệ sau: - -• Tin nổi bật -• Tiếp tục xem -• Khám phá thêm kênh -• Nghe lại -• Mua sắm -• Xem lại" - Ẩn các kệ được cá nhân hoá - Thanh danh mục được hiển thị trong bảng tin. - Thanh danh mục đã ẩn trong bảng tin. - Ẩn trên bảng tin - Thanh danh mục được hiển thị khi lướt xem các video có liên quan. - Thanh danh mục đã ẩn khi lướt xem các video có liên quan. - Ẩn khi lướt xem các video có liên quan - Thanh danh mục được hiển thị trong kết quả tìm kiếm. - Thanh danh mục đã ẩn trong kết quả tìm kiếm. - Ẩn trong kết quả tìm kiếm - Các nhãn nguyên tắc (Nguyên tắc cộng đồng, Nguyên tắc của kênh, Trở thành hội viên,...) được hiển thị. - Các nhãn nguyên tắc (Nguyên tắc cộng đồng, Nguyên tắc của kênh, Trở thành hội viên,...) đã ẩn. - Ẩn các nhãn nguyên tắc - Kệ ghi nhận hội viên của kênh được hiển thị. - Kệ ghi nhận hội viên của kênh đã ẩn. - Ẩn kệ ghi nhận hội viên của kênh - Các đường liên kết ở đầu hồ sơ kênh được hiển thị. - Các đường liên kết ở đầu hồ sơ kênh đã ẩn. - Ẩn đường liên kết trên hồ sơ kênh - "Shorts -Danh sách phát -Cửa hàng" - Nhập tên các thẻ trên kênh mà bạn muốn lọc được phân cách bằng dòng. - Cài đặt bộ lọc - Bộ lọc thẻ trên kênh đã tắt. - Bộ lọc thẻ trên kênh đã bật. - Bộ lọc thẻ trên kênh - Hình mờ kênh được hiển thị. - Hình mờ của kênh đã ẩn. - Ẩn hình mờ của kênh - Phần Phân cảnh đã hiển thị. - Phần Phân cảnh đã ẩn. - Ẩn phần Phân cảnh - Kệ danh mục đề xuất được hiển thị. - Kệ danh mục đề xuất đã ẩn. - Ẩn kệ danh mục được đề xuất - Nút Tạo đoạn video được hiển thị. - Nút Tạo đoạn video đã ẩn. - Ẩn nút Tạo đoạn video - Đã hiện nút Tạo video ngắn. - Đã ẩn nút Tạo video ngắn. - Ẩn nút Tạo video ngắn - Các bình luận chữ xanh đã được hiển thị. - Các bình luận chữ xanh đã bị ẩn. - Ẩn các bình luận chữ xanh - Nút Cảm ơn được hiển thị. - Nút Cảm ơn đã ẩn. - Ẩn nút Cảm ơn - Nút dấu thời gian và các Biểu tượng cảm xúc được hiển thị. - Nút dấu thời gian và các Biểu tượng cảm xúc đã ẩn. - Ẩn nút dấu thời gian và các Biểu tượng cảm xúc - Biểu ngữ Bình luận của hội viên được hiển thị. - Biểu ngữ Bình luận của hội viên đã ẩn. - Ẩn biểu ngữ Bình luận của hội viên - Phần Bình luận được hiển thị trên thẻ Trang chủ. - Phần Bình luận đã ẩn trên thẻ Trang chủ. - Ẩn phần Bình luận trên thẻ Trang chủ - Phần Bình luận được hiển thị. - Phần Bình luận đã ẩn. - Ẩn phần Bình luận - Bài đăng cộng đồng được hiển thị trong hồ sơ kênh. - Bài đăng cộng đồng đã ẩn trong hồ sơ kênh. - Ẩn trong hồ sơ kênh - Bài đăng cộng đồng được hiển thị trên bảng tin và khi lướt xem các video có liên quan. - Bài đăng cộng đồng đã ẩn trên bảng tin và khi lướt xem các video có liên quan. - Ẩn trên thẻ Trang chủ và các video liên quan - Bài đăng cộng đồng được hiển thị trên thẻ Kênh đăng ký. - Bài đăng cộng đồng đã ẩn trên thẻ Kênh đăng ký. - Ẩn trên thẻ Kênh đăng ký - Phần \"Cách tạo Nội dung này\" đã được hiển thị. - Phần \"Cách tạo Nội dung này\" đã bị ẩn. - Ẩn phần Nội dung - Chiến dịch gây quỹ được hiển thị. - Chiến dịch gây quỹ đã ẩn. - Ẩn chiến dịch gây quỹ - Lớp phủ khi nhấn đúp để tua đã được hiển thị. - Lớp phủ khi nhấn đúp để tua đã bị ẩn. - Ẩn lớp phủ khi nhấn đúp để tua - Nút Tải xuống được hiển thị. - Nút Tải xuống đã ẩn. - Ẩn nút Tải xuống - Đã hiện các thẻ màn hình kết thúc. - Đã ẩn các thẻ màn hình kết thúc. - Ẩn các thẻ màn hình kết thúc - Bảng giới thiệu mở rộng được hiển thị bên dưới video. - Bảng giới thiệu mở rộng đã ẩn bên dưới video. - Ẩn bảng giới thiệu mở rộng - Kệ mở rộng đã hiển thị. - Kệ mở rộng đã ẩn. - Ẩn kệ mở rộng - Nút Phụ đề được hiển thị. - Nút Phụ đề đã ẩn. - Ẩn nút Phụ đề - Nhập tên các mục thành phần của trình đơn tuỳ chọn mà bạn muốn lọc được phân cách bằng dòng. - Cài đặt bộ lọc - Bộ lọc trình đơn tuỳ chọn trên bảng tin đã tắt. - Bộ lọc trình đơn tuỳ chọn trên bảng tin đã bật. - Bộ lọc trình đơn tuỳ chọn trên bảng tin - Thanh tìm kiếm được hiển thị. - Thanh tìm kiếm đã bị ẩn. - Ẩn thanh tìm kiếm - Khảo sát được hiển thị. - Khảo sát đã ẩn. - Ẩn khảo sát - Tua chính xác đã bật. - Tua chính xác đã tắt. - Tắt tua chính xác - Nút nổi như nút \"Điều chỉnh trang chủ của bạn\" đã được hiển thị. - Nút nổi như nút \"Điều chỉnh trang chủ của bạn\" đã bị ẩn. - Ẩn nút nổi - Đã ẩn nút Tìm kiếm bằng giọng nói. - Nút Tìm kiếm bằng giọng nói nổi đã ẩn khi tìm kiếm. - Ẩn nút Tìm kiếm bằng giọng nói - Kệ Dành cho bạn được hiển thị. - Kệ Dành cho bạn đã ẩn. - Ẩn kệ Dành cho bạn - Quảng cáo toàn màn hình được hiển thị. - Quảng cáo toàn màn hình đã ẩn. - Ẩn quảng cáo toàn màn hình - "Quảng cáo toàn màn hình bị chặn. - -Hạn chế: Hình ảnh của bài đăng cộng đồng ở chế độ toàn màn hình có thể bị chặn." - Đóng quảng cáo toàn màn hình bằng nút Đóng. - Đóng quảng cáo toàn màn hình - Quảng cáo chung được hiển thị. - Quảng cáo chung đã ẩn. - Ẩn quảng cáo chung - Quảng cáo YouTube Premium được hiển thị. - Quảng cáo YouTube Premium đã ẩn. - Ẩn quảng cáo YouTube Premium - Đã hiện dải phân cách màu xám. - Đã ẩn dải phân cách màu xám. - Ẩn dải phân cách màu xám - Tên hiển thị được hiển thị. - Tên hiển thị đã ẩn. - Ẩn tên hiển thị - Nút tìm kiếm bằng hình ảnh được hiển thị. - Nút tìm kiếm bằng hình ảnh đã bị ẩn. - Ẩn nút tìm kiếm bằng hình ảnh - Kệ Hình ảnh từ web được hiển thị trong kết quả tìm kiếm. - Kệ Hình ảnh từ web đã ẩn khỏi kết quả tìm kiếm. - Ẩn kệ Hình ảnh từ web - Phần thẻ thông tin được hiển thị. - Phần thẻ thông tin đã ẩn. - Ẩn phần thẻ thông tin - Thẻ thông tin được hiển thị. - Thẻ thông tin đã ẩn. - Ẩn thẻ thông tin - Bảng thông tin được hiển thị. - Bảng thông tin đã ẩn. - Ẩn bảng thông tin - Nút Tham gia/Xem đặc quyền được hiển thị. - Nút Tham gia/Xem đặc quyền đã ẩn. - Ẩn nút Tham gia/Xem đặc quyền - Phần Khái niệm chính được hiển thị. - Phần Khái niệm chính bị ẩn. - Ẩn phần Khái niệm chính - "Nội dung khớp với từ khoá bạn đã đặt sẽ bị ẩn trên thẻ Trang chủ/Kênh đăng ký và kết quả tìm kiếm. - - Hạn chế: -• Video ngắn sẽ không bị ẩn theo tên kênh. -• Một số thành phần giao diện người dùng có thể không bị ẩn. -• Tìm kiếm từ khoá có thể không cho kết quả nào." - Giới thiệu về lọc từ khoá - Việc đặt từ/cụm từ cần lọc trong dấu ngoặc kép sẽ ngăn chặn các kết quả chỉ trùng một phần với tiêu đề video và tên kênh.<br><br>Ví dụ,<br><b>\"ai\"</b> sẽ ẩn video: <b>How does AI work?</b><br>nhưng sẽ không ẩn: <b>What does fair use mean?</b> - Khớp toàn bộ từ - Các bình luận không được lọc theo từ khoá đã đặt. - Các bình luận đã được lọc theo từ khoá đã đặt. - Ẩn các bình luận theo từ khoá - Các Video trên thẻ Trang chủ không được lọc theo từ khoá đã đặt. - Các Video trên thẻ Trang chủ đã được lọc theo từ khoá đã đặt. - Ẩn video trên thẻ Trang chủ theo từ khoá - "Nhập từ hoặc cụm từ cần ẩn được phân cách bằng dòng. - -Từ khóa có thể là tên kênh hoặc bất kỳ văn bản nào hiển thị trong tiêu đề video. - -Bộ lọc có phân biệt chữ hoa chữ thường, vì vậy bạn cần nhập chính xác định dạng để lọc (Ví dụ: iPhone, TikTok, LeBlanc)." - Bộ lọc từ khoá - Kết quả tìm kiếm không được lọc theo từ khoá đã đặt. - Kết quả tìm kiếm đã được lọc theo từ khoá đã đặt. - Ẩn kết quả tìm kiếm theo từ khóa - Các Video trên thẻ Kênh đăng ký không được lọc theo từ khoá đã đặt. - Các Video trên thẻ Kênh đăng ký đã được lọc theo từ khoá đã đặt. - Ẩn video trên thẻ Kênh đăng ký theo từ khoá - Từ khóa sẽ ẩn tất cả video: %s. - Không thể sử dụng từ khoá: %s. - Hãy thêm dấu ngoặc kép để sử dụng từ khoá: %s. - Từ khóa có các định nghĩa mâu thuẫn với nhau. %s. - Từ khóa quá ngắn và cần phải có dấu ngoặc kép: %s. - Bài đăng mới nhất được hiển thị. - Bài đăng mới nhất đã ẩn. - Ẩn bài đăng mới nhất - Nút Video mới nhất được hiển thị. - Nút Video mới nhất đã ẩn. - Ẩn nút Video mới nhất - Các nút Thích và Không thích được hiển thị. - Các nút Thích và Không thích đã ẩn. - Ẩn các nút Thích và Không thích - Tin nhắn trò chuyện trực tiếp được hiển thị.\n\nTuỳ chọn này cũng áp dụng cho video trực tiếp trên Shorts. - Tin nhắn trò chuyện trực tiếp đã ẩn.\n\nTuỳ chọn này cũng áp dụng cho video trực tiếp trên Shorts. - Ẩn tin nhắn Trò chuyện trực tiếp - Nút phát lại trò chuyện trực tiếp đã được hiển thị.\n\nNó sẽ xuất hiện ở chế độ toàn màn hình khi bạn đóng trò chuyện trực tiếp. - Nút phát lại trò chuyện trực tiếp đã bị ẩn.\n\nNó sẽ xuất hiện ở chế độ toàn màn hình khi bạn đóng trò chuyện trực tiếp. - Ẩn nút Trò chuyện trực tiếp - Ẩn các Video có dưới 1.000 lượt xem từ các kênh chưa đăng ký khỏi thẻ Trang chủ. - Ẩn video có lượt xem thấp - Bảng thông tin y tế được hiển thị. - Bảng thông tin y tế đã ẩn. - Ẩn bảng thông tin y tế - Kệ Sản phẩm được hiển thị. - Kệ Sản phẩm đã ẩn. - Ẩn kệ Sản phẩm - Danh sách kết hợp được hiển thị. - Danh sách kết hợp đã ẩn. - Ẩn Danh sách kết hợp - Kệ phim và chương trình truyền hình được hiển thị. - Kệ phim và chương trình truyền hình đã ẩn. - Ẩn kệ Phim và chương trình truyền hình - Thanh điều hướng đã được hiển thị. - Thanh điều hướng đã bị ẩn. - Ẩn Thanh điều hướng - Đã hiện nút Tạo. - Đã ẩn nút Tạo. - Ẩn nút Tạo - Nút Trang chủ được hiển thị. - Nút Trang chủ đã ẩn. - Ẩn nút Trang chủ - Đã hiện tên các thẻ. - Đã ẩn tên các thẻ. - Ẩn tên các thẻ - Nút Bạn được hiển thị. - Nút Bạn đã ẩn. - Ẩn nút Bạn - Đã hiện nút Thông báo. - Đã ẩn nút Thông báo. - Ẩn nút Thông báo - Đã hiện nút Shorts. - Đã ẩn nút Shorts. - Ẩn nút Shorts - Nút Kênh đăng ký được hiển thị. - Nút Kênh đăng ký đã ẩn. - Ẩn nút Kênh đăng ký - Nút Thông báo cho tôi được hiển thị bên dưới video sắp diễn ra. - Nút Thông báo cho tôi đã ẩn bên dưới video sắp diễn ra. - Ẩn nút Thông báo cho tôi - Nhãn Nội dung được trả tiền để quảng cáo được hiển thị. - Nhãn Nội dung được trả tiền để quảng cáo đã ẩn. - Ẩn nhãn quảng cáo được tài trợ - Kệ Chơi game trên YouTube được hiển thị. - Kệ Chơi game trên YouTube đã ẩn. - Ẩn kệ Chơi game trên YouTube - Nút Tự động phát được hiển thị. - Nút Tự động phát đã ẩn. - Ẩn nút Tự động phát - Nút Phụ đề được hiển thị. - Nút Phụ đề đã ẩn. - Ẩn nút Phụ đề - Nút Truyền được hiển thị. - Nút Truyền đã ẩn. - Ẩn nút Truyền - Nút Thu gọn được hiển thị. - Nút Thu gọn đã ẩn. - Ẩn nút Thu gọn - Mục Chế độ môi trường xung quanh được hiển thị. - Mục Chế độ môi trường xung quanh đã ẩn. - Ẩn mục Chế độ môi trường xung quanh - Mục Bản âm thanh được hiển thị. - Mục Bản âm thanh đã ẩn. - Ẩn mục Bản âm thanh - Ghi chú cuối mục Phụ đề được hiển thị. - Ghi chú cuối mục Phụ đề đã ẩn. - Ẩn ghi chú cuối mục Phụ đề - Mục Phụ đề được hiển thị. - Mục Phụ đề đã ẩn. - Ẩn mục Phụ đề - Mục 1080p Premium đã hiển thị. - Mục 1080p Premium đã ẩn. - Ẩn mục 1080p Premium - Mục Trợ giúp & phản hồi được hiển thị. - Mục Trợ giúp & phản hồi đã ẩn. - Ẩn mục Trợ giúp & phản hồi - Mục Nghe trên YouTube Music được hiển thị. - Mục Nghe trên YouTube Music đã bị ẩn. - Ẩn mục Nghe trên YouTube Music - Mục Khoá màn hình được hiển thị. - Mục Khoá màn hình đã ẩn. - Ẩn mục Khoá màn hình - Mục lặp lại video được hiển thị. - Mục lặp lại video đã ẩn. - Ẩn mục Cho video lặp lại - Mục Nội dung khác từ kênh được hiển thị. - Mục Nội dung khác từ kênh đã ẩn. - Ẩn mục Nội dung khác từ kênh - Mục hình trong hình được hiển thị. - Mục hình trong hình đã ẩn. - Ẩn mục Hình trong hình - Mục Tốc độ phát được hiển thị. - Mục Tốc độ phát đã ẩn. - Ẩn mục Tốc độ phát - Mục Nút điều khiển cho gói Premium được hiển thị. - Mục Nút điều khiển cho gói Premium đã ẩn. - Ẩn mục Nút điều khiển cho gói Premium - Ghi chú cuối mục Chất lượng video hiện tại được hiển thị. - Ghi chú cuối mục Chất lượng video hiện tại đã ẩn. - Ẩn ghi chú cuối mục Chất lượng - Tiêu đề mục Chất lượng được hiển thị. - Tiêu đề mục Chất lượng đã ẩn. - Ẩn tiêu đề mục Chất lượng - Mục Báo vi phạm được hiển thị. - Mục Báo vi phạm đã ẩn. - Ẩn mục Báo cáo - Mục Hẹn giờ ngủ đã hiển thị. - Mục Hẹn giờ ngủ đã ẩn. - Ẩn mục Hẹn giờ ngủ - Mục Âm lượng ổn định được hiển thị. - Mục Âm lượng ổn định đã ẩn. - Ẩn mục Âm lượng ổn định - Mục Thống kê chi tiết được hiển thị. - Mục Thống kê chi tiết đã ẩn. - Ẩn mục Thống kê chi tiết - Mục Xem ở chế độ thực tế ảo được hiển thị. - Mục Xem ở chế độ thực tế ảo đã ẩn. - Ẩn mục Xem ở chế độ thực tế ảo - Nút Toàn màn hình được hiển thị. - Nút Toàn màn hình đã ẩn. - Ẩn nút Toàn màn hình - Các nút được hiển thị. - Các nút đã ẩn. - Ẩn các nút Chuyển đến video trước đó/tiếp theo - Đã hiện kệ cửa hàng. - Đã ẩn kệ cửa hàng. - Ẩn kệ cửa hàng bên dưới trình phát - Nút YouTube Music được hiển thị. - Nút YouTube Music đã ẩn. - Ẩn nút YouTube Music - Nút Lưu video vào danh sách phát được hiển thị. - Nút Lưu video vào danh sách phát đã ẩn. - Ẩn nút Lưu - Phần Khám phá podcast được hiển thị. - Phần Khám phá podcast đã ẩn. - Ẩn phần Khám phá podcast - Phần Xem trước bình luận được hiển thị. - Phần Xem trước bình luận đã ẩn. - Ẩn phần Xem trước bình luận - Tuỳ chọn này làm thay đổi kích thước của phần Bình luận, khiến bạn không thể mở Phát lại cuộc trò chuyện trực tiếp trong phần Bình luận. - Tuỳ chọn này không làm thay đổi kích thước của phần Bình luận, vì vậy có thể mở Phát lại cuộc trò chuyện trực tiếp trong phần Bình luận. - Ẩn nội dung bình luận - Biểu ngữ thông báo khuyến mãi đã hiển thị. - Biểu ngữ thông báo khuyến mãi đã ẩn. - Ẩn biểu ngữ thông báo khuyến mãi - Nút Bình luận được hiển thị. - Nút Bình luận đã ẩn. - Ẩn nút Bình luận - Nút Không thích đã được hiển thị. - Nút Không thích đã bị ẩn. - Ẩn nút Không thích - Nút Thích đã hiển thị. - Nút Thích đã ẩn. - Ẩn nút Thích - Nút trò chuyện trực tiếp được hiển thị. - Nút trò chuyện trực tiếp đã ẩn. - Ẩn nút Trò chuyện trực tiếp - Nút thêm (...) đã hiển thị. - Nút thêm (...) đã ẩn. - Ẩn nút thêm - Nút Danh sách kết hợp được hiển thị. - Nút Danh sách kết hợp đã ẩn. - Ẩn nút Danh sách kết hợp - Nút Danh sách phát được hiển thị. - Nút Danh sách phát đã ẩn. - Ẩn nút Danh sách phát - Nút Lưu được hiển thị. - Nút Lưu đã bị ẩn. - Ẩn nút Lưu - Nút Chia sẻ được hiển thị. - Nút Chia sẻ đã ẩn. - Ẩn nút Chia sẻ - Bảng nút thao tác nhanh được hiển thị. - Bảng nút thao tác nhanh đã ẩn. - Ẩn bảng nút thao tác nhanh - "Ẩn các video đề xuất sau: - -• Video có nhãn \"Chỉ dành cho hội viên\". -• Video có cụm từ như \"Mọi người cũng xem video này\" ở bên dưới hình thu nhỏ." - Ẩn video đề xuất - Phần video thêm trong bảng nút thao tác nhanh và lớp phủ video liên quan đã được hiển thị. - Phần video thêm trong bảng nút thao tác nhanh và lớp phủ video liên quan đã bị ẩn. - Ẩn lớp phủ video liên quan - Các Video có liên quan được hiển thị. - Các video có liên quan đã bị ẩn. - Ẩn các video có liên quan - "Cài đặt này giới hạn số lượng bố cục tối đa có thể được tải trên màn hình trình phát. - -Nếu bố cục của màn hình trình phát thay đổi do các thay đổi từ phía máy chủ, các bố cục không mong muốn có thể bị ẩn trên màn hình trình phát." - Nút Phối lại được hiển thị. - Nút Phối lại đã ẩn. - Ẩn nút Phối lại - Nút Báo vi phạm được hiển thị. - Nút Báo vi phạm đã ẩn. - Ẩn nút Báo vi phạm - Nút Quà thưởng được hiển thị. - Nút Quà thưởng đã ẩn. - Ẩn nút Quà thưởng - Hình thu nhỏ của từ khoá tìm kiếm được hiển thị trong lịch sử tìm kiếm. - Hình thu nhỏ của từ khoá tìm kiếm đã ẩn khỏi lịch sử tìm kiếm. - Ẩn hình thu nhỏ của từ khoá tìm kiếm - Thông báo \"Trượt sang trái hoặc phải để tua\" được hiển thị. - Thông báo \"Trượt sang trái hoặc phải để tua\" đã ẩn. - Ẩn thông báo khi trượt để tua - Thông báo \"Thả ra để huỷ\" được hiển thị. - Thông báo \"Thả ra để huỷ\" đã ẩn. - Ẩn thông báo khi huỷ tua - Tên phân cảnh kế bên dấu thời gian đã hiển thị. - Tên phân cảnh kế bên dấu thời gian đã ẩn. - Ẩn tên phân cảnh trên thanh tiến trình - Thanh tiến trình video được hiển thị trong trình phát. - Thanh tiến trình video đã ẩn khỏi trình phát. - Thanh tiến trình được hiển thị trong trình phát thu nhỏ video. - Thanh tiến trình đã ẩn khỏi trình phát thu nhỏ video. - Ẩn thanh tiến trình trong trình phát thu nhỏ - Ẩn thanh tiến trình trong trình phát - Thẻ được tài trợ được hiển thị. - Thẻ được tài trợ đã ẩn. - Ẩn thẻ được tài trợ - Đã hiện mục Hỗ trợ tiếp cận. - Đã ẩn mục Giới thiệu. - Ẩn mục Giới thiệu - Đã hiện mục Hỗ trợ tiếp cận. - Đã ẩn mục Hỗ trợ tiếp cận. - Ẩn mục Hỗ trợ tiếp cận - Đã hiện mục Tài khoản. - Đã ẩn mục Tài khoản. - Ẩn mục Tài khoản - Đã hiện mục Tự động phát. - Đã ẩn mục Tự động phát. - Ẩn mục Tự động phát - Đã hiện mục Lập hoá đơn và thanh toán. - Đã ẩn mục Lập hoá đơn và thanh toán. - Ẩn mục Lập hoá đơn và thanh toán - Đã hiện mục Phụ đề. - Đã ẩn mục Phụ đề. - Ẩn mục Phụ đề - Đã hiện mục Ứng dụng đã kết nối. - Đã ẩn mục Ứng dụng đã kết nối. - Ẩn mục Ứng dụng đã kết nối - Đã hiện mục Tiết kiệm dữ liệu. - Đã ẩn mục Tiết kiệm dữ liệu. - Ẩn mục Tiết kiệm dữ liệu - Đã hiện mục Chung. - Đã ẩn mục Chung. - Ẩn mục Chung - Đã hiện mục Quản lý toàn bộ nhật ký hoạt động. - Đã ẩn mục Quản lý toàn bộ nhật ký hoạt động. - Ẩn mục Quản lý toàn bộ nhật ký hoạt động - Đã hiện mục Trò chuyện trực tiếp. - Đã ẩn mục Trò chuyện trực tiếp. - Ẩn mục Trò chuyện trực tiếp - Đã hiện mục Thông báo. - Đã ẩn mục Thông báo. - Ẩn mục Thông báo - Đã hiện mục Phát trong nền và nội dung tải xuống. - Đã ẩn mục Phát trong nền và nội dung tải xuống. - Ẩn mục Phát trong nền và nội dung tải xuống - Đã hiện mục Xem trên TV. - Đã ẩn mục Xem trên TV. - Ẩn mục Xem trên TV - Đã hiện mục Trung tâm dành cho gia đình. - Đã ẩn mục Trung tâm dành cho gia đình. - Ẩn mục Trung tâm dành cho gia đình - Đã hiện mục Thử nghiệm các tính năng mới. - Đã ẩn mục Thử nghiệm các tính năng mới. - Ẩn mục Thử nghiệm các tính năng mới - Đã hiện mục Quyền riêng tư. - Đã ẩn mục Quyền riêng tư. - Ẩn mục Quyền riêng tư - Đã hiện mục Giao dịch mua và gói thành viên. - Đã ẩn mục Giao dịch mua và gói thành viên. - Ẩn mục Giao dịch mua và gói thành viên - Ẩn các thành phần trong trình đơn Cài đặt YouTube. - Ẩn trình đơn Cài đặt YouTube - Đã hiện mục Lựa chọn ưu tiên về chất lượng video. - Đã ẩn mục Lựa chọn ưu tiên về chất lượng video. - Ẩn mục Lựa chọn ưu tiên về chất lượng video - Đã hiện mục Dữ liệu của bạn trong Youtube. - Đã ẩn mục Dữ liệu của bạn trong Youtube. - Ẩn mục Dữ liệu của bạn trong Youtube - Nút Chia sẻ được hiển thị. - Nút Chia sẻ đã ẩn. - Ẩn nút Chia sẻ - Nút Cửa hàng được hiển thị. - Nút Cửa hàng đã ẩn. - Ẩn nút Cửa hàng - Phần Sản phẩm được hiển thị. - Phần Sản phẩm đã ẩn. - Ẩn phần Sản phẩm - Đã hiện Thanh kênh. - Đã ẩn Thanh kênh. - Ẩn Thanh kênh - Đã hiện nút Bình luận. - Đã ẩn nút Bình luận. - Ẩn nút Bình luận - Nút Bình luận đã tắt hoặc nhãn \"0\" được hiển thị. - Nút Bình luận đã tắt hoặc nhãn \"0\" đã ẩn. - Ẩn nút Đóng bình luận - Đã hiện nút Không thích. - Đã ẩn nút Không thích. - Ẩn nút Không thích - "Các nút nổi như 'Dùng âm thanh này' được hiển thị trong thẻ kênh Shorts." - "Các nút nổi như 'Dùng âm thanh này' đã ẩn trong thẻ kênh Shorts." - Ẩn nút nổi - Đã hiện nhãn Liên kết video. - Đã ẩn nhãn Liên kết video. - Ẩn nhãn Liên kết toàn video - Đã hiện nút Phông xanh. - Đã ẩn nút Phông xanh. - Ẩn nút Phông xanh - Đã hiện Bảng thông tin. - Đã ẩn Bảng thông tin. - Ẩn Bảng thông tin - Đã hiện nút Tham gia. - Đã ẩn nút Tham gia. - Ẩn nút Tham gia - Đã hiện nút Thích. - Đã ẩn nút Thích. - Ẩn nút Thích - Đã hiện tiêu đề Trò chuyện trực tiếp.\n\nNút Quay lại trong tiêu đề sẽ không bị ẩn. - Đã ẩn tiêu đề Trò chuyện trực tiếp.\n\nNút Quay lại trong tiêu đề sẽ không bị ẩn. - Ẩn tiêu đề Trò chuyện trực tiếp - Đã hiện nút Vị trí. - Đã ẩn nút Vị trí. - Ẩn nút Vị trí - Đã hiện Thanh điều hướng. - Đã ẩn Thanh điều hướng. - Ẩn Thanh điều hướng - Đã hiện Nhãn quảng cáo được tài trợ. - Đã ẩn Nhãn quảng cáo được tài trợ. - Ẩn nhãn quảng cáo được tài trợ - Đã hiện Tiêu đề tạm dừng. - Đã ẩn Tiêu đề tạm dừng. - Ẩn tiêu đề tạm dừng - Đã hiện các nút phủ lên khi tạm dừng. - Đã ẩn các nút phủ lên khi tạm dừng. - Ẩn các nút phủ lên khi tạm dừng - Đã hiển thị nền của nút. - Đã ẩn nền của nút. - Ẩn nền các nút Phát & Tạm dừng - Đã hiện nút Phối lại. - Đã ẩn nút Phối lại. - Ẩn nút Phối lại - Đã hiện nút lưu nhạc. - Đã ẩn nút Lưu nhạc. - Ẩn nút Lưu nhạc - Đã hiện nút Gợi ý tìm kiếm. - Đã ẩn nút Gợi ý tìm kiếm. - Ẩn nút Gợi ý tìm kiếm - Đã hiện nút Chia sẻ. - Đã ẩn nút Chia sẻ. - Ẩn nút Chia sẻ - Đã hiện trong hồ sơ kênh. - "Đã ẩn trong hồ sơ kênh. - -Cụ thể: -• Chỉ những kệ có tiêu đề Shorts trên thẻ trang chủ mới bị ẩn." - Ẩn trong hồ sơ kênh - Hiển thị trong phần Nhật ký xem. - Ẩn trong phần Nhật ký xem. - Ẩn trong phần Nhật ký xem - Hiển thị trong thẻ Trang chủ và các video có liên quan. - Ẩn trên thẻ Trang chủ và các video liên quan. - Ẩn trên thẻ Trang chủ và các video liên quan - Hiển thị trong kết quả tìm kiếm. - Ẩn trong kết quả tìm kiếm. - Ẩn trong kết quả tìm kiếm - Hiển thị trong thẻ Kênh đăng ký. - Ẩn trong thẻ Kênh đăng ký. - Ẩn trong thẻ Kênh đăng ký - "\nHạn chế: Tiêu đề chính thức trong kết quả tìm kiếm sẽ được ẩn." - Ẩn kệ Shorts - Đã hiện nút Cửa hàng. - Đã ẩn nút Cửa hàng. - Nút Cửa hàng - Đã hiện nút Mua sắm. - Đã ẩn nút Mua sắm. - Ẩn nút Mua sắm - Đã hiện nút Âm thanh. - Đã ẩn nút Âm thanh. - Nút Âm thanh - Đã hiện nhãn Siêu dữ liệu. - Đã ẩn nhãn Siêu dữ liệu. - Ẩn nhãn Siêu dữ liệu âm thanh - Đã hiện nhãn dán. - Đã ẩn nhãn dán. - Ẩn nhãn dán - Đã hiện nút Đăng ký. - Đã ẩn nút Đăng ký. - Ẩn nút Đăng ký - Đã hiện nút Super Thanks. - Đã ẩn nút Super Thanks. - Ẩn nút Super Thanks - Đã hiện Sản phẩm được gắn thẻ. - Đã ẩn Sản phẩm được gắn thẻ. - Ẩn sản phẩm được gắn thẻ - Đã hiện Thanh công cụ. - Đã ẩn Thanh công cụ. - Ẩn thanh công cụ - Đã hiện nút Thịnh hành. - Đã ẩn nút Thịnh hành. - Ẩn nút Thịnh hành - Đã hiện nút Sử dụng mẫu. - Đã ẩn nút Sử dụng mẫu. - Ẩn nút Sử dụng mẫu - Đã hiện nút Dùng âm thanh này. - Đã ẩn nút Dùng âm thanh này. - Ẩn nút Dùng âm thanh này - Đã hiện Tiêu đề. - Đã ẩn Tiêu đề. - Ẩn Tiêu đề video - Nút Hiện thêm được hiển thị. - Nút Hiện thêm đã ẩn. - Ẩn nút Hiện thêm - Thanh thông báo nhanh được hiển thị. - Thanh thông báo nhanh đã ẩn. - Ẩn thanh thông báo nhanh - Nút mua Kênh Primetime (Bắt đầu dùng thử/Dùng thử miễn phí/Đăng ký/Mua Sunday Ticket,...) được hiển thị. - Nút mua Kênh Primetime (Bắt đầu dùng thử/Dùng thử miễn phí/Đăng ký/Mua Sunday Ticket,...) đã ẩn. - Ẩn nút mua Kênh Primetime - Danh sách cuộn Kênh đăng ký đã hiển thị. - Danh sách cuộn Kênh đăng ký đã ẩn. - Ẩn danh sách cuộn Kênh đăng ký - Hành động đề xuất được hiển thị. - Hành động đề xuất đã ẩn. - Ẩn hành động đề xuất - "This setting has been deprecated. - -Instead, use the 'Settings → Autoplay → Autoplay next video' setting. - -Note: -• If you have any issues with 'Suggested video end screen', try restarting the app." - Video đề xuất ở màn hình kết thúc được hiển thị. - "Video đề xuất ở màn hình kết thúc đã ẩn khi tắt tính năng Tự động phát. - -Tính năng Tự động phát có thể thay đổi trong Cài đặt YouTube: -Cài đặt → Tự động phát → Tự động phát video tiếp theo." - Ẩn video đề xuất ở màn hình kết thúc - Nút Cảm ơn được hiển thị. - Nút Cảm ơn đã ẩn. - Ẩn nút Cảm ơn - Kệ bán vé được hiển thị. - Kệ bán vé đã ẩn. - Ẩn kệ bán vé - Dấu thời gian được hiển thị. - Dấu thời gian đã ẩn. - Ẩn Dấu thời gian - Phản ứng theo thời gian được hiển thị. - Phản ứng theo thời gian đã ẩn. - Ẩn phản ứng theo thời gian - Nút Truyền được hiển thị. - Nút Truyền đã ẩn. - Ẩn nút Truyền - Nút Tạo được hiển thị. - Nút Tạo đã ẩn. - Ẩn nút Tạo - Nút Thông báo được hiển thị. - Nút Thông báo đã ẩn. - Ẩn nút Thông báo - Phần Bản chép lời được hiển thị. - Phần Bản chép lời đã ẩn. - Ẩn phần Bản chép lời - Quảng cáo dạng video được hiển thị. - Quảng cáo dạng video đã ẩn. - Ẩn quảng cáo dạng video - "Các Video có số lượt xem ít hoặc nhiều hơn con số bạn đã đặt sẽ bị ẩn trên thẻ Trang chủ/Kênh đăng ký/Kết quả tìm kiếm. - -Hạn chế: -• Không ẩn đối với Video ngắn. -• Các Video có 0 lượt xem cũng không bị lọc." - Về việc lọc theo số lượt xem - Các Video trên thẻ Trang chủ không được lọc theo số lượt xem đã đặt. - Các Video trên thẻ Trang chủ đã được lọc theo số lượt xem đã đặt. - Ẩn video trên thẻ Trang chủ theo số lượt xem - Kết quả tìm kiếm không được lọc theo số lượt xem đã đặt. - Kết quả tìm kiếm đã được lọc theo số lượt xem đã đặt. - Ẩn kết quả tìm kiếm theo lượt xem - Các Video trên thẻ Kênh đăng ký không được lọc theo số lượt xem đã đặt. - Các Video trên thẻ Kênh đăng ký đã được lọc theo số lượt xem đã đặt. - Ẩn video trên thẻ Kênh đăng ký theo số lượt xem - Ẩn các video đề xuất có số lượt xem ít hơn số lượt xem bạn đã đặt.\n\nSự cố đã biết: Các video chưa có lượt xem nào sẽ không bị lọc. - Ẩn video được đề xuất theo số lượt xem - Nhập số lượt xem. Video có số lượt xem cao hơn mức này sẽ bị ẩn. - Cao hơn - Nhập số lượt xem. Video có số lượt xem thấp hơn mức này sẽ bị ẩn. - Thấp hơn - N -> 1 000\nTr -> 1 000 000\nT -> 1 000 000 000\nlượt xem -> lượt xem - Nhập kí tự đại diện cho số lượt xem được hiển thị bên dưới video theo mẫu ngôn ngữ của bạn. Mỗi kí tự đại diện -> Số lượt xem tương ứng và được phân cách bằng dòng. Nếu bạn thay đổi ngôn ngữ ứng dụng hoặc hệ thống, bạn cần phải đặt lại tuỳ chọn này.\n\nVí dụ:\n•Tiếng Việt: 10 N lượt xem = N -> 1000, lượt xem -> lượt xem.\n•Tiếng Anh: 10K views = K -> 1000, views -> lượt xem. - Ký tự đại diện số lượt xem - Nhãn Xem sản phẩm được hiển thị. - Nhãn Xem sản phẩm đã ẩn. - Ẩn nhãn Xem sản phẩm - Đã hiện nút Tìm kiếm bằng giọng nói. - Đã ẩn nút Tìm kiếm bằng giọng nói. - Ẩn nút Tìm kiếm bằng giọng nói - Kết quả tìm kiếm từ web được hiển thị. - Kết quả tìm kiếm từ web đã ẩn. - Ẩn kết quả tìm kiếm từ web - YouTube Doodles đã được hiển thị. - YouTube Doodles đã bị ẩn. - Ẩn YouTube Doodles - "YouTube Doodles là những hình ảnh hoặc thiết kế cách điệu được YouTube sử dụng tạm thời trên logo của mình trong một số dịp đặc biệt, tương tự như Google Doodles trên trang chủ của Google. Và chúng thường chỉ xuất hiện trong một khoảng thời gian ngắn, có thể là vài ngày mỗi năm. - -Nếu YouTube Doodle đang hiển thị đồng thời tuỳ chọn ẩn này cũng đang bật, thì bộ lọc tìm kiếm cũng sẽ bị ẩn." - Lớp phủ khi chụm để thu phóng đã được hiển thị. - Lớp phủ khi chụm để thu phóng đã bị ẩn. - Ẩn lớp phủ khi chụm để thu phóng - AFN Blue - AFN Red - Tùy chỉnh - Stock - MMT - Revancify Blue - Revancify Red - YouTube - Giữ chế độ toàn màn hình hoạt động trong lúc bạn tắt và đánh thức thiết bị khi đang xem chế độ toàn màn hình. - Số mili giây mà chế độ toàn màn hình được giữ. - Thời gian giữ chế độ toàn màn hình - Giữ chế độ toàn màn hình - Nguyên gốc - Đã vô hiệu hoá thao tác nhấn đúp. - "Đã kích hoạt thao tác nhấn đúp. - -• Nhấn đúp để phóng to video đang thu nhỏ. -• Nhấn đúp một lần nữa để trả về kích thước ban đầu." - Thao tác nhấn đúp - Đã vô hiệu kéo và thả. - Đã kích hoạt kéo và thả. - Kéo và thả - Đã hiện các nút Mở rộng và Đóng. - Đã ẩn các nút.\n(vuốt trình phát thu nhỏ để mở rộng hoặc đóng) - Ẩn các nút Mở rộng và Đóng - Đã hiện các nút tua tới và tua lùi. - Đã ẩn các nút tua tới và tua lùi. - Ẩn các nút tua tới và tua lùi - Đã hiện các văn bản phụ. - Đã ẩn các văn bản phụ. - Ẩn các văn bản phụ - Độ mờ của lớp phủ trình phát thu nhỏ phải nằm trong khoảng 0 - 100. - Giá trị độ mờ của lớp phủ trình phát thu nhỏ trong khoảng từ 0 đến 100, trong đó 0 là trong suốt. - Độ mờ lớp phủ - Gốc - Điện thoại - Máy tính bảng - Hiện đại 1 - Hiện đại 2 - Hiện đại 3 - Loại trình phát thu nhỏ - Nút trên lớp phủ trình phát - "Nhấn để luôn phát lặp lại video. -Nhấn giữ để tạm dừng sau khi hết thời lượng video." - Nút Phát lặp lại một video - "Nhấn để sao chép URL video. -Nhấn giữ để sao chép URL video kèm theo dấu thời gian hiện tại." - "Nhấn để sao chép URL video với dấu thời gian. -Nhấn và giữ để sao chép dấu thời gian hiện tại." - Nút Sao chép URL video với dấu thời gian - Nút Sao chép URL video - Nhấn để khởi chạy trình tải xuống bên ngoài. - Nút Tải xuống bên ngoài - Nhấn để tắt tiếng của video hiện tại. Nhấn lần nữa để bật trở lại. - Nút Tắt tiếng - Nhấn giữ để thay đổi trạng thái nút. - Đã đặt lại Tốc độ phát: %sx. - "Nhấn để mở hộp thoại Tốc độ phát. -Nhấn giữ để đặt lại tốc độ phát video bình thường (1.0x). Nhấn giữ lần nữa để đặt lại về tốc độ mặc định đã đặt." - Nút Tốc độ phát - "Nhấn để tạo danh sách phát gồm tất cả video từ kênh từ cũ nhất đến mới nhất. -Nhấn và giữ để hoàn tác." - Nút danh sách phát theo thứ tự thời gian - Nhấn để mở hộp thoại Danh sách trắng. -Nhấn giữ để mở hộp thoại cài đặt Danh sách trắng. - Nút Danh sách trắng - Nếu được hiển thị, nút tải xuống danh sách phát sẽ mở trình tải xuống tích hợp sẵn trong ứng dụng. - Nút tải xuống danh sách phát sẽ luôn được hiển thị, và khi thao tác sẽ mở trình tải xuống bên ngoài đối với các danh sách phát công khai. - Ghi đè nút tải xuống danh sách phát - Nút tải xuống video sẽ mở trình tải xuống tích hợp sẵn trong ứng dụng. - Nút tải xuống video sẽ mở trình tải xuống bên ngoài của bạn. - Ghi đè nút tải xuống video - Cần phải có YouTube Music để ghi đè chức năng của nút. Nhấn vào đây để tải YouTube Music. - Điều kiện tiên quyết - Nút Youtube Music sẽ mở ứng dụng YT Music. - Nút Youtube Music sẽ mở ứng dụng RVX Music. - Ghi đè nút Youtube Music - Không bao gồm - Đã bao gồm - Bình thường - Nút thao tác - Chế độ cài đặt khác - Hoạt ảnh / Phản hồi - Nút tải xuống - Tính năng thử nghiệm - Hạn chế về hình ảnh do khu vực - Nhập/Xuất dưới dạng tập tin - Nhập/Xuất dưới dạng văn bản - Bộ lọc từ khoá - Nhiều hơn - Nút trên lớp phủ trình phát - Thông tin bản vá - Thao tác nhanh - Video được đề xuất - Kệ Shorts - Hành động đề xuất - Công cụ được sử dụng - Bộ lọc số lượt xem - Ẩn hoặc hiển thị các thành phần của trình đơn Tài khoản và thẻ Bạn. - Trình đơn Tài khoản - Ẩn hoặc hiển thị các nút thao tác bên dưới video. - Các nút thao tác - Quảng cáo - Hình thu nhỏ thay thế - Tắt hoặc bỏ qua các hạn chế của Chế độ môi trường xung quanh. - Chế độ môi trường xung quanh - Ẩn hoặc hiển thị thanh danh mục trong bảng tin, kết quả tìm kiếm và khi lướt xem các video có liên quan. - Thanh danh mục - Ẩn hoặc hiển thị các thành phần của thanh kênh bên dưới video. - Thanh kênh - Ẩn hoặc hiển thị các thành phần trong hồ sơ kênh. - Hồ sơ kênh - Ẩn hoặc hiển thị các thành phần của phần Bình luận. - Bình luận - Ẩn hoặc hiển thị bài đăng cộng đồng trong bảng tin và trong hồ sơ kênh. - Bài đăng cộng đồng - Ẩn các thành phần không mong muốn bằng bộ lọc tuỳ chỉnh. - Bộ lọc tuỳ chỉnh - Ẩn hoặc hiển thị thành phần của trình đơn tuỳ chọn trên bảng tin. - Trình đơn tuỳ chọn - Bảng tin - Ẩn hoặc thay đổi các thành phần liên quan đến toàn màn hình. - Toàn màn hình - Tổng quan - Tắt hoặc bật phản hồi xúc giác. - Phản hồi xúc giác - Ghi đè chức năng của các nút trong ứng dụng. - Điều chỉnh nút - Nhập hoặc xuất cài đặt. - Nhập/Xuất cài đặt - Thay đổi kiểu trình phát thu nhỏ trong ứng dụng. - Trình phát thu nhỏ - Cài đặt khác - Ẩn hoặc hiển thị các thành phần trên Thanh điều hướng. - Thanh điều hướng - Thông tin về các bản vá đã được áp dụng. - Thông tin bản vá - Ẩn hoặc hiển thị các nút trong trình phát. - Nút trong trình phát - Ẩn hoặc thay đổi thành phần của trình đơn tuỳ chọn trong trình phát video. - Trình đơn tuỳ chọn - Trình phát - Return YouTube Username - Return YouTube Dislike - SponsorBlock - Tùy chỉnh thanh tiến trình. - Thanh tiến trình - Ẩn các thành phần của trình đơn Cài đặt YouTube. - Trình đơn Cài đặt - Ẩn hoặc hiển thị các thành phần trong trình phát Shorts. - Trình phát Shorts - Trình Shorts - Giả mạo dữ liệu phát trực tiếp để ngăn chặn sự cố phát. - Giả mạo dữ liệu phát trực tiếp - Cử chỉ vuốt - Ẩn hoặc thay đổi các thành phần trên thanh công cụ, chẳng hạn như thanh tìm kiếm, các nút trên thanh công cụ và tiêu đề YouTube. - Thanh công cụ - Ẩn hoặc hiển thị các thành phần mô tả video. - Mô tả video - Ẩn video theo từ khoá hoặc số lượt xem. - Bộ lọc video - Video - Thay đổi cài đặt liên quan đến Nhật ký xem. - Nhật ký xem - Lề trên bảng nút thao tác nhanh phải nằm trong khoảng 0 - 32. - Giá trị khoảng cách từ thanh tiến trình đến bảng nút thao tác nhanh trong khoảng từ 0 đến 32. - Lề trên bảng nút thao tác nhanh - "Buộc từ chối phản hồi codec AV1 phần mềm. -Một codec khác sẽ được áp dụng sau khoảng 20 giây tải bộ đệm." - Từ chối phản hồi codec AV1 phần mềm - Quá trình dự phòng khiến cho việc tải bộ đệm mất khoảng 20 giây trước khi bắt đầu. - Độ lệch - Thay đổi tốc độ phát chỉ áp dụng cho video hiện tại. - Thay đổi tốc độ phát áp dụng cho tất cả video. - Lưu thay đổi tốc độ phát - Thông báo ngắn sẽ không hiển thị khi thay đổi tốc độ phát mặc định. - Thông báo ngắn sẽ được hiển thị khi thay đổi tốc độ phát mặc định. - Hiện một thông báo ngắn - Đã lưu tốc độ phát mặc định thành %s. - Thay đổi chất lượng chỉ áp dụng cho video hiện tại. - Thay đổi chất lượng áp dụng cho tất cả video. - Lưu thay đổi chất lượng video - Thông báo ngắn sẽ không hiển thị khi thay đổi chất lượng mặc định của video. - Thông báo ngắn sẽ được hiển thị khi thay đổi chất lượng mặc định của video. - Hiện một thông báo ngắn - Thay đổi chất lượng trên dữ liệu di động mặc định thành %s. - Không thể đặt chất lượng video. - Thay đổi chất lượng trên WiFi mặc định thành %s. - "Loại bỏ hộp thoại cảnh báo nội dung cần cân nhắc trước khi xem. -Tuỳ chọn này chỉ tự động chấp nhận hộp thoại cảnh báo, chứ không thể bỏ qua giới hạn về độ tuổi." - Loại bỏ hộp thoại cảnh báo trước khi xem - Thay thế codec AV1 phần mềm bằng codec VP9. - Thay thế codec AV1 phần mềm - Đang áp dụng tên hiển thị của kênh (@handle). - Đang áp dụng tên kênh. - Thay thế tên hiển thị của kênh - Nhấn để hiển thị thời gian còn lại. - Nhấn để mở mục Tốc độ phát hoặc Chất lượng video. - Thay thế hành động của dấu thời gian - Thay thế nút Tạo bằng nút Cài đặt. - Thay thế nút Tạo - "Nhấn để mở cài đặt YouTube. -Nhấn và giữ để mở cài đặt RVX." - "Nhấn để mở cài đặt RVX. -Nhấn và giữ để mở cài đặt YouTube." - Thao tác kích hoạt nút - Hình thu nhỏ khi tua sẽ xuất hiện ở chế độ toàn màn hình. - Hình thu nhỏ khi tua sẽ xuất hiện phía trên thanh tiến trình. - Khôi phục hình thu nhỏ trên thanh tiến trình kiểu cũ - Mục chất lượng video kiểu cũ không được hiển thị. - Mục chất lượng video kiểu cũ được hiển thị. - Khôi phục mục chất lượng video kiểu cũ - \@handle (Tên người dùng) - Định dạng hiển thị - Tên người dùng (@handle) - Tên người dùng - Tên hiển thị (@handle) đã được áp dụng. - Tên người dùng đang được áp dụng. - Kích hoạt Return YouTube Username - "Khoá nhà phát triển YouTube Data API v3 là một mã khoá cho phép các nhà phát triển truy cập lấy dữ liệu từ Youtube, và chúng cũng cần thiết để thay thế @tên hiển thị thành tên người dùng. - -Giới hạn truy cập hàng ngày cho các khoá API trên gói miễn phí là 10000 lần, với mỗi lượt truy cập chỉ áp dụng cho 1 bình luận. - -Nhấp vào đây để xem các bước phát hành khóa API." - Giới thiệu về khoá YouTube Data API - Khoá nhà phát triển để sử dụng YouTube Data API v3. - Khoá Youtube Data API - 1. <a href=%1$s>Tạo dự án mới</a>.<br>2. Ấn vào <b>CREATE</b>.<br>3. Đi tới <a href=%2$s>YouTube Data API v3</a>.<br>4. Ấn vào <b>ENABLE</b>.<br>5. Ấn vào <b>CREATE CREDENTIALS</b>.<br>6. Chọn <b>Public data</b>.<br>7. Ấn vào <b>NEXT</b>.<br>8. Sao chép mã API.<br><br>※ Không nên chia sẻ mã API với người khác, vì vậy chúng cũng không có mặt trong mục Nhập/Xuất cài đặt. - Phát hành mã khoá - Giới thiệu - Dữ liệu về lượt không thích được cung cấp bởi API Return YouTube Dislike. Nhấn vào đây để tìm hiểu thêm. - ReturnYouTubeDislike.com - Nút Thích được thiết kế để đồng bộ khả năng hiển thị với nút Không thích. - Nút Thích được thiết kế để tối ưu kích thước hiển thị. - Nút Thích thu gọn - Số lượt không thích được hiển thị dưới dạng số. - Số lượt không thích được hiển thị dưới dạng phần trăm. - Hiện số lượt không thích theo % - Số lượt Không thích đã bị ẩn. - Số lượt không thích được hiển thị. - Hiện số lượt không thích - Số lượt thích ước tính đã ẩn. - Số lượt thích ước tính đã hiển thị. - Hiển thị số lượt thích ước tính - Số lượt không thích không khả dụng (đã đạt đến giới hạn API máy khách). - Số lượt không thích không khả dụng (trạng thái %d). - Số lượt không thích tạm thời không khả dụng (API đã hết thời gian chờ). - Số lượt không thích không khả dụng (%s). - Tải lại video để bình chọn sử dụng Return YouTube Dislike - Số lượt không thích đã ẩn khỏi trình phát Shorts. - Số lượt không thích được hiển thị trong trình phát Shorts. - "Số lượt không thích được hiển thị trên Shorts. - -Hạn chế: Lượt không thích có thể không hiển thị nếu người dùng không đăng nhập hoặc ở chế độ ẩn danh." - Hiện số lượt Không thích trong Shorts - Thông báo ngắn nếu API Return YouTube Dislike không khả dụng đã tắt. - Hiển thị thông báo ngắn nếu API Return YouTube Dislike không khả dụng. - Thông báo ngắn nếu API không khả dụng - Đã ẩn - Loại bỏ các tham số truy vấn theo dõi khỏi URL khi chia sẻ liên kết. - Làm sạch liên kết chia sẻ - "Các cụm từ như '#', 'Fundraiser', 'Shop' and 'products' đã được hiển thị trong phụ đề video." - "Các cụm từ như '#', 'Fundraiser', 'Shop' and 'products' đã bị ẩn khỏi phụ đề video." - Làm sạch phụ đề video - Giới thiệu - sponsor.ajay.app - Dữ liệu này được cung cấp bởi API SponsorBlock. Nhấn vào đây để tìm hiểu thêm và xem các bản tải xuống cho các nền tảng khác. - Đã thay đổi API URL. - URL API không hợp lệ. - Đặt lại URL API. - Giao diện - Đã thay đổi màu. - Màu: - Mã màu không hợp lệ. - Đặt lại màu - Tạo phân đoạn mới - Cài đặt phân đoạn - Tự động ẩn nút Bỏ qua - Nút Bỏ qua được hiển thị cho toàn bộ phân đoạn. - Nút Bỏ qua sẽ ẩn sau vài giây. - Dùng nút Bỏ qua phân đoạn thu gọn - Nút Bỏ qua phân đoạn được thiết kế với giao diện tốt nhất. - Nút Bỏ qua phân đoạn được thiết kế để tối ưu kích thước hiển thị. - Nút tạo phân đoạn mới - Đã ẩn nút Tạo phân đoạn mới. - Đã hiện nút Tạo phân đoạn mới. - Kích hoạt SponsorBlock - SponsorBlock là một tiện ích được đóng góp bởi cộng đồng giúp bỏ qua các phần gây khó chịu trong video trên YouTube. - Nút Bình chọn phân đoạn - Nút Bình chọn phân đoạn đã ẩn. - Nút Bình chọn phân đoạn được hiển thị. - Chung - Điều chỉnh phân đoạn mới - Giá trị nhập vào phải là một số dương. - Số mili giây bạn có thể tua đi và tua lại khi sử dụng các nút điều chỉnh thời gian trong lúc tạo phân đoạn mới. - Thay đổi URL API - Địa chỉ SponsorBlock sử dụng để liên lạc đến máy chủ. - Thời lượng phân đoạn tối thiểu - Thời lượng không hợp lệ. - Các đoạn ngắn hơn giá trị này (tính bằng giây) sẽ không được hiển thị hoặc bị bỏ qua. - Bật theo dõi số lần bỏ qua - Theo dõi số lần bỏ qua không được bật. - Cho bảng xếp hạng của SponsorBlock biết lượng thời gian đã tiết kiệm được. Một thông báo sẽ được gửi tới bảng xếp hạng mỗi khi một phân đoạn đã bỏ qua. - Hiện thông báo ngắn - Thông báo không được hiển thị. Nhấn vào đây để xem ví dụ. - Hiện thông báo ngắn mỗi khi tự động bỏ qua phân đoạn. Nhấn vào đây để xem ví dụ. - Hiển thị thời lượng video không có phân đoạn - Độ dài video đầy đủ được hiển thị. - Thời lượng video trừ đi tất cả các phân đoạn, được hiển thị trong dấu ngoặc đơn bên cạnh thời lượng video đầy đủ. - Id người dùng riêng tư của bạn - ID người dùng riêng tư phải dài ít nhất 30 ký tự. - Mã Id này giống như mật khẩu của bạn vậy, do đó không nên chia sẻ với bất kỳ ai. Nếu ai đó có được nó, họ có thể mạo danh bạn. - Đã đọc - Hãy đọc nguyên tắc của SponsorBlock trước khi tạo phân đoạn mới. - Xem ngay - Thực hiện theo các nguyên tắc - Hướng dẫn bao gồm các nguyên tắc và mẹo về cách tạo phân đoạn mới. - Xem nguyên tắc - Chọn danh mục phân đoạn - The segment lasts from %1$02d:%2$02d to %3$02d:%4$02d (%5$d minutes %6$02d seconds)\nIs it ready to submit? - Phân đoạn bắt đầu từ\n\n%1$s\nđến\n%2$s\n\n%3$s\n\nSẵn sàng gửi? - Thời lượng phân đoạn đã chính xác chưa? - Danh mục đã tắt trong cài đặt. Cho phép danh mục để gửi. - Bạn muốn thay đổi thời gian bắt đầu hay kết thúc của phân đoạn? - Thời gian đã đặt không hợp lệ. - Chỉnh sửa thời gian của phân đoạn theo cách thủ công - Đặt %s là bắt đầu hoặc là kết thúc của một phân đoạn mới? - kết thúc - Đánh dấu hai điểm bắt đầu và kết thúc phân đoạn trên thanh tiến trình trước. - bắt đầu - ngay lúc này - Hãy xem trước phân đoạn để đảm bảo rằng nó bỏ qua suôn sẻ. - Thời gian bắt đầu phải trước thời gian kết thúc. - Phân đoạn kết thúc lúc - Phân đoạn bắt đầu lúc - Đoạn SponsorBlock mới - Đặt lại - Đặt lại màu - Lạc đề/Hài hước - Phân cảnh được thêm vào chỉ để câu giờ hoặc gây cười nhưng không cần thiết cho nội dung chính của video. Không bao gồm phân đoạn cung cấp bối cảnh hoặc chi tiết nền. - Khúc nổi bật - Phần video được nhiều người tìm kiếm nhất. - Nhắc nhở tương tác (Đăng ký) - Một lời nhắc ngắn rằng bạn hãy ấn thích, đăng ký hoặc theo dõi họ ở giữa nội dung. Nếu nó dài hoặc về một cái gì đó cụ thể, thay vào đó nó nên được tự quảng cáo. - Đoạn tạm ngưng/Giới thiệu - Một khoảng thời gian không có nội dung thực tế. Có thể là tạm dừng, khung tĩnh hoặc hoạt ảnh lặp lại. Không bao gồm các chuyển tiếp chứa thông tin. - Âm nhạc: Phần không chứa âm nhạc - Các phần của video âm nhạc mà không có âm nhạc, cũng không thuộc danh mục nào. - Đoạn kết thúc/Danh đề - Danh đề hoặc đoạn kết thúc của Youtube xuất hiện. Không dành cho phần kết chứa thông tin. - Đoạn xem trước/Tóm tắt/Gây chú ý - Tập hợp các đoạn cắt thể hiện những gì sẽ xảy ra trong video hoặc trong loạt video khác, nơi mà tất cả thông tin được lặp lại ở nơi khác. - Không trả tiền/Tự quảng cáo - Tương tự như Nhà tài trợ, ngoại trừ việc không được trả tiền hoặc tự quảng cáo. Bao gồm các phần về hàng hóa, quyên góp hoặc thông tin về người họ cộng tác. - Nhà Tài Trợ - Quảng cáo, giới thiệu được trả tiền và quảng cáo trực tiếp. Không phải tự quảng cáo hoặc lời cảm ơn miễn phí đến các tác nhân/nhà sáng tạo/trang web/sản phẩm mà họ yêu thích. - Sao chép - Xuất cài đặt thất bại: %s. - Nhập/Xuất cài đặt - Cấu hình tệp JSON SponsorBlock của bạn có thể được nhập/xuất tới ReVanced Extended và các nền tảng SponsorBlock khác. - Cấu hình tệp JSON SponsorBlock của bạn có thể được nhập/xuất tới ReVanced Extended và các nền tảng SponsorBlock khác. Điều này bao gồm cả ID riêng tư của bạn. Vì vậy hãy thật cẩn thận khi chia sẻ nó. - Nhập cài đặt thất bại: %s. - Nhập cài đặt thành công. - Cài đặt của bạn chứa ID SponsorBlock cá nhân.\n\nID của bạn cũng giống như mật khẩu vậy, nên đừng bao giờ chia sẻ nó.\n - Không hiển thị lại - Đã sao chép cài đặt vào bảng nhớ tạm. - Tự động bỏ qua - Tự động bỏ qua một lần - Bỏ qua - Khúc Nổi bật - Bỏ qua Đoạn Trống - Bỏ qua Khúc Nổi bật - Bỏ qua Nhắc Tương Tác - Bỏ qua Phần Giới Thiệu - Bỏ qua Đoạn Tạm Ngưng - Bỏ qua Đoạn Tạm Ngưng - Bỏ qua Phần Không Nhạc - Bỏ qua Phần Kết - Bỏ qua Phần Xem Trước - Bỏ qua Phần Tóm Tắt - Bỏ qua Phần Xem Trước - Bỏ qua khuyến mãi - Bỏ qua Nhà tài trợ - Bỏ qua Phân Đoạn - Tắt - Hiển thị trong thanh tiến trình - Hiển thị nút Bỏ qua - Đã bỏ qua Đoạn Trống. - Đã bỏ qua Khúc Nổi bật. - Đã bỏ qua Nhắc Tương Tác. - Đã bỏ qua Phần Giới Thiệu. - Đã bỏ qua Đoạn Tạm Ngưng. - Đã bỏ qua Đoạn Tạm Ngưng. - Đã bỏ qua nhiều phân đoạn. - Đã bỏ qua Phần không chứa âm nhạc. - Đã bỏ qua Phần Kết. - Đã bỏ qua Phần Xem Trước. - Đã bỏ qua Phần Tóm Tắt. - Đã bỏ qua Phần Xem Trước. - Đã bỏ qua Tự Quảng Cáo. - Đã bỏ qua Nhà Tài Trợ. - Đã bỏ qua phân đoạn chưa gửi. - SponsorBlock tạm thời không khả dụng. - SponsorBlock tạm thời không khả dụng (trạng thái %d). - SponsorBlock tạm thời không khả dụng (hết thời gian chờ API). - Thống kê - Số liệu thống kê tạm thời không khả dụng (API ngừng hoạt động). - Đang tải... - Xếp hạng của bạn là <b>%.2f</b> - Bạn đã hỗ trợ mọi người <b>%s</b> phân đoạn - %1$s giờ %2$s phút - %1$s phút %2$s giây - %s giây - Đó là <b>%s</b> của mọi người.<br>Nhấn vào để xem bảng xếp hạng - Nhấn vào đây để xem số liệu thống kê toàn cầu và những người đóng góp hàng đầu. - Bảng xếp hạng SponsorBlock - Đã vô hiệu hoá SponsorBlock. - Bạn đã bỏ qua <b>%s</b> phân đoạn - Đặt lại bộ đếm phân đoạn đã bỏ qua? - Đó là <b>%s</b>. - Bạn đã tạo <b>%s</b> phân đoạn - Nhấn vào đây để xem các phân đoạn của bạn. - Tên người dùng của bạn: <b>%s</b> - Nhấn vào đây để thay đổi tên người dùng của bạn - Không thể thay đổi tên người dùng: Trạng thái: %1$d %2$s. - Tên người dùng đã được thay đổi thành công. - Không thể gửi phân đoạn.\nĐã tồn tại. - Không thể gửi phân đoạn: %s. - Không thể gửi phân đoạn: %s. - Không thể gửi phân đoạn.\nGiới hạn truy cập (quá nhiều phân đoạn được gửi từ cùng một người dùng hoặc IP). - SponsorBlock tạm thời ngưng hoạt động. - Không thể gửi phân đoạn (trạng thái: %1$d %2$s). - Đã gửi phân đoạn thành công. - Không hiện thông báo nếu SponsorBlock không khả dụng. - Hiển thị thông báo ngắn nếu SponsorBlock không khả dụng. - Thông báo nếu API không khả dụng - Đổi danh mục - Phản đối - Không thể bỏ phiếu cho phân đoạn: %s. - Không thể bỏ phiếu cho phân đoạn (API đã hết thời gian chờ). - Không thể bỏ phiếu cho phân đoạn (trạng thái: %1$d %2$s). - Không có phân đoạn nào để bình chọn. - Ủng hộ - Đã sao chép cài đặt sang bảng nhớ tạm. - Đã sao chép dấu thời gian vào bảng nhớ tạm. (%s) - Đã sao chép URL sang bảng nhớ tạm. - Đã sao chép URL cùng dấu thời gian vào bảng nhớ tạm. - Gốc - Thích - Thích (Cairo) - Trái tim - Trái tim (Đỏ) - Ẩn - Hoạt ảnh nhấn đúp - Lề dưới của bảng Meta phải nằm trong khoảng từ 0 đến 64. - Cấu hình khoảng cách từ thanh tiến trình tới bảng meta, nằm trong khoảng 0 đến 64. - Lề dưới của bảng Meta - Chiều cao phải nằm trong khoảng từ 0 đến 100 (%). - Cấu hình chiều cao của khoảng trống còn lại khi thanh điều hướng bị ẩn, nằm trong khoảng từ 0 đến 100 (%). - Chiều cao của khoảng trống - Nhấn và giữ vào Dấu thời gian để thay đổi trạng thái phát lặp lại trên trình phát Shorts. - Nhấn và giữ Dấu thời gian - "Hiển thị phần tiêu đề video ở chế độ toàn màn hình. - -Hạn chế: Tiêu đề video sẽ biến mất khi nhấn vào." - Hiển thị phần tiêu đề video - Nếu tính năng Tự động phát được bật, video tiếp theo sẽ được phát sau khi đếm ngược kết thúc. - Nếu tính năng Tự động phát được bật, video tiếp theo sẽ được phát ngay lập tức. - Bỏ qua tự động đếm ngược trước khi phát - "Bỏ qua bộ đệm tải trước ở đầu video để áp dụng ngay chất lượng video mặc định. - -Chi tiết: -• Khi video bắt đầu, sẽ có độ trễ khoảng 0,3 giây. -• Không áp dụng cho video HDR, video phát trực tiếp, hoặc video ngắn hơn 15 giây." - Bỏ qua bộ đệm tải trước - Thông báo ngắn đã ẩn. - Thông báo ngắn được hiển thị. - Hiện thông báo ngắn khi bỏ qua - Việc bật cài đặt này có thể gây ra sự cố phát video. - Đã bỏ qua bộ đệm tải trước. - Tốc độ phát khi nhấn và giữ phải nằm trong khoảng 0 - 8.0. - Nhập tốc độ phát khi nhấn và giữ trong khoảng từ 0 đến 8.0. - Tốc độ phát khi nhấn và giữ - "Giả mạo phiên bản YouTube hiện tại thành phiên bản cũ. - -Lưu ý:\n- Tuỳ chọn này sẽ thay đổi giao diện ứng dụng, tuy nhiên có thể xảy ra các sự cố chưa xác định khác. -- Nếu tắt tuỳ chọn này sau đó, giao diện cũ có thể vẫn tồn tại cho đến khi bạn xoá dữ liệu ứng dụng." - Phiên bản không được giả mạo - Phiên bản đã được giả mạo - 17.33.42 - Khôi phục giao diện kiểu cũ - 17.41.37 - Khôi phục kệ Danh sách phát kiểu cũ - 18.05.40 - Khôi phục hộp nhập bình luận kiểu cũ - 18.17.43 - Khôi phục bảng điều khiển trình phát cũ - 18.33.40 - Khôi phục thanh thao tác trình Shorts kiểu cũ - 18.38.45 - Khôi phục phương thức áp dụng chất lượng video mặc định kiểu cũ - 18.48.39 - Vô hiệu hoá cập nhật số \"lượt xem\" và \"lượt thích\" theo thời gian thực - 19.13.37 - Khôi phục hoạt ảnh Số cuộn kiểu cũ - Phiên bản giả mạo - Nhập phiên bản giả mạo mà bạn muốn hướng tới. - Tuỳ chọn phiên bản giả mạo - Giả mạo phiên bản ứng dụng - "Phiên bản ứng dụng sẽ được giả mạo thành một phiên bản cũ hơn của Youtube. - -Điều này sẽ làm thay đổi giao diện và tính năng của ứng dụng, nhưng đồng thời cũng có thể xẩy ra một số lỗi không xác định. - -Nếu muốn tắt tính năng này sau đó, bạn nên xóa dữ liệu ứng dụng để tránh phát sinh lỗi giao diện." - "Giả lập kích thước thiết bị đến giá trị tối đa. -Chất lượng cao có thể được mở khóa trên một số video yêu cầu kích thước thiết bị lớn, nhưng không phải tất cả các video." - Giả mạo kích thước thiết bị - Codec video trên iOS là AVC (H.264), VP9, hoặc là AV1. - Codec video trên iOS là AVC (H.264). - Buộc iOS sử dụng AVC (H.264) - "Bật chức năng này có thể tăng cường thời lượng pin và khắc phục tình trạng giật lag khi phát video. - -AVC (H.264) có độ phân giải tối đa 1080p, và phát video sẽ dùng nhiều dữ liệu di động hơn với VP9 hoặc AV1." - "• Mục Bản âm thanh bị thiếu. -• Mục Âm lượng ổn định không khả dụng." - "• Mục Bản âm thanh bị thiếu. -• Mục Âm lượng ổn định không khả dụng." - "• Phim hoặc video trả phí có thể không phát được. -• Video phát trực tiếp sẽ khởi chạy từ đầu. -• Video có thể kết thúc sớm 1 giây. -• Không có codec âm thanh opus." - Hạn chế của việc giả mạo - • Video có thể không phát được. - Máy khách được sử dụng để lấy dữ liệu phát trực tiếp sẽ bị ẩn trong Thống kê chi tiết. - Máy khách được sử dụng để lấy dữ liệu truyền trực tuyến sẽ được hiển thị trong Thống kê chi tiết. - Hiển thị trong Thống kê chi tiết - "Chưa giả mạo dữ liệu phát trực tiếp. Phát video có thể không hoạt động bình thường." - Đã giả mạo dữ liệu phát trực tiếp. - Giả mạo dữ liệu phát trực tiếp - Android - Android TV - Android VR - iOS - Máy khách mặc định - Việc tắt cài đặt này có thể gây ra sự cố khi phát video. - Độ nhạy khi vuốt để điều chỉnh độ sáng phải nằm trong khoảng từ 1-1000 (%). - Cấu hình khoảng cách tối thiểu để vuốt điều chỉnh độ sáng trong khoảng từ 1 đến 1000 (%).\nKhoảng cách tối thiểu càng ngắn thì mức độ sáng thay đổi càng nhanh. - Độ nhạy khi vuốt để điều chỉnh độ sáng - Đã vô hiệu hoá cử chỉ vuốt ở chế độ Khóa màn hình. - Đã kích hoạt cử chỉ vuốt ở chế độ Khóa màn hình. - Vuốt ở chế độ Khoá màn hình - Tự động - Độ rộng của ngưỡng vuốt để thực hiện cử chỉ vuốt. - Độ rộng ngưỡng vuốt - Độ trong suốt của nền khi thực hiện cử chỉ vuốt. - Độ trong suốt lớp phủ - Kích thước khu vực vuốt không được lớn hơn 50. - Phần diện tích màn hình có thể vuốt (tính bằng %).\n\nLưu ý: Tuỳ chọn này cũng sẽ thay đổi kích thước vùng màn hình đối với cử chỉ nhấn đúp để tua. - Kích thước màn hình lớp phủ - Độ lớn của văn bản được hiển thị trên lớp phủ khi vuốt. - Kích thước văn bản trên lớp phủ - Số mili giây mà lớp phủ khi vuốt được hiển thị. - Thời gian hiển thị lớp phủ - Độ nhạy khi vuốt để điều chỉnh âm lượng phải nằm trong khoảng từ 1 đến 1000 (%). - Cấu hình khoảng cách tối thiểu để vuốt điều chỉnh âm lượng trong khoảng từ 1 đến 1000 (%).\n\nKhoảng cách tối thiểu càng ngắn thì mức âm lượng thay đổi càng nhanh.\n\nĐộ nhạy vuốt âm lượng được khuyến nghị là 100% với mức âm lượng 15 và 10% với mức âm lượng 150. - Độ nhạy khi vuốt để điều chỉnh âm lượng - "Hoán đổi vị trí của nút Tạo và nút Thông báo bằng cách giả mạo thông tin thiết bị. - -• Tuỳ chọn này có thể không có hiệu lực cho đến khi khởi động lại thiết bị. -• Tắt tuỳ chọn này sẽ tải thêm quảng cáo từ phía máy chủ. -• Tắt tuỳ chọn này có thể hiển thị quảng cáo dạng video." - Nút Tạo và nút Thông báo như mặc định. - "Nút Tạo đã được đổi vị trí với nút Thông báo. - -Lưu ý: Việc bật tuỳ chọn này cũng sẽ ẩn các quảng cáo video." - Đổi vị trí nút Tạo và nút Thông báo - "Tắt tùy chọn này có thể tải thêm quảng cáo từ máy chủ. - -Ngoài ra, quảng cáo sẽ không còn bị chặn trong trình phát Shorts. - -Nếu cài đặt này không có hiệu lực, hãy thử chuyển sang chế độ Ẩn danh." - Nguyên gốc - RVX Music - Hiện %s chưa được cài đặt. Hãy cài đặt và thử lại. - Tên gói của RVX Music đã được cài đặt. - Tên gói của RVX Music - • Nhật ký xem bị chặn. - "• Tuân theo cài đặt Nhật ký xem của tài khoản Google. -• Nhật ký xem có thể không hoạt động do DNS hoặc VPN." - • Tuân theo cài đặt Nhật ký xem của tài khoản Google. - Trạng thái - Nhấn để mở mục quản lý Nhật ký xem trên YouTube. - Quản lý toàn bộ lịch sử - Gốc - Thay thế miền - Chặn nhật ký - Kiểu nhật ký - Không thể thêm kênh \'%1$s\' vào Danh sách trắng %2$s. - Kênh \'%1$s\' đã được thêm vào Danh sách trắng %2$s. - Không có kênh nào nằm trong Danh sách trắng. - Chưa được thêm vào Danh sách trắng. - Không tải được thông tin kênh. - Đã thêm vào Danh sách trắng. - Tốc độ phát - Xóa kênh \'%1$s\' khỏi Danh sách trắng %2$s? - Không thể xóa kênh \'%1$s\' khỏi Danh sách trắng %2$s. - Kênh \'%1$s\' đã bị xóa khỏi Danh sách trắng %2$s. - Kiểm tra hoặc xóa các kênh đã thêm vào Danh sách trắng. - Danh sách trắng - SponsorBlock - diff --git a/src/main/resources/youtube/translations/zh-rCN/missing_strings.xml b/src/main/resources/youtube/translations/zh-rCN/missing_strings.xml deleted file mode 100644 index 8956daf6e..000000000 --- a/src/main/resources/youtube/translations/zh-rCN/missing_strings.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - AI-generated video summary section is shown. - AI-generated video summary section is hidden. - Hide AI-generated video summary section - MMT Orange - MMT Pink - MMT Turquoise - diff --git a/src/main/resources/youtube/translations/zh-rCN/strings.xml b/src/main/resources/youtube/translations/zh-rCN/strings.xml deleted file mode 100644 index d95cdc7ce..000000000 --- a/src/main/resources/youtube/translations/zh-rCN/strings.xml +++ /dev/null @@ -1,1712 +0,0 @@ - - - 开启视频播放器的访问控制? - 由于无障碍服务已启用,您的控制被修改 - 继续 - 不再提示 - "GmsCore 没有后台运行的权限 - -请遵循适用于您手机的 “Don't kill my app!” 指南,并将这些说明应用于您的 GmsCore 安装 - -这是应用程序运行所必需的" - "必须禁用 GmsCore 电池优化才能防止出现问题 - -点击继续按钮并禁用电池优化" - 打开网站 - 需要采取措施 - 启用云消息传递以接收通知 - 打开 GmsCore - GmsCore 没有安装。请安装它 - "DeArrow 提供 YouTube 视频的众包缩略图。 这些缩略图通常比 YouTube 提供的缩略图更相关。 - -如果启用,视频链接将发送到 API 服务器,这不会发送其他数据。 - -点击此处了解更多关于 DeArrow 的信息。" - 关于 DeArrow - 无效的 DeArrow API URL - DeArrow 缩略图缓存端点 URL。除非您知道自己在做什么,否则请勿更改 - DeArrow API 端点 - 如果 DeArrow 不可用,不显示 Toast - 如果 DeArrow 不可用,显示 Toast - 如果 API 不可用,则显示 Toast - DeArrow 暂时不可用 (status code: %s) - DeArrow 暂时不可用 - 首页标签 - 你的标签 - 原始缩略图 - DeArrow & 原始缩略图 - DeArrow & 静态视频捕获 - 静态视频捕获 - 播放列表、建议 - 搜索结果 - 静态视频捕获 - 静态捕捉是从每个视频的开头/中间/结尾处截取的。这些图像内置于 YouTube 中,不使用外部 API - 关于静态视频捕捉 - 使用高质量的静态捕捉。 - 使用中等质量仍然可以捕获缩略图加载速度会更快,但直播、未发布和非常旧的视频可能会显示空白缩略图 - 使用快速静态捕捉 - 视频开头 - 视频中间 - 视频结尾 - 拍摄静态捕捉的时间 - 订阅标签 - 添加时间戳信息已关闭 - "添加时间戳信息已显示" - 添加时间戳信息 - 添加播放速度。播放视频时,长按时间戳可更改类型 - 添加视频分辨率。播放视频时,长按时间戳可更改类型 - 添加信息类型 - 省电模式下氛围模式已禁用 - 省电模式下氛围模式已启用 - 解除氛围模式限制 - 获取图片的域名\n注意:仅输入域名,不输入“https\:\/\/”前缀 - 替代域名 - 使用原始图像主机\n\n启用此功能可以修复在某些地区被阻止的缺失图像 - 使用图像主机 yt4.ggpht.com - 绕过图像区域限制 - 原版 - 手机 - 手机 (最大 480 dip) - 平板 - 平板 (最小 600 dip) - 调整布局 - 使用开关切换 - 使用文本切换 - 更改切换类型 - 应用内分享菜单已启用 - 系统分享菜单已启用 - 更改分享菜单 - 自动播放 - 默认 - 暂停 - 重复播放 - 更改 Shorts 重复状态 - 浏览频道 - 课程 / 学习资源 - 默认 - 探索 - 游戏 - 历史 - 媒体库 - 喜欢的视频 - 直播 - 电影 - 音乐 - 搜索 - Shorts - 体育 - 订阅 - 热门 - 稍后观看 - 更改起始页 - 开始页面只更改一次 - "开始页面总是更改 - -限制:工具栏上的返回按钮可能无法工作" - 更改首页类型 - 默认标题已启用 - 高级标题已启用 - 更改 YouTube 标题 - 按行分隔的名称筛选组件 - 编辑自定义筛选 - 自定义筛选已禁用 - 自定义筛选已启用 - 自定义筛选 - 自定义过滤器无效: %s - 使用旧版弹出菜单 - 使用自定义对话框 - 自定义播放速度菜单类型 - 自定义播放速度必须小于 %sx,已重置为默认值 - 无效的自定义播放速度将使用默认值 - 添加或更改播放速度 - 编辑自定义播放速度 - 播放器的不透明度必须介于 0-100 之间 重置为默认值 - 不透明度值介于 0-100 之间,0 为透明 - 自定义播放器的不透明度 - 输入进度条颜色16进制代码 - 自定义进度条颜色 - 要从外部浏览器中打开 Revanced Extended,请在“系统应用设置 - 默认打开 - 要在此应用中打开链接”中添加支持的网页链接 - 打开默认应用设置 - 默认播放速度 - 移动网络的默认视频画质 - WiFi 网络的默认视频画质 - Disables ambient mode for fullscreen only. - 全屏下氛围模式已启用 - 全屏下氛围模式已禁用 - 在全屏时禁用氛围模式 - Disables ambient mode. - 氛围模式已启用 - 氛围模式已禁用 - 禁用氛围模式 - 强制自动音轨已启用 - 强制自动音轨已禁用 - 禁用强制自动音轨 - 强制显示字幕已启用 - 强制显示字幕已禁用 - 禁用强制显示字幕 - 已禁用自动播放器弹出面板 - 已启用自动播放器弹出面板 - 禁用播放器弹出面板 - "自动播放开启时自动切换混合播放列表 - -自动播放可以在 YouTube 设置中更改: -设置 → 自动播放→ 自动播放下一个视频" - 自动切换混合播放列表已禁用 - 禁用切换混合播放列表 - 启用此功能将禁止在自动播放音乐时自动切换到 YouTube Mix - 直播默认播放速度已启用 - 直播默认播放速度已禁用 - 禁用直播默认播放速度 - 音乐默认播放速度已启用 - "音乐默认播放速度已禁用 - -限制:此设置可能不适用于不包含“监听YouTube Music”广告的视频" - 禁用音乐播放速度 - 启用互动面板 - 禁用互动面板 - 禁用互动面板 - 启用章节触感反馈 - 禁用章节触感反馈 - 禁用章节触感反馈 - 启用滑动触感反馈 - 禁用滑动触感反馈 - 禁用滑动触感反馈 - 启用进度触感反馈 - 禁用进度触感反馈 - 禁用进度触感反馈 - 启用进度条跳转撤销的震动反馈 - 禁用进度条跳转撤销的震动反馈 - 禁用进度条跳转撤销的震动反馈 - 启用缩放触感反馈 - 禁用缩放触感反馈 - 缩放触感反馈 - HDR 视频自动亮度已启用 - HDR 视频自动亮度已禁用 - 禁用 HDR 视频自动亮度 - HDR 视频已启用 - HDR 视频已禁用 - HDR 视频 - 全屏时的横屏模式已启用 - 全屏时的横屏模式已禁用 - 横屏模式 - 点赞和点踩按钮点击时有动效 - 点赞和点踩按钮点击时无动效 - 禁用点赞和点踩按钮动效 - "禁用 Cronet 引擎的 QUIC 协议" - 禁用 QUIC 协议 - 应用启动时恢复 Shorts 播放器 - 应用启动时不会恢复 Shorts 播放器 - 禁止恢复 Shorts 播放器 - 滚动动画已启用 - 滚动动画已禁用 - 禁用滚动数字动画 - 进度条章节已启用 - 进度条章节已禁用 - 禁用进度条章节 - 点赞按钮动画已启用 - 点赞按钮动画已禁用 - 禁用点赞按钮动画 - "长按时禁用“2x>>” - -注意: -• 禁用速度叠加会恢复旧布局的“滑动查找”行为 -• 禁用此设置不会强制启用速度叠加显示" - 禁用速度叠加 - 开屏动画已启用 - 开屏动画已禁用 - 禁用开屏动画 - "展开视频描述时禁用以下交互: - -• 点击滚动 -• 长按选择文本" - 禁用视频描述交互 - VP9 编解码器已启用 - "VP9 编解码器已禁用 - -• 最大分辨率是 1080p -• 视频播放将使用比 VP9 更多的网络数据 -要获取 HDR 播放,HDR 视频仍使用VP9 编解码器" - 禁用 VP9 编解码器 - Cairo 搜索栏已禁用 - "Cairo 搜索栏已启用 - -副作用:Cairo 主题也应用于通知点" - 启用 Cairo 搜索栏 - 紧凑的播放器布局已禁用 - 紧凑的播放器布局已启用 - 启用紧凑的播放器布局叠加层 - 自定义播放速度已禁用 - 自定义播放速度已启用 - 启用自定义播放速度 - 自定义进度条颜色已禁用 - 自定义进度条颜色已启用 - 启用自定义进度条颜色 - Debug 日志不包括缓冲区 - Debug 日志不包括缓冲区 - 启用 Debug 缓冲区日志记录 - Debug 日志已禁用 - Debug 日志已启用 - 启用 Debug 日志 - 默认播放速度不适用于 Shorts - 默认播放速度适用于 Shorts - 启用 Shorts 默认播放速度 - 外部浏览器已禁用 - 外部浏览器已启用 - 启用外部浏览器 - 渐变的加载屏幕已禁用 - 渐变的加载屏幕已启用 - 启用渐变的加载屏幕 - 导航按钮之间的间距不会变窄 - 导航按钮之间的间距变窄 - 启用窄间距导航按钮 - 遵循默认重定向策略 - 绕过链接重定向 - 启用直接打开链接 - 如果播放器支持 OPUS 编解码器,则启用 OPUS 编解码器 - 启用 OPUS 编解码器 - 退出/进入全屏时不保存或恢复亮度 - 退出/进入全屏时保存和恢复亮度 - 启用保存和恢复亮度 - 进度条点击已禁用 - 进度条点击已启用 - 启用进度条点击 - "这将恢复没有进度条缩略图的直播的缩略图 - -互联网数据使用量可能会增加,进度条缩略图会在轻微延迟后显示 - -此功能在非常快速的互联网连接下效果最佳" - 进度条缩略图将会以中质量显示 - 进度条缩略图将会以高质量显示 - 启用高质量缩略图 - 时间戳已禁用 - "时间戳已启用 - -已知问题:由于这是谷歌开发阶段的一个功能,布局可能已损坏" - 启用时间戳 - 滑动控制亮度已禁用 - 滑动控制亮度已启用 - 启用滑动控制亮度 - 触觉反馈已禁用 - 触觉反馈已启用 - 启用触觉反馈 - 亮度手势的最低值不会启用自动亮度调节 - 亮度手势的最低值会启用自动亮度调节 - 启用自动亮度手势 - 轻触激活滑动手势 - 长按激活滑动手势 - 启用长按滑动手势 - 向上/向下滑动不会播放下一/上一个视频 - 向上/向下滑动将播放下一个/上一个视频 - 启用滑动以在全屏中更改视频 - 滑动控制音量已禁用 - 滑动控制音量已启用 - 音量手势 - 导航栏不透明 - 导航栏半透明 - 启用半透明导航栏 - 当在视频播放器下方向下滑动时,禁用进入全屏模式 - 当在视频播放器下方向下滑动时,启用进入全屏模式 - 启用观看面板手势 - "启用此设置将禁用你的选项卡中的设置按钮 - -在这种情况下,请使用以下路径访问设置: -你的选项卡 → 查看频道 → 菜单 → 设置" - 在你的选项卡中启用宽搜索栏 - 宽搜索栏已禁用 - 宽搜索栏已启用 - 宽搜索栏 - 宽搜索栏不包括 YouTube 标题 - 宽搜索栏包括 YouTube 标题 - 启用带标题的宽搜索栏 - 描述 - "在视频描述面板中输入标题 -如果保存了不正确的字符串,则“展开视频描述”可能无法正常工作" - 视频描述面板中的标题 - 手动展开视频描述 - 自动展开视频描述 - 自动展开视频描述 - 你想继续吗? - 重置为默认值 - 重启应用以正常加载界面布局 - "YouTube 服务端的一个错误会导致一些用户隐藏像点赞数、播放量和上传日期这样的滚动数字文本 - -此问题的临时解决方法是伪装应用版本为19.13.37 - -您是否想要在重启应用程序之前伪装应用版本?" - 刷新并重启 - 导出配置失败 - 导出配置成功 - 导出配置到文件 - 导出配置 - 导入 - 复制 - 导入 / 导出配置文本 - 导入 / 导出配置文本 - 导入配置失败 - 配置已被重置为默认值 - 导入配置成功 - 从文件导入配置 - 导入配置 - 重置 - 搜索 %s - ReVanced Extended - 外部下载器 - 未安装 - "%1$s 未安装 -请从网站下载 %2$s" - 警告 - %s 未安装,请先安装它。 - 已安装的外部下载器应用包名,例如 YTDLnis - 播放列表下载器名称 - 已安装的外部下载器应用的包名,例如 NewPipe 或 YTDLnis - 视频下载器应用包名 - "在以下情况下,视频将切换为全屏: - -• 开始播放视频时 -• 点击评论中的时间戳时" - 强制全屏 - 每次启动应用时显示 GMSCore 的优化对话框 - 显示 GMSCore 优化对话框 - 要过滤的帐户菜单名称列表,每行一个名称 - 编辑账号菜单过滤 - "隐藏帐户菜单和你的选项卡的元素 -某些组件可能不会隐藏" - 账号菜单 - 专辑卡片已显示 - 专辑卡片已隐藏 - 隐藏专辑卡片 - 精选位置、游戏和音乐部分已显示 - 精选位置、游戏和音乐部分已隐藏 - 隐藏属性部分 - 自动播放预览面板已显示 - 自动播放预览面板已隐藏 - 隐藏自动播放预览面板 - 浏览商店按钮已显示 - 浏览商店按钮已隐藏 - 隐藏浏览商店按钮 - "隐藏以下分类: -• 突发新闻 -• 继续观看 -• 探索更多频道 -• 再次收听 -• 购物 -• 重新观看" - 隐藏轮播内容 - 在新闻源中显示 - 在新闻源中隐藏 - 在新闻源中隐藏 - 在相关视频中显示 - 在相关视频中隐藏 - 在相关视频中隐藏 - 在搜索结果中显示 - 在搜索结果中隐藏 - 在搜索结果中隐藏 - 频道指南已显示 - 频道指南已隐藏 - 隐藏频道指南 - 频道会员列表已显示 - 频道会员列表已隐藏 - 隐藏频道会员列表 - 频道简介顶部的连结已显示 - 频道简介顶部的连结已隐藏 - 隐藏频道个人档案的链接 - "Shorts -播放列表 -商店" - 要过滤的频道标签名称列表,每行一个名称 - 频道标签过滤器 - 频道标签过滤器已禁用 - 频道标签过滤器已启用。 - 启用频道标签过滤器 - 频道水印已显示 - 频道水印已隐藏 - 隐藏频道水印 - 章节部分已显示 - 章节部分已隐藏 - 隐藏章节部分 - Chips 视频栏已显示 - Chips 视频栏已隐藏 - 隐藏 Chips 视频栏 - 剪辑按钮已显示 - 剪辑按钮已隐藏 - 隐藏剪辑按钮 - 创建 Shorts 按钮已显示 - 创建 Shorts 按钮已隐藏 - 隐藏创建 Shorts 按钮 - 高亮搜索链接已显示 - 高亮搜索链接已隐藏 - 隐藏高亮搜索链接 - 感谢按钮已显示 - 感谢按钮已隐藏 - 隐藏感谢按钮 - 时间戳和表情按钮已显示 - 时间戳和表情按钮已隐藏 - 隐藏时间戳和表情按钮 - 会员评论中横幅已显示 - 会员评论中横幅已隐藏 - 隐藏会员评论横幅 - 首页动态中评论部分已显示 - 首页动态中评论部分已隐藏 - 隐藏首页动态中的评论部分 - 评论部分已显示 - 评论部分已隐藏 - 隐藏评论部分 - 频道中的社区帖子已显示 - 频道中的社区帖子隐藏 - 隐藏频道中的社区帖子 - 首页和相关视频中的社区帖子已显示 - 首页和相关视频中的社区帖子已隐藏 - 隐藏首页和相关视频中的社区帖子 - 订阅内容中的社区帖子已显示 - 订阅内容中的社区帖子已隐藏 - 隐藏订阅内容中的社区帖子 - 此内容的制作过程部分已显示 - 此内容的制作过程部分已隐藏 - 隐藏内容部分 - 众筹箱已显示 - 众筹箱已隐藏 - 隐藏众筹箱 - 双击叠加层过滤器已显示 - 双击叠加层过滤器已隐藏 - 隐藏双击叠加层过滤器 - 显示下载按钮 - 隐藏下载按钮 - 隐藏下载按钮 - 结束界面卡片已显示 - 结束界面卡片已隐藏 - 隐藏结束界面卡片 - 扩展面板已显示 - 扩展面板已隐藏 - 隐藏视频下方的扩展面板 - 扩展边框已显示 - 扩展边框已隐藏 - 隐藏扩展边框 - 显示字幕按钮 - 隐藏字幕按钮 - 隐藏动态字幕按钮 - 要过滤的动态弹出菜单名称列表,每行一个名称 - 编辑资讯弹出菜单过滤 - 资讯弹出菜单已显示 - 资讯弹出菜单已隐藏 - 隐藏资讯弹出菜单 - 显示动态搜索栏 - 隐藏动态搜索栏 - 隐藏新闻源搜索栏 - 问卷调查已显示 - 问卷调查已隐藏 - 隐藏问卷调查 - 影片条叠加层已显示 - 影片条叠加层已隐藏 - 隐藏影片条叠加层 - 悬浮按钮已显示 - 悬浮按钮已隐藏 - 隐藏悬浮按钮 - 弹出麦克风按钮已显示 - 弹出麦克风按钮已隐藏 - 隐藏弹出麦克风按钮 - “个性化” 功能架已显示 - “个性化” 功能架已隐藏 - 隐藏”个性化“功能架 - 全屏广告已显示 - 全屏广告已隐藏 - 隐藏全屏广告 - "全屏广告已屏蔽 - -限制:全屏下的社区帖子图片可能被屏蔽" - 全屏广告已通过关闭按钮关闭 - 关闭全屏广告 - 一般广告已显示 - 一般广告已隐藏 - 隐藏一般广告 - YouTube Premium 推广已显示 - YouTube Premium 推广已隐藏 - 隐藏 YouTube Premium 推广 - 灰色分隔符已显示 - 灰色分隔符已隐藏 - 隐藏灰色分隔符 - 控制列已显示 - 控制列已隐藏 - 隐藏控制列 - 图片搜索按钮已显示 - 图片搜索按钮已隐藏 - 隐藏图片搜索按钮 - 图片栏已显示 - 图片栏已隐藏 - 隐藏图片栏 - 信息卡片已显示 - 信息卡片已隐藏 - 隐藏视频中的信息卡片 - 资料卡已显示 - 资料卡已隐藏 - 隐藏资料卡 - 信息面板已显示 - 信息面板已隐藏 - 隐藏信息面板 - 加入按钮已显示 - 加入按钮已隐藏 - 隐藏加入按钮 - 关键概念部分已显示 - 关键概念部分已隐藏 - 隐藏关键概念部分 - "搜索、首页、订阅和评论会被过滤以隐藏与关键词短语匹配的内容 - -限制: -• 某些 Shorts 可能不会隐藏 -• 某些 UI 组件可能不会隐藏 -• 搜索关键词可能不会显示任何结果" - 关于关键词过滤 - 环绕一个关键字/短语带双引号会防止视频标题和频道名称<br><br>例如,<br><b>\"ai\"</b> 将隐藏视频: <b>How does AI work?</b><br>但不会隐藏: <b>What does fair use mean?</b> - 全词匹配 - 评论关键词过滤器已禁用 - 评论关键词过滤器已启用 - 评论关键词过滤 - 首页订阅内容的关键词过滤已禁用 - 首页订阅内容的关键词过滤已启用 - 启用首页关键词过滤器 - "配置要隐藏的关键词和短语,以换行符分隔\n\n -中间有大写字母的单词必须使用大小写(例如:iPhone、TikTok、LeBlanc)" - 要隐藏的关键词 - 搜索结果过滤器未启用 - 搜索结果过滤器已启用 - 启用搜索关键词过滤 - 订阅视频的关键词过滤器已禁用 - 订阅视频的关键词过滤器已启用 - 启用订阅视频关键词过滤器 - 关键字将隐藏所有视频: %s - 关键词无效无法使用:\'%s\' 作为过滤器 - 添加引号以使用关键词: %s - 关键词有冲突声明: %s - 关键词太短,需要引号: %s - 最新投稿已显示 - 最新投稿已隐藏 - 隐藏最新投稿 - 最新视频按钮已显示 - 最新视频按钮已隐藏 - 隐藏最新视频按钮 - 显示赞和踩按钮 - 隐藏赞和踩按钮 - 隐藏赞和踩按钮 - 实时聊天消息已显示\n\n此设置也适用于 Shorts - 实时聊天消息已隐藏\n\n此设置也适用于 Shorts - 隐藏实时聊天消息 - 实时聊天重播按钮已显示\n\n关闭实时聊天时它会以全屏显示 - 实时聊天重播按钮已隐藏\n\n在关闭实时聊天时它会以全屏显示 - 隐藏实时聊天重播按钮 - 从主页隐藏未订阅的频道上传的且播放量少于 1,000 的推荐视频 - 隐藏低播放量的视频 - 医疗面板已显示 - 医疗面板已隐藏 - 隐藏医疗面板 - 商品栏已显示 - 商品栏已隐藏 - 隐藏商品栏 - 合辑播放列表已显示 - 合辑播放列表已隐藏 - 隐藏合辑播放列表 - 电影面板已显示 - 电影面板已隐藏 - 隐藏电影栏 - 导航栏已显示 - 导航栏已隐藏 - 隐藏导航栏 - 创建按钮已显示 - 创建按钮已隐藏 - 隐藏创建按钮 - 首页按钮已显示 - 首页按钮已隐藏 - 隐藏首页按钮 - 导航栏已显示 - 导航栏已隐藏 - 隐藏导航栏 - 库按钮已显示 - 库按钮已隐藏 - 隐藏库按钮 - 通知按钮已显示 - 通知按钮已隐藏 - 隐藏通知按钮 - Shorts 按钮已显示 - Shorts 按钮已隐藏 - 隐藏 Shorts 按钮 - 订阅按钮已显示 - 订阅按钮已隐藏 - 隐藏订阅按钮 - 通知按钮已显示 - 通知按钮已隐藏 - 隐藏动态中的通知按钮 - 付费推广横幅已显示 - 付费推广横幅已隐藏 - 隐藏付费推广横幅 - 可播放内容已显示 - 可播放内容已隐藏 - 隐藏可播放内容 - 自动播放按钮已显示 - 自动播放按钮已隐藏 - 隐藏自动播放按钮 - 字幕按钮已显示 - 字幕按钮已隐藏 - 隐藏字幕按钮 - 投屏按钮已显示 - 投屏按钮已隐藏 - 隐藏投屏按钮 - 折叠按钮已显示 - 折叠按钮已隐藏 - 隐藏折叠按钮 - 氛围模式菜单已显示 - 氛围模式菜单已隐藏 - 隐藏氛围模式菜单 - 音轨菜单已显示 - 音轨菜单已隐藏 - 隐藏音轨菜单 - 字幕菜单页脚已显示 - 字幕菜单页脚已隐藏 - 隐藏字幕菜单页脚 - 字幕菜单已显示 - 字幕菜单已隐藏 - 隐藏字幕菜单 - 1080p Premium 菜单已显示 - 1080p Premium 菜单已隐藏 - 隐藏 1080p Premium 菜单 - 帮助 & 反馈菜单已显示 - 帮助 & 反馈菜单已隐藏 - 隐藏帮助 & 反馈菜单 - 使用 YouTube Music 收听菜单已显示 - 使用 YouTube Music 收听菜单已隐藏 - 使用 YouTube Music 收听菜单 - 锁定屏幕菜单已显示 - 锁定屏幕菜单已隐藏 - 隐藏锁定屏幕菜单 - 循环播放菜单已显示 - 循环播放菜单已隐藏 - 隐藏循环播放菜单 - 更多信息菜单已显示 - 更多信息菜单已隐藏 - 隐藏更多信息菜单 - 画中画菜单已显示 - 画中画菜单已隐藏 - 隐藏画中画菜单 - 播放速度菜单已显示 - 播放速度菜单已隐藏 - 隐藏播放速度菜单 - 高级控件菜单已显示 - 高级控件菜单已隐藏 - 隐藏高级控件菜单 - 画质菜单页脚已显示 - 画质菜单页脚已隐藏 - 隐藏画质菜单页脚 - 画质菜单栏已显示 - 画质菜单栏已隐藏 - 隐藏画质菜单栏 - 举报菜单已显示 - 举报菜单已隐藏 - 隐藏举报菜单 - 睡眠计时器已显示 - 睡眠计时器已隐藏 - 隐藏睡眠计时器菜单 - 稳定音量菜单已显示 - 稳定音量菜单已隐藏 - 隐藏稳定音量菜单 - 技术统计菜单已显示 - 技术统计菜单已隐藏 - 隐藏技术统计菜单 - 在 VR 中观看菜单已显示 - 在 VR 中观看菜单已隐藏 - 隐藏在 VR 中观看菜单 - 全屏按钮已显示 - 全屏按钮已隐藏 - 隐藏全屏按钮 - 上一个和下一个按钮已显示 - 上一个和下一个按钮已隐藏 - 隐藏上一个 & 下一个按钮 - 商店栏已显示 - 商店栏已隐藏 - 隐藏播放器商店栏 - YouTube Music 按钮已显示 - YouTube Music 按钮已隐藏 - 隐藏 YouTube Music 按钮 - 保存到播放列表按钮已显示 - 保存到播放列表按钮已隐藏 - 隐藏保存到播放列表按钮 - 播客部分已显示 - 播客部分已隐藏 - 隐藏播客部分 - 预览评论已显示 - 预览评论已隐藏 - 隐藏预览评论 - 这会改变评论区的大小,因此无法在评论区打开即时聊天回放 - 这不会改变评论区的大小,因此可以在评论区打开即时聊天回放 - 预览评论类型 - 推广横幅广告已显示 - 推广横幅广告已隐藏 - 隐藏推广横幅广告 - 评论按钮已显示 - 评论按钮已隐藏 - 隐藏评论按钮 - 点踩按钮已显示 - 点踩按钮已隐藏 - 隐藏点踩按钮 - 点赞按钮已显示 - 点赞按钮已隐藏 - 隐藏点赞按钮 - 实时聊天按钮已显示 - 实时聊天按钮已隐藏 - 隐藏实时聊天按钮 - 更多按钮已显示 - 更多按钮已隐藏 - 隐藏更多按钮 - 打开混合播放列表按钮已显示 - 打开混合播放列表按钮已隐藏 - 隐藏打开混合播放列表按钮 - 打开播放列表按钮已显示 - 打开播放列表按钮已隐藏 - 隐藏打开播放列表按钮 - 保存到播放列表按钮已显示 - 保存到播放列表按钮已隐藏 - 隐藏保存到播放列表按钮 - 分享按钮已显示 - 分享按钮已隐藏 - 隐藏分享按钮 - 快速操作面板已显示 - 快速操作面板已隐藏 - 隐藏快速操作面板 - "隐藏以下推荐视频: - -• 带有“仅限会员”标签的视频 -• 视频底部带有“用户还观看了”等短语的视频 -• 来自未订阅频道且观看次数少于1,000次的视频" - 隐藏推荐视频 - 相关视频叠加层已显示 - 相关视频叠加层已隐藏 - 隐藏相关视频叠加层 - 相关视频已显示 - 相关视频已隐藏 - 在相关视频中隐藏 - "该设置限制了播放器屏幕上可以加载的最大布局数量 - -如果由于服务器端的更改导致播放器屏幕的布局发生变化,未预期的布局可能会被隐藏在播放器屏幕上" - 混剪按钮已显示 - 混剪按钮已隐藏 - 隐藏混剪按钮 - 举报按钮已显示 - 举报按钮已隐藏 - 隐藏举报按钮 - 奖励按钮已显示 - 奖励按钮已隐藏 - 隐藏奖励按钮 - 搜索词历史记录中的缩略图已显示 - 搜索词历史记录中的缩略图已隐藏 - 隐藏搜索词缩略图 - 进度条信息已显示 - 进度条信息已隐藏 - 隐藏进度条信息 - 进度条跳转撤销讯息已显示 - 进度条撤销消息已隐藏 - 隐藏进度条跳转撤销消息 - 时间戳旁的章节标签已显示 - 时间戳旁的章节标签已隐藏 - 隐藏进度条章节标签 - 视频播放器进度条已显示 - 视频播放器进度条已隐藏 - 进度条缩略图预览已显示 - 进度条缩略图预览已隐藏 - 隐藏进度条缩略图预览 - 隐藏视频播放器进度条 - 自我推广卡片已显示 - 自我推广卡片已隐藏 - 隐藏自我推广卡片 - 关于菜单已显示 - 关于菜单已隐藏 - 隐藏关于菜单 - 辅助功能菜单已显示 - 辅助功能菜单已隐藏 - 隐藏辅助功能菜单 - 账户菜单已显示 - 账户菜单已隐藏 - 隐藏账户菜单 - 自动播放菜单已显示 - 自动播放菜单已隐藏 - 隐藏自动播放菜单 - 账单和支付菜单已显示 - 账单和支付菜单已隐藏 - 隐藏账单和支付菜单 - 字幕菜单已显示 - 字幕菜单已隐藏 - 隐藏字幕菜单 - 已连接的应用菜单已显示 - 已连接的应用菜单已隐藏 - 隐藏已连接应用菜单 - 数据保存菜单已显示 - 数据保存菜单已隐藏 - 隐藏数据保存菜单 - 常规菜单已显示 - 常规菜单已隐藏 - 隐藏常规菜单 - 管理全部历史记录菜单已显示 - 管理全部历史记录菜单已隐藏 - 隐藏管理全部历史记录菜单 - 实时聊天菜单已显示 - 实时聊天菜单已隐藏 - 隐藏实时聊天菜单 - 通知菜单已显示 - 通知菜单已隐藏 - 隐藏通知菜单 - 背景菜单已显示 - 背景菜单已隐藏 - 隐藏背景菜单 - 在 TV 中观看菜单已显示 - 在 TV 中观看菜单已隐藏 - 隐藏在 TV 中观看菜单 - 家庭中心菜单已显示 - 家庭中心菜单已隐藏 - 隐藏家庭中心菜单 - 新实验性功能菜单已显示 - 新实验性功能菜单已隐藏 - 隐藏新实验性功能菜单 - 隐私菜单已显示 - 隐私菜单已隐藏 - 隐藏隐私菜单 - 购买和会员菜单已显示 - 购买和会员菜单已隐藏 - 隐藏购买与会员菜单 - 隐藏 YouTube 设置菜单中的元素 - YouTube 设置菜单 - 视频质量首选项菜单已显示 - 视频质量偏好菜单已隐藏 - 隐藏视频质量偏好菜单 - 您在 YouTube 菜单中的数据已显示 - 您在 YouTube 菜单中的数据已隐藏 - 在 YouTube 菜单中隐藏您的数据 - 分享按钮已显示 - 分享按钮已隐藏 - 隐藏分享按钮 - 商店按钮已显示 - 商店按钮已隐藏 - 隐藏商店按钮 - 购物链接已显示 - 购物链接已隐藏 - 隐藏购物链接 - 频道栏已显示 - 频道栏已隐藏 - 隐藏频道栏 - 评论按钮已显示 - 评论按钮已隐藏 - 隐藏评论按钮 - 已禁用评论按钮或显示“0”的标签已显示 - 已禁用评论按钮或显示“0”的标签已隐藏 - 隐藏已禁用评论按钮 - 点踩按钮已显示 - 点踩按钮已隐藏 - 隐藏点踩按钮 - "‘使用此声音’等浮动按钮已在短视频频道标签中显示" - "‘使用此声音’等浮动按钮已在 Shorts 频道标签中隐藏" - 隐蔽悬浮按钮 - 视频链接标签已显示 - 视频链接标签已隐藏 - 隐藏完整视频链接标签 - 绿幕按钮已显示 - 绿幕按钮已隐藏 - 隐藏绿幕按钮 - 信息面板已显示 - 信息面板已隐藏 - 隐藏信息面板 - 加入按钮已显示 - 加入按钮已隐藏 - 隐藏加入按钮 - 点赞按钮已显示 - 点赞按钮已隐藏 - 隐藏点赞按钮 - 实时聊天栏已显示\n\n其中的返回按钮不会隐藏 - 实时聊天栏已隐藏\n\n其中的返回按钮不会隐藏 - 隐藏实时聊天栏 - 位置按钮已显示 - 位置按钮已隐藏 - 隐藏位置按钮 - 导航栏已显示 - 导航栏已隐藏 - 隐藏导航栏 - 付费推广横幅标签已显示 - 付费推广横幅已隐藏 - 隐藏付费推广横幅 - 已暂停的标题已显示 - 已暂停的标题已隐藏 - 隐藏已暂停的标题 - 暂停时叠加按钮已显示 - 暂停时叠加按钮已隐藏 - 隐藏暂停时叠加按钮 - 按钮背景已显示 - 按钮背景已隐藏 - 隐藏播放 & 暂停按钮背景 - 混剪按钮已显示 - 混剪按钮已隐藏 - 隐藏混剪按钮 - 保存音乐按钮已显示 - 保存音乐按钮已隐藏 - 隐藏保存音乐按钮 - 搜索建议按钮已显示 - 搜索建议按钮已隐藏 - 隐藏搜索建议按钮 - 分享按钮已显示 - 分享按钮已隐藏 - 隐藏分享按钮 - 频道中的社区帖子已显示 - "已在频道中隐藏 - -信息: -• 仅隐藏主页选项卡上带有 Shorts 标题的架子" - 隐藏频道中的社区帖子 - 在观看历史中显示 - 在观看历史中隐藏 - 在观看历史中隐藏 - 在首页和相关视频中显示 - 在首页和相关视频中隐藏 - 在首页和相关视频中隐藏 - 搜索结果中的短视频已显示 - 搜索结果中的短视频已隐藏 - 隐藏搜索结果中的短视频 - 订阅中的短视频已显示 - 订阅中的短视频已隐藏 - 隐藏订阅中的短视频 - "隐藏 Shorts 栏 - -已知问题:搜索结果中的官方标题将被隐藏" - 隐藏 Shorts 栏 - 商店按钮已显示 - 商店按钮已隐藏 - 隐藏商店按钮 - 商店按钮已显示 - 商店按钮已隐藏 - 隐藏商店按钮 - 声音按钮已显示 - 声音按钮已隐藏 - 隐藏声音按钮 - 元数据标签已显示 - 元数据标签已隐藏 - 隐藏声音元数据标签 - 贴纸已显示 - 贴纸已隐藏 - 隐藏贴纸 - 订阅按钮已显示 - 订阅按钮已隐藏 - 隐藏订阅按钮 - 超级感谢按钮已显示 - 超级感谢按钮已隐藏 - 隐藏超级感谢按钮 - 标记的产品已显示 - 标记的产品已隐藏 - 隐藏标记的产品 - 工具栏已显示 - 工具栏已隐藏 - 隐藏工具栏 - 趋势按钮已显示 - 趋势按钮已隐藏 - 隐藏趋势按钮 - 使用模板按钮已显示 - 使用模板按钮已隐藏 - 隐藏使用模板按钮 - 使用此声音按钮已显示 - 使用此声音按钮已隐藏 - 隐藏使用此声音按钮 - 标题已显示 - 标题已隐藏 - 隐藏视频标题 - “显示更多”按钮已显示 - “显示更多”按钮已隐藏 - 隐藏“显示更多”按钮 - 弹出消息已显示 - 弹出消息已隐藏 - 隐藏弹出消息 - 开始试用按钮已显示 - 开始试用按钮已隐藏 - 隐藏开始试用按钮 - 订阅轮播已显示 - 订阅轮播已隐藏 - 隐藏订阅轮播 - 操作建议已显示 - 操作建议已隐藏 - 隐藏操作建议 - "This setting has been deprecated. - -Instead, use the 'Settings → Autoplay → Autoplay next video' setting. - -Note: -• If you have any issues with 'Suggested video end screen', try restarting the app." - 推荐视频结束界面已显示 - "关闭自动播放时,推荐视频结束界面会隐藏 - - 可以在 YouTube 设置中更改自动播放: - “设置→自动播放→自动播放下一个视频”" - 隐藏推荐视频结束界面 - 感谢按钮已显示 - 感谢按钮已隐藏 - 隐藏感谢按钮 - 购票栏已显示 - 购票栏已隐藏 - 隐藏购票栏 - 时间戳已显示 - 时间戳已隐藏 - 隐藏时间戳 - (评论)时间跳转已显示 - (评论)时间跳转已隐藏 - 隐藏时间跳转 - Cast 按钮已显示 - Cast 按钮已隐藏 - 隐藏 Cast 按钮 - 创建按钮已显示 - 创建按钮已隐藏 - 隐藏创建按钮 - 通知按钮已显示 - 通知按钮已隐藏 - 隐藏通知按钮 - 转写文稿部分已显示 - 转写文稿部分已隐藏 - 隐藏转写文稿部分 - 视频广告已显示 - 视频广告已隐藏 - 隐藏视频广告 - "主页/订阅/搜索结果被过滤以隐藏视图小于或大于指定数字的视频 - -限制: -• 不能隐藏 Shorts -• 不能过滤 0 播放量的视频" - 关于播放量过滤 - 首页订阅内容的关键词过滤已禁用 - 首页订阅内容的关键词过滤已启用 - 启用首页播放量过滤器 - 搜索结果过滤器已禁用 - 搜索结果过滤器已启用 - 启用搜索播放量过滤 - 订阅视频的关键词过滤器已禁用 - 订阅视频的关键词过滤器已启用 - 启用订阅视频播放量过滤器 - 隐藏播放量低于此数字的推荐视频 - 播放量 - 播放量大于此数字的视频将被隐藏。 - 高于播放量 - 播放量低于此数字的视频将被隐藏。 - 低于播放量 - 千 -> 1 000\n百万 -> 1 000 000\n十亿 -> 1 000 000 000\n播放量 -> views - 指定你的语言模板来修饰用户界面中每个视频下显示的播放量。每个关键字(在你的语言中的一个字/词) -> 值(关键字的含义) 必须单独一行。在“->”符号之前是关键字。如果更改应用程序或系统语言,则需要重置此设置。\n\n示例:\n英语:10K views = K -> 1000,views -> views\n西班牙语:10 K vistas = K -> 1000,vistas -> views - 播放量的值 - 商品介绍横幅已显示 - 商品介绍横幅已隐藏 - 隐藏商品介绍横幅 - 语音搜索按钮已显示 - 语音搜索按钮已隐藏 - 隐藏语音搜索按钮 - 网页搜索结果已显示 - 网页搜索结果已隐藏 - 隐藏网页搜索结果 - YouTube 涂鸦已显示 - YouTube 涂鸦已隐藏 - 隐藏 YouTube 涂鸦 - "YouTube 涂鸦每年显示几天 - -如果 YouTube 涂鸦目前在您的地区显示且此设置已开启,搜索栏下方的过滤栏也会隐藏" - 缩放叠加层已显示 - 缩放叠加层已隐藏 - 隐藏缩放叠加层 - Afn Blue - Afn Red - Custom - Stock - MMT - MMT Blue - MMT Green - MMT Yellow - Revancify Blue - Revancify Red - Revancify Yellow - Vanced Black - Vanced Light - Xisr Yellow - YouTube - 在全屏状态下关闭和打开屏幕时,保持横屏模式 - 强制横屏模式的毫秒数 - 保持横屏模式超时 - 保持横屏模式 - 默认 - 双击操作已禁用 - "双击操作已启用 - -• Modern 1:双击可将最小化的视频更改为更大的尺寸 -• Modern 2, 3:双击可关闭最小化的视频" - 双击操作 - 拖放已禁用 - 拖放已启用 - 启用拖放 - 展开和关闭按钮已显示 - 按钮已隐藏\n(滑动迷你播放器来展开或关闭) - 隐藏展开和关闭按钮 - 跳过和返回按钮已显示 - 跳过和返回按钮已隐藏 - 隐藏跳过和返回按钮 - 子文本已显示 - 子文本已隐藏 - 隐藏子文本 - 迷你播放器的叠加层不透明度必须介于 0-100 之间 重置为默认值 - 不透明度值介于 0-100 之间,0 为透明 - 叠加层不透明度 - 原版 - 手机 - 平板 - Modern 1 - Modern 2 - Modern 3 - 迷你播放器样式 - 叠加层按钮 - "点击可保持循环播放状态 -长按在循环播放后暂停" - 循环播放按钮 - "点击复制视频链接 -长按复制带时间戳的视频链接" - "点击复制带时间戳的视频链接 -长按复制时间戳" - 带时间戳的复制链接按钮 - 复制链接按钮 - 点击以打开外部下载器 - 外部下载按钮 - 点击以静音当前视频 再次点击取消静音 - 显示静音按钮 - 长按以更改按钮状态 - 重置播放速度: %sx - "点击选择视频播放速度 -长按设置默认播放速度(1.0x)" - 播放速度按钮 - "点按生成频道中从最旧到最新的所有视频的播放列表 -长按撤消" - 显示按时间排序的播放列表按钮 - 点按打开白名单对话框 -长按打开白名单设置对话框 - 显示白名单按钮 - 播放列表下载按钮打开应用内下载器 - 播放列表下载按钮打开外部下载器 - 覆盖播放列表下载按钮 - 视频下载按钮打开应用内下载器 - 视频下载按钮打开外部下载器 - 覆盖视频下载按钮 - 需要 YouTube Music 来覆盖按钮操作,点击此处下载 YouTube Music - 前提条件 - YouTube Music 按钮会打开原生应用 - YouTube Music 按钮会打开 RVX Music - 覆盖 YouTube Music 按钮 - Excluded - Included - 正常 - 操作按钮 - 其他设置 - 动画/反馈 - 下载按钮 - 实验性功能 - 图像区域限制 - 导入 / 导出为文件 - 导入 / 导出为文本 - 关键词过滤器 - 其他 - 叠加按钮 - 补丁信息 - 快速操作 - 推荐视频 - Shorts 栏 - 操作建议 - 使用的工具 - 观看次数过滤器 - 隐藏或显示账户菜单和“你”选项卡中的元素 - 账户菜单 - 隐藏或显示视频下方的操作按钮 - 操作按钮 - 广告 - 替换缩略图 - 绕过氛围模式限制或禁用氛围模式 - 氛围模式 - 在首页、搜索和相关视频中隐藏或显示类别栏 - 类别栏 - 隐藏或显示视频下方的频道栏组件 - 频道栏 - 隐藏或显示频道简介中的组件 - 频道简介 - 隐藏或显示评论区组件 - 评论 - 在首页和频道中隐藏或显示社区帖子 - 社区帖子 - 使用自定义过滤器隐藏组件 - 自定义过滤器 - 隐藏或显示首页中的弹出菜单 - 弹出菜单 - 首页 - 隐藏或更改与全屏相关的组件 - 全屏 - 常规设置 - 禁用或启用触觉反馈 - 触觉反馈 - 覆盖应用内按钮的点击动作 - 挂钩按钮 - 导入或导出设置 - 导入/导出设置 - 更改应用最小化播放器样式 - 迷你播放器 - 杂项 - 隐藏或显示导航栏区域的组件 - 导航栏 - 已应用补丁的信息 - 补丁信息 - 隐藏或显示视频中的按钮 - 播放器按钮 - 隐藏或更改视频播放器中的弹出菜单 - 弹出菜单 - 播放器 - 恢复 YouTube 用户名 - 恢复 YouTube 点踩 - SponsorBlock - 自定义进度条组件 - 进度条 - 隐藏 YouTube 设置菜单中的元素 - 设置菜单 - 隐藏或显示短视频播放器中的组件 - Shorts 播放器 - Shorts - 伪装流媒体数据以防止播放问题 - 伪装流媒体数据 - 滑动控制 - 隐藏或更改工具栏上的组件,如工具栏按钮、搜索栏、标题 - 工具栏 - 隐藏或显示视频描述组件 - 视频描述 - 按关键词或观看次数隐藏视频 - 视频筛选 - 视频 - 更改与观看历史记录相关的设置 - 观看历史 - 快速操作顶部边距必须在 0-32 之间 重置为默认值 - 配置从进度条到快速操作容器的间距,范围在0-32之间 - 快速操作顶部边距 - "强制拒绝软件 AV1 编解码器响应约 20 秒缓冲后,切换到其他编解码器" - 拒绝软件 AV1 编解码器切换 - 切换过程会导致约 20 秒缓冲 - 偏移 - 播放速度更改仅适用于当前视频 - 播放速度更改适用于所有视频 - 记住播放速度更改 - 更改默认播放速度时,不显示 Toast - 更改默认播放速度时,显示 Toast - 显示 Toast - 将默认速度更改为 %s - 画质更改仅适用于当前视频 - 画质更改适用于所有视频 - 记住视频画质更改 - 更改默认视频画质时,不显示 Toast - 更改默认视频画质时,显示 Toast - 显示 Toast - 将默认移动数据画质更改为 %s - 无法设置视频画质 - 将默认 WiFi 画质更改为 %s - "移除查看器的自由裁量对话框 -这不会绕过年龄限制它只会自动同意" - 移除查看器的自由裁量对话框 - 使用 VP9 编解码器替换软件 AV1 编解码器 - 替换软件 AV1 编解码器 - 频道标识已使用 - 频道名称已使用 - 替换频道标识 - 点击显示剩余时间 - 点击打开播放速度或视频画质弹出菜单 - 替换时间戳操作 - 用设置按钮替换创建按钮 - 替换创建按钮 - "点击打开 YouTube 设置长按打开 RVX 设置" - "点击打开 RVX 设置长按打开 YouTube 设置" - 分配给按钮的操作类型 - 在全屏模式下显示进度条缩略图 - 将进度条缩略图显示在进度条上方 - 恢复旧的进度条缩略图 - 不显示旧的视频画质菜单 - 显示旧的视频画质菜单 - 恢复旧的视频画质菜单 - \@handle (用户名) - 显示格式 - 用户名 (@handle) - 用户名 - Handle 已使用 - 用户名已使用 - 启用恢复 YouTube 用户名 - "需要 YouTube Data API v3 开发者密钥才能将 handle 替换为用户名 - -免费计划下的 API 密钥每日配额为 10,000,其中每替换 1 条评论的 handle 为用户名会消耗 1 个配额 - -点击查看如何获取 API 密钥" - 关于 YouTube Data API 密钥 - 使用 YouTube Data API v3 的开发者密钥 - YouTube Data API 密钥 - 1. 前往 <a href=%1$s> 创建一个新项目 </a>.<br>2. 点击 <b> 创建 </b> 按钮 <br>3. 前往 <a href=%2$s> YouTube Data API v3 </a>.<br>4. 点击 <b> 启用 </b> 按钮 <br>5. 点击 <b> 创建凭据 </b> 按钮 <br>6. 选择 <b> 公共数据 </b> 选项 <br>7. 点击 <b> 下一步 </b> 按钮 <br>8. 复制 API 密钥 <br><br>※ API 密钥绝不可与他人分享,因此不包含在导入 / 导出设置中 - 获取 YouTube Data API v3 开发者密钥 - 关于 - 点踩数据由 Return YouTube Dislike API 提供。点击了解更多信息 - ReturnYouTubeDislike.com - 点赞按钮样式:最佳显示 - 点赞按钮样式:最小宽度 - 紧凑点赞按钮 - 点踩显示为数字 - 点踩显示为百分比 - 点踩百分比 - 点踩数已隐藏 - 点踩数已显示 - 恢复 YouTube 点踩 - 预估点赞数已隐藏 - 预估点赞数已显示 - 显示预估点赞数 - 点踩数不可用(已达到客户端 API 限制) - 点踩数不可用(状态 %d) - 点踩数暂时不可用(API 连接超时) - 点踩数不可用(%s) - 重新加载视频以使用 Return YouTube Dislike 进行投票 - 点踩数已在 Shorts 中隐藏 - 点踩数已在 Shorts 中显示 - "在 Shorts 中显示点踩数 - -限制:在无痕模式下,点踩数可能不会显示" - 在 Shorts 中显示点踩数 - 如果恢复 YouTube 点踩不可用,不显示 Toast - 如果恢复 YouTube 点踩不可用,则显示 Toast - 如果 API 不可用,显示 Toast - 隐藏 - 共享链接时,删除 URL 中的跟踪查询参数 - 清理共享链接 - "像'#', 'Shop' 和 'N products' 这样的词组已在视频字幕中显示" - "像'#', 'Shop' 和 'N products' 这样的词组已在视频字幕中隐藏" - 清理视频字幕 - 关于 - sponsor.ajay.app - 数据由 SponsorBlock API 提供,点击此处了解更多信息并查看其他平台的下载 - API URL 已更改 - API URL 无效 - API URL 已重置 - 外观 - 颜色已更改 - 颜色: - 无效的颜色代码 - 颜色已重置 - 创建新片段 - 更改片段行为 - 自动隐藏跳过按钮 - 整个片段显示跳过按钮 - 几秒后隐藏跳过按钮 - 使用紧凑型跳过按钮 - 跳过按钮样式适合最佳外观 - 跳过按钮样式适合最小宽度 - 显示创建新片段按钮 - 不显示创建新片段按钮 - 显示创建新片段按钮 - 启用 SponsorBlock - SponsorBlock 是一个众包系统,用于跳过 YouTube 视频中的烦人部分 - 显示投票按钮 - 不显示片段投票按钮 - 显示片段投票按钮 - 常规设置 - 调整新片段步长 - 数值必须为正数 - 创建新片段时,时间调整按钮移动的毫秒数 - 更改 API URL - SponsorBlock 用于向服务器发出调用的地址 - 最短片段持续时间 - 无效的时长 - 短于此值 (以秒为单位) 的片段将不会被显示或跳过 - 启用跳过计数跟踪 - 未启用跳过计数跟踪 - 允许 SponsorBlock 排行榜了解节省了多少时间每次跳过片段时都会向排行榜发送消息 - 自动跳过时显示提示 - 不显示提示点击此处查看示例 - 自动跳过片段时显示提示点击此处查看示例 - 显示不含片段的视频长度 - 显示完整视频长度 - 显示视频长度减去所有片段的长度,括号中显示完整视频长度 - 您的私人用户 ID - 私人用户 ID 长度必须至少为30个字符 - 此 ID 应被保密。这类似于密码,不应与任何人分享。如果有人获取此 ID,他就可以冒充您 - 已阅读 - 在创建新片段之前,请阅读 SponsorBlock 指南 - 查看 - 遵循指南 - 指南包含创建新片段的规则和技巧 - 查看指南 - 调整:标记片段的开始和结束时间 - 选择片段类别 - 验证片段 - The segment lasts from %1$02d:%2$02d to %3$02d:%4$02d (%5$d minutes %6$02d seconds)\nIs it ready to submit? - 该片段从\n\n%1$s\n到\n%2$s\n\n(%3$s)\n\n准备好提交了吗? - 时间是否正确? - 类别在设置中被禁用请启用类别以提交 - 编辑片段 - 是否要手动编辑片段的开始或结束时间? - 提供的时间无效 - 手动编辑片段时间 - 前进指定时间(默认:150ms) - 将 %s 设为新片段的开头或结尾? - 结束 - 首先在时间轴上标记两个位置 - 开始 - 立即 - 预览片段,并确保跳过顺畅 - 发布已创建的片段 - 后退指定时间(默认:150ms) - 开始时间必须早于结束时间 - 片段结束时间 - 片段开始时间 - 新的 SponsorBlock 片段 - 重置 - 重置颜色 - 灌水内容/笑话 - 跑题片段,例如闲聊或幽默,对理解视频主要内容并无帮助。这不会包含叙述环境与背景信息的片段 - 突出显示 - 大家都在搜的视频 - 交互提醒(订阅) - 视频中间简短提醒观众来点赞、订阅或关注。 如果片段较长,或是关于某个具体事物,则应分类为自我推广 - 过场/开场动画 - 没有实际内容的间隔片段。可以是暂停、静态帧或重复动画。不包括信息的过渡 - 音乐:非音乐片段 - 仅用于音乐视频。音乐视频的非音乐部分,尚未包含在另一个类别中 - 结束画面/演职员表 - 鸣谢画面或出现 YouTube 片尾画面不包括含信息的结尾 - 预告/回顾 - 展示此视频或同系列后续视频将出现的画面集锦,所有内容都将在之后再次出现 - 无偿广告/自我推广 - 类似于“赞助”,非付费或自我推广除外。这包括有关商品、捐赠或与他人合作的信息 - 赞助内容 - 付费推广、付费推荐和直接广告。非自我推广或免费提及、推荐他们喜欢的事物/创作者/网站/产品 - 复制 - 导出失败:%s - 导入 / 导出设置 - 您的 SponsorBlock JSON 配置,可导入 / 导出到 ReVanced Extended 和其他 SponsorBlock 平台 - 您的 SponsorBlock JSON 配置,可导入 / 导出到 ReVanced Extended 和其他 SponsorBlock 平台包含您的私人用户 ID,请谨慎分享 - 导入失败:%s - 配置文件成功导入 - 您的设置包含一个 SponsorBlock 用户 Id\n\n您的用户 Id 应该像密码一样不要分享给他人\n - 不再显示 - 设置已复制到剪贴板 - 自动跳过 - 自动跳过一次 - 跳过 - 突出显示 - 跳过灌水片段 - 跳转至突出显示 - 跳过交互提醒 - 跳过片头 - 跳过幕间 - 跳过幕间 - 跳过非音乐部分 - 跳过片尾 - 跳过预告 - 跳过总结 - 跳过预告 - 跳过自我宣传 - 跳过赞助内容 - 跳过未提交片段 - 禁用 - 仅在进度条中显示 - 显示跳过按钮 - 跳过灌水内容/笑话 - 跳过高光部分 - 跳过烦人的提醒 - 跳过开场 - 跳过幕间 - 跳过幕间 - 跳过多个片段 - 跳过非音乐部分 - 跳过片尾 - 跳过预告 - 跳过总结 - 跳过预告 - 跳过自我推广 - 跳过赞助内容 - 跳过未提交的片段 - SponsorBlock 暂时无法使用 - SponsorBlock 暂时无法使用(状态 %d) - SponsorBlock 暂时无法使用(API 超时) - 统计信息 - 统计信息暂时无法使用(API 出现问题) - 加载中... - 您的声誉为 <b>%.2f</b> - 您为人们节省了 <b>%s</b> 个片段 - %1$s 小时 %2$s 分钟 - %1$s 分钟 %2$s 秒 - %s 秒 - 这相当于他们生活中的 <b>%s</b><br>点击此处查看排行榜 - 点击此处查看全球统计数据和前几名贡献者 - SponsorBlock 排行榜 - SponsorBlock 已禁用 - 你已经跳过了 <b>%s</b> 段视频 - 重置跳过的视频片段计数器? - 一共 <b>%s</b> - 你已经创建了 <b>%s</b> 段视频 - 点击此处查看您的片段 - 你的用户名:<b>%s</b> - 点击这里更改你的用户名 - 无法更改用户名:状态:%1$d %2$s。 - 用户名成功更改。 - 无法提交该视频片段\n已存在 - 无法提交片段:%s - 无法提交片段:%s - 无法提交片段\n频率受限(来自同一用户或 IP 次数过多) - SponsorBlock 暂时无法使用 - 无法提交片段(状态:%1$d %2$s) - 片段提交成功 - 如果 SponsorBlock 不可用,则不显示提示 - 如果 SponsorBlock 不可用,则显示提示 - 如果 API 不可用,则显示提示 - 更改类别 - 反对 - 无法为片段投票:%s - 无法为片段投票(API 超时) - 无法为片段投票(状态:%1$d %2$s) - 没有可供投票的片段 - 赞成 - 设置已复制到剪贴板 - 时间戳已复制到剪贴板 (%s) - URL 已复制到剪贴板 - 带有时间戳的 URL 已复制到剪贴板 - 原版 - - 缩略图 (Cairo) - 心形 - 心形(着色) - 隐藏 - 双击动画 - Meta 面板底边距必须在 0-64 之间 已重置为默认值 - 配置从搜索栏到 Meta 面板的间距,范围在 0-64 之间 - Meta 面板底边距 - 高度百分比必须介于 0-100 之间(%) - 配置隐藏导航栏时空白空间的高度百分比,介于 0 到 100 之间(%) - 空白空间的高度百分比 - 长按时间戳以更改 Shorts 重复状态 - 时间戳长按操作 - "在全屏模式下显示视频标题部分 - -限制:点击后视频标题消失" - 显示视频标题部分 - 如果自动播放已开启,下一个视频将在倒计时结束后播放 - 如果自动播放已开启,下一个视频将在无倒计时的情况下播放 - 跳过自动播放倒计时 - "视频播放开始时,跳过预加载缓冲的默认视频画质 - -• 视频开始时约有 0.7 秒延迟,但默认视频分辨率会立即生效 -• 不适用于 HDR 视频或短于 10 秒的视频" - 跳过预加载缓冲 - 提示信息已隐藏 - 提示信息已显示 - 跳过时的提示信息 - 开启此选项可能会导致视频不能正常播放 - 跳过预加载缓冲 - 速度叠加值必须在 0 到 8.0 之间 重置为默认值 - 速度叠加值在 0 到 8.0 之间 - 速度叠加值 - "Spoofing the client version to the old version. - -• This will change the appearance of the app, but unknown side effects may occur. -• If later turned off, the old UI may remain until clear the app data." - 客户端版本未伪装 - 客户端版本已伪装 - 17.33.42 - 恢复旧版UI布局 - 17.41.37 - 恢复旧版的播放列表 - 18.05.40 - 恢复旧版评论输入框 - 18.17.43 - 恢复旧版播放器弹出设置面板 - 18.33.40 - 恢复旧的 Shorts 选项卡 - 18.38.45 - 恢复旧的默认视频质量行为 - \"18.48.39 - 禁止实时更新“播放量”和“喜欢次数” - 19.13.37 - 恢复旧版数字滚动动画风格 - 伪装应用版本 - 选择伪装的应用版本 - 编辑伪装应用版本 - 伪装应用版本 - "应用版本将被伪装成旧版本的 YouTube - -这将改变应用程序的外观和功能,但可能会出现未知的副作用 - -如果稍后关闭,建议清除应用数据以防止UI错误" - "伪装设备尺寸,以解锁设备可能无法提供的更高视频质量" - 伪装设备尺寸 - iOS 视频编解码器是 AVC (H.264), VP9, 或 AV1 - iOS 视频编解码器是 AVC (H.264) - 强制使用 iOS AVC (H.264) - "启用此功能可能会改善耗电并修复播放卡顿问题 - -AVC (H.264) 的最大解析度为 1080p,且视频播放将使用比 VP9 或 AV1 更多的网路数据" - "• 音轨菜单缺失" - "• 音轨菜单缺失" - "• 电影或付费视频可能无法播放" - 伪装副作用 - • 视频可能无法播放 - 用于获取流媒体数据的客户端已在统计信息中隐藏 - 用于获取流媒体数据的客户端已在统计信息中显示 - 显示统计信息 - "流媒体数据未伪装,视频可能无法正常播放" - 流媒体数据已伪装 - 伪装流媒体数据 - Android - Android TV - Android VR - iOS - 默认客户端 - 关闭此选项可能会导致视频不能正常播放 - 亮度滑动灵敏度必须介于 1-1000 之间 (%) - 配置亮度滑动的最小距离范围为 1 到 1000 (%)\n最小距离越短,亮度变化越快 - 亮度滑动灵敏度 - 在“锁定屏幕”模式下禁用滑动手势 - 在“锁定屏幕”模式下启用滑动手势 - 在“锁定屏幕”模式下滑动手势 - 自动 - 防误触的滑动幅度阈值 - 滑动幅度阈值 - 滑动叠加层背景透明度 - 滑动背景透明度 - 可滑动区域大小不能超过50 重置为默认值 - 可滑动屏幕区域的百分比\n\n注意:这也会改变双击搜索手势的屏幕区域大小 - 滑动叠加层大小 - 滑动叠加层上的文本大小 - 滑动叠加层上的文本大小 - 滑动叠加层显示的时长(毫秒) - 滑动叠加层时长 - 音量滑动灵敏度必须介于 1-1000 之间 (%) - 配置音量滑动的最小距离范围为 1 到 1000 (%)\n\n最小距离越短,音量等级变化越快\n\n推荐的音量滑动灵敏度:在 15 级音量调节为 100%,在 150 级音量调节为 10% - 音量滑动灵敏度 - "通过伪装设备信息,交换创建按钮和通知按钮的位置 - -• 更改此设置,可能需要重新启动设备才能生效 -• 禁用此设置会从服务器端加载更多广告 -• 你应该禁用此设置以显示视频广告" - 创建按钮未替换为通知按钮 - "创建按钮替换为通知按钮 - -注意:启用此选项也强制隐藏视频广告" - 交换创建和通知按钮 - "禁用此功能可能会从服务器加载更多广告 - -此外,Shorts 中的广告将不再被屏蔽 - -如果此设置未生效,请尝试切换到隐身模式" - Stock - RVX Music - %s 未安装,请先安装 - 已安装的 RVX Music 包名 - RVX Music 包名 - • 历史记录不可用 - "• 遵循谷歌帐户的观看历史设置 -• 由于 DNS 或 VPN 的原因,观看历史可能无法工作" - • 遵循谷歌帐户的观看历史设置 - 关于历史记录 - 点击以打开 YouTube 历史记录管理 - 管理所有历史记录 - 原版 - 替换域名 - 屏蔽历史记录 - 历史记录类型 - 无法将频道 \'%1$s\' 添加到 %2$s 白名单 - 频道 \'%1$s\'已添加到 %2$s 白名单 - 白名单中没有频道 - 未添加到白名单 - 加载频道信息失败 - 加入白名单 - 播放速度 - 从 %2$s 白名单中移除频道 \'%1$s\'? - 无法将频道 \'%1$s\' 从 %2$s 白名单中移除 - 頻道 \'%1$s\' 已从 %2$s 白名单中移除 - 检查或删除添加到白名单的频道列表 - 频道白名单 - SponsorBlock - diff --git a/src/main/resources/youtube/translations/zh-rTW/missing_strings.xml b/src/main/resources/youtube/translations/zh-rTW/missing_strings.xml deleted file mode 100644 index a820effd6..000000000 --- a/src/main/resources/youtube/translations/zh-rTW/missing_strings.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - Package name of your installed external downloader app, such as NewPipe or YTDLnis, on long press. - Long press video downloader package name - AI-generated video summary section is shown. - AI-generated video summary section is hidden. - Hide AI-generated video summary section - MMT Orange - MMT Pink - MMT Turquoise - Xisr Yellow - Adjust: Mark Start and End Time for segment - Verify the Segment - Edit the Segment - Forward by Specified Time (Default: 150ms) - Publish Created Segment - Rewind by Specified Time (Default: 150ms) - diff --git a/src/main/resources/youtube/translations/zh-rTW/strings.xml b/src/main/resources/youtube/translations/zh-rTW/strings.xml deleted file mode 100644 index 6e36fbbde..000000000 --- a/src/main/resources/youtube/translations/zh-rTW/strings.xml +++ /dev/null @@ -1,1712 +0,0 @@ - - - 是否啟用影片播放器的無障礙控制? - 由於已啟用無障礙服務,因此您的控制項被修改。 - 繼續 - 不再顯示 - "GmsCore 沒有後台運行的權限 - -遵循“不要殺死我的應用程式!”指南,並將說明應用於您的 GmsCore 安裝。 - -這是應用程序運行所必需的" - "必須停用 GmsCore 電池優化才能防止問題 - -點擊繼續按鈕並停用電池優化" - 打開網站 - 需要採取行動 - 啟用雲端訊息傳遞以接收通知 - 打開 GmsCore - GMSCore未安裝 請安裝 - "DeArrow 提供群眾外包的 YouTube 影片縮圖。這些縮圖通常比 YouTube 提供的縮圖更具相關性。 - -如果啟用,影片 URL 會傳送到 API 伺服器,不會傳送其他資料。如果影片沒有 DeArrow 縮圖,則顯示原始或靜態截圖。 - -輕觸這裡來進一步了解關於 DeArrow 的資訊。" - 關於 DeArrow - DeArrow API URL 無效。 - DeArrow 縮圖快取端點網址。 - 縮圖快取端點的 URL - 如果 DeArrow 無法使用,不顯示提示訊息 - 如果 DeArrow 無法使用,則顯示提示訊息 - 當 API 無法使用時顯示提示訊息 - DeArrow 暫時不可用(狀態碼:%s) - DeArrow 暫時不可用 - 首頁標籤 - 你的標籤 - 原始縮圖 - DeArrow & 原始縮圖 - DeArrow & 靜態影片擷取 - 靜態影片擷取 - 播放器播放清單、推薦 - 搜尋結果 - 靜態影片擷取 - 靜態捕捉是截取每個影片的開頭/中間/結尾作為縮圖。 這些圖像內建於 YouTube 中,不會使用外部 API。 - 關於靜態影片擷取。 - 使用高品質的靜態捕捉 - 使用中等品質仍然可以捕獲縮略圖加載速度會更快,但直播、未發佈和非常舊的影片可能會顯示空白縮略圖 - 使用快速靜態捕捉 - 影片開頭 - 影片中間 - 影片結尾 - 拍攝靜態捕捉的時間 - 訂閱標籤 - 添加時間戳訊息已關閉 - "添加時間戳訊息已顯示" - 添加時間戳訊息 - 附加播放速度。 - 附加影片品質。 - 添加訊息類型 - 省電模式下微光模式已停用 - 省電模式下微光模式已啟用 - 解除微光模式限制 - 從中取得圖像網域。\n注意:只輸入域名, i.e., 不含 \"https\:\/\/\" 前綴。 - 替代域名 - 使用原圖主機。\n\n啟用此功能可以修復某些區域被封鎖的遺失影像。 - 使用圖像主機 yt4.ggpht.com - 繞過影像區域限制 - 原始 - 手機 - 手機 (最大 480 dip) - 平板電腦 - 平板電腦 (最少 600 dip) - 變更佈局 - 使用開關切換 - 使用檔案切換 - 更改切換類型 - 使用應用程式內分享擴展。 - 使用系統分享擴展。 - 更改分享擴展 - 自動播放 - 預設 - 暫停 - 重複播放 - 更改短片重複狀態 - 瀏覽頻道 - 課程 / 學習 - 預設 - 探索 - 遊戲 - 歷史 - 媒體庫 - 喜歡的影片 - 直播 - 電影 - 音樂 - 搜尋 - 短片 - 運動 - 訂閱 - 熱門 - 稍後觀看 - 更改起始頁 - 起始頁僅更改一次。 - "起始頁總是改變。 - -限制:工具列上的後退按鈕可能無法運作。" - 變更起始頁類型 - 預設標題已啟用 - 高級標題已啟用 - 變更 YouTube 標題 - 以換行分隔的名稱來過濾元件 - 編輯自訂篩選器 - 自訂篩選已停用 - 自訂篩選已啟用 - 啟用自訂篩選器 - 無效的自訂篩選器:%s - 使用舊版彈出選單 - 使用自定義對話框 - 自定義播放速度選單類型 - 自訂速度必須小於 %sx -使用預設值 - 無效的自定播放速度將使用預設值 - 添加或更改播放速度 - 編輯自定義播放速度 - 播放器疊加層不透明度必須介於 0-100 之間。重設為預設值。 - 不透明度值介於 0-100 之間,0 為透明 - 自訂播放器的不透明度 - 輸入套用於搜尋欄的十六進制顏色代碼 - 自定義進度條顏色 - 若要以外部瀏覽器開啟 RVX,請在設定開啟「開啟支援連結」並啟用支援的網址。 - 開啟預設應用程式設定 - 預設播放速度 - 行動數據的預設影片畫質 - Wi-Fi 預設的影片畫質 - 在全螢幕狀態下,將微光效果關閉 - 全螢幕模式下啟用微光模式。 - 全螢幕模式下停用微光模式。 - 在全螢幕時停用微光模式 - 永遠停用微光效果 - 已啟用微光效果。 - 微光模式已停用。 - 停用微光模式 - 強制自動音軌已啟用 - 強制自動音軌已停用 - 停用強制自動音軌 - 強制顯示字幕已啟用 - 強制顯示字幕已停用 - 停用強制顯示字幕 - 自動播放器彈出面板已停用 - 自動播放器彈出面板已啟用 - 停用播放器彈出面板 - "自動播放開啟時會啟用自動切換混合播放清單。 - -可以在 YouTube 設定中變更自動播放: -設定 → 自動播放 → 自動播放下一個影片" - 自動切換混合播放清單已停用。 - 停用切換混合播放列表 - 啟用此功能將禁止在自動播放開啟時播放音樂時自動切換到 YouTube Mix。 - 直播預設播放速度已啟用 - 直播預設播放速度已停用 - 在直播中停用預設播放速度 - 音樂的預設播放速度已啟用。 - "音樂的預設播放速度被停用。 - -限制:此設定可能不適用於不包含「在 YouTube 音樂上收聽」橫幅影片。" - 停用音樂的播放速度 - 啟用互動面板 - 停用互動面板 - 停用互動面板 - 章節觸覺反饋啟用 - 章節觸覺反饋已停用 - 停用章節觸覺反饋 - 觸覺反饋已啟用。 - 觸覺反饋已停用。 - 停用滑動觸覺反饋 - 進度觸覺反饋已啟用 - 進度觸覺反饋已停用 - 停用進度觸感反饋 - 進度條跳轉撤銷的震動反饋已啟用 - 進度條跳轉撤銷的震動反饋已停用 - 停用進度條跳轉撤銷的震動反饋 - 縮放觸覺反饋已啟用 - 縮放觸覺反饋已停用 - 停用縮放觸覺反饋 - HDR 影片自動亮度已啟用 - HDR 影片自動亮度已停用 - 停用 HDR 影片自動亮度 - HDR 影片已停用 - HDR 影片已啟用 - HDR 影片 - 進入全螢幕時橫向模式已啟用 - 進入全螢幕時橫向模式已停用 - 停用橫向模式 - 讚和反讚按鈕在提及時會發光。 - 讚和反讚按鈕在提及時不會發光。 - 停用讚和反讚按鈕發光 - "停用 Cronet 引擎的 QUIC 協議" - 停用 QUIC 協議 - 短片播放器在應用程式啟動時會恢復播放。 - 短片播放器在應用程式啟動時不會恢復播放。 - 停用恢復短片播放器 - 滾動動畫已啟用 - 滾動動畫已停用 - 停用滾動數字動畫 - 搜尋欄中的章節已啟用。 - 搜尋欄中的章節已停用。 - 停用搜尋列章節 - 點讚按鈕上方的噴泉動畫已啟用。 - 點讚按鈕上方的噴泉動畫已停用。 - 停用讚按鈕動畫 - "停用「按住即可將播放速度設為 2 倍」功能 - -注意: -・停用速度覆蓋層會恢復舊版面配置的「滑動以尋找」行為。 -・停用這項設定不會強制啟用速度覆蓋層。" - 停用速度疊加 - 啟動動畫已啟用 - 啟動動畫已停用 - 停用啟動動畫 - "當影片描述展開時,停用以下互動: - -・點擊捲動。 -・點擊並按住以選擇文字。" - 停用影片描述交互 - VP9 編解碼器已啟用。 - "VP9 編解碼器已停用。 - -• 最高解析度為 1080p。 -• 影片播放將使用比 VP9 更多的網路數據。 -• 若要獲得 HDR 播放,HDR 影片仍會使用 VP9 編解碼器。" - 停用 VP9 編解碼器 - 開羅搜尋欄已停用。 - "開羅搜尋欄已啟用。 -副作用:開羅主題也適用於通知點。" - 啟用開羅搜尋欄 - 緊湊的播放器佈局已停用 - 精簡控制選單已啟用 - 啟用緊湊的播放器佈局疊加層 - 自定義播放速度已停用 - 自定義播放速度已啟用 - 啟用自定義播放速度 - 自定義進度條顏色已停用 - 自定義進度條顏色已啟用 - 啟用自訂搜尋欄顏色 - Debug 日誌不包括緩衝區 - Debug 日誌不包括緩衝區 - 啟用 Debug 緩衝區日誌記錄 - Debug 日誌已停用 - Debug 日誌已啟用 - 啟用 Debug 日誌 - 預設播放速度不適用於短片 - 預設播放速度適用於短片 - 啟用短片預設播放速度 - 外部瀏覽器已停用 - 外部瀏覽器已啟用 - 啟用外部瀏覽器 - 漸變載入畫面已停用 - 漸變載入畫面已啟用 - 啟用漸變載入畫面 - 導航按鈕之間的間距不會變窄 - 導航按鈕之間的間距變窄 - 啟用窄間距導航按鈕 - 跟隨預設的重新載入行為 - 繞過連結重定向 - 啟用直接打開連結 - 如果播放器回應包含 OPUS 編解碼器,則啟用 OPUS 編解碼器。 - 啟用 OPUS 編解碼器 - 退出或進入全螢幕時不儲存和還原亮度。 - 退出或進入全螢幕時儲存和還原亮度。 - 啟用儲存和還原亮度 - 進度條點擊已停用 - 進度條點擊已啟用 - 啟用進度條點擊 - "這會將縮圖恢復到沒有搜尋欄縮圖的直播。 - -網路資料使用量可能會更高,搜尋欄縮圖在顯示之前會稍微延遲。 - -此功能在網路連線速度非常快的情況下效果最佳。" - 搜尋欄縮圖品質中等。 - 搜尋欄縮圖品質很高。 - 啟用高品質縮圖 - 時間戳已停用。 - "已知問題:由於這是 Google 開發階段的功能,因此佈局可能會被破壞。" - 啟用時間戳 - 滑動控制亮度已停用 - 滑動控制亮度已啟用 - 啟用滑動控制亮度 - 觸覺反饋已停用 - 觸覺反饋已啟用 - 啟用觸覺反饋 - 亮度手勢的最低值會啟動自動亮度。 - 亮度手勢的最低值會啟用自動亮度調節 - 啟用自動亮度手勢 - 輕觸啟動滑動手勢 - 長按啟動滑動手勢 - 啟用長按滑動手勢 - 向上/向下滑動將不會播放下一個/上一個影片。 - 向上/向下滑動將播放下一個/上一個影片。 - 啟用滑動以全屏更改影片 - 滑動控制音量已停用 - 滑動控制音量已啟用 - 音量手勢 - 導覽列不透明。 - 導覽列是半透明的。 - 啟用半透明導覽列 - 當在影片播放器下方向下滑動時,停用進入全螢幕模式 - 當在影片播放器下方向下滑動時,啟用進入全螢幕模式 - 啟用觀看面板手勢 - "啟用這項設定後,將停用「你的內容」分頁中的設定按鈕 - -在這種情況下,請依序輕觸以下路徑來存取設定: -你的內容分頁 → 瀏覽頻道 → 選單 → 設定" - 在你的內容分頁中啟用寬搜尋欄 - 寬搜尋欄已停用 - 寬搜尋列已啟用 - 啟用寬搜尋列 - 寬搜尋欄不包括 YouTube 標題 - 寬搜尋欄包括 YouTube 標題 - 啟用帶標題的寬搜尋欄 - 描述 - "在影片描述面板中輸入標題 -如果保存了不正確的字符串,則「展開影片描述」可能無法正常工作" - 影片描述面板中的標題 - 手動展開影片描述 - 自動展開影片描述 - 自動展開影片描述 - 你想繼續嗎? - 重設為預設值。 - 重新啟動以套用更改後的介面 - "YouTube 伺服器端存在一個錯誤,導致某些用戶隱藏滾動數位文字,例如按讚數、觀看次數和上傳日期。 - -此問題的臨時解決方法是將應用程式版本欺騙為 19.13.37。 - -您想在重新啟動應用程式之前偽裝應用程式版本嗎?" - 重新啟動以重新整理介面 - 導出配置失敗 - 導出配置成功 - 導出配置到文件 - 導出配置 - 導入 - 複製 - 導入 / 導出配置檔案 - 導入 / 導出配置檔案 - 導入配置失敗 - 設定重設為預設值 - 導入配置成功 - 從文件導入配置 - 導入配置 - 重置 - 搜尋設定 - ReVanced Extended - 外部下載器 - 未安裝 - "%1$s 未安裝 -請從網站下載 %2$s" - 警告 - %s 未安裝,請先安裝 - 你安裝的外部下載器應用程式的套件名稱,例如 YTDLnis。 - 播放清單下載器套件名稱 - 您安裝的外部下載器應用程式的套件名稱,例如 NewPipe 或 YTDLnis。 - 影片下載套件名 - "影片將在以下情況下切換到全螢幕模式: - -・當影片開始播放時。 -・當點擊評論中的時間戳記時。" - 強制全螢幕 - 在每次應用程式啟動時,顯示 GMSCore 的優化對話框。 - 顯示 GMSCore 優化對話框 - 要篩選的帳戶選單名稱清單,每行一個名稱 - 編輯帳號選單篩選 - "隱藏帳戶選單和你的內容分頁的元素 -部分元件可能不會隱藏" - 隱藏帳戶選單 - 專輯卡片已顯示 - 專輯卡片已隱藏 - 隱藏專輯卡片 - 特色地點, 遊戲, 和音樂部分已顯示。 - 特色地點, 遊戲, 和音樂部分均已隱藏。 - 隱藏屬性部分 - 自動播放預覽面板已顯示 - 自動播放預覽面板已隱藏 - 隱藏自動播放預覽面板 - 瀏覽商店按鈕已顯示 - 瀏覽商店按鈕已隱藏 - 隱藏瀏覽商店按鈕 - "隱藏以下分類區: -・最新消息 -・繼續觀看 -・探索更多頻道 -・再次收聽 -・購物 -・再看一次" - 隱藏輪播內容 - 在探索欄顯示 - 在探索欄隱藏 - 隱藏探索欄 - 在相關影片中顯示 - 在相關影片中隱藏 - 隱藏相關影片 - 在搜尋結果中顯示 - 搜尋結果中已隱藏 - 在搜尋結果中隱藏 - 頻道指南已顯示 - 頻道指南已隱藏 - 隱藏頻道指南 - 頻道會員清單已顯示 - 頻道會員清單已隱藏 - 隱藏頻道會員清單 - 頻道簡介頂部的連結已顯示 - 頻道簡介頂部的連結已隱藏 - 隱藏頻道個人檔案的連結 - "Shorts -播放清單 -商店" - 要篩選的頻道標籤名稱清單,每行一個名稱 - 頻道標籤篩選器 - 頻道標籤篩選器已停用 - 頻道標籤篩選器已啟用 - 啟用頻道標籤篩選器 - 頻道浮水印已顯示 - 頻道浮水印已隱藏 - 隱藏頻道浮水印 - 章節部份已顯示 - 章節部份已隱藏 - 隱藏章節部份 - 剪輯已顯示 - Chips 影片欄已隱藏 - 隱藏 Chips 影片欄 - 剪輯按鈕已顯示 - 剪輯按鈕已隱藏 - 隱藏剪輯按鈕 - 創建短片按鈕已顯示 - 創建短片按鈕已隱藏 - 隱藏創建短片按鈕 - 被標記的搜尋連結已顯示 - 被標記的搜尋連結已隱藏 - 隱藏被標記的搜尋連結 - 感謝按鈕已顯示 - 感謝按鈕已隱藏 - 隱藏感謝按鈕 - 時間戳和表情按鈕已顯示 - 時間戳和表情按鈕已隱藏 - 隱藏時間戳和表情按鈕 - 會員的評論橫幅已顯示 - 會員的評論橫幅已隱藏 - 隱藏會員評論橫幅 - 首頁動態中評論部分已顯示 - 首頁動態中評論部分已隱藏 - 隱藏首頁動態中的評論部分 - 評論部分已顯示 - 評論部分已隱藏 - 隱藏評論部分 - 顯示在頻道裡 - 頻道中的社群貼文隱藏 - 隱藏頻道中的社群貼文 - 在探索首頁和相關影片中已顯示 - 在探索首頁和相關影片中已隱藏 - 隱藏在探索首頁和相關影片中 - 訂閱內容中的社群貼文已顯示 - 訂閱內容中的社群貼文已隱藏 - 隱藏訂閱內容中的社群貼文 - 「此內容的製作過程」區已顯示。 - 「此內容的製作過程」區已隱藏。 - 隱藏內容區 - 募資已顯示 - 募款活動已隱藏 - 隱藏募款活動 - 雙擊覆蓋過濾器已顯示。 - 雙擊覆蓋過濾器已隱藏。 - 隱藏雙擊覆蓋過濾器 - 顯示下載按鈕 - 隱藏下載按鈕 - 隱藏下載按鈕 - 片尾資訊卡已顯示 - 片尾資訊卡已隱藏 - 隱藏結束界面卡片 - 影片下方的章節選擇欄已顯示 - 影片下方的章節選擇欄已隱藏 - 隱藏影片下方的章節選擇欄 - 可展開的選單已顯示。 - 可展開的選單已隱藏。 - 隱藏可展開的選單 - 字幕按鈕已顯示 - 字幕按鈕已隱藏 - 隱藏動態字幕按鈕 - 要過濾的動態彈出選單名稱清單,每行一個名稱 - 編輯資訊彈出式選單篩選 - 資訊彈出式選單已顯示 - 資訊彈出式選單已隱藏 - 隱藏資訊彈出式選單 - 動態搜尋欄已顯示 - 動態搜尋欄已隱藏 - 隱藏動態搜尋欄 - 動態問卷調查已顯示 - 動態問卷調查已隱藏 - 隱藏動態問卷調查 - 影片條覆蓋層已顯示 - 影片條覆蓋層已隱藏 - 隱藏影片條覆蓋層 - 浮動按鈕已顯示。 - 浮動按鈕已隱藏。 - 隱藏浮動按鈕 - 語音辨識按鈕已顯示 - 語音辨識按鈕已隱藏 - 隱藏語音辨識按鈕 - 「為你推薦」功能列已顯示。 - 「為你推薦」功能列已隱藏 - 隱藏「為你推薦」功能列 - 全螢幕廣告已顯示 - 全螢幕廣告已隱藏 - 隱藏全螢幕廣告 - "全螢幕廣告已攔截。 - -限制:全螢幕上的社群貼文圖片可能會被攔截。" - 全螢幕廣告可透過關閉按鈕隱藏。 - 隱藏全螢幕廣告 - 一般廣告已顯示 - 一般廣告已隱藏 - 隱藏一般廣告 - YouTube Premium 推廣已顯示 - YouTube Premium 推廣已隱藏 - 隱藏 YouTube Premium 推廣 - 灰色分隔線已顯示 - 灰色分隔線已隱藏 - 隱藏灰色分隔線 - 控制列已顯示 - 控制列已隱藏 - 隱藏控制列 - 圖像搜尋按鈕已顯示。 - 圖片搜尋按鈕已隱藏。 - 隱藏圖像搜尋按鈕 - 圖片欄已顯示 - 圖片欄已隱藏 - 隱藏圖片欄 - 訊息卡片已顯示 - 訊息卡片已隱藏 - 隱藏影片中的訊息卡片 - 資訊卡已顯示 - 資訊卡已隱藏 - 隱藏資訊卡 - 資訊欄已顯示 - 資訊欄已隱藏 - 隱藏資訊欄 - 加入按鈕已顯示 - 加入按鈕已隱藏 - 隱藏加入按鈕 - 關鍵概念部分已顯示。 - 關鍵概念部分被隱藏。 - 隱藏關鍵概念部分 - "搜尋、首頁、訂閱和留言會篩選隱藏包含關鍵字詞的內容 - -限制: - • 部分 Shorts 可能不會隱藏 - • 部分介面元件可能不會隱藏 - • 搜尋關鍵字可能不會顯示任何結果" - 關於關鍵詞篩選 - 用雙引號將關鍵字/短語括起來將防止影片標題和頻道名稱部分匹配。<br></b>例如,<br><b>\"ai\"</b>將隱藏影片:<b>人工智慧是如何運作的?</b><br> 但不會隱藏: <b>合理使用是什麼意思?</b> - 匹配整個單字 - 留言未篩選 - 留言已篩選 - 依關鍵字隱藏留言 - 首頁訂閱內容的關鍵字篩選已停用 - 首頁訂閱內容的關鍵字篩選已啟用 - 依關鍵字隱藏首頁影片 - "需要隱藏的關鍵字和詞組,請用新行分隔。 -具有中間大寫字母的單詞必須依照大小寫輸入(例如:iPhone、TikTok、LeBlanc)。" - 要隱藏的關鍵詞 - 搜尋結果未篩選 - 搜尋結果按關鍵字進行篩選 - 按關鍵字隱藏搜尋結果 - 訂閱影片的關鍵字篩選器已停用 - 訂閱影片的關鍵字篩選器已啟用 - 啟用訂閱影片關鍵字篩選器 - 關鍵字 \'%1$s\' 將隱藏所有影片。 - 無效的關鍵字。不能使用:「%s」作為篩選器 - 添加引號以使用關鍵字:%s. - 關鍵字有衝突的聲明:%s. - 關鍵字太短,需要引號:%s. - 最新貼文已顯示 - 最新貼文已隱藏 - 隱藏最新貼文 - 「最新影片」按鈕已顯示 - 「最新影片」按鈕已隱藏 - 隱藏「最新影片」按鈕 - 顯示讚和踩按鈕 - 隱藏讚和踩按鈕 - 隱藏讚和踩按鈕 - 即時聊天訊息已顯示。\n\n這項設定也適用於 Shorts 直播影片。 - 即時聊天訊息已隱藏。\n\n這項設定也適用於 Shorts 直播影片。 - 隱藏即時聊天訊息 - 顯示即時聊天重播按鈕。\n\n關閉即時聊天時它會以全螢幕顯示。 - 即時聊天重播按鈕已隱藏。\n\n關閉即時聊天時它會以全螢幕顯示。 - 隱藏即時聊天重播按鈕 - 在首頁隱藏來自未訂閱頻道的、觀看次數少於 1,000 次的影片。 - 隱藏低觀看次數的影片 - 醫療面板已顯示 - 醫療面板已隱藏 - 隱藏醫療資訊 - 商品欄已顯示 - 商品欄已隱藏 - 隱藏商品欄 - 合輯播放清單已顯示 - 合輯播放清單已隱藏 - 隱藏合輯播放清單 - 電影庫已顯示 - 電影庫已隱藏 - 隱藏電影庫 - 導覽列已顯示。 - 導覽列已隱藏。 - 隱藏導覽列 - 創作按鈕已顯示 - 創作按鈕已隱藏 - 隱藏創作按鈕 - 首頁按鈕已顯示 - 首頁按鈕已隱藏 - 隱藏首頁按鈕 - 導覽列已顯示 - 導覽列已隱藏 - 隱藏導覽列 - 媒體庫按鈕已顯示 - 媒體庫按鈕已隱藏 - 隱藏媒體庫按鈕 - 通知按鈕已顯示 - 通知按鈕已隱藏 - 隱藏通知按鈕 - Shorts 按鈕已顯示 - Shorts 按鈕已隱藏 - 隱藏 Shorts 按鈕 - 訂閱按鈕已顯示 - 訂閱按鈕已隱藏 - 隱藏訂閱按鈕 - 通知我按鈕已顯示 - 通知我按鈕已隱藏 - 隱藏「通知我」按鈕 - 付費推廣標籤已顯示 - 付費推廣標籤已隱藏 - 隱藏付費推廣標籤 - Youtube 遊戲已顯示 - Youtube 遊戲已隱藏 - 隱藏 Youtube 遊戲 - 自動播放按鈕已顯示 - 自動播放按鈕已隱藏 - 隱藏自動播放按鈕 - 字幕按鈕已顯示 - 字幕按鈕已隱藏 - 隱藏字幕按鈕 - 投屏按鈕已顯示 - 投屏按鈕已隱藏 - 隱藏投屏按鈕 - 折疊按鈕已顯示 - 折疊按鈕已隱藏 - 隱藏折疊按鈕 - 微光模式選單已顯示。 - 微光模式選單已隱藏。 - 隱藏微光模式選單 - 音軌選單已顯示 - 音軌選單已隱藏 - 隱藏音軌選單 - 字幕選單頁腳已顯示 - 字幕選單頁腳已隱藏 - 隱藏字幕選單頁腳 - 字幕選單已顯示 - 字幕選單已隱藏 - 隱藏字幕選單 - 1080p 進階選單已顯示。 - 1080p 進階選單已隱藏。 - 隱藏 1080p 進階選單 - 幫助與反饋選單已顯示 - 幫助與反饋選單已隱藏 - 隱藏幫助與反饋選單 - 使用 YouTube Music 收聽選單已顯示 - 使用 YouTube Music 收聽選單已隱藏 - 使用 YouTube Music 收聽選單 - 鎖定螢幕選單已顯示 - 鎖定螢幕選單已隱藏 - 隱藏鎖定螢幕選單 - 循環播放選單已顯示 - 循環播放選單已隱藏 - 隱藏循環播放選單 - 更多訊息選單已顯示 - 更多訊息選單已隱藏 - 隱藏更多訊息選單 - 畫中畫選單已顯示 - 畫中畫選單已隱藏 - 隱藏畫中畫選單 - 播放速度選單已顯示 - 播放速度選單已隱藏 - 隱藏播放速度選單 - 高級控件選單已顯示 - 高級控件選單已隱藏 - 隱藏高級控件選單 - 顯示品質選單頁尾。 - 畫質選單頁腳已隱藏 - 隱藏畫質選單頁腳 - 顯示畫質選單標題。 - 畫質選單標題已隱藏。 - 隱藏畫質選單標題 - 舉報選單已顯示 - 舉報選單已隱藏 - 隱藏舉報選單 - 顯示睡眠定時器選單。 - 睡眠定時器選單已隱藏。 - 隱藏睡眠定時器選單 - 穩定音量選單已顯示 - 穩定音量選單已隱藏 - 隱藏穩定音量選單 - 技術統計選單已顯示 - 技術統計選單已隱藏 - 隱藏技術統計選單 - 在 VR 中觀看選單已顯示 - 在 VR 中觀看選單已隱藏 - 隱藏在 VR 中觀看選單 - 全螢幕按鈕已顯示 - 全螢幕按鈕已隱藏 - 隱藏全螢幕按鈕 - 上一個和下一個按鈕已顯示 - 上一個和下一個按鈕已隱藏 - 隱藏上一個和下一個按鈕 - 購物架已顯示。 - 購物架已隱藏。 - 隱藏玩家購物架 - YouTube Music 按鈕已顯示 - YouTube Music 按鈕已隱藏 - 隱藏 YouTube Music 按鈕 - 保存到播放列表按鈕已顯示 - 保存到播放列表按鈕已隱藏 - 隱藏保存到播放列表按鈕 - Podcast 已顯示 - Podcast 已隱藏 - 隱藏播客部分 - 預覽評論已顯示 - 預覽評論已隱藏 - 隱藏預覽評論 - 這會改變評論區的大小,因此無法在評論區打開直播聊天重播。 - 這不會改變評論區的大小,因此可以在評論區打開直播聊天重播。 - 預覽評論類型 - 顯示促銷警報橫幅。 - 促銷警報橫幅已隱藏。 - 隱藏促銷警報橫幅 - 評論按鈕已顯示 - 評論按鈕已隱藏 - 隱藏評論按鈕 - 倒讚按鈕已顯示 - 倒讚按鈕已隱藏 - 隱藏倒讚按鈕 - 點讚按鈕已顯示 - 點讚按鈕已隱藏 - 隱藏點贊按鈕 - 實時聊天按鈕已顯示 - 實時聊天按鈕已隱藏 - 隱藏實時聊天按鈕 - 更多按鈕已顯示 - 更多按鈕已隱藏 - 隱藏更多按鈕 - 打開混合播放列表按鈕已顯示 - 打開混合播放列表按鈕已隱藏 - 隱藏打開混合播放列表按鈕 - 打開播放列表按鈕已顯示 - 打開播放列表按鈕已隱藏 - 隱藏打開播放列表按鈕 - 保存到播放列表按鈕已顯示 - 保存到播放列表按鈕已隱藏 - 隱藏保存到播放列表按鈕 - 分享按鈕已顯示 - 分享按鈕已隱藏 - 隱藏分享按鈕 - 快速操作面板已顯示 - 快速操作面板已隱藏 - 隱藏快速操作面板 - "隱藏以下推薦影片: - -・標有「頻道會員專屬」標籤的影片。 -・下方標有「其他人還看了」字樣的影片。" - 隱藏推薦影片 - 相關影片疊加層已顯示 - 相關影片疊加層已隱藏 - 隱藏相關影片疊加層 - 相關影片已顯示。 - 相關影片已隱藏。 - 隱藏相關影片 - "此設定限制播放器螢幕上可以載入佈局的最大數量。 - -如果由於伺服器端變更而導致播放器螢幕佈局發生變化,則播放器螢幕上可能會隱藏非預期的佈局。" - 混剪按鈕已顯示 - 混剪按鈕已隱藏 - 隱藏混剪按鈕 - 舉報按鈕已顯示 - 舉報按鈕已隱藏 - 隱藏舉報按鈕 - 獎勵按鈕已顯示 - 獎勵按鈕已隱藏 - 隱藏獎勵按鈕 - 在歷史記錄中的搜尋字詞縮圖已顯示 - 在歷史記錄中的搜尋字詞縮圖已隱藏 - 隱藏搜尋字詞縮圖 - 進度條訊息已顯示 - 進度條訊息已隱藏 - 隱藏進度條訊息 - 進度條跳轉撤銷訊息已顯示 - 進度條撤銷訊息已隱藏 - 隱藏進度條跳轉撤銷訊息 - 時間戳旁邊的章節標籤已隱藏。 - 時間戳旁邊的章節標籤已隱藏。 - 隱藏搜尋列章節標籤 - 影片播放器進度條已顯示 - 影片播放器進度條已隱藏 - 進度條縮略圖預覽已顯示 - 進度條縮略圖預覽已隱藏 - 隱藏進度條縮略圖預覽 - 隱藏影片播放器進度條 - 自我推廣卡片已顯示 - 自我推廣卡片已隱藏 - 隱藏自我推廣卡片 - 關於選單已顯示。 - 關於選單已隱藏。 - 隱藏關於選單 - 輔助使用功能選單已顯示。 - 輔助使用選單已隱藏。 - 隱藏輔助使用選單 - 帳戶選單已顯示。 - 帳戶選單已隱藏。 - 隱藏帳戶選單 - 自動播放選單已顯示。 - 自動播放選單已隱藏。 - 隱藏自動播放選單 - 帳單和付款選單已顯示。 - 帳單和付款選單已隱藏。 - 隱藏帳單和付款選單 - 字幕選單已顯示。 - 字幕選單已隱藏。 - 隱藏字幕選單 - 已連接的應用程式選單已顯示。 - 已連接的應用程式選單已隱藏。 - 隱藏已連接的應用程式選單 - 資料保存選單已顯示。 - 資料保存選單已隱藏。 - 隱藏資料保存選單 - 一般選單已顯示。 - 一般選單已隱藏。 - 隱藏一般選單 - 管理所有歷史選單已顯示。 - 管理所有歷史選單已隱藏。 - 隱藏管理所有歷史選單 - 即時聊天選單已顯示。 - 即時聊天選單已隱藏。 - 隱藏即時聊天選單 - 通知選單已顯示。 - 通知選單已隱藏。 - 隱藏通知選單 - 背景選單已顯示。 - 背景選單已隱藏。 - 隱藏背景選單 - 在電視上觀看選單已顯示。 - 在電視上觀看選單已隱藏。 - 隱藏在電視上觀看選單 - 家庭中心選單已顯示。 - 家庭中心選單已隱藏。 - 隱藏家庭中心選單 - 嘗試實驗性新功能選單已顯示。 - 嘗試實驗性新功能選單已隱藏。 - 隱藏嘗試實驗性新功能選單 - 隱私選單已顯示。 - 隱私選單已隱藏。 - 隱藏隱私選單 - 購買和會員選單已顯示。 - 購買和會員選單已隱藏。 - 隱藏購買和會員選單 - 隱藏 YouTube 設定選單中的元素 - YouTube 設定選單 - 影片品質選項選單已顯示。 - 影片品質選項選單已隱藏。 - 隱藏影片品質選項選單 - YouTube 選單中您的資料已顯示。 - 您在 YouTube 選單中的資料已隱藏。 - 在 YouTube 選單中隱藏您的資料 - 分享按鈕已顯示 - 分享按鈕已隱藏 - 隱藏分享按鈕 - 商店按鈕已顯示 - 商店按鈕已隱藏 - 隱藏商店按鈕 - 購物連結已顯示 - 購物連結已隱藏 - 隱藏購物連結 - 頻道欄已顯示 - 頻道欄已隱藏 - 隱藏頻道欄 - 評論按鈕已顯示 - 評論按鈕已隱藏 - 隱藏評論按鈕 - 已停用的評論按鈕或標有 \"0\" 的按鈕已顯示。 - 已停用的評論按鈕或標有 \"0\" 的按鈕已隱藏。 - 隱藏已停用的評論按鈕 - 倒讚按鈕已顯示 - 倒讚按鈕已隱藏 - 隱藏倒讚按鈕 - "「使用此聲音」等浮動按鈕顯示在 短片頻道標籤中。" - "「使用此聲音」等浮動按鈕隱藏在 短片頻道標籤中。" - 隱藏浮動按鈕 - 影片連結標簽已顯示 - 影片連結標簽已隱藏 - 隱藏完整影片連結標簽 - 綠幕按鈕已顯示。 - 綠幕按鈕已隱藏。 - 隱藏綠幕按鈕 - 訊息面板已顯示 - 訊息面板已隱藏 - 隱藏訊息面板 - 加入按鈕已顯示 - 加入按鈕已隱藏 - 隱藏加入按鈕 - 點讚按鈕已顯示 - 點讚按鈕已隱藏 - 隱藏點贊按鈕 - 顯示即時聊天標題。\n\n標題中的後退按鈕不會被隱藏。 - 即時聊天標題被隱藏。\n\n標題中的後退按鈕不會被隱藏。 - 隱藏即時聊天標題 - 顯示位置按鈕。 - 位置按鈕已隱藏。 - 隱藏位置按鈕 - 導航欄已顯示 - 導航欄已隱藏 - 隱藏導航欄 - 付費推廣橫幅標簽已顯示 - 付費促銷標籤被隱藏 - 隱藏付費促銷標籤 - 顯示暫停的標題。 - 暫停的標題被隱藏。 - 隱藏暫停的標題 - 暫停時疊加按鈕已顯示 - 暫停時疊加按鈕已隱藏 - 隱藏暫停時疊加按鈕 - 按鈕背景被顯示。 - 按鈕背景被隱藏。 - 隱藏播放 & 暫停按鈕背景 - 混剪按鈕已顯示 - 混剪按鈕已隱藏 - 隱藏混剪按鈕 - 儲存音樂按鈕已顯示。 - 儲存音樂按鈕已隱藏。 - 隱藏儲存音樂按鈕 - 顯示搜尋建議按鈕。 - 搜尋建議按鈕已隱藏。 - 隱藏搜尋建議按鈕 - 分享按鈕已顯示 - 分享按鈕已隱藏 - 隱藏分享按鈕 - 顯示在頻道中。 - "隱藏在頻道中。 - -資訊: -• 僅隱藏主頁標籤上有 Shorts 標題的書架。" - 隱藏在頻道中 - 在觀看歷史中顯示 - 在觀看歷史中隱藏 - 在觀看歷史中隱藏 - 在首頁和相關影片中顯示 - 在首頁和相關影片中隱藏 - 在首頁和相關影片中隱藏 - 搜尋結果中的短片已顯示 - 搜尋結果中的短片已隱藏 - 隱藏搜尋結果中的短片 - 訂閱中的短片已顯示 - 訂閱中的短片已隱藏 - 隱藏訂閱中的短片 - "隱藏短片欄 - -已知問題:搜索結果中的官方標題將被隱藏" - 隱藏短片欄 - 商店按鈕已顯示 - 商店按鈕已隱藏 - 隱藏商店按鈕 - 顯示購物按鈕。 - 購物按鈕已隱藏。 - 隱藏購物按鈕 - 聲音按鈕已顯示 - 聲音按鈕已隱藏 - 隱藏聲音按鈕 - 元數據標簽已顯示 - 元數據標簽已隱藏 - 隱藏聲音元數據標簽 - 貼圖已顯示。 - 貼圖已隱藏。 - 隱藏貼圖 - 訂閱按鈕已顯示 - 訂閱按鈕已隱藏 - 隱藏訂閱按鈕 - 超級感謝按鈕已顯示。 - 超級感謝按鈕已隱藏。 - 隱藏超級感謝按鈕 - 標記的產品已顯示 - 標記的產品已隱藏 - 隱藏標記的產品 - 工具欄已顯示 - 工具欄已隱藏 - 隱藏工具欄 - 按鈕已顯示。 - 按鈕被隱藏。 - 隱藏按鈕 - 顯示使用模板按鈕。 - 使用模板按鈕已隱藏 - 隱藏使用模板按鈕 - 顯示使用此聲音按鈕。 - 使用此聲音按鈕已隱藏。 - 隱藏使用此聲音按鈕 - 標題已顯示 - 標題已隱藏 - 隱藏影片標題 - 「顯示更多」按鈕已顯示 - 「顯示更多」按鈕已隱藏 - 隱藏「顯示更多」按鈕 - 彈出訊息已顯示 - 彈出訊息已隱藏 - 隱藏彈出訊息 - 開始試用按鈕已顯示 - 開始試用按鈕已隱藏 - 隱藏開始試用按鈕 - 訂閱輪播已顯示。 - 訂閱輪播已隱藏。 - 隱藏訂閱輪播 - 操作建議已顯示 - 操作建議已隱藏 - 隱藏操作建議 - "This setting has been deprecated. - -Instead, use the 'Settings → Autoplay → Autoplay next video' setting. - -Note: -• If you have any issues with 'Suggested video end screen', try restarting the app." - 顯示建議的影片結束畫面。 - "關閉自動播放時,建議的影片結束畫面會隱藏。 - - 可以在 YouTube 設定中變更自動播放: - '設定 → 自動播放 → 自動播放下一個影片'" - 隱藏推薦影片結束界面 - 感謝按鈕已顯示 - 感謝按鈕已隱藏 - 隱藏感謝按鈕 - 購票庫已顯示 - 購票庫已隱藏 - 隱藏購票庫 - 時間戳已顯示 - 時間戳已隱藏 - 隱藏時間戳 - (評論)時間跳轉已顯示 - (評論)時間跳轉已隱藏 - 隱藏時間跳轉 - 投放按鈕已顯示 - 投放按鈕已隱藏 - 隱藏螢幕投放按鈕 - 創作按鈕已顯示 - 創作按鈕已隱藏 - 隱藏創作按鈕 - 通知按鈕已顯示 - 通知按鈕已隱藏 - 隱藏通知按鈕 - 轉寫文稿部分已顯示 - 轉寫文稿部分已隱藏 - 隱藏轉寫文稿部分 - 影片廣告已顯示 - 影片廣告已隱藏 - 隱藏影片廣告 - "主頁/訂閱/搜尋結果將被過濾以隱藏觀看次數小於或大於指定數量的影片。 - -限制: -• 短影片無法隱藏。 -• 觀看次數為0的影片不會被曬選。" - 關於觀看次數篩選 - 首頁中的影片不會被篩選。 - 首頁中的影片已被篩選。 - 依觀看次數隱藏家庭影片 - 搜尋結果不會被篩選。 - 搜尋結果已被篩選。 - 按觀看次數隱藏搜尋結果 - 訂閱來源中的影片不會被過濾。 - 訂閱來源中的影片已被篩選。 - 按觀看次數隱藏訂閱影片 - 隱藏觀看次數低於這個值的推薦影片 - 依觀看次數隱藏推薦影片 - 播放量大於此數字的影片將被隱藏。 - 高於觀看次數 - 觀看次數低於這個值的影片會被隱藏。 - 低於觀看次數 - 千 -> 1 000\n百萬 -> 1 000 000\n十億 -> 1 000 000 000\n觀看數 -> 觀看數 - 指定你的語言模板來修飾用戶界面中每個影片下顯示的播放量。每個關鍵字(在你的語言中的一個字/詞) -> 值(關鍵字的含義)必須單獨一行。在「->」符號之前是關鍵字。如果更改應用程序或系統語言,則需要重置此設定。\n\n示範:\n英語:10K views = K -> 1000,views -> views\n西班牙語:10 K vistas = K -> 1000,vistas -> views - 觀看次數的值 - 查看商品橫幅已顯示 - 查看商品橫幅已隱藏 - 隱藏檢視商品橫幅 - 語音搜尋按鈕已顯示 - 語音搜尋按鈕已隱藏 - 隱藏語音搜尋按鈕 - 網頁搜尋結果已顯示 - 網頁搜尋結果已隱藏 - 隱藏網頁搜尋結果 - YouTube 塗鴉已顯示。 - YouTube 塗鴉已隱藏。 - 隱藏 YouTube 塗鴉 - "YouTube 塗鴉每年都會出現幾天。 - -如果您所在的地區目前正在顯示 YouTube 塗鴉,且此隱藏設定處於啟用狀態,則搜尋列下方的篩選列也會被隱藏。" - 顯示縮放疊加。 - 縮放疊加被隱藏。 - 隱藏縮放疊加 - Afn Blue - Afn Red - 自訂 - 預設 - MMT - MMT 藍 - MMT 綠 - MMT 黃 - 復興藍 - 復興紅 - 復興黃 - Vanced Black - Vanced Light - YouTube - 在全螢幕模式下關閉和開啟螢幕時保持橫向模式。 - 已強制橫向模式的毫秒數。 - 保持橫向模式的超時時間 - 保持橫向模式 - 預設 - 雙擊操作已停用。 - "雙擊操作已啟用。 - - • Modern 1:雙擊將最小化影片變更為更大的尺寸。 - • Modern 2, 3:雙擊可關閉最小化影片。" - 雙擊操作 - 拖曳已停用。 - 拖曳已啟用。 - 啟用拖曳。 - 展開和關閉按鈕已顯示 - 按鈕已隱藏\n(滑動小型播放器即可展開或關閉) - 隱藏展開和關閉按鈕 - 快轉和倒轉按鈕已顯示 - 快轉和倒轉按鈕已隱藏 - 隱藏快轉和倒轉按鈕 - 對話已顯示 - 對話已隱藏 - 隱藏對話 - 小型播放器覆蓋層不透明度必須介於 0-100 之間。重設為預設值。 - 不透明度值介於 0-100 之間,其中 0 表示透明 - 覆蓋層不透明度 - 原始 - 電話 - 平板電腦 - Modern 1 - Modern 2 - Modern 3 - 最小化播放器類型 - 畫面疊加按鈕 - "點擊可保持循環播放狀態 -長按在循環播放後暫停" - 循環播放按鈕 - "點擊複製影片連結 -長按複製帶時間戳的影片連結" - "點擊複製帶時間戳的影片連結 -長按複製時間戳" - 帶時間戳的複製連結按鈕 - 複製連結按鈕 - 點擊以打開外部下載器 - 外部下載按鈕 - 點選可將目前影片靜音。 再次點擊即可取消靜音。 - 顯示靜音按鈕 - 長按以更改按鈕狀態 - 播放速度重置:%sx。 - "點擊選擇影片播放速度 -長按設定預設播放速度(1.0x)" - 播放速度按鈕 - "點擊可產生頻道中從最舊到最新的所有影片的播放清單。 - 點選並按住可撤銷。" - 顯示按時間排序的播放清單按鈕 - 點選可開啟白名單對話框。 - 點選並按住可開啟白名單設定對話框。 - 顯示白名單按鈕 - 原生播放清單下載按鈕可開啟本機應用程式內下載器。 - 原生播放清單下載按鈕可開啟您的外部下載器。 - 覆蓋播放清單下載按鈕 - 原生影片下載按鈕可開啟本機應用程式內下載器。 - 原生影片下載按鈕可開啟你的外部下載器。 - 覆蓋影片下載按鈕 - YouTube音樂 需要覆蓋按鈕操作。 按此下載 YouTube音樂。 - 先決條件 - YouTube音樂 按鈕可開啟本機應用程式。 - YouTube音樂 按鈕可開啟 RVX 音樂。 - 覆蓋 YouTube音樂 按鈕 - 排除 - 包括 - 一般 - 操作按鈕 - 其他設定 - 動畫 / 回饋 - 下載按鈕 - 實驗性功能 - 影像區域限制 - 導入 / 導出為文件 - 導入 / 導出為檔案 - 關鍵字篩選器 - 其他 - 疊加按鈕 - 補丁訊息 - 快速操作 - 推薦影片 - 短片欄 - 建議採取的行動 - 使用的工具 --唐懂翻譯 - 觀看次數篩選器 - 隱藏或顯示帳戶選單和你的內容分頁中的元素。 - 帳戶選單 - 隱藏或顯示影片下方的操作按鈕 - 操作按鈕 - 廣告 - 替代縮圖 - 繞過微光模式限制或停用微光模式 - 微光模式 - 隱藏或顯示來源、搜尋和相關影片中的類別欄 - 類別欄 - 隱藏或顯示影片下方的頻道欄組件 - 頻道欄 - 隱藏或顯示頻道簡介中的元件 - 頻道簡介 - 隱藏或顯示評論區組件 - 評論 - 在首頁和頻道中隱藏或顯示社群貼文 - 社群貼文 - 使用自訂篩選器隱藏元件 - 自訂篩選器 - 隱藏或顯示首頁中的彈出式選單 - 彈出式選單 - 首頁 - 隱藏或更改與全螢幕相關的組件 - 全螢幕 - 一般設定 - 停用或啟用觸覺反饋 - 觸覺反饋 - 覆蓋應用程式內按鈕的點選操作。 - 掛鉤按鈕 - 導入或導出設定 - 導入/導出設定 - 變更應用程式內最小化播放器的樣式。 - 最小化播放器 - 其他設定 - 隱藏或顯示導覽列部分組件。 - 導覽列 - 已應用補丁的訊息 - 補丁訊息 - 隱藏或顯示影片中的按鈕 - 播放器按鈕 - 隱藏或更改影片播放器中的彈出選單 - 彈出選單 - 播放器 - 恢復 YouTube 使用者名稱 - 恢復 YouTube 倒讚 - 贊助區塊阻擋(SponsorBlock) - 自定義進度條組件 - 進度條 - 隱藏 YouTube 設定選單中的元素 - 設定選單 - 隱藏或顯示短片播放器中的組件 - 短片播放器 - 短片 - 偽裝串流資料以防止播放問題。 - 偽裝串流數據 - 滑動控制 - 隱藏或變更工具欄上的元件,例如工具欄按鈕、搜尋欄、標題 - 工具欄 - 隱藏或顯示影片描述組件 - 影片描述 - 依關鍵字或觀看次數隱藏影片 - 影片篩選器 - 影片 - 變更與觀看歷史記錄相關的設定。 - 觀看歷史記錄 - 快速操作上邊距必須介於 0-32 之間。 重設為預設值。 - 配置從進度條到快速操作容器的間距,範圍在 0 到 32 之間。 - 快速操作頂部邊距 - "強制拒絕軟體 AV1 編解碼器回應。 -約 20 秒的緩衝後會應用不同的編解碼器。" - 拒絕軟體 AV1 編解碼器回應 - 切換過程會導致約 20 秒載入 - 偏移 - 播放速度更改僅適用於當前影片 - 播放速度更改適用於所有影片 - 記住播放速度更改 - 變更預設播放速度時不會顯示提示訊息。 - 變更預設播放速度時會顯示提示訊息。 - 顯示提示訊息 - 將預設速度更改為 %s - 畫質更改僅適用於當前影片 - 畫質更改適用於所有影片 - 記住影片畫質更改 - 更改預設影片畫質時不會顯示提示訊息。 - 更改預設影片畫質時將顯示提示訊息。 - 顯示提示訊息 - 將預設移動數據畫質更改為 %s - 無法設定影片畫質 - 將預設 WiFi 畫質更改為 %s - "移除觀眾酌情觀看對話方塊。 -這項選項不會繞過年齡限制,它只會自動接受。" - 移除查看器的自由裁量對話框 - 將軟體 AV1 編解碼器替換為 VP9 編解碼器。 - 替換軟體 AV1 編解碼器 - 頻道代號已使用。 - 頻道名稱已使用。 - 更換頻道代號 - 點擊顯示剩餘時間 - 點擊打開播放速度或影片畫質彈出選單 - 替換時間戳操作 - 將創作按鈕替換成設定按鈕 - 替換創作按鈕 - "輕觸開啟 YouTube 設定 -按住開啟 RVX 設定" - "輕觸開啟 RVX 設定 -按住開啟 YouTube 設定" - 分配給按鈕的操作類型 - 在全螢幕模式下顯示進度條縮略圖 - 將進度條縮略圖顯示在進度條上方 - 恢復舊的進度條縮略圖 - 不顯示舊的影片畫質選單 - 顯示舊的影片畫質選單 - 恢復舊的影片畫質選單 - \@使用者帳號 (使用者名稱) - 顯示的格式 - 使用者名稱 (@使用者帳號) - 使用者名稱 - 使用者名稱已使用 - 使用者名稱已被使用 - 啟用恢復 YouTube 使用者名稱 - "需要 YouTube 資料 API v3 開發人員金鑰才能將名字替換為使用者名稱。 - -免費方案的 API 金鑰每日配額為 10,000 個, 1個配額用於將名字替換為使用者名稱 以獲得1條留言。 - -按一下查看如何取得 API 金鑰。" - 關於 YouTube 資料 API 金鑰 - 使用 YouTube 資料 API v3 的開發人員金鑰 - YouTube 資料 API 金鑰 - 1. 前往 <a href=%1$s>建立新專案&p;.<br>2.點擊<b>創建</b> 按鈕。<br>3.<br>轉到 <a href=%2$s>YouTube 資料 API v3</a>.<br>4.點選<b>啟用</b> 按鈕。<br>5.點選<b>建立憑證<b> 按鈕。 <br>6.選擇<b>公共資料</b> 選項。<br>7.點選<b>下一步</b> 按鈕。<br>8.複製 API 金鑰。<br><br>※ API 金鑰不應與其他人共用,因此它不包含在匯入/匯出設定中。 - 取得 YouTube 資料 API v3 開發人員金鑰 - 關於 - 倒讚資訊由 Return YouTube Dislike API 提供 - -點擊了解更多資訊 - ReturnYouTubeDislike.com - 點讚按鈕樣式:最佳顯示 - 點讚按鈕樣式:最小寬度 - 緊湊點讚按鈕 - 倒讚顯示為數字 - 倒讚顯示為百份比 - 倒讚百份比 - 倒讚數已隱藏 - 倒讚數已顯示 - 啟用恢復 YouTube 倒讚 - 估計喜歡的次數已隱藏。 - 估計喜歡的次數已顯示。 - 顯示估計喜歡的次數 - 倒讚數不可用(已達到用戶端 API 限制) - 倒讚數不可用(狀態 %d) - 倒讚數暫時不可用(API 連接超時) - 倒讚數不可用(%s) - 重新載入影片以使用 恢復 YouTube 倒讚 進行投票 - 倒讚已隱藏 - 短片中顯示的不喜歡內容 %s - "在短片中顯示不喜歡 - -限制:在無痕模式下,倒讚可能不會顯示" - 倒讚 - 如果 恢復 YouTube 倒讚 無法使用,不顯示訊息 - 如果 恢復 YouTube 倒讚 無法使用,則顯示提示訊息 - 如果 API 無法使用,顯示提示訊息 - 隱藏 - 共享連結時,刪除 URL 中的跟蹤查詢參數 - 清理共享連結 - "像這樣的短語 '#', 'Shop' 和 'N products' 被顯示在影片字幕中。" - "像這樣的短語 '#', 'Shop' 和 'N products' 被隱藏在影片字幕中。" - 清理影片字幕 - 關於 - sponsor.ajay.app - 數據由 SponsorBlock API 提供 -點擊此處了解更多資訊並查看其他平台的下載 - API 網址 已更改 - API URL 無效 - API URL 已重置 - 外觀 - 顏色已更改 - 顏色: - 無效的顏色代碼 - 顏色已重置 - 創建新片段 - 更改片段行為 - 自動隱藏跳過按鈕 - 整個片段顯示跳過按鈕 - 幾秒後隱藏跳過按鈕 - 使用緊湊型跳過按鈕 - 跳過按鈕樣式適合最佳外觀 - 跳過按鈕樣式適合最小寬度 - 顯示創建新片段按鈕 - 不顯示創建新片段按鈕 - 顯示創建新片段按鈕 - 啟用 SponsorBlock - SponsorBlock 是一個公共系統,用於跳過 YouTube 影片中的煩人部分 - 顯示投票按鈕 - 不顯示片段投票按鈕 - 顯示片段投票按鈕 - 一般設定 - 調整新片段步長 - 數值必須為正數 - 創建新片段時,時間調整按鈕移動的毫秒數 - 更改 API 網址 - SponsorBlock 用於向伺服器發出調用的位址 - 最短片段持續時間 - 持續時間無效。 - 小於此值(以秒為單位)的片段將不會顯示或跳過 - 啟用跳過計數跟蹤 - 未啟用跳過計數跟蹤 - 允許 SponsorBlock 排行榜了解節省了多少時間每次跳過片段時都會向排行榜發送訊息 - 自動跳過時顯示提示 - 不顯示提示點擊此處查看範例 - 自動跳過片段時顯示提示點擊此處查看範例 - 顯示不含片段的影片長度 - 顯示完整影片長度 - 顯示影片長度減去所有片段的長度,括號中顯示完整影片長度 - 您的私人用戶 ID - 私人用戶 ID 長度必須至少為30個字符 - 此 ID 應保密這類似於密碼,不應與任何人分享如果有人獲取此 ID,他們可以冒充您 - 已閱讀 - 在創建新片段之前,請閱讀 SponsorBlock 指南 - 查看 - 遵循指南 - 指南包含創建新片段的規則和技巧 - 查看指南 - 選擇片段類別 - The segment lasts from %1$02d:%2$02d to %3$02d:%4$02d (%5$d minutes %6$02d seconds)\nIs it ready to submit? - 段落從\n\n%1$s\n到\n%2$s\n\n(%3$s)\n\n確定要提交? - 時間是否正確? - 類別在設定中被停用請啟用類別以提交 - 是否要手動編輯片段的開始或結束時間? - 提供的時間無效 - 手動編輯片段時間 - 將 %s 設定為新片段的開頭或結尾? - 結束 - 首先在時間軸上標記兩個位置 - 開始 - 立即 - 預覽片段,並確保跳過順暢 - 開始時間必須早於結束時間 - 片段結束時間 - 片段開始時間 - 新的 SponsorBlock 片段 - 重置 - 重置顏色 - 灌水內容/笑話 - 跑題片段,例如閒聊或幽默,對理解影片主要內容並無幫助。這不會包含敘述環境與背景訊息的片段 - 重點 - 大多數人正在尋找的影片重點位置 - 交互提醒(訂閱) - 影片中間簡短提醒觀眾來點讚、訂閱或關注。 如果片段較長,或是關於某個具體事物,則應分類為自我推廣 - 過場/開場動畫 - 沒有實際內容的間隔。 可能是暫停、靜態影格或重複動畫。 不包括包含資訊的轉換。 - 音樂:非音樂片段 - 僅用於音樂影片。音樂影片的非音樂部分,尚未包含在另一個類別中 - 結束畫面/演職員表 - 鳴謝畫面或出現 YouTube 片尾畫面不包括含訊息的結尾 - 預告/回顧 - 展示此影片或同系列後續影片將出現的畫面集錦,所有內容都將在之後再次出現 - 無償廣告/自我推廣 - 類似於「贊助」,非付費或自我推廣除外。這包括有關商品、捐贈或與他人合作的訊息 - 贊助內容 - 付費推廣、付費推薦和直接廣告。非自我推廣或免費提及、推薦他們喜歡的事物/創作者/網站/產品 - 複製 - 導出失敗:%s - 導入 / 導出設定 - 您的 SponsorBlock JSON 配置,可導入 / 導出到 ReVanced Extended 和其他 SponsorBlock平台 - 您的 SponsorBlock JSON 配置,可導入 / 導出到 ReVanced Extended 和其他 SponsorBlock 平台包含您的私人用戶 ID,請謹慎分享 - 導入失敗:%s - 配置文件成功導入 - 您的設定包含私人 SponsorBlock 用戶 ID - - 您的用戶ID就像密碼一樣,不應分享給任何人 - 不再顯示 - 設定已複製到剪貼簿。 - 自動跳過 - 自動跳過一次 - 跳過 - 重點 - 跳過灌水片段 - 跳轉至突出顯示 - 跳過交互提醒 - 跳過片頭 - 跳過中場 - 跳過中場 - 跳過非音樂部分 - 跳過片尾 - 跳過預告 - 跳過總結 - 跳過預告 - 跳過自我宣傳 - 跳過贊助內容 - 跳過未提交片段 - 停用 - 僅在進度條中顯示 - 顯示跳過按鈕 - 跳過灌水內容/笑話 - 跳過高光部分 - 跳過煩人的提醒 - 跳過開場 - 跳過中場 - 跳過中場 - 跳過多個片段 - 跳過非音樂部份 - 跳過片尾 - 跳過預告 - 跳過總結 - 跳過預告 - 跳過自我推廣 - 跳過贊助內容 - 跳過未提交的片段 - SponsorBlock 暫時無法使用 - SponsorBlock 暫時無法使用(狀態 %d) - SponsorBlock 暫時無法使用(API 超時) - 統計資訊 - 統計訊息暫時無法使用(API 出現問題) - 加載中... - 您的聲譽為 <b>%.2f</b> - 您為人們節省了 <b>%s</b> 個片段 - %1$s 小時 %2$s 分鐘 - %1$s 分鐘 %2$s 秒 - %s 秒 - 這相當於他們生活中的 <b>%s</b>點擊此處查看排行榜 - 點擊此處查看全球統計數據和前幾名貢獻者 - SponsorBlock 排行榜 - SponsorBlock 已停用 - 你已經跳過了 <b>%s</b> 段影片 - 重置跳過片段影片計數器? - 那是 <b>%s</b>。 - 你已經創建了 <b>%s</b> 段影片 - 點擊此處查看您的片段。 - 你的使用者名稱:<b>%s</b> - 點擊這裡變更你的使用者名稱 - 無法更改使用者名稱:狀態:%1$d %2$s。 - 使用者名稱變更完成 - 無法提交該段影片。\n已存在。 - 無法提交片段:%s - 無法提交片段:%s - 無法提交片段 - 速率限制(同一用戶或 IP 提交太多) - SponsorBlock 暫時無法使用 - 無法提交片段(狀態:%1$d %2$s) - 片段提交成功 - 如果 SponsorBlock 無法使用,不顯示提示訊息 - 如果 SponsorBlock 無法使用,顯示提示訊息 - 如果 API 無法使用,則顯示提示訊息 - 更改類別 - 反對 - 無法為片段投票:%s - 無法為片段投票(API 超時) - 無法為片段投票(狀態:%1$d %2$s) - 沒有可供投票的片段 - 贊成 - 設定已複製到剪貼簿。 - 時間戳記已複製到剪貼簿。(%s) - URL 已複製到剪貼簿。 - 帶時間戳記的 URL 已複製到剪貼簿。 - 原始 - 比讚 - Cairo - 愛心 - 愛心 (著色) - 隱藏 - 雙擊動畫 - 面板底部邊距必須介於 0-64 之間。 重設為預設值。 - 配置從搜尋欄到面板的間距,範圍為 0-64。 - 面板下邊距 - 高度百分比必須介於 0-100 (%) 之間。 - 配置隱藏導覽列時留下的空白空間的高度百分比,介於 0 到 100 (%) 之間。 - 空白空間高度百分比 - 按住時間戳記可變更短片重複狀態。 - 長按時間戳記 - "在全螢幕模式下顯示影片標題部分 - -限制:點擊後影片標題消失" - 顯示影片標題部份 - 如果自動播放已開啟,下一個影片將在倒計時結束後播放 - 如果自動播放已開啟,下一個影片將在無倒計時的情況下播放 - 跳過自動播放倒計時 - "跳過影片開頭的預先載入緩衝區以立即套用預設影片品質 - - 資訊: - • 影片開始時,會有大約0.3 秒的延遲 - • 不適用於HDR 影片、直播或短於15 秒的影片" - 跳過預加載緩衝 - 提示訊息已隱藏 - 提示訊息已顯示 - 跳過時的提示訊息 - 啟用此設定可能會導致影片播放問題。 - 跳過預加載緩衝 - 速度疊加層值必須介於 0-8.0 之間。重設為預設值。 - 速度疊加值在 0 到 8.0 之間 - 速度疊加值 - "偽裝成舊版的客戶端 - -• 執行這項操作會改變應用程式的界面,但可能會引發未知的副作用 -• 若之後取消這項設定,舊版的使用者介面可能依然會顯示,除非你清空該應用程式的儲存" - 未偽裝版本 - 已偽裝版本 - 17.33.42 - 恢復舊版介面版面配置 - 17.41.37 - 恢復舊版的播放清單 - 18.05.40 - 恢復舊版留言輸入方塊 - 18.17.43 - 恢復舊版播放器彈出式面板 - 18.33.40 - 恢復舊的 Shorts 頁籤 - 18.38.45 - 恢復舊版預設影片畫質 - 18.48.39 - 停用即時更新「觀看次數」和「喜歡次數」 - 19.13.37 - 恢復舊式滾動數位動畫 - 欺騙應用程式版本目標 - 輸入欺騙的應用程式版本目標 - 編輯欺騙應用程式版本 - 偽裝應用程式版本 - "應用程式版本將被偽裝到舊版本的 YouTube。 - -這將改變應用程式的外觀和功能,但可能會出現未知的副作用。 - -如果稍後關閉,建議清除應用程式資料以防止 UI 錯誤。" - "變更裝置尺寸設定,以便解鎖在您目前的裝置上原本不支援的較高影片品質。" - 偽裝裝置尺寸 - iOS 視訊編解碼器是 AVC (H.264)、VP9 或 AV1。 - iOS 影片編解碼器為 AVC (H.264)、VP9 或 AV1。 - 強制 iOS AVC (H.264) - "啟用此功能可能會延長電池壽命並修復播放卡頓問題。 - -AVC (H.264) 的最大解析度為 1080p,影片播放將比 VP9 或 AV1 使用更多的網路資料。" - "• 音軌選單遺失。" - "• 音軌選單遺失。" - "• 電影或付費影片可能無法播放。" - 偽裝副作用 - • 影片可能無法播放。 - 用於獲取串流資料的用戶端隱藏在統計資料中。 - 用於取得串流資料的用戶端顯示在統計資料中。 - 顯示統計資料 - "串流資料未偽裝。 影片可能無法播放。" - 串流資料已偽裝。 - 偽裝串流數據 - 安卓 - Android 電視 - Android VR - iOS - 預設客戶端 - 關閉此設定可能會導致影片播放問題。 - 滑動控制 (亮度) 靈敏度的值必須在 1-1000 (%) 之間 - 配置亮度滑動的最小距離,範圍為 1 到 1000 (%)。\n最小距離越短,亮度等級變化越快。 - 滑動控制 (亮度) 靈敏度 - 在「鎖定螢幕」模式下停用滑動手勢 - 在「鎖定螢幕」模式下啟用滑動手勢 - 在「鎖定螢幕」模式下滑動手勢 - 自動 - 防誤觸的滑動幅度閾值 - 滑動幅度閾值 - 滑動疊加層背景透明度 - 滑動背景透明度 - 可滑動區域大小不能超過50。重設為預設值。 - 可滑動螢幕區域的百分比。\n\n注意:這也會改變雙擊搜尋手勢的螢幕區域大小。 - 滑動覆蓋螢幕尺寸 - 滑動疊加層上的檔案大小 - 滑動疊加層上的檔案大小 - 滑動疊加層顯示的時長(毫秒) - 滑動疊加層時長 - 滑動控制 (音量) 靈敏度的值必須在 1-1000 (%) 之間 - 將音量滑動的最小距離配置為1 到1000 (%) 之間。 \n\n最小距離越短,亮度等級變化越快。 \n\n建議的音量滑動靈敏度在 15 個音量步長時為 100%,在 150 個音量步長時為 10%。 - 滑動控制 (音量) 靈敏度 - "交換創作按鈕與通知按鈕的位置,透過偽裝裝置資訊來實現 - -• 變更這項設定可能需要重新啟動裝置 -• 停用這項設定會從伺服器端載入更多廣告 -• 你應該停用這項設定以顯示影片廣告" - 「建立」按鈕未與「通知」按鈕交換。 - "「建立」按鈕已與「通知」按鈕交換。 - -注意:啟用此功能也會強制隱藏影片廣告。" - 交換創作和通知按鈕 - "停用此功能可能會從伺服器載入更多廣告。 - -此外,廣告將不再在 Shorts 中被封鎖。 - -若此設定未生效,請嘗試切換至無痕模式。" - 預設 - RVX 音樂 - %s 未安裝。 請安裝它。 - 已安裝的 RVX 音樂 套件名稱。 - RVX 音樂包名稱 - • 觀看歷史記錄不起作用。 - "• 遵循Google 帳戶的觀看記錄設定。 -• 由於 DNS 或 VPN 的原因,觀看記錄可能無法運作。" - • 遵循Google 帳戶的觀看記錄設定。 - 關於觀看歷史記錄 - 按一下可開啟 YouTube 觀看記錄管理。 - 管理所有歷史記錄 - 原始 - 替換域名 - 區塊觀看歷史記錄 - 觀看歷史類型 - 無法將頻道 %1$s 新增至 %2$s 白名單。 - 頻道 %1$s 已加入 %2$s 白名單。 - 沒有列入白名單的頻道。 - 未加入白名單。 - 載入頻道資訊失敗。 - 已新增至白名單。 - 播放速度 - 從 %2$s 白名單中刪除頻道 %1$s 嗎? - 無法從 %2$s 白名單中刪除頻道 %1$s 。 - 頻道 %1$s 已從 %2$s 白名單中刪除。 - 查看或刪除已新增至白名單的頻道清單。 - 頻道白名單 - SponsorBlock - diff --git a/xml_tools/config/__init__.py b/xml_tools/config/__init__.py new file mode 100644 index 000000000..601b0f127 --- /dev/null +++ b/xml_tools/config/__init__.py @@ -0,0 +1,5 @@ +"""Config package for application settings.""" + +from .settings import Settings + +__all__: list[str] = ["Settings"] diff --git a/xml_tools/config/settings.py b/xml_tools/config/settings.py index e221097ee..e3d252b7a 100644 --- a/xml_tools/config/settings.py +++ b/xml_tools/config/settings.py @@ -1,11 +1,12 @@ +"""Settings.""" + from dataclasses import dataclass from pathlib import Path @dataclass class Settings: - """ - Application settings and configuration for XML processing tools. + """Application settings and configuration for XML processing tools. This class manages all path configurations and settings for the XML processing tools. It automatically resolves absolute paths based on the project structure and @@ -16,11 +17,11 @@ class Settings: BASE_DIR (Path): Absolute path to the project root directory SRC_DIR (Path): Absolute path to the source directory RESOURCES_DIR (Path): Absolute path to the resources directory + """ - def __post_init__(self): - """ - Initialize all path attributes with absolute paths. + def __post_init__(self) -> None: + """Initialize all path attributes with absolute paths. This method is automatically called after class instantiation and sets up all directory paths as absolute paths based on the location of this settings file. @@ -30,6 +31,7 @@ def __post_init__(self): Raises: FileNotFoundError: If critical directories cannot be found + """ # Get absolute path to the xml_tools directory (where settings.py is located) self.XML_TOOLS_DIR = Path(__file__).resolve().parent.parent @@ -38,7 +40,7 @@ def __post_init__(self): self.BASE_DIR = self.XML_TOOLS_DIR.parent # Define all other paths as absolute paths - self.SRC_DIR = (self.BASE_DIR / "src").resolve() + self.SRC_DIR = (self.BASE_DIR / "patches/src").resolve() self.RESOURCES_DIR = (self.SRC_DIR / "main" / "resources").resolve() # XML file settings @@ -46,8 +48,7 @@ def __post_init__(self): MAX_LINE_LENGTH: int = 120 def get_resource_path(self, app: str, resource_type: str) -> Path: - """ - Get absolute path to a specific resource directory or file. + """Get absolute path to a specific resource directory or file. Args: app (str): Application identifier (e.g., 'youtube' or 'music') @@ -62,5 +63,6 @@ def get_resource_path(self, app: str, resource_type: str) -> Path: >>> path = settings.get_resource_path('youtube', 'settings/xml/prefs.xml') >>> str(path) '/absolute/path/to/project/src/main/resources/youtube/settings/xml/prefs.xml' + """ return (self.RESOURCES_DIR / app / resource_type).resolve() diff --git a/xml_tools/core/__init__.py b/xml_tools/core/__init__.py new file mode 100644 index 000000000..fd37dbd5b --- /dev/null +++ b/xml_tools/core/__init__.py @@ -0,0 +1,5 @@ +"""Core package for application logging.""" + +from .logging import log_process, setup_logging + +__all__: list[str] = ["log_process", "setup_logging"] diff --git a/xml_tools/core/exceptions.py b/xml_tools/core/exceptions.py deleted file mode 100644 index 3c3555c36..000000000 --- a/xml_tools/core/exceptions.py +++ /dev/null @@ -1,13 +0,0 @@ -class XMLToolsError(Exception): - """Base exception for XML tools.""" - pass - - -class ConfigError(XMLToolsError): - """Configuration related errors.""" - pass - - -class XMLProcessingError(XMLToolsError): - """XML processing related errors.""" - pass diff --git a/xml_tools/core/logging.py b/xml_tools/core/logging.py index 60c7add16..6bb0c3a18 100644 --- a/xml_tools/core/logging.py +++ b/xml_tools/core/logging.py @@ -1,6 +1,13 @@ +"""Core package for application logging.""" + +from __future__ import annotations + import logging -from pathlib import Path -from typing import Optional +import sys +from typing import TYPE_CHECKING, ClassVar + +if TYPE_CHECKING: + from pathlib import Path # ANSI escape codes for colors BLUE: str = "\033[94m" @@ -13,13 +20,14 @@ class ColorFormatter(logging.Formatter): - """ - Custom formatter to add colors based on log level and special message formatting. + """Custom formatter to add colors based on log level and special message formatting. Attributes: level_colors (dict): Mapping of log levels to their corresponding colors. + """ - level_colors = { + + level_colors: ClassVar[dict[str, str]] = { "DEBUG": BLUE, "INFO": GREEN, "WARNING": YELLOW, @@ -28,8 +36,7 @@ class ColorFormatter(logging.Formatter): } def format(self, record: logging.LogRecord) -> str: - """ - Format the log record with colors and special message handling. + """Format the log record with colors and special message handling. Args: record (logging.LogRecord): The log record to format. @@ -41,6 +48,7 @@ def format(self, record: logging.LogRecord) -> str: - Preserves original record attributes by saving and restoring them - Applies special coloring to "Starting process:" messages - Colors log levels according to severity + """ # Save original values original_levelname = record.levelname @@ -64,9 +72,22 @@ def format(self, record: logging.LogRecord) -> str: return formatted_message -def setup_logging(log_file: Optional[Path] = None, debug: bool = True) -> logging.Logger: - """ - Configure logging with colored level names for console output and optional file logging. +class ExitOnErrorHandler(logging.Handler): + """Custom handler to exit the program on ERROR or CRITICAL log levels.""" + + def emit(self, record: logging.LogRecord) -> None: + """Check the log level and exit if it's ERROR or CRITICAL. + + Args: + record (logging.LogRecord): The log record to evaluate. + + """ + if record.levelno >= logging.ERROR: + sys.exit(1) + + +def setup_logging(log_file: Path | None = None, *, debug: bool = True) -> logging.Logger: + """Configure logging with colored level names for console output and optional file logging. Args: log_file (Optional[Path]): Path to the log file. If None, only console logging is configured. @@ -75,11 +96,6 @@ def setup_logging(log_file: Optional[Path] = None, debug: bool = True) -> loggin Returns: logging.Logger: Configured logger instance. - Note: - - Console output uses colors for better readability - - File output (if enabled) uses plain text without colors - - DEBUG messages are enabled by default - - Clears any existing handlers before configuration """ # Create logger logger = logging.getLogger("xml_tools") @@ -94,9 +110,7 @@ def setup_logging(log_file: Optional[Path] = None, debug: bool = True) -> loggin # Console handler with colors console_handler = logging.StreamHandler() console_handler.setLevel(base_level) # Use same level as logger - console_formatter = ColorFormatter( - "%(asctime)s - %(levelname)s - %(message)s" - ) + console_formatter = ColorFormatter("%(asctime)s - %(levelname)s - %(message)s") console_handler.setFormatter(console_formatter) logger.addHandler(console_handler) @@ -104,23 +118,24 @@ def setup_logging(log_file: Optional[Path] = None, debug: bool = True) -> loggin if log_file: file_handler = logging.FileHandler(log_file) file_handler.setLevel(base_level) # Use same level as logger - file_formatter = logging.Formatter( - "%(asctime)s - %(name)s - %(levelname)s - %(message)s" - ) + file_formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) + # Add the ExitOnErrorHandler + exit_handler = ExitOnErrorHandler() + logger.addHandler(exit_handler) + # Log initial setup logger.debug("Logging system initialized") if log_file: - logger.debug(f"Log file created at: {log_file}") + logger.debug("Log file created at: %s", log_file) return logger def log_process(logger: logging.Logger, process_name: str) -> None: - """ - Log the start of a process with special formatting. + """Log the start of a process with special formatting. Args: logger (logging.Logger): The logger instance to use. @@ -128,5 +143,6 @@ def log_process(logger: logging.Logger, process_name: str) -> None: Note: Uses special color formatting for "Starting process:" messages. + """ - logger.info(f"Starting process: {process_name}") + logger.info("Starting process: %s", process_name) diff --git a/xml_tools/handlers/__init__.py b/xml_tools/handlers/__init__.py new file mode 100644 index 000000000..63ec6a478 --- /dev/null +++ b/xml_tools/handlers/__init__.py @@ -0,0 +1 @@ +"""Handlers package for application XML functions.""" diff --git a/xml_tools/handlers/check_prefs.py b/xml_tools/handlers/check_prefs.py new file mode 100644 index 000000000..2e16e56a2 --- /dev/null +++ b/xml_tools/handlers/check_prefs.py @@ -0,0 +1,70 @@ +"""Check missing prefs keys.""" + +import logging +import re +from pathlib import Path + +from config.settings import Settings + +logger = logging.getLogger("xml_tools") + + +def extract_keys(path: Path) -> set[str]: + """Extract keys from XML file. + + Args: + path: Path to XML file + + Returns: + set of extracted keys + + """ + try: + key_pattern = re.compile(r'android:key="(\w+)"') # Compile the regex pattern to match keys + keys_found = set() # Use a set to store unique keys + + # Open the XML file and search for the keys + with path.open(encoding="utf-8") as file: + for line in file: + matches = key_pattern.findall(line) # Find all keys in the line + keys_found.update(matches) # Add found keys to the set + + except FileNotFoundError: + logger.exception("Failed to extract keys from %s: ", path) + else: + return keys_found + + +def process(app: str, base_dir: Path) -> None: + """Process prefs files to find missing keys. + + Args: + app: Application name (youtube/music) + base_dir: Base directory of RVX patches operations + + """ + settings = Settings() + base_path = settings.get_resource_path(app, "settings") + + # Define file paths using base_dir + prefs_path_1 = base_dir / "src/main/resources/youtube/settings/xml/revanced_prefs.xml" + prefs_path_2 = base_path / "xml/revanced_prefs.xml" + + try: + # Extract keys from both files + keys_1 = extract_keys(prefs_path_1) + keys_2 = extract_keys(prefs_path_2) + + # Find missing keys + missing_keys = keys_1 - keys_2 + + # Log results + if missing_keys: + logger.info("Missing keys found:") + for key in sorted(missing_keys): + logger.info(key) + else: + logger.info("No missing keys found") + + except Exception: + logger.exception("Failed to process preference files: ") diff --git a/xml_tools/handlers/check_strings.py b/xml_tools/handlers/check_strings.py new file mode 100644 index 000000000..53adc5d05 --- /dev/null +++ b/xml_tools/handlers/check_strings.py @@ -0,0 +1,70 @@ +"""Check missing strings keys.""" + +import logging +import re +from pathlib import Path + +from config.settings import Settings + +logger = logging.getLogger("xml_tools") + + +def extract_keys(path: Path) -> set[str]: + """Extract keys from XML file. + + Args: + path: Path to XML file + + Returns: + set of extracted keys + + """ + try: + key_pattern = re.compile(r'name="(\w+)"') # Compile the regex pattern to match keys + keys_found = set() # Use a set to store unique keys + + # Open the XML file and search for the keys + with path.open(encoding="utf-8") as file: + for line in file: + matches = key_pattern.findall(line) # Find all keys in the line + keys_found.update(matches) # Add found keys to the set + + except FileNotFoundError: + logger.exception("Failed to extract keys from %s: ", path) + else: + return keys_found + + +def process(app: str, base_dir: Path) -> None: + """Process prefs files to find missing keys. + + Args: + app: Application name (youtube/music) + base_dir: Base directory of RVX patches operations + + """ + settings = Settings() + base_path = settings.get_resource_path(app, "settings") + + # Define file paths using base_dir + prefs_path_1 = base_dir / "src/main/resources" / app / "settings/host/values/strings.xml" + prefs_path_2 = base_path / "host/values/strings.xml" + + try: + # Extract keys from both files + keys_1 = extract_keys(prefs_path_1) + keys_2 = extract_keys(prefs_path_2) + + # Find missing keys + missing_keys = keys_1 - keys_2 + + # Log results + if missing_keys: + logger.info("Missing keys found:") + for key in sorted(missing_keys): + logger.info(key) + else: + logger.info("No missing keys found") + + except Exception: + logger.exception("Failed to process preference files: ") diff --git a/xml_tools/handlers/missing_prefs.py b/xml_tools/handlers/missing_prefs.py deleted file mode 100644 index 314b424ac..000000000 --- a/xml_tools/handlers/missing_prefs.py +++ /dev/null @@ -1,72 +0,0 @@ -from pathlib import Path -from typing import Set -import re -import logging - -from config.settings import Settings -from core.exceptions import XMLProcessingError - -logger = logging.getLogger("xml_tools") - - -def extract_keys(path: Path) -> Set[str]: - """Extract keys from XML file. - - Args: - path: Path to XML file - - Returns: - Set of extracted keys - - Raises: - XMLProcessingError: If parsing fails - """ - try: - key_pattern = re.compile(r'android:key="(\w+)"') # Compile the regex pattern to match keys - keys_found = set() # Use a set to store unique keys - - # Open the XML file and search for the keys - with open(path, "r", encoding="utf-8") as file: - for line in file: - matches = key_pattern.findall(line) # Find all keys in the line - keys_found.update(matches) # Add found keys to the set - - return keys_found - except Exception as e: - logger.error(f"Failed to extract keys from {path}: {e}") - raise XMLProcessingError(f"Failed to extract keys from {path}: {e}") - - -def process(app: str, base_dir: Path) -> None: - """Process prefs files to find missing keys. - - Args: - app: Application name (youtube/music) - base_dir: Base directory of RVX patches operations - """ - settings = Settings() - base_path = settings.get_resource_path(app, "settings") - - # Define file paths using base_dir - prefs_path_1 = base_dir / "src/main/resources/youtube/settings/xml/revanced_prefs.xml" - prefs_path_2 = base_path / "xml/revanced_prefs.xml" - - try: - # Extract keys from both files - keys_1 = extract_keys(prefs_path_1) - keys_2 = extract_keys(prefs_path_2) - - # Find missing keys - missing_keys = keys_1 - keys_2 - - # Log results - if missing_keys: - logger.info("Missing keys found:") - for key in sorted(missing_keys): - logger.info(key) - else: - logger.info("No missing keys found") - - except XMLProcessingError as e: - logger.error(f"Failed to process preference files: {e}") - raise diff --git a/xml_tools/handlers/missing_strings.py b/xml_tools/handlers/missing_strings.py index eba720515..d385d733e 100644 --- a/xml_tools/handlers/missing_strings.py +++ b/xml_tools/handlers/missing_strings.py @@ -1,9 +1,11 @@ -from pathlib import Path +"""Find missing strings and create the file with them.""" + import logging -from lxml import etree as ET +from pathlib import Path + +from lxml import etree as et from config.settings import Settings -from core.exceptions import XMLProcessingError from utils.xml import XMLProcessor logger = logging.getLogger("xml_tools") @@ -16,6 +18,7 @@ def compare_and_update(source_path: Path, dest_path: Path, missing_path: Path) - source_path: Path to source XML file dest_path: Path to destination XML file missing_path: Path to missing strings file + """ try: # Parse source and destination files @@ -25,32 +28,25 @@ def compare_and_update(source_path: Path, dest_path: Path, missing_path: Path) - # Find missing strings missing_strings = {} - for name, data in source_strings.items(): - if name not in dest_strings: - missing_strings[name] = data + missing_strings = {name: data for name, data in source_strings.items() if name not in dest_strings} if missing_strings: # Create new root with missing strings - root = ET.Element("resources") - for name, data in sorted(missing_strings.items()): - # If the string is already in the file and the count of strings is the same, then skip. - if name in missing_path_strings and len(missing_strings.keys()) == len(missing_path_strings.keys()): - logger.info(f"Up to date: {missing_path}") - return - string_elem = ET.Element("string", **data["attributes"]) + root = et.Element("resources") + for _name, data in sorted(missing_strings.items()): + string_elem = et.Element("string", **data["attributes"]) string_elem.text = data["text"] root.append(string_elem) # Write missing strings file XMLProcessor.write_file(missing_path, root) - logger.info(f"Modified missing strings file: {missing_path}") + logger.info("Modified missing strings file: %s", missing_path) elif missing_path.exists(): missing_path.unlink() - logger.info(f"Removed empty missing strings file: {missing_path}") + logger.info("Removed empty missing strings file: %s", missing_path) - except Exception as e: - logger.error(f"Failed to process missing strings: {e}") - raise XMLProcessingError(str(e)) + except Exception: + logger.exception("Failed to process missing strings: ") def process(app: str) -> None: @@ -58,6 +54,7 @@ def process(app: str) -> None: Args: app: Application name (youtube/music) + """ settings = Settings() source_path = settings.get_resource_path(app, "settings") / "host/values/strings.xml" @@ -70,6 +67,5 @@ def process(app: str) -> None: missing_path = lang_dir / "missing_strings.xml" compare_and_update(source_path, dest_path, missing_path) - except Exception as e: - logger.error(f"Failed to process {app} translations: {e}") - raise XMLProcessingError(str(e)) + except Exception: + logger.exception("Failed to process %s translations: ", app) diff --git a/xml_tools/handlers/remove_unused_strings.py b/xml_tools/handlers/remove_unused_strings.py index d7c081716..5ff6080d2 100644 --- a/xml_tools/handlers/remove_unused_strings.py +++ b/xml_tools/handlers/remove_unused_strings.py @@ -1,45 +1,45 @@ -from typing import Set, Dict, List +"""Remove unused strings.""" + import logging import os -from lxml import etree as ET from pathlib import Path +from lxml import etree as et + from config.settings import Settings -from core.exceptions import XMLProcessingError from utils.xml import XMLProcessor logger = logging.getLogger("xml_tools") # Constants -BLACKLISTED_STRINGS: Set[str] = { +BLACKLISTED_STRINGS: set[str] = { "revanced_remember_video_quality_mobile", "revanced_remember_video_quality_wifi", "revanced_sb_api_url_sum", "revanced_sb_enabled", "revanced_sb_enabled_sum", "revanced_sb_toast_on_skip", - "revanced_sb_toast_on_skip_sum" + "revanced_sb_toast_on_skip_sum", + "revanced_spoof_streaming_data_type_entry_android_creator", + "revanced_third_party_youtube_music_not_installed_dialog_title", } PREFIX_TO_IGNORE: tuple[str, ...] = ( "revanced_icon_", + "revanced_shorts_custom_actions_", "revanced_spoof_app_version_target_entry_", - "revanced_spoof_streaming_data_side_effects_" + "revanced_spoof_streaming_data_side_effects_", ) settings_instance = Settings() SCRIPT_DIR = settings_instance.BASE_DIR -SEARCH_DIRECTORIES = [ - str(SCRIPT_DIR.parent / "revanced-patches"), - str(SCRIPT_DIR.parent / "revanced-integrations") -] +SEARCH_DIRECTORIES = [str(SCRIPT_DIR.parent / "revanced-patches")] ALLOWED_EXTENSIONS = (".kt", ".java", ".xml") def get_base_name(name: str) -> str: - """ - Return the base name by stripping known suffixes from a string name. + """Return the base name by stripping known suffixes from a string name. Args: name (str): The original string name. @@ -50,29 +50,24 @@ def get_base_name(name: str) -> str: Example: >>> get_base_name("my_setting_summary_on") 'my_setting' + """ - suffixes = [ - "_title", - "_summary_off", - "_summary_on", - "_summary" - ] + suffixes = ["_title", "_summary_off", "_summary_on", "_summary"] for suffix in suffixes: if name.endswith(suffix): - return name[:-len(suffix)] + return name[: -len(suffix)] return name -def search_in_files(directories: List[str], name_values: Set[str]) -> Dict[str, List[str]]: - """ - Search for string names in all files within specified directories. +def search_in_files(directories: list[str], name_values: set[str]) -> dict[str, list[str]]: + """Search for string names in all files within specified directories. Args: - directories (List[str]): List of directory paths to search. - name_values (Set[str]): Set of string names to search for. + directories (list[str]): list of directory paths to search. + name_values (set[str]): set of string names to search for. Returns: - Dict[str, List[str]]: Dictionary mapping each string name to a list of file paths where it was found. + dict[str, list[str]]: Dictionary mapping each string name to a list of file paths where it was found. Raises: OSError: If there are problems accessing the directories or files. @@ -83,42 +78,42 @@ def search_in_files(directories: List[str], name_values: Set[str]) -> Dict[str, - Ignores 'strings.xml' and 'missing_strings.xml' files - Only searches files with extensions defined in ALLOWED_EXTENSIONS - Searches for both original names and their base forms (without suffixes) + """ results = {name: [] for name in name_values} for directory in directories: - abs_dir = os.path.abspath(directory) - logger.info(f"Searching in directory: {abs_dir} (exists: {os.path.exists(abs_dir)})") + abs_dir = Path(directory).resolve() + logger.info("Searching in directory: %s (exists: %s)", abs_dir, Path(abs_dir).exists()) for root, dirs, files in os.walk(directory): # Skip hidden and build directories dirs[:] = [d for d in dirs if not d.startswith(".") and d != "build"] for file in files: - if (file in ("strings.xml", "missing_strings.xml") or not file.endswith(ALLOWED_EXTENSIONS)): + if file in ("strings.xml", "missing_strings.xml") or not file.endswith(ALLOWED_EXTENSIONS): continue - file_path = os.path.join(root, file) + file_path = Path(root) / file try: - with open(file_path, "r", encoding="utf-8") as f: + with file_path.open(encoding="utf-8") as f: content = f.read() for name in name_values: # Check both original name and base name if name in content or get_base_name(name) in content: results[name].append(file_path) - except Exception as e: - logger.error(f"Error reading {file_path}: {e}") + except Exception: + logger.exception("Error reading %s: ", file_path) return results -def should_remove(name: str, unused_names: Set[str]) -> bool: - """ - Determine if a string should be removed based on various criteria. +def should_remove(name: str, unused_names: set[str]) -> bool: + """Determine if a string should be removed based on various criteria. Args: name (str): The string name to check. - unused_names (Set[str]): Set of string names that were not found in any source files. + unused_names (set[str]): set of string names that were not found in any source files. Returns: bool: True if the string should be removed, False otherwise. @@ -128,30 +123,28 @@ def should_remove(name: str, unused_names: Set[str]) -> bool: - The string or its base name is in the unused_names set - The string is not in BLACKLISTED_STRINGS - The string does not start with any prefix in PREFIX_TO_IGNORE + """ base_name = get_base_name(name) return ( - (name in unused_names or base_name in unused_names) and - name not in BLACKLISTED_STRINGS and - not any(name.startswith(prefix) for prefix in PREFIX_TO_IGNORE) + (name in unused_names or base_name in unused_names) + and name not in BLACKLISTED_STRINGS + and not any(name.startswith(prefix) for prefix in PREFIX_TO_IGNORE) ) -def process_xml_file(file_path: Path, unused_names: Set[str]) -> None: - """ - Process a single XML file to remove unused strings. +def process_xml_file(file_path: Path, unused_names: set[str]) -> None: + """Process a single XML file to remove unused strings. Args: file_path (Path): Path to the XML file to process. - unused_names (Set[str]): Set of string names that should be considered for removal. - - Raises: - XMLProcessingError: If there are any errors during XML processing. + unused_names (set[str]): set of string names that should be considered for removal. Notes: - Creates a new XML tree containing only the strings that should be kept - Only writes the file if strings were actually removed - Maintains original XML structure and attributes + """ try: _, _, strings_dict = XMLProcessor.parse_file(file_path) @@ -160,11 +153,11 @@ def process_xml_file(file_path: Path, unused_names: Set[str]) -> None: initial_count = len(strings_dict) # Create new root with only used strings - new_root = ET.Element("resources") + new_root = et.Element("resources") kept_strings = 0 for name, data in sorted(strings_dict.items()): if not should_remove(name, unused_names): - string_elem = ET.Element("string", **data["attributes"]) + string_elem = et.Element("string", **data["attributes"]) string_elem.text = data["text"] new_root.append(string_elem) kept_strings += 1 @@ -173,33 +166,30 @@ def process_xml_file(file_path: Path, unused_names: Set[str]) -> None: if kept_strings < initial_count: XMLProcessor.write_file(file_path, new_root) logger.info( - f"Updated {file_path}: " - f"removed {initial_count - kept_strings} strings, " - f"kept {kept_strings} strings" + "Updated %s: removed %s strings, kept %s strings", + file_path, + initial_count - kept_strings, + kept_strings, ) else: - logger.info(f"No changes needed for {file_path}") + logger.info("No changes needed for %s", file_path) - except Exception as e: - logger.error(f"Error processing {file_path}: {e}") - raise XMLProcessingError(f"Failed to process {file_path}: {str(e)}") + except Exception: + logger.exception("Error processing %s: ", file_path) def process(app: str) -> None: - """ - Remove unused strings from XML files for a given application. + """Remove unused strings from XML files for a given application. Args: app (str): The application identifier to process. - Raises: - XMLProcessingError: If there are any errors during XML processing. - Notes: - Processes both the source strings file and all translation files - Uses settings from the Settings class to determine file locations - Maintains a log of all operations - Skips writing files if no changes are needed + """ settings = Settings() base_path = settings.get_resource_path(app, "settings") @@ -225,6 +215,5 @@ def process(app: str) -> None: if dest_path.exists(): process_xml_file(dest_path, unused_names) - except Exception as e: - logger.error(f"Error during processing: {e}") - raise XMLProcessingError(str(e)) + except Exception: + logger.exception("Error during processing: ") diff --git a/xml_tools/handlers/replace_strings.py b/xml_tools/handlers/replace_strings.py index 55c63832f..8f0b77a59 100644 --- a/xml_tools/handlers/replace_strings.py +++ b/xml_tools/handlers/replace_strings.py @@ -1,9 +1,10 @@ -from pathlib import Path +"""Get strings from provided source and replace strings in destination.""" + import logging -from lxml import etree as ET +from pathlib import Path +from xml.etree import ElementTree as ET from config.settings import Settings -from core.exceptions import XMLProcessingError from utils.xml import XMLProcessor logger = logging.getLogger("xml_tools") @@ -15,6 +16,7 @@ def update_strings(target_path: Path, source_path: Path) -> None: Args: target_path: Path to target XML file source_path: Path to source XML file + """ try: # Parse source and target files @@ -31,18 +33,17 @@ def update_strings(target_path: Path, source_path: Path) -> None: del source_strings[name] # Add new strings - for name, data in sorted(source_strings.items()): + for _name, data in sorted(source_strings.items()): string_elem = ET.Element("string", **data["attributes"]) string_elem.text = data["text"] target_root.append(string_elem) # Write updated file XMLProcessor.write_file(target_path, target_root) - logger.info(f"Updated strings in {target_path}") + logger.info("Updated strings in %s", target_path) - except Exception as e: - logger.error(f"Failed to update strings in {target_path}: {e}") - raise XMLProcessingError(str(e)) + except Exception: + logger.exception("Failed to update strings in %s: ", target_path) def process(app: str, base_dir: Path) -> None: @@ -51,6 +52,7 @@ def process(app: str, base_dir: Path) -> None: Args: app: Application name (youtube/music) base_dir: Base directory of RVX patches operations + """ settings = Settings() base_path = settings.get_resource_path(app, "settings") @@ -73,6 +75,5 @@ def process(app: str, base_dir: Path) -> None: if rvx_lang_path.exists(): update_strings(target_path, rvx_lang_path) - except Exception as e: - logger.error(f"Failed to process {app} translations: {e}") - raise XMLProcessingError(str(e)) + except Exception: + logger.exception("Failed to process %s translations: ", app) diff --git a/xml_tools/handlers/sort_strings.py b/xml_tools/handlers/sort_strings.py index 20eebd7ca..0e432ff26 100644 --- a/xml_tools/handlers/sort_strings.py +++ b/xml_tools/handlers/sort_strings.py @@ -1,9 +1,11 @@ -from pathlib import Path -from lxml import etree as ET +"""Check strings in destination.""" + import logging +from pathlib import Path + +from lxml import etree as et from config.settings import Settings -from core.exceptions import XMLProcessingError from utils.xml import XMLProcessor logger = logging.getLogger("xml_tools") @@ -14,24 +16,24 @@ def sort_file(path: Path) -> None: Args: path: Path to XML file to sort + """ try: _, root, strings = XMLProcessor.parse_file(path) # Create new root with sorted strings - new_root = ET.Element("resources") + new_root = et.Element("resources") for name in sorted(strings.keys()): data = strings[name] - string_elem = ET.Element("string", **data["attributes"]) + string_elem = et.Element("string", **data["attributes"]) string_elem.text = data["text"] new_root.append(string_elem) XMLProcessor.write_file(path, new_root) - logger.info(f"Sorted strings in {path}") + logger.info("Sorted strings in %s", path) - except Exception as e: - logger.error(f"Failed to sort {path}: {e}") - raise XMLProcessingError(f"Failed to sort {path}: {e}") + except Exception: + logger.exception("Failed to sort %s: ", path) def process(app: str) -> None: @@ -39,6 +41,7 @@ def process(app: str) -> None: Args: app: Application name (youtube/music) + """ settings = Settings() base_path = settings.get_resource_path(app, "settings") diff --git a/xml_tools/main.py b/xml_tools/main.py index 70a01bdfa..d4f7ed54f 100644 --- a/xml_tools/main.py +++ b/xml_tools/main.py @@ -1,206 +1,155 @@ -from pathlib import Path -import click +"""CLI tool to run xml commands.""" + +from __future__ import annotations + import os import sys -from typing import Optional, List, Tuple, Callable -from logging import Logger +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING, Callable + +import click + +from config import Settings +from core import log_process, setup_logging +from handlers import ( + check_prefs, + check_strings, + missing_strings, + remove_unused_strings, + replace_strings, + sort_strings, +) +from utils import GitClient + +if TYPE_CHECKING: + from logging import Logger -from config.settings import Settings -from core.exceptions import ConfigError -from core.logging import setup_logging, log_process -from utils.git import GitClient -from handlers import missing_prefs, missing_strings, remove_unused_strings, replace_strings, sort_strings settings = Settings() -def get_rvx_base_dir() -> Path: - """Get RVX base directory from environment variable. +@dataclass +class CLIConfig: + """Configuration for CLI commands.""" - Returns: - Path: The path to the RVX base directory. + log_file: str | None + rvx_base_dir: Path | None + app: str + logger: Logger - Raises: - ConfigError: If RVX_BASE_DIR environment variable is not set. - Note: - This function checks for the RVX_BASE_DIR environment variable - which must be set before running the application. - """ +def get_rvx_base_dir(logger: Logger) -> Path: + """Get the RVX base directory from the environment.""" rvx_dir = os.getenv("RVX_BASE_DIR") if not rvx_dir: - raise ConfigError("RVX_BASE_DIR environment variable must be set") + logger.error("RVX_BASE_DIR must be provided for replace operation.") + sys.exit(1) return Path(rvx_dir) -def validate_rvx_base_dir(ctx: click.Context, base_dir: Optional[str] = None) -> Path: - """Validate and return the RVX base directory path. - - Args: - ctx (click.Context): The Click context object containing shared resources. - base_dir (Optional[str], optional): The base directory path string. Defaults to None. - - Returns: - Path: A validated Path object for the RVX base directory. - - Raises: - SystemExit: If the base directory validation fails. - - Note: - If base_dir is not provided, the function attempts to get it from - the RVX_BASE_DIR environment variable. - """ - if not base_dir: - try: - base_dir = str(get_rvx_base_dir()) - except ConfigError as e: - ctx.obj['logger'].error(str(e)) - sys.exit(1) - return Path(base_dir) - - -def process_all(app: str, base_dir: Path, logger: Logger) -> None: - """Run all processing steps in sequence for the specified application. - - Args: - app (str): The application to process ('youtube' or 'music'). - base_dir (Path): The base directory path for RVX operations. - logger (Logger): The logger instance for recording operations. - - Raises: - SystemExit: If any processing step fails or Git sync fails. - Exception: If any handler encounters an error during execution. - - Note: - This function executes the following steps in order: - 1. Syncs the Git repository - 2. Replaces strings for YouTube and YouTube Music - 3. Removes unused strings - 4. Sorts strings - 5. Checks for missing strings - 6. Checks for missing preferences - """ +def is_rvx_dir_needed(options: dict) -> bool: + """Determine if rvx_base_dir validation is needed based on options.""" + return options.get("run_all") or options.get("replace") or options.get("prefs") or options.get("check") + + +@click.group(invoke_without_command=True) +@click.option("--log-file", type=str, help="Path to log file") +@click.option("--rvx-base-dir", type=str, envvar="RVX_BASE_DIR", help="Path to RVX 'patches' directory") +@click.option("-a", "--all", "run_all", is_flag=True, help="Run all commands in order") +@click.option("-m", "--missing", is_flag=True, help="Run missing strings check") +@click.option("-r", "--replace", is_flag=True, help="Run replace strings operation") +@click.option("--remove", is_flag=True, help="Remove unused strings") +@click.option("-s", "--sort", is_flag=True, help="Sort strings in XML files") +@click.option("-c", "--check", is_flag=True, help="Run missing strings check") +@click.option("-p", "--prefs", is_flag=True, help="Run missing preferences check") +@click.option("--youtube/--music", default=True, help="Process YouTube or Music strings") +@click.pass_context +def cli(ctx: click.Context, **kwargs: dict[str, bool | str | None]) -> None: + """CLI tool for processing XML commands.""" + log_file = kwargs.get("log_file") + app = "youtube" if kwargs.get("youtube") else "music" + + logger = setup_logging(Path(log_file) if log_file else None) + + rvx_base_dir = kwargs.get("rvx_base_dir") or get_rvx_base_dir(logger) if is_rvx_dir_needed(kwargs) else None + + ctx.obj = CLIConfig( + log_file=log_file, + rvx_base_dir=Path(rvx_base_dir) if rvx_base_dir else None, + app=app, + logger=logger, + ) + + if kwargs.get("run_all"): + process_all(ctx.obj) + + handle_individual_operations(ctx.obj, kwargs) + + +def process_all(config: CLIConfig) -> None: + """Run all operations in sequence.""" + logger = config.logger + base_dir = config.rvx_base_dir + git = GitClient(base_dir) if not git.sync_repository(): sys.exit(1) - handlers: List[Tuple[str, Callable, List[str]]] = [ + handlers: list[tuple[str, Callable, list[str]]] = [ ("Replace Strings (YouTube)", replace_strings.process, ["youtube", base_dir]), ("Replace Strings (YouTube Music)", replace_strings.process, ["music", base_dir]), ("Remove Unused Strings (YouTube)", remove_unused_strings.process, ["youtube"]), ("Remove Unused Strings (YouTube Music)", remove_unused_strings.process, ["music"]), ("Sort Strings (YouTube)", sort_strings.process, ["youtube"]), ("Sort Strings (YouTube Music)", sort_strings.process, ["music"]), - ("Missing Strings Check (YouTube)", missing_strings.process, ["youtube"]), - ("Missing Strings Check (YouTube Music)", missing_strings.process, ["music"]), - ("Missing Prefs Check", missing_prefs.process, ["youtube", base_dir]), + ("Missing Strings Creation (YouTube)", missing_strings.process, ["youtube"]), + ("Missing Strings Creation (YouTube Music)", missing_strings.process, ["music"]), + ("Missing Prefs Check", check_prefs.process, ["youtube", base_dir]), + ("Missing Strings Check (YouTube)", check_strings.process, ["youtube", base_dir]), + ("Missing Strings Check (YouTube Music)", check_strings.process, ["music", base_dir]), ] - for process_name, handler, args in handlers: - try: - log_process(logger, process_name) - handler(*args) - except Exception as e: - logger.error(f"Handler {process_name} failed: {e}") - sys.exit(1) + for name, handler, args in handlers: + log_process(logger, name) + handler(*args) -@click.group(invoke_without_command=True) -@click.option("--log-file", type=str, help="Path to log file") -@click.option("--rvx-base-dir", type=str, help="Base directory of RVX patches operations", envvar="RVX_BASE_DIR") -@click.option("-a", "--all", "run_all", is_flag=True, help="Run all commands in order") -@click.option("-m", "--missing", is_flag=True, help="Run missing strings check") -@click.option("-r", "--replace", is_flag=True, help="Run replace strings operation") -@click.option("--remove", is_flag=True, help="Remove unused strings") -@click.option("-s", "--sort", is_flag=True, help="Sort strings in XML files") -@click.option("-p", "--prefs", is_flag=True, help="Run missing preferences check") -@click.option("--youtube/--music", default=True, help="Process YouTube or Music strings") -@click.pass_context -def cli(ctx: click.Context, - log_file: Optional[str], - rvx_base_dir: Optional[str], - run_all: bool, - missing: bool, - replace: bool, - remove: bool, - sort: bool, - prefs: bool, - youtube: bool) -> None: - """XML processing tools for RVX patches with backwards compatibility. - - Args: - ctx (click.Context): Click context object for sharing resources between commands. - log_file (Optional[str]): Path to the log file. If None, logs to stdout. - rvx_base_dir (Optional[str]): Base directory for RVX operations. Can be set via RVX_BASE_DIR env var. - run_all (bool): Flag to run all processing steps in sequence. - missing (bool): Flag to run missing strings check. - replace (bool): Flag to run string replacement operation. - remove (bool): Flag to remove unused strings. - sort (bool): Flag to sort strings in XML files. - prefs (bool): Flag to run missing preferences check. - youtube (bool): Flag to process YouTube (--youtube) or Music (--music) strings. - - Raises: - SystemExit: If any processing step fails. - Exception: If any operation encounters an error. - - Note: - - The function initializes logging and validates the RVX base directory when required. - - Operations are executed in the order specified by command line flags. - - The --youtube/--music flag determines which application's strings to process. - - When --all is specified, all operations are run in a predefined sequence. - """ - # Initialize the logger - logger = setup_logging(Path(log_file) if log_file else None) +def handle_individual_operations(config: CLIConfig, options: dict) -> None: + """Handle individual operations based on user flags.""" + logger = config.logger + base_dir = config.rvx_base_dir + app = config.app - app = "youtube" if youtube else "music" - - # Store common context - ctx.obj = { - "app": app, - "logger": logger - } - - # Only validate RVX_BASE_DIR for commands that need it - needs_rvx_dir = run_all or replace or prefs - if needs_rvx_dir: - base_dir = validate_rvx_base_dir(ctx, rvx_base_dir) - - # Handle all operations if --all is specified - if run_all: - try: - process_all(app, base_dir, logger) - return - except Exception as e: - logger.error(f"Error during processing: {e}") - sys.exit(1) - - # Handle individual operations try: - if missing: + if options.get("missing"): log_process(logger, "Missing Strings Check") missing_strings.process(app) - if prefs: - log_process(logger, "Missing Preferences Check") - missing_prefs.process(app, base_dir) - - if remove: + if options.get("remove"): log_process(logger, "Remove Unused Strings") remove_unused_strings.process(app) - if replace: + if options.get("replace"): git = GitClient(base_dir) if git.sync_repository(): log_process(logger, "Replace Strings") replace_strings.process(app, base_dir) - if sort: + if options.get("sort"): log_process(logger, "Sort Strings") sort_strings.process(app) - except Exception as e: - logger.error(f"Error during processing: {e}") + if options.get("check"): + log_process(logger, "Check Strings") + check_strings.process(app, base_dir) + + if options.get("prefs"): + log_process(logger, "Check Preferences") + check_prefs.process(app, base_dir) + + except Exception: + logger.exception("Error during processing: ") sys.exit(1) diff --git a/xml_tools/utils/__init__.py b/xml_tools/utils/__init__.py new file mode 100644 index 000000000..8d5b94c9a --- /dev/null +++ b/xml_tools/utils/__init__.py @@ -0,0 +1,5 @@ +"""Utils package for application utilities.""" + +from .git import GitClient + +__all__: list[str] = ["GitClient"] diff --git a/xml_tools/utils/git.py b/xml_tools/utils/git.py index 14a670a09..5cefe1be9 100644 --- a/xml_tools/utils/git.py +++ b/xml_tools/utils/git.py @@ -1,64 +1,106 @@ -import subprocess -from pathlib import Path -from typing import Tuple +"""Git commands.""" + import logging +from pathlib import Path + +import git logger = logging.getLogger("xml_tools") class GitClient: - """ - Handler for Git operations on a repository. + """Handler for Git operations on a repository using GitPython.""" - This class provides methods to perform Git operations on a specified repository path. + def __init__(self, repo_path: Path) -> None: + """Initialize GitClient with repository path. - Attributes: - repo_path (Path): Path to the Git repository - """ + Args: + repo_path: Path to the Git repository. + + Raises: + ValueError: If the provided path is not a valid Git repository. - def __init__(self, repo_path: Path) -> None: """ - Initialize GitClient with repository path. + self.repo_path = repo_path + try: + self.repo = git.Repo(repo_path, search_parent_directories=True) + except git.InvalidGitRepositoryError as e: + msg = f"Invalid Git repository at path: {repo_path}" + raise ValueError(msg) from e + + def _handle_git_error(self, operation: str, exception: Exception) -> tuple[int, str, str]: + """Handle GitPython exceptions. Args: - repo_path (Path): Path to the Git repository + operation: The name of the Git operation that failed. + exception: The exception that was raised by GitPython. Returns: - None - """ - self.repo_path = repo_path + A tuple containing: + - an error code (1), + - an empty output string, + - the exception message as error string. - def run_command(self, command: list) -> Tuple[int, str, str]: """ - Execute a Git command and return its result. + logger.error("Git %s failed: %s", operation, exception) + return 1, "", str(exception) + + def switch_branch(self, branch_name: str) -> tuple[int, str, str]: + """Switches to the specified branch. Args: - command (list): List of command components (e.g., ["git", "pull"]) + branch_name: The name of the branch to switch to. Returns: - Tuple[int, str, str]: A tuple containing: - - Return code (0 for success) - - Command output (stdout) - - Error output (stderr) + A tuple containing: + - Return code (0 for success, 1 for failure) + - Command output (success message or empty) + - Error output (empty string on success, error message on failure) - Note: - Commands are executed in the repository directory specified during initialization. """ try: - result = subprocess.run( - command, - cwd=self.repo_path, - capture_output=True, - text=True - ) - return result.returncode, result.stdout, result.stderr - except subprocess.SubprocessError as e: - logger.error(f"Git command failed: {e}") - return 1, "", str(e) + self.repo.git.checkout(branch_name) + except git.GitCommandError as e: + return self._handle_git_error(f"checkout {branch_name}", e) + else: + return 0, f"Switched to branch {branch_name}", "" + + def fetch(self) -> tuple[int, str, str]: + """Fetche updates from remote. + + Returns: + A tuple containing: + - Return code (0 for success, 1 for failure) + - Command output (success message or empty) + - Error output (empty string on success, error message on failure) + + """ # noqa: D401 + try: + self.repo.remotes.origin.fetch() + except git.GitCommandError as e: + return self._handle_git_error("fetch", e) + else: + return 0, "Fetch successful", "" + + def pull(self) -> tuple[int, str, str]: + """Pull changes from remote. + + Returns: + A tuple containing: + - Return code (0 for success, 1 for failure) + - Command output (success message or empty) + - Error output (empty string on success, error message on failure) - def sync_repository(self) -> bool: """ - Synchronize the repository with its remote. + try: + self.repo.remotes.origin.pull() + except git.GitCommandError as e: + return self._handle_git_error("pull", e) + else: + return 0, "Pull successful", "" + + def sync_repository(self) -> bool: + """Synchronize the repository with its remote. This method performs three operations in sequence: 1. Switches to the 'dev' branch @@ -66,22 +108,18 @@ def sync_repository(self) -> bool: 3. Pulls changes Returns: - bool: True if all operations succeeded, False otherwise + True if all operations succeeded, False otherwise. - Note: - Logs success/failure of each operation through the logger. """ operations = [ - (["git", "switch", "dev"], "checkout"), - (["git", "fetch"], "fetch"), - (["git", "pull"], "pull") + (self.switch_branch, "dev"), + (self.fetch,), + (self.pull,), ] - for command, operation in operations: - code, out, err = self.run_command(command) + for operation, *args in operations: + code, _, _ = operation(*args) if code != 0: - logger.error(f"Git {operation} failed: {err}") return False - logger.info(f"Git {operation} successful") - + logger.info("Git %s successful", operation.__name__) return True diff --git a/xml_tools/utils/xml.py b/xml_tools/utils/xml.py index 4d473fc8c..e65450df1 100644 --- a/xml_tools/utils/xml.py +++ b/xml_tools/utils/xml.py @@ -1,51 +1,52 @@ -from lxml import etree as ET -from typing import Dict, Tuple -from pathlib import Path +"""XML Processor.""" + import logging -from core.exceptions import XMLProcessingError +from pathlib import Path +from xml.etree import ElementTree as ET + +from defusedxml import ElementTree logger = logging.getLogger("xml_tools") +BYTES: int = 2 class XMLProcessor: - """ - Utilities for processing XML files. + """Utilities for processing XML files. This class provides static methods for parsing and writing XML files, with special handling for elements containing 'name' attributes. + Uses defusedxml for secure XML processing. """ @staticmethod - def parse_file(path: Path) -> Tuple[ET.ElementTree, ET.Element, Dict[str, Dict[str, str]]]: - """ - Parse an XML file and extract data from elements with 'name' attributes. + def parse_file(path: Path) -> tuple[ET.ElementTree, ET.Element, dict[str, dict[str, str]]]: + """Parse an XML file and extract data from elements with 'name' attributes. Args: path (Path): Path to the XML file to parse Returns: - Tuple[ET.ElementTree, ET.Element, Dict[str, Dict[str, str]]]: A tuple containing: + tuple[ET.ElementTree, ET.Element, dict[str, dict[str, str]]]: A tuple containing: - The parsed XML tree - Root element - Dictionary mapping element names to their properties: - { + { "element_name": { - "text": "element text content", - "attributes": {"attr1": "value1", ...} + "text": "element text content", + "attributes": {"attr1": "value1", ...} } - } - - Raises: - XMLProcessingError: If the file cannot be parsed or read + } Note: Only elements with 'name' attributes are included in the returned dictionary. + """ - if not path.exists() or path.stat().st_size < 2: + if not path.exists() or path.stat().st_size < BYTES: return None, None, {} try: - tree = ET.parse(path) + # Parse XML using defusedxml for security + tree = ElementTree.parse(str(path)) # defusedxml requires string path root = tree.getroot() # Capture all elements with a 'name' attribute @@ -53,44 +54,32 @@ def parse_file(path: Path) -> Tuple[ET.ElementTree, ET.Element, Dict[str, Dict[s for elem in root.findall(".//*[@name]"): name = elem.get("name") if name: - strings[name] = { - "text": elem.text or "", - "attributes": dict(elem.attrib) - } + strings[name] = {"text": elem.text or "", "attributes": dict(elem.attrib)} + except (OSError, ET.ParseError): + logger.exception("Failed to parse %s: ", path) + else: return tree, root, strings - except (ET.ParseError, IOError) as e: - logger.error(f"Failed to parse {path}: {e}") - raise XMLProcessingError(f"Failed to parse {path}: {e}") @staticmethod - def write_file(path: Path, root: ET.Element, pretty_print: bool = True) -> None: - """ - Write an XML element tree to a file. + def write_file(path: Path, root: ET.Element) -> None: + """Write an XML element tree to a file. Args: path (Path): Output file path root (ET.Element): Root element to write pretty_print (bool): Whether to format the output with proper indentation - Raises: - XMLProcessingError: If the file cannot be written - Note: - Creates parent directories if they don't exist - Uses 4-space indentation when pretty_print is True - Writes in UTF-8 encoding with XML declaration + """ try: path.parent.mkdir(parents=True, exist_ok=True) tree = ET.ElementTree(root) ET.indent(tree, space=" ") # Set indentation to 4 spaces - tree.write( - path, - encoding="utf-8", - xml_declaration=True, - pretty_print=pretty_print - ) - except IOError as e: - logger.error(f"Failed to write {path}: {e}") - raise XMLProcessingError(f"Failed to write {path}: {e}") + tree.write(path, encoding="utf-8", xml_declaration=True) + except OSError: + logger.exception("Failed to write %s: ", path)