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 deleted file mode 100644 index 910adefc7..000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: 🐞 Bug report -description: Report a bug or an issue. -title: 'bug: ' -labels: ['Bug report'] -body: - - type: markdown - attributes: - value: | - # ReVanced Patches bug report - - Please check for existing bug reports - [here](https://github.com/ReVanced/revanced-patches/labels/Bug%20report) - before creating a new one. - - - type: textarea - attributes: - label: Bug description - description: | - - Describe your bug in detail - - Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...) - - Add images and videos if possible - - List used patches if applicable - validations: - required: true - - type: textarea - attributes: - label: Error logs - description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell. - render: shell - - type: textarea - attributes: - label: Solution - description: If applicable, add a possible solution to the bug. - - type: textarea - attributes: - label: Additional context - description: Add additional context here. - - type: checkboxes - id: acknowledgements - attributes: - label: Acknowledgements - description: Your issue will be closed if you don't follow the checklist below. - options: - - label: This request is not a duplicate of an existing issue. - required: true - - label: I have chosen an appropriate title. - required: true - - label: All requested information has been provided properly. - required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000..f623d8a57 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,110 @@ +name: 🐞 Bug report +description: Report a bug or an issue. +title: 'bug: ' +labels: ['Bug report'] +body: + - type: markdown + attributes: + value: | +

+ + + + +
+ + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + + +
+
+ Continuing the legacy of Vanced +

+ + # ReVanced Patches bug report + + Before creating a new bug report, please keep the following in mind: + + - **Do not submit a duplicate bug report**: Search for existing bug reports [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Bug+report%22). + - **Review the contribution guidelines**: Make sure your bug report adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md). + - **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app). + - type: textarea + attributes: + label: Bug description + description: | + - Describe your bug in detail + - Add steps to reproduce the bug if possible (Step 1. ... Step 2. ...) + - Add images and videos if possible + - List used patches if applicable + validations: + required: true + - type: textarea + attributes: + label: Error logs + description: Exceptions can be captured by running `logcat | grep AndroidRuntime` in a shell. + render: shell + - type: textarea + attributes: + label: Solution + description: If applicable, add a possible solution to the bug. + - type: textarea + attributes: + label: Additional context + description: Add additional context here. + - type: checkboxes + id: acknowledgements + attributes: + label: Acknowledgements + description: Your bug report will be closed if you don't follow the checklist below. + options: + - label: I have checked all open and closed bug reports and this is not a duplicate. + required: true + - label: I have chosen an appropriate title. + required: true + - label: All requested information has been provided properly. + required: true diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml deleted file mode 100644 index 63f7ad2da..000000000 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: ⭐ Feature request -description: Create a detailed request for a new feature. -title: 'feat: ' -labels: ['Feature request'] -body: - - type: markdown - attributes: - value: | - # ReVanced Patches feature request - - Please check for existing feature requests - [here](https://github.com/ReVanced/revanced-patches/labels/Feature%20request) - before creating a new one. - - type: textarea - attributes: - label: Feature description - description: | - - Describe your feature in detail - - Add images, videos, links, examples, references, etc. if possible - - Add the target application name in case you request a new patch - - type: textarea - attributes: - label: Motivation - description: | - A strong motivation is necessary for a feature request to be considered. - - - Why should this feature be implemented? - - What is the explicit use case? - - What are the benefits? - - What makes this feature important? - validations: - required: true - - type: checkboxes - id: acknowledgements - attributes: - label: Acknowledgements - description: Your issue will be closed if you don't follow the checklist below. - options: - - label: This request is not a duplicate of an existing issue. - required: true - - label: I have chosen an appropriate title. - required: true - - label: All requested information has been provided properly. - required: true diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000..f49436ec6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,106 @@ +name: ⭐ Feature request +description: Create a detailed request for a new feature. +title: 'feat: ' +labels: ['Feature request'] +body: + - type: markdown + attributes: + value: | +

+ + + + +
+ + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + +     + + + + + + +
+
+ Continuing the legacy of Vanced +

+ + # ReVanced Patches feature request + + Before creating a new feature request, please keep the following in mind: + + - **Do not submit a duplicate feature request**: Search for existing feature requests [here](https://github.com/ReVanced/revanced-patches/issues?q=label%3A%22Feature+request%22). + - **Review the contribution guidelines**: Make sure your feature request adheres to it. You can find the guidelines [here](https://github.com/ReVanced/revanced-patches/blob/main/CONTRIBUTING.md). + - **Do not use the issue page for support**: If you need help or have questions, check out other platforms on [revanced.app](https://revanced.app). + - type: textarea + attributes: + label: Feature description + description: | + - Describe your feature in detail + - Add images, videos, links, examples, references, etc. if possible + - Add the target application name in case you request a new patch + - type: textarea + attributes: + label: Motivation + description: | + A strong motivation is necessary for a feature request to be considered. + + - Why should this feature be implemented? + - What is the explicit use case? + - What are the benefits? + - What makes this feature important? + validations: + required: true + - type: checkboxes + id: acknowledgements + attributes: + label: Acknowledgements + description: Your feature request will be closed if you don't follow the checklist below. + options: + - label: I have checked all open and closed feature requests and this is not a duplicate + required: true + - label: I have chosen an appropriate title. + required: true + - label: All requested information has been provided properly. + 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 new file mode 100644 index 000000000..193a26af0 --- /dev/null +++ b/.github/workflows/build_pull_request.yml @@ -0,0 +1,31 @@ +name: Build pull request + +on: + workflow_dispatch: + pull_request: + branches: + - dev + +jobs: + release: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + 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 + + - name: Build + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: ./gradlew build --no-daemon diff --git a/.github/workflows/open_pull_request.yml b/.github/workflows/open_pull_request.yml new file mode 100644 index 000000000..721ab088d --- /dev/null +++ b/.github/workflows/open_pull_request.yml @@ -0,0 +1,28 @@ +name: Open a PR to main + +on: + push: + branches: + - dev + workflow_dispatch: + +env: + MESSAGE: Merge branch `${{ github.head_ref || github.ref_name }}` to `main` + +jobs: + pull-request: + name: Open pull request + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Open pull request + uses: repo-sync/pull-request@v2 + with: + 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 new file mode 100644 index 000000000..608140585 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,60 @@ +name: Release + +on: + workflow_dispatch: + push: + branches: + - main + - dev + +jobs: + release: + name: Release + permissions: + contents: write + packages: write + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + # Make sure the release step uses its own credentials: + # https://github.com/cycjimmy/semantic-release-action#private-packages + persist-credentials: false + fetch-depth: 0 + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + + - name: Cache Gradle + uses: burrunan/gradle-cache-action@v1 + + - name: Build + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # 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 + with: + node-version: "lts/*" + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.GPG_PASSPHRASE }} + fingerprint: ${{ vars.GPG_FINGERPRINT }} + + - name: Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npm exec semantic-release diff --git a/.github/workflows/update-gradle-wrapper.yml b/.github/workflows/update-gradle-wrapper.yml new file mode 100644 index 000000000..8136ad5f3 --- /dev/null +++ b/.github/workflows/update-gradle-wrapper.yml @@ -0,0 +1,18 @@ +name: Update Gradle wrapper + +on: + schedule: + - cron: "0 0 1 * *" + workflow_dispatch: + +jobs: + update: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Update Gradle Wrapper + uses: gradle-update/update-gradle-wrapper-action@v1 + with: + target-branch: dev diff --git a/.gitignore b/.gitignore index 74dd4e728..62f6eb424 100644 --- a/.gitignore +++ b/.gitignore @@ -122,7 +122,8 @@ gradle-app.setting # Dependency directories node_modules/ -# gradle properties, due to Github token +# Gradle properties, due to Github token ./gradle.properties -.DS_Store +# One package is called the same as the Gradle build folder +!**/src/**/build/ \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index e086a70c4..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/README-template.md b/README-template.md index a242ee62f..bd5e74d79 100644 --- a/README-template.md +++ b/README-template.md @@ -26,7 +26,6 @@ Example: } ], "use":true, - "requiresIntegrations":false, "options": [] }, { @@ -39,7 +38,6 @@ Example: } ], "use":true, - "requiresIntegrations":false, "options": [] }, { @@ -52,7 +50,6 @@ Example: } ], "use":true, - "requiresIntegrations":true, "options": [] } ] diff --git a/README.md b/README.md index 1bc4ed592..d197e26d6 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm | `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 | +| `Custom branding icon for YouTube` | Changes the YouTube app icon to the icon specified in patch options. | 18.29.38 ~ 19.16.39 | +| `Custom branding name for YouTube` | Renames the YouTube app to the name specified in patch options. | 18.29.38 ~ 19.16.39 | +| `Custom double tap length` | Adds Double-tap to seek values that are specified in patch options. | 18.29.38 ~ 19.16.39 | | `Custom header for YouTube` | Applies a custom header in the top left corner within the app. | 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 | @@ -67,7 +67,7 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm | `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 | | `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 | +| `Theme` | Changes the app's theme to the values specified in patch options. | 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 | @@ -86,8 +86,8 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm | `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 branding icon for YouTube Music` | Changes the YouTube Music app icon to the icon specified in patch options. | 6.20.51 ~ 7.16.53 | +| `Custom branding name for YouTube Music` | Renames the YouTube Music app to the name specified in patch options. | 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 | @@ -114,6 +114,7 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm | `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 | | `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 | +| `Spoof client` | Adds options to spoof the client to allow track playback. | 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 | @@ -124,8 +125,8 @@ See the [documentation](https://github.com/inotia00/revanced-documentation#readm | 💊 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 | +| `Change package name` | Changes the package name for Reddit to the name specified in patch options. | 2023.12.0 ~ 2024.17.0 | +| `Custom branding name for Reddit` | Renames the Reddit app to the name specified in patch options. | 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 | @@ -166,7 +167,6 @@ Example: } ], "use":true, - "requiresIntegrations":false, "options": [] }, { @@ -185,7 +185,6 @@ Example: } ], "use":true, - "requiresIntegrations":false, "options": [] }, { @@ -201,7 +200,6 @@ Example: } ], "use":true, - "requiresIntegrations":true, "options": [] } ] diff --git a/build.gradle.kts b/build.gradle.kts deleted file mode 100644 index c2151294c..000000000 --- a/build.gradle.kts +++ /dev/null @@ -1,149 +0,0 @@ -import org.gradle.kotlin.dsl.support.listFilesOrdered -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.kotlin) - `maven-publish` - signing -} - -group = "app.revanced" - -repositories { - mavenCentral() - mavenLocal() - google() - maven { - url = uri("https://maven.pkg.github.com/revanced/multidexlib2") - 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) - // Used in JsonGenerator. - implementation(libs.gson) -} - -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } -} - -java { - targetCompatibility = JavaVersion.VERSION_11 -} - -tasks { - withType(Jar::class) { - exclude("app/revanced/generator") - - 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/inotia00/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/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..70895b027 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/flyout/FlyoutPatch.java @@ -0,0 +1,175 @@ +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()), + REMOVE_FROM_PLAYLIST("REMOVE_FROM_PLAYLIST", Settings.HIDE_FLYOUT_MENU_REMOVE_FROM_PLAYLIST.get()), + DOWNLOAD("OFFLINE_DOWNLOAD", Settings.HIDE_FLYOUT_MENU_DOWNLOAD.get()), + SAVE_TO_PLAYLIST("ADD_TO_PLAYLIST", Settings.HIDE_FLYOUT_MENU_SAVE_TO_PLAYLIST.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()), + 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/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..ab3a25dc2 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofClientPatch.java @@ -0,0 +1,84 @@ +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 boolean SPOOF_CLIENT_ENABLED = Settings.SPOOF_CLIENT.get(); + private static final ClientType clientType = ClientType.IOS_MUSIC; + + /** + * Injection point. + */ + public static int getClientTypeId(int originalClientTypeId) { + if (SPOOF_CLIENT_ENABLED) { + return clientType.id; + } + + return originalClientTypeId; + } + + /** + * Injection point. + */ + public static String getClientVersion(String originalClientVersion) { + if (SPOOF_CLIENT_ENABLED) { + return clientType.clientVersion; + } + + return originalClientVersion; + } + + /** + * Injection point. + */ + public static String getClientModel(String originalClientModel) { + if (SPOOF_CLIENT_ENABLED) { + return clientType.deviceModel; + } + + return originalClientModel; + } + + /** + * Injection point. + */ + public static String getOsVersion(String originalOsVersion) { + if (SPOOF_CLIENT_ENABLED) { + return clientType.osVersion; + } + + return originalOsVersion; + } + + /** + * Injection point. + */ + public static String getUserAgent(String originalUserAgent) { + if (SPOOF_CLIENT_ENABLED) { + return clientType.userAgent; + } + + return originalUserAgent; + } + + /** + * Injection point. + */ + public static boolean isClientSpoofingEnabled() { + return SPOOF_CLIENT_ENABLED; + } + + /** + * Injection point. + * When spoofing the client to iOS, the playback speed menu is missing from the player response. + * Return true to force create the playback speed menu. + */ + public static boolean forceCreatePlaybackSpeedMenu(boolean original) { + if (SPOOF_CLIENT_ENABLED) { + 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..684c8b0b0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/client/AppClient.java @@ -0,0 +1,122 @@ +package app.revanced.extension.music.patches.misc.client; + +import android.os.Build; + +import androidx.annotation.Nullable; + +public class AppClient { + + /** + * 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 = "6.21"; + private static final String DEVICE_MAKE_IOS = "Apple"; + /** + * See this GitHub Gist for more + * information. + *

+ */ + private static final String DEVICE_MODEL_IOS = "iPhone16,2"; + private static final String OS_NAME_IOS = "iOS"; + 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 = "com.google.ios.youtubemusic/" + + CLIENT_VERSION_IOS + + "(" + + DEVICE_MODEL_IOS + + "; U; CPU iOS " + + USER_AGENT_VERSION_IOS + + " like Mac OS X)"; + + private AppClient() { + } + + public enum ClientType { + IOS_MUSIC(26, + DEVICE_MAKE_IOS, + DEVICE_MODEL_IOS, + CLIENT_VERSION_IOS, + OS_NAME_IOS, + OS_VERSION_IOS, + null, + USER_AGENT_IOS, + true + ); + + /** + * YouTube + * client type + */ + public final int id; + + /** + * Device manufacturer. + */ + @Nullable + public final String deviceMake; + + /** + * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) + */ + public final String deviceModel; + + /** + * Device OS name. + */ + @Nullable + public final String osName; + + /** + * 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. + */ + public final Integer androidSdkVersion; + + /** + * App version. + */ + public final String clientVersion; + + /** + * If the client can access the API logged in. + */ + public final boolean canLogin; + + ClientType(int id, + @Nullable String deviceMake, + String deviceModel, + String clientVersion, + @Nullable String osName, + String osVersion, + Integer androidSdkVersion, + String userAgent, + boolean canLogin + ) { + this.id = id; + this.deviceMake = deviceMake; + this.deviceModel = deviceModel; + this.clientVersion = clientVersion; + this.osName = osName; + this.osVersion = osVersion; + this.androidSdkVersion = androidSdkVersion; + this.userAgent = userAgent; + this.canLogin = canLogin; + } + } +} 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..0e31a45df --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/player/PlayerPatch.java @@ -0,0 +1,198 @@ +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; + +@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() { + if (!Settings.ALWAYS_SHUFFLE.get()) + return; + 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..d72807458 --- /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 getColor(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..4524a10d0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/InitializationPatch.java @@ -0,0 +1,34 @@ +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.music.utils.ExtendedUtils; +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); + } + + public static void setDeviceInformation(@NonNull Activity mActivity) { + ExtendedUtils.setApplicationLabel(); + ExtendedUtils.setVersionName(); + } +} \ 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..b864a9b3f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/utils/ReturnYouTubeDislikePatch.java @@ -0,0 +1,112 @@ +package app.revanced.extension.music.patches.utils; + +import static app.revanced.extension.shared.returnyoutubedislike.ReturnYouTubeDislike.Vote; + +import android.text.Spanned; + +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 { + /** + * 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); + } 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..bec27d1b3 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/returnyoutubedislike/ReturnYouTubeDislike.java @@ -0,0 +1,564 @@ +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' + + private static final int SEPARATOR_COLOR = 872415231; + + /** + * 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 final Rect leftSeparatorBounds; + private static final Rect middleSeparatorBounds; + + static { + DisplayMetrics dp = Objects.requireNonNull(Utils.getContext()).getResources().getDisplayMetrics(); + + leftSeparatorBounds = new Rect(0, 0, + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.2f, dp), + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 25, dp)); + final int middleSeparatorSize = + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp); + middleSeparatorBounds = new Rect(0, 0, middleSeparatorSize, middleSeparatorSize); + + 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; + + @NonNull + private static SpannableString createDislikeSpan(@NonNull Spanned oldSpannable, + @NonNull RYDVoteData voteData) { + 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\u2009"); + final boolean compactLayout = Settings.RYD_COMPACT_LAYOUT.get(); + + if (!compactLayout) { + String leftSeparatorString = "\u200E "; // u200E = left to right character + Spannable leftSeparatorSpan = new SpannableString(leftSeparatorString); + ShapeDrawable shapeDrawable = new ShapeDrawable(new RectShape()); + shapeDrawable.getPaint().setColor(SEPARATOR_COLOR); + 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(SEPARATOR_COLOR); + 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) { + return waitForFetchAndUpdateReplacementSpan(original); + } + + @NonNull + private Spanned waitForFetchAndUpdateReplacementSpan(@NonNull Spanned original) { + 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); + 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..6235a32a5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java @@ -0,0 +1,252 @@ +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.utils.PatchStatus; +import app.revanced.extension.shared.settings.BaseSettings; +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.LongSetting; +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", TRUE); + 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_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_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 ENABLE_OPUS_CODEC = new BooleanSetting("revanced_enable_opus_codec", FALSE, true); + public static final BooleanSetting SPOOF_CLIENT = new BooleanSetting("revanced_spoof_client", FALSE, true); + public static final BooleanSetting SETTINGS_IMPORT_EXPORT = new BooleanSetting("revanced_extended_settings_import_export", FALSE, false); + + + // 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); + + 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, + 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..a819e425b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/preference/ReVancedPreferenceFragment.java @@ -0,0 +1,351 @@ +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.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.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)) { + 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(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.getApplicationLabel(); + var versionName = ExtendedUtils.getVersionName(); + 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(sb.toString(), false); + if (restartNeeded) { + ReVancedPreferenceFragment.showRebootDialog(); + } + } catch (IOException e) { + showToastShort(str("revanced_extended_settings_import_failed")); + throw new RuntimeException(e); + } + } + + private void importSettings(String replacementSettings) { + try { + existingSettings = Setting.exportToJson(null); + if (replacementSettings.equals(existingSettings)) { + return; + } + final boolean restartNeeded = Setting.importFromJSON(replacementSettings, false); + 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..813e0d0f9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/sponsorblock/SponsorBlockSettings.java @@ -0,0 +1,52 @@ +package app.revanced.extension.music.sponsorblock; + +import androidx.annotation.NonNull; + +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 { + 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(); + } + + /** + * Updates internal data based on {@link Setting} values. + */ + public static void updateFromImportedSettings() { + SegmentCategory.loadAllCategoriesFromSettings(); + } +} 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..3a7243544 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/utils/ExtendedUtils.java @@ -0,0 +1,46 @@ +package app.revanced.extension.music.utils; + +import android.app.AlertDialog; +import android.content.Context; +import android.util.TypedValue; +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); + } + + private static int dpToPx(float dp) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, context.getResources().getDisplayMetrics()); + } + + @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..96a049b87 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/GmsCoreSupport.java @@ -0,0 +1,233 @@ +package app.revanced.extension.shared.patches; + +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.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 positiveButtonStringRef, + DialogInterface.OnClickListener onPositiveClickListener) { + // 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(() -> new AlertDialog.Builder(context) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setTitle(str("gms_core_dialog_title")) + .setMessage(str(dialogMessageRef)) + .setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener) + // Allow using back button to skip the action, just in case the check can never be satisfied. + .setCancelable(true) + .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", + (dialog, id) -> open(mActivity, DONT_KILL_MY_APP_LINK)); + return; + } + + // Check if GmsCore is whitelisted from battery optimizations. + if (batteryOptimizationsEnabled(mActivity)) { + Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations"); + showBatteryOptimizationDialog(mActivity, + "gms_core_dialog_not_whitelisted_using_battery_optimizations_message", + "gms_core_dialog_continue_text", + (dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(mActivity)); + } + } 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..d1065c5ba --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/PatchStatus.java @@ -0,0 +1,8 @@ +package app.revanced.extension.shared.patches; + +@SuppressWarnings("unused") +public class PatchStatus { + public static boolean HideFullscreenAdsDefaultBoolean() { + 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..32e177c17 --- /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/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..e22d73f52 --- /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..f82bcfe87 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/patches/spans/InclusiveSpanPatch.java @@ -0,0 +1,201 @@ +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/requests/Requester.java b/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Requester.java new file mode 100644 index 000000000..8ab950f25 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/requests/Requester.java @@ -0,0 +1,142 @@ +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; + +@SuppressWarnings("unused") +public class Requester { + public 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()); + connection.setRequestProperty("User-Agent", System.getProperty("http.agent") + ";"); + + 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..9ce0c7654 --- /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 Route.Method method; + private final int paramCount; + + public Route(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 Route.Method getMethod() { + return method; + } + + public Route.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 Route.CompiledRoute(this, compiledRoute.toString()); + } + + 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 + } + + 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 Route.Method getMethod() { + return baseRoute.method; + } + } +} \ 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..5ef848c54 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java @@ -0,0 +1,46 @@ +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 app.revanced.extension.shared.patches.ReturnYouTubeUsernamePatch.DisplayFormat; + +/** + * 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); + + /** + * 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); + + /** + * @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..cdd81b5b4 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/Setting.java @@ -0,0 +1,475 @@ +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.jetbrains.annotations.NotNull; +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; + }; + } + + /** + * 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.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); + } + + @NotNull + @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); + } + } + if (alertDialogContext != null) { + app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings.showExportWarningIfNeeded(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 ""; + } + } + + public static boolean importFromJSON(@NonNull String settingsJsonString) { + return importFromJSON(settingsJsonString, true); + } + + /** + * @return if any settings that require a reboot were changed. + */ + public static boolean importFromJSON(@NonNull String settingsJsonString, boolean isYouTube) { + 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(); + } + } + + // SB Enum categories are saved using StringSettings. + // Which means they need to reload again if changed by other code (such as here). + // This call could be removed by creating a custom Setting class that manages the + // "String <-> Enum" logic or by adding an event hook of when settings are imported. + // But for now this is simple and works. + if (isYouTube) { + app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings.updateFromImportedSettings(); + } else { + app.revanced.extension.music.sponsorblock.SponsorBlockSettings.updateFromImportedSettings(); + } + + 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..414ca0a18 --- /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(getEditText().getText().toString()) + ); + } catch (Exception ex) { + Logger.printException(() -> "onPrepareDialogBuilder failure", ex); + } + } + + private void importSettings(String replacementSettings) { + try { + if (replacementSettings.equals(existingSettings)) { + return; + } + AbstractPreferenceFragment.settingImportInProgress = true; + final boolean rebootNeeded = Setting.importFromJSON(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..6583fc7ef --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Logger.java @@ -0,0 +1,126 @@ +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) { + if (ENABLE_DEBUG_LOGGING.get()) { + Log.d(REVANCED_LOG_PREFIX + message.findOuterClassSimpleName(), message.buildMessageString()); + } + } + + /** + * 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..7975ba063 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/PackageUtils.java @@ -0,0 +1,91 @@ +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 { + private static String applicationLabel = ""; + private static int smallestScreenWidthDp = 0; + private static String versionName = ""; + + public static String getApplicationLabel() { + return applicationLabel; + } + + public static String getVersionName() { + return versionName; + } + + 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 smallestScreenWidthDp >= 600; + } + + public static void setApplicationLabel() { + final PackageInfo packageInfo = getPackageInfo(); + if (packageInfo != null) { + final ApplicationInfo applicationInfo = packageInfo.applicationInfo; + if (applicationInfo != null) { + applicationLabel = (String) applicationInfo.loadLabel(getPackageManager()); + } + } + } + + public static void setSmallestScreenWidthDp() { + smallestScreenWidthDp = context.getResources().getConfiguration().smallestScreenWidthDp; + } + + public static void setVersionName() { + final PackageInfo packageInfo = getPackageInfo(); + if (packageInfo != null) { + versionName = packageInfo.versionName; + } + } + + public static int getSmallestScreenWidthDp() { + return 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..aaf9f21c7 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/utils/Utils.java @@ -0,0 +1,737 @@ +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.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; + } + + /** + * @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; + } + + /** + * 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..e19532662 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/AdsFilter.java @@ -0,0 +1,160 @@ +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", + + // "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..0f31c1e05 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CarouselShelfFilter.java @@ -0,0 +1,95 @@ +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_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_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 + ); + + 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; + } + 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..e375488e2 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FeedComponentsFilter.java @@ -0,0 +1,278 @@ +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", + "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..9b0779ecc --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FeedVideoViewsFilter.java @@ -0,0 +1,180 @@ +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.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 final StringFilterGroup feedVideoFilter = new StringFilterGroup( + null, + "video_lockup_with_attachment.eml" + ); + + 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; + } + + private final String ARROW = " -> "; + private final String VIEWS = "views"; + private final String[] parts = Settings.HIDE_VIDEO_VIEW_COUNTS_MULTIPLIER.get().split("\\n"); + private Pattern[] viewCountPatterns = null; + + /** + * 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 (viewCountPatterns == null) { + viewCountPatterns = getViewCountPatterns(parts); + } + + for (Pattern pattern : viewCountPatterns) { + final Matcher matcher = pattern.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); + return num * multiplierValue < lessThan || num * multiplierValue > greaterThan; + } + } + + 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[] getViewCountPatterns(String[] parts) { + StringBuilder prefixPatternBuilder = new StringBuilder("(\\d+(?:[.,]\\d+)?)\\s?("); // LTR layout + StringBuilder secondPatternBuilder = new StringBuilder(); // RTL layout + StringBuilder suffixBuilder = getSuffixBuilder(parts, prefixPatternBuilder, secondPatternBuilder); + + prefixPatternBuilder.deleteCharAt(prefixPatternBuilder.length() - 1); // Remove the trailing | + prefixPatternBuilder.append(")?\\s*"); + prefixPatternBuilder.append(suffixBuilder.length() > 0 ? suffixBuilder.toString() : VIEWS); + + secondPatternBuilder.deleteCharAt(secondPatternBuilder.length() - 1); // Remove the trailing | + secondPatternBuilder.append(")?"); + + final Pattern[] patterns = new Pattern[2]; + patterns[0] = Pattern.compile(prefixPatternBuilder.toString()); + patterns[1] = Pattern.compile(secondPatternBuilder.toString()); + + return patterns; + } + + @NonNull + private synchronized StringBuilder getSuffixBuilder(String[] parts, StringBuilder prefixPatternBuilder, StringBuilder secondPatternBuilder) { + StringBuilder suffixBuilder = new StringBuilder(); + + for (String part : parts) { + final String[] pair = part.split(ARROW); + final String pair0 = pair[0].trim(); + final String pair1 = pair[1].trim(); + + if (pair.length == 2 && !pair1.equals(VIEWS)) { + prefixPatternBuilder.append(pair0).append("|"); + } + + if (pair.length == 2 && pair1.equals(VIEWS)) { + suffixBuilder.append(pair0); + secondPatternBuilder.append(pair0).append("\\s*").append(prefixPatternBuilder); + } + } + return suffixBuilder; + } + + private synchronized long getMultiplierValue(String[] parts, String multiplier) { + for (String part : parts) { + final String[] pair = part.split(ARROW); + final String pair0 = pair[0].trim(); + final String pair1 = pair[1].trim(); + + if (pair.length == 2 && pair0.equals(multiplier) && !pair1.equals(VIEWS)) { + return Long.parseLong(pair[1].replaceAll("[^\\d]", "")); + } + } + + 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..835b709d5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlayerComponentsFilter.java @@ -0,0 +1,129 @@ +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 liveChat = new StringFilterGroup( + Settings.HIDE_LIVE_CHAT_MESSAGES, + "live_chat_text_message", + "viewer_engagement_message" // message about poll, not poll itself + ); + + final StringFilterGroup medicalPanel = new StringFilterGroup( + Settings.HIDE_MEDICAL_PANEL, + "emergency_onebox", + "medical_panel" + ); + + 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, + liveChat, + medicalPanel, + 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..c6dd027cf --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsButtonFilter.java @@ -0,0 +1,274 @@ +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; + +@SuppressWarnings("unused") +public final class ShortsButtonFilter extends Filter { + 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 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 + // + 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 (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(path, identifier, allValue, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + 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/ShortsShelfFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsShelfFilter.java new file mode 100644 index 000000000..113b07ec9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsShelfFilter.java @@ -0,0 +1,189 @@ +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; + } + + if (selectedNavButton == null) { + return hideHomeAndRelatedVideos; // Unknown tab, treat the same as home. + } + + 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..46a494a87 --- /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 boolean hideFloatingButton() { + return Settings.HIDE_FLOATING_BUTTON.get(); + } + + 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..272eac1dd --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/ChangeStartPagePatch.java @@ -0,0 +1,134 @@ +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), + + /** + * 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..cccf47d41 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/GeneralPatch.java @@ -0,0 +1,589 @@ +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.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 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; + } + + public static boolean enableTranslucentNavigationBar() { + return Settings.ENABLE_TRANSLUCENT_NAVIGATION_BAR.get(); + } + + 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); + } + + // 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..9c13d5a52 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/general/MiniplayerPatch.java @@ -0,0 +1,197 @@ +package app.revanced.extension.youtube.patches.general; + +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.validateValue; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +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 final class MiniplayerPatch { + + /** + * Mini player type. Null fields indicates to use the original un-patched value. + */ + public enum MiniplayerType { + /** + * Unmodified type, and same as un-patched. + */ + ORIGINAL(null, null), + PHONE(false, null), + TABLET(true, null), + MODERN_1(null, 1), + MODERN_2(null, 2), + MODERN_3(null, 3); + + /** + * 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; + } + } + + /** + * 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(); + + private static final boolean DOUBLE_TAP_ACTION_ENABLED = + (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_2 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get(); + + private static final boolean DRAG_AND_DROP_ENABLED = + CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_DRAG_AND_DROP.get(); + + private static final boolean HIDE_EXPAND_CLOSE_AVAILABLE = + (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && + !DOUBLE_TAP_ACTION_ENABLED && + !DRAG_AND_DROP_ENABLED; + + private static final boolean HIDE_EXPAND_CLOSE_ENABLED = + HIDE_EXPAND_CLOSE_AVAILABLE && Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get(); + + private static final boolean HIDE_SUBTEXT_ENABLED = + (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_SUBTEXT.get(); + + private static final boolean HIDE_REWIND_FORWARD_ENABLED = + CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get(); + + private static final int OPACITY_LEVEL; + + static { + final int opacity = validateValue( + Settings.MINIPLAYER_OPACITY, + 0, + 100, + "revanced_miniplayer_opacity_invalid_toast" + ); + + OPACITY_LEVEL = (opacity * 255) / 100; + } + + /** + * 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 enableMiniplayerDoubleTapAction() { + return DOUBLE_TAP_ACTION_ENABLED; + } + + /** + * Injection point. + */ + public static boolean enableMiniplayerDragAndDrop() { + return DRAG_AND_DROP_ENABLED; + } + + /** + * 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 boolean hideMiniplayerSubTexts(View view) { + // Different subviews are passed in, but only TextView and layouts are of interest here. + final boolean hideView = HIDE_SUBTEXT_ENABLED && (view instanceof TextView || view instanceof LinearLayout); + Utils.hideViewByRemovingFromParentUnderCondition(hideView, view); + return hideView || view == null; + } + + /** + * Injection point. + */ + public static void playerOverlayGroupCreated(View group) { + // Modern 2 has an half broken subtitle that is always present. + // Always hide it to make the miniplayer mostly usable. + if (CURRENT_TYPE == MODERN_2 && MODERN_OVERLAY_SUBTITLE_TEXT != 0) { + if (group instanceof ViewGroup viewGroup) { + View subtitleText = Utils.getChildView(viewGroup, true, + view -> view.getId() == MODERN_OVERLAY_SUBTITLE_TEXT); + + if (subtitleText != null) { + subtitleText.setVisibility(View.GONE); + Logger.printDebug(() -> "Modern overlay subtitle view set to hidden"); + } + } + } + } +} 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..4cf3456ee --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/BackgroundPlaybackPatch.java @@ -0,0 +1,12 @@ +package app.revanced.extension.youtube.patches.misc; + +import app.revanced.extension.youtube.shared.ShortsPlayerState; + +@SuppressWarnings("unused") +public class BackgroundPlaybackPatch { + + public static boolean allowBackgroundPlayback(boolean original) { + return original || ShortsPlayerState.getCurrent().isClosed(); + } + +} 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/SpoofStreamingDataPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/SpoofStreamingDataPatch.java new file mode 100644 index 000000000..f2ed0c18b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/SpoofStreamingDataPatch.java @@ -0,0 +1,185 @@ +package app.revanced.extension.youtube.patches.misc; + +import android.net.Uri; +import android.text.TextUtils; + +import androidx.annotation.Nullable; + +import java.nio.ByteBuffer; +import java.util.Map; +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; +import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType; +import app.revanced.extension.youtube.patches.misc.requests.StreamingDataRequest; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class SpoofStreamingDataPatch { + private static final boolean SPOOF_STREAMING_DATA = Settings.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); + + /** + * 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 returning unreachable url"); + + return UNREACHABLE_HOST_URI_STRING; + } + } catch (Exception ex) { + Logger.printException(() -> "blockInitPlaybackRequest failure", ex); + } + } + + return originalUrlString; + } + + /** + * Injection point. + */ + public static boolean isSpoofingEnabled() { + return SPOOF_STREAMING_DATA; + } + + /** + * 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. + if (path != null && path.contains("player") && !path.contains("heartbeat")) { + String videoId = Objects.requireNonNull(uri.getQueryParameter("id")); + StreamingDataRequest.fetchRequest(videoId, 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 (Settings.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. + * 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 && Settings.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; + } + + public static final class iOSAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return Settings.SPOOF_STREAMING_DATA.get() && Settings.SPOOF_STREAMING_DATA_TYPE.get() == ClientType.IOS; + } + } +} 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/misc/client/AppClient.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/client/AppClient.java new file mode 100644 index 000000000..5c8ed662c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/client/AppClient.java @@ -0,0 +1,221 @@ +package app.revanced.extension.youtube.patches.misc.client; + +import static app.revanced.extension.shared.utils.StringRef.str; + +import android.os.Build; + +import androidx.annotation.Nullable; + +public class AppClient { + + // ANDROID + private static final String OS_NAME_ANDROID = "Android"; + + // IOS + /** + * 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.47.7"; + private static final String DEVICE_MAKE_IOS = "Apple"; + /** + * The device machine id for the iPhone XS Max (iPhone11,4), used to get 60fps. + * The device machine id for the iPhone 16 Pro Max (iPhone17,2), used to get HDR with AV1 hardware decoding. + * + *

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

+ */ + private static final String DEVICE_MODEL_IOS = DeviceHardwareSupport.allowAV1() + ? "iPhone17,2" + : "iPhone11,4"; + private static final String OS_NAME_IOS = "iOS"; + /** + * The minimum supported OS version for the iOS YouTube client is iOS 14.0. + * Using an invalid OS version will use the AVC codec. + */ + private static final String OS_VERSION_IOS = DeviceHardwareSupport.allowVP9() + ? "18.1.1.22B91" + : "13.7.17H35"; + private static final String USER_AGENT_VERSION_IOS = DeviceHardwareSupport.allowVP9() + ? "18_1_1" + : "13_7"; + private static final String USER_AGENT_IOS = "com.google.ios.youtube/" + + CLIENT_VERSION_IOS + + "(" + + DEVICE_MODEL_IOS + + "; U; CPU iOS " + + USER_AGENT_VERSION_IOS + + " like Mac OS X)"; + + // ANDROID VR + /** + * 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.60.19"; + /** + * 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 int ANDROID_SDK_VERSION_ANDROID_VR = 32; + /** + * 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 USER_AGENT_ANDROID_VR = "com.google.android.apps.youtube.vr.oculus/" + + CLIENT_VERSION_ANDROID_VR + + " (Linux; U; Android " + + OS_VERSION_ANDROID_VR + + "; GB) gzip"; + + // ANDROID UNPLUGGED + private static final String CLIENT_VERSION_ANDROID_UNPLUGGED = "8.47.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 int ANDROID_SDK_VERSION_ANDROID_UNPLUGGED = 34; + private static final String USER_AGENT_ANDROID_UNPLUGGED = "com.google.android.apps.youtube.unplugged/" + + CLIENT_VERSION_ANDROID_UNPLUGGED + + " (Linux; U; Android " + + OS_VERSION_ANDROID_UNPLUGGED + + "; GB) gzip"; + + private AppClient() { + } + + public enum ClientType { + IOS(5, + DEVICE_MAKE_IOS, + DEVICE_MODEL_IOS, + CLIENT_VERSION_IOS, + OS_NAME_IOS, + OS_VERSION_IOS, + null, + USER_AGENT_IOS, + false + ), + ANDROID_VR(28, + null, + DEVICE_MODEL_ANDROID_VR, + CLIENT_VERSION_ANDROID_VR, + OS_NAME_ANDROID, + OS_VERSION_ANDROID_VR, + ANDROID_SDK_VERSION_ANDROID_VR, + USER_AGENT_ANDROID_VR, + true + ), + ANDROID_UNPLUGGED(29, + null, + DEVICE_MODEL_ANDROID_UNPLUGGED, + CLIENT_VERSION_ANDROID_UNPLUGGED, + OS_NAME_ANDROID, + OS_VERSION_ANDROID_UNPLUGGED, + ANDROID_SDK_VERSION_ANDROID_UNPLUGGED, + USER_AGENT_ANDROID_UNPLUGGED, + true + ); + + public final String friendlyName; + + /** + * YouTube + * client type + */ + public final int id; + + /** + * Device manufacturer. + */ + @Nullable + public final String deviceMake; + + /** + * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model) + */ + public final String deviceModel; + + /** + * Device OS name. + */ + @Nullable + public final String osName; + + /** + * 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. + */ + public final Integer androidSdkVersion; + + /** + * App version. + */ + public final String clientVersion; + + /** + * If the client can access the API logged in. + */ + public final boolean canLogin; + + ClientType(int id, + @Nullable String deviceMake, + String deviceModel, + String clientVersion, + @Nullable String osName, + String osVersion, + Integer androidSdkVersion, + String userAgent, + boolean canLogin + ) { + this.friendlyName = str("revanced_spoof_streaming_data_type_entry_" + name().toLowerCase()); + this.id = id; + this.deviceMake = deviceMake; + this.deviceModel = deviceModel; + this.clientVersion = clientVersion; + this.osName = osName; + this.osVersion = osVersion; + this.androidSdkVersion = androidSdkVersion; + this.userAgent = userAgent; + this.canLogin = canLogin; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/client/DeviceHardwareSupport.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/client/DeviceHardwareSupport.java new file mode 100644 index 000000000..91ffd5aae --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/client/DeviceHardwareSupport.java @@ -0,0 +1,54 @@ +package app.revanced.extension.youtube.patches.misc.client; + +import static app.revanced.extension.shared.utils.Utils.isSDKAbove; + +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; + +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.youtube.settings.Settings; + +public class DeviceHardwareSupport { + private static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9; + private static final boolean DEVICE_HAS_HARDWARE_DECODING_AV1; + + static { + boolean vp9found = false; + boolean av1found = false; + MediaCodecList codecList = new MediaCodecList(MediaCodecList.ALL_CODECS); + final boolean deviceIsAndroidTenOrLater = isSDKAbove(29); + + for (MediaCodecInfo codecInfo : codecList.getCodecInfos()) { + final boolean isHardwareAccelerated = deviceIsAndroidTenOrLater + ? codecInfo.isHardwareAccelerated() + : !codecInfo.getName().startsWith("OMX.google"); // Software decoder. + if (isHardwareAccelerated && !codecInfo.isEncoder()) { + for (String type : codecInfo.getSupportedTypes()) { + if (type.equalsIgnoreCase("video/x-vnd.on2.vp9")) { + vp9found = true; + } else if (type.equalsIgnoreCase("video/av01")) { + av1found = true; + } + } + } + } + + DEVICE_HAS_HARDWARE_DECODING_VP9 = vp9found; + DEVICE_HAS_HARDWARE_DECODING_AV1 = av1found; + + Logger.printDebug(() -> DEVICE_HAS_HARDWARE_DECODING_AV1 + ? "Device supports AV1 hardware decoding\n" + : "Device does not support AV1 hardware decoding\n" + + (DEVICE_HAS_HARDWARE_DECODING_VP9 + ? "Device supports VP9 hardware decoding" + : "Device does not support VP9 hardware decoding")); + } + + public static boolean allowVP9() { + return DEVICE_HAS_HARDWARE_DECODING_VP9 && !Settings.SPOOF_STREAMING_DATA_IOS_FORCE_AVC.get(); + } + + public static boolean allowAV1() { + return allowVP9() && DEVICE_HAS_HARDWARE_DECODING_AV1; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/PlayerRoutes.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/PlayerRoutes.java new file mode 100644 index 000000000..22032119a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/PlayerRoutes.java @@ -0,0 +1,103 @@ +package app.revanced.extension.youtube.patches.misc.requests; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.Objects; + +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; +import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType; + +@SuppressWarnings("deprecation") +public final class PlayerRoutes { + /** + * The base URL of requests of non-web clients to the InnerTube internal API. + */ + private static final String YOUTUBEI_V1_GAPIS_URL = "https://youtubei.googleapis.com/youtubei/v1/"; + + static final Route.CompiledRoute GET_STREAMING_DATA = new Route( + Route.Method.POST, + "player" + + "?fields=streamingData" + + "&alt=proto" + ).compile(); + + static final Route.CompiledRoute GET_PLAYLIST_PAGE = new Route( + Route.Method.POST, + "next" + + "?fields=contents.singleColumnWatchNextResults.playlist.playlist" + ).compile(); + + /** + * TCP connection and HTTP read timeout + */ + private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds. + + private PlayerRoutes() { + } + + static String createInnertubeBody(ClientType clientType, String videoId) { + return createInnertubeBody(clientType, videoId, null); + } + + static String createInnertubeBody(ClientType clientType, String videoId, String playlistId) { + JSONObject innerTubeBody = new JSONObject(); + + try { + JSONObject context = new JSONObject(); + + JSONObject client = new JSONObject(); + client.put("clientName", clientType.name()); + client.put("clientVersion", clientType.clientVersion); + client.put("deviceModel", clientType.deviceModel); + client.put("osVersion", clientType.osVersion); + if (clientType.deviceMake != null) { + client.put("deviceMake", clientType.deviceMake); + } + if (clientType.osName != null) { + client.put("osName", clientType.osName); + } + if (clientType.androidSdkVersion != null) { + client.put("androidSdkVersion", clientType.androidSdkVersion.toString()); + } + String languageCode = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().locale.getLanguage(); + client.put("hl", languageCode); + + context.put("client", client); + + innerTubeBody.put("context", context); + innerTubeBody.put("contentCheckOk", true); + innerTubeBody.put("racyCheckOk", true); + innerTubeBody.put("videoId", videoId); + if (playlistId != null) { + innerTubeBody.put("playlistId", playlistId); + } + } catch (JSONException e) { + Logger.printException(() -> "Failed to create innerTubeBody", e); + } + + return innerTubeBody.toString(); + } + + /** + * @noinspection SameParameterValue + */ + static HttpURLConnection getPlayerResponseConnectionFromRoute(Route.CompiledRoute route, ClientType clientType) throws IOException { + var connection = Requester.getConnectionFromCompiledRoute(YOUTUBEI_V1_GAPIS_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/youtube/patches/misc/requests/PlaylistRequest.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/PlaylistRequest.java new file mode 100644 index 000000000..370e23cfc --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/PlaylistRequest.java @@ -0,0 +1,199 @@ +package app.revanced.extension.youtube.patches.misc.requests; + +import static app.revanced.extension.youtube.patches.misc.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.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; +import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType; +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 = PlayerRoutes.createInnertubeBody( + clientType, + 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/patches/misc/requests/StreamingDataRequest.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/StreamingDataRequest.java new file mode 100644 index 000000000..f3a2479ec --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/misc/requests/StreamingDataRequest.java @@ -0,0 +1,242 @@ +package app.revanced.extension.youtube.patches.misc.requests; + +import static app.revanced.extension.youtube.patches.misc.requests.PlayerRoutes.GET_STREAMING_DATA; + +import androidx.annotation.GuardedBy; +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.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +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.components.ByteArrayFilterGroup; +import app.revanced.extension.shared.utils.Logger; +import app.revanced.extension.shared.utils.Utils; +import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType; +import app.revanced.extension.youtube.settings.Settings; + +public class StreamingDataRequest { + private static final ClientType[] ALL_CLIENT_TYPES = ClientType.values(); + private static final ClientType[] CLIENT_ORDER_TO_USE; + + static { + ClientType preferredClient = Settings.SPOOF_STREAMING_DATA_TYPE.get(); + CLIENT_ORDER_TO_USE = new ClientType[ALL_CLIENT_TYPES.length]; + + CLIENT_ORDER_TO_USE[0] = preferredClient; + + int i = 1; + for (ClientType c : ALL_CLIENT_TYPES) { + if (c != preferredClient) { + CLIENT_ORDER_TO_USE[i++] = c; + } + } + } + + private static ClientType lastSpoofedClientType; + + public static String getLastSpoofedClientName() { + return lastSpoofedClientType == null + ? "Unknown" + : lastSpoofedClientType.friendlyName; + } + + /** + * 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; + + @GuardedBy("itself") + 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 void fetchRequest(@NonNull String videoId, Map fetchHeaders) { + cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); + } + + @Nullable + public static StreamingDataRequest getRequestForVideoId(@Nullable String videoId) { + return cache.get(videoId); + } + + private static void handleConnectionError(String toastMessage, @Nullable Exception ex) { + Logger.printInfo(() -> toastMessage, ex); + } + + // Available only to logged in users. + private static final String AUTHORIZATION_HEADER = "Authorization"; + + private static final String[] REQUEST_HEADER_KEYS = { + AUTHORIZATION_HEADER, + "X-GOOG-API-FORMAT-VERSION", + "X-Goog-Visitor-Id" + }; + + private static void writeInnerTubeBody(HttpURLConnection connection, ClientType clientType, + String videoId, Map playerHeaders) { + try { + connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS); + connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS); + + if (playerHeaders != null) { + for (String key : REQUEST_HEADER_KEYS) { + if (!clientType.canLogin && key.equals(AUTHORIZATION_HEADER)) { + continue; + } + String value = playerHeaders.get(key); + if (value != null) { + connection.setRequestProperty(key, value); + } + } + } + + String innerTubeBody = PlayerRoutes.createInnertubeBody(clientType, videoId); + byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(requestBody.length); + connection.getOutputStream().write(requestBody); + } catch (IOException ex) { + handleConnectionError("Network error", 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(); + String clientTypeName = clientType.name(); + Logger.printDebug(() -> "Fetching video streams for: " + videoId + " using client: " + clientType.name()); + + try { + HttpURLConnection connection = PlayerRoutes.getPlayerResponseConnectionFromRoute(GET_STREAMING_DATA, clientType); + writeInnerTubeBody(connection, clientType, videoId, playerHeaders); + + final int responseCode = connection.getResponseCode(); + if (responseCode == 200) return connection; + + handleConnectionError(clientTypeName + " not available with response code: " + + responseCode + " message: " + connection.getResponseMessage(), + null); + } catch (Exception ex) { + Logger.printException(() -> "send failed", ex); + } finally { + Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms"); + } + + return null; + } + + private static final ByteArrayFilterGroup liveStreams = + new ByteArrayFilterGroup( + Settings.SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK, + "yt_live_broadcast", + "yt_premiere_broadcast" + ); + + private static ByteBuffer fetch(@NonNull 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); + + // gzip encoding doesn't response with content length (-1), + // but empty response body does. + if (connection == null || connection.getContentLength() == 0) + continue; + + 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); + } + if (clientType == ClientType.IOS && liveStreams.check(buffer).isFiltered()) { + Logger.printDebug(() -> "Ignore IOS spoofing as it is a livestream (video: " + videoId + ")"); + continue; + } + 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; + } + + 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 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 + '\'' + '}'; + } +} \ No newline at end of file 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..e6a572af6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/overlaybutton/ExternalDownload.java @@ -0,0 +1,52 @@ +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(), + null + ); + } + + /** + * 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..688a9901a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/player/PlayerPatch.java @@ -0,0 +1,730 @@ +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.graphics.Color; +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 + + 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(); + } + + // 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; + } + + 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(); + } + + // endregion + + // region [Seekbar components] patch + + public static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0000; + + 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 int getSeekbarClickedColorValue(final int colorValue) { + return colorValue == ORIGINAL_SEEKBAR_COLOR + ? overrideSeekbarColor(colorValue) + : colorValue; + } + + public static int resumedProgressBarColor(final int colorValue) { + return Settings.ENABLE_CUSTOM_SEEKBAR_COLOR.get() + ? getSeekbarClickedColorValue(colorValue) + : colorValue; + } + + /** + * Overrides all drawable color 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 getColor(int colorValue) { + if (colorValue == ORIGINAL_SEEKBAR_COLOR) { + if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) { + return 0x00000000; + } + return overrideSeekbarColor(ORIGINAL_SEEKBAR_COLOR); + } + return colorValue; + } + + /** + * Points where errors occur when playing videos on the PlayStore (ROOT Build) + */ + public static int overrideSeekbarColor(final int colorValue) { + try { + return Settings.ENABLE_CUSTOM_SEEKBAR_COLOR.get() + ? Color.parseColor(Settings.ENABLE_CUSTOM_SEEKBAR_COLOR_VALUE.get()) + : colorValue; + } catch (Exception ignored) { + Settings.ENABLE_CUSTOM_SEEKBAR_COLOR_VALUE.resetToDefault(); + } + return colorValue; + } + + 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/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/ShortsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/ShortsPatch.java new file mode 100644 index 000000000..1bf0f5bc5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/shorts/ShortsPatch.java @@ -0,0 +1,224 @@ +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 Enum repeat; + public static Enum singlePlay; + public static Enum endScreen; + + public static Enum changeShortsRepeatState(Enum currentState) { + switch (Settings.CHANGE_SHORTS_REPEAT_STATE.get()) { + case 1 -> currentState = repeat; + case 2 -> currentState = singlePlay; + case 3 -> currentState = endScreen; + } + + return currentState; + } + + 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 + ); + } + +} 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..24ee3f4a3 --- /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 enableSwipeToSwitchVideo() { + return Settings.ENABLE_SWIPE_TO_SWITCH_VIDEO.get(); + } + + /** + * Injection point. + */ + public static boolean enableWatchPanelGestures() { + return Settings.ENABLE_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..853779b37 --- /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 getColor(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..4dd5f0821 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/InitializationPatch.java @@ -0,0 +1,39 @@ +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.setApplicationLabel(); + ExtendedUtils.setSmallestScreenWidthDp(); + ExtendedUtils.setVersionName(); + 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/PlayerControlsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerControlsPatch.java new file mode 100644 index 000000000..0fb6115e6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerControlsPatch.java @@ -0,0 +1,122 @@ +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(); + } +} 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..ea9bd114c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/PlayerTypeHookPatch.java @@ -0,0 +1,52 @@ +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..3cb20d617 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/utils/ProgressBarDrawable.java @@ -0,0 +1,46 @@ +package app.revanced.extension.youtube.patches.utils; + +import static app.revanced.extension.youtube.patches.player.PlayerPatch.ORIGINAL_SEEKBAR_COLOR; +import static app.revanced.extension.youtube.patches.player.PlayerPatch.resumedProgressBarColor; + +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.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(resumedProgressBarColor(ORIGINAL_SEEKBAR_COLOR)); + 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..6964d3625 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/video/PlaybackSpeedPatch.java @@ -0,0 +1,139 @@ +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.misc.requests.PlaylistRequest; +import app.revanced.extension.youtube.patches.utils.PatchStatus; +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) { + 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); + } + } + + 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/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..8494aeb64 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -0,0 +1,654 @@ +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.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.YouTubeMusicActionsPatch; +import app.revanced.extension.youtube.patches.misc.SpoofStreamingDataPatch; +import app.revanced.extension.youtube.patches.misc.WatchHistoryPatch.WatchHistoryType; +import app.revanced.extension.youtube.patches.misc.client.AppClient.ClientType; +import app.revanced.extension.youtube.patches.shorts.AnimationFeedbackPatch.AnimationType; +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", TRUE); + + 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_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); + public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_enable_double_tap_action", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3)); + public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_enable_drag_and_drop", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1)); + public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true); + 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", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1)); + 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 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_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_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_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_SLEEP_TIMER = new BooleanSetting("revanced_hide_player_flyout_menu_sleep_timer", 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", FALSE); + 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", FALSE); + 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_DESCENDING); + 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", "#FF0000", 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 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", TRUE); + public static final IntegerSetting CHANGE_SHORTS_REPEAT_STATE = new IntegerSetting("revanced_change_shorts_repeat_state", 0); + + // 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_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); + + 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); + + + // 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); + + // 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 ENABLE_SWIPE_TO_SWITCH_VIDEO = new BooleanSetting("revanced_enable_swipe_to_switch_video", FALSE, true); + public static final BooleanSetting ENABLE_WATCH_PANEL_GESTURES = new BooleanSetting("revanced_enable_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", FALSE, 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 + // The order of the settings should not be changed otherwise the app may crash + 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 BooleanSetting SPOOF_STREAMING_DATA_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_streaming_data_ios_force_avc", FALSE, true, + "revanced_spoof_streaming_data_ios_force_avc_user_dialog_message", new SpoofStreamingDataPatch.iOSAvailability()); + public static final BooleanSetting SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK = new BooleanSetting("revanced_spoof_streaming_data_ios_skip_livestream_playback", TRUE, true, new SpoofStreamingDataPatch.iOSAvailability()); + public static final EnumSetting SPOOF_STREAMING_DATA_TYPE = new EnumSetting<>("revanced_spoof_streaming_data_type", ClientType.IOS, true, parent(SPOOF_STREAMING_DATA)); + public static final BooleanSetting SPOOF_STREAMING_DATA_STATS_FOR_NERDS = new BooleanSetting("revanced_spoof_streaming_data_stats_for_nerds", TRUE, parent(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.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_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 + } +} 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/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..77c1c6b75 --- /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(getEditText().getText().toString())); + } catch (Exception ex) { + Logger.printException(() -> "onPrepareDialogBuilder failure", ex); + } + } + + private void importSettings(String replacementSettings) { + try { + if (replacementSettings.equals(existingSettings)) { + return; + } + ReVancedPreferenceFragment.settingImportInProgress = true; + final boolean rebootNeeded = Setting.importFromJSON(replacementSettings, true); + 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..086243f9a --- /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.getApplicationLabel(); + final String versionName = ExtendedUtils.getVersionName(); + 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(sb.toString(), true); + 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..e902d2ab8 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedSettingsPreference.java @@ -0,0 +1,277 @@ +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(31), + Settings.ENABLE_TRANSLUCENT_NAVIGATION_BAR + ); + } + + /** + * 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..6a8a4a0b5 --- /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 + Utils.showToastLong(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/SpoofStreamingDataSideEffectsPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SpoofStreamingDataSideEffectsPreference.java new file mode 100644 index 000000000..3ada4f0ad --- /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.patches.misc.client.AppClient.ClientType; +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 ClientType clientType = Settings.SPOOF_STREAMING_DATA_TYPE.get(); + + final String summaryTextKey; + if (clientType == ClientType.IOS && Settings.SPOOF_STREAMING_DATA_IOS_SKIP_LIVESTREAM_PLAYBACK.get()) { + summaryTextKey = "revanced_spoof_streaming_data_side_effects_ios_skip_livestream_playback"; + } else { + summaryTextKey = "revanced_spoof_streaming_data_side_effects_" + clientType.name().toLowerCase(); + } + + setSummary(str(summaryTextKey)); + setEnabled(Settings.SPOOF_STREAMING_DATA.get()); + } +} \ 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..456a56143 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlaylistIdPrefix.java @@ -0,0 +1,32 @@ +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_DESCENDING("UU"), + ALL_CONTENTS_WITH_POPULAR_DESCENDING("PU"), + VIDEOS_ONLY_WITH_TIME_DESCENDING("UULF"), + VIDEOS_ONLY_WITH_POPULAR_DESCENDING("UULP"), + SHORTS_ONLY_WITH_TIME_DESCENDING("UUSH"), + SHORTS_ONLY_WITH_POPULAR_DESCENDING("UUPS"), + LIVESTREAMS_ONLY_WITH_TIME_DESCENDING("UULV"), + LIVESTREAMS_ONLY_WITH_POPULAR_DESCENDING("UUPV"), + ALL_MEMBERSHIPS_CONTENTS("UUMO"), + MEMBERSHIPS_VIDEOS_ONLY("UUMF"), + MEMBERSHIPS_SHORTS_ONLY("UUMS"), + MEMBERSHIPS_LIVESTREAMS_ONLY("UUMV"); + + /** + * Prefix of playlist id. + */ + @NonNull + public final String prefixId; + + PlaylistIdPrefix(@NonNull String prefixId) { + this.prefixId = prefixId; + } +} 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..0c0b87d81 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/VideoInformation.java @@ -0,0 +1,570 @@ +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) { + 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..c8d6e171b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java @@ -0,0 +1,246 @@ +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 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). + */ + public 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('.'); + 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(); + } + + /** + * Updates internal data based on {@link Setting} values. + */ + public static void updateFromImportedSettings() { + SegmentCategory.loadAllCategoriesFromSettings(); + SponsorBlockSettingsPreference.updateSegmentCategories(); + } +} 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..88d128e7e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java @@ -0,0 +1,495 @@ +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 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..1ff8a8d03 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java @@ -0,0 +1,282 @@ +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.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(); + + final String messageToToast = 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()); + }; + Utils.showToastLong(messageToToast); + } catch (SocketTimeoutException ex) { + // Always show, even if show connection toasts is turned off + Utils.showToastLong(str("revanced_sb_submit_failed_timeout")); + } catch (IOException ex) { + Utils.showToastLong(str("revanced_sb_submit_failed_unknown_error", 0, ex.getMessage())); + } catch (Exception ex) { + Logger.printException(() -> "failed to submit segments", ex); + } + } + + 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(); + + switch (responseCode) { + case HTTP_STATUS_CODE_SUCCESS: + Logger.printDebug(() -> "Vote success for segment: " + segment); + break; + case 403: + Utils.showToastLong( + str("revanced_sb_vote_failed_forbidden", Requester.parseErrorStringAndDisconnect(connection))); + break; + default: + Utils.showToastLong( + str("revanced_sb_vote_failed_unknown_error", responseCode, connection.getResponseMessage())); + break; + } + } 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..829c79e4c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/NewSegmentLayout.java @@ -0,0 +1,122 @@ +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.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.widget.FrameLayout; +import android.widget.ImageButton; + +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; + + 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" + ); + } + + /** + * 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..321e28de0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ExtendedUtils.java @@ -0,0 +1,111 @@ +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 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_SLEEP_TIMER, + 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_SLEEP_TIMER, + 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..aafdc17cb --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/ThemeUtils.java @@ -0,0 +1,119 @@ +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 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..485d64cd2 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/utils/VideoUtils.java @@ -0,0 +1,244 @@ +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.text.TextUtils; +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.IntegerSetting; +import app.revanced.extension.shared.utils.IntentUtils; +import app.revanced.extension.shared.utils.Logger; +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.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_FORMAT = "vnd.youtube://%s?start=%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); + } + + private 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() { + return getVideoScheme(VideoInformation.getVideoId()); + } + + private static String getVideoScheme(String videoId) { + return String.format(Locale.ENGLISH, VIDEO_SCHEME_FORMAT, videoId, VideoInformation.getVideoTimeInSeconds()); + } + + public static void copyUrl(boolean withTimestamp) { + setClipboard(getVideoUrl(withTimestamp), 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 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(getVideoScheme(videoId), ""); + } + + public static void openVideo(@NonNull PlaylistIdPrefix prefixId) { + openVideo(getVideoScheme(), prefixId.prefixId); + } + + /** + * Create playlist with all channel videos. + */ + public static void openVideo(@NonNull String videoScheme, @NonNull String prefixId) { + if (!TextUtils.isEmpty(prefixId)) { + 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; + } + videoScheme += "&list=" + prefixId + channelId.substring(2); + } + final String finalVideoScheme = videoScheme; + Logger.printInfo(() -> finalVideoScheme); + + launchView(videoScheme, 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 IntegerSetting 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; + + new AlertDialog.Builder(context) + .setTitle(str(settingsKey + "_title")) + .setSingleChoiceItems(mEntries, mClickedDialogEntryIndex, (dialog, id) -> { + mClickedDialogEntryIndex = id; + setting.save(id); + 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..06ad3a7d0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/whitelist/Whitelist.java @@ -0,0 +1,309 @@ +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); + } + if (playbackSpeedButton != null && playbackSpeedDrawable != null) { + playbackSpeedDrawable.setColorFilter(cf); + playbackSpeedButton.setCompoundDrawablesWithIntrinsicBounds(playbackSpeedDrawable, null, null, null); + } + } + + 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/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/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 fb02b8c60..d87a70eca 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,8 @@ -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 = 4.16.1 +kotlin.jvm.target.validation.mode = IGNORE +version = 5.0.1 + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a719a5552..169ef7199 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,13 +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" gson = "2.11.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" } 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] -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 7e0b101d5..c67622290 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,103 +8,45 @@ "@saithodev/semantic-release-backmerge": "^4.0.1", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", - "gradle-semantic-release-plugin": "^1.9.2", - "semantic-release": "^24.1.0" + "gradle-semantic-release-plugin": "^1.10.1", + "semantic-release": "^24.1.2" } }, "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "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/highlight": "^7.23.4", - "chalk": "^2.4.2" + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/code-frame/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, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/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, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/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, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/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 - }, - "node_modules/@babel/code-frame/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, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/code-frame/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, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "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.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "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.22.20", + "@babel/helper-validator-identifier": "^7.24.7", "chalk": "^2.4.2", - "js-tokens": "^4.0.0" + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, "engines": { "node": ">=6.9.0" @@ -115,6 +57,7 @@ "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" }, @@ -127,6 +70,7 @@ "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", @@ -141,6 +85,7 @@ "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" } @@ -149,13 +94,15 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "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" } @@ -165,6 +112,7 @@ "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" }, @@ -177,6 +125,7 @@ "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">=0.1.90" @@ -187,6 +136,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -200,6 +150,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } @@ -209,6 +160,7 @@ "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -218,149 +170,158 @@ } }, "node_modules/@octokit/auth-token": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", - "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", + "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 18" } }, "node_modules/@octokit/core": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz", - "integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", + "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/auth-token": "^4.0.0", - "@octokit/graphql": "^7.0.0", - "@octokit/request": "^8.0.2", - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", - "before-after-hook": "^2.2.0", - "universal-user-agent": "^6.0.0" + "@octokit/auth-token": "^5.0.0", + "@octokit/graphql": "^8.0.0", + "@octokit/request": "^9.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.0.0", + "before-after-hook": "^3.0.2", + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/endpoint": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz", - "integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", + "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/types": "^12.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.2" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/graphql": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", - "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", + "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/request": "^8.0.1", - "@octokit/types": "^12.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/request": "^9.0.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^7.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/openapi-types": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", - "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==", - "dev": true + "version": "22.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", + "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", + "dev": true, + "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "9.1.5", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz", - "integrity": "sha512-WKTQXxK+bu49qzwv4qKbMMRXej1DU2gq017euWyKVudA6MldaSSQuxtz+vGbhxV4CjxpUxjZu6rM2wfc1FiWVg==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.5.tgz", + "integrity": "sha512-cgwIRtKrpwhLoBi0CUNuY83DPGRMaWVjqVI/bGKsLJ4PzyWZNaEmhHroI2xlrVXkk6nFv0IsZpOp+ZWSWUS2AQ==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/types": "^12.4.0" + "@octokit/types": "^13.6.0" }, "engines": { "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=5" + "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-retry": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", - "integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-7.1.2.tgz", + "integrity": "sha512-XOWnPpH2kJ5VTwozsxGurw+svB2e61aWlmk5EVIYZPwFK5F9h4cyPyj9CIKRyMXMHSwpIsI3mPOdpMmrRhe7UQ==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", + "@octokit/request-error": "^6.0.0", + "@octokit/types": "^13.0.0", "bottleneck": "^2.15.3" }, "engines": { "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=5" + "@octokit/core": ">=6" } }, "node_modules/@octokit/plugin-throttling": { - "version": "8.1.3", - "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.1.3.tgz", - "integrity": "sha512-pfyqaqpc0EXh5Cn4HX9lWYsZ4gGbjnSmUILeu4u2gnuM50K/wIk9s1Pxt3lVeVwekmITgN/nJdoh43Ka+vye8A==", + "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": { - "@octokit/types": "^12.2.0", + "@octokit/types": "^13.0.0", "bottleneck": "^2.15.3" }, "engines": { "node": ">= 18" }, "peerDependencies": { - "@octokit/core": "^5.0.0" + "@octokit/core": "^6.0.0" } }, "node_modules/@octokit/request": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.2.0.tgz", - "integrity": "sha512-exPif6x5uwLqv1N1irkLG1zZNJkOtj8bZxuVHd71U5Ftuxf2wGNvAJyNBcPbPC+EBzwYEbBDdSFb8EPcjpYxPQ==", + "version": "9.1.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", + "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/endpoint": "^9.0.0", - "@octokit/request-error": "^5.0.0", - "@octokit/types": "^12.0.0", - "universal-user-agent": "^6.0.0" + "@octokit/endpoint": "^10.0.0", + "@octokit/request-error": "^6.0.1", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^7.0.2" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/request-error": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz", - "integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.5.tgz", + "integrity": "sha512-IlBTfGX8Yn/oFPMwSfvugfncK2EwRLjzbrpifNaMY8o/HTEAFqCA1FZxjD9cWvSKBHgrIhc4CSBIzMxiLsbzFQ==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/types": "^12.0.0", - "deprecation": "^2.0.0", - "once": "^1.4.0" + "@octokit/types": "^13.0.0" }, "engines": { "node": ">= 18" } }, "node_modules/@octokit/types": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz", - "integrity": "sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==", + "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": { - "@octokit/openapi-types": "^19.1.0" + "@octokit/openapi-types": "^22.2.0" } }, "node_modules/@pnpm/config.env-replace": { @@ -368,6 +329,7 @@ "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.22.0" } @@ -377,6 +339,7 @@ "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "4.2.10" }, @@ -388,13 +351,15 @@ "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/@pnpm/npm-conf": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz", - "integrity": "sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", + "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", "dev": true, + "license": "MIT", "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", @@ -409,6 +374,7 @@ "resolved": "https://registry.npmjs.org/@saithodev/semantic-release-backmerge/-/semantic-release-backmerge-4.0.1.tgz", "integrity": "sha512-WDsU28YrXSLx0xny7FgFlEk8DCKGcj6OOhA+4Q9k3te1jJD1GZuqY8sbIkVQaw9cqJ7CT+fCZUN6QDad8JW4Dg==", "dev": true, + "license": "MIT", "dependencies": { "@semantic-release/error": "^3.0.0", "aggregate-error": "^3.1.0", @@ -418,11 +384,419 @@ "semantic-release": "^22.0.7" } }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@octokit/auth-token": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@octokit/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.0.tgz", + "integrity": "sha512-1LFfa/qnMQvEOAdzlQymH0ulepxbxnCYAKJZfMci/5XJyIHWgEYnDmgnKakbTh7CH2tFQ5O60oYDvns4i9RAIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.1.0", + "@octokit/request": "^8.3.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@octokit/endpoint": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.5.tgz", + "integrity": "sha512-ekqR4/+PCLkEBF6qgj8WqJfvDq65RH85OAgrtnVp1mSxaXF03u2xW/hUdweGS5654IlC0wkNYC18Z50tSYTAFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@octokit/graphql": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.1.0.tgz", + "integrity": "sha512-r+oZUH7aMFui1ypZnAvZmn0KSqAUgE1/tUXIWaqUCa1758ts/Jio84GZuzsvUkme98kv0WFY8//n0J1Z+vsIsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^8.3.0", + "@octokit/types": "^13.0.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@octokit/openapi-types": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-20.0.0.tgz", + "integrity": "sha512-EtqRBEjp1dL/15V7WiX5LJMIxxkdiGJnabzYx5Apx4FkQIFgAfKumXeYAqqJCj1s+BMX4cPFIFC4OLCR6stlnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@octokit/plugin-paginate-rest": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.2.1.tgz", + "integrity": "sha512-wfGhE/TAkXZRLjksFXuDZdmGnJQHvtU/joFQdweXUgzo1XwvBCD4o4+75NtFfjfLK5IwLf9vHTfSiU3sLRYpRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.6.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "5" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@octokit/plugin-retry": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-6.0.1.tgz", + "integrity": "sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=5" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@octokit/plugin-throttling": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-8.2.0.tgz", + "integrity": "sha512-nOpWtLayKFpgqmgD0y3GqXafMFuKcA4tRPZIfu7BArd2lEZeb1988nhWhwx4aZWmjDmUfdgVf7W+Tt4AmvRmMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^12.2.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": "^5.0.0" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@octokit/plugin-throttling/node_modules/@octokit/types": { + "version": "12.6.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.6.0.tgz", + "integrity": "sha512-1rhSOfRa6H9w4YwK0yrf5faDaDTb+yLyBUKOCV4xtCDB5VmIPqd/v9yr9o6SAzOAlRxMiRiCic6JVM1/kunVkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^20.0.0" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@octokit/request": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.4.0.tgz", + "integrity": "sha512-9Bb014e+m2TgBeEJGEbdplMVWwPmL1FPtggHQRkV+WVsMggPtEkLKPlcVYm/o8xKLkpJ7B+6N8WfQMtDLX2Dpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^9.0.1", + "@octokit/request-error": "^5.1.0", + "@octokit/types": "^13.1.0", + "universal-user-agent": "^6.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@octokit/request-error": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.1.0.tgz", + "integrity": "sha512-GETXfE05J0+7H2STzekpKObFe765O5dlAKUTLNGeH+x47z7JjXHfsHKo5z21D/o/IOZTUEI6nyWyR+bZVP/n5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^13.1.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@semantic-release/commit-analyzer": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-11.1.0.tgz", + "integrity": "sha512-cXNTbv3nXR2hlzHjAMgbuiQVtvWHTlwwISt60B+4NZv01y/QRY7p2HcJm8Eh2StzcTJoNnflvKjHH/cjFS7d5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-changelog-angular": "^7.0.0", + "conventional-commits-filter": "^4.0.0", + "conventional-commits-parser": "^5.0.0", + "debug": "^4.0.0", + "import-from-esm": "^1.0.3", + "lodash-es": "^4.17.21", + "micromatch": "^4.0.2" + }, + "engines": { + "node": "^18.17 || >=20.6.1" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@semantic-release/github": { + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-9.2.6.tgz", + "integrity": "sha512-shi+Lrf6exeNZF+sBhK+P011LSbhmIAoUEgEY6SsxF8irJ+J2stwI5jkyDQ+4gzYyDImzV6LCKdYB9FXnQRWKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^5.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-retry": "^6.0.0", + "@octokit/plugin-throttling": "^8.0.0", + "@semantic-release/error": "^4.0.0", + "aggregate-error": "^5.0.0", + "debug": "^4.3.4", + "dir-glob": "^3.0.1", + "globby": "^14.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "issue-parser": "^6.0.0", + "lodash-es": "^4.17.21", + "mime": "^4.0.0", + "p-filter": "^4.0.0", + "url-join": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@semantic-release/github/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@semantic-release/github/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@semantic-release/npm": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-11.0.3.tgz", + "integrity": "sha512-KUsozQGhRBAnoVg4UMZj9ep436VEGwT536/jwSqB7vcEfA6oncCUU7UIYTRdLx7GvTtqn0kBjnkfLVkcnBa2YQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@semantic-release/error": "^4.0.0", + "aggregate-error": "^5.0.0", + "execa": "^8.0.0", + "fs-extra": "^11.0.0", + "lodash-es": "^4.17.21", + "nerf-dart": "^1.0.0", + "normalize-url": "^8.0.0", + "npm": "^10.5.0", + "rc": "^1.2.8", + "read-pkg": "^9.0.0", + "registry-auth-token": "^5.0.0", + "semver": "^7.1.2", + "tempy": "^3.0.0" + }, + "engines": { + "node": "^18.17 || >=20" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@semantic-release/npm/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@semantic-release/npm/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@semantic-release/npm/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@semantic-release/npm/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@semantic-release/release-notes-generator": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-12.1.0.tgz", + "integrity": "sha512-g6M9AjUKAZUZnxaJZnouNBeDNTCUrJ5Ltj+VJ60gJeDaRRahcHsry9HW8yKrnKkKNkx5lbWiEP1FPMqVNQz8Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-changelog-angular": "^7.0.0", + "conventional-changelog-writer": "^7.0.0", + "conventional-commits-filter": "^4.0.0", + "conventional-commits-parser": "^5.0.0", + "debug": "^4.0.0", + "get-stream": "^7.0.0", + "import-from-esm": "^1.0.3", + "into-stream": "^7.0.0", + "lodash-es": "^4.17.21", + "read-pkg-up": "^11.0.0" + }, + "engines": { + "node": "^18.17 || >=20.6.1" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/@semantic-release/release-notes-generator/node_modules/get-stream": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-7.0.1.tgz", + "integrity": "sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/ansi-escapes": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.1.tgz", + "integrity": "sha512-4nJ3yixlEthEJ9Rk4vPcdBRkZvQZlYyu8j4/Mqz5sgIkddmEnH2Yj2ZrnP9S3tQOvSNRUIgVNF/1yPpRAGNRig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@saithodev/semantic-release-backmerge/node_modules/clean-stack": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "5.0.0" }, @@ -433,11 +807,75 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/conventional-changelog-writer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-7.0.1.tgz", + "integrity": "sha512-Uo+R9neH3r/foIvQ0MKcsXkX642hdm9odUp7TqgFS7BsalTcjzRlIfWZrZR1gbxOozKucaKt5KAbjW8J8xRSmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-commits-filter": "^4.0.0", + "handlebars": "^4.7.7", + "json-stringify-safe": "^5.0.1", + "meow": "^12.0.1", + "semver": "^7.5.2", + "split2": "^4.0.0" + }, + "bin": { + "conventional-changelog-writer": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/conventional-commits-filter": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-4.0.0.tgz", + "integrity": "sha512-rnpnibcSOdFcdclpFwWa+pPlZJhXE7l+XK04zxhbWrhgpR96h33QLz8hITTXbcYICxVr3HZFtbtUAQ+4LdBo9A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@saithodev/semantic-release-backmerge/node_modules/cosmiconfig": { "version": "8.3.6", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, + "license": "MIT", "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", @@ -464,6 +902,7 @@ "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-10.0.0.tgz", "integrity": "sha512-U4xcd/utDYFgMh0yWj07R1H6L5fwhVbmxBCpnL0DbVSDZVnsC82HONw0wxtxNkIAcua3KtbomQvIk5xFZGAQJw==", "dev": true, + "license": "MIT", "dependencies": { "execa": "^8.0.0", "java-properties": "^1.0.2" @@ -477,6 +916,7 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", @@ -500,6 +940,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" }, @@ -512,6 +953,7 @@ "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" }, @@ -519,11 +961,41 @@ "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", + "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-regex": "^4.0.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, "node_modules/@saithodev/semantic-release-backmerge/node_modules/human-signals": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=16.17.0" } @@ -533,6 +1005,7 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -545,6 +1018,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -552,11 +1026,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/issue-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-6.0.0.tgz", + "integrity": "sha512-zKa/Dxq2lGsBIXQ7CUZWTHfvxPC2ej0KfO7fIPqLlHB9J2hJ7rGhZ5rilhuufylr4RXYPzJUeFjKxz305OsNlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.capitalize": "^4.2.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.uniqby": "^4.7.0" + }, + "engines": { + "node": ">=10.13" + } + }, "node_modules/@saithodev/semantic-release-backmerge/node_modules/marked": { "version": "9.1.6", "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", "dev": true, + "license": "MIT", "bin": { "marked": "bin/marked.js" }, @@ -569,6 +1061,7 @@ "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-6.2.0.tgz", "integrity": "sha512-ubWhwcBFHnXsjYNsu+Wndpg0zhY4CahSpPlA70PlO0rR9r2sZpkyU+rkCsOWH+KMEkx847UpALON+HWgxowFtw==", "dev": true, + "license": "MIT", "dependencies": { "ansi-escapes": "^6.2.0", "cardinal": "^2.1.1", @@ -584,11 +1077,25 @@ "marked": ">=1 <12" } }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@saithodev/semantic-release-backmerge/node_modules/mimic-fn": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -597,10 +1104,11 @@ } }, "node_modules/@saithodev/semantic-release-backmerge/node_modules/npm-run-path": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^4.0.0" }, @@ -616,6 +1124,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^4.0.0" }, @@ -631,6 +1140,7 @@ "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-3.0.0.tgz", "integrity": "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -643,6 +1153,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -655,6 +1166,7 @@ "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-22.0.12.tgz", "integrity": "sha512-0mhiCR/4sZb00RVFJIUlMuiBkW3NMpVIW2Gse7noqEMoFGkvfPPAImEQbkBV8xga4KOPP4FdTRYuLLy32R1fPw==", "dev": true, + "license": "MIT", "dependencies": { "@semantic-release/commit-analyzer": "^11.0.0", "@semantic-release/error": "^4.0.0", @@ -698,6 +1210,7 @@ "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -707,6 +1220,7 @@ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", "dev": true, + "license": "MIT", "dependencies": { "clean-stack": "^5.2.0", "indent-string": "^5.0.0" @@ -723,6 +1237,7 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", @@ -746,6 +1261,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" }, @@ -758,6 +1274,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -770,6 +1287,7 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -777,17 +1295,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@saithodev/semantic-release-backmerge/node_modules/universal-user-agent": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", + "dev": true, + "license": "ISC" + }, "node_modules/@sec-ant/readable-stream": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@semantic-release/changelog": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/@semantic-release/changelog/-/changelog-6.0.3.tgz", "integrity": "sha512-dZuR5qByyfe3Y03TpmCvAxCyTnp7r5XwtHRf/8vD9EAn4ZWbavUX8adMtXYzE86EVh0gyLA7lm5yW4IV30XUag==", "dev": true, + "license": "MIT", "dependencies": { "@semantic-release/error": "^3.0.0", "aggregate-error": "^3.0.0", @@ -802,21 +1329,23 @@ } }, "node_modules/@semantic-release/commit-analyzer": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-11.1.0.tgz", - "integrity": "sha512-cXNTbv3nXR2hlzHjAMgbuiQVtvWHTlwwISt60B+4NZv01y/QRY7p2HcJm8Eh2StzcTJoNnflvKjHH/cjFS7d5g==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-13.0.0.tgz", + "integrity": "sha512-KtXWczvTAB1ZFZ6B4O+w8HkfYm/OgQb1dUGNFZtDgQ0csggrmkq8sTxhd+lwGF8kMb59/RnG9o4Tn7M/I8dQ9Q==", "dev": true, + "license": "MIT", "dependencies": { - "conventional-changelog-angular": "^7.0.0", - "conventional-commits-filter": "^4.0.0", - "conventional-commits-parser": "^5.0.0", + "conventional-changelog-angular": "^8.0.0", + "conventional-changelog-writer": "^8.0.0", + "conventional-commits-filter": "^5.0.0", + "conventional-commits-parser": "^6.0.0", "debug": "^4.0.0", "import-from-esm": "^1.0.3", "lodash-es": "^4.17.21", "micromatch": "^4.0.2" }, "engines": { - "node": "^18.17 || >=20.6.1" + "node": ">=20.8.1" }, "peerDependencies": { "semantic-release": ">=20.1.0" @@ -827,6 +1356,7 @@ "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-3.0.0.tgz", "integrity": "sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.17" } @@ -836,6 +1366,7 @@ "resolved": "https://registry.npmjs.org/@semantic-release/git/-/git-10.0.1.tgz", "integrity": "sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w==", "dev": true, + "license": "MIT", "dependencies": { "@semantic-release/error": "^3.0.0", "aggregate-error": "^3.0.0", @@ -854,15 +1385,16 @@ } }, "node_modules/@semantic-release/github": { - "version": "9.2.6", - "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-9.2.6.tgz", - "integrity": "sha512-shi+Lrf6exeNZF+sBhK+P011LSbhmIAoUEgEY6SsxF8irJ+J2stwI5jkyDQ+4gzYyDImzV6LCKdYB9FXnQRWKA==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-11.0.0.tgz", + "integrity": "sha512-Uon6G6gJD8U1JNvPm7X0j46yxNRJ8Ui6SgK4Zw5Ktu8RgjEft3BGn+l/RX1TTzhhO3/uUcKuqM+/9/ETFxWS/Q==", "dev": true, + "license": "MIT", "dependencies": { - "@octokit/core": "^5.0.0", - "@octokit/plugin-paginate-rest": "^9.0.0", - "@octokit/plugin-retry": "^6.0.0", - "@octokit/plugin-throttling": "^8.0.0", + "@octokit/core": "^6.0.0", + "@octokit/plugin-paginate-rest": "^11.0.0", + "@octokit/plugin-retry": "^7.0.0", + "@octokit/plugin-throttling": "^9.0.0", "@semantic-release/error": "^4.0.0", "aggregate-error": "^5.0.0", "debug": "^4.3.4", @@ -870,17 +1402,17 @@ "globby": "^14.0.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", - "issue-parser": "^6.0.0", + "issue-parser": "^7.0.0", "lodash-es": "^4.17.21", "mime": "^4.0.0", "p-filter": "^4.0.0", "url-join": "^5.0.0" }, "engines": { - "node": ">=18" + "node": ">=20.8.1" }, "peerDependencies": { - "semantic-release": ">=20.1.0" + "semantic-release": ">=24.1.0" } }, "node_modules/@semantic-release/github/node_modules/@semantic-release/error": { @@ -888,6 +1420,7 @@ "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -897,6 +1430,7 @@ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", "dev": true, + "license": "MIT", "dependencies": { "clean-stack": "^5.2.0", "indent-string": "^5.0.0" @@ -913,6 +1447,7 @@ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "5.0.0" }, @@ -928,6 +1463,7 @@ "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" }, @@ -940,6 +1476,7 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -948,19 +1485,20 @@ } }, "node_modules/@semantic-release/npm": { - "version": "11.0.2", - "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-11.0.2.tgz", - "integrity": "sha512-owtf3RjyPvRE63iUKZ5/xO4uqjRpVQDUB9+nnXj0xwfIeM9pRl+cG+zGDzdftR4m3f2s4Wyf3SexW+kF5DFtWA==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-12.0.1.tgz", + "integrity": "sha512-/6nntGSUGK2aTOI0rHPwY3ZjgY9FkXmEHbW9Kr+62NVOsyqpKKeP0lrCH+tphv+EsNdJNmqqwijTEnVWUMQ2Nw==", "dev": true, + "license": "MIT", "dependencies": { "@semantic-release/error": "^4.0.0", "aggregate-error": "^5.0.0", - "execa": "^8.0.0", + "execa": "^9.0.0", "fs-extra": "^11.0.0", "lodash-es": "^4.17.21", "nerf-dart": "^1.0.0", "normalize-url": "^8.0.0", - "npm": "^10.0.0", + "npm": "^10.5.0", "rc": "^1.2.8", "read-pkg": "^9.0.0", "registry-auth-token": "^5.0.0", @@ -968,7 +1506,7 @@ "tempy": "^3.0.0" }, "engines": { - "node": "^18.17 || >=20" + "node": ">=20.8.1" }, "peerDependencies": { "semantic-release": ">=20.1.0" @@ -979,15 +1517,30 @@ "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } }, + "node_modules/@semantic-release/npm/node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@semantic-release/npm/node_modules/aggregate-error": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", "dev": true, + "license": "MIT", "dependencies": { "clean-stack": "^5.2.0", "indent-string": "^5.0.0" @@ -1004,6 +1557,7 @@ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "5.0.0" }, @@ -1019,6 +1573,7 @@ "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" }, @@ -1027,47 +1582,57 @@ } }, "node_modules/@semantic-release/npm/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "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": { + "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.0", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.0.0", "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.0.0" }, "engines": { - "node": ">=16.17" + "node": "^18.19.0 || >=20.5.0" }, "funding": { "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, "node_modules/@semantic-release/npm/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, "engines": { - "node": ">=16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@semantic-release/npm/node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", + "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", "dev": true, + "license": "Apache-2.0", "engines": { - "node": ">=16.17.0" + "node": ">=18.18.0" } }, "node_modules/@semantic-release/npm/node_modules/indent-string": { @@ -1075,6 +1640,7 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1083,54 +1649,30 @@ } }, "node_modules/@semantic-release/npm/node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@semantic-release/npm/node_modules/npm-run-path": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", - "dev": true, - "dependencies": { - "path-key": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@semantic-release/npm/node_modules/onetime": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", "dev": true, + "license": "MIT", "dependencies": { - "mimic-fn": "^4.0.0" + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1141,6 +1683,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1153,6 +1696,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -1161,36 +1705,51 @@ } }, "node_modules/@semantic-release/npm/node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@semantic-release/release-notes-generator": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-12.1.0.tgz", - "integrity": "sha512-g6M9AjUKAZUZnxaJZnouNBeDNTCUrJ5Ltj+VJ60gJeDaRRahcHsry9HW8yKrnKkKNkx5lbWiEP1FPMqVNQz8Kg==", + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-14.0.1.tgz", + "integrity": "sha512-K0w+5220TM4HZTthE5dDpIuFrnkN1NfTGPidJFm04ULT1DEZ9WG89VNXN7F0c+6nMEpWgqmPvb7vY7JkB2jyyA==", "dev": true, + "license": "MIT", "dependencies": { - "conventional-changelog-angular": "^7.0.0", - "conventional-changelog-writer": "^7.0.0", - "conventional-commits-filter": "^4.0.0", - "conventional-commits-parser": "^5.0.0", + "conventional-changelog-angular": "^8.0.0", + "conventional-changelog-writer": "^8.0.0", + "conventional-commits-filter": "^5.0.0", + "conventional-commits-parser": "^6.0.0", "debug": "^4.0.0", "get-stream": "^7.0.0", "import-from-esm": "^1.0.3", "into-stream": "^7.0.0", "lodash-es": "^4.17.21", - "read-pkg-up": "^11.0.0" + "read-package-up": "^11.0.0" }, "engines": { - "node": "^18.17 || >=20.6.1" + "node": ">=20.8.1" }, "peerDependencies": { "semantic-release": ">=20.1.0" @@ -1201,6 +1760,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-7.0.1.tgz", "integrity": "sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" }, @@ -1213,6 +1773,7 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -1221,10 +1782,11 @@ } }, "node_modules/@sindresorhus/merge-streams": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.2.0.tgz", - "integrity": "sha512-UTce8mUwUW0RikMb/eseJ7ys0BRkZVFB86orHzrfW12ZmFtym5zua8joZ4L7okH2dDFHkcFjqnZ5GocWBXOFtA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -1236,19 +1798,22 @@ "version": "2.4.4", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.3.4" }, @@ -1261,6 +1826,7 @@ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", "dev": true, + "license": "MIT", "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -1270,15 +1836,16 @@ } }, "node_modules/ansi-escapes": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz", - "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", "dev": true, + "license": "MIT", "dependencies": { - "type-fest": "^3.0.0" + "environment": "^1.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1289,6 +1856,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -1298,6 +1866,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1312,51 +1881,59 @@ "version": "0.3.2", "resolved": "https://registry.npmjs.org/ansicolors/-/ansicolors-0.3.2.tgz", "integrity": "sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/any-promise": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "dev": true, + "license": "Python-2.0" }, "node_modules/argv-formatter": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/argv-formatter/-/argv-formatter-1.0.0.tgz", "integrity": "sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/before-after-hook": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", - "dev": true + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", + "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/bottleneck": { "version": "2.19.5", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -1367,6 +1944,7 @@ "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1376,6 +1954,7 @@ "resolved": "https://registry.npmjs.org/cardinal/-/cardinal-2.1.1.tgz", "integrity": "sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==", "dev": true, + "license": "MIT", "dependencies": { "ansicolors": "~0.3.2", "redeyed": "~2.1.0" @@ -1389,6 +1968,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -1401,6 +1981,7 @@ "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -1410,6 +1991,7 @@ "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1419,6 +2001,7 @@ "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", "dev": true, + "license": "ISC", "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", @@ -1440,6 +2023,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -1456,6 +2040,7 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -1467,6 +2052,7 @@ "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -1485,15 +2071,17 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/cli-table3": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.3.tgz", - "integrity": "sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dev": true, + "license": "MIT", "dependencies": { "string-width": "^4.2.0" }, @@ -1509,6 +2097,7 @@ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -1523,6 +2112,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -1534,13 +2124,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/compare-func": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", "dev": true, + "license": "MIT", "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" @@ -1551,68 +2143,69 @@ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", "dev": true, + "license": "MIT", "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "node_modules/conventional-changelog-angular": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", - "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.0.0.tgz", + "integrity": "sha512-CLf+zr6St0wIxos4bmaKHRXWAcsCXrJU6F4VdNDrGRK3B8LDLKoX3zuMV5GhtbGkVR/LohZ6MT6im43vZLSjmA==", "dev": true, + "license": "ISC", "dependencies": { "compare-func": "^2.0.0" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/conventional-changelog-writer": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-7.0.1.tgz", - "integrity": "sha512-Uo+R9neH3r/foIvQ0MKcsXkX642hdm9odUp7TqgFS7BsalTcjzRlIfWZrZR1gbxOozKucaKt5KAbjW8J8xRSmA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-8.0.0.tgz", + "integrity": "sha512-TQcoYGRatlAnT2qEWDON/XSfnVG38JzA7E0wcGScu7RElQBkg9WWgZd1peCWFcWDh1xfb2CfsrcvOn1bbSzztA==", "dev": true, + "license": "MIT", "dependencies": { - "conventional-commits-filter": "^4.0.0", + "@types/semver": "^7.5.5", + "conventional-commits-filter": "^5.0.0", "handlebars": "^4.7.7", - "json-stringify-safe": "^5.0.1", - "meow": "^12.0.1", - "semver": "^7.5.2", - "split2": "^4.0.0" + "meow": "^13.0.0", + "semver": "^7.5.2" }, "bin": { - "conventional-changelog-writer": "cli.mjs" + "conventional-changelog-writer": "dist/cli/index.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/conventional-commits-filter": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-4.0.0.tgz", - "integrity": "sha512-rnpnibcSOdFcdclpFwWa+pPlZJhXE7l+XK04zxhbWrhgpR96h33QLz8hITTXbcYICxVr3HZFtbtUAQ+4LdBo9A==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-5.0.0.tgz", + "integrity": "sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==", "dev": true, + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/conventional-commits-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", - "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.0.0.tgz", + "integrity": "sha512-TbsINLp48XeMXR8EvGjTnKGsZqBemisPoyWESlpRyR8lif0lcwzqz+NMtYSj1ooF/WYjSuu7wX0CtdeeMEQAmA==", "dev": true, + "license": "MIT", "dependencies": { - "is-text-path": "^2.0.0", - "JSONStream": "^1.3.5", - "meow": "^12.0.1", - "split2": "^4.0.0" + "meow": "^13.0.0" }, "bin": { - "conventional-commits-parser": "cli.mjs" + "conventional-commits-parser": "dist/cli/index.js" }, "engines": { - "node": ">=16" + "node": ">=18" } }, "node_modules/convert-hrtime": { @@ -1620,6 +2213,7 @@ "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1631,13 +2225,15 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cosmiconfig": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, + "license": "MIT", "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -1664,6 +2260,7 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -1678,6 +2275,7 @@ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^1.0.1" }, @@ -1693,6 +2291,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -1701,12 +2300,13 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", "dev": true, + "license": "MIT", "dependencies": { - "ms": "2.1.2" + "ms": "^2.1.3" }, "engines": { "node": ">=6.0" @@ -1722,6 +2322,7 @@ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4.0.0" } @@ -1730,13 +2331,15 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, + "license": "MIT", "dependencies": { "path-type": "^4.0.0" }, @@ -1749,6 +2352,7 @@ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", "dev": true, + "license": "MIT", "dependencies": { "is-obj": "^2.0.0" }, @@ -1761,6 +2365,7 @@ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "readable-stream": "^2.0.2" } @@ -1769,19 +2374,22 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/emojilib": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/env-ci": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.0.0.tgz", - "integrity": "sha512-apikxMgkipkgTvMdRT9MNqWx5VLOci79F4VBd7Op/7OPjjoanjdAvn6fglMCCEf/1bAh8eOiuEVCUs4V3qP3nQ==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.1.0.tgz", + "integrity": "sha512-Z8dnwSDbV1XYM9SBF2J0GcNVvmfmfh3a49qddGIROhBoVro6MZVTji15z/sJbQ2ko2ei8n988EU1wzoLU/tF+g==", "dev": true, + "license": "MIT", "dependencies": { "execa": "^8.0.0", "java-properties": "^1.0.2" @@ -1795,6 +2403,7 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", @@ -1818,6 +2427,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" }, @@ -1830,6 +2440,7 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=16.17.0" } @@ -1839,6 +2450,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -1851,6 +2463,7 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1859,10 +2472,11 @@ } }, "node_modules/env-ci/node_modules/npm-run-path": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^4.0.0" }, @@ -1878,6 +2492,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^4.0.0" }, @@ -1893,6 +2508,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1905,6 +2521,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -1917,6 +2534,7 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1929,24 +2547,40 @@ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/escalade": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", - "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -1956,6 +2590,7 @@ "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" } @@ -1965,6 +2600,7 @@ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", "dev": true, + "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -1978,6 +2614,7 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -2001,6 +2638,7 @@ "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -2017,6 +2655,7 @@ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -2026,6 +2665,7 @@ "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", "dev": true, + "license": "MIT", "dependencies": { "is-unicode-supported": "^2.0.0" }, @@ -2037,10 +2677,11 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2053,6 +2694,7 @@ "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", "dev": true, + "license": "MIT", "dependencies": { "locate-path": "^2.0.0" }, @@ -2065,6 +2707,7 @@ "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.0.tgz", "integrity": "sha512-q7Us7kcjj2VMePAa02hDAF6d+MzsdsAWEwYyOpwUtlerRBkOEPBCRZrAV4XfcSN8fHAgaD0hP7miwoay6DCprw==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -2073,15 +2716,17 @@ } }, "node_modules/find-versions": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-5.1.0.tgz", - "integrity": "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-6.0.0.tgz", + "integrity": "sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==", "dev": true, + "license": "MIT", "dependencies": { - "semver-regex": "^4.0.5" + "semver-regex": "^4.0.5", + "super-regex": "^1.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2092,6 +2737,7 @@ "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.1", "readable-stream": "^2.0.0" @@ -2102,6 +2748,7 @@ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -2111,20 +2758,12 @@ "node": ">=14.14" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/function-timeout": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-1.0.2.tgz", "integrity": "sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -2137,6 +2776,7 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -2146,6 +2786,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2154,17 +2795,18 @@ } }, "node_modules/git-log-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.0.tgz", - "integrity": "sha512-rnCVNfkTL8tdNryFuaY0fYiBWEBcgF748O6ZI61rslBvr2o7U65c2/6npCRqH40vuAhtgtDiqLTJjBVdrejCzA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.1.tgz", + "integrity": "sha512-PI+sPDvHXNPl5WNOErAK05s3j0lgwUzMN6o8cyQrDaKfT3qd7TmNJKeXX+SknI5I0QhG5fVPAEwSY4tRGDtYoQ==", "dev": true, + "license": "MIT", "dependencies": { "argv-formatter": "~1.0.0", "spawn-error-forwarder": "~1.0.0", "split2": "~1.0.0", "stream-combiner2": "~1.1.1", "through2": "~2.0.0", - "traverse": "~0.6.6" + "traverse": "0.6.8" } }, "node_modules/git-log-parser/node_modules/split2": { @@ -2172,6 +2814,7 @@ "resolved": "https://registry.npmjs.org/split2/-/split2-1.0.0.tgz", "integrity": "sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg==", "dev": true, + "license": "ISC", "dependencies": { "through2": "~2.0.0" } @@ -2181,6 +2824,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -2189,10 +2833,11 @@ } }, "node_modules/globby": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", - "integrity": "sha512-jOMLD2Z7MAhyG8aJpNOpmziMOP4rPLcc95oQPKXBazW82z+CEgPFBQvEpRUa1KeIMUJo4Wsm+q6uzO/Q/4BksQ==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", "dev": true, + "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", "fast-glob": "^3.3.2", @@ -2213,6 +2858,7 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -2224,12 +2870,13 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/gradle-semantic-release-plugin": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/gradle-semantic-release-plugin/-/gradle-semantic-release-plugin-1.9.2.tgz", - "integrity": "sha512-8qpf4GYFPQ+UMUymYBy/VchOOwLILAWzZMrZX1R0RR3JMgJBMN2R0tJn92R/3rXmxx4OAqwUFH6Np51eFoxr3w==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/gradle-semantic-release-plugin/-/gradle-semantic-release-plugin-1.10.1.tgz", + "integrity": "sha512-Q4dLAFICjPouUyRRHEKK8cXNB75nraXoioYZDZlVQOg4sYKudnTDZ3ohLmV3k4cPGiiMCh1ckXETkx9JnuyKmA==", "dev": true, "funding": [ { @@ -2237,6 +2884,7 @@ "url": "https://github.com/sponsors/KengoTODA" } ], + "license": "MIT", "dependencies": { "promisified-properties": "^3.0.0", "split2": "^4.1.0" @@ -2253,6 +2901,7 @@ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", @@ -2274,27 +2923,17 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/highlight.js": { "version": "10.7.3", "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": "*" } @@ -2304,6 +2943,7 @@ "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-3.0.0.tgz", "integrity": "sha512-jHRQzjSDzMtFy34AGj1DN+vq54WVuhSvKgrHf0OMiFQTwDD4L/qqofVEWjLOBMTn5+lCD3fPg32W9yOfnEJTTw==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -2312,22 +2952,24 @@ } }, "node_modules/hosted-git-info": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.1.tgz", - "integrity": "sha512-+K84LB1DYwMHoHSgaOY/Jfhw3ucPmSET5v98Ke/HdNSw4a0UktWzyW1mjhjpuxxTqOOsfWT/7iVshHmVZ4IpOA==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.0.tgz", + "integrity": "sha512-4nw3vOVR+vHUOT8+U4giwe2tcGv+R3pwwRidUe67DoMBTjhrfr6rZYJVVwdkBE+Um050SG+X9tf0Jo4fOpn01w==", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^10.0.1" }, "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "node_modules/http-proxy-agent": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.1.tgz", - "integrity": "sha512-My1KCEPs6A0hb4qCVzYp8iEvA8j8YqcvXLZZH8C9OFuTYpYjHE7N2dtG3mRl1HMD4+VGXpF3XcDVcxGBT7yDZQ==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -2337,10 +2979,11 @@ } }, "node_modules/https-proxy-agent": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.3.tgz", - "integrity": "sha512-kCnwztfX0KZJSLOBrcL0emLeFako55NWMovvyPP2AjsghNk9RB1yjSI+jVumPHYZsNXegNoqupSW9IY3afSH8w==", + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.0.2", "debug": "4" @@ -2354,15 +2997,17 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } }, "node_modules/ignore": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } @@ -2372,6 +3017,7 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -2388,15 +3034,17 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/import-from-esm": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.3.3.tgz", - "integrity": "sha512-U3Qt/CyfFpTUv6LOP2jRTLYjphH6zg3okMfHbyqRa/W2w6hr8OsJWVggNlR4jxuojQy81TgTJTxgSkyoteRGMQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-1.3.4.tgz", + "integrity": "sha512-7EyUlPFC0HOlBDpUFGfYstsU7XHxZJKAAMzCT8wZ0hMW7b+hG51LIKTDcsgtz8Pu6YC0HqRVbX+rVUtsGMUKvg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.3.4", "import-meta-resolve": "^4.0.0" @@ -2406,10 +3054,11 @@ } }, "node_modules/import-meta-resolve": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.0.0.tgz", - "integrity": "sha512-okYUR7ZQPH+efeuMJGlq4f8ubUgO50kByRPyt/Cy1Io4PSRsPjxME+YlVaCOx+NIToW7hCsZNFJyTPFFKepRSA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.1.0.tgz", + "integrity": "sha512-I6fiaX09Xivtk+THaMfAwnA3MVA5Big1WHF1Dfx9hFuvNIWpXnorlkzhcQf6ehrqQiiZECRt1poOAkPmer3ruw==", "dev": true, + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" @@ -2420,6 +3069,7 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2429,6 +3079,7 @@ "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-0.1.2.tgz", "integrity": "sha512-MWDKS3AS1bGCHLBA2VLImJz42f7bJh8wQsTGCzI3j519/CASStoDONUBVz2I/VID0MpiX3SGSnbOD2xUalbE5g==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -2440,19 +3091,22 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/into-stream": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-7.0.0.tgz", "integrity": "sha512-2dYz766i9HprMBasCMvHMuazJ7u4WzhJwo5kb3iPSiW/iRYV6uPari3zHoqZlnuaR7V1bEiNMxikhp37rdBXbw==", "dev": true, + "license": "MIT", "dependencies": { "from2": "^2.3.0", "p-is-promise": "^3.0.0" @@ -2468,25 +3122,15 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-core-module": { - "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, - "dependencies": { - "hasown": "^2.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "license": "MIT" }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -2496,6 +3140,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2505,6 +3150,7 @@ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -2517,6 +3163,7 @@ "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } @@ -2526,6 +3173,7 @@ "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -2535,6 +3183,7 @@ "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -2547,6 +3196,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -2559,6 +3209,7 @@ "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", "dev": true, + "license": "MIT", "dependencies": { "text-extensions": "^2.0.0" }, @@ -2567,10 +3218,11 @@ } }, "node_modules/is-unicode-supported": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.0.0.tgz", - "integrity": "sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -2582,19 +3234,22 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/issue-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-6.0.0.tgz", - "integrity": "sha512-zKa/Dxq2lGsBIXQ7CUZWTHfvxPC2ej0KfO7fIPqLlHB9J2hJ7rGhZ5rilhuufylr4RXYPzJUeFjKxz305OsNlA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-7.0.1.tgz", + "integrity": "sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==", "dev": true, + "license": "MIT", "dependencies": { "lodash.capitalize": "^4.2.1", "lodash.escaperegexp": "^4.1.2", @@ -2603,7 +3258,7 @@ "lodash.uniqby": "^4.7.0" }, "engines": { - "node": ">=10.13" + "node": "^18.17 || >=20.6.1" } }, "node_modules/java-properties": { @@ -2611,6 +3266,7 @@ "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", "integrity": "sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6.0" } @@ -2619,13 +3275,15 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2637,25 +3295,29 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -2670,13 +3332,15 @@ "dev": true, "engines": [ "node >= 0.2.0" - ] + ], + "license": "MIT" }, "node_modules/JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", "dev": true, + "license": "(MIT OR Apache-2.0)", "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" @@ -2692,13 +3356,15 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/load-json-file": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.1.2", "parse-json": "^4.0.0", @@ -2714,6 +3380,7 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", "dev": true, + "license": "MIT", "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" @@ -2727,6 +3394,7 @@ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", "dev": true, + "license": "MIT", "dependencies": { "p-locate": "^2.0.0", "path-exists": "^3.0.0" @@ -2739,58 +3407,64 @@ "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.capitalize": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.uniqby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lru-cache": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz", - "integrity": "sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "engines": { - "node": "14 || >=16.14" - } + "license": "ISC" }, "node_modules/marked": { - "version": "12.0.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.0.tgz", - "integrity": "sha512-Vkwtq9rLqXryZnWaQc86+FHLC6tr/fycMfYAhiOIXkrNmeGAyhSxjqu0Rs1i0bBqw5u0S7+lV9fdH2ZSVaoa0w==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", "dev": true, + "license": "MIT", "bin": { "marked": "bin/marked.js" }, @@ -2799,15 +3473,16 @@ } }, "node_modules/marked-terminal": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.0.0.tgz", - "integrity": "sha512-sNEx8nn9Ktcm6pL0TnRz8tnXq/mSS0Q1FRSwJOAqw4lAB4l49UeDf85Gm1n9RPFm5qurCPjwi1StAQT2XExhZw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.1.0.tgz", + "integrity": "sha512-+pvwa14KZL74MVXjYdPR3nSInhGhNvPce/3mqLVZT2oUvt654sL1XImFuLZ1pkA866IYZ3ikDTOFUIC7XzpZZg==", "dev": true, + "license": "MIT", "dependencies": { - "ansi-escapes": "^6.2.0", + "ansi-escapes": "^7.0.0", "chalk": "^5.3.0", "cli-highlight": "^2.1.11", - "cli-table3": "^0.6.3", + "cli-table3": "^0.6.5", "node-emoji": "^2.1.3", "supports-hyperlinks": "^3.0.0" }, @@ -2815,16 +3490,17 @@ "node": ">=16.0.0" }, "peerDependencies": { - "marked": ">=1 <13" + "marked": ">=1 <14" } }, "node_modules/meow": { - "version": "12.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", - "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=16.10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2834,24 +3510,27 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { @@ -2859,13 +3538,14 @@ } }, "node_modules/mime": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.1.tgz", - "integrity": "sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", + "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", "dev": true, "funding": [ "https://github.com/sponsors/broofa" ], + "license": "MIT", "bin": { "mime": "bin/cli.js" }, @@ -2878,6 +3558,7 @@ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -2887,21 +3568,24 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, "node_modules/mz": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", "dev": true, + "license": "MIT", "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", @@ -2912,19 +3596,22 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/nerf-dart": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/nerf-dart/-/nerf-dart-1.0.0.tgz", "integrity": "sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/node-emoji": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", "integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==", "dev": true, + "license": "MIT", "dependencies": { "@sindresorhus/is": "^4.6.0", "char-regex": "^1.0.2", @@ -2936,13 +3623,13 @@ } }, "node_modules/normalize-package-data": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.0.tgz", - "integrity": "sha512-UL7ELRVxYBHBgYEtZCXjxuD5vPxnmvMGq0jp/dGPKKrN7tfsBh2IY7TlJ15WWwdjRWD3RJbnsygUurTK3xkPkg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "hosted-git-info": "^7.0.0", - "is-core-module": "^2.8.1", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" }, @@ -2950,11 +3637,25 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", + "node_modules/normalize-package-data/node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-url": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.1.tgz", + "integrity": "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w==", + "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -2963,9 +3664,9 @@ } }, "node_modules/npm": { - "version": "10.8.2", - "resolved": "https://registry.npmjs.org/npm/-/npm-10.8.2.tgz", - "integrity": "sha512-x/AIjFIKRllrhcb48dqUNAAZl0ig9+qMuN91RpZo3Cb2+zuibfh+KISl6+kVVyktDz230JKc208UkQwwMqyB+w==", + "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", @@ -3037,6 +3738,14 @@ "write-file-atomic" ], "dev": true, + "license": "Artistic-2.0", + "workspaces": [ + "docs", + "smoke-tests", + "mock-globals", + "mock-registry", + "workspaces/*" + ], "dependencies": { "@isaacs/string-locale-compare": "^1.1.0", "@npmcli/arborist": "^7.5.4", @@ -3050,13 +3759,13 @@ "@sigstore/tuf": "^2.3.4", "abbrev": "^2.0.0", "archy": "~1.0.0", - "cacache": "^18.0.3", + "cacache": "^18.0.4", "chalk": "^5.3.0", "ci-info": "^4.0.0", "cli-columns": "^4.0.0", "fastest-levenshtein": "^1.0.16", "fs-minipass": "^3.0.3", - "glob": "^10.4.2", + "glob": "^10.4.5", "graceful-fs": "^4.2.11", "hosted-git-info": "^7.0.2", "ini": "^4.1.3", @@ -3065,7 +3774,7 @@ "json-parse-even-better-errors": "^3.0.2", "libnpmaccess": "^8.0.6", "libnpmdiff": "^6.1.4", - "libnpmexec": "^8.1.3", + "libnpmexec": "^8.1.4", "libnpmfund": "^5.0.12", "libnpmhook": "^10.0.5", "libnpmorg": "^6.0.6", @@ -3079,12 +3788,12 @@ "minipass": "^7.1.1", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^10.1.0", + "node-gyp": "^10.2.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.2", + "npm-package-arg": "^11.0.3", "npm-pick-manifest": "^9.1.0", "npm-profile": "^10.0.0", "npm-registry-fetch": "^17.1.0", @@ -3095,7 +3804,7 @@ "proc-log": "^4.2.0", "qrcode-terminal": "^0.12.0", "read": "^3.0.1", - "semver": "^7.6.2", + "semver": "^7.6.3", "spdx-expression-parse": "^4.0.0", "ssri": "^10.0.6", "supports-color": "^9.4.0", @@ -3120,6 +3829,7 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -3665,7 +4375,7 @@ } }, "node_modules/npm/node_modules/cacache": { - "version": "18.0.3", + "version": "18.0.4", "dev": true, "inBundle": true, "license": "ISC", @@ -3832,7 +4542,7 @@ } }, "node_modules/npm/node_modules/debug": { - "version": "4.3.5", + "version": "4.3.6", "dev": true, "inBundle": true, "license": "MIT", @@ -3916,7 +4626,7 @@ } }, "node_modules/npm/node_modules/foreground-child": { - "version": "3.2.1", + "version": "3.3.0", "dev": true, "inBundle": true, "license": "ISC", @@ -3944,7 +4654,7 @@ } }, "node_modules/npm/node_modules/glob": { - "version": "10.4.2", + "version": "10.4.5", "dev": true, "inBundle": true, "license": "ISC", @@ -3959,9 +4669,6 @@ "bin": { "glob": "dist/esm/bin.mjs" }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -4145,16 +4852,13 @@ "license": "ISC" }, "node_modules/npm/node_modules/jackspeak": { - "version": "3.4.0", + "version": "3.4.3", "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/cliui": "^8.0.2" }, - "engines": { - "node": ">=14" - }, "funding": { "url": "https://github.com/sponsors/isaacs" }, @@ -4240,7 +4944,7 @@ } }, "node_modules/npm/node_modules/libnpmexec": { - "version": "8.1.3", + "version": "8.1.4", "dev": true, "inBundle": true, "license": "ISC", @@ -4374,13 +5078,10 @@ } }, "node_modules/npm/node_modules/lru-cache": { - "version": "10.2.2", + "version": "10.4.3", "dev": true, "inBundle": true, - "license": "ISC", - "engines": { - "node": "14 || >=16.14" - } + "license": "ISC" }, "node_modules/npm/node_modules/make-fetch-happen": { "version": "13.0.1", @@ -4592,7 +5293,7 @@ } }, "node_modules/npm/node_modules/node-gyp": { - "version": "10.1.0", + "version": "10.2.0", "dev": true, "inBundle": true, "license": "MIT", @@ -4603,9 +5304,9 @@ "graceful-fs": "^4.2.6", "make-fetch-happen": "^13.0.0", "nopt": "^7.0.0", - "proc-log": "^3.0.0", + "proc-log": "^4.1.0", "semver": "^7.3.5", - "tar": "^6.1.2", + "tar": "^6.2.1", "which": "^4.0.0" }, "bin": { @@ -4615,15 +5316,6 @@ "node": "^16.14.0 || >=18.0.0" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/proc-log": { - "version": "3.0.0", - "dev": true, - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/npm/node_modules/nopt": { "version": "7.2.1", "dev": true, @@ -4696,7 +5388,7 @@ } }, "node_modules/npm/node_modules/npm-package-arg": { - "version": "11.0.2", + "version": "11.0.3", "dev": true, "inBundle": true, "license": "ISC", @@ -4870,7 +5562,7 @@ } }, "node_modules/npm/node_modules/postcss-selector-parser": { - "version": "6.1.0", + "version": "6.1.2", "dev": true, "inBundle": true, "license": "MIT", @@ -5008,7 +5700,7 @@ "optional": true }, "node_modules/npm/node_modules/semver": { - "version": "7.6.2", + "version": "7.6.3", "dev": true, "inBundle": true, "license": "ISC", @@ -5531,6 +6223,7 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -5540,6 +6233,7 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } @@ -5549,6 +6243,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -5564,6 +6259,7 @@ "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz", "integrity": "sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -5576,6 +6272,7 @@ "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-4.1.0.tgz", "integrity": "sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==", "dev": true, + "license": "MIT", "dependencies": { "p-map": "^7.0.1" }, @@ -5591,6 +6288,7 @@ "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5600,6 +6298,7 @@ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", "dev": true, + "license": "MIT", "dependencies": { "p-try": "^1.0.0" }, @@ -5612,6 +6311,7 @@ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", "dev": true, + "license": "MIT", "dependencies": { "p-limit": "^1.1.0" }, @@ -5620,10 +6320,11 @@ } }, "node_modules/p-map": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.1.tgz", - "integrity": "sha512-2wnaR0XL/FDOj+TgpDuRb2KTjLnu3Fma6b1ZUwGY7LcqenMcvP/YFpjpbPKY6WVGsbuJZRuoUz8iPrt8ORnAFw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.2.tgz", + "integrity": "sha512-z4cYYMMdKHzw4O5UkWJImbZynVIo0lSGTXc7bzB1e/rrDqkgGUNysK/o4bTr+0+xKvvLoTyGqYC4Fgljy9qe1Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -5636,6 +6337,7 @@ "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz", "integrity": "sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5645,6 +6347,7 @@ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -5654,6 +6357,7 @@ "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", "dev": true, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -5666,6 +6370,7 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -5684,6 +6389,7 @@ "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -5695,13 +6401,15 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/parse5-htmlparser2-tree-adapter": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", "dev": true, + "license": "MIT", "dependencies": { "parse5": "^6.0.1" } @@ -5710,19 +6418,22 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/parsimmon": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz", "integrity": "sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-exists": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -5732,6 +6443,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -5741,15 +6453,24 @@ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/picocolors": { + "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" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -5762,6 +6483,7 @@ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -5771,6 +6493,7 @@ "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", "integrity": "sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==", "dev": true, + "license": "MIT", "dependencies": { "find-up": "^2.0.0", "load-json-file": "^4.0.0" @@ -5784,6 +6507,7 @@ "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.1.0.tgz", "integrity": "sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==", "dev": true, + "license": "MIT", "dependencies": { "parse-ms": "^4.0.0" }, @@ -5798,13 +6522,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/promisified-properties": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/promisified-properties/-/promisified-properties-3.0.0.tgz", "integrity": "sha512-ARteuBuUpPg/+spsMhcKHvdtOW/q8btyyVYYxxegGgx+7u9ix9at8DjP2KM2t8+4SuI8wBLt+3X876FMQx91yQ==", "dev": true, + "license": "MIT", "dependencies": { "parsimmon": "^1.13.0" }, @@ -5817,7 +6543,8 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/queue-microtask": { "version": "1.2.3", @@ -5837,13 +6564,15 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -5859,6 +6588,7 @@ "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", "dev": true, + "license": "MIT", "dependencies": { "find-up-simple": "^1.0.0", "read-pkg": "^9.0.0", @@ -5871,23 +6601,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-package-up/node_modules/type-fest": { - "version": "4.25.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.25.0.tgz", - "integrity": "sha512-bRkIGlXsnGBRBQRAY56UXBm//9qH4bmJfFvq83gSz41N282df+fjy8ofcEgc1sM8geNt5cl6mC2g9Fht1cs8Aw==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/read-pkg": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", "dev": true, + "license": "MIT", "dependencies": { "@types/normalize-package-data": "^2.4.3", "normalize-package-data": "^6.0.0", @@ -5908,6 +6627,7 @@ "integrity": "sha512-LOVbvF1Q0SZdjClSefZ0Nz5z8u+tIE7mV5NibzmE9VYmDe9CaBbAVtz1veOSZbofrdsilxuDAYnFenukZVp8/Q==", "deprecated": "Renamed to read-package-up", "dev": true, + "license": "MIT", "dependencies": { "find-up-simple": "^1.0.0", "read-pkg": "^9.0.0", @@ -5920,23 +6640,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-pkg-up/node_modules/type-fest": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.2.tgz", - "integrity": "sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/read-pkg/node_modules/parse-json": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.1.0.tgz", "integrity": "sha512-rum1bPifK5SSar35Z6EKZuYPJx85pkNaFrxBK3mwdfSJ1/WKbYrjoW/zTPSjRRamfmVX1ACBIdFAO0VRErW/EA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.22.13", "index-to-position": "^0.1.2", @@ -5949,23 +6658,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/read-pkg/node_modules/type-fest": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.10.2.tgz", - "integrity": "sha512-anpAG63wSpdEbLwOqH8L84urkL6PiVIov3EMmgIhhThevh9aiMQov+6Btx0wldNcvm4wV+e2/Rt1QdDwKHFbHw==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, + "license": "MIT", "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -5981,6 +6679,7 @@ "resolved": "https://registry.npmjs.org/redeyed/-/redeyed-2.1.1.tgz", "integrity": "sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==", "dev": true, + "license": "MIT", "dependencies": { "esprima": "~4.0.0" } @@ -5990,6 +6689,7 @@ "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==", "dev": true, + "license": "MIT", "dependencies": { "@pnpm/npm-conf": "^2.1.0" }, @@ -6002,6 +6702,7 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6011,6 +6712,7 @@ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -6020,6 +6722,7 @@ "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -6044,6 +6747,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } @@ -6052,17 +6756,19 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/semantic-release": { - "version": "24.1.0", - "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.1.0.tgz", - "integrity": "sha512-FwaE2hKDHQn9G6GA7xmqsc9WnsjaFD/ppLM5PUg56Do9oKSCf+vH6cPeb3hEBV/m06n8Sh9vbVqPjHu/1onzQw==", + "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": { "@semantic-release/commit-analyzer": "^13.0.0-beta.1", "@semantic-release/error": "^4.0.0", - "@semantic-release/github": "^10.0.0", + "@semantic-release/github": "^11.0.0", "@semantic-release/npm": "^12.0.0", "@semantic-release/release-notes-generator": "^14.0.0-beta.1", "aggregate-error": "^5.0.0", @@ -6075,7 +6781,7 @@ "get-stream": "^6.0.0", "git-log-parser": "^1.2.0", "hook-std": "^3.0.0", - "hosted-git-info": "^7.0.0", + "hosted-git-info": "^8.0.0", "import-from-esm": "^1.3.1", "lodash-es": "^4.17.21", "marked": "^12.0.0", @@ -6097,279 +6803,22 @@ "node": ">=20.8.1" } }, - "node_modules/semantic-release/node_modules/@octokit/auth-token": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-5.1.1.tgz", - "integrity": "sha512-rh3G3wDO8J9wSjfI436JUKzHIxq8NaiL0tVeB2aXmG6p/9859aUOAjA9pmSPNGGZxfwmaJ9ozOJImuNVJdpvbA==", - "dev": true, - "engines": { - "node": ">= 18" - } - }, - "node_modules/semantic-release/node_modules/@octokit/core": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-6.1.2.tgz", - "integrity": "sha512-hEb7Ma4cGJGEUNOAVmyfdB/3WirWMg5hDuNFVejGEDFqupeOysLc2sG6HJxY2etBp5YQu5Wtxwi020jS9xlUwg==", - "dev": true, - "dependencies": { - "@octokit/auth-token": "^5.0.0", - "@octokit/graphql": "^8.0.0", - "@octokit/request": "^9.0.0", - "@octokit/request-error": "^6.0.1", - "@octokit/types": "^13.0.0", - "before-after-hook": "^3.0.2", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/semantic-release/node_modules/@octokit/endpoint": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-10.1.1.tgz", - "integrity": "sha512-JYjh5rMOwXMJyUpj028cu0Gbp7qe/ihxfJMLc8VZBMMqSwLgOxDI1911gV4Enl1QSavAQNJcwmwBF9M0VvLh6Q==", - "dev": true, - "dependencies": { - "@octokit/types": "^13.0.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/semantic-release/node_modules/@octokit/graphql": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-8.1.1.tgz", - "integrity": "sha512-ukiRmuHTi6ebQx/HFRCXKbDlOh/7xEV6QUXaE7MJEKGNAncGI/STSbOkl12qVXZrfZdpXctx5O9X1AIaebiDBg==", - "dev": true, - "dependencies": { - "@octokit/request": "^9.0.0", - "@octokit/types": "^13.0.0", - "universal-user-agent": "^7.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/semantic-release/node_modules/@octokit/openapi-types": { - "version": "22.2.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-22.2.0.tgz", - "integrity": "sha512-QBhVjcUa9W7Wwhm6DBFu6ZZ+1/t/oYxqc2tp81Pi41YNuJinbFRx8B133qVOrAaBbF7D/m0Et6f9/pZt9Rc+tg==", - "dev": true - }, - "node_modules/semantic-release/node_modules/@octokit/plugin-paginate-rest": { - "version": "11.3.3", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-11.3.3.tgz", - "integrity": "sha512-o4WRoOJZlKqEEgj+i9CpcmnByvtzoUYC6I8PD2SA95M+BJ2x8h7oLcVOg9qcowWXBOdcTRsMZiwvM3EyLm9AfA==", - "dev": true, - "dependencies": { - "@octokit/types": "^13.5.0" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/semantic-release/node_modules/@octokit/plugin-retry": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-7.1.1.tgz", - "integrity": "sha512-G9Ue+x2odcb8E1XIPhaFBnTTIrrUDfXN05iFXiqhR+SeeeDMMILcAnysOsxUpEWcQp2e5Ft397FCXTcPkiPkLw==", - "dev": true, - "dependencies": { - "@octokit/request-error": "^6.0.0", - "@octokit/types": "^13.0.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": ">=6" - } - }, - "node_modules/semantic-release/node_modules/@octokit/plugin-throttling": { - "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, - "dependencies": { - "@octokit/types": "^13.0.0", - "bottleneck": "^2.15.3" - }, - "engines": { - "node": ">= 18" - }, - "peerDependencies": { - "@octokit/core": "^6.0.0" - } - }, - "node_modules/semantic-release/node_modules/@octokit/request": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-9.1.3.tgz", - "integrity": "sha512-V+TFhu5fdF3K58rs1pGUJIDH5RZLbZm5BI+MNF+6o/ssFNT4vWlCh/tVpF3NxGtP15HUxTTMUbsG5llAuU2CZA==", - "dev": true, - "dependencies": { - "@octokit/endpoint": "^10.0.0", - "@octokit/request-error": "^6.0.1", - "@octokit/types": "^13.1.0", - "universal-user-agent": "^7.0.2" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/semantic-release/node_modules/@octokit/request-error": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-6.1.4.tgz", - "integrity": "sha512-VpAhIUxwhWZQImo/dWAN/NpPqqojR6PSLgLYAituLM6U+ddx9hCioFGwBr5Mi+oi5CLeJkcAs3gJ0PYYzU6wUg==", - "dev": true, - "dependencies": { - "@octokit/types": "^13.0.0" - }, - "engines": { - "node": ">= 18" - } - }, - "node_modules/semantic-release/node_modules/@octokit/types": { - "version": "13.5.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-13.5.0.tgz", - "integrity": "sha512-HdqWTf5Z3qwDVlzCrP8UJquMwunpDiMPt5er+QjGzL4hqr/vBVY/MauQgS1xWxCDT1oMx1EULyqxncdCY/NVSQ==", - "dev": true, - "dependencies": { - "@octokit/openapi-types": "^22.2.0" - } - }, - "node_modules/semantic-release/node_modules/@semantic-release/commit-analyzer": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-13.0.0.tgz", - "integrity": "sha512-KtXWczvTAB1ZFZ6B4O+w8HkfYm/OgQb1dUGNFZtDgQ0csggrmkq8sTxhd+lwGF8kMb59/RnG9o4Tn7M/I8dQ9Q==", - "dev": true, - "dependencies": { - "conventional-changelog-angular": "^8.0.0", - "conventional-changelog-writer": "^8.0.0", - "conventional-commits-filter": "^5.0.0", - "conventional-commits-parser": "^6.0.0", - "debug": "^4.0.0", - "import-from-esm": "^1.0.3", - "lodash-es": "^4.17.21", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=20.8.1" - }, - "peerDependencies": { - "semantic-release": ">=20.1.0" - } - }, "node_modules/semantic-release/node_modules/@semantic-release/error": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } }, - "node_modules/semantic-release/node_modules/@semantic-release/github": { - "version": "10.1.7", - "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-10.1.7.tgz", - "integrity": "sha512-QnhP4k1eqzYLz6a4kpWrUQeKJYXqHggveMykvUFbSquq07GF85BXvr/QLhpOD7bpDcmEfL8VnphRA7KT5i9lzQ==", - "dev": true, - "dependencies": { - "@octokit/core": "^6.0.0", - "@octokit/plugin-paginate-rest": "^11.0.0", - "@octokit/plugin-retry": "^7.0.0", - "@octokit/plugin-throttling": "^9.0.0", - "@semantic-release/error": "^4.0.0", - "aggregate-error": "^5.0.0", - "debug": "^4.3.4", - "dir-glob": "^3.0.1", - "globby": "^14.0.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", - "issue-parser": "^7.0.0", - "lodash-es": "^4.17.21", - "mime": "^4.0.0", - "p-filter": "^4.0.0", - "url-join": "^5.0.0" - }, - "engines": { - "node": ">=20.8.1" - }, - "peerDependencies": { - "semantic-release": ">=20.1.0" - } - }, - "node_modules/semantic-release/node_modules/@semantic-release/npm": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-12.0.1.tgz", - "integrity": "sha512-/6nntGSUGK2aTOI0rHPwY3ZjgY9FkXmEHbW9Kr+62NVOsyqpKKeP0lrCH+tphv+EsNdJNmqqwijTEnVWUMQ2Nw==", - "dev": true, - "dependencies": { - "@semantic-release/error": "^4.0.0", - "aggregate-error": "^5.0.0", - "execa": "^9.0.0", - "fs-extra": "^11.0.0", - "lodash-es": "^4.17.21", - "nerf-dart": "^1.0.0", - "normalize-url": "^8.0.0", - "npm": "^10.5.0", - "rc": "^1.2.8", - "read-pkg": "^9.0.0", - "registry-auth-token": "^5.0.0", - "semver": "^7.1.2", - "tempy": "^3.0.0" - }, - "engines": { - "node": ">=20.8.1" - }, - "peerDependencies": { - "semantic-release": ">=20.1.0" - } - }, - "node_modules/semantic-release/node_modules/@semantic-release/release-notes-generator": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-14.0.1.tgz", - "integrity": "sha512-K0w+5220TM4HZTthE5dDpIuFrnkN1NfTGPidJFm04ULT1DEZ9WG89VNXN7F0c+6nMEpWgqmPvb7vY7JkB2jyyA==", - "dev": true, - "dependencies": { - "conventional-changelog-angular": "^8.0.0", - "conventional-changelog-writer": "^8.0.0", - "conventional-commits-filter": "^5.0.0", - "conventional-commits-parser": "^6.0.0", - "debug": "^4.0.0", - "get-stream": "^7.0.0", - "import-from-esm": "^1.0.3", - "into-stream": "^7.0.0", - "lodash-es": "^4.17.21", - "read-package-up": "^11.0.0" - }, - "engines": { - "node": ">=20.8.1" - }, - "peerDependencies": { - "semantic-release": ">=20.1.0" - } - }, - "node_modules/semantic-release/node_modules/@semantic-release/release-notes-generator/node_modules/get-stream": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-7.0.1.tgz", - "integrity": "sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ==", - "dev": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/semantic-release/node_modules/@sindresorhus/merge-streams": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -6382,6 +6831,7 @@ "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", "dev": true, + "license": "MIT", "dependencies": { "clean-stack": "^5.2.0", "indent-string": "^5.0.0" @@ -6393,17 +6843,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/semantic-release/node_modules/before-after-hook": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-3.0.2.tgz", - "integrity": "sha512-Nik3Sc0ncrMK4UUdXQmAnRtzmNQTAAXmXIopizwZ1W1t8QmfJj+zL4OA2I7XPTPW5z5TDqv4hRo/JzouDJnX3A==", - "dev": true - }, "node_modules/semantic-release/node_modules/clean-stack": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.2.0.tgz", "integrity": "sha512-TyUIUJgdFnCISzG5zu3291TAsE77ddchd0bepon1VVQrKLGKFED4iXFEDQ24mIPdPBbyE16PK3F8MYE1CmcBEQ==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "5.0.0" }, @@ -6414,66 +6859,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/semantic-release/node_modules/conventional-changelog-angular": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.0.0.tgz", - "integrity": "sha512-CLf+zr6St0wIxos4bmaKHRXWAcsCXrJU6F4VdNDrGRK3B8LDLKoX3zuMV5GhtbGkVR/LohZ6MT6im43vZLSjmA==", - "dev": true, - "dependencies": { - "compare-func": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/semantic-release/node_modules/conventional-changelog-writer": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-8.0.0.tgz", - "integrity": "sha512-TQcoYGRatlAnT2qEWDON/XSfnVG38JzA7E0wcGScu7RElQBkg9WWgZd1peCWFcWDh1xfb2CfsrcvOn1bbSzztA==", - "dev": true, - "dependencies": { - "@types/semver": "^7.5.5", - "conventional-commits-filter": "^5.0.0", - "handlebars": "^4.7.7", - "meow": "^13.0.0", - "semver": "^7.5.2" - }, - "bin": { - "conventional-changelog-writer": "dist/cli/index.js" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/semantic-release/node_modules/conventional-commits-filter": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-5.0.0.tgz", - "integrity": "sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==", - "dev": true, - "engines": { - "node": ">=18" - } - }, - "node_modules/semantic-release/node_modules/conventional-commits-parser": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.0.0.tgz", - "integrity": "sha512-TbsINLp48XeMXR8EvGjTnKGsZqBemisPoyWESlpRyR8lif0lcwzqz+NMtYSj1ooF/WYjSuu7wX0CtdeeMEQAmA==", - "dev": true, - "dependencies": { - "meow": "^13.0.0" - }, - "bin": { - "conventional-commits-parser": "dist/cli/index.js" - }, - "engines": { - "node": ">=18" - } - }, "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" }, @@ -6482,10 +6873,11 @@ } }, "node_modules/semantic-release/node_modules/execa": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-9.3.1.tgz", - "integrity": "sha512-gdhefCCNy/8tpH/2+ajP9IQc14vXchNdd0weyzSJEFURhRMGncQ+zKFxwjAufIewPEJm9BPOaJnvg2UtlH2gPQ==", + "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": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.3", @@ -6494,7 +6886,7 @@ "human-signals": "^8.0.0", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", - "npm-run-path": "^5.2.0", + "npm-run-path": "^6.0.0", "pretty-ms": "^9.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", @@ -6512,6 +6904,7 @@ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", "dev": true, + "license": "MIT", "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" @@ -6523,27 +6916,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/semantic-release/node_modules/find-versions": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-6.0.0.tgz", - "integrity": "sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==", - "dev": true, - "dependencies": { - "semver-regex": "^4.0.5", - "super-regex": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/semantic-release/node_modules/human-signals": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.0.tgz", "integrity": "sha512-/1/GPCpDUCCYwlERiYjxoczfP0zfvZMU/OWgQPMya9AbAE24vseigFdhAMObpc8Q4lc/kjutPfUddDYyAmejnA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=18.18.0" } @@ -6553,6 +6931,7 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6565,34 +6944,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/semantic-release/node_modules/issue-parser": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-7.0.1.tgz", - "integrity": "sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==", - "dev": true, - "dependencies": { - "lodash.capitalize": "^4.2.1", - "lodash.escaperegexp": "^4.1.2", - "lodash.isplainobject": "^4.0.6", - "lodash.isstring": "^4.0.1", - "lodash.uniqby": "^4.7.0" - }, - "engines": { - "node": "^18.17 || >=20.6.1" - } - }, - "node_modules/semantic-release/node_modules/meow": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", - "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", - "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -6601,15 +6953,17 @@ } }, "node_modules/semantic-release/node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", "dev": true, + "license": "MIT", "dependencies": { - "path-key": "^4.0.0" + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6620,6 +6974,7 @@ "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-3.0.0.tgz", "integrity": "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6632,6 +6987,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6644,6 +7000,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -6656,6 +7013,7 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -6663,20 +7021,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/semantic-release/node_modules/universal-user-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", - "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", - "dev": true + "node_modules/semantic-release/node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -6689,6 +7052,7 @@ "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.3.5" }, @@ -6704,6 +7068,7 @@ "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6711,23 +7076,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -6740,6 +7094,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -6748,13 +7103,15 @@ "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/signale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz", "integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^2.3.2", "figures": "^2.0.0", @@ -6769,6 +7126,7 @@ "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" }, @@ -6781,6 +7139,7 @@ "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", @@ -6795,6 +7154,7 @@ "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" } @@ -6803,13 +7163,15 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/signale/node_modules/figures": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.5" }, @@ -6822,6 +7184,7 @@ "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" } @@ -6831,6 +7194,7 @@ "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" }, @@ -6843,6 +7207,7 @@ "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", "dev": true, + "license": "MIT", "dependencies": { "unicode-emoji-modifier-base": "^1.0.0" }, @@ -6855,6 +7220,7 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -6867,6 +7233,7 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -6875,45 +7242,51 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz", "integrity": "sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/spdx-correct": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-exceptions": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz", - "integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==", - "dev": true + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" }, "node_modules/spdx-expression-parse": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, + "license": "MIT", "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", - "dev": true + "version": "3.0.20", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.20.tgz", + "integrity": "sha512-jg25NiDV/1fLtSgEgyvVyDunvaNHbuwF9lfNV17gSmPFAlYzdfNBlLtLzXTevwkPj7DhGbmN9VnmJIgLnhvaBw==", + "dev": true, + "license": "CC0-1.0" }, "node_modules/split2": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", "dev": true, + "license": "ISC", "engines": { "node": ">= 10.x" } @@ -6923,6 +7296,7 @@ "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", "integrity": "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==", "dev": true, + "license": "MIT", "dependencies": { "duplexer2": "~0.1.0", "readable-stream": "^2.0.2" @@ -6933,6 +7307,7 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "~5.1.0" } @@ -6942,6 +7317,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -6956,6 +7332,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -6968,6 +7345,7 @@ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -6977,6 +7355,7 @@ "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -6986,6 +7365,7 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -6995,6 +7375,7 @@ "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-1.0.0.tgz", "integrity": "sha512-CY8u7DtbvucKuquCmOFEKhr9Besln7n9uN8eFbwcoGYWXOMW07u2o8njWaiXt11ylS3qoGF55pILjRmPlbodyg==", "dev": true, + "license": "MIT", "dependencies": { "function-timeout": "^1.0.1", "time-span": "^5.1.0" @@ -7011,6 +7392,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -7019,16 +7401,20 @@ } }, "node_modules/supports-hyperlinks": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz", - "integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.1.0.tgz", + "integrity": "sha512-2rn0BZ+/f7puLOHZm1HOJfwBggfaHXUpPUSSG/SWM4TWp5KCfmNYwnC3hruy2rZlMnmWZ+QAGpZfchu3f3695A==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" }, "engines": { "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/temp-dir": { @@ -7036,6 +7422,7 @@ "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" } @@ -7045,6 +7432,7 @@ "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.1.0.tgz", "integrity": "sha512-7jDLIdD2Zp0bDe5r3D2qtkd1QOCacylBuL7oa4udvN6v2pqr4+LcCr67C8DR1zkpaZ8XosF5m1yQSabKAW6f2g==", "dev": true, + "license": "MIT", "dependencies": { "is-stream": "^3.0.0", "temp-dir": "^3.0.0", @@ -7063,6 +7451,7 @@ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -7075,6 +7464,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=12.20" }, @@ -7087,6 +7477,7 @@ "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" }, @@ -7099,6 +7490,7 @@ "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, + "license": "MIT", "dependencies": { "any-promise": "^1.0.0" } @@ -7108,6 +7500,7 @@ "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "dev": true, + "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" }, @@ -7119,13 +7512,15 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, + "license": "MIT", "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" @@ -7136,6 +7531,7 @@ "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", "dev": true, + "license": "MIT", "dependencies": { "convert-hrtime": "^5.0.0" }, @@ -7151,6 +7547,7 @@ "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -7163,6 +7560,7 @@ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz", "integrity": "sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7171,22 +7569,24 @@ } }, "node_modules/type-fest": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", - "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=14.16" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/uglify-js": { - "version": "3.17.4", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", - "integrity": "sha512-T9q82TJI9e/C1TAxYvfb16xO120tMVFZrGA3f9/P4424DNu6ypK103y0GPFVa17yotwSyZW5iYXgjYHkGrJW/g==", + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", "dev": true, + "license": "BSD-2-Clause", "optional": true, "bin": { "uglifyjs": "bin/uglifyjs" @@ -7200,6 +7600,7 @@ "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -7209,6 +7610,7 @@ "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -7221,6 +7623,7 @@ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", "dev": true, + "license": "MIT", "dependencies": { "crypto-random-string": "^4.0.0" }, @@ -7232,16 +7635,18 @@ } }, "node_modules/universal-user-agent": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", - "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", - "dev": true + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.2.tgz", + "integrity": "sha512-0JCqzSKnStlRRQfCdowvqy3cy0Dvtlb8xecj/H8JFZuCze4rwjPZQOgvFvn0Ws/usCHQFGpyr+pB9adaGwXn4Q==", + "dev": true, + "license": "ISC" }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.0.0" } @@ -7251,6 +7656,7 @@ "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } @@ -7259,13 +7665,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, + "license": "Apache-2.0", "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" @@ -7276,6 +7684,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -7290,13 +7699,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -7313,13 +7724,15 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.4" } @@ -7329,21 +7742,17 @@ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -7362,6 +7771,7 @@ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -7371,6 +7781,7 @@ "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.1.tgz", "integrity": "sha512-GQHQqAopRhwU8Kt1DDM8NjibDXHC8eoh1erhGAJPEyveY9qqVeXvVikNKrDz69sHowPMorbPUrH/mx8c50eiBQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, diff --git a/package.json b/package.json index 827688e6b..105a5ca87 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "@saithodev/semantic-release-backmerge": "^4.0.1", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", - "gradle-semantic-release-plugin": "^1.9.2", - "semantic-release": "^24.1.0" + "gradle-semantic-release-plugin": "^1.10.1", + "semantic-release": "^24.1.2" } } diff --git a/patches.json b/patches.json index 1d595f23c..4d9af1097 100644 --- a/patches.json +++ b/patches.json @@ -1 +1 @@ -[{"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.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":"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.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 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 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 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 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":"cairo","values":{"Cairo":"cairo","Outline":"outline","OutlineCircle":"outlinecircle","Round":"round","YoutubeOutline":"youtubeoutline","YouTube":"youtube"},"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":true,"requiresIntegrations":false,"options":[{"key":"AppIcon","default":"revancify_blue","values":{"AFN Blue":"afn_blue","AFN Red":"afn_red","MMT":"mmt","Revancify Blue":"revancify_blue","Revancify Red":"revancify_red","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":"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":true,"requiresIntegrations":false,"options":[{"key":"AppIcon","default":"revancify_blue","values":{"AFN Blue":"afn_blue","AFN Red":"afn_red","MMT":"mmt","Revancify Blue":"revancify_blue","Revancify Red":"revancify_red","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":"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":true,"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":true,"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","description":"Applies a custom header in the top left corner within the app.","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":"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\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}]},{"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.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 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 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.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 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 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 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 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 hide player buttons background","description":"Removes, at compile time, the dark background surrounding the video player controls.","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":"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.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":"app.rvx.android.youtube","values":{"Clone":"com.rvx.android.youtube","Default":"app.rvx.android.youtube"},"title":"Package name of YouTube","description":"The name of the package to use in GmsCore support.","required":true},{"key":"PackageNameYouTubeMusic","default":"app.rvx.android.apps.youtube.music","values":{"Clone":"com.rvx.android.apps.youtube.music","Default":"app.rvx.android.apps.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.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":"app.rvx.android.youtube","values":{"Clone":"com.rvx.android.youtube","Default":"app.rvx.android.youtube"},"title":"Package name of YouTube","description":"The name of the package to use in GmsCore support.","required":true},{"key":"PackageNameYouTubeMusic","default":"app.rvx.android.apps.youtube.music","values":{"Clone":"com.rvx.android.apps.youtube.music","Default":"app.rvx.android.apps.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.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 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 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.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 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 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.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":"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":"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":"bold","values":{"Bold":"bold","Rounded":"rounded","Thin":"thin"},"title":"Icon type","description":"The icon type.","required":true},{"key":"BottomMargin","default":"2.5dip","values":{"Default":"2.5dip","None":"0.0dip","Wider":"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 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":"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":"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 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 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 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.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 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":"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 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 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 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":false,"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":false,"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":"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":"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":[]},{"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":"@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"},"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 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":"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":false,"values":null,"title":"Outline icons","description":"Apply the outline icon.","required":true}]},{"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 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 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":"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":"CustomTranslations","default":"","values":null,"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},{"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":"CustomTranslations","default":"","values":null,"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 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.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":"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":"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":"RVXSettingsMenuIcon","default":"extension","values":{"Custom branding icon":"custom_branding_icon","Extension":"extension","Gear":"gear","YT alt":"yt_alt","ReVanced":"revanced","ReVanced Colored":"revanced_colored"},"title":"RVX settings menu icon","description":"The icon for the RVX settings menu.","required":true},{"key":"ApplyToAll","default":false,"values":null,"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}]},{"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":"RVXSettingsMenuIcon","default":"extension","values":{"Custom branding icon":"custom_branding_icon","Extension":"extension","Gear":"gear","ReVanced":"revanced","ReVanced Colored":"revanced_colored"},"title":"RVX settings menu icon","description":"The icon for the RVX settings menu.","required":true}]},{"name":"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":[]}] \ 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.","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,"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,"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,"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,"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,"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,"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,"options":[]},{"name":"Change package name","description":"Changes the package name for Reddit to the name specified in patch options.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":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,"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,"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,"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,"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,"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.","compatiblePackages":null,"use":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":{"Lowest":"1","Highest":"2147483647"},"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,"options":[{"key":"iconType","default":"cairo","values":{"Cairo":"cairo","Outline":"outline","OutlineCircle":"outlinecircle","Round":"round","YoutubeOutline":"youtubeoutline","YouTube":"youtube"},"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 patch options.","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,"options":[{"key":"appIcon","default":"revancify_blue","values":{"AFN Blue":"afn_blue","AFN Red":"afn_red","MMT":"mmt","Revancify Blue":"revancify_blue","Revancify Red":"revancify_red","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":"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 patch options.","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,"options":[{"key":"appIcon","default":"revancify_blue","values":{"AFN Blue":"afn_blue","AFN Red":"afn_red","MMT":"mmt","Revancify Blue":"revancify_blue","Revancify Red":"revancify_red","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":"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 patch options.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":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 patch options.","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,"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 patch options.","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,"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 patch options.","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,"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","description":"Applies a custom header in the top left corner within the app.","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,"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\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}]},{"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"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,"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,"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,"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,"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,"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,"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,"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,"options":[]},{"name":"Force hide player buttons background","description":"Removes, at compile time, the dark background surrounding the video player controls.","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,"options":[]},{"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,"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.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":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":"packageNameYouTube","default":"app.rvx.android.youtube","values":{"Clone":"com.rvx.android.youtube","Default":"app.rvx.android.youtube"},"title":"Package name of YouTube","description":"The name of the package to use in GmsCore support.","required":true},{"key":"packageNameYouTubeMusic","default":"app.rvx.android.apps.youtube.music","values":{"Clone":"com.rvx.android.apps.youtube.music","Default":"app.rvx.android.apps.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.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":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":"packageNameYouTube","default":"app.rvx.android.youtube","values":{"Clone":"com.rvx.android.youtube","Default":"app.rvx.android.youtube"},"title":"Package name of YouTube","description":"The name of the package to use in GmsCore support.","required":true},{"key":"packageNameYouTubeMusic","default":"app.rvx.android.apps.youtube.music","values":{"Clone":"com.rvx.android.apps.youtube.music","Default":"app.rvx.android.apps.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,"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,"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,"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,"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,"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,"options":[]},{"name":"Hide ads","description":"Adds options to hide ads.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":true,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"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,"options":[{"key":"iconType","default":"bold","values":{"Bold":"bold","Rounded":"rounded","Thin":"thin"},"title":"Icon type","description":"The icon type.","required":true},{"key":"bottomMargin","default":"2.5dip","values":{"Default":"2.5dip","None":"0.0dip","Wider":"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 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,"options":[]},{"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,"options":[]},{"name":"Premium icon","description":"Unlocks premium app icons.","compatiblePackages":[{"name":"com.reddit.frontpage","versions":["2023.12.0","2024.17.0"]}],"use":true,"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,"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,"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,"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,"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,"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,"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,"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,"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":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":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,"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,"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,"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,"options":[]},{"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,"options":[{"key":"settingsLabel","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,"options":[{"key":"insertPosition","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"},"title":"Insert position","description":"The settings menu name that the RVX settings menu should be above.","required":true},{"key":"settingsLabel","default":"ReVanced Extended","values":null,"title":"RVX settings label","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,"options":[{"key":"settingsLabel","default":"ReVanced Extended","values":null,"title":"RVX settings label","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,"options":[]},{"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,"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,"options":[{"key":"outlineIcon","default":false,"values":null,"title":"Outline icons","description":"Apply the outline icon.","required":true}]},{"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,"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,"options":[]},{"name":"Spoof client","description":"Adds options to spoof the client to allow track playback.","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,"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,"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,"options":[]},{"name":"Theme","description":"Changes the app\u0027s theme to the values specified in patch options.","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,"options":[{"key":"darkThemeBackgroundColor","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"},"title":"Dark theme background color","description":"Can be a hex color (#AARRGGBB) or a color resource reference.","required":false},{"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":false}]},{"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,"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,"options":[{"key":"customTranslations","default":"","values":null,"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},{"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,"options":[{"key":"customTranslations","default":"","values":null,"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},{"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.apps.youtube.music","versions":["6.20.51","6.29.59","6.42.55","6.51.53","7.16.53"]}],"use":true,"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.youtube","versions":["18.29.38","18.33.40","18.38.44","18.48.39","19.05.36","19.16.39"]}],"use":true,"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,"options":[{"key":"settingsMenuIcon","default":"extension","values":{"Custom branding icon":"custom_branding_icon","Extension":"extension","Gear":"gear","YT alt":"yt_alt","ReVanced":"revanced","ReVanced Colored":"revanced_colored"},"title":"RVX settings menu icon","description":"The icon for the RVX settings menu.","required":true},{"key":"applyToAll","default":false,"values":null,"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}]},{"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,"options":[{"key":"settingsMenuIcon","default":"extension","values":{"Custom branding icon":"custom_branding_icon","Extension":"extension","Gear":"gear","ReVanced":"revanced","ReVanced Colored":"revanced_colored"},"title":"RVX settings menu icon","description":"The icon for the RVX settings menu.","required":true}]},{"name":"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,"options":[]}] \ No newline at end of file diff --git a/patches/api/patches.api b/patches/api/patches.api new file mode 100644 index 000000000..9b0565ada --- /dev/null +++ b/patches/api/patches.api @@ -0,0 +1,1126 @@ +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 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/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/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_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 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 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/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/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/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/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/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/resourceid/SharedResourceIdPatchKt { + public static final fun getCancelButton ()J + public static final fun getLabelAcknowledgements ()J + public static final fun getScreenShotShareBanner ()J + public static final fun getTextAppearanceRedditBaseOldButtonColored ()J + public static final fun getToolBarNavSearchCtaContainer ()J +} + +public final class app/revanced/patches/reddit/utils/settings/SettingsPatchKt { + public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +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/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/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/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 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/shortsplayback/ShortsPlaybackPatchKt { + public static final fun getShortsPlaybackPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +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_15_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_28_or_greater ()Z + public static final fun is_19_32_or_greater ()Z + public static final fun is_19_44_or_greater ()Z +} + +public final class app/revanced/patches/youtube/utils/recyclerview/BottomSheetRecyclerViewPatchKt { + public static final fun bottomSheetRecyclerViewHook (Ljava/lang/String;)V + public static final fun getBottomSheetRecyclerViewPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +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 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 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 getNotice ()J + public static final fun getNotificationBigPictureIconWidth ()J + public static final fun getOfflineActionsVideoDeletedUndoSnackbarText ()J + public static final fun getPlayerCollapseButton ()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 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 asSequence (Lorg/w3c/dom/NodeList;)Lkotlin/sequences/Sequence; + public static final fun childElementsSequence (Lorg/w3c/dom/Node;)Lkotlin/sequences/Sequence; + public static final fun cloneNodes (Lorg/w3c/dom/Node;Lorg/w3c/dom/Node;)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 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 forEachChildElement (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 insertFirst (Lorg/w3c/dom/Node;Lorg/w3c/dom/Node;)V + 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 iterateXmlNodeChildren (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;Ljava/lang/String;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 startsWithAny (Ljava/lang/String;[Ljava/lang/String;)Z + public static final fun underBarOrThrow (Lapp/revanced/patcher/patch/Option;)Ljava/lang/String; + 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..27f36e191 --- /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/inotia00/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..dab126698 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/generator/JsonPatchesFileGenerator.kt @@ -0,0 +1,62 @@ +package app.revanced.generator + +import app.revanced.patcher.patch.Package +import app.revanced.patcher.patch.Patch +import com.google.gson.GsonBuilder +import java.io.File + +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.compatiblePackages, + it.use, + it.options.values.map { option -> + JsonPatch.Option( + option.key, + option.default, + option.values, + option.title, + option.description, + option.required, + ) + }, + ) + }.let { + patchesJson.writeText(GsonBuilder().serializeNulls().create().toJson(it)) + } + + patchesJson.writeText( + patchesJson.readText() + .replace( + "\"first\":", + "\"name\":" + ).replace( + "\"second\":", + "\"versions\":" + ) + ) + } + + @Suppress("unused") + private class JsonPatch( + val name: String? = null, + val description: String? = null, + val compatiblePackages: Set? = null, + val use: Boolean = true, + 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/src/main/kotlin/app/revanced/patches/all/misc/versioncode/ChangeVersionCodePatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/versioncode/ChangeVersionCodePatch.kt similarity index 52% rename from src/main/kotlin/app/revanced/patches/all/misc/versioncode/ChangeVersionCodePatch.kt rename to patches/src/main/kotlin/app/revanced/patches/all/misc/versioncode/ChangeVersionCodePatch.kt index 2a4e04b29..370200a11 100644 --- a/src/main/kotlin/app/revanced/patches/all/misc/versioncode/ChangeVersionCodePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/versioncode/ChangeVersionCodePatch.kt @@ -1,49 +1,55 @@ package app.revanced.patches.all.misc.versioncode -import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.booleanPatchOption -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption +import app.revanced.patcher.patch.booleanOption +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 -@Patch( +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 options.json. " + + 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, -) -@Suppress("unused") -object ChangeVersionCodePatch : ResourcePatch() { - private const val MAX_VALUE = Int.MAX_VALUE.toString() - - private val ChangeVersionCode by booleanPatchOption( - key = "ChangeVersionCode", +) { + val changeVersionCode by booleanOption( + key = "changeVersionCode", default = false, title = "Change version code", description = "Changes the version code of the app.", required = true ) - private val VersionCode = stringPatchOption( - key = "VersionCode", + 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 + required = true, ) - override fun execute(context: ResourceContext) { - if (ChangeVersionCode == false) { + execute { + if (changeVersionCode == false) { println("INFO: Version code will remain unchanged as 'ChangeVersionCode' is false.") - return + return@execute } - - val versionCodeString = VersionCode.valueOrThrow() + 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 { @@ -56,15 +62,9 @@ object ChangeVersionCodePatch : ResourcePatch() { throw throwVersionCodeException(versionCodeString) } - context.document["AndroidManifest.xml"].use { document -> - val manifestElement = document.getElementsByTagName("manifest").item(0) as Element + document("AndroidManifest.xml").use { document -> + val manifestElement = document.getNode("manifest") as Element manifestElement.setAttribute("android:versionCode", "$versionCode") } } - - private fun throwVersionCodeException(versionCodeString: String): PatchException = - PatchException( - "Invalid versionCode: $versionCodeString, " + - "Version code should be larger than 1 and smaller than $MAX_VALUE." - ) -} +} \ 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/src/main/kotlin/app/revanced/patches/music/actionbar/components/ActionBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/ActionBarComponentsPatch.kt similarity index 65% rename from src/main/kotlin/app/revanced/patches/music/actionbar/components/ActionBarComponentsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/ActionBarComponentsPatch.kt index af0f94b91..3fcca843d 100644 --- a/src/main/kotlin/app/revanced/patches/music/actionbar/components/ActionBarComponentsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/actionbar/components/ActionBarComponentsPatch.kt @@ -1,25 +1,27 @@ package app.revanced.patches.music.actionbar.components -import app.revanced.patcher.data.BytecodeContext 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.actionbar.components.fingerprints.ActionBarComponentFingerprint -import app.revanced.patches.music.actionbar.components.fingerprints.LikeDislikeContainerFingerprint import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE -import app.revanced.patches.music.utils.integrations.Constants.ACTIONBAR_CLASS_DESCRIPTOR -import app.revanced.patches.music.utils.resourceid.SharedResourceIdPatch -import app.revanced.patches.music.utils.resourceid.SharedResourceIdPatch.LikeDislikeContainer +import app.revanced.patches.music.utils.extension.Constants.ACTIONBAR_CLASS_DESCRIPTOR +import app.revanced.patches.music.utils.patch.PatchList.HIDE_ACTION_BAR_COMPONENTS +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.SettingsPatch -import app.revanced.patches.music.video.information.VideoInformationPatch +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.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstructionOrThrow -import app.revanced.util.indexOfFirstWideLiteralInstructionValueOrThrow -import app.revanced.util.patch.BaseBytecodePatch -import app.revanced.util.resultOrThrow +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 @@ -29,24 +31,22 @@ import com.android.tools.smali.dexlib2.iface.reference.MethodReference import kotlin.math.min @Suppress("unused") -object ActionBarComponentsPatch : BaseBytecodePatch( - name = "Hide action bar components", - description = "Adds options to hide action bar components and replace the offline download button with an external download button.", - dependencies = setOf( - SettingsPatch::class, - SharedResourceIdPatch::class, - VideoInformationPatch::class - ), - compatiblePackages = COMPATIBLE_PACKAGE, - fingerprints = setOf( - ActionBarComponentFingerprint, - LikeDislikeContainerFingerprint - ) +val actionBarComponentsPatch = bytecodePatch( + HIDE_ACTION_BAR_COMPONENTS.title, + HIDE_ACTION_BAR_COMPONENTS.summary, ) { - override fun execute(context: BytecodeContext) { - ActionBarComponentFingerprint.resultOrThrow().let { - it.mutableMethod.apply { + compatibleWith(COMPATIBLE_PACKAGE) + dependsOn( + settingsPatch, + sharedResourceIdPatch, + videoInformationPatch, + ) + + execute { + + actionBarComponentFingerprint.matchOrThrow().let { + it.method.apply { // hook download button val addViewIndex = indexOfFirstInstructionOrThrow { opcode == Opcode.INVOKE_VIRTUAL && @@ -63,14 +63,14 @@ object ActionBarComponentsPatch : BaseBytecodePatch( // 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;") + opcode == Opcode.INVOKE_DIRECT && + reference.endsWith("(Landroid/content/Context;)V") && + !reference.contains("Lcom/google/android/libraries/youtube/common/ui/YouTubeButton;") } - 2 val replaceIndex = indexOfFirstInstructionOrThrow { - val reference = (this as? ReferenceInstruction)?.reference.toString() - opcode == Opcode.INVOKE_DIRECT - && reference.endsWith("Lcom/google/android/libraries/youtube/common/ui/YouTubeButton;->(Landroid/content/Context;)V") + 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 @@ -110,11 +110,11 @@ object ActionBarComponentsPatch : BaseBytecodePatch( removeInstruction(spannedIndex) // set action button identifier - val buttonTypeDownloadIndex = it.scanResult.patternScanResult!!.startIndex + 1 + val buttonTypeDownloadIndex = it.patternMatch!!.startIndex + 1 val buttonTypeDownloadRegister = getInstruction(buttonTypeDownloadIndex).registerA - val buttonTypeIndex = it.scanResult.patternScanResult!!.endIndex - 1 + val buttonTypeIndex = it.patternMatch!!.endIndex - 1 val buttonTypeRegister = getInstruction(buttonTypeIndex).registerA @@ -130,64 +130,64 @@ object ActionBarComponentsPatch : BaseBytecodePatch( } } - LikeDislikeContainerFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val insertIndex = - indexOfFirstWideLiteralInstructionValueOrThrow(LikeDislikeContainer) + 2 - val insertRegister = getInstruction(insertIndex).registerA + 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" - ) - } + addInstruction( + insertIndex + 1, + "invoke-static {v$insertRegister}, $ACTIONBAR_CLASS_DESCRIPTOR->hideLikeDislikeButton(Landroid/view/View;)V" + ) } - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.ACTION_BAR, "revanced_hide_action_button_like_dislike", "false" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.ACTION_BAR, "revanced_hide_action_button_comment", "false" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.ACTION_BAR, "revanced_hide_action_button_add_to_playlist", "false" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.ACTION_BAR, "revanced_hide_action_button_download", "false" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.ACTION_BAR, "revanced_hide_action_button_share", "false" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.ACTION_BAR, "revanced_hide_action_button_radio", "false" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.ACTION_BAR, "revanced_hide_action_button_label", "false" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.ACTION_BAR, "revanced_external_downloader_action", "false" ) - SettingsPatch.addPreferenceWithIntent( + addPreferenceWithIntent( CategoryType.ACTION_BAR, "revanced_external_downloader_package_name", "revanced_external_downloader_action" ) + updatePatchStatus(HIDE_ACTION_BAR_COMPONENTS) + } -} \ No newline at end of file +} 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..e53e7db0c --- /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( + baseAdsPatch("$ADS_PATH/MusicAdsPatch;", "hideMusicAds"), + lithoFilterPatch, + navigationBarComponentsPatch, // for 'Hide upgrade button' setting + sharedResourceIdPatch, + settingsPatch, + ) + + 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/src/main/kotlin/app/revanced/patches/music/flyoutmenu/components/FlyoutMenuComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/flyoutmenu/components/FlyoutMenuComponentsPatch.kt similarity index 51% rename from src/main/kotlin/app/revanced/patches/music/flyoutmenu/components/FlyoutMenuComponentsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/music/flyoutmenu/components/FlyoutMenuComponentsPatch.kt index 77c31d0b4..da0c53faa 100644 --- a/src/main/kotlin/app/revanced/patches/music/flyoutmenu/components/FlyoutMenuComponentsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/flyoutmenu/components/FlyoutMenuComponentsPatch.kt @@ -1,40 +1,41 @@ package app.revanced.patches.music.flyoutmenu.components -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.bytecodePatch +import app.revanced.patcher.patch.resourcePatch import app.revanced.patcher.util.smali.ExternalLabel -import app.revanced.patches.music.flyoutmenu.components.fingerprints.EndButtonsContainerFingerprint -import app.revanced.patches.music.flyoutmenu.components.fingerprints.MenuItemFingerprint -import app.revanced.patches.music.flyoutmenu.components.fingerprints.ScreenWidthFingerprint -import app.revanced.patches.music.flyoutmenu.components.fingerprints.ScreenWidthParentFingerprint -import app.revanced.patches.music.flyoutmenu.components.fingerprints.SleepTimerFingerprint -import app.revanced.patches.music.flyoutmenu.components.fingerprints.TouchOutsideFingerprint -import app.revanced.patches.music.flyoutmenu.components.fingerprints.TrimSilenceConfigFingerprint -import app.revanced.patches.music.flyoutmenu.components.fingerprints.TrimSilenceSwitchFingerprint import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE -import app.revanced.patches.music.utils.flyoutmenu.FlyoutMenuHookPatch -import app.revanced.patches.music.utils.integrations.Constants.COMPONENTS_PATH -import app.revanced.patches.music.utils.integrations.Constants.FLYOUT_CLASS_DESCRIPTOR -import app.revanced.patches.music.utils.resourceid.SharedResourceIdPatch -import app.revanced.patches.music.utils.resourceid.SharedResourceIdPatch.EndButtonsContainer -import app.revanced.patches.music.utils.resourceid.SharedResourceIdPatch.TrimSilenceSwitch +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.SettingsPatch -import app.revanced.patches.music.utils.videotype.VideoTypeHookPatch -import app.revanced.patches.music.video.information.VideoInformationPatch -import app.revanced.patches.shared.litho.LithoFilterPatch -import app.revanced.util.alsoResolve +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.indexOfFirstWideLiteralInstructionValueOrThrow -import app.revanced.util.injectLiteralInstructionBooleanCall -import app.revanced.util.patch.BaseBytecodePatch -import app.revanced.util.resultOrThrow +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 @@ -42,43 +43,58 @@ 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 -@Suppress("unused") -object FlyoutMenuComponentsPatch : BaseBytecodePatch( - name = "Flyout menu components", - description = "Adds options to hide or change flyout menu components.", - dependencies = setOf( - FlyoutMenuComponentsResourcePatch::class, - FlyoutMenuHookPatch::class, - LithoFilterPatch::class, - SettingsPatch::class, - SharedResourceIdPatch::class, - VideoInformationPatch::class, - VideoTypeHookPatch::class - ), - compatiblePackages = COMPATIBLE_PACKAGE, - fingerprints = setOf( - EndButtonsContainerFingerprint, - MenuItemFingerprint, - ScreenWidthParentFingerprint, - SleepTimerFingerprint, - TouchOutsideFingerprint, - TrimSilenceConfigFingerprint, - TrimSilenceSwitchFingerprint - ) -) { - private const val FILTER_CLASS_DESCRIPTOR = - "$COMPONENTS_PATH/PlayerFlyoutMenuFilter;" +private val resourceFileArray = arrayOf( + "yt_outline_play_arrow_half_circle_black_24" +).map { "$it.png" }.toTypedArray() - override fun execute(context: BytecodeContext) { +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( + flyoutMenuComponentsResourcePatch, + flyoutMenuHookPatch, + lithoFilterPatch, + sharedResourceIdPatch, + settingsPatch, + versionCheckPatch, + videoInformationPatch, + videoTypeHookPatch, + ) + + execute { var trimSilenceIncluded = false // region patch for enable compact dialog - ScreenWidthFingerprint.alsoResolve( - context, ScreenWidthParentFingerprint - ).let { - it.mutableMethod.apply { - val index = it.scanResult.patternScanResult!!.startIndex + screenWidthFingerprint.matchOrThrow(screenWidthParentFingerprint).let { + it.method.apply { + val index = it.patternMatch!!.startIndex val register = getInstruction(index).registerA addInstructions( @@ -94,51 +110,48 @@ object FlyoutMenuComponentsPatch : BaseBytecodePatch( // region patch for enable trim silence - TrimSilenceConfigFingerprint.result?.let { - TrimSilenceConfigFingerprint.injectLiteralInstructionBooleanCall( - 45619123, + if (trimSilenceConfigFingerprint.resolvable()) { + trimSilenceConfigFingerprint.injectLiteralInstructionBooleanCall( + 45619123L, "$FLYOUT_CLASS_DESCRIPTOR->enableTrimSilence(Z)Z" ) - TrimSilenceSwitchFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val constIndex = - indexOfFirstWideLiteralInstructionValueOrThrow(TrimSilenceSwitch) - val onCheckedChangedListenerIndex = - indexOfFirstInstructionOrThrow(constIndex, Opcode.INVOKE_DIRECT) - val onCheckedChangedListenerReference = - getInstruction(onCheckedChangedListenerIndex).reference - val onCheckedChangedListenerDefiningClass = - (onCheckedChangedListenerReference as MethodReference).definingClass + 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 - context.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(context, 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 - """ - ) + 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 } @@ -146,11 +159,11 @@ object FlyoutMenuComponentsPatch : BaseBytecodePatch( // region patch for hide flyout menu components and replace menu - MenuItemFingerprint.resultOrThrow().let { - it.mutableMethod.apply { + menuItemFingerprint.matchOrThrow().let { + it.method.apply { val freeIndex = indexOfFirstInstructionOrThrow(Opcode.OR_INT_LIT16) - val textViewIndex = it.scanResult.patternScanResult!!.startIndex - val imageViewIndex = it.scanResult.patternScanResult!!.endIndex + val textViewIndex = it.patternMatch!!.startIndex + val imageViewIndex = it.patternMatch!!.endIndex val freeRegister = getInstruction(freeIndex).registerA @@ -174,40 +187,36 @@ object FlyoutMenuComponentsPatch : BaseBytecodePatch( move-result v$freeRegister if-nez v$freeRegister, :hide """, - ExternalLabel("hide", getInstruction(implementation!!.instructions.size - 1)) + ExternalLabel("hide", getInstruction(implementation!!.instructions.lastIndex)) ) } } - TouchOutsideFingerprint.resultOrThrow().let { - it.mutableMethod.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" - ) + 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.resultOrThrow().let { - it.mutableMethod.apply { - val startIndex = - indexOfFirstWideLiteralInstructionValueOrThrow(EndButtonsContainer) - val targetIndex = - indexOfFirstInstructionOrThrow(startIndex, Opcode.MOVE_RESULT_OBJECT) - val targetRegister = getInstruction(targetIndex).registerA + 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" - ) - } + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $FLYOUT_CLASS_DESCRIPTOR->hideLikeDislikeContainer(Landroid/view/View;)V" + ) } // endregion @@ -218,9 +227,9 @@ object FlyoutMenuComponentsPatch : BaseBytecodePatch( * Forces sleep timer menu to be enabled. * This method may be desperate in the future. */ - SleepTimerFingerprint.result?.let { - it.mutableMethod.apply { - val insertIndex = implementation!!.instructions.size - 1 + if (sleepTimerFingerprint.resolvable()) { + sleepTimerFingerprint.methodOrThrow().apply { + val insertIndex = implementation!!.instructions.lastIndex val targetRegister = getInstruction(insertIndex).registerA addInstruction( @@ -232,211 +241,214 @@ object FlyoutMenuComponentsPatch : BaseBytecodePatch( // endregion - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_enable_compact_dialog", "true" ) if (trimSilenceIncluded) { - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_enable_trim_silence", "false" ) } - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_like_dislike", "false", false ) - if (SettingsPatch.upward0636) { - LithoFilterPatch.addFilter(FILTER_CLASS_DESCRIPTOR) + if (is_6_36_or_greater) { + addLithoFilter(FILTER_CLASS_DESCRIPTOR) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_3_column_component", "false", false ) } - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_add_to_queue", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_captions", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_delete_playlist", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_dismiss_queue", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_download", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_edit_playlist", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_go_to_album", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_go_to_artist", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_go_to_episode", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_go_to_podcast", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_help", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_play_next", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_quality", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_remove_from_library", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_remove_from_playlist", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_report", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_save_episode_for_later", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_save_to_library", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_save_to_playlist", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_share", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_shuffle_play", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_sleep_timer", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_start_radio", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_stats_for_nerds", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_subscribe", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_hide_flyout_menu_view_song_credit", "false", false ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_replace_flyout_menu_dismiss_queue", "false" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_replace_flyout_menu_dismiss_queue_continue_watch", "true", "revanced_replace_flyout_menu_dismiss_queue" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.FLYOUT, "revanced_replace_flyout_menu_report", "true" ) - SettingsPatch.addSwitchPreference( + 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..310578ff1 --- /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;->getColor(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..9e0a76ad9 --- /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( + lithoFilterPatch, + sharedResourceIdPatch, + settingsPatch, + 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..b7bb4d3a3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/redirection/Fingerprints.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.music.general.redirection + +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;"), + literals = listOf(53465L), + customFingerprint = { method, _ -> + method.name == "onClick" + } +) + 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..118dd8735 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/spoofappversion/SpoofAppVersionPatch.kt @@ -0,0 +1,85 @@ +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.COMPATIBLE_PACKAGE +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_18_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.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_18_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(COMPATIBLE_PACKAGE) + + dependsOn( + spoofAppVersionBytecodePatch, + oldStyleLibraryShelfPatch, + settingsPatch, + versionCheckPatch, + ) + + execute { + if (is_7_18_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/src/main/kotlin/app/revanced/patches/music/general/startpage/ChangeStartPagePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/startpage/ChangeStartPagePatch.kt similarity index 55% rename from src/main/kotlin/app/revanced/patches/music/general/startpage/ChangeStartPagePatch.kt rename to patches/src/main/kotlin/app/revanced/patches/music/general/startpage/ChangeStartPagePatch.kt index 06a05633f..ac6fb70e0 100644 --- a/src/main/kotlin/app/revanced/patches/music/general/startpage/ChangeStartPagePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/startpage/ChangeStartPagePatch.kt @@ -1,31 +1,33 @@ package app.revanced.patches.music.general.startpage -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.patches.music.general.startpage.fingerprints.ColdStartUpFingerprint +import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE -import app.revanced.patches.music.utils.integrations.Constants.GENERAL_CLASS_DESCRIPTOR +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.SettingsPatch -import app.revanced.util.patch.BaseBytecodePatch -import app.revanced.util.resultOrThrow +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") -object ChangeStartPagePatch : BaseBytecodePatch( - name = "Change start page", - description = "Adds an option to set which page the app opens in instead of the homepage.", - dependencies = setOf(SettingsPatch::class), - compatiblePackages = COMPATIBLE_PACKAGE, - fingerprints = setOf(ColdStartUpFingerprint) +val changeStartPagePatch = bytecodePatch( + CHANGE_START_PAGE.title, + CHANGE_START_PAGE.summary, ) { - override fun execute(context: BytecodeContext) { + compatibleWith(COMPATIBLE_PACKAGE) - ColdStartUpFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val targetIndex = it.scanResult.patternScanResult!!.endIndex + dependsOn(settingsPatch) + + execute { + + coldStartUpFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.endIndex val targetRegister = getInstruction(targetIndex).registerA addInstructions( @@ -39,10 +41,12 @@ object ChangeStartPagePatch : BaseBytecodePatch( } } - SettingsPatch.addPreferenceWithIntent( + addPreferenceWithIntent( CategoryType.GENERAL, "revanced_change_start_page" ) + updatePatchStatus(CHANGE_START_PAGE) + } } diff --git a/src/main/kotlin/app/revanced/patches/music/general/startpage/fingerprints/ColdStartUpFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/music/general/startpage/Fingerprints.kt similarity index 64% rename from src/main/kotlin/app/revanced/patches/music/general/startpage/fingerprints/ColdStartUpFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/music/general/startpage/Fingerprints.kt index cfd4a9179..613adde88 100644 --- a/src/main/kotlin/app/revanced/patches/music/general/startpage/fingerprints/ColdStartUpFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/general/startpage/Fingerprints.kt @@ -1,11 +1,12 @@ -package app.revanced.patches.music.general.startpage.fingerprints +package app.revanced.patches.music.general.startpage -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint +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 object ColdStartUpFingerprint : MethodFingerprint( +internal val coldStartUpFingerprint = legacyFingerprint( + name = "coldStartUpFingerprint", returnType = "Ljava/lang/String;", accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, parameters = emptyList(), @@ -16,3 +17,4 @@ internal object ColdStartUpFingerprint : MethodFingerprint( ), strings = listOf("FEmusic_library_sideloaded_tracks", "FEmusic_home") ) + diff --git a/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 similarity index 64% rename from src/main/kotlin/app/revanced/patches/music/layout/branding/icon/CustomBrandingIconPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/music/layout/branding/icon/CustomBrandingIconPatch.kt index d52981c9c..19e00fbcb 100644 --- a/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 @@ -1,92 +1,99 @@ package app.revanced.patches.music.layout.branding.icon -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.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.SettingsPatch +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.copyResources import app.revanced.util.getResourceGroup -import app.revanced.util.patch.BaseResourcePatch import app.revanced.util.underBarOrThrow import org.w3c.dom.Element import java.io.File import java.nio.file.Files import java.nio.file.StandardCopyOption -@Suppress("DEPRECATION", "unused") -object CustomBrandingIconPatch : BaseResourcePatch( - name = "Custom branding icon for YouTube Music", - description = "Changes the YouTube Music app icon to the icon specified in options.json.", - compatiblePackages = COMPATIBLE_PACKAGE +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 = "revancify_blue" + +private val availableIcon = mapOf( + "AFN Blue" to "afn_blue", + "AFN Red" to "afn_red", + "MMT" to "mmt", + "Revancify Blue" to DEFAULT_ICON, + "Revancify Red" to "revancify_red", + "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, ) { - 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 = "revancify_blue" + compatibleWith(COMPATIBLE_PACKAGE) - private val availableIcon = mapOf( - "AFN Blue" to "afn_blue", - "AFN Red" to "afn_red", - "MMT" to "mmt", - "Revancify Blue" to DEFAULT_ICON, - "Revancify Red" to "revancify_red", - "YouTube Music" to "youtube_music" + dependsOn( + settingsPatch, + versionCheckPatch, ) - 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 drawableDirectories = sizeArray.map { "drawable-$it" } - - 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) - - val AppIcon = stringPatchOption( - key = "AppIcon", + val appIconOption = stringOption( + key = "appIcon", default = DEFAULT_ICON, values = availableIcon, title = "App icon", @@ -101,19 +108,19 @@ object CustomBrandingIconPatch : BaseResourcePatch( ${launcherIconResourceFileNames.joinToString("\n") { "- $it" }} """.trimIndentMultiline(), - required = true + required = true, ) - private val ChangeSplashIcon by booleanPatchOption( - key = "ChangeSplashIcon", + val changeSplashIconOption by booleanOption( + key = "changeSplashIcon", default = true, title = "Change splash icons", description = "Apply the custom branding icon to the splash screen.", required = true ) - private val RestoreOldSplashIcon by booleanPatchOption( - key = "RestoreOldSplashIcon", + val restoreOldSplashIconOption by booleanOption( + key = "restoreOldSplashIcon", default = false, title = "Restore old splash icon", description = """ @@ -123,24 +130,23 @@ object CustomBrandingIconPatch : BaseResourcePatch( Old style splash icon will appear first and then the Cairo splash animation will start. """.trimIndentMultiline(), - required = true + required = true, ) - override fun execute(context: ResourceContext) { - + execute { // Check patch options first. - val appIcon = AppIcon - .underBarOrThrow() + val 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)) { launcherIconResourceGroups.let { resourceGroups -> try { val path = File(appIcon) - val resourceDirectory = context["res"] resourceGroups.forEach { group -> val fromDirectory = path.resolve(group.resourceDirectoryName) @@ -163,7 +169,7 @@ object CustomBrandingIconPatch : BaseResourcePatch( // Change launcher icon. launcherIconResourceGroups.let { resourceGroups -> resourceGroups.forEach { - context.copyResources("$appIconResourcePath/launcher", it) + copyResources("$appIconResourcePath/launcher", it) } } @@ -174,15 +180,15 @@ object CustomBrandingIconPatch : BaseResourcePatch( "ic_app_icons_themed_youtube_music.xml" ) ).forEach { resourceGroup -> - context.copyResources("$appIconResourcePath/monochrome", resourceGroup) + copyResources("$appIconResourcePath/monochrome", resourceGroup) } // Change splash icon. - if (RestoreOldSplashIcon == true) { + if (restoreOldSplashIconOption == true) { var oldSplashIconNotExists: Boolean - context.xmlEditor["res/drawable/splash_screen.xml"].use { editor -> - editor.file.apply { + document("res/drawable/splash_screen.xml").use { document -> + document.apply { val node = getElementsByTagName("layer-list").item(0) oldSplashIconNotExists = (node as Element) .getElementsByTagName("item") @@ -204,7 +210,7 @@ object CustomBrandingIconPatch : BaseResourcePatch( if (oldSplashIconNotExists) { splashIconResourceGroups.let { resourceGroups -> resourceGroups.forEach { - context.copyResources( + copyResources( "$youtubeMusicIconResourcePath/splash", it, createDirectoryIfNotExist = true @@ -215,13 +221,13 @@ object CustomBrandingIconPatch : BaseResourcePatch( } // Change splash icon. - if (ChangeSplashIcon == true) { + if (changeSplashIconOption == true) { // Some resources have been removed in the latest YouTube Music. // For compatibility, use try...catch. try { splashIconResourceGroups.let { resourceGroups -> resourceGroups.forEach { - context.copyResources("$appIconResourcePath/splash", it) + copyResources("$appIconResourcePath/splash", it) } } } catch (_: Exception) { @@ -231,19 +237,20 @@ object CustomBrandingIconPatch : BaseResourcePatch( setIconType(appIcon) } + updatePatchStatus(CUSTOM_BRANDING_ICON_FOR_YOUTUBE_MUSIC) + // region fix app icon - if (!SettingsPatch.upward0723) { - return + if (!is_7_23_or_greater) { + return@execute } if (appIcon == "youtube_music") { - return + return@execute } fun getAdaptiveIconResourceFile(tag: String): String { - context.xmlEditor["res/mipmap-anydpi/ic_launcher_release.xml"].use { editor -> - val adaptiveIcon = editor - .file + document("res/mipmap-anydpi/ic_launcher_release.xml").use { document -> + val adaptiveIcon = document .getElementsByTagName("adaptive-icon") .item(0) as Element @@ -263,7 +270,7 @@ object CustomBrandingIconPatch : BaseResourcePatch( ADAPTIVE_ICON_FOREGROUND_FILE_NAME to getAdaptiveIconResourceFile("foreground") ).forEach { (oldIconResourceFile, newIconResourceFile) -> mipmapDirectories.forEach { - val mipmapDirectory = context["res"].resolve(it) + val mipmapDirectory = resourceDirectory.resolve(it) Files.move( mipmapDirectory .resolve("$oldIconResourceFile.png") @@ -277,6 +284,5 @@ object CustomBrandingIconPatch : BaseResourcePatch( } // endregion - } } diff --git a/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 similarity index 58% rename from src/main/kotlin/app/revanced/patches/music/layout/branding/name/CustomBrandingNamePatch.kt rename to patches/src/main/kotlin/app/revanced/patches/music/layout/branding/name/CustomBrandingNamePatch.kt index 96f79b686..01b6eb823 100644 --- a/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 @@ -1,23 +1,28 @@ package app.revanced.patches.music.layout.branding.name -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption +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.shared.elements.StringsElementsUtils.removeStringsElements -import app.revanced.util.patch.BaseResourcePatch +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 -@Suppress("DEPRECATION", "unused") -object CustomBrandingNamePatch : BaseResourcePatch( - name = "Custom branding name for YouTube Music", - description = "Renames the YouTube Music app to the name specified in options.json.", - compatiblePackages = COMPATIBLE_PACKAGE -) { - private const val APP_NAME_NOTIFICATION = "ReVanced Extended Music" - private const val APP_NAME_LAUNCHER = "RVX Music" +private const val APP_NAME_NOTIFICATION = "ReVanced Extended Music" +private const val APP_NAME_LAUNCHER = "RVX Music" - private val AppNameNotification = stringPatchOption( - key = "AppNameNotification", +@Suppress("unused") +val customBrandingNamePatch = resourcePatch( + CUSTOM_BRANDING_NAME_FOR_YOUTUBE_MUSIC.title, + CUSTOM_BRANDING_NAME_FOR_YOUTUBE_MUSIC.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + val appNameNotificationOption = stringOption( + key = "appNameNotification", default = APP_NAME_LAUNCHER, values = mapOf( "ReVanced Extended Music" to APP_NAME_NOTIFICATION, @@ -30,8 +35,8 @@ object CustomBrandingNamePatch : BaseResourcePatch( required = true ) - private val AppNameLauncher = stringPatchOption( - key = "AppNameLauncher", + val appNameLauncherOption = stringOption( + key = "appNameLauncher", default = APP_NAME_LAUNCHER, values = mapOf( "ReVanced Extended Music" to APP_NAME_NOTIFICATION, @@ -44,21 +49,18 @@ object CustomBrandingNamePatch : BaseResourcePatch( required = true ) - override fun execute(context: ResourceContext) { - + execute { // Check patch options first. - val notificationName = AppNameNotification + val notificationName = appNameNotificationOption .valueOrThrow() - val launcherName = AppNameLauncher + val launcherName = appNameLauncherOption .valueOrThrow() - context.removeStringsElements( + removeStringsElements( arrayOf("app_launcher_name", "app_name") ) - context.xmlEditor["res/values/strings.xml"].use { editor -> - val document = editor.file - + document("res/values/strings.xml").use { document -> mapOf( "app_name" to notificationName, "app_launcher_name" to launcherName @@ -72,5 +74,8 @@ object CustomBrandingNamePatch : BaseResourcePatch( .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..3be7d2c93 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/header/ChangeHeaderPatch.kt @@ -0,0 +1,176 @@ +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.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.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. + val customHeader = customHeaderOption + .valueOrThrow() + + val customBrandingIconType = getIconType() + val customBrandingIconIncluded = customBrandingIconType != "default" + + val warnings = "WARNING: Invalid header path: $customHeader. Does not apply patches." + + if (customHeader != DEFAULT_HEADER_VALUE) { + copyFile( + headerIconResourceGroups, + customHeader, + warnings + ) + } else if (customBrandingIconIncluded) { + headerIconResourceGroups.let { resourceGroups -> + resourceGroups.forEach { + copyResources("music/branding/$customBrandingIconType/header", it) + } + } + } else { + println(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..f11adc92a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/visual/VisualPreferencesIconsPatch.kt @@ -0,0 +1,154 @@ +package app.revanced.patches.music.layout.visual + +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.ResourceGroup +import app.revanced.util.copyResources +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" + +@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, + ) + + execute { + // Check patch options first. + val selectedIconType = settingsMenuIconOption + .underBarOrThrow() + + val appIconOption = customBrandingIconPatch + .getStringOptionValue("appIcon") + + val customBrandingIconType = appIconOption + .underBarOrThrow() + + // region copy shared resources. + + arrayOf( + ResourceGroup( + "drawable", + *preferenceKey.map { it + "_icon.xml" }.toTypedArray() + ), + ).forEach { resourceGroup -> + copyResources("music/visual/shared", resourceGroup) + } + + // endregion. + + // region copy RVX settings menu icon. + + val iconPath = when (selectedIconType) { + "custom_branding_icon" -> "music/branding/$customBrandingIconType/settings" + else -> "music/visual/icons/$selectedIconType" + } + val resourceGroup = ResourceGroup( + "drawable", + "revanced_extended_settings_icon.xml" + ) + + try { + copyResources(iconPath, resourceGroup) + } catch (_: Exception) { + // Ignore if resource copy fails + } + + // 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 -> title + "_icon" + else -> null + } + + drawableName?.let { + node.setAttribute("android:icon", "@drawable/$it") + } + } + } + } + + // endregion. + } +} + + +// region preference key and icon. + +private val preferenceKey = setOf( + // YouTube settings. + "pref_key_parent_tools", + "settings_header_general", + "settings_header_playback", + "settings_header_data_saving", + "settings_header_downloads_and_storage", + "settings_header_notifications", + "settings_header_privacy_and_location", + "settings_header_recommendations", + "settings_header_paid_memberships", + "settings_header_about_youtube_music", + + // RVX settings. + "revanced_extended_settings", + + "revanced_preference_screen_account", + "revanced_preference_screen_action_bar", + "revanced_preference_screen_ads", + "revanced_preference_screen_flyout", + "revanced_preference_screen_general", + "revanced_preference_screen_navigation", + "revanced_preference_screen_player", + "revanced_preference_screen_settings", + "revanced_preference_screen_video", + "revanced_preference_screen_ryd", + "revanced_preference_screen_return_youtube_username", + "revanced_preference_screen_sb", + "revanced_preference_screen_misc", +) + +// endregion. + + diff --git a/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt similarity index 53% rename from src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt index a2a3f7043..4e0e2b1bd 100644 --- a/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt @@ -1,44 +1,38 @@ package app.revanced.patches.music.misc.backgroundplayback -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.patches.music.misc.backgroundplayback.fingerprints.BackgroundPlaybackManagerFingerprint -import app.revanced.patches.music.misc.backgroundplayback.fingerprints.DataSavingSettingsFragmentFingerprint -import app.revanced.patches.music.misc.backgroundplayback.fingerprints.KidsBackgroundPlaybackPolicyControllerFingerprint -import app.revanced.patches.music.misc.backgroundplayback.fingerprints.MusicBrowserServiceFingerprint -import app.revanced.patches.music.misc.backgroundplayback.fingerprints.PodCastConfigFingerprint +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 app.revanced.util.patch.BaseBytecodePatch -import app.revanced.util.resultOrThrow 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") -object BackgroundPlaybackPatch : BaseBytecodePatch( - name = "Remove background playback restrictions", - description = "Removes restrictions on background playback, including for kids videos.", - compatiblePackages = COMPATIBLE_PACKAGE, - fingerprints = setOf( - BackgroundPlaybackManagerFingerprint, - DataSavingSettingsFragmentFingerprint, - KidsBackgroundPlaybackPolicyControllerFingerprint, - MusicBrowserServiceFingerprint, - PodCastConfigFingerprint, - ) +val backgroundPlaybackPatch = bytecodePatch( + REMOVE_BACKGROUND_PLAYBACK_RESTRICTIONS.title, + REMOVE_BACKGROUND_PLAYBACK_RESTRICTIONS.summary, ) { - override fun execute(context: BytecodeContext) { + compatibleWith(COMPATIBLE_PACKAGE) + dependsOn(settingsPatch) + + execute { // region patch for background play - BackgroundPlaybackManagerFingerprint.resultOrThrow().mutableMethod.addInstructions( + backgroundPlaybackManagerFingerprint.methodOrThrow().addInstructions( 0, """ const/4 v0, 0x1 return v0 @@ -50,9 +44,9 @@ object BackgroundPlaybackPatch : BaseBytecodePatch( // region patch for exclusive audio playback // don't play music video - MusicBrowserServiceFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val stringIndex = it.scanResult.stringsScanResult!!.matches.first().index + musicBrowserServiceFingerprint.matchOrThrow().let { + it.method.apply { + val stringIndex = it.stringMatches!!.first().index val targetIndex = indexOfFirstInstructionOrThrow(stringIndex) { val reference = getReference() opcode == Opcode.INVOKE_VIRTUAL && @@ -60,7 +54,7 @@ object BackgroundPlaybackPatch : BaseBytecodePatch( reference.parameterTypes.size == 0 } - getWalkerMethod(context, targetIndex).addInstructions( + getWalkerMethod(targetIndex).addInstructions( 0, """ const/4 v0, 0x1 return v0 @@ -72,16 +66,10 @@ object BackgroundPlaybackPatch : BaseBytecodePatch( // don't play podcast videos // enable by default from YouTube Music 7.05.52+ - val podCastConfigFingerprintResult = PodCastConfigFingerprint.result - val dataSavingSettingsFragmentFingerprintResult = - DataSavingSettingsFragmentFingerprint.result - - val isPatchingOldVersion = - podCastConfigFingerprintResult != null - && dataSavingSettingsFragmentFingerprintResult != null - - if (isPatchingOldVersion) { - podCastConfigFingerprintResult!!.mutableMethod.apply { + if (podCastConfigFingerprint.resolvable() && + dataSavingSettingsFragmentFingerprint.resolvable() + ) { + podCastConfigFingerprint.methodOrThrow().apply { val insertIndex = implementation!!.instructions.size - 1 val targetRegister = getInstruction(insertIndex).registerA @@ -91,7 +79,7 @@ object BackgroundPlaybackPatch : BaseBytecodePatch( ) } - dataSavingSettingsFragmentFingerprintResult!!.mutableMethod.apply { + dataSavingSettingsFragmentFingerprint.methodOrThrow().apply { val insertIndex = indexOfFirstStringInstructionOrThrow("pref_key_dont_play_nma_video") + 4 val targetRegister = getInstruction(insertIndex).registerD @@ -107,11 +95,13 @@ object BackgroundPlaybackPatch : BaseBytecodePatch( // region patch for minimized playback - KidsBackgroundPlaybackPolicyControllerFingerprint.resultOrThrow().mutableMethod.addInstruction( + 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/src/main/kotlin/app/revanced/patches/music/misc/bitrate/BitrateDefaultValuePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/bitrate/BitrateDefaultValuePatch.kt similarity index 54% rename from src/main/kotlin/app/revanced/patches/music/misc/bitrate/BitrateDefaultValuePatch.kt rename to patches/src/main/kotlin/app/revanced/patches/music/misc/bitrate/BitrateDefaultValuePatch.kt index a8b4d4616..c0311d746 100644 --- a/src/main/kotlin/app/revanced/patches/music/misc/bitrate/BitrateDefaultValuePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/bitrate/BitrateDefaultValuePatch.kt @@ -1,20 +1,23 @@ package app.revanced.patches.music.misc.bitrate -import app.revanced.patcher.data.ResourceContext +import app.revanced.patcher.patch.resourcePatch import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE -import app.revanced.util.patch.BaseResourcePatch +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("DEPRECATION", "unused") -object BitrateDefaultValuePatch : BaseResourcePatch( - name = "Bitrate default value", - description = "Sets the audio quality to 'Always High' when you first install the app.", - compatiblePackages = COMPATIBLE_PACKAGE +@Suppress("unused") +val bitrateDefaultValuePatch = resourcePatch( + BITRATE_DEFAULT_VALUE.title, + BITRATE_DEFAULT_VALUE.summary, ) { - private const val RESOURCE_FILE_PATH = "res/xml/data_saving_settings.xml" + compatibleWith(COMPATIBLE_PACKAGE) - override fun execute(context: ResourceContext) { - context.xmlEditor[RESOURCE_FILE_PATH].use { editor -> - editor.file.getElementsByTagName("com.google.android.apps.youtube.music.ui.preference.PreferenceCategoryCompat") + 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) { @@ -31,5 +34,8 @@ object BitrateDefaultValuePatch : BaseResourcePatch( } } } + + 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/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..53de64e8d --- /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( + lithoFilterPatch, + settingsPatch, + 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/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt similarity index 51% rename from src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt index 5b345a176..79779e4d6 100644 --- a/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/splash/CairoSplashAnimationPatch.kt @@ -1,66 +1,65 @@ package app.revanced.patches.music.misc.splash -import app.revanced.patcher.data.BytecodeContext 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.annotation.CompatiblePackage -import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.util.smali.ExternalLabel -import app.revanced.patches.music.misc.splash.fingerprints.CairoSplashAnimationConfigFingerprint -import app.revanced.patches.music.utils.integrations.Constants.MISC_PATH -import app.revanced.patches.music.utils.resourceid.SharedResourceIdPatch -import app.revanced.patches.music.utils.resourceid.SharedResourceIdPatch.MainActivityLaunchAnimation +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.SettingsPatch +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.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.indexOfFirstWideLiteralInstructionValueOrThrow -import app.revanced.util.injectLiteralInstructionBooleanCall -import app.revanced.util.resultOrThrow +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 -@Patch( - name = "Disable Cairo splash animation", - description = "Adds an option to disable Cairo splash animation.", - dependencies = [ - SettingsPatch::class, - SharedResourceIdPatch::class - ], - compatiblePackages = [ - CompatiblePackage( - "com.google.android.apps.youtube.music", - [ - "7.06.54", - "7.16.53", - ] - ) - ] -) +private const val EXTENSION_METHOD_DESCRIPTOR = + "$MISC_PATH/CairoSplashAnimationPatch;->disableCairoSplashAnimation(Z)Z" + @Suppress("unused") -object CairoSplashAnimationPatch : BytecodePatch( - setOf(CairoSplashAnimationConfigFingerprint) +val cairoSplashAnimationPatch = bytecodePatch( + DISABLE_CAIRO_SPLASH_ANIMATION.title, + DISABLE_CAIRO_SPLASH_ANIMATION.summary, ) { - private const val INTEGRATIONS_METHOD_DESCRIPTOR = - "$MISC_PATH/CairoSplashAnimationPatch;->disableCairoSplashAnimation(Z)Z" + compatibleWith( + YOUTUBE_MUSIC_PACKAGE_NAME( + "7.06.54", + "7.16.53", + ), + ) - override fun execute(context: BytecodeContext) { + dependsOn( + settingsPatch, + sharedResourceIdPatch, + versionCheckPatch, + ) - if (!SettingsPatch.upward0706) { + execute { + if (!is_7_06_or_greater) { println("WARNING: This patch is not supported in this version. Use YouTube Music 7.06.54 or later.") - return - } else if (!SettingsPatch.upward0720) { - CairoSplashAnimationConfigFingerprint.injectLiteralInstructionBooleanCall( - 45635386, - INTEGRATIONS_METHOD_DESCRIPTOR + return@execute + } else if (!is_7_20_or_greater) { + cairoSplashAnimationConfigFingerprint.injectLiteralInstructionBooleanCall( + 45635386L, + EXTENSION_METHOD_DESCRIPTOR ) } else { - CairoSplashAnimationConfigFingerprint.resultOrThrow().mutableMethod.apply { - val literalIndex = indexOfFirstWideLiteralInstructionValueOrThrow( - MainActivityLaunchAnimation + cairoSplashAnimationConfigFingerprint.methodOrThrow().apply { + val literalIndex = indexOfFirstLiteralInstructionOrThrow( + mainActivityLaunchAnimation ) val insertIndex = indexOfFirstInstructionReversedOrThrow(literalIndex) { opcode == Opcode.INVOKE_VIRTUAL && @@ -82,7 +81,7 @@ object CairoSplashAnimationPatch : BytecodePatch( addInstructionsWithLabels( insertIndex, """ const/4 v$freeRegister, 0x1 - invoke-static {v$freeRegister}, $INTEGRATIONS_METHOD_DESCRIPTOR + invoke-static {v$freeRegister}, $EXTENSION_METHOD_DESCRIPTOR move-result v$freeRegister if-eqz v$freeRegister, :skip """, ExternalLabel("skip", getInstruction(jumpIndex)) @@ -90,11 +89,13 @@ object CairoSplashAnimationPatch : BytecodePatch( } } - SettingsPatch.addSwitchPreference( + 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/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt similarity index 53% rename from src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt index a2e16a724..55adfbadf 100644 --- a/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/navigation/components/NavigationBarComponentsPatch.kt @@ -1,55 +1,71 @@ package app.revanced.patches.music.navigation.components -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.patch.PatchException -import app.revanced.patches.music.navigation.components.fingerprints.TabLayoutFingerprint -import app.revanced.patches.music.navigation.components.fingerprints.TabLayoutTextFingerprint +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.integrations.Constants.NAVIGATION_CLASS_DESCRIPTOR -import app.revanced.patches.music.utils.resourceid.SharedResourceIdPatch -import app.revanced.patches.music.utils.resourceid.SharedResourceIdPatch.ColorGrey -import app.revanced.patches.music.utils.resourceid.SharedResourceIdPatch.Text1 +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.SettingsPatch +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.indexOfFirstWideLiteralInstructionValueOrThrow -import app.revanced.util.patch.BaseBytecodePatch -import app.revanced.util.resultOrThrow +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.formats.Instruction35c import com.android.tools.smali.dexlib2.iface.reference.MethodReference -@Suppress("DEPRECATION", "SpellCheckingInspection", "unused") -object NavigationBarComponentsPatch : BaseBytecodePatch( - name = "Navigation bar components", - description = "Adds options to hide or change components related to the navigation bar.", - dependencies = setOf( - SettingsPatch::class, - SharedResourceIdPatch::class - ), - compatiblePackages = COMPATIBLE_PACKAGE, - fingerprints = setOf( - TabLayoutFingerprint, - TabLayoutTextFingerprint - ) +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" ) { - private const val FLAG = "android:layout_weight" - private const val RESOURCE_FILE_PATH = "res/layout/image_with_text_tab.xml" + execute { + document(RESOURCE_FILE_PATH).use { document -> + with(document.getElementsByTagName("ImageView").item(0)) { + if (attributes.getNamedItem(FLAG) != null) + return@with - override fun execute(context: BytecodeContext) { + 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.resultOrThrow().mutableMethod.apply { - val constIndex = indexOfFirstWideLiteralInstructionValueOrThrow(ColorGrey) + tabLayoutFingerprint.methodOrThrow().apply { + val constIndex = indexOfFirstLiteralInstructionOrThrow(colorGrey) val insertIndex = indexOfFirstInstructionOrThrow(constIndex) { opcode == Opcode.INVOKE_VIRTUAL && getReference()?.name == "setBackgroundColor" @@ -67,43 +83,28 @@ object NavigationBarComponentsPatch : BaseBytecodePatch( /** * Hide navigation labels */ - TabLayoutTextFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val constIndex = - indexOfFirstWideLiteralInstructionValueOrThrow(Text1) - val targetIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.CHECK_CAST) - val targetParameter = getInstruction(targetIndex).reference - val targetRegister = getInstruction(targetIndex).registerA + 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") + 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" - ) - } - } - - SettingsPatch.contexts.xmlEditor[RESOURCE_FILE_PATH].use { editor -> - val document = editor.file - - with(document.getElementsByTagName("ImageView").item(0)) { - if (attributes.getNamedItem(FLAG) != null) - return@with - - document.createAttribute(FLAG) - .apply { value = "0.5" } - .let(attributes::setNamedItem) - } + addInstruction( + targetIndex + 1, + "invoke-static {v$targetRegister}, $NAVIGATION_CLASS_DESCRIPTOR->hideNavigationLabel(Landroid/widget/TextView;)V" + ) } /** * Hide navigation bar & buttons */ - TabLayoutTextFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val enumIndex = it.scanResult.patternScanResult!!.startIndex + 3 + 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 @@ -111,7 +112,8 @@ object NavigationBarComponentsPatch : BaseBytecodePatch( opcode == Opcode.INVOKE_VIRTUAL && getReference()?.name == "getVisibility" } - val pivotTabRegister = getInstruction(pivotTabIndex).registerC + val pivotTabRegister = + getInstruction(pivotTabIndex).registerC addInstruction( pivotTabIndex, @@ -125,45 +127,48 @@ object NavigationBarComponentsPatch : BaseBytecodePatch( } } - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.NAVIGATION, "revanced_enable_black_navigation_bar", "true" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.NAVIGATION, "revanced_hide_navigation_home_button", "false" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.NAVIGATION, "revanced_hide_navigation_samples_button", "false" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.NAVIGATION, "revanced_hide_navigation_explore_button", "false" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.NAVIGATION, "revanced_hide_navigation_library_button", "false" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.NAVIGATION, "revanced_hide_navigation_upgrade_button", "true" ) - SettingsPatch.addSwitchPreference( + addSwitchPreference( CategoryType.NAVIGATION, "revanced_hide_navigation_bar", "false" ) - SettingsPatch.addSwitchPreference( + 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..ee62ac266 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/player/components/PlayerComponentsPatch.kt @@ -0,0 +1,1077 @@ +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.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( + playerComponentsResourcePatch, + sharedResourceIdPatch, + settingsPatch, + 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 + + repeatTrackFingerprint.matchOrThrow().let { + it.method.apply { + 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 + """ + ) + } + } + + 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" + ) + + 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..83606bd71 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/compatibility/Constants.kt @@ -0,0 +1,19 @@ +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 supported by the RVX patch. + ) + ) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/music/utils/integrations/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/Constants.kt similarity index 81% rename from src/main/kotlin/app/revanced/patches/music/utils/integrations/Constants.kt rename to patches/src/main/kotlin/app/revanced/patches/music/utils/extension/Constants.kt index 4887e8a0b..554e0e0dc 100644 --- a/src/main/kotlin/app/revanced/patches/music/utils/integrations/Constants.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/extension/Constants.kt @@ -1,10 +1,10 @@ -package app.revanced.patches.music.utils.integrations +package app.revanced.patches.music.utils.extension @Suppress("MemberVisibilityCanBePrivate") -object Constants { - const val INTEGRATIONS_PATH = "Lapp/revanced/integrations/music" - const val SHARED_PATH = "$INTEGRATIONS_PATH/shared" - const val PATCHES_PATH = "$INTEGRATIONS_PATH/patches" +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" 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..8ef58e42f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/client/SpoofClientPatch.kt @@ -0,0 +1,255 @@ +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.COMPATIBLE_PACKAGE +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.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.createPlayerRequestBodyWithModelFingerprint +import app.revanced.patches.shared.indexOfModelInstruction +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, +) { + dependsOn(settingsPatch) + + compatibleWith(COMPATIBLE_PACKAGE) + + 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 + + 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 + """, + ) + } + } + } + + addSwitchPreference( + CategoryType.MISC, + "revanced_spoof_client", + "false" + ) + + 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/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..a28b0ebfa --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/gms/GmsCoreSupportPatch.kt @@ -0,0 +1,61 @@ +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.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" + ) + + 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..704df7df5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt @@ -0,0 +1,160 @@ +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_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 track 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/src/main/kotlin/app/revanced/patches/music/utils/playertype/fingerprint/PlayerTypeFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/playertype/Fingerprints.kt similarity index 51% rename from src/main/kotlin/app/revanced/patches/music/utils/playertype/fingerprint/PlayerTypeFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/music/utils/playertype/Fingerprints.kt index 9cde15328..8c8847ebb 100644 --- a/src/main/kotlin/app/revanced/patches/music/utils/playertype/fingerprint/PlayerTypeFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/playertype/Fingerprints.kt @@ -1,11 +1,12 @@ -package app.revanced.patches.music.utils.playertype.fingerprint +package app.revanced.patches.music.utils.playertype -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint +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 object PlayerTypeFingerprint : MethodFingerprint( +internal val playerTypeFingerprint = legacyFingerprint( + name = "playerTypeFingerprint", returnType = "V", accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, parameters = listOf("L"), @@ -15,7 +16,7 @@ internal object PlayerTypeFingerprint : MethodFingerprint( Opcode.IPUT_OBJECT, Opcode.RETURN_VOID ), - customFingerprint = { methodDef, _ -> - methodDef.definingClass.endsWith("/MppWatchWhileLayout;") + 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..27115309c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/playservice/VersionCheckPatch.kt @@ -0,0 +1,45 @@ +@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_18_or_greater = false + private set +var is_7_20_or_greater = false + private set +var is_7_23_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_18_or_greater = 243699000 <= playStoreServicesVersion + is_7_20_or_greater = 243899000 <= playStoreServicesVersion + is_7_23_or_greater = 244199000 <= 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..6daf407ae --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubedislike/ReturnYouTubeDislikePatch.kt @@ -0,0 +1,159 @@ +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.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.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 + ) + + 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 + """, + ) + } + + + 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 + """ + ) + } + + 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..c908814c3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/returnyoutubeusername/ReturnYouTubeUsernamePatch.kt @@ -0,0 +1,55 @@ +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, + use = false, +) { + 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 875ccfa44..70c69ed8d 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) { ACCOUNT("account", false), ACTION_BAR("action_bar", false), ADS("ads", 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/src/main/kotlin/app/revanced/patches/music/utils/settings/ResourceUtils.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/settings/ResourceUtils.kt similarity index 65% rename from src/main/kotlin/app/revanced/patches/music/utils/settings/ResourceUtils.kt rename to patches/src/main/kotlin/app/revanced/patches/music/utils/settings/ResourceUtils.kt index aa5a2edf5..4889d098f 100644 --- a/src/main/kotlin/app/revanced/patches/music/utils/settings/ResourceUtils.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/settings/ResourceUtils.kt @@ -1,14 +1,20 @@ package app.revanced.patches.music.utils.settings -import app.revanced.patcher.data.ResourceContext +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 -@Suppress("DEPRECATION") -object ResourceUtils { +internal object ResourceUtils { + private lateinit var context: ResourcePatchContext + + fun setContext(context: ResourcePatchContext) { + this.context = context + } private const val RVX_SETTINGS_KEY = "revanced_extended_settings" @@ -26,7 +32,7 @@ object ResourceUtils { const val ACTIVITY_HOOK_TARGET_CLASS = "com.google.android.gms.common.api.GoogleApiActivity" - var musicPackageName = "com.google.android.apps.youtube.music" + var musicPackageName = YOUTUBE_MUSIC_PACKAGE_NAME private var iconType = "default" fun getIconType() = iconType @@ -43,9 +49,10 @@ object ResourceUtils { return false } - private fun ResourceContext.replacePackageName() { - this[SETTINGS_HEADER_PATH].writeText( - this[SETTINGS_HEADER_PATH].readText() + private fun replacePackageName() = context.apply { + val xmlFile = get(SETTINGS_HEADER_PATH) + xmlFile.writeText( + xmlFile.readText() .replace( "\"com.google.android.apps.youtube.music\"", "\"" + musicPackageName + "\"" @@ -53,6 +60,7 @@ object ResourceUtils { ) } + private fun setPreferenceCategory(newCategory: String) { CategoryType.entries.forEach { preference -> if (newCategory == preference.value) @@ -60,16 +68,18 @@ object ResourceUtils { } } - fun ResourceContext.updatePackageName(newPackage: String) { + fun updatePackageName(newPackage: String) { musicPackageName = newPackage replacePackageName() } - fun ResourceContext.addPreferenceCategory( - category: String - ) { - this.xmlEditor[SETTINGS_HEADER_PATH].use { editor -> - val tags = editor.file.getElementsByTagName(PREFERENCE_SCREEN_TAG_NAME) + 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 { @@ -87,12 +97,12 @@ object ResourceUtils { } } - fun ResourceContext.addPreferenceCategoryUnderPreferenceScreen( + fun addPreferenceCategoryUnderPreferenceScreen( preferenceScreenKey: String, category: String ) { - this.xmlEditor[SETTINGS_HEADER_PATH].use { editor -> - val tags = editor.file.getElementsByTagName(PREFERENCE_SCREEN_TAG_NAME) + 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 { @@ -104,28 +114,12 @@ object ResourceUtils { } } - fun ResourceContext.setPreferenceScreenIcon( + fun sortPreferenceCategory( category: String ) { - this.xmlEditor[SETTINGS_HEADER_PATH].use { editor -> - editor.file.doRecursively loop@{ - if (it !is Element) return@loop - - it.getAttributeNode("android:key")?.let { attribute -> - if (attribute.textContent == "revanced_preference_screen_$category") { - it.cloneNodes(it.parentNode) - } - } - } - } - } - - fun ResourceContext.sortPreferenceCategory( - category: String - ) { - this.xmlEditor[SETTINGS_HEADER_PATH].use { editor -> - editor.file.doRecursively loop@{ - if (it !is Element) return@loop + 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") { @@ -137,14 +131,14 @@ object ResourceUtils { replacePackageName() } - fun ResourceContext.addMicroGPreference( + fun addGmsCorePreference( category: String, key: String, packageName: String, targetClassName: String ) { - this.xmlEditor[SETTINGS_HEADER_PATH].use { editor -> - val tags = editor.file.getElementsByTagName(PREFERENCE_SCREEN_TAG_NAME) + 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") @@ -166,15 +160,15 @@ object ResourceUtils { } } - fun ResourceContext.addSwitchPreference( + fun addSwitchPreference( category: String, key: String, defaultValue: String, dependencyKey: String, setSummary: Boolean ) { - this.xmlEditor[SETTINGS_HEADER_PATH].use { editor -> - val tags = editor.file.getElementsByTagName(PREFERENCE_SCREEN_TAG_NAME) + 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") @@ -195,13 +189,13 @@ object ResourceUtils { } } - fun ResourceContext.addPreferenceWithIntent( + fun addPreferenceWithIntent( category: String, key: String, dependencyKey: String ) { - this.xmlEditor[SETTINGS_HEADER_PATH].use { editor -> - val tags = editor.file.getElementsByTagName(PREFERENCE_SCREEN_TAG_NAME) + 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") @@ -227,37 +221,36 @@ object ResourceUtils { } } - fun ResourceContext.addRVXSettingsPreference() { - this.xmlEditor[SETTINGS_HEADER_PATH].use { editor -> - with(editor.file) { - doRecursively loop@{ - if (it !is Element) return@loop - 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@loop + 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 } } + } - doRecursively loop@{ - if (it !is Element) return@loop + document.doRecursively node@{ + if (it !is Element) return@node - it.getAttributeNode("app:allowDividerBelow")?.let { attribute -> - if (attribute.textContent == "true") { - attribute.textContent = "false" - } + it.getAttributeNode("app:allowDividerBelow")?.let { attribute -> + if (attribute.textContent == "true") { + attribute.textContent = "false" } } } 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..1966122b5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/settings/SettingsPatch.kt @@ -0,0 +1,306 @@ +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.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, + "setDeviceInformation" + ) + injectOnCreateMethodCall( + EXTENSION_INITIALIZATION_CLASS_DESCRIPTOR, + "onCreate" + ) + injectConstructorMethodCall( + EXTENSION_UTILS_CLASS_DESCRIPTOR, + "setActivity" + ) + + } +} + +private const val DEFAULT_LABEL = "ReVanced Extended" +private lateinit var customName: String + +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, + ) + + execute { + /** + * check patch options + */ + customName = settingsLabel + .valueOrThrow() + + /** + * 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) + ResourceUtils.addSwitchPreference(categoryValue, key, defaultValue, dependencyKey, setSummary) +} + +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..ba2b5bd6f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/Fingerprints.kt @@ -0,0 +1,64 @@ +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.IGET_OBJECT, + 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..8eb98a1e6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/sponsorblock/SponsorBlockPatch.kt @@ -0,0 +1,395 @@ +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.ResourcePatchContext +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 = it.patternMatch!!.startIndex + 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 { + 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( + SEGMENTS_CATEGORY_KEY, + "sb_segments_sponsor", + "sb_enabled" + ) + addSegmentsPreference( + SEGMENTS_CATEGORY_KEY, + "sb_segments_selfpromo", + "sb_enabled" + ) + addSegmentsPreference( + SEGMENTS_CATEGORY_KEY, + "sb_segments_interaction", + "sb_enabled" + ) + addSegmentsPreference( + SEGMENTS_CATEGORY_KEY, + "sb_segments_intro", + "sb_enabled" + ) + addSegmentsPreference( + SEGMENTS_CATEGORY_KEY, + "sb_segments_outro", + "sb_enabled" + ) + addSegmentsPreference( + SEGMENTS_CATEGORY_KEY, + "sb_segments_preview", + "sb_enabled" + ) + addSegmentsPreference( + SEGMENTS_CATEGORY_KEY, + "sb_segments_filler", + "sb_enabled" + ) + addSegmentsPreference( + SEGMENTS_CATEGORY_KEY, + "sb_segments_nomusic", + "sb_enabled" + ) + + addPreferenceCategoryUnderPreferenceScreen( + CategoryType.SPONSOR_BLOCK.value, + ABOUT_CATEGORY_KEY + ) + + addAboutPreference( + ABOUT_CATEGORY_KEY, + "sb_about_api", + "https://sponsor.ajay.app" + ) + + get(SETTINGS_HEADER_PATH).apply { + writeText( + readText() + .replace( + "\"sb_segments_nomusic", + "\"sb_segments_music_offtopic" + ) + ) + } + + updatePatchStatus(SPONSORBLOCK) + + } +} + +private fun ResourcePatchContext.addSwitchPreference( + category: String, + key: String, + defaultValue: String +) = addSwitchPreference(category, key, defaultValue, "") + +private fun ResourcePatchContext.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) + } + } + } + } +} + +private fun ResourcePatchContext.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 + ) + } + } + } + } +} + +private fun ResourcePatchContext.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) + } + } + } +} + +private fun ResourcePatchContext.addSegmentsPreference( + preferenceCategoryKey: 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(preferenceCategoryKey) } + .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 + ) + } + } + } + } +} + +private fun ResourcePatchContext.addAboutPreference( + preferenceCategoryKey: String, + key: String, + data: 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(preferenceCategoryKey) } + .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) + } + } + } + } +} + 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/src/main/kotlin/app/revanced/patches/music/video/playback/fingerprints/UserQualityChangeFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/playback/Fingerprints.kt similarity index 59% rename from src/main/kotlin/app/revanced/patches/music/video/playback/fingerprints/UserQualityChangeFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/music/video/playback/Fingerprints.kt index 4335cc5df..e074cd7ee 100644 --- a/src/main/kotlin/app/revanced/patches/music/video/playback/fingerprints/UserQualityChangeFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/video/playback/Fingerprints.kt @@ -1,9 +1,10 @@ -package app.revanced.patches.music.video.playback.fingerprints +package app.revanced.patches.music.video.playback -import app.revanced.patcher.fingerprint.MethodFingerprint +import app.revanced.util.fingerprint.legacyFingerprint import com.android.tools.smali.dexlib2.Opcode -internal object UserQualityChangeFingerprint : MethodFingerprint( +internal val userQualityChangeFingerprint = legacyFingerprint( + name = "userQualityChangeFingerprint", returnType = "V", opcodes = listOf( Opcode.CONST_STRING, 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..9b8d4d5b8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/AdsPatch.kt @@ -0,0 +1,134 @@ +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 com.android.tools.smali.dexlib2.Opcode +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 +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +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( + bannerAdsPatch, + commentAdsPatch, + settingsPatch + ) + + 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. + newAdPostFingerprint.methodOrThrow().apply { + val targetIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.toString() == "Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z" + } + 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..c4218a431 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/Fingerprints.kt @@ -0,0 +1,54 @@ +package app.revanced.patches.reddit.ad + +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 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 = { method, classDef -> + method.definingClass.endsWith("/Listing;") && + method.name == "" && + classDef.sourceFile == "Listing.kt" + }, +) + +internal val newAdPostFingerprint = legacyFingerprint( + name = "newAdPostFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + opcodes = listOf(Opcode.INVOKE_VIRTUAL), + strings = listOf( + "chain", + "feedElement" + ), + customFingerprint = { _, classDef -> classDef.sourceFile == "AdElementConverter.kt" }, +) diff --git a/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 similarity index 62% rename from src/main/kotlin/app/revanced/patches/reddit/layout/branding/name/CustomBrandingNamePatch.kt rename to patches/src/main/kotlin/app/revanced/patches/reddit/layout/branding/name/CustomBrandingNamePatch.kt index 41f6a917e..f32ed7dc2 100644 --- a/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 @@ -1,25 +1,27 @@ package app.revanced.patches.reddit.layout.branding.name -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption +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.util.patch.BaseResourcePatch +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.valueOrThrow import java.io.FileWriter import java.nio.file.Files -@Suppress("DEPRECATION", "unused") -object CustomBrandingNamePatch : BaseResourcePatch( - name = "Custom branding name for Reddit", - description = "Renames the Reddit app to the name specified in options.json.", - compatiblePackages = COMPATIBLE_PACKAGE, - use = false -) { - private const val ORIGINAL_APP_NAME = "Reddit" - private const val APP_NAME = "RVX Reddit" +private const val ORIGINAL_APP_NAME = "Reddit" +private const val APP_NAME = "RVX Reddit" - private val AppName = stringPatchOption( - key = "AppName", +@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, @@ -30,16 +32,16 @@ object CustomBrandingNamePatch : BaseResourcePatch( required = true ) - override fun execute(context: ResourceContext) { - val appName = AppName + execute { + val appName = appNameOption .valueOrThrow() if (appName == ORIGINAL_APP_NAME) { println("INFO: App name will remain unchanged as it matches the original.") - return + return@execute } - val resDirectory = context["res"] + val resDirectory = get("res") val valuesV24Directory = resDirectory.resolve("values-v24") if (!valuesV24Directory.isDirectory) @@ -53,9 +55,7 @@ object CustomBrandingNamePatch : BaseResourcePatch( } } - context.xmlEditor["res/values-v24/strings.xml"].use { editor -> - val document = editor.file - + document("res/values-v24/strings.xml").use { document -> mapOf( "app_name" to appName ).forEach { (k, v) -> @@ -67,5 +67,7 @@ object CustomBrandingNamePatch : BaseResourcePatch( 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..9201291d9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/branding/packagename/ChangePackageNamePatch.kt @@ -0,0 +1,110 @@ +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.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) { + println("INFO: 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..eeb7f8051 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/communities/Fingerprints.kt @@ -0,0 +1,14 @@ +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, + customFingerprint = { method, _ -> + method.definingClass.endsWith("/CommunityRecommendationSection;") + } +) 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..e0cc431d7 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/communities/RecommendedCommunitiesPatch.kt @@ -0,0 +1,44 @@ +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().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..7316791b7 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.reddit.layout.navigation + +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 bottomNavScreenFingerprint = legacyFingerprint( + name = "bottomNavScreenFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ), + customFingerprint = { method, classDef -> + method.name == "onGlobalLayout" && + classDef.type.startsWith("Lcom/reddit/launch/bottomnav/BottomNavScreen\$") + } +) 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..e517df7ec --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/navigation/NavigationButtonsPatch.kt @@ -0,0 +1,45 @@ +package app.revanced.patches.reddit.layout.navigation + +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.HIDE_NAVIGATION_BUTTONS +import app.revanced.patches.reddit.utils.settings.settingsPatch +import app.revanced.patches.reddit.utils.settings.updatePatchStatus +import app.revanced.util.fingerprint.matchOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction + +private const val EXTENSION_METHOD_DESCRIPTOR = + "$PATCHES_PATH/NavigationButtonsPatch;->hideNavigationButtons(Landroid/view/ViewGroup;)V" + +@Suppress("unused") +val navigationButtonsPatch = bytecodePatch( + HIDE_NAVIGATION_BUTTONS.title, + HIDE_NAVIGATION_BUTTONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + execute { + bottomNavScreenFingerprint.matchOrThrow().let { + it.method.apply { + val startIndex = it.patternMatch!!.startIndex + val targetRegister = + getInstruction(startIndex).registerC + + addInstruction( + startIndex + 1, + "invoke-static {v$targetRegister}, $EXTENSION_METHOD_DESCRIPTOR" + ) + } + } + + 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..899ceb2fe --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/premiumicon/Fingerprints.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.reddit.layout.premiumicon + +import app.revanced.util.fingerprint.legacyFingerprint + +internal val premiumIconFingerprint = legacyFingerprint( + name = "premiumIconFingerprint", + returnType = "Z", + customFingerprint = { method, classDef -> + method.definingClass.endsWith("/MyAccount;") && + method.name == "isPremiumSubscriber" && + classDef.sourceFile == "MyAccount.kt" + } +) 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..f057a104e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/recentlyvisited/Fingerprints.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.reddit.layout.recentlyvisited + +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 communityDrawerPresenterFingerprint = legacyFingerprint( + name = "communityDrawerPresenterFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf(Opcode.AGET), + customFingerprint = { method, _ -> + method.definingClass.endsWith("/CommunityDrawerPresenter;") + } +) 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..1981cb894 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/recentlyvisited/RecentlyVisitedShelfPatch.kt @@ -0,0 +1,80 @@ +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.findMethodOrThrow +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.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +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 communityDrawerPresenterMethod = communityDrawerPresenterFingerprint.methodOrThrow() + val constructorMethod = findMethodOrThrow(communityDrawerPresenterMethod.definingClass) + val recentlyVisitedReference = with(constructorMethod) { + val recentlyVisitedFieldIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "RECENTLY_VISITED" + } + val recentlyVisitedObjectIndex = + indexOfFirstInstructionOrThrow( + recentlyVisitedFieldIndex, + Opcode.IPUT_OBJECT + ) + getInstruction(recentlyVisitedObjectIndex).reference + } + communityDrawerPresenterMethod.apply { + val recentlyVisitedObjectIndex = indexOfFirstInstructionOrThrow { + getReference()?.toString() == recentlyVisitedReference.toString() + } + 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..e5bc11a19 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/Fingerprints.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.reddit.layout.screenshotpopup + +import app.revanced.patches.reddit.utils.resourceid.screenShotShareBanner +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val screenshotTakenBannerFingerprint = legacyFingerprint( + name = "screenshotTakenBannerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(screenShotShareBanner), + customFingerprint = { _, classDef -> + classDef.sourceFile == "ScreenshotTakenBanner.kt" + } +) 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..cf8f02664 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/screenshotpopup/ScreenshotPopupPatch.kt @@ -0,0 +1,47 @@ +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.resourceid.sharedResourceIdPatch +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( + sharedResourceIdPatch, + 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..e4810f9b8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/subredditdialog/Fingerprints.kt @@ -0,0 +1,27 @@ +package app.revanced.patches.reddit.layout.subredditdialog + +import app.revanced.patches.reddit.utils.resourceid.cancelButton +import app.revanced.patches.reddit.utils.resourceid.textAppearanceRedditBaseOldButtonColored +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val frequentUpdatesSheetScreenFingerprint = legacyFingerprint( + name = "frequentUpdatesSheetScreenFingerprint", + returnType = "Landroid/view/View;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(cancelButton), + customFingerprint = { _, classDef -> + classDef.sourceFile == "FrequentUpdatesSheetScreen.kt" + } +) + +internal val redditAlertDialogsFingerprint = legacyFingerprint( + name = "redditAlertDialogsFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(textAppearanceRedditBaseOldButtonColored), + customFingerprint = { _, classDef -> + classDef.sourceFile == "RedditAlertDialogs.kt" + } +) \ 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..2684e5fbc --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/subredditdialog/SubRedditDialogPatch.kt @@ -0,0 +1,65 @@ +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.resourceid.cancelButton +import app.revanced.patches.reddit.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.reddit.utils.resourceid.textAppearanceRedditBaseOldButtonColored +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.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +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( + sharedResourceIdPatch, + settingsPatch + ) + + execute { + frequentUpdatesSheetScreenFingerprint.methodOrThrow().apply { + val cancelButtonViewIndex = + indexOfFirstLiteralInstructionOrThrow(cancelButton) + 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 insertIndex = + indexOfFirstLiteralInstructionOrThrow( + textAppearanceRedditBaseOldButtonColored + ) + 1 + 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..79b69ffb4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/toolbar/Fingerprints.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.reddit.layout.toolbar + +import app.revanced.patches.reddit.utils.resourceid.toolBarNavSearchCtaContainer +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;"), + literals = listOf(toolBarNavSearchCtaContainer), + 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..29c0b517a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/toolbar/ToolBarButtonPatch.kt @@ -0,0 +1,47 @@ +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.resourceid.sharedResourceIdPatch +import app.revanced.patches.reddit.utils.resourceid.toolBarNavSearchCtaContainer +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.indexOfFirstLiteralInstructionOrThrow +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( + sharedResourceIdPatch, + settingsPatch + ) + + execute { + homePagerScreenFingerprint.methodOrThrow().apply { + val targetIndex = + indexOfFirstLiteralInstructionOrThrow(toolBarNavSearchCtaContainer) + 3 + val targetRegister = + getInstruction(targetIndex - 1).registerA + + addInstruction( + targetIndex, + "invoke-static {v$targetRegister}, $EXTENSION_METHOD_DESCRIPTOR" + ) + } + + updatePatchStatus( + "enableToolBarButton", + HIDE_TOOLBAR_BUTTON + ) + } +} diff --git a/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/fingerprints/ScreenNavigatorFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/Fingerprints.kt similarity index 65% rename from src/main/kotlin/app/revanced/patches/reddit/misc/openlink/fingerprints/ScreenNavigatorFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/Fingerprints.kt index deee7c05b..108e0d742 100644 --- a/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/fingerprints/ScreenNavigatorFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/Fingerprints.kt @@ -1,11 +1,12 @@ -package app.revanced.patches.reddit.misc.openlink.fingerprints +package app.revanced.patches.reddit.misc.openlink -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint +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 object ScreenNavigatorFingerprint : MethodFingerprint( +internal val screenNavigatorFingerprint = legacyFingerprint( + name = "screenNavigatorFingerprint", returnType = "V", accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, opcodes = listOf( @@ -16,4 +17,4 @@ internal object ScreenNavigatorFingerprint : MethodFingerprint( ), strings = listOf("activity", "uri"), customFingerprint = { _, classDef -> classDef.sourceFile == "RedditScreenNavigator.kt" } -) \ No newline at end of file +) 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..d9b0e6728 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/OpenLinksDirectlyPatch.kt @@ -0,0 +1,39 @@ +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 +import app.revanced.util.fingerprint.methodOrThrow + +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) + + execute { + screenNavigatorFingerprint.methodOrThrow().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..781c0e48c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/openlink/OpenLinksExternallyPatch.kt @@ -0,0 +1,48 @@ +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.fingerprint.methodOrThrow +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) + + execute { + screenNavigatorFingerprint.methodOrThrow().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/tracking/url/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/tracking/url/Fingerprints.kt new file mode 100644 index 000000000..739c84cf3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/tracking/url/Fingerprints.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.reddit.misc.tracking.url + +import app.revanced.util.fingerprint.legacyFingerprint + +internal val shareLinkFormatterFingerprint = legacyFingerprint( + name = "shareLinkFormatterFingerprint", + returnType = "Ljava/lang/String;", + parameters = listOf("Ljava/lang/String;", "Ljava/util/Map;"), + customFingerprint = { method, classDef -> + method.definingClass.startsWith("Lcom/reddit/sharing/") && + classDef.sourceFile == "UrlUtil.kt" + } +) 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..450b2521f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/compatibility/Constants.kt @@ -0,0 +1,14 @@ +package app.revanced.patches.reddit.utils.compatibility + +import app.revanced.patcher.patch.PackageName +import app.revanced.patcher.patch.VersionName + +internal object Constants { + val COMPATIBLE_PACKAGE: Pair?> = Pair( + "com.reddit.frontpage", + setOf( + "2023.12.0", + "2024.17.0" + ) + ) +} \ 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/resourceid/SharedResourceIdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatch.kt new file mode 100644 index 000000000..45fc71eba --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/resourceid/SharedResourceIdPatch.kt @@ -0,0 +1,49 @@ +package app.revanced.patches.reddit.utils.resourceid + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.shared.mapping.ResourceType.ID +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 cancelButton = -1L + private set +var labelAcknowledgements = -1L + private set +var screenShotShareBanner = -1L + private set +var textAppearanceRedditBaseOldButtonColored = -1L + private set +var toolBarNavSearchCtaContainer = -1L + private set + +internal val sharedResourceIdPatch = resourcePatch( + description = "sharedResourceIdPatch" +) { + dependsOn(resourceMappingPatch) + + execute { + cancelButton = resourceMappings[ + ID, + "cancel_button", + ] + labelAcknowledgements = resourceMappings[ + STRING, + "label_acknowledgements" + ] + screenShotShareBanner = resourceMappings[ + STRING, + "screenshot_share_banner_title" + ] + textAppearanceRedditBaseOldButtonColored = resourceMappings[ + STYLE, + "TextAppearance.RedditBase.OldButton.Colored" + ] + toolBarNavSearchCtaContainer = resourceMappings[ + ID, + "toolbar_nav_search_cta_container" + ] + } +} \ 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..f5d7a9772 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/settings/Fingerprints.kt @@ -0,0 +1,41 @@ +package app.revanced.patches.reddit.utils.settings + +import app.revanced.patches.reddit.utils.extension.Constants.EXTENSION_PATH +import app.revanced.patches.reddit.utils.resourceid.labelAcknowledgements +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", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroidx/preference/Preference;"), + literals = listOf(labelAcknowledgements), + 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 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..e872e637c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/utils/settings/SettingsPatch.kt @@ -0,0 +1,158 @@ +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.PatchException +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.reddit.utils.resourceid.labelAcknowledgements +import app.revanced.patches.shared.sharedSettingFingerprint +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import app.revanced.util.valueOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +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 + +private val settingsBytecodePatch = bytecodePatch( + description = "settingsBytecodePatch" +) { + + execute { + /** + * 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 insertIndex = + indexOfFirstLiteralInstructionOrThrow(labelAcknowledgements) + 3 + 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").forEach { targetXML -> + val resDirectory = get("res") + val targetXml = resDirectory.resolve("xml").resolve(targetXML).toPath() + + if (!targetXml.exists()) + throw PatchException("The preferences can not be found.") + + 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..d3735393c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/Fingerprints.kt @@ -0,0 +1,113 @@ +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 +} + +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..5d181481c --- /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/src/main/kotlin/app/revanced/patches/shared/customspeed/BaseCustomPlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/customspeed/CustomPlaybackSpeedPatch.kt similarity index 53% rename from src/main/kotlin/app/revanced/patches/shared/customspeed/BaseCustomPlaybackSpeedPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/customspeed/CustomPlaybackSpeedPatch.kt index b2e29869b..c23316358 100644 --- a/src/main/kotlin/app/revanced/patches/shared/customspeed/BaseCustomPlaybackSpeedPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/customspeed/CustomPlaybackSpeedPatch.kt @@ -1,35 +1,29 @@ package app.revanced.patches.shared.customspeed -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.replaceInstruction -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patches.shared.customspeed.fingerprints.SpeedArrayGeneratorFingerprint -import app.revanced.patches.shared.customspeed.fingerprints.SpeedLimiterFallBackFingerprint -import app.revanced.patches.shared.customspeed.fingerprints.SpeedLimiterFingerprint +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.resultOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow import com.android.tools.smali.dexlib2.Opcode -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.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference -abstract class BaseCustomPlaybackSpeedPatch( - private val descriptor: String, - private val maxSpeed: Float -) : BytecodePatch( - setOf( - SpeedArrayGeneratorFingerprint, - SpeedLimiterFallBackFingerprint - ) +fun customPlaybackSpeedPatch( + descriptor: String, + maxSpeed: Float +) = bytecodePatch( + description = "customPlaybackSpeedPatch" ) { - override fun execute(context: BytecodeContext) { - SpeedArrayGeneratorFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val targetIndex = it.scanResult.patternScanResult!!.startIndex + execute { + arrayGeneratorFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.startIndex val targetRegister = getInstruction(targetIndex).registerA addInstructions( @@ -65,35 +59,32 @@ abstract class BaseCustomPlaybackSpeedPatch( } } - val speedLimiterParentResult = SpeedLimiterFallBackFingerprint.resultOrThrow() - SpeedLimiterFingerprint.resolve(context, speedLimiterParentResult.classDef) - val speedLimiterResult = SpeedLimiterFingerprint.resultOrThrow() + 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) - arrayOf( - speedLimiterParentResult, - speedLimiterResult - ).forEach { - it.mutableMethod.apply { - val limiterMinConstIndex = - indexOfFirstInstructionOrThrow { (this as? NarrowLiteralInstruction)?.narrowLiteral == 0.25f.toRawBits() } - val limiterMaxConstIndex = - indexOfFirstInstructionOrThrow(limiterMinConstIndex + 1, Opcode.CONST_HIGH16) - - val limiterMinConstDestination = - getInstruction(limiterMinConstIndex).registerA - val limiterMaxConstDestination = - getInstruction(limiterMaxConstIndex).registerA + val limitMinRegister = + getInstruction(limitMinIndex).registerA + val limitMaxRegister = + getInstruction(limitMaxIndex).registerA replaceInstruction( - limiterMinConstIndex, - "const/high16 v$limiterMinConstDestination, 0x0" + limitMinIndex, + "const/high16 v$limitMinRegister, 0x0" ) replaceInstruction( - limiterMaxConstIndex, - "const/high16 v$limiterMaxConstDestination, ${maxSpeed.toRawBits()}" + 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/src/main/kotlin/app/revanced/patches/shared/drawable/DrawableColorPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/drawable/DrawableColorHookPatch.kt similarity index 56% rename from src/main/kotlin/app/revanced/patches/shared/drawable/DrawableColorPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/drawable/DrawableColorHookPatch.kt index c0c53cf1e..89d8c816a 100644 --- a/src/main/kotlin/app/revanced/patches/shared/drawable/DrawableColorPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/drawable/DrawableColorHookPatch.kt @@ -1,23 +1,25 @@ package app.revanced.patches.shared.drawable -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.patch.bytecodePatch import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patches.shared.drawable.fingerprints.DrawableFingerprint +import app.revanced.util.fingerprint.methodOrThrow import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstructionReversedOrThrow -import app.revanced.util.resultOrThrow import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference -object DrawableColorPatch : BytecodePatch( - setOf(DrawableFingerprint) -) { - override fun execute(context: BytecodeContext) { +private lateinit var insertMethod: MutableMethod +private var insertIndex: Int = 0 +private var insertRegister: Int = 0 +private var offset = 0 - DrawableFingerprint.resultOrThrow().mutableMethod.apply { +val drawableColorHookPatch = bytecodePatch( + description = "drawableColorHookPatch" +) { + execute { + drawableColorFingerprint.methodOrThrow().apply { insertMethod = this insertIndex = indexOfFirstInstructionReversedOrThrow { getReference()?.name == "setColor" @@ -25,22 +27,17 @@ object DrawableColorPatch : BytecodePatch( insertRegister = getInstruction(insertIndex).registerD } } +} - private lateinit var insertMethod: MutableMethod - private var insertIndex: Int = 0 - private var insertRegister: Int = 0 - private var offset = 0 - - fun injectCall( - methodDescriptor: String - ) { - insertMethod.addInstructions( - insertIndex + offset, """ +internal fun addDrawableColorHook( + methodDescriptor: String +) { + insertMethod.addInstructions( + insertIndex + offset, """ invoke-static {v$insertRegister}, $methodDescriptor move-result v$insertRegister """ - ) - offset += 2 - } + ) + 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..6229b593f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/extension/Constants.kt @@ -0,0 +1,13 @@ +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 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..d389463ab --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/extension/SharedExtensionPatch.kt @@ -0,0 +1,59 @@ +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) { + if (System.getenv("GITHUB_REPOSITORY") == null) { + 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..29cd4178b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/gms/Fingerprints.kt @@ -0,0 +1,109 @@ +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..f23b5411f --- /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 = "com.rvx.android.youtube" +private const val DEFAULT_PACKAGE_NAME_YOUTUBE = "app.rvx.android.youtube" +internal const val ORIGINAL_PACKAGE_NAME_YOUTUBE = "com.google.android.youtube" + +private const val CLONE_PACKAGE_NAME_YOUTUBE_MUSIC = "com.rvx.android.apps.youtube.music" +private const val DEFAULT_PACKAGE_NAME_YOUTUBE_MUSIC = "app.rvx.android.apps.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/src/main/kotlin/app/revanced/patches/shared/litho/LithoFilterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/litho/LithoFilterPatch.kt similarity index 55% rename from src/main/kotlin/app/revanced/patches/shared/litho/LithoFilterPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/litho/LithoFilterPatch.kt index 58beb75ff..af61831fd 100644 --- a/src/main/kotlin/app/revanced/patches/shared/litho/LithoFilterPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/litho/LithoFilterPatch.kt @@ -1,28 +1,23 @@ package app.revanced.patches.shared.litho -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.or -import app.revanced.patcher.patch.BytecodePatch +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.integrations.Constants.COMPONENTS_PATH -import app.revanced.patches.shared.litho.fingerprints.BufferUpbFeatureFlagFingerprint -import app.revanced.patches.shared.litho.fingerprints.ByteBufferFingerprint -import app.revanced.patches.shared.litho.fingerprints.EmptyComponentsFingerprint -import app.revanced.patches.shared.litho.fingerprints.PathBuilderFingerprint -import app.revanced.patches.shared.litho.fingerprints.PathUpbFeatureFlagFingerprint +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.injectLiteralInstructionBooleanCall -import app.revanced.util.resultOrThrow +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 @@ -34,45 +29,37 @@ 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 -import java.io.Closeable -@Suppress("SpellCheckingInspection", "unused") -object LithoFilterPatch : BytecodePatch( - setOf( - ByteBufferFingerprint, - EmptyComponentsFingerprint, - BufferUpbFeatureFlagFingerprint, - PathUpbFeatureFlagFingerprint, - ) -), Closeable { - private const val INTEGRATIONS_LITHO_FILER_CLASS_DESCRIPTOR = - "$COMPONENTS_PATH/LithoFilterPatch;" +private const val EXTENSION_LITHO_FILER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/LithoFilterPatch;" - private const val INTEGRATIONS_FILER_ARRAY_DESCRIPTOR = - "[$COMPONENTS_PATH/Filter;" +private const val EXTENSION_FILER_ARRAY_DESCRIPTOR = + "[$COMPONENTS_PATH/Filter;" - private lateinit var filterArrayMethod: MutableMethod - private var filterCount = 0 +private lateinit var filterArrayMethod: MutableMethod +private var filterCount = 0 - internal lateinit var addFilter: (String) -> Unit - private set +internal lateinit var addLithoFilter: (String) -> Unit + private set - override fun execute(context: BytecodeContext) { +val lithoFilterPatch = bytecodePatch( + description = "lithoFilterPatch", +) { + execute { - // region Pass the buffer into Integrations. + // region Pass the buffer into extension. - ByteBufferFingerprint.resultOrThrow().mutableMethod.addInstruction( + byteBufferFingerprint.methodOrThrow().addInstruction( 0, - "invoke-static { p2 }, $INTEGRATIONS_LITHO_FILER_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V" + "invoke-static { p2 }, $EXTENSION_LITHO_FILER_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V" ) // endregion var (emptyComponentMethod, emptyComponentLabel) = - EmptyComponentsFingerprint.resultOrThrow().let { - PathBuilderFingerprint.resolve(context, it.classDef) - with(it.mutableMethod) { - val emptyComponentMethodIndex = it.scanResult.patternScanResult!!.startIndex + 1 + emptyComponentsFingerprint.matchOrThrow().let { + with(it.method) { + val emptyComponentMethodIndex = it.patternMatch!!.startIndex + 1 val emptyComponentMethodReference = getInstruction(emptyComponentMethodIndex).reference val emptyComponentFieldReference = @@ -124,41 +111,39 @@ object LithoFilterPatch : BytecodePatch( } } - PathBuilderFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - checkMethodSignatureMatch(this) + 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}, $INTEGRATIONS_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)) - ) + 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. @@ -166,35 +151,33 @@ object LithoFilterPatch : BytecodePatch( // 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.result != null && - PathUpbFeatureFlagFingerprint.result != null) { + if (bufferUpbFeatureFlagFingerprint.second.methodOrNull != null && + pathUpbFeatureFlagFingerprint.second.methodOrNull != null + ) { mapOf( - BufferUpbFeatureFlagFingerprint to 45419603, - PathUpbFeatureFlagFingerprint to 45631264, + bufferUpbFeatureFlagFingerprint to 45419603L, + pathUpbFeatureFlagFingerprint to 45631264L, ).forEach { (fingerprint, literalValue) -> - fingerprint.result?.let { - fingerprint.injectLiteralInstructionBooleanCall( - literalValue, - "0x0" - ) - } + fingerprint.injectLiteralInstructionBooleanCall( + literalValue, + "0x0" + ) } } // endregion // Create a new method to get the filter array to avoid register conflicts. - // This fixes an issue with Integrations compiled with Android Gradle Plugin 8.3.0+. + // This fixes an issue with extension compiled with Android Gradle Plugin 8.3.0+. // https://github.com/ReVanced/revanced-patches/issues/2818 - val lithoFilterMethods = - context.findMethodsOrThrow(INTEGRATIONS_LITHO_FILER_CLASS_DESCRIPTOR) + val lithoFilterMethods = findMethodsOrThrow(EXTENSION_LITHO_FILER_CLASS_DESCRIPTOR) lithoFilterMethods .first { it.name == "" } .apply { val setArrayIndex = indexOfFirstInstructionOrThrow { opcode == Opcode.SPUT_OBJECT && - getReference()?.type == INTEGRATIONS_FILER_ARRAY_DESCRIPTOR + getReference()?.type == EXTENSION_FILER_ARRAY_DESCRIPTOR } val setArrayRegister = getInstruction(setArrayIndex).registerA @@ -202,7 +185,7 @@ object LithoFilterPatch : BytecodePatch( addInstructions( setArrayIndex, """ - invoke-static {}, $INTEGRATIONS_LITHO_FILER_CLASS_DESCRIPTOR->$addedMethodName()$INTEGRATIONS_FILER_ARRAY_DESCRIPTOR + invoke-static {}, $EXTENSION_LITHO_FILER_CLASS_DESCRIPTOR->$addedMethodName()$EXTENSION_FILER_ARRAY_DESCRIPTOR move-result-object v$setArrayRegister """ ) @@ -211,7 +194,7 @@ object LithoFilterPatch : BytecodePatch( definingClass, addedMethodName, emptyList(), - INTEGRATIONS_FILER_ARRAY_DESCRIPTOR, + EXTENSION_FILER_ARRAY_DESCRIPTOR, AccessFlags.PRIVATE or AccessFlags.STATIC, null, null, @@ -226,7 +209,7 @@ object LithoFilterPatch : BytecodePatch( lithoFilterMethods.add(filterArrayMethod) } - addFilter = { classDescriptor -> + addLithoFilter = { classDescriptor -> filterArrayMethod.addInstructions( 0, """ @@ -239,11 +222,14 @@ object LithoFilterPatch : BytecodePatch( } } - override fun close() = filterArrayMethod.addInstructions( - 0, - """ - const/16 v0, $filterCount - new-array v2, v0, $INTEGRATIONS_FILER_ARRAY_DESCRIPTOR - """ - ) -} \ No newline at end of file + 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/src/main/kotlin/app/revanced/patches/shared/opus/BaseOpusCodecsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/opus/BaseOpusCodecsPatch.kt similarity index 61% rename from src/main/kotlin/app/revanced/patches/shared/opus/BaseOpusCodecsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/opus/BaseOpusCodecsPatch.kt index 8344971ad..3918ebfbd 100644 --- a/src/main/kotlin/app/revanced/patches/shared/opus/BaseOpusCodecsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/opus/BaseOpusCodecsPatch.kt @@ -1,35 +1,25 @@ package app.revanced.patches.shared.opus -import app.revanced.patcher.data.BytecodeContext 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.bytecodePatch import app.revanced.patcher.util.smali.ExternalLabel -import app.revanced.patches.shared.opus.fingerprints.CodecReferenceFingerprint -import app.revanced.patches.shared.opus.fingerprints.CodecSelectorFingerprint +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.resultOrThrow 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 -/** - * This patch is generally not required for the latest versions of YouTube and YouTube Music. - * For YouTube Music, if user spoofs the app version to v4.27.53, mp4a codec is still used, this is the patch for some of these users. - */ -abstract class BaseOpusCodecsPatch( - private val descriptor: String -) : BytecodePatch( - setOf( - CodecReferenceFingerprint, - CodecSelectorFingerprint - ) +fun baseOpusCodecsPatch( + descriptor: String, +) = bytecodePatch( + description = "baseOpusCodecsPatch" ) { - override fun execute(context: BytecodeContext) { - - val opusCodecReference = with(CodecReferenceFingerprint.resultOrThrow().mutableMethod) { + execute { + val opusCodecReference = with(codecReferenceFingerprint.methodOrThrow()) { val codecIndex = indexOfFirstInstructionOrThrow { opcode == Opcode.INVOKE_STATIC && getReference()?.returnType == "Ljava/util/Set;" @@ -37,10 +27,10 @@ abstract class BaseOpusCodecsPatch( getInstruction(codecIndex).reference } - CodecSelectorFingerprint.resultOrThrow().let { - it.mutableMethod.apply { + codecSelectorFingerprint.matchOrThrow().let { + it.method.apply { val freeRegister = implementation!!.registerCount - parameters.size - 2 - val targetIndex = it.scanResult.patternScanResult!!.endIndex + val targetIndex = it.patternMatch!!.endIndex val targetRegister = getInstruction(targetIndex).registerA addInstructionsWithLabels( @@ -56,3 +46,4 @@ abstract class BaseOpusCodecsPatch( } } } + 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/src/main/kotlin/app/revanced/patches/shared/spoofappversion/BaseSpoofAppVersionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/appversion/BaseSpoofAppVersionPatch.kt similarity index 55% rename from src/main/kotlin/app/revanced/patches/shared/spoofappversion/BaseSpoofAppVersionPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/spoof/appversion/BaseSpoofAppVersionPatch.kt index c8254ad5f..8d4025c41 100644 --- a/src/main/kotlin/app/revanced/patches/shared/spoofappversion/BaseSpoofAppVersionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/appversion/BaseSpoofAppVersionPatch.kt @@ -1,24 +1,22 @@ -package app.revanced.patches.shared.spoofappversion +package app.revanced.patches.shared.spoof.appversion -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.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint -import app.revanced.patches.shared.fingerprints.CreatePlayerRequestBodyWithModelFingerprint.indexOfReleaseInstruction +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 app.revanced.util.resultOrThrow import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction -abstract class BaseSpoofAppVersionPatch( - private val descriptor: String -) : BytecodePatch( - setOf(CreatePlayerRequestBodyWithModelFingerprint) +fun baseSpoofAppVersionPatch( + descriptor: String, +) = bytecodePatch( + description = "baseSpoofAppVersionPatch" ) { - override fun execute(context: BytecodeContext) { - - CreatePlayerRequestBodyWithModelFingerprint.resultOrThrow().mutableMethod.apply { + execute { + createPlayerRequestBodyWithModelFingerprint.methodOrThrow().apply { val versionIndex = indexOfReleaseInstruction(this) + 1 val insertIndex = indexOfFirstInstructionReversedOrThrow(versionIndex, Opcode.IPUT_OBJECT) @@ -31,6 +29,5 @@ abstract class BaseSpoofAppVersionPatch( """ ) } - } -} \ No newline at end of file +} diff --git a/src/main/kotlin/app/revanced/patches/shared/spoofuseragent/BaseSpoofUserAgentPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/useragent/BaseSpoofUserAgentPatch.kt similarity index 57% rename from src/main/kotlin/app/revanced/patches/shared/spoofuseragent/BaseSpoofUserAgentPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/spoof/useragent/BaseSpoofUserAgentPatch.kt index f1ae927cb..105472d90 100644 --- a/src/main/kotlin/app/revanced/patches/shared/spoofuseragent/BaseSpoofUserAgentPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/spoof/useragent/BaseSpoofUserAgentPatch.kt @@ -1,38 +1,32 @@ -package app.revanced.patches.shared.spoofuseragent +package app.revanced.patches.shared.spoof.useragent import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patches.shared.transformation.BaseTransformInstructionsPatch import app.revanced.patches.shared.transformation.IMethodCall -import app.revanced.patches.shared.transformation.Instruction35cInfo 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.ClassDef -import com.android.tools.smali.dexlib2.iface.Method -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.iface.reference.StringReference -abstract class BaseSpoofUserAgentPatch( - private val packageName: String -) : BaseTransformInstructionsPatch() { - override fun filterMap( - classDef: ClassDef, - method: Method, - instruction: Instruction, - instructionIndex: Int, - ) = filterMapInstruction35c( - "Lapp/revanced/integrations", - classDef, - instruction, - instructionIndex, - ) +private const val USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE = + "Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;" - override fun transform(mutableMethod: MutableMethod, entry: Instruction35cInfo) { +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. @@ -40,17 +34,17 @@ abstract class BaseSpoofUserAgentPatch( // After context.getPackageName() the result is moved to a register. val targetRegister = ( getInstruction(instructionIndex + 1) - as? OneRegisterInstruction ?: return + as? OneRegisterInstruction ?: return@transform ).registerA - // IndexOutOfBoundsException is possible here, + // 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 + return@transform } // Do not change the package name in methods that use resources, or for methods that use GmsCore. @@ -62,7 +56,7 @@ abstract class BaseSpoofUserAgentPatch( (reference?.string == "android.resource://" || reference?.string == "gcore_") } if (resourceOrGmsStringInstructionIndex >= 0) { - return + return@transform } // Overwrite the result of context.getPackageName() with the original package name. @@ -71,25 +65,20 @@ abstract class BaseSpoofUserAgentPatch( "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;", - ), - } - - private companion object { - private const val USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE = - "Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;" - } +@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/src/main/kotlin/app/revanced/patches/shared/transformation/MethodCall.kt b/patches/src/main/kotlin/app/revanced/patches/shared/transformation/MethodCall.kt similarity index 68% rename from src/main/kotlin/app/revanced/patches/shared/transformation/MethodCall.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/transformation/MethodCall.kt index b581112ac..37213570e 100644 --- a/src/main/kotlin/app/revanced/patches/shared/transformation/MethodCall.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/transformation/MethodCall.kt @@ -10,7 +10,6 @@ import com.android.tools.smali.dexlib2.iface.reference.MethodReference typealias Instruction35cInfo = Triple -@Suppress("unused") interface IMethodCall { val definedClassName: String val methodName: String @@ -19,8 +18,8 @@ interface IMethodCall { /** * Replaces an invoke-virtual instruction with an invoke-static instruction, - * which calls a static replacement method in the respective integrations class. - * The method definition in the integrations class is expected to be the same, + * 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. * @@ -28,56 +27,58 @@ interface IMethodCall { * * original method: Window#setFlags(int, int) * - * replacement method: Integrations#setFlags(Window, int, int) + * replacement method: Extension#setFlags(Window, int, int) */ - fun replaceInvokeVirtualWithIntegrations( + fun replaceInvokeVirtualWithExtension( definingClassDescriptor: String, method: MutableMethod, instruction: Instruction35c, - instructionIndex: Int + instructionIndex: Int, ) { val registers = arrayOf( instruction.registerC, instruction.registerD, instruction.registerE, instruction.registerF, - instruction.registerG + 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}." + "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 replacementMethodDefinition = - "${methodName}(${definedClassName}${methodParams.joinToString(separator = "")})${returnType}" + 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}->${replacementMethodDefinition}" + "invoke-static { $args }, $definingClassDescriptor->$replacementMethod", ) } } -inline fun fromMethodReference(methodReference: MethodReference) +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 + search.definedClassName == methodReference.definingClass && + search.methodName == methodReference.name && + methodReference.parameterTypes.toTypedArray().contentEquals(search.methodParams) && + search.returnType == methodReference.returnType } inline fun filterMapInstruction35c( - integrationsClassDescriptorPrefix: String, + extensionClassDescriptorPrefix: String, classDef: ClassDef, instruction: Instruction, - instructionIndex: Int + instructionIndex: Int, ): Instruction35cInfo? where E : Enum, E : IMethodCall { - if (classDef.type.startsWith(integrationsClassDescriptorPrefix)) { + if (classDef.startsWith(extensionClassDescriptorPrefix)) { // avoid infinite recursion return null } diff --git a/src/main/kotlin/app/revanced/patches/shared/transformation/BaseTransformInstructionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/transformation/TransformInstructionsPatch.kt similarity index 70% rename from src/main/kotlin/app/revanced/patches/shared/transformation/BaseTransformInstructionsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/transformation/TransformInstructionsPatch.kt index d8ed90ef3..46a78a714 100644 --- a/src/main/kotlin/app/revanced/patches/shared/transformation/BaseTransformInstructionsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/transformation/TransformInstructionsPatch.kt @@ -1,36 +1,29 @@ package app.revanced.patches.shared.transformation -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.patch.BytecodePatch +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 -@Suppress("MemberVisibilityCanBePrivate") -abstract class BaseTransformInstructionsPatch : BytecodePatch(emptySet()) { - abstract fun filterMap( - classDef: ClassDef, - method: Method, - instruction: Instruction, - instructionIndex: Int, - ): T? - - abstract fun transform(mutableMethod: MutableMethod, entry: T) - +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? { - return method.implementation?.instructions?.asSequence()?.withIndex() + fun findPatchIndices(classDef: ClassDef, method: Method): Sequence? = + method.implementation?.instructions?.asSequence()?.withIndex() ?.mapNotNull { (index, instruction) -> filterMap(classDef, method, instruction, index) } - } - override fun execute(context: BytecodeContext) { + execute { // Find all methods to patch buildMap { - context.classes.forEach { classDef -> + classes.forEach { classDef -> val methods = buildList { classDef.methods.forEach { method -> // Since the Sequence executes lazily, @@ -46,7 +39,7 @@ abstract class BaseTransformInstructionsPatch : BytecodePatch(emptySet()) { } }.forEach { (classDef, methods) -> // And finally transform the methods... - val mutableClass = context.proxy(classDef).mutableClass + val mutableClass = proxy(classDef).mutableClass methods.map(mutableClass::findMutableMethodOf).forEach methods@{ mutableMethod -> val patchIndices = 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/src/main/kotlin/app/revanced/patches/youtube/ads/general/AdsBytecodePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ads/general/AdsPatch.kt similarity index 54% rename from src/main/kotlin/app/revanced/patches/youtube/ads/general/AdsBytecodePatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/ads/general/AdsPatch.kt index 9a9868502..abcdbe687 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/ads/general/AdsBytecodePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ads/general/AdsPatch.kt @@ -1,63 +1,116 @@ package app.revanced.patches.youtube.ads.general -import app.revanced.patcher.data.BytecodeContext 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.annotation.Patch +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.util.smali.ExternalLabel -import app.revanced.patches.youtube.ads.general.VideoAdsPatch.hookLithoFullscreenAds -import app.revanced.patches.youtube.ads.general.VideoAdsPatch.hookNonLithoFullscreenAds -import app.revanced.patches.youtube.ads.general.fingerprints.CompactYpcOfferModuleViewFingerprint -import app.revanced.patches.youtube.ads.general.fingerprints.InterstitialsContainerFingerprint -import app.revanced.patches.youtube.ads.general.fingerprints.ShowDialogCommandFingerprint -import app.revanced.patches.youtube.utils.integrations.Constants.ADS_CLASS_DESCRIPTOR -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.AdAttribution -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.InterstitialsContainer +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 app.revanced.util.resultOrThrow 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 -@Patch(dependencies = [SharedResourceIdPatch::class]) -object AdsBytecodePatch : BytecodePatch( - setOf( - CompactYpcOfferModuleViewFingerprint, - InterstitialsContainerFingerprint, - ShowDialogCommandFingerprint - ) +private const val ADS_FILTER_CLASS_DESCRIPTOR = + "$COMPONENTS_PATH/AdsFilter;" + +@Suppress("unused") +val adsPatch = bytecodePatch( + HIDE_ADS.title, + HIDE_ADS.summary, ) { - override fun execute(context: BytecodeContext) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseAdsPatch(ADS_CLASS_DESCRIPTOR, "hideVideoAds"), + doubleBackToClosePatch, + lithoFilterPatch, + sharedResourceIdPatch, + settingsPatch, + swipeRefreshPatch, + ) + + execute { + addLithoFilter(ADS_FILTER_CLASS_DESCRIPTOR) // region patch for hide fullscreen ads // non-litho view, used in some old clients. - InterstitialsContainerFingerprint - .resultOrThrow() - .hookNonLithoFullscreenAds(InterstitialsContainer) + interstitialsContainerFingerprint + .methodOrThrow() + .hookNonLithoFullscreenAds(interstitialsContainer) // litho view, used in 'ShowDialogCommandOuterClass' in innertube - ShowDialogCommandFingerprint - .resultOrThrow() + showDialogCommandFingerprint + .matchOrThrow() .hookLithoFullscreenAds() // endregion // region patch for hide general ads - hideAdAttributionView(context) + 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.resultOrThrow().let { - it.mutableMethod.apply { - val startIndex = it.scanResult.patternScanResult!!.startIndex + compactYpcOfferModuleViewFingerprint.matchOrThrow().let { + it.method.apply { + val startIndex = it.patternMatch!!.startIndex val measuredWidthRegister = getInstruction(startIndex).registerA val measuredHeightInstruction = @@ -79,41 +132,23 @@ object AdsBytecodePatch : BytecodePatch( // endregion - } + findMethodOrThrow("$PATCHES_PATH/PatchStatus;") { + name == "HideFullscreenAdsDefaultBoolean" + }.replaceInstruction( + 0, + "const/4 v0, 0x1" + ) - private fun hideAdAttributionView(context: BytecodeContext) { - context.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 + // region add settings - val insertIndex = index + 1 + addPreference( + arrayOf( + "PREFERENCE_SCREEN: ADS" + ), + HIDE_ADS + ) - // Call to get the view with the id adAttribution - (instructions.elementAt(insertIndex)).apply { - if (opcode != Opcode.INVOKE_VIRTUAL) - return@forEachIndexed + // endregion - // Hide the view - val viewRegister = (this as Instruction35c).registerC - context.proxy(classDef) - .mutableClass - .findMutableMethodOf(method) - .injectHideViewCall( - insertIndex, - viewRegister, - ADS_CLASS_DESCRIPTOR, - "hideAdAttributionView" - ) - } - } - } - } - } } } 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/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt similarity index 52% rename from src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt index a4603d22d..4c19d9fc4 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/components/FeedComponentsPatch.kt @@ -1,61 +1,46 @@ package app.revanced.patches.youtube.feed.components -import app.revanced.patcher.data.BytecodeContext +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.fingerprint.MethodFingerprint 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.LithoFilterPatch -import app.revanced.patches.youtube.feed.components.fingerprints.BreakingNewsFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.CaptionsButtonFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.CaptionsButtonSyntheticFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.ChannelListSubMenuFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.ChannelListSubMenuTabletFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.ChannelListSubMenuTabletSyntheticFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.ChannelTabBuilderFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.ChannelTabRendererFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.ContentPillFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.ElementParserFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.ElementParserParentFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.EngagementPanelUpdateFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.FilterBarHeightFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.LatestVideosButtonFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.LinearLayoutManagerItemCountsFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.RelatedChipCloudFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.SearchResultsChipBarFingerprint -import app.revanced.patches.youtube.feed.components.fingerprints.ShowMoreButtonFingerprint -import app.revanced.patches.youtube.utils.bottomsheet.BottomSheetHookPatch +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.fingerprints.EngagementPanelBuilderFingerprint -import app.revanced.patches.youtube.utils.fingerprints.ScrollTopParentFingerprint -import app.revanced.patches.youtube.utils.integrations.Constants.COMPONENTS_PATH -import app.revanced.patches.youtube.utils.integrations.Constants.FEED_CLASS_DESCRIPTOR -import app.revanced.patches.youtube.utils.integrations.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.playertype.PlayerTypeHookPatch -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.Bar -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.CaptionToggleContainer -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.ChannelListSubMenu -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.ContentPill -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch.HorizontalCardList -import app.revanced.patches.youtube.utils.settings.SettingsPatch +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.alsoResolve +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.indexOfFirstWideLiteralInstructionValueOrThrow -import app.revanced.util.injectLiteralInstructionViewCall -import app.revanced.util.patch.BaseBytecodePatch -import app.revanced.util.resultOrThrow +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 @@ -66,85 +51,66 @@ 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 -@Suppress("unused") -object FeedComponentsPatch : BaseBytecodePatch( - name = "Hide feed components", - description = "Adds options to hide components related to feeds.", - dependencies = setOf( - LithoFilterPatch::class, - MainActivityResolvePatch::class, - NavigationBarHookPatch::class, - PlayerTypeHookPatch::class, - SettingsPatch::class, - SharedResourceIdPatch::class, - BottomSheetHookPatch::class, - ), - compatiblePackages = COMPATIBLE_PACKAGE, - fingerprints = setOf( - BreakingNewsFingerprint, - CaptionsButtonFingerprint, - CaptionsButtonSyntheticFingerprint, - ChannelListSubMenuFingerprint, - ChannelListSubMenuTabletFingerprint, - ChannelListSubMenuTabletSyntheticFingerprint, - ChannelTabRendererFingerprint, - ContentPillFingerprint, - ElementParserParentFingerprint, - EngagementPanelBuilderFingerprint, - FilterBarHeightFingerprint, - LatestVideosButtonFingerprint, - LinearLayoutManagerItemCountsFingerprint, - RelatedChipCloudFingerprint, - ScrollTopParentFingerprint, - SearchResultsChipBarFingerprint, - ShowMoreButtonFingerprint, - ) -) { - 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;" +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;" - override fun execute(context: BytecodeContext) { +@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, + breakingNewsFingerprint, "hideBreakingNewsShelf", - HorizontalCardList + horizontalCardList ), // subscriptions channel section. Triple( - ChannelListSubMenuFingerprint, + channelListSubMenuFingerprint, "hideSubscriptionsChannelSection", - ChannelListSubMenu + channelListSubMenu ), // latest videos button Triple( - ContentPillFingerprint, + contentPillFingerprint, "hideLatestVideosButton", - ContentPill + contentPill ), Triple( - LatestVideosButtonFingerprint, + latestVideosButtonFingerprint, "hideLatestVideosButton", - Bar + bar ), ).forEach { (fingerprint, methodName, literal) -> val smaliInstruction = """ - invoke-static {v$REGISTER_TEMPLATE_REPLACEMENT}, $FEED_CLASS_DESCRIPTOR->$methodName(Landroid/view/View;)V - """ + invoke-static {v$REGISTER_TEMPLATE_REPLACEMENT}, $FEED_CLASS_DESCRIPTOR->$methodName(Landroid/view/View;)V + """ fingerprint.injectLiteralInstructionViewCall(literal, smaliInstruction) } @@ -152,8 +118,8 @@ object FeedComponentsPatch : BaseBytecodePatch( // region patch for hide caption button - CaptionsButtonFingerprint.resultOrThrow().mutableMethod.apply { - val constIndex = indexOfFirstWideLiteralInstructionValueOrThrow(CaptionToggleContainer) + captionsButtonFingerprint.methodOrThrow().apply { + val constIndex = indexOfFirstLiteralInstructionOrThrow(captionToggleContainer) val insertIndex = indexOfFirstInstructionReversedOrThrow(constIndex, Opcode.IF_EQZ) val insertRegister = getInstruction(insertIndex).registerA @@ -165,8 +131,8 @@ object FeedComponentsPatch : BaseBytecodePatch( ) } - CaptionsButtonSyntheticFingerprint.resultOrThrow().mutableMethod.apply { - val constIndex = indexOfFirstWideLiteralInstructionValueOrThrow(CaptionToggleContainer) + captionsButtonSyntheticFingerprint.methodOrThrow().apply { + val constIndex = indexOfFirstLiteralInstructionOrThrow(captionToggleContainer) val targetIndex = indexOfFirstInstructionOrThrow(constIndex, Opcode.MOVE_RESULT_OBJECT) val targetRegister = getInstruction(targetIndex).registerA @@ -180,7 +146,7 @@ object FeedComponentsPatch : BaseBytecodePatch( // region patch for hide floating button - MainActivityResolvePatch.onCreateMethod.apply { + onCreateMethod.apply { val fabIndex = indexOfFirstInstructionOrThrow { opcode == Opcode.CONST_STRING && getReference()?.string == "fab" @@ -210,12 +176,12 @@ object FeedComponentsPatch : BaseBytecodePatch( ) } - EngagementPanelBuilderFingerprint.resultOrThrow().let { - it.mutableClass.methods.filter { method -> - method.indexOfEngagementPanelBuilderInstruction(it.mutableMethod) >= 0 + engagementPanelBuilderFingerprint.matchOrThrow().let { + it.classDef.methods.filter { method -> + method.indexOfEngagementPanelBuilderInstruction(it.method) >= 0 }.forEach { method -> method.apply { - val index = indexOfEngagementPanelBuilderInstruction(it.mutableMethod) + val index = indexOfEngagementPanelBuilderInstruction(it.method) val register = getInstruction(index + 1).registerA addInstruction( @@ -227,19 +193,15 @@ object FeedComponentsPatch : BaseBytecodePatch( } } - EngagementPanelUpdateFingerprint.alsoResolve( - context, EngagementPanelBuilderFingerprint - ).mutableMethod.addInstruction( - 0, - "invoke-static {}, $RELATED_VIDEO_CLASS_DESCRIPTOR->hideEngagementPanel()V" - ) + engagementPanelUpdateFingerprint.methodOrThrow(engagementPanelBuilderFingerprint) + .addInstruction( + 0, + "invoke-static {}, $RELATED_VIDEO_CLASS_DESCRIPTOR->hideEngagementPanel()V" + ) - // BytecodeUtils.getWalkerMethod must be used here - // Otherwise, MethodWalker finds the wrong class in YouTube 18.29.38: - // https://github.com/ReVanced/revanced-patcher/issues/309 - LinearLayoutManagerItemCountsFingerprint.resultOrThrow().let { + linearLayoutManagerItemCountsFingerprint.matchOrThrow().let { val methodWalker = - it.getWalkerMethod(context, it.scanResult.patternScanResult!!.endIndex) + it.getWalkerMethod(it.patternMatch!!.endIndex) methodWalker.apply { val index = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT) val register = getInstruction(index).registerA @@ -258,10 +220,10 @@ object FeedComponentsPatch : BaseBytecodePatch( // region patch for hide subscriptions channel section for tablet arrayOf( - ChannelListSubMenuTabletFingerprint, - ChannelListSubMenuTabletSyntheticFingerprint + channelListSubMenuTabletFingerprint, + channelListSubMenuTabletSyntheticFingerprint ).forEach { fingerprint -> - fingerprint.resultOrThrow().mutableMethod.apply { + fingerprint.methodOrThrow().apply { addInstructionsWithLabels( 0, """ invoke-static {}, $FEED_CLASS_DESCRIPTOR->hideSubscriptionsChannelSection()Z @@ -277,19 +239,36 @@ object FeedComponentsPatch : BaseBytecodePatch( // region patch for hide category bar - FilterBarHeightFingerprint.patch { register -> + 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 -> + relatedChipCloudFingerprint.patch(1) { register -> "invoke-static { v$register }, " + "$FEED_CLASS_DESCRIPTOR->hideCategoryBarInRelatedVideos(Landroid/view/View;)V" } - SearchResultsChipBarFingerprint.patch(-1, -2) { register -> + searchResultsChipBarFingerprint.patch(-1, -2) { register -> """ invoke-static { v$register }, $FEED_CLASS_DESCRIPTOR->hideCategoryBarInSearch(I)I move-result v$register @@ -300,25 +279,21 @@ object FeedComponentsPatch : BaseBytecodePatch( // region patch for hide mix playlists - ElementParserFingerprint.resolve( - context, - ElementParserParentFingerprint.resultOrThrow().classDef - ) - ElementParserFingerprint.resultOrThrow().let { - it.mutableMethod.apply { + 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") + 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.scanResult.patternScanResult!!.startIndex + val jumpIndex = it.patternMatch!!.startIndex addInstructionsWithLabels( insertIndex, """ @@ -339,9 +314,9 @@ object FeedComponentsPatch : BaseBytecodePatch( // region patch for hide show more button - ShowMoreButtonFingerprint.resultOrThrow().let { + showMoreButtonFingerprint.mutableClassOrThrow().let { val getViewMethod = - it.mutableClass.methods.find { method -> + it.methods.find { method -> method.parameters.isEmpty() && method.returnType == "Landroid/view/View;" } @@ -361,15 +336,11 @@ object FeedComponentsPatch : BaseBytecodePatch( // region patch for hide channel tab - ChannelTabBuilderFingerprint.resolve( - context, - ScrollTopParentFingerprint.resultOrThrow().classDef - ) + val channelTabBuilderMethod = + channelTabBuilderFingerprint.methodOrThrow(scrollTopParentFingerprint) - val channelTabBuilderMethod = ChannelTabBuilderFingerprint.resultOrThrow().mutableMethod - - ChannelTabRendererFingerprint.resultOrThrow().let { - it.mutableMethod.apply { + channelTabRendererFingerprint.matchOrThrow().let { + it.method.apply { val iteratorIndex = indexOfFirstInstructionOrThrow { getReference()?.name == "hasNext" } @@ -379,9 +350,9 @@ object FeedComponentsPatch : BaseBytecodePatch( val targetIndex = indexOfFirstInstructionOrThrow { val reference = ((this as? ReferenceInstruction)?.reference as? MethodReference) - opcode == Opcode.INVOKE_INTERFACE - && reference?.returnType == channelTabBuilderMethod.returnType - && reference.parameterTypes == channelTabBuilderMethod.parameterTypes + opcode == Opcode.INVOKE_INTERFACE && + reference?.returnType == channelTabBuilderMethod.returnType && + reference.parameterTypes == channelTabBuilderMethod.parameterTypes } val objectIndex = @@ -405,39 +376,23 @@ object FeedComponentsPatch : BaseBytecodePatch( // endregion - LithoFilterPatch.addFilter(CAROUSEL_SHELF_FILTER_CLASS_DESCRIPTOR) - LithoFilterPatch.addFilter(FEED_COMPONENTS_FILTER_CLASS_DESCRIPTOR) - LithoFilterPatch.addFilter(FEED_VIDEO_FILTER_CLASS_DESCRIPTOR) - LithoFilterPatch.addFilter(FEED_VIDEO_VIEWS_FILTER_CLASS_DESCRIPTOR) - LithoFilterPatch.addFilter(KEYWORD_FILTER_CLASS_DESCRIPTOR) + 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) - /** - * Add settings - */ - SettingsPatch.addPreference( + // region add settings + + addPreference( arrayOf( "PREFERENCE_SCREEN: FEED", "SETTINGS: HIDE_FEED_COMPONENTS" - ) + ), + HIDE_FEED_COMPONENTS ) - SettingsPatch.updatePatchStatus(this) + // endregion + } - - private fun MethodFingerprint.patch( - insertIndexOffset: Int = 0, - hookRegisterOffset: Int = 0, - instructions: (Int) -> String - ) = - resultOrThrow().let { - it.mutableMethod.apply { - val endIndex = it.scanResult.patternScanResult!!.endIndex - - val insertIndex = endIndex + insertIndexOffset - val register = - getInstruction(endIndex + hookRegisterOffset).registerA - - addInstructions(insertIndex, instructions(register)) - } - } } 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/src/main/kotlin/app/revanced/patches/youtube/feed/flyoutmenu/FeedFlyoutMenuPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/flyoutmenu/FeedFlyoutMenuPatch.kt similarity index 54% rename from src/main/kotlin/app/revanced/patches/youtube/feed/flyoutmenu/FeedFlyoutMenuPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/feed/flyoutmenu/FeedFlyoutMenuPatch.kt index 9fde2697f..0223783bc 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/feed/flyoutmenu/FeedFlyoutMenuPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/flyoutmenu/FeedFlyoutMenuPatch.kt @@ -1,50 +1,45 @@ package app.revanced.patches.youtube.feed.flyoutmenu -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.patch.PatchException -import app.revanced.patches.youtube.feed.flyoutmenu.fingerprints.BottomSheetMenuItemBuilderFingerprint -import app.revanced.patches.youtube.feed.flyoutmenu.fingerprints.BottomSheetMenuItemBuilderLegacyFingerprint -import app.revanced.patches.youtube.feed.flyoutmenu.fingerprints.ContextualMenuItemBuilderFingerprint +import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE -import app.revanced.patches.youtube.utils.integrations.Constants.FEED_CLASS_DESCRIPTOR -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch -import app.revanced.patches.youtube.utils.settings.SettingsPatch -import app.revanced.util.patch.BaseBytecodePatch -import app.revanced.util.resultOrThrow +import app.revanced.patches.youtube.utils.extension.Constants.FEED_CLASS_DESCRIPTOR +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.matchOrNull +import app.revanced.util.fingerprint.matchOrThrow 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.Instruction35c import com.android.tools.smali.dexlib2.iface.reference.MethodReference @Suppress("unused") -object FeedFlyoutMenuPatch : BaseBytecodePatch( - name = "Hide feed flyout menu", - description = "Adds the ability to hide feed flyout menu components using a custom filter.", - dependencies = setOf( - SettingsPatch::class, - SharedResourceIdPatch::class - ), - compatiblePackages = COMPATIBLE_PACKAGE, - fingerprints = setOf( - BottomSheetMenuItemBuilderFingerprint, - BottomSheetMenuItemBuilderLegacyFingerprint, - ContextualMenuItemBuilderFingerprint - ) +val feedFlyoutMenuPatch = bytecodePatch( + HIDE_FEED_FLYOUT_MENU.title, + HIDE_FEED_FLYOUT_MENU.summary, ) { - override fun execute(context: BytecodeContext) { + compatibleWith(COMPATIBLE_PACKAGE) - /** - * Phone - */ - val bottomSheetMenuItemBuilderResult = BottomSheetMenuItemBuilderLegacyFingerprint.result - ?: BottomSheetMenuItemBuilderFingerprint.resultOrThrow() + dependsOn( + sharedResourceIdPatch, + settingsPatch, + ) + execute { - bottomSheetMenuItemBuilderResult.let { - it.mutableMethod.apply { - val targetIndex = it.scanResult.patternScanResult!!.endIndex + // region patch for phone + + val bottomSheetMenuItemBuilderMatch = + bottomSheetMenuItemBuilderLegacyFingerprint.matchOrNull() + ?: bottomSheetMenuItemBuilderFingerprint.matchOrThrow() + + bottomSheetMenuItemBuilderMatch.let { + it.method.apply { + val targetIndex = it.patternMatch!!.endIndex val targetRegister = getInstruction(targetIndex).registerA val targetParameter = @@ -61,12 +56,13 @@ object FeedFlyoutMenuPatch : BaseBytecodePatch( } } - /** - * Tablet - */ - ContextualMenuItemBuilderFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val targetIndex = it.scanResult.patternScanResult!!.startIndex + 1 + // endregion + + // region patch for tablet + + contextualMenuItemBuilderFingerprint.matchOrThrow().let { + it.method.apply { + val targetIndex = it.patternMatch!!.startIndex + 1 val targetInstruction = getInstruction(targetIndex) val targetReferenceName = @@ -82,16 +78,19 @@ object FeedFlyoutMenuPatch : BaseBytecodePatch( } } - /** - * Add settings - */ - SettingsPatch.addPreference( + // endregion + + // region add settings + + addPreference( arrayOf( "PREFERENCE_SCREEN: FEED", "SETTINGS: HIDE_FEED_FLYOUT_MENU" - ) + ), + HIDE_FEED_FLYOUT_MENU ) - SettingsPatch.updatePatchStatus(this) + // 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..2c905c376 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/feed/flyoutmenu/Fingerprints.kt @@ -0,0 +1,56 @@ +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 + +/** + * Compatible with YouTube v19.11.43~ + */ +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 with iconType: ") +) + +/** + * Compatible with ~YouTube v19.10.39 + */ +internal val bottomSheetMenuItemBuilderLegacyFingerprint = legacyFingerprint( + name = "bottomSheetMenuItemBuilderLegacyFingerprint", + returnType = "L", + parameters = listOf("L"), + opcodes = listOf( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT + ), + strings = listOf("ElementTransformer, ElementPresenter and InteractionLogger cannot be null") +) + +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..20b30093c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/components/LayoutComponentsPatch.kt @@ -0,0 +1,245 @@ +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.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( + lithoFilterPatch, + sharedResourceIdPatch, + settingsPatch, + 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/src/main/kotlin/app/revanced/patches/youtube/general/downloads/DownloadActionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/downloads/DownloadActionsPatch.kt similarity index 58% rename from src/main/kotlin/app/revanced/patches/youtube/general/downloads/DownloadActionsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/general/downloads/DownloadActionsPatch.kt index 688d34454..d6f4d54b4 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/general/downloads/DownloadActionsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/downloads/DownloadActionsPatch.kt @@ -1,29 +1,24 @@ package app.revanced.patches.youtube.general.downloads -import app.revanced.patcher.data.BytecodeContext 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.getInstructions +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.general.downloads.fingerprints.AccessibilityOfflineButtonSyncFingerprint -import app.revanced.patches.youtube.general.downloads.fingerprints.DownloadPlaylistButtonOnClickFingerprint -import app.revanced.patches.youtube.general.downloads.fingerprints.DownloadPlaylistButtonOnClickFingerprint.indexOfPlaylistDownloadActionInvokeInstruction -import app.revanced.patches.youtube.general.downloads.fingerprints.OfflinePlaylistEndpointFingerprint -import app.revanced.patches.youtube.general.downloads.fingerprints.OfflineVideoEndpointFingerprint -import app.revanced.patches.youtube.general.downloads.fingerprints.SetPlaylistDownloadButtonVisibilityFingerprint -import app.revanced.patches.youtube.utils.compatibility.Constants -import app.revanced.patches.youtube.utils.integrations.Constants.GENERAL_PATH -import app.revanced.patches.youtube.utils.pip.PiPStateHookPatch -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch -import app.revanced.patches.youtube.utils.settings.SettingsPatch -import app.revanced.util.alsoResolve +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 app.revanced.util.patch.BaseBytecodePatch -import app.revanced.util.resultOrThrow 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 @@ -31,37 +26,33 @@ 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") -object DownloadActionsPatch : BaseBytecodePatch( - name = "Hook download actions", - description = "Adds support to download videos with an external downloader app using the in-app download button.", - dependencies = setOf( - PiPStateHookPatch::class, - SharedResourceIdPatch::class, - SettingsPatch::class - ), - compatiblePackages = Constants.COMPATIBLE_PACKAGE, - fingerprints = setOf( - AccessibilityOfflineButtonSyncFingerprint, - DownloadPlaylistButtonOnClickFingerprint, - OfflinePlaylistEndpointFingerprint, - OfflineVideoEndpointFingerprint, - ) +val downloadActionsPatch = bytecodePatch( + HOOK_DOWNLOAD_ACTIONS.title, + HOOK_DOWNLOAD_ACTIONS.summary, ) { - private const val INTEGRATIONS_CLASS_DESCRIPTOR = - "$GENERAL_PATH/DownloadActionsPatch;" + compatibleWith(COMPATIBLE_PACKAGE) - private const val OFFLINE_PLAYLIST_ENDPOINT_OUTER_CLASS_DESCRIPTOR = - "Lcom/google/protos/youtube/api/innertube/OfflinePlaylistEndpointOuterClass${'$'}OfflinePlaylistEndpoint;" + dependsOn( + pipStateHookPatch, + sharedResourceIdPatch, + settingsPatch, + ) - override fun execute(context: BytecodeContext) { + execute { // region patch for hook download actions (video action bar and flyout panel) - OfflineVideoEndpointFingerprint.resultOrThrow().mutableMethod.apply { + offlineVideoEndpointFingerprint.methodOrThrow().apply { addInstructionsWithLabels( 0, """ - invoke-static/range {p3 .. p3}, $INTEGRATIONS_CLASS_DESCRIPTOR->inAppVideoDownloadButtonOnClick(Ljava/lang/String;)Z + invoke-static/range {p3 .. p3}, $EXTENSION_CLASS_DESCRIPTOR->inAppVideoDownloadButtonOnClick(Ljava/lang/String;)Z move-result v0 if-eqz v0, :show_native_downloader return-void @@ -74,11 +65,11 @@ object DownloadActionsPatch : BaseBytecodePatch( // region patch for hook download actions (playlist) val onClickListenerClass = - DownloadPlaylistButtonOnClickFingerprint.resultOrThrow().mutableMethod.let { + downloadPlaylistButtonOnClickFingerprint.methodOrThrow().let { val playlistDownloadActionInvokeIndex = indexOfPlaylistDownloadActionInvokeInstruction(it) - it.getInstructions().subList( + it.instructions.subList( playlistDownloadActionInvokeIndex - 10, playlistDownloadActionInvokeIndex, ).find { instruction -> @@ -88,29 +79,29 @@ object DownloadActionsPatch : BaseBytecodePatch( ?: throw PatchException("Could not find onClickListenerClass") } - context.findMethodOrThrow(onClickListenerClass) { + findMethodOrThrow(onClickListenerClass) { name == "onClick" }.apply { val insertIndex = indexOfFirstInstructionOrThrow { - opcode == Opcode.INVOKE_STATIC - && getReference()?.name == "isEmpty" + opcode == Opcode.INVOKE_STATIC && + getReference()?.name == "isEmpty" } val insertRegister = getInstruction(insertIndex).registerC addInstructions( insertIndex, """ - invoke-static {v$insertRegister}, $INTEGRATIONS_CLASS_DESCRIPTOR->inAppPlaylistDownloadButtonOnClick(Ljava/lang/String;)Ljava/lang/String; + invoke-static {v$insertRegister}, $EXTENSION_CLASS_DESCRIPTOR->inAppPlaylistDownloadButtonOnClick(Ljava/lang/String;)Ljava/lang/String; move-result-object v$insertRegister """ ) } - OfflinePlaylistEndpointFingerprint.resultOrThrow().mutableMethod.apply { + offlinePlaylistEndpointFingerprint.methodOrThrow().apply { val playlistIdParameter = parameterTypes.indexOf("Ljava/lang/String;") + 1 if (playlistIdParameter > 0) { addInstructionsWithLabels( 0, """ - invoke-static {p$playlistIdParameter}, $INTEGRATIONS_CLASS_DESCRIPTOR->inAppPlaylistDownloadMenuOnClick(Ljava/lang/String;)Z + invoke-static {p$playlistIdParameter}, $EXTENSION_CLASS_DESCRIPTOR->inAppPlaylistDownloadMenuOnClick(Ljava/lang/String;)Z move-result v0 if-eqz v0, :show_native_downloader return-void @@ -138,7 +129,7 @@ object DownloadActionsPatch : BaseBytecodePatch( targetIndex + 1, """ iget-object v$freeRegister, v$targetRegister, $playlistIdReference - invoke-static {v$freeRegister}, $INTEGRATIONS_CLASS_DESCRIPTOR->inAppPlaylistDownloadMenuOnClick(Ljava/lang/String;)Z + 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 @@ -152,16 +143,16 @@ object DownloadActionsPatch : BaseBytecodePatch( // region patch for show the playlist download button - SetPlaylistDownloadButtonVisibilityFingerprint - .alsoResolve(context, AccessibilityOfflineButtonSyncFingerprint).let { - it.mutableMethod.apply { - val insertIndex = it.scanResult.patternScanResult!!.startIndex + 2 + setPlaylistDownloadButtonVisibilityFingerprint + .matchOrThrow(accessibilityOfflineButtonSyncFingerprint).let { + it.method.apply { + val insertIndex = it.patternMatch!!.startIndex + 2 val insertRegister = getInstruction(insertIndex).registerA addInstructions( insertIndex, """ - invoke-static {}, $INTEGRATIONS_CLASS_DESCRIPTOR->overridePlaylistDownloadButtonVisibility()Z + invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->overridePlaylistDownloadButtonVisibility()Z move-result v$insertRegister """ ) @@ -170,17 +161,18 @@ object DownloadActionsPatch : BaseBytecodePatch( // endregion - /** - * Add settings - */ - SettingsPatch.addPreference( + // region add settings + + addPreference( arrayOf( "PREFERENCE_SCREEN: GENERAL", "SETTINGS: HOOK_BUTTONS", "SETTINGS: HOOK_DOWNLOAD_ACTIONS" - ) + ), + HOOK_DOWNLOAD_ACTIONS ) - SettingsPatch.updatePatchStatus(this) + // endregion + } -} \ No newline at end of file +} 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/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/fingerprints/LayoutSwitchFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/Fingerprints.kt similarity index 60% rename from src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/fingerprints/LayoutSwitchFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/Fingerprints.kt index 327df6160..f5d8e5b4b 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/fingerprints/LayoutSwitchFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/layoutswitch/Fingerprints.kt @@ -1,11 +1,22 @@ -package app.revanced.patches.youtube.general.layoutswitch.fingerprints +package app.revanced.patches.youtube.general.layoutswitch -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint +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 object LayoutSwitchFingerprint : MethodFingerprint( +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"), @@ -32,4 +43,4 @@ internal object LayoutSwitchFingerprint : MethodFingerprint( 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..79c5c029b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/loadingscreen/Fingerprints.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.youtube.general.loadingscreen + +import app.revanced.util.fingerprint.legacyFingerprint + +internal val gradientLoadingScreenPrimaryFingerprint = legacyFingerprint( + name = "gradientLoadingScreenPrimaryFingerprint", + literals = listOf(45412406L), +) + +internal val gradientLoadingScreenSecondaryFingerprint = legacyFingerprint( + name = "gradientLoadingScreenSecondaryFingerprint", + literals = listOf(45418917L), +) \ No newline at end of file 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..1cd08b8b2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/loadingscreen/GradientLoadingScreenPatch.kt @@ -0,0 +1,45 @@ +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 { + + mapOf( + gradientLoadingScreenPrimaryFingerprint to 45412406L, + gradientLoadingScreenSecondaryFingerprint to 45418917L + ).forEach { (fingerprint, literal) -> + fingerprint.injectLiteralInstructionBooleanCall( + literal, + "$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..6a5f0b04e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/miniplayer/Fingerprints.kt @@ -0,0 +1,163 @@ +@file:Suppress("SpellCheckingInspection") + +package app.revanced.patches.youtube.general.miniplayer + +import app.revanced.patches.youtube.utils.playservice.is_19_25_or_greater +import app.revanced.patches.youtube.utils.resourceid.floatyBarTopMargin +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.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 +import com.android.tools.smali.dexlib2.util.MethodUtil + +internal val miniplayerDimensionsCalculatorParentFingerprint = legacyFingerprint( + name = "miniplayerDimensionsCalculatorParentFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + literals = listOf(floatyBarTopMargin), +) + +internal val miniplayerModernAddViewListenerFingerprint = legacyFingerprint( + name = "miniplayerModernAddViewListenerFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "V", + parameters = listOf("Landroid/view/View;") +) + +internal val miniplayerModernCloseButtonFingerprint = legacyFingerprint( + name = "miniplayerModernCloseButtonFingerprint", + returnType = "Landroid/widget/ImageView;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(modernMiniPlayerClose), +) + +private var constructorMethodCount = 0 + +internal fun isMultiConstructorMethod() = constructorMethodCount > 1 + +internal val miniplayerModernConstructorFingerprint = legacyFingerprint( + name = "miniplayerModernConstructorFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + parameters = listOf("L"), + literals = listOf(45623000L), + customFingerprint = custom@{ method, classDef -> + classDef.methods.forEach { + if (MethodUtil.isConstructor(it)) constructorMethodCount += 1 + } + + if (!is_19_25_or_greater) + return@custom true + + // Double tap action (Used in YouTube 19.25.39+). + method.containsLiteralInstruction(45628823L) + && method.containsLiteralInstruction(45630429L) + } +) + +internal val miniplayerModernDragAndDropFingerprint = legacyFingerprint( + name = "miniplayerModernDragAndDropFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + parameters = listOf("L"), + literals = listOf(45628752L), +) + +internal val miniplayerModernEnabledFingerprint = legacyFingerprint( + name = "miniplayerModernEnabledFingerprint", + literals = listOf(45622882L), +) + +internal val miniplayerModernExpandButtonFingerprint = legacyFingerprint( + name = "miniplayerModernExpandButtonFingerprint", + returnType = "Landroid/widget/ImageView;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(modernMiniPlayerExpand), +) + +internal val miniplayerModernExpandCloseDrawablesFingerprint = legacyFingerprint( + name = "miniplayerModernExpandCloseDrawablesFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + literals = listOf(ytOutlinePictureInPictureWhite), +) + +internal val miniplayerModernForwardButtonFingerprint = legacyFingerprint( + name = "miniplayerModernForwardButtonFingerprint", + returnType = "Landroid/widget/ImageView;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(modernMiniPlayerForwardButton), +) + +internal val miniplayerModernOverlayViewFingerprint = legacyFingerprint( + name = "miniplayerModernOverlayViewFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(scrimOverlay), +) + +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 miniplayerOverrideFingerprint = legacyFingerprint( + name = "miniplayerOverrideFingerprint", + returnType = "L", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L"), + 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.CHECK_CAST, + Opcode.CHECK_CAST, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.IF_NEZ, + ) +) + +internal val youTubePlayerOverlaysLayoutFingerprint = legacyFingerprint( + name = "youTubePlayerOverlaysLayoutFingerprint", + customFingerprint = { _, classDef -> + classDef.type == YOUTUBE_PLAYER_OVERLAYS_LAYOUT_CLASS_NAME + } +) + +internal const val YOUTUBE_PLAYER_OVERLAYS_LAYOUT_CLASS_NAME = + "Lcom/google/android/apps/youtube/app/common/player/overlay/YouTubePlayerOverlaysLayout;" 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..6fa89f9ba --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/miniplayer/MiniplayerPatch.kt @@ -0,0 +1,376 @@ +package app.revanced.patches.youtube.general.miniplayer + +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.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.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.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.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.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.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.TypeReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter +import com.android.tools.smali.dexlib2.util.MethodUtil + +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 targetInstruction = getInstruction(iPutIndex) + val targetReference = (targetInstruction as ReferenceInstruction).reference + + addInstructions( + iPutIndex + 1, """ + invoke-static { v${targetInstruction.registerA} }, $EXTENSION_CLASS_DESCRIPTOR->getModernMiniplayerOverrideType(I)I + move-result v${targetInstruction.registerA} + # Original instruction + iput v${targetInstruction.registerA}, v${targetInstruction.registerB}, $targetReference + """ + ) + removeInstruction(iPutIndex) + } + + fun Pair.hookInflatedView( + literalValue: Long, + hookedClassType: String, + extensionMethodName: String, + ) { + methodOrThrow(miniplayerModernViewParentFingerprint).apply { + 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_LEGACY" + 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 (MethodUtil.isConstructor(it)) { + val iPutIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.IPUT && + getReference()?.type == "I" + } + + insertModernMiniplayerTypeOverride(iPutIndex) + } else if (isMultiConstructorMethod()) { + findReturnIndicesReversed().forEach { index -> + insertModernMiniplayerOverride( + index + ) + } + } + } + } + + if (is_19_25_or_greater) { + miniplayerModernEnabledFingerprint.injectLiteralInstructionBooleanCall( + 45622882L, + "$EXTENSION_CLASS_DESCRIPTOR->getModernMiniplayerOverride(Z)Z" + ) + } + + // endregion + + // region Enable double tap action. + + if (is_19_25_or_greater) { + miniplayerModernConstructorFingerprint.injectLiteralInstructionBooleanCall( + 45628823L, + "$EXTENSION_CLASS_DESCRIPTOR->enableMiniplayerDoubleTapAction()Z" + ) + miniplayerModernConstructorFingerprint.injectLiteralInstructionBooleanCall( + 45630429L, + "$EXTENSION_CLASS_DESCRIPTOR->getModernMiniplayerOverride(Z)Z" + ) + settingArray += "SETTINGS: MINIPLAYER_DOUBLE_TAP_ACTION" + } + + // 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.hookInflatedView( + literalValue, + "Landroid/widget/ImageView;", + "$EXTENSION_CLASS_DESCRIPTOR->$methodName(Landroid/widget/ImageView;)V" + ) + } + + miniplayerModernAddViewListenerFingerprint.methodOrThrow( + miniplayerModernViewParentFingerprint + ).apply { + addInstructionsWithLabels( + 0, + """ + invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->hideMiniplayerSubTexts(Landroid/view/View;)Z + move-result v0 + if-nez v0, :hidden + """, + ExternalLabel("hidden", getInstruction(implementation!!.instructions.lastIndex)) + ) + } + + + // 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. + 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 + + // region Enable drag and drop. + + if (is_19_23_or_greater) { + miniplayerModernDragAndDropFingerprint.injectLiteralInstructionBooleanCall( + 45628752L, + "$EXTENSION_CLASS_DESCRIPTOR->enableMiniplayerDragAndDrop()Z" + ) + settingArray += "SETTINGS: MINIPLAYER_DRAG_AND_DROP" + } + + // endregion + + settingArray += "SETTINGS: MINIPLAYER_TYPE_MODERN" + + // region add settings + + addPreference(settingArray, MINIPLAYER) + + // endregion + + } +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/general/music/fingerprints/AppDeepLinkFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/music/Fingerprints.kt similarity index 68% rename from src/main/kotlin/app/revanced/patches/youtube/general/music/fingerprints/AppDeepLinkFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/general/music/Fingerprints.kt index d4d7d2330..08009eeff 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/general/music/fingerprints/AppDeepLinkFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/music/Fingerprints.kt @@ -1,14 +1,15 @@ -package app.revanced.patches.youtube.general.music.fingerprints +package app.revanced.patches.youtube.general.music -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint +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 object AppDeepLinkFingerprint : MethodFingerprint( +internal val appDeepLinkFingerprint = legacyFingerprint( + name = "appDeepLinkFingerprint", returnType = "V", accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, parameters = listOf("L", "Ljava/util/Map;"), @@ -19,9 +20,9 @@ internal object AppDeepLinkFingerprint : MethodFingerprint( Opcode.CONST_STRING, ), strings = listOf("android.intent.action.VIEW"), - customFingerprint = { methodDef, _ -> - methodDef.indexOfFirstInstruction { + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { getReference()?.name == "appDeepLinkEndpoint" } >= 0 } -) \ No newline at end of file +) diff --git a/src/main/kotlin/app/revanced/patches/youtube/general/music/YouTubeMusicActionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/music/YouTubeMusicActionsPatch.kt similarity index 51% rename from src/main/kotlin/app/revanced/patches/youtube/general/music/YouTubeMusicActionsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/general/music/YouTubeMusicActionsPatch.kt index b62e7e57d..25666dd63 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/general/music/YouTubeMusicActionsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/music/YouTubeMusicActionsPatch.kt @@ -1,46 +1,46 @@ package app.revanced.patches.youtube.general.music -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.replaceInstruction -import app.revanced.patches.youtube.general.music.fingerprints.AppDeepLinkFingerprint +import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE -import app.revanced.patches.youtube.utils.gms.GmsCoreSupportResourcePatch.PackageNameYouTubeMusic -import app.revanced.patches.youtube.utils.integrations.Constants.GENERAL_PATH -import app.revanced.patches.youtube.utils.integrations.Constants.PATCH_STATUS_CLASS_DESCRIPTOR -import app.revanced.patches.youtube.utils.settings.SettingsBytecodePatch -import app.revanced.patches.youtube.utils.settings.SettingsPatch +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 app.revanced.util.patch.BaseBytecodePatch -import app.revanced.util.resultOrThrow -import app.revanced.util.valueOrThrow 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 -import java.io.Closeable + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$GENERAL_PATH/YouTubeMusicActionsPatch;" @Suppress("unused") -object YouTubeMusicActionsPatch : BaseBytecodePatch( - name = "Hook YouTube Music actions", - description = "Adds support for opening music in RVX Music using the in-app YouTube Music button.", - dependencies = setOf(SettingsPatch::class), - compatiblePackages = COMPATIBLE_PACKAGE, - fingerprints = setOf(AppDeepLinkFingerprint) -), Closeable { - private const val INTEGRATIONS_CLASS_DESCRIPTOR = - "$GENERAL_PATH/YouTubeMusicActionsPatch;" +val youtubeMusicActionsPatch = bytecodePatch( + HOOK_YOUTUBE_MUSIC_ACTIONS.title, + HOOK_YOUTUBE_MUSIC_ACTIONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) - override fun execute(context: BytecodeContext) { + dependsOn(settingsPatch) - AppDeepLinkFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val packageNameIndex = it.scanResult.patternScanResult!!.startIndex + execute { + + appDeepLinkFingerprint.matchOrThrow().let { + it.method.apply { + val packageNameIndex = it.patternMatch!!.startIndex val packageNameField = getInstruction(packageNameIndex).reference.toString() @@ -58,7 +58,7 @@ object YouTubeMusicActionsPatch : BaseBytecodePatch( addInstructions( index + 1, """ - invoke-static {v$register}, $INTEGRATIONS_CLASS_DESCRIPTOR->overridePackageName(Ljava/lang/String;)Ljava/lang/String; + invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->overridePackageName(Ljava/lang/String;)Ljava/lang/String; move-result-object v$register """ ) @@ -66,33 +66,34 @@ object YouTubeMusicActionsPatch : BaseBytecodePatch( } } - /** - * Add settings - */ - SettingsPatch.addPreference( + // region add settings + + addPreference( arrayOf( "PREFERENCE_SCREEN: GENERAL", "SETTINGS: HOOK_BUTTONS", "SETTINGS: HOOK_YOUTUBE_MUSIC_ACTIONS" - ) + ), + HOOK_YOUTUBE_MUSIC_ACTIONS ) - SettingsPatch.updatePatchStatus(this) + // endregion + } - override fun close() { - if (SettingsPatch.containsPatch("GmsCore support")) { - val musicPackageName = PackageNameYouTubeMusic.valueOrThrow() - SettingsPatch.contexts.addEntryValues( - "revanced_third_party_youtube_music_label", - "RVX Music" - ) - SettingsPatch.contexts.addEntryValues( - "revanced_third_party_youtube_music_package_name", - musicPackageName - ) - - SettingsBytecodePatch.contexts.findMethodOrThrow(PATCH_STATUS_CLASS_DESCRIPTOR) { + 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) @@ -101,10 +102,9 @@ object YouTubeMusicActionsPatch : BaseBytecodePatch( replaceInstruction( replaceIndex, - "const-string v$replaceRegister, \"$musicPackageName\"" + "const-string v$replaceRegister, \"$youtubeMusicPackageName\"" ) } } - } -} \ No newline at end of file +} 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..c1fbb878f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/Fingerprints.kt @@ -0,0 +1,65 @@ +package app.revanced.patches.youtube.general.navigation + +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 autoMotiveFingerprint = legacyFingerprint( + name = "autoMotiveFingerprint", + opcodes = listOf( + Opcode.GOTO, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ + ), + strings = listOf("Android Automotive") +) + +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 translucentNavigationBarFingerprint = legacyFingerprint( + name = "translucentNavigationBarFingerprint", + literals = listOf(45630927L), +) \ 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..8a41f9bd8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/navigation/NavigationBarComponentsPatch.kt @@ -0,0 +1,135 @@ +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.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.versionCheckPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +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.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 navigationBarComponentsPatch = bytecodePatch( + NAVIGATION_BAR_COMPONENTS.title, + NAVIGATION_BAR_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + 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( + 45630927L, + "$GENERAL_CLASS_DESCRIPTOR->enableTranslucentNavigationBar()Z" + ) + + 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") - 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 + + // 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/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/src/main/kotlin/app/revanced/patches/youtube/general/splashanimation/SplashAnimationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/general/splashanimation/SplashAnimationPatch.kt similarity index 50% rename from src/main/kotlin/app/revanced/patches/youtube/general/splashanimation/SplashAnimationPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/general/splashanimation/SplashAnimationPatch.kt index 30214d902..838c4e15e 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/general/splashanimation/SplashAnimationPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/splashanimation/SplashAnimationPatch.kt @@ -1,48 +1,41 @@ package app.revanced.patches.youtube.general.splashanimation -import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patches.youtube.general.splashanimation.fingerprints.SplashAnimationFingerprint -import app.revanced.patches.youtube.general.splashanimation.fingerprints.StartUpResourceIdFingerprint -import app.revanced.patches.youtube.general.splashanimation.fingerprints.StartUpResourceIdParentFingerprint +import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE -import app.revanced.patches.youtube.utils.integrations.Constants.GENERAL_CLASS_DESCRIPTOR -import app.revanced.patches.youtube.utils.resourceid.SharedResourceIdPatch -import app.revanced.patches.youtube.utils.settings.SettingsPatch -import app.revanced.util.patch.BaseBytecodePatch -import app.revanced.util.resultOrThrow +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") -object SplashAnimationPatch : BaseBytecodePatch( - name = "Disable splash animation", - description = "Adds an option to disable the splash animation on app startup.", - dependencies = setOf( - SettingsPatch::class, - SharedResourceIdPatch::class - ), - compatiblePackages = COMPATIBLE_PACKAGE, - fingerprints = setOf( - SplashAnimationFingerprint, - StartUpResourceIdParentFingerprint - ) +val splashAnimationPatch = bytecodePatch( + DISABLE_SPLASH_ANIMATION.title, + DISABLE_SPLASH_ANIMATION.summary, ) { - override fun execute(context: BytecodeContext) { + compatibleWith(COMPATIBLE_PACKAGE) - StartUpResourceIdFingerprint.resolve( - context, - StartUpResourceIdParentFingerprint.resultOrThrow().classDef - ) + dependsOn( + sharedResourceIdPatch, + settingsPatch, + ) - val startUpResourceIdMethod = StartUpResourceIdFingerprint.resultOrThrow().mutableMethod + execute { + + val startUpResourceIdMethod = + startUpResourceIdFingerprint.methodOrThrow(startUpResourceIdParentFingerprint) val startUpResourceIdMethodCall = startUpResourceIdMethod.definingClass + "->" + startUpResourceIdMethod.name + "(I)Z" - SplashAnimationFingerprint.resultOrThrow().let { - it.mutableMethod.apply { + 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) @@ -63,16 +56,17 @@ object SplashAnimationPatch : BaseBytecodePatch( } } - /** - * Add settings - */ - SettingsPatch.addPreference( + // region add settings + + addPreference( arrayOf( "PREFERENCE_SCREEN: GENERAL", "SETTINGS: DISABLE_SPLASH_ANIMATION" - ) + ), + DISABLE_SPLASH_ANIMATION ) - SettingsPatch.updatePatchStatus(this) + // 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..269bb1f39 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/spoofappversion/SpoofAppVersionPatch.kt @@ -0,0 +1,55 @@ +package app.revanced.patches.youtube.general.spoofappversion + +import app.revanced.patcher.patch.resourcePatch +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.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.versionCheckPatch +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.appendAppVersion + +@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;"), + settingsPatch, + versionCheckPatch, + ) + + execute { + + if (is_18_34_or_greater) { + appendAppVersion("18.33.40") + if (is_18_39_or_greater) { + appendAppVersion("18.38.45") + if (is_18_49_or_greater) { + appendAppVersion("18.48.39") + } + } + } + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: GENERAL", + "PREFERENCE_CATEGORY: GENERAL_EXPERIMENTAL_FLAGS", + "SETTINGS: SPOOF_APP_VERSION" + ), + SPOOF_APP_VERSION + ) + + // endregion + + } +} 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..00d8842c2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/Fingerprints.kt @@ -0,0 +1,216 @@ +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, _ -> + indexOfStaticInstruction(method) >= 0 + } +) + +internal fun indexOfStaticInstruction(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 && + indexOfStaticInstructions(method) >= 0 + } +) + +internal fun indexOfStartDelayInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "setStartDelay" + } + +internal fun indexOfStaticInstructions(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 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), +) + +internal val setWordMarkHeaderFingerprint = legacyFingerprint( + name = "setWordMarkHeaderFingerprint", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + returnType = "V", + parameters = listOf("Landroid/widget/ImageView;"), + opcodes = listOf( + Opcode.IGET_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.IF_NEZ, + Opcode.IGET_BOOLEAN, + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, + Opcode.CONST, + Opcode.INVOKE_STATIC, + ) +) + +@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..1a72c93b1 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/general/toolbar/ToolBarComponentsPatch.kt @@ -0,0 +1,413 @@ +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.extension.Constants.PATCH_STATUS_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.patch.PatchList.TOOLBAR_COMPONENTS +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.actionBarRingoBackground +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.youtube.utils.resourceid.voiceSearch +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.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.indexOfFirstInstructionReversedOrThrow +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import app.revanced.util.replaceLiteralInstructionCall +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.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 = indexOfStaticInstruction(this) + 1 + val targetRegister = getInstruction(targetIndex).registerA + + injectSearchBarHook( + targetIndex + 1, + targetRegister, + "enableWideSearchBarWithHeaderInverse" + ) + } + + actionBarRingoTextFingerprint.methodOrThrow(actionBarRingoBackgroundFingerprint).apply { + val targetIndex = indexOfStaticInstruction(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 startIndex = indexOfFirstLiteralInstructionOrThrow(voiceSearch) + val setOnClickListenerIndex = indexOfFirstInstructionOrThrow(startIndex) { + 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/src/main/kotlin/app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch.kt similarity index 70% rename from src/main/kotlin/app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch.kt index 60297f3e4..1f1dac491 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/actionbuttons/ShortsActionButtonsPatch.kt @@ -1,26 +1,29 @@ package app.revanced.patches.youtube.layout.actionbuttons -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption +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.settings.SettingsPatch +import app.revanced.patches.youtube.utils.patch.PatchList.CUSTOM_SHORTS_ACTION_BUTTONS +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.lowerCaseOrThrow -import app.revanced.util.patch.BaseResourcePatch + +private const val DEFAULT_ICON = "cairo" +private const val YOUTUBE_ICON = "youtube" @Suppress("unused") -object ShortsActionButtonsPatch : BaseResourcePatch( - name = "Custom Shorts action buttons", - description = "Changes, at compile time, the icon of the action buttons of the Shorts player.", - dependencies = setOf(SettingsPatch::class), - compatiblePackages = COMPATIBLE_PACKAGE +val shortsActionButtonsPatch = resourcePatch( + CUSTOM_SHORTS_ACTION_BUTTONS.title, + CUSTOM_SHORTS_ACTION_BUTTONS.summary, ) { - private const val DEFAULT_ICON = "cairo" - private const val YOUTUBE_ICON = "youtube" + compatibleWith(COMPATIBLE_PACKAGE) - private val IconType = stringPatchOption( - key = "IconType", + dependsOn(settingsPatch) + + val iconType = stringOption( + key = "iconType", default = DEFAULT_ICON, values = mapOf( "Cairo" to DEFAULT_ICON, @@ -32,19 +35,19 @@ object ShortsActionButtonsPatch : BaseResourcePatch( ), title = "Shorts icon style ", description = "The style of the icons for the action buttons in the Shorts player.", - required = true + required = true, ) - override fun execute(context: ResourceContext) { + execute { // Check patch options first. - val iconType = IconType + val iconType = iconType .lowerCaseOrThrow() if (iconType == YOUTUBE_ICON) { println("INFO: Shorts action buttons will remain unchanged as it matches the original.") - SettingsPatch.updatePatchStatus(this) - return + addPreference(CUSTOM_SHORTS_ACTION_BUTTONS) + return@execute } arrayOf( @@ -54,7 +57,7 @@ object ShortsActionButtonsPatch : BaseResourcePatch( "hdpi", "mdpi" ).forEach { dpi -> - context.copyResources( + copyResources( "youtube/shorts/actionbuttons/$iconType", ResourceGroup( "drawable-$dpi", @@ -81,12 +84,13 @@ object ShortsActionButtonsPatch : BaseResourcePatch( ) } + addPreference(CUSTOM_SHORTS_ACTION_BUTTONS) + if (iconType == DEFAULT_ICON) { - SettingsPatch.updatePatchStatus(this) - return + return@execute } - context.copyResources( + copyResources( "youtube/shorts/actionbuttons/shared", ResourceGroup( "drawable", @@ -96,6 +100,5 @@ object ShortsActionButtonsPatch : BaseResourcePatch( ) ) - SettingsPatch.updatePatchStatus(this) } } 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..d63b02506 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/icon/CustomBrandingIconPatch.kt @@ -0,0 +1,185 @@ +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.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.copyFile +import app.revanced.util.copyResources +import app.revanced.util.copyXmlNode +import app.revanced.util.getResourceGroup +import app.revanced.util.underBarOrThrow + +private const val DEFAULT_ICON = "revancify_blue" + +private val availableIcon = mapOf( + "AFN Blue" to "afn_blue", + "AFN Red" to "afn_red", + "MMT" to "mmt", + "Revancify Blue" to DEFAULT_ICON, + "Revancify Red" to "revancify_red", + "YouTube" to "youtube" +) + +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( + "adaptiveproduct_youtube_background_color_108", + "adaptiveproduct_youtube_foreground_color_108", + "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, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + + 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. + val appIcon = appIconOption.underBarOrThrow() + + val appIconResourcePath = "youtube/branding/$appIcon" + + + // Check if a custom path is used in the patch options. + if (!availableIcon.containsValue(appIcon)) { + val copiedFiles = copyFile( + launcherIconResourceGroups, + appIcon, + "WARNING: 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_monochrome_ic_youtube_launcher.xml" + ) + ).forEach { resourceGroup -> + copyResources("$appIconResourcePath/monochrome", resourceGroup) + } + + // Change splash icon. + if (changeSplashIconOption == true) { + splashIconResourceGroups.let { resourceGroups -> + resourceGroups.forEach { + copyResources("$appIconResourcePath/splash", it) + } + } + } + + // Change splash screen. + if (restoreOldSplashAnimationOption == true) { + oldSplashAnimationResourceGroups.let { resourceGroups -> + resourceGroups.forEach { + copyResources("$appIconResourcePath/splash", it) + } + } + + copyXmlNode( + "$appIconResourcePath/splash", + "values-v31/styles.xml", + "resources" + ) + } + + updatePatchStatusIcon(appIcon) + } + } +} 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..a1fe0d202 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/name/CustomBrandingNamePatch.kt @@ -0,0 +1,59 @@ +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, +) { + 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/src/main/kotlin/app/revanced/patches/youtube/layout/doubletaplength/DoubleTapLengthPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/doubletaplength/DoubleTapLengthPatch.kt similarity index 53% rename from src/main/kotlin/app/revanced/patches/youtube/layout/doubletaplength/DoubleTapLengthPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/layout/doubletaplength/DoubleTapLengthPatch.kt index 83c3d816e..f9348f918 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/layout/doubletaplength/DoubleTapLengthPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/doubletaplength/DoubleTapLengthPatch.kt @@ -1,53 +1,59 @@ package app.revanced.patches.youtube.layout.doubletaplength -import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption +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.settings.SettingsPatch +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.patch.BaseResourcePatch +import app.revanced.util.valueOrThrow import java.nio.file.Files -@Suppress("DEPRECATION", "unused") -object DoubleTapLengthPatch : BaseResourcePatch( - name = "Custom double tap length", - description = "Adds Double-tap to seek values that are specified in options.json.", - dependencies = setOf(SettingsPatch::class), - compatiblePackages = COMPATIBLE_PACKAGE +@Suppress("unused") +val doubleTapLengthPatch = resourcePatch( + CUSTOM_DOUBLE_TAP_LENGTH.title, + CUSTOM_DOUBLE_TAP_LENGTH.summary, ) { - private val DoubleTapLengthArrays by stringPatchOption( - key = "DoubleTapLengthArrays", + 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 + required = true, ) - override fun execute(context: ResourceContext) { + execute { + // Check patch options first. + val doubleTapLengthArrays = doubleTapLengthArraysOption + .valueOrThrow() // Check patch options first. - val splits = DoubleTapLengthArrays - ?.replace(" ", "") - ?.split(",") - ?: throw PatchException("Invalid double-tap length array.") - if (splits.isEmpty()) throw IllegalArgumentException("Invalid double-tap length elements") + 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 = context["res"].resolve("values-v21") + val valuesV21Directory = get("res").resolve("values-v21") if (!valuesV21Directory.isDirectory) Files.createDirectories(valuesV21Directory.toPath()) /** * Copy arrays */ - context.copyResources( + copyResources( "youtube/doubletap", ResourceGroup( "values-v21", @@ -56,18 +62,19 @@ object DoubleTapLengthPatch : BaseResourcePatch( ) for (index in 0 until splits.count()) { - context.addEntryValues( + addEntryValues( entryValueName, lengthElements[index], path = arrayPath ) - context.addEntryValues( + addEntryValues( entriesName, lengthElements[index], path = arrayPath ) } - SettingsPatch.updatePatchStatus(this) + addPreference(CUSTOM_DOUBLE_TAP_LENGTH) + } -} \ No newline at end of file +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/layout/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/header/ChangeHeaderPatch.kt similarity index 56% rename from src/main/kotlin/app/revanced/patches/youtube/layout/header/ChangeHeaderPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/layout/header/ChangeHeaderPatch.kt index 57b2c90dc..93ae7b70d 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/layout/header/ChangeHeaderPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/header/ChangeHeaderPatch.kt @@ -1,77 +1,75 @@ package app.revanced.patches.youtube.layout.header -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption -import app.revanced.patches.youtube.utils.compatibility.Constants -import app.revanced.patches.youtube.utils.integrations.Constants.PATCH_STATUS_CLASS_DESCRIPTOR -import app.revanced.patches.youtube.utils.settings.ResourceUtils -import app.revanced.patches.youtube.utils.settings.SettingsBytecodePatch +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.trimIndentMultiline import app.revanced.util.copyFile import app.revanced.util.copyResources -import app.revanced.util.patch.BaseResourcePatch import app.revanced.util.underBarOrThrow -import app.revanced.util.updatePatchStatus import java.io.File import java.nio.file.Files import kotlin.io.path.copyTo import kotlin.io.path.exists -@Suppress("DEPRECATION", "unused") -object ChangeHeaderPatch : BaseResourcePatch( - name = "Custom header for YouTube", - description = "Applies a custom header in the top left corner within the app.", - compatiblePackages = Constants.COMPATIBLE_PACKAGE, - use = false, +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, ) { - private const val GENERIC_HEADER_FILE_NAME = "yt_wordmark_header" - private const val PREMIUM_HEADER_FILE_NAME = "yt_premium_wordmark_header" + compatibleWith(COMPATIBLE_PACKAGE) - /** - * - */ - 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" + dependsOn(settingsPatch) - 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(), - ) - } - - private val CustomHeader = stringPatchOption( - key = "CustomHeader", + val customHeaderOption = stringOption( + key = "customHeader", default = DEFAULT_HEADER_VALUE, values = mapOf( DEFAULT_HEADER_KEY to DEFAULT_HEADER_VALUE @@ -115,20 +113,19 @@ object ChangeHeaderPatch : BaseResourcePatch( required = true, ) - override fun execute(context: ResourceContext) { - + execute { // Check patch options first. - val customHeader = CustomHeader + val customHeader = customHeaderOption .underBarOrThrow() - val customBrandingIconType = ResourceUtils.getIconType() + val customBrandingIconType = getIconType() val customBrandingIconIncluded = customBrandingIconType != "default" && customBrandingIconType != "custom" val warnings = "WARNING: Invalid header path: $customHeader. Does not apply patches." if (customHeader != DEFAULT_HEADER_VALUE) { - context.copyFile( + copyFile( headerIconResourceGroups, customHeader, warnings @@ -136,19 +133,12 @@ object ChangeHeaderPatch : BaseResourcePatch( } else if (customBrandingIconIncluded) { headerIconResourceGroups.let { resourceGroups -> resourceGroups.forEach { - context.copyResources("youtube/branding/$customBrandingIconType/header", it) + copyResources("youtube/branding/$customBrandingIconType/header", it) } } - - if (customBrandingIconType == "youtube_minimal_header") { - SettingsBytecodePatch.contexts.updatePatchStatus( - PATCH_STATUS_CLASS_DESCRIPTOR, - "MinimalHeader" - ) - } } else { println(warnings) - return + return@execute } // The size of the new header is the same, only the file name is different. @@ -158,7 +148,7 @@ object ChangeHeaderPatch : BaseResourcePatch( GENERIC_HEADER_FILE_NAME to NEW_GENERIC_HEADER_FILE_NAME ).forEach { (original, replacement) -> premiumHeaderResourceDirectoryNames.keys.forEach { - context["res"].resolve(it).takeIf(File::exists)?.toPath()?.let { path -> + get("res").resolve(it).takeIf(File::exists)?.toPath()?.let { path -> variants.forEach { mode -> val newHeaderPath = path.resolve("${replacement}_$mode.webp") @@ -176,5 +166,6 @@ object ChangeHeaderPatch : BaseResourcePatch( } } } + } } 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..fc3df243a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/playerbuttonbg/PlayerButtonBackgroundPatch.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.youtube.layout.playerbuttonbg + +import app.revanced.patcher.patch.resourcePatch +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 + +@Suppress("unused") +val playerButtonBackgroundPatch = resourcePatch( + FORCE_HIDE_PLAYER_BUTTONS_BACKGROUND.title, + FORCE_HIDE_PLAYER_BUTTONS_BACKGROUND.summary, + false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn(settingsPatch) + + 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 = "@android:color/transparent" + } + } + } + + 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..21585d2a4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/MaterialYouPatch.kt @@ -0,0 +1,53 @@ +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.ResourceGroup +import app.revanced.util.copyResources +import app.revanced.util.copyXmlNode + +@Suppress("unused") +val materialYouPatch = resourcePatch( + MATERIALYOU.title, + MATERIALYOU.summary, + false, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + sharedThemePatch, + settingsPatch, + ) + + execute { + arrayOf( + ResourceGroup( + "drawable-night-v31", + "new_content_dot_background.xml" + ), + ResourceGroup( + "drawable-v31", + "new_content_count_background.xml", + "new_content_dot_background.xml" + ), + ResourceGroup( + "layout-v31", + "new_content_count.xml" + ) + ).forEach { + get("res/${it.resourceDirectoryName}").mkdirs() + copyResources("youtube/materialyou", it) + } + + 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..89d7f1133 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/SharedThemePatch.kt @@ -0,0 +1,141 @@ +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 app.revanced.patches.youtube.utils.playservice.is_19_32_or_greater +import app.revanced.patches.youtube.utils.playservice.versionCheckPatch +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, + versionCheckPatch, + ) + + execute { + addDrawableColorHook("$UTILS_PATH/DrawableColorPatch;->getColor(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" -> SPLASH_SCREEN_COLOR_ATTRIBUTE + else -> "null" + } + + else -> "null" + } + ) + ) + + if (this.textContent != "null") + node.appendChild(this) + } + } + } + } + + setOf( + "res/drawable/quantum_launchscreen_youtube.xml", + "res/drawable-sw600dp/quantum_launchscreen_youtube.xml" + ).forEach editSplashScreen@{ resourceFile -> + document(resourceFile).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") + } + } + + if (is_19_32_or_greater) { + // 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. + 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 == "Theme.YouTube.Launcher" || nodeAttributeName == "Theme.YouTube.Launcher.Cairo") { + 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/layout/theme/ThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt new file mode 100644 index 000000000..bb94e8a30 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt @@ -0,0 +1,130 @@ +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", + ) + + 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..a72aa6957 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/translations/TranslationsPatch.kt @@ -0,0 +1,68 @@ +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 + +// 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" + ) + + 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..610481143 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/visual/VisualPreferencesIconsPatch.kt @@ -0,0 +1,446 @@ +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.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", + ), + 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 + ) + + lateinit var preferenceIcon: Map + + fun Set.setPreferenceIcon() = associateWith { title -> + when (title) { + // Main RVX settings + "revanced_preference_screen_general" -> "general_key_icon" + "revanced_preference_screen_sb" -> "sb_enable_create_segment_icon" + + // Internal RVX settings + "revanced_alt_thumbnail_home" -> "revanced_hide_navigation_home_button_icon" + "revanced_alt_thumbnail_library" -> "revanced_preference_screen_video_icon" + "revanced_alt_thumbnail_player" -> "revanced_preference_screen_player_icon" + "revanced_alt_thumbnail_search" -> "revanced_hide_shorts_shelf_search_icon" + "revanced_alt_thumbnail_subscriptions" -> "revanced_hide_navigation_subscriptions_button_icon" + "revanced_change_share_sheet" -> "revanced_hide_shorts_share_button_icon" + "revanced_custom_player_overlay_opacity" -> "revanced_swipe_overlay_background_alpha_icon" + "revanced_default_app_settings" -> "revanced_preference_screen_settings_menu_icon" + "revanced_default_playback_speed" -> "revanced_overlay_button_speed_dialog_icon" + "revanced_enable_old_quality_layout" -> "revanced_default_video_quality_wifi_icon" + "revanced_enable_watch_panel_gestures" -> "revanced_preference_screen_swipe_controls_icon" + "revanced_hide_download_button" -> "revanced_overlay_button_external_downloader_icon" + "revanced_hide_keyword_content_comments" -> "revanced_hide_quick_actions_comment_button_icon" + "revanced_hide_keyword_content_home" -> "revanced_hide_navigation_home_button_icon" + "revanced_hide_keyword_content_search" -> "revanced_hide_shorts_shelf_search_icon" + "revanced_hide_keyword_content_subscriptions" -> "revanced_hide_navigation_subscriptions_button_icon" + "revanced_hide_like_dislike_button" -> "sb_enable_voting_icon" + "revanced_hide_navigation_library_button" -> "revanced_preference_screen_video_icon" + "revanced_hide_navigation_notifications_button" -> "notification_key_icon" + "revanced_hide_navigation_shorts_button" -> "revanced_preference_screen_shorts_icon" + "revanced_hide_player_autoplay_button" -> "revanced_change_player_flyout_menu_toggle_icon" + "revanced_hide_player_captions_button" -> "captions_key_icon" + "revanced_hide_player_flyout_menu_ambient_mode" -> "revanced_preference_screen_ambient_mode_icon" + "revanced_hide_player_flyout_menu_captions" -> "captions_key_icon" + "revanced_hide_player_flyout_menu_listen_with_youtube_music" -> "revanced_hide_player_youtube_music_button_icon" + "revanced_hide_player_flyout_menu_loop_video" -> "revanced_overlay_button_always_repeat_icon" + "revanced_hide_player_flyout_menu_more_info" -> "about_key_icon" + "revanced_hide_player_flyout_menu_pip" -> "offline_key_icon" + "revanced_hide_player_flyout_menu_premium_controls" -> "premium_early_access_browse_page_key_icon" + "revanced_hide_player_flyout_menu_quality_header" -> "revanced_default_video_quality_wifi_icon" + "revanced_hide_player_flyout_menu_report" -> "revanced_hide_report_button_icon" + "revanced_hide_player_fullscreen_button" -> "revanced_preference_screen_fullscreen_icon" + "revanced_hide_quick_actions_dislike_button" -> "revanced_preference_screen_ryd_icon" + "revanced_hide_quick_actions_live_chat_button" -> "live_chat_key_icon" + "revanced_hide_quick_actions_save_to_playlist_button" -> "revanced_hide_playlist_button_icon" + "revanced_hide_quick_actions_share_button" -> "revanced_hide_shorts_share_button_icon" + "revanced_hide_remix_button" -> "revanced_hide_shorts_remix_button_icon" + "revanced_hide_share_button" -> "revanced_hide_shorts_share_button_icon" + "revanced_hide_shorts_comments_button" -> "revanced_hide_quick_actions_comment_button_icon" + "revanced_hide_shorts_dislike_button" -> "revanced_preference_screen_ryd_icon" + "revanced_hide_shorts_like_button" -> "revanced_hide_quick_actions_like_button_icon" + "revanced_hide_shorts_navigation_bar" -> "revanced_preference_screen_navigation_bar_icon" + "revanced_hide_shorts_shelf_home_related_videos" -> "revanced_hide_navigation_home_button_icon" + "revanced_hide_shorts_shelf_subscriptions" -> "revanced_hide_navigation_subscriptions_button_icon" + "revanced_hide_shorts_toolbar" -> "revanced_preference_screen_toolbar_icon" + "revanced_hide_toolbar_cast_button" -> "revanced_hide_player_cast_button_icon" + "revanced_hide_toolbar_create_button" -> "revanced_hide_navigation_create_button_icon" + "revanced_hide_toolbar_notification_button" -> "notification_key_icon" + "revanced_preference_screen_account_menu" -> "account_switcher_key_icon" + "revanced_preference_screen_channel_bar" -> "account_switcher_key_icon" + "revanced_preference_screen_channel_profile" -> "account_switcher_key_icon" + "revanced_preference_screen_comments" -> "revanced_hide_quick_actions_comment_button_icon" + "revanced_preference_screen_feed_flyout_menu" -> "revanced_preference_screen_player_flyout_menu_icon" + "revanced_preference_screen_haptic_feedback" -> "revanced_enable_swipe_haptic_feedback_icon" + "revanced_preference_screen_hook_buttons" -> "revanced_preference_screen_import_export_icon" + "revanced_preference_screen_miniplayer" -> "offline_key_icon" + "revanced_preference_screen_patch_information" -> "about_key_icon" + "revanced_preference_screen_shorts_player" -> "revanced_preference_screen_shorts_icon" + "revanced_preference_screen_video_filter" -> "revanced_preference_screen_video_icon" + "revanced_preference_screen_watch_history" -> "history_key_icon" + "revanced_swipe_gestures_lock_mode" -> "revanced_hide_player_flyout_menu_lock_screen_icon" + "revanced_disable_hdr_auto_brightness" -> "revanced_disable_hdr_video_icon" + else -> "${title}_icon" + } + } + + execute { + // Check patch options first. + val selectedIconType = settingsMenuIconOption + .underBarOrThrow() + + val appIconOption = customBrandingIconPatch + .getStringOptionValue("appIcon") + + val customBrandingIconType = appIconOption + .underBarOrThrow() + + if (applyToAll == true) { + preferenceKey += rvxPreferenceKey + } + + preferenceIcon = preferenceKey.setPreferenceIcon() + + // region copy shared resources. + + arrayOf( + ResourceGroup( + "drawable", + *preferenceIcon.values.map { "$it.xml" }.toTypedArray() + ), + ResourceGroup( + "drawable-xxhdpi", + "$EMPTY_ICON.png" + ), + ).forEach { resourceGroup -> + copyResources("youtube/visual/shared", 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 -> preferenceIcon[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 = setOf( + // YouTube settings. + "about_key", + "accessibility_settings_key", + "account_switcher_key", + "auto_play_key", + "billing_and_payment_key", + "captions_key", + "connected_accounts_browse_page_key", + "data_saving_settings_key", + "general_key", + "history_key", + "live_chat_key", + "notification_key", + "offline_key", + "pair_with_tv_key", + "parent_tools_key", + "premium_early_access_browse_page_key", + "privacy_key", + "subscription_product_setting_key", + "video_quality_settings_key", + "your_data_key", + + // RVX settings. + "revanced_preference_screen_ads", + "revanced_preference_screen_alt_thumbnails", + "revanced_preference_screen_feed", + "revanced_preference_screen_general", + "revanced_preference_screen_player", + "revanced_preference_screen_shorts", + "revanced_preference_screen_swipe_controls", + "revanced_preference_screen_video", + "revanced_preference_screen_ryd", + "revanced_preference_screen_return_youtube_username", + "revanced_preference_screen_sb", + "revanced_preference_screen_misc", +) + +private var rvxPreferenceKey = setOf( + // Internal RVX settings (items without prefix are listed first, others are sorted alphabetically) + "gms_core_settings", + "sb_enable_create_segment", + "sb_enable_voting", + + "revanced_alt_thumbnail_home", + "revanced_alt_thumbnail_library", + "revanced_alt_thumbnail_player", + "revanced_alt_thumbnail_search", + "revanced_alt_thumbnail_subscriptions", + "revanced_change_share_sheet", + "revanced_change_shorts_repeat_state", + "revanced_custom_player_overlay_opacity", + "revanced_default_app_settings", + "revanced_default_playback_speed", + "revanced_default_video_quality_wifi", + "revanced_disable_default_playback_speed_music", + "revanced_disable_hdr_auto_brightness", + "revanced_disable_hdr_video", + "revanced_disable_quic_protocol", + "revanced_enable_debug_logging", + "revanced_enable_default_playback_speed_shorts", + "revanced_enable_external_browser", + "revanced_enable_old_quality_layout", + "revanced_enable_open_links_directly", + "revanced_enable_opus_codec", + "revanced_enable_save_and_restore_brightness", + "revanced_enable_swipe_brightness", + "revanced_enable_swipe_haptic_feedback", + "revanced_enable_swipe_lowest_value_auto_brightness", + "revanced_enable_swipe_press_to_engage", + "revanced_enable_swipe_to_switch_video", + "revanced_enable_swipe_volume", + "revanced_enable_watch_panel_gestures", + "revanced_hide_clip_button", + "revanced_hide_download_button", + "revanced_hide_keyword_content_comments", + "revanced_hide_keyword_content_home", + "revanced_hide_keyword_content_search", + "revanced_hide_keyword_content_subscriptions", + "revanced_hide_like_dislike_button", + "revanced_hide_navigation_create_button", + "revanced_hide_navigation_home_button", + "revanced_hide_navigation_library_button", + "revanced_hide_navigation_notifications_button", + "revanced_hide_navigation_shorts_button", + "revanced_hide_navigation_subscriptions_button", + "revanced_hide_player_autoplay_button", + "revanced_hide_player_captions_button", + "revanced_hide_player_cast_button", + "revanced_hide_player_collapse_button", + "revanced_hide_player_flyout_menu_ambient_mode", + "revanced_hide_player_flyout_menu_audio_track", + "revanced_hide_player_flyout_menu_captions", + "revanced_hide_player_flyout_menu_help", + "revanced_hide_player_flyout_menu_listen_with_youtube_music", + "revanced_hide_player_flyout_menu_lock_screen", + "revanced_hide_player_flyout_menu_loop_video", + "revanced_hide_player_flyout_menu_more_info", + "revanced_hide_player_flyout_menu_pip", + "revanced_hide_player_flyout_menu_premium_controls", + "revanced_hide_player_flyout_menu_playback_speed", + "revanced_hide_player_flyout_menu_quality_header", + "revanced_hide_player_flyout_menu_report", + "revanced_hide_player_flyout_menu_stable_volume", + "revanced_hide_player_flyout_menu_stats_for_nerds", + "revanced_hide_player_flyout_menu_watch_in_vr", + "revanced_hide_player_fullscreen_button", + "revanced_hide_player_previous_next_button", + "revanced_hide_player_youtube_music_button", + "revanced_hide_playlist_button", + "revanced_hide_quick_actions_comment_button", + "revanced_hide_quick_actions_dislike_button", + "revanced_hide_quick_actions_like_button", + "revanced_hide_quick_actions_live_chat_button", + "revanced_hide_quick_actions_more_button", + "revanced_hide_quick_actions_save_to_playlist_button", + "revanced_hide_quick_actions_share_button", + "revanced_hide_remix_button", + "revanced_hide_report_button", + "revanced_hide_rewards_button", + "revanced_hide_share_button", + "revanced_hide_shop_button", + "revanced_hide_shorts_comments_button", + "revanced_hide_shorts_dislike_button", + "revanced_hide_shorts_like_button", + "revanced_hide_shorts_navigation_bar", + "revanced_hide_shorts_remix_button", + "revanced_hide_shorts_share_button", + "revanced_hide_shorts_shelf_history", + "revanced_hide_shorts_shelf_home_related_videos", + "revanced_hide_shorts_shelf_search", + "revanced_hide_shorts_shelf_subscriptions", + "revanced_hide_shorts_toolbar", + "revanced_hide_thanks_button", + "revanced_hide_toolbar_cast_button", + "revanced_hide_toolbar_create_button", + "revanced_hide_toolbar_notification_button", + "revanced_overlay_button_always_repeat", + "revanced_overlay_button_copy_video_url", + "revanced_overlay_button_copy_video_url_timestamp", + "revanced_overlay_button_mute_volume", + "revanced_overlay_button_external_downloader", + "revanced_overlay_button_play_all", + "revanced_overlay_button_speed_dialog", + "revanced_overlay_button_whitelist", + "revanced_preference_screen_account_menu", + "revanced_preference_screen_action_buttons", + "revanced_preference_screen_ambient_mode", + "revanced_preference_screen_category_bar", + "revanced_preference_screen_channel_bar", + "revanced_preference_screen_channel_profile", + "revanced_preference_screen_comments", + "revanced_preference_screen_community_posts", + "revanced_preference_screen_custom_filter", + "revanced_preference_screen_feed_flyout_menu", + "revanced_preference_screen_fullscreen", + "revanced_preference_screen_haptic_feedback", + "revanced_preference_screen_hook_buttons", + "revanced_preference_screen_import_export", + "revanced_preference_screen_miniplayer", + "revanced_preference_screen_navigation_bar", + "revanced_preference_screen_patch_information", + "revanced_preference_screen_player_buttons", + "revanced_preference_screen_player_flyout_menu", + "revanced_preference_screen_seekbar", + "revanced_preference_screen_settings_menu", + "revanced_preference_screen_shorts_player", + "revanced_preference_screen_spoof_streaming_data", + "revanced_preference_screen_toolbar", + "revanced_preference_screen_video_description", + "revanced_preference_screen_video_filter", + "revanced_preference_screen_watch_history", + "revanced_sanitize_sharing_links", + "revanced_swipe_gestures_lock_mode", + "revanced_swipe_magnitude_threshold", + "revanced_swipe_overlay_background_alpha", + "revanced_swipe_overlay_rect_size", + "revanced_swipe_overlay_text_size", + "revanced_swipe_overlay_timeout", + "revanced_switch_create_with_notifications_button", + "revanced_change_player_flyout_menu_toggle", +) + +private val intentKey = setOf( + "revanced_extended_settings_key", +) + +val intentIcon = intentKey.associateWith { "${it}_icon" } + +private val emptyTitles = setOf( + "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_hide_shorts_comments_disabled_button", + "revanced_hide_player_flyout_menu_captions_footer", + "revanced_hide_player_flyout_menu_quality_footer", + "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_enable_debug_buffer_logging", + "revanced_whitelist_settings", +) \ No newline at end of file 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..06d763e8c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt @@ -0,0 +1,105 @@ +package app.revanced.patches.youtube.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.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.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.findInstructionIndicesReversedOrThrow +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.getWalkerMethod +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 + +@Suppress("unused") +val backgroundPlaybackPatch = bytecodePatch( + REMOVE_BACKGROUND_PLAYBACK_RESTRICTIONS.title, + REMOVE_BACKGROUND_PLAYBACK_RESTRICTIONS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + playerTypeHookPatch, + settingsPatch, + ) + + execute { + + backgroundPlaybackManagerFingerprint.methodOrThrow().apply { + findInstructionIndicesReversedOrThrow(Opcode.RETURN).forEach { index -> + val register = getInstruction(index).registerA + + // Replace to preserve control flow label. + replaceInstruction( + index, + "invoke-static { v$register }, $MISC_PATH/BackgroundPlaybackPatch;->allowBackgroundPlayback(Z)Z" + ) + + addInstructions( + index + 1, + """ + move-result v$register + return v$register + """ + ) + } + } + + // Enable background playback option in YouTube settings + backgroundPlaybackSettingsFingerprint.methodOrThrow().apply { + val booleanCalls = implementation!!.instructions.withIndex() + .filter { instruction -> + ((instruction.value as? ReferenceInstruction)?.reference as? MethodReference)?.returnType == "Z" + } + + val booleanIndex = booleanCalls.elementAt(1).index + val booleanMethod = getWalkerMethod(booleanIndex) + + booleanMethod.addInstructions( + 0, """ + const/4 v0, 0x1 + return v0 + """ + ) + } + + // Force allowing background play for videos labeled for kids. + kidsBackgroundPlaybackPolicyControllerFingerprint.methodOrThrow( + kidsBackgroundPlaybackPolicyControllerParentFingerprint + ).addInstruction( + 0, + "return-void" + ) + + pipControllerFingerprint.matchOrThrow().let { + val targetMethod = + it.getWalkerMethod(it.patternMatch!!.endIndex) + + targetMethod.apply { + val targetRegister = getInstruction(0).registerA + + addInstruction( + 1, + "const/4 v$targetRegister, 0x1" + ) + } + } + + // region add settings + + addPreference(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..5ef3d2c08 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/Fingerprints.kt @@ -0,0 +1,69 @@ +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 pipControllerFingerprint = legacyFingerprint( + name = "pipControllerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + opcodes = listOf( + Opcode.IF_NEZ, + Opcode.INVOKE_DIRECT + ), + literals = listOf(151635310L), +) \ 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..58fb7cbc5 --- /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( + lithoFilterPatch, + sharedResourceIdPatch, + settingsPatch, + ) + + 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..df6df8817 --- /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( + lithoFilterPatch, + settingsPatch, + ) + + execute { + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + + // region add settings + + addPreference( + arrayOf( + "PREFERENCE_SCREEN: PLAYER", + "SETTINGS: HIDE_ACTION_BUTTONS" + ), + HIDE_ACTION_BUTTONS + ) + + // endregion + + } +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/player/ambientmode/AmbientModeSwitchPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/ambientmode/AmbientModeSwitchPatch.kt similarity index 64% rename from src/main/kotlin/app/revanced/patches/youtube/player/ambientmode/AmbientModeSwitchPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/player/ambientmode/AmbientModeSwitchPatch.kt index f5179bfe7..6f99c8fda 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/player/ambientmode/AmbientModeSwitchPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/ambientmode/AmbientModeSwitchPatch.kt @@ -1,49 +1,43 @@ package app.revanced.patches.youtube.player.ambientmode -import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patches.youtube.player.ambientmode.fingerprints.AmbientModeInFullscreenFingerprint -import app.revanced.patches.youtube.player.ambientmode.fingerprints.PowerSaveModeBroadcastReceiverFingerprint -import app.revanced.patches.youtube.player.ambientmode.fingerprints.PowerSaveModeSyntheticFingerprint +import app.revanced.patcher.patch.bytecodePatch import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE -import app.revanced.patches.youtube.utils.integrations.Constants.PLAYER_CLASS_DESCRIPTOR -import app.revanced.patches.youtube.utils.settings.SettingsPatch +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.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.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionReversedOrThrow import app.revanced.util.indexOfFirstStringInstructionOrThrow -import app.revanced.util.injectLiteralInstructionBooleanCall -import app.revanced.util.patch.BaseBytecodePatch -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.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.reference.MethodReference @Suppress("unused") -object AmbientModeSwitchPatch : BaseBytecodePatch( - name = "Ambient mode control", - description = "Adds options to disable Ambient mode and to bypass Ambient mode restrictions.", - dependencies = setOf(SettingsPatch::class), - compatiblePackages = COMPATIBLE_PACKAGE, - fingerprints = setOf( - AmbientModeInFullscreenFingerprint, - PowerSaveModeBroadcastReceiverFingerprint, - PowerSaveModeSyntheticFingerprint - ) +val ambientModeSwitchPatch = bytecodePatch( + AMBIENT_MODE_CONTROL.title, + AMBIENT_MODE_CONTROL.summary, ) { - private var syntheticClassList = emptyArray() + compatibleWith(COMPATIBLE_PACKAGE) - override fun execute(context: BytecodeContext) { + dependsOn(settingsPatch) + execute { // region patch for bypass ambient mode restrictions + var syntheticClassList = emptyArray() + mapOf( - PowerSaveModeBroadcastReceiverFingerprint to false, - PowerSaveModeSyntheticFingerprint to true + powerSaveModeBroadcastReceiverFingerprint to false, + powerSaveModeSyntheticFingerprint to true ).forEach { (fingerprint, reversed) -> - fingerprint.resultOrThrow().mutableMethod.apply { + fingerprint.methodOrThrow().apply { val stringIndex = indexOfFirstStringInstructionOrThrow("android.os.action.POWER_SAVE_MODE_CHANGED") val targetIndex = @@ -59,7 +53,7 @@ object AmbientModeSwitchPatch : BaseBytecodePatch( } syntheticClassList.distinct().forEach { className -> - context.findMethodOrThrow(className) { + findMethodOrThrow(className) { name == "accept" }.apply { implementation!!.instructions @@ -89,23 +83,24 @@ object AmbientModeSwitchPatch : BaseBytecodePatch( // region patch for disable ambient mode in fullscreen - AmbientModeInFullscreenFingerprint.injectLiteralInstructionBooleanCall( - 45389368, + ambientModeInFullscreenFingerprint.injectLiteralInstructionBooleanCall( + 45389368L, "$PLAYER_CLASS_DESCRIPTOR->disableAmbientModeInFullscreen()Z" ) // endregion - /** - * Add settings - */ - SettingsPatch.addPreference( + // region add settings + + addPreference( arrayOf( "PREFERENCE_SCREEN: PLAYER", "SETTINGS: AMBIENT_MODE_CONTROLS" - ) + ), + AMBIENT_MODE_CONTROL ) - SettingsPatch.updatePatchStatus(this) + // endregion + } -} \ No newline at end of file +} 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..901532aee --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/ambientmode/Fingerprints.kt @@ -0,0 +1,33 @@ +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 val ambientModeInFullscreenFingerprint = legacyFingerprint( + name = "ambientModeInFullscreenFingerprint", + returnType = "V", + literals = listOf(45389368L), +) + +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") +) \ 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..34b42a899 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/buttons/PlayerButtonsPatch.kt @@ -0,0 +1,214 @@ +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.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.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.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.RegisterRangeInstruction +import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction + +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 + + 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..a90668b88 --- /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( + inclusiveSpanPatch, + lithoFilterPatch, + sharedResourceIdPatch, + settingsPatch, + ) + + 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..d0a493b91 --- /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.notice +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 noticeOnClickListenerFingerprint = legacyFingerprint( + name = "noticeOnClickListenerFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + literals = listOf(notice), +) + +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..5b379ed4e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/components/PlayerComponentsPatch.kt @@ -0,0 +1,660 @@ +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.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( + controlsOverlayConfigPatch, + inclusiveSpanPatch, + lithoFilterPatch, + playerTypeHookPatch, + sharedResourceIdPatch, + settingsPatch, + 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, + noticeOnClickListenerFingerprint, + offlineActionsOnClickListenerFingerprint, + startVideoInformerFingerprint, + ).forEach { fingerprint -> + fingerprint.methodOrThrow().apply { + if (fingerprint == startVideoInformerFingerprint) { + hookInitVideoPanel(1) + } else { + val syntheticIndex = + indexOfFirstInstruction(Opcode.NEW_INSTANCE) + if (syntheticIndex >= 0) { + val syntheticReference = + getInstruction(syntheticIndex).reference.toString() + + findMethodOrThrow(syntheticReference) { + name == "onClick" + }.hookInitVideoPanel(0) + } + } + } + } + + 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..060a88a85 --- /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.bottomSheetRecyclerViewHook +import app.revanced.patches.youtube.utils.recyclerview.bottomSheetRecyclerViewPatch +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( + bottomSheetRecyclerViewPatch, + lithoFilterPatch, + playerTypeHookPatch, + sharedResourceIdPatch, + settingsPatch, + 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" + ) + } + + bottomSheetRecyclerViewHook("$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..254670acd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/Fingerprints.kt @@ -0,0 +1,115 @@ +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 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..03c96914c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/hide/PlayerFlyoutMenuPatch.kt @@ -0,0 +1,137 @@ +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.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( + lithoFilterPatch, + playerTypeHookPatch, + sharedResourceIdPatch, + settingsPatch, + 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 + + addLithoFilter(PANELS_FILTER_CLASS_DESCRIPTOR) + + // region add settings + + addPreference(settingArray, HIDE_PLAYER_FLYOUT_MENU) + + // endregion + + } +} diff --git a/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 similarity index 60% rename from src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/toggle/ChangeTogglePatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/player/flyoutmenu/toggle/ChangeTogglePatch.kt index 3a2d60ae2..3c94fcd09 100644 --- a/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 @@ -1,28 +1,24 @@ package app.revanced.patches.youtube.player.flyoutmenu.toggle -import app.revanced.patcher.data.BytecodeContext +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.fingerprint.MethodFingerprint import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.util.smali.ExternalLabel -import app.revanced.patches.youtube.player.flyoutmenu.toggle.fingerprints.AdditionalSettingsConfigFingerprint -import app.revanced.patches.youtube.player.flyoutmenu.toggle.fingerprints.CinematicLightingFingerprint -import app.revanced.patches.youtube.player.flyoutmenu.toggle.fingerprints.PiPFingerprint -import app.revanced.patches.youtube.player.flyoutmenu.toggle.fingerprints.PlaybackLoopInitFingerprint -import app.revanced.patches.youtube.player.flyoutmenu.toggle.fingerprints.PlaybackLoopOnClickListenerFingerprint -import app.revanced.patches.youtube.player.flyoutmenu.toggle.fingerprints.StableVolumeFingerprint import app.revanced.patches.youtube.utils.compatibility.Constants.COMPATIBLE_PACKAGE -import app.revanced.patches.youtube.utils.integrations.Constants.PLAYER_CLASS_DESCRIPTOR -import app.revanced.patches.youtube.utils.settings.SettingsPatch +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 app.revanced.util.patch.BaseBytecodePatch -import app.revanced.util.resultOrThrow 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 @@ -31,110 +27,33 @@ import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference @Suppress("unused") -object ChangeTogglePatch : BaseBytecodePatch( - name = "Change player flyout menu toggles", - description = "Adds an option to use text toggles instead of switch toggles within the additional settings menu.", - dependencies = setOf(SettingsPatch::class), - compatiblePackages = COMPATIBLE_PACKAGE, - fingerprints = setOf( - AdditionalSettingsConfigFingerprint, - CinematicLightingFingerprint, - PiPFingerprint, - PlaybackLoopOnClickListenerFingerprint, - StableVolumeFingerprint - ) +val changeTogglePatch = bytecodePatch( + CHANGE_PLAYER_FLYOUT_MENU_TOGGLES.title, + CHANGE_PLAYER_FLYOUT_MENU_TOGGLES.summary, ) { - override fun execute(context: BytecodeContext) { + compatibleWith(COMPATIBLE_PACKAGE) - val additionalSettingsConfigMethod = - AdditionalSettingsConfigFingerprint.resultOrThrow().mutableMethod - val methodToCall = - additionalSettingsConfigMethod.definingClass + "->" + additionalSettingsConfigMethod.name + "()Z" + dependsOn(settingsPatch) - // Resolves fingerprints - val playbackLoopOnClickListenerResult = - PlaybackLoopOnClickListenerFingerprint.resultOrThrow() - PlaybackLoopInitFingerprint.resolve(context, playbackLoopOnClickListenerResult.classDef) + execute { + fun changeToggleCinematicLightingHook() { + val stableVolumeMethod = stableVolumeFingerprint.methodOrThrow() - var fingerprintArray = arrayOf( - CinematicLightingFingerprint, - PlaybackLoopInitFingerprint, - PlaybackLoopOnClickListenerFingerprint, - StableVolumeFingerprint - ) - - PiPFingerprint.result?.let { - fingerprintArray += PiPFingerprint - } - - fingerprintArray.forEach { fingerprint -> - injectCall(fingerprint, methodToCall) - } - - /** - * Add settings - */ - SettingsPatch.addPreference( - arrayOf( - "PREFERENCE_SCREEN: PLAYER", - "PREFERENCE_SCREENS: FLYOUT_MENU", - "SETTINGS: CHANGE_PLAYER_FLYOUT_MENU_TOGGLE" - ) - ) - - SettingsPatch.updatePatchStatus(this) - } - - private fun injectCall( - fingerprint: MethodFingerprint, - methodToCall: String - ) { - fingerprint.resultOrThrow().let { - it.mutableMethod.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) - injectCinematicLightingMethod() - else - throw PatchException("Target reference was not found in ${fingerprint.javaClass.simpleName}.") - } + 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.") - private fun injectCinematicLightingMethod() { - val stableVolumeMethod = StableVolumeFingerprint.resultOrThrow().mutableMethod + val stringReference = + stableVolumeMethod.getInstruction(stringReferenceIndex).reference - 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.javaClass.simpleName}.") - - val stringReference = - stableVolumeMethod.getInstruction(stringReferenceIndex).reference - - CinematicLightingFingerprint.resultOrThrow().let { - it.mutableMethod.apply { + cinematicLightingFingerprint.methodOrThrow().apply { val iGetIndex = indexOfFirstInstructionOrThrow { - opcode == Opcode.IGET - && getReference()?.definingClass == definingClass + opcode == Opcode.IGET && + getReference()?.definingClass == definingClass } val classRegister = getInstruction(iGetIndex).registerB @@ -191,5 +110,74 @@ object ChangeTogglePatch : BaseBytecodePatch( ) } } + + 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..f87d20ea4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/fullscreen/FullscreenComponentsPatch.kt @@ -0,0 +1,335 @@ +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.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.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( + lithoFilterPatch, + mainActivityResolvePatch, + sharedResourceIdPatch, + settingsPatch, + 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) { + 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" + } + + // 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/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt similarity index 68% rename from src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt index b2a31d9c5..00b239fed 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/overlaybuttons/OverlayButtonsPatch.kt @@ -1,50 +1,84 @@ package app.revanced.patches.youtube.player.overlaybuttons -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.booleanPatchOption -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption +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.fix.bottomui.CfBottomUIPatch -import app.revanced.patches.youtube.utils.integrations.Constants.OVERLAY_BUTTONS_PATH -import app.revanced.patches.youtube.utils.pip.PiPStateHookPatch -import app.revanced.patches.youtube.utils.playercontrols.PlayerControlsPatch -import app.revanced.patches.youtube.utils.settings.SettingsPatch +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 app.revanced.util.patch.BaseResourcePatch import org.w3c.dom.Element -/** - * Patch to add overlay buttons in the YouTube video player. - * - * This patch integrates various buttons such as copy URL, speed, repeat, etc., into the video player's - * control overlay, providing enhanced functionality directly in the player interface. - */ -@Suppress("DEPRECATION", "unused") -object OverlayButtonsPatch : BaseResourcePatch( - name = "Overlay buttons", - description = "Adds options to display overlay buttons in the video player.", - dependencies = setOf( - CfBottomUIPatch::class, - PiPStateHookPatch::class, - PlayerControlsPatch::class, - SettingsPatch::class, - OverlayButtonsBytecodePatch::class - ), - compatiblePackages = COMPATIBLE_PACKAGE +private const val EXTENSION_ALWAYS_REPEAT_CLASS_DESCRIPTOR = + "$UTILS_PATH/AlwaysRepeatPatch;" + +private val overlayButtonsBytecodePatch = bytecodePatch( + description = "overlayButtonsBytecodePatch" ) { - private const val MARGIN_NONE = "0.0dip" - private const val MARGIN_DEFAULT = "2.5dip" - private const val MARGIN_WIDER = "5.0dip" + dependsOn(videoInformationPatch) - private const val DEFAULT_ICON = "bold" + execute { - // Option to select icon type - private val IconType = stringPatchOption( - key = "IconType", + // 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 = "bold" + +@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 DEFAULT_ICON, @@ -56,9 +90,8 @@ object OverlayButtonsPatch : BaseResourcePatch( required = true ) - // Option to set bottom margin - private val BottomMargin = stringPatchOption( - key = "BottomMargin", + val bottomMarginOption = stringOption( + key = "bottomMargin", default = MARGIN_DEFAULT, values = mapOf( "Default" to MARGIN_DEFAULT, @@ -70,54 +103,47 @@ object OverlayButtonsPatch : BaseResourcePatch( required = true ) - // Option to choose wider between-buttons space - private val WiderButtonsSpace by booleanPatchOption( - key = "WiderButtonsSpace", + 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 ) - // Option to change top buttons - private val ChangeTopButtons by booleanPatchOption( - key = "ChangeTopButtons", + val changeTopButtons by booleanOption( + key = "changeTopButtons", default = false, title = "Change top buttons", description = "Change the icons at the top of the player.", required = true ) - /** - * Main execution method for applying the patch. - * - * @param context The resource context for patching. - */ - override fun execute(context: ResourceContext) { + execute { // Check patch options first. - val iconType = IconType + val iconType = iconTypeOption .lowerCaseOrThrow() - val marginBottom = BottomMargin + val marginBottom = bottomMarginOption .lowerCaseOrThrow() // Inject hooks for overlay buttons. - arrayOf( + setOf( "AlwaysRepeat;", "CopyVideoUrl;", "CopyVideoUrlTimestamp;", "MuteVolume;", "ExternalDownload;", + "PlayAll;", "SpeedDialog;", - "TimeOrderedPlaylist;", "Whitelists;" ).forEach { className -> - PlayerControlsPatch.hookBottomControlButton("$OVERLAY_BUTTONS_PATH/$className") + hookBottomControlButton("$OVERLAY_BUTTONS_PATH/$className") } // Copy necessary resources for the overlay buttons. - context.copyResources( + copyResources( "youtube/overlaybuttons/shared", ResourceGroup( "drawable", @@ -136,7 +162,7 @@ object OverlayButtonsPatch : BaseResourcePatch( "hdpi", "mdpi" ).forEach { dpi -> - context.copyResources( + copyResources( "youtube/overlaybuttons/$iconType", ResourceGroup( "drawable-$dpi", @@ -145,13 +171,13 @@ object OverlayButtonsPatch : BaseResourcePatch( "quantum_ic_fullscreen_exit_white_24.png", "quantum_ic_fullscreen_grey600_24.png", "quantum_ic_fullscreen_white_24.png", - "revanced_time_ordered_playlist_button.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_speed_button.png", "revanced_whitelist_button.png", "yt_fill_arrow_repeat_white_24.png", "yt_outline_arrow_repeat_1_white_24.png", @@ -169,7 +195,7 @@ object OverlayButtonsPatch : BaseResourcePatch( } // Merge XML nodes from the host to their respective XML files. - context.copyXmlNode( + copyXmlNode( "youtube/overlaybuttons/shared/host", "layout/youtube_controls_bottom_ui_container.xml", "android.support.constraint.ConstraintLayout" @@ -181,10 +207,10 @@ object OverlayButtonsPatch : BaseResourcePatch( "youtube_controls_fullscreen_button.xml", "youtube_controls_cf_fullscreen_button.xml", ).forEach { xmlFile -> - val targetXml = context["res"].resolve("layout").resolve(xmlFile) + val targetXml = get("res").resolve("layout").resolve(xmlFile) if (targetXml.exists()) { - context.xmlEditor["res/layout/$xmlFile"].use { editor -> - editor.file.doRecursively loop@{ node -> + document("res/layout/$xmlFile").use { document -> + document.doRecursively loop@{ node -> if (node !is Element) return@loop // Change the relationship between buttons @@ -212,8 +238,7 @@ object OverlayButtonsPatch : BaseResourcePatch( "@id/timestamps_container" to "14.0dip" ) - val widerButtonsSpace = WiderButtonsSpace == true - val layoutHeightWidth = if (widerButtonsSpace) + val layoutHeightWidth = if (widerButtonsSpace == true) "56.0dip" else "48.0dip" @@ -229,7 +254,7 @@ object OverlayButtonsPatch : BaseResourcePatch( } } else if (timBarItem.containsKey(id)) { node.setAttribute("android:layout_marginBottom", marginBottom) - if (!widerButtonsSpace) { + if (widerButtonsSpace != true) { node.setAttribute("android:paddingBottom", timBarItem.getValue(id)) } } @@ -238,7 +263,7 @@ object OverlayButtonsPatch : BaseResourcePatch( } } - if (ChangeTopButtons == true) { + if (changeTopButtons == true) { // Apply the selected icon type to the top buttons. arrayOf( "xxxhdpi", @@ -247,7 +272,7 @@ object OverlayButtonsPatch : BaseResourcePatch( "hdpi", "mdpi" ).forEach { dpi -> - context.copyResources( + copyResources( "youtube/overlaybuttons/$iconType", ResourceGroup( "drawable-$dpi", @@ -261,18 +286,18 @@ object OverlayButtonsPatch : BaseResourcePatch( } } - /** - * Add settings for the overlay buttons. - */ - SettingsPatch.addPreference( + // region add settings + + addPreference( arrayOf( "PREFERENCE_SCREEN: PLAYER", "PREFERENCE_SCREENS: PLAYER_BUTTONS", "SETTINGS: OVERLAY_BUTTONS" - ) + ), + OVERLAY_BUTTONS ) - // Update the patch status in settings to reflect the applied changes - SettingsPatch.updatePatchStatus(this) + // 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..089e7a15b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/Fingerprints.kt @@ -0,0 +1,87 @@ +package app.revanced.patches.youtube.player.seekbar + +import app.revanced.patches.youtube.utils.resourceid.reelTimeBarPlayedColor +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 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..3465db479 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/player/seekbar/SeekbarComponentsPatch.kt @@ -0,0 +1,325 @@ +package app.revanced.patches.youtube.player.seekbar + +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.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.PATCH_STATUS_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.extension.Constants.PLAYER_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.flyoutmenu.flyoutMenuHookPatch +import app.revanced.patches.youtube.utils.patch.PatchList.SEEKBAR_COMPONENTS +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.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.findMethodsOrThrow +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 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.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 + +@Suppress("unused") +val seekbarComponentsPatch = bytecodePatch( + SEEKBAR_COMPONENTS.title, + SEEKBAR_COMPONENTS.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + drawableColorHookPatch, + flyoutMenuHookPatch, + sharedResourceIdPatch, + settingsPatch, + videoInformationPatch, + versionCheckPatch, + ) + + 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.hookSeekbarColor(literal: Long) { + val insertIndex = indexOfFirstLiteralInstructionOrThrow(literal) + 2 + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex + 1, """ + invoke-static {v$insertRegister}, $PLAYER_CLASS_DESCRIPTOR->overrideSeekbarColor(I)I + move-result v$insertRegister + """ + ) + } + + + playerSeekbarColorFingerprint.methodOrThrow().apply { + hookSeekbarColor(inlineTimeBarColorizedBarPlayedColorDark) + hookSeekbarColor(inlineTimeBarPlayedNotHighlightedColor) + } + + shortsSeekbarColorFingerprint.methodOrThrow().apply { + hookSeekbarColor(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}, $PLAYER_CLASS_DESCRIPTOR->getSeekbarClickedColorValue(I)I + move-result v$colorRegister + """ + ) + } + } + + addDrawableColorHook("$PLAYER_CLASS_DESCRIPTOR->getColor(I)I") + + getContext().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) + } + + // 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(playerButtonsVisibilityFingerprint).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 { + println("WARNING: Restore old seekbar thumbnails setting 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..fae0d2e8d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/Fingerprints.kt @@ -0,0 +1,155 @@ +package app.revanced.patches.youtube.shorts.components + +import app.revanced.patches.youtube.utils.resourceid.badgeLabel +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.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.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +// multiFingerprint +internal val bottomBarContainerHeightFingerprint = legacyFingerprint( + name = "bottomBarContainerHeightFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("Landroid/view/View;", "Landroid/os/Bundle;"), + strings = listOf("r_pfvc"), + literals = listOf(bottomBarContainer), +) + +internal val reelEnumConstructorFingerprint = legacyFingerprint( + name = "reelEnumConstructorFingerprint", + returnType = "V", + strings = listOf( + "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" + } +) 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..8f60b0687 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shorts/components/ShortsComponentPatch.kt @@ -0,0 +1,645 @@ +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.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.textcomponent.hookSpannableString +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.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.lottie.LOTTIE_ANIMATION_VIEW_CLASS_DESCRIPTOR +import app.revanced.patches.youtube.utils.lottie.lottieAnimationViewHookPatch +import app.revanced.patches.youtube.utils.navigation.addBottomBarContainerHook +import app.revanced.patches.youtube.utils.navigation.navigationBarHookPatch +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_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.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.video.information.hookShortsVideoInformation +import app.revanced.patches.youtube.video.information.videoInformationPatch +import app.revanced.util.REGISTER_TEMPLATE_REPLACEMENT +import app.revanced.util.ResourceGroup +import app.revanced.util.copyResources +import app.revanced.util.findMutableMethodOf +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.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.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 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 val shortsRepeatPatch = bytecodePatch( + description = "shortsRepeatPatch" +) { + execute { + reelEnumConstructorFingerprint.methodOrThrow().apply { + arrayOf( + "REEL_LOOP_BEHAVIOR_END_SCREEN" to "endScreen", + "REEL_LOOP_BEHAVIOR_REPEAT" to "repeat", + "REEL_LOOP_BEHAVIOR_SINGLE_PLAY" to "singlePlay" + ).map { (enumName, fieldName) -> + val stringIndex = indexOfFirstStringInstructionOrThrow(enumName) + val insertIndex = indexOfFirstInstructionOrThrow(stringIndex, Opcode.SPUT_OBJECT) + val insertRegister = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex + 1, + "sput-object v$insertRegister, $SHORTS_CLASS_DESCRIPTOR->$fieldName:Ljava/lang/Enum;" + ) + } + + 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}, $SHORTS_CLASS_DESCRIPTOR->changeShortsRepeatState(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( + shortsAnimationPatch, + shortsNavigationBarPatch, + shortsRepeatPatch, + shortsTimeStampPatch, + shortsToolBarPatch, + + lithoFilterPatch, + playerTypeHookPatch, + sharedResourceIdPatch, + settingsPatch, + 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" + } + + // 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 + + 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..4c5555a51 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/Fingerprints.kt @@ -0,0 +1,46 @@ +package app.revanced.patches.youtube.swipe.controls + +import app.revanced.patches.youtube.utils.extension.Constants.EXTENSION_PATH +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;" + } +) + +/** + * This fingerprint is compatible with YouTube v19.19.39+ + */ +internal val swipeToSwitchVideoFingerprint = legacyFingerprint( + name = "swipeToSwitchVideoFingerprint", + returnType = "V", + literals = listOf(45631116L), +) + +/** + * This fingerprint is compatible with YouTube v18.29.38+ + */ +internal val watchPanelGesturesFingerprint = legacyFingerprint( + name = "watchPanelGesturesFingerprint", + returnType = "V", + literals = listOf(45372793L), +) \ 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..ed84af9ec --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/swipe/controls/SwipeControlsPatch.kt @@ -0,0 +1,170 @@ +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.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.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.fingerprint.resolvable +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.iface.instruction.OneRegisterInstruction +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, + ) + + 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" + ) + + // region patch for disable HDR auto brightness + + // Since it does not support all versions, + // add settings only if the patch is successful. + if (hdrBrightnessFingerprint.resolvable()) { + 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 enable swipe to switch video + + // Since it does not support all versions, + // add settings only if the patch is successful. + + if (swipeToSwitchVideoFingerprint.resolvable()) { + swipeToSwitchVideoFingerprint.injectLiteralInstructionBooleanCall( + 45631116L, + "$EXTENSION_SWIPE_CONTROLS_PATCH_CLASS_DESCRIPTOR->enableSwipeToSwitchVideo()Z" + ) + + settingArray += "SETTINGS: ENABLE_SWIPE_TO_SWITCH_VIDEO" + } + + // endregion + + // region patch for enable watch panel gestures + + // Since it does not support all versions, + // add settings only if the patch is successful. + if (watchPanelGesturesFingerprint.resolvable()) { + watchPanelGesturesFingerprint.injectLiteralInstructionBooleanCall( + 45372793L, + "$EXTENSION_SWIPE_CONTROLS_PATCH_CLASS_DESCRIPTOR->enableWatchPanelGestures()Z" + ) + + settingArray += "SETTINGS: ENABLE_WATCH_PANEL_GESTURES" + } + + // 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..fba851b85 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/Fingerprints.kt @@ -0,0 +1,202 @@ +package app.revanced.patches.youtube.utils + +import app.revanced.patches.youtube.player.components.playerComponentsPatch +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.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.or +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +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") +) + +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 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..39b1a7259 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/compatibility/Constants.kt @@ -0,0 +1,20 @@ +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 latest version supported by the RVX patch. + ) + ) +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/controlsoverlay/ControlsOverlayConfigPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/controlsoverlay/ControlsOverlayConfigPatch.kt similarity index 65% rename from src/main/kotlin/app/revanced/patches/youtube/utils/controlsoverlay/ControlsOverlayConfigPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/utils/controlsoverlay/ControlsOverlayConfigPatch.kt index fdc4281a3..2e585519c 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/controlsoverlay/ControlsOverlayConfigPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/controlsoverlay/ControlsOverlayConfigPatch.kt @@ -1,25 +1,25 @@ package app.revanced.patches.youtube.utils.controlsoverlay -import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.InstructionExtensions.addInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patches.youtube.utils.controlsoverlay.fingerprints.ControlsOverlayConfigFingerprint +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 -object ControlsOverlayConfigPatch : BytecodePatch( - setOf(ControlsOverlayConfigFingerprint) +val controlsOverlayConfigPatch = bytecodePatch( + description = "controlsOverlayConfigPatch" ) { - override fun execute(context: BytecodeContext) { + 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. */ - ControlsOverlayConfigFingerprint.result?.let { - it.mutableMethod.apply { + if (controlsOverlayConfigFingerprint.resolvable()) { + controlsOverlayConfigFingerprint.methodOrThrow().apply { val targetIndex = implementation!!.instructions.size - 1 val targetRegister = getInstruction(targetIndex).registerA @@ -29,6 +29,5 @@ object ControlsOverlayConfigPatch : BytecodePatch( ) } } - } } 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/src/main/kotlin/app/revanced/patches/youtube/utils/integrations/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/Constants.kt similarity index 82% rename from src/main/kotlin/app/revanced/patches/youtube/utils/integrations/Constants.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/Constants.kt index bd6be2c49..b0c20cdfe 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/integrations/Constants.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/extension/Constants.kt @@ -1,10 +1,10 @@ -package app.revanced.patches.youtube.utils.integrations +package app.revanced.patches.youtube.utils.extension @Suppress("MemberVisibilityCanBePrivate") -object Constants { - const val INTEGRATIONS_PATH = "Lapp/revanced/integrations/youtube" - const val SHARED_PATH = "$INTEGRATIONS_PATH/shared" - const val PATCHES_PATH = "$INTEGRATIONS_PATH/patches" +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" 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..9729757cd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/bottomui/CfBottomUIPatch.kt @@ -0,0 +1,28 @@ +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( + exploderControlsFingerprint to 45643739L, + fullscreenButtonViewStubFingerprint to 45617294L, + fullscreenButtonPositionFingerprint to 45627640L + ).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..9f42e20ca --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/bottomui/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.youtube.utils.fix.bottomui + +import app.revanced.util.fingerprint.legacyFingerprint + +internal val exploderControlsFingerprint = legacyFingerprint( + name = "exploderControlsFingerprint", + returnType = "Z", + literals = listOf(45643739L), +) + +internal val fullscreenButtonPositionFingerprint = legacyFingerprint( + name = "fullscreenButtonPositionFingerprint", + returnType = "Z", + literals = listOf(45627640L), +) + +internal val fullscreenButtonViewStubFingerprint = legacyFingerprint( + name = "fullscreenButtonViewStubFingerprint", + returnType = "Z", + literals = listOf(45617294L), +) 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..751f987c8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/cairo/CairoSettingsPatch.kt @@ -0,0 +1,28 @@ +package app.revanced.patches.youtube.utils.fix.cairo + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.resolvable + +val cairoSettingsPatch = bytecodePatch( + description = "cairoSettingsPatch" +) { + 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. + */ + if (carioFragmentConfigFingerprint.resolvable()) { + carioFragmentConfigFingerprint.injectLiteralInstructionBooleanCall( + 45532100L, + "0x0" + ) + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/fix/cairo/fingerprints/CarioFragmentConfigFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/cairo/Fingerprints.kt similarity index 51% rename from src/main/kotlin/app/revanced/patches/youtube/utils/fix/cairo/fingerprints/CarioFragmentConfigFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/cairo/Fingerprints.kt index d9ac778ee..73f157247 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/fix/cairo/fingerprints/CarioFragmentConfigFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/cairo/Fingerprints.kt @@ -1,7 +1,7 @@ -package app.revanced.patches.youtube.utils.fix.cairo.fingerprints +package app.revanced.patches.youtube.utils.fix.cairo -import app.revanced.patcher.extensions.or -import app.revanced.util.fingerprint.LiteralValueFingerprint +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or import com.android.tools.smali.dexlib2.AccessFlags /** @@ -10,8 +10,9 @@ import com.android.tools.smali.dexlib2.AccessFlags * 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 object CarioFragmentConfigFingerprint : LiteralValueFingerprint( +internal val carioFragmentConfigFingerprint = legacyFingerprint( + name = "carioFragmentConfigFingerprint", returnType = "Z", accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, - literalSupplier = { 45532100 }, -) \ No newline at end of file + literals = listOf(45532100L), +) 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/shortsplayback/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/shortsplayback/Fingerprints.kt new file mode 100644 index 000000000..0abb3acf2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/shortsplayback/Fingerprints.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.youtube.utils.fix.shortsplayback + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val shortsPlaybackFingerprint = legacyFingerprint( + name = "shortsPlaybackFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + literals = listOf(45387052L), +) 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..12255cd7d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/shortsplayback/ShortsPlaybackPatch.kt @@ -0,0 +1,25 @@ +package app.revanced.patches.youtube.utils.fix.shortsplayback + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.resolvable + +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. + */ + if (shortsPlaybackFingerprint.resolvable()) { + shortsPlaybackFingerprint.injectLiteralInstructionBooleanCall( + 45387052L, + "0x0" + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt new file mode 100644 index 000000000..79758b285 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/Fingerprints.kt @@ -0,0 +1,134 @@ +package app.revanced.patches.youtube.utils.fix.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.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val buildBrowseRequestFingerprint = legacyFingerprint( + name = "buildBrowseRequestFingerprint", + customFingerprint = { method, _ -> + method.implementation != null && + indexOfRequestFinishedListenerInstruction(method) >= 0 && + !method.definingClass.startsWith("Lorg/") && + indexOfNewUrlRequestBuilderInstruction(method) >= 0 && + // YouTube 17.34.36 ~ YouTube 18.35.36 + (indexOfEntrySetInstruction(method) >= 0 || + // YouTube 18.36.39 ~ + 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 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 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 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 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 + ), + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { + opcode == Opcode.SGET_OBJECT && + getReference()?.name == "playerThreedRenderer" + } >= 0 + }, +) + +internal val nerdsStatsVideoFormatBuilderFingerprint = legacyFingerprint( + name = "nerdsStatsVideoFormatBuilderFingerprint", + returnType = "Ljava/lang/String;", + accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, + parameters = listOf("Lcom/google/android/libraries/youtube/innertube/model/media/FormatStreamModel;"), + 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" }, +) 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..0a7ddf7a6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/streamingdata/SpoofStreamingDataPatch.kt @@ -0,0 +1,230 @@ +package app.revanced.patches.youtube.utils.fix.streamingdata + +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.shared.spoof.useragent.baseSpoofUserAgentPatch +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.SPOOF_STREAMING_DATA +import app.revanced.patches.youtube.utils.settings.ResourceUtils.addPreference +import app.revanced.patches.youtube.utils.settings.settingsPatch +import app.revanced.util.findInstructionIndicesReversedOrThrow +import app.revanced.util.fingerprint.definingClassOrThrow +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow +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.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 = + "$MISC_PATH/SpoofStreamingDataPatch;" + +val spoofStreamingDataPatch = bytecodePatch( + SPOOF_STREAMING_DATA.title, + SPOOF_STREAMING_DATA.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + baseSpoofUserAgentPatch("com.google.android.youtube"), + settingsPatch + ) + + execute { + // 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 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 Fetch replacement streams. + + buildBrowseRequestFingerprint.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. + + createStreamingDataFingerprint.matchOrThrow().let { result -> + result.method.apply { + 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 videoDetailsClass = + getInstruction(videoDetailsIndex).getReference()!!.type + + val insertIndex = videoDetailsIndex + 1 + val videoDetailsRegister = + getInstruction(videoDetailsIndex).registerA + + val overrideRegister = getInstruction(insertIndex).registerA + val freeRegister = implementation!!.registerCount - parameters.size - 2 + + addInstructionsWithLabels( + insertIndex, + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z + move-result v$freeRegister + if-eqz v$freeRegister, :disabled + + # Get video id. + # From YouTube 17.34.36 to YouTube 19.16.39, the field names and field types are the same. + iget-object v$freeRegister, v$videoDetailsRegister, $videoDetailsClass->c:Ljava/lang/String; + if-eqz v$freeRegister, :disabled + + # Get streaming data. + invoke-static { v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer; + move-result-object v$freeRegister + if-eqz v$freeRegister, :disabled + + # Parse streaming data. + sget-object v$overrideRegister, $playerProtoClass->a:$playerProtoClass + invoke-static { v$overrideRegister, v$freeRegister }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass + move-result-object v$freeRegister + check-cast v$freeRegister, $playerProtoClass + + # Set streaming data. + iget-object v$freeRegister, v$freeRegister, $getStreamingDataField + if-eqz v$freeRegister, :disabled + iput-object v$freeRegister, p0, $setStreamingDataField + + """, + ExternalLabel("disabled", getInstruction(insertIndex)) + ) + } + } + + // 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. + # From YouTube 17.34.36 to YouTube 19.16.39, the field names and field types are the same. + 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 add settings + + addPreference( + arrayOf( + "SETTINGS: SPOOF_STREAMING_DATA" + ), + SPOOF_STREAMING_DATA + ) + + // endregion + + } +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/fingerprints/RemoveOnLayoutChangeListenerFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/Fingerprints.kt similarity index 55% rename from src/main/kotlin/app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/fingerprints/RemoveOnLayoutChangeListenerFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/Fingerprints.kt index 2c923219b..210b9d45f 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/fingerprints/RemoveOnLayoutChangeListenerFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/Fingerprints.kt @@ -1,18 +1,33 @@ -package app.revanced.patches.youtube.utils.fix.suggestedvideoendscreen.fingerprints +package app.revanced.patches.youtube.utils.fix.suggestedvideoendscreen -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint +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 object RemoveOnLayoutChangeListenerFingerprint : MethodFingerprint( +internal val removeOnLayoutChangeListenerFingerprint = legacyFingerprint( + name = "removeOnLayoutChangeListenerFingerprint", returnType = "V", accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, parameters = emptyList(), @@ -21,8 +36,8 @@ internal object RemoveOnLayoutChangeListenerFingerprint : MethodFingerprint( Opcode.INVOKE_VIRTUAL ), // This is the only reference present in the entire smali. - customFingerprint = { methodDef, _ -> - methodDef.indexOfFirstInstruction { + customFingerprint = { method, _ -> + method.indexOfFirstInstruction { getReference()?.toString() ?.endsWith("YouTubePlayerOverlaysLayout;->removeOnLayoutChangeListener(Landroid/view/View${'$'}OnLayoutChangeListener;)V") == true } >= 0 diff --git a/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 similarity index 68% rename from src/main/kotlin/app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/SuggestedVideoEndScreenPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/suggestedvideoendscreen/SuggestedVideoEndScreenPatch.kt index 5b46d286f..422b64f36 100644 --- a/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 @@ -1,35 +1,24 @@ package app.revanced.patches.youtube.utils.fix.suggestedvideoendscreen -import app.revanced.patcher.data.BytecodeContext 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.annotation.Patch +import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.util.smali.ExternalLabel -import app.revanced.patches.youtube.utils.fix.suggestedvideoendscreen.fingerprints.AutoNavConstructorFingerprint -import app.revanced.patches.youtube.utils.fix.suggestedvideoendscreen.fingerprints.AutoNavStatusFingerprint -import app.revanced.patches.youtube.utils.fix.suggestedvideoendscreen.fingerprints.RemoveOnLayoutChangeListenerFingerprint -import app.revanced.patches.youtube.utils.integrations.Constants.PLAYER_CLASS_DESCRIPTOR -import app.revanced.util.alsoResolve +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 app.revanced.util.resultOrThrow 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 -@Patch( - description = "Fixes an issue where the suggested video end screen is always visible regardless of whether autoplay is set or not." -) -object SuggestedVideoEndScreenPatch : BytecodePatch( - setOf( - AutoNavConstructorFingerprint, - RemoveOnLayoutChangeListenerFingerprint - ) +val suggestedVideoEndScreenPatch = bytecodePatch( + description = "suggestedVideoEndScreenPatch" ) { - override fun execute(context: BytecodeContext) { + execute { /** * The reasons why this patch is classified as a patch that fixes a 'bug' are as follows: @@ -39,19 +28,18 @@ object SuggestedVideoEndScreenPatch : BytecodePatch( * 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.resultOrThrow().let { + removeOnLayoutChangeListenerFingerprint.matchOrThrow().let { val walkerIndex = - it.getWalkerMethod(context, it.scanResult.patternScanResult!!.endIndex) + it.getWalkerMethod(it.patternMatch!!.endIndex) walkerIndex.apply { - val autoNavStatusMethodName = AutoNavStatusFingerprint.alsoResolve( - context, AutoNavConstructorFingerprint - ).mutableMethod.name + val autoNavStatusMethodName = + autoNavStatusFingerprint.methodOrThrow(autoNavConstructorFingerprint).name val invokeIndex = indexOfFirstInstructionOrThrow { val reference = getReference() reference?.returnType == "Z" && - reference.parameterTypes.size == 0 && + reference.parameterTypes.isEmpty() && reference.name == autoNavStatusMethodName } val iGetObjectIndex = @@ -86,6 +74,5 @@ object SuggestedVideoEndScreenPatch : BytecodePatch( ) } } - } } 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/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 similarity index 50% rename from src/main/kotlin/app/revanced/patches/youtube/utils/fix/swiperefresh/SwipeRefreshPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/utils/fix/swiperefresh/SwipeRefreshPatch.kt index 364f8ea88..7086ff040 100644 --- a/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 @@ -1,21 +1,19 @@ package app.revanced.patches.youtube.utils.fix.swiperefresh -import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.InstructionExtensions.addInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patches.youtube.utils.fix.swiperefresh.fingerprint.SwipeRefreshLayoutFingerprint -import app.revanced.util.resultOrThrow +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.fingerprint.matchOrThrow import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction -object SwipeRefreshPatch : BytecodePatch( - setOf(SwipeRefreshLayoutFingerprint) +val swipeRefreshPatch = bytecodePatch( + description = "swipeRefreshPatch" ) { - override fun execute(context: BytecodeContext) { + execute { - SwipeRefreshLayoutFingerprint.resultOrThrow().let { - it.mutableMethod.apply { - val insertIndex = it.scanResult.patternScanResult!!.endIndex + swipeRefreshLayoutFingerprint.matchOrThrow().let { + it.method.apply { + val insertIndex = it.patternMatch!!.endIndex val register = getInstruction(insertIndex).registerA addInstruction( 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..46fb90184 --- /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 = "afb0fed5eeaebdd86f56a97742f4b6b33ef59875", + 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..8201842fb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/patch/PatchList.kt @@ -0,0 +1,256 @@ +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 hide player buttons background", + "Removes, at compile time, the dark background surrounding the video player controls." + ), + 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 video 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( + "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/src/main/kotlin/app/revanced/patches/youtube/utils/pip/fingerprints/PiPPlaybackFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/pip/Fingerprints.kt similarity index 52% rename from src/main/kotlin/app/revanced/patches/youtube/utils/pip/fingerprints/PiPPlaybackFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/utils/pip/Fingerprints.kt index a34253664..6eea42b60 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/pip/fingerprints/PiPPlaybackFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/pip/Fingerprints.kt @@ -1,10 +1,11 @@ -package app.revanced.patches.youtube.utils.pip.fingerprints +package app.revanced.patches.youtube.utils.pip -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patches.youtube.utils.PlayerResponseModelUtils.PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR +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 object PiPPlaybackFingerprint : MethodFingerprint( +internal val pipPlaybackFingerprint = legacyFingerprint( + name = "pipPlaybackFingerprint", returnType = "Z", parameters = listOf(PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR), opcodes = listOf( @@ -14,4 +15,4 @@ internal object PiPPlaybackFingerprint : MethodFingerprint( Opcode.MOVE_RESULT, Opcode.IF_NEZ ) -) \ No newline at end of file +) 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..390a7d7b6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playercontrols/Fingerprints.kt @@ -0,0 +1,72 @@ +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") +) \ 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..034856435 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playercontrols/PlayerControlsPatch.kt @@ -0,0 +1,233 @@ +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.playerButtonsResourcesFingerprint +import app.revanced.patches.youtube.utils.playerButtonsVisibilityFingerprint +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( + sharedResourceIdPatch + ) + + 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 + } +} + +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..5a418d9f0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/Fingerprints.kt @@ -0,0 +1,79 @@ +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 fun indexOfLayoutDirectionInstruction(method: Method) = + method.indexOfFirstInstruction { + opcode == Opcode.INVOKE_VIRTUAL && + getReference().toString() == "Landroid/view/View;->setLayoutDirection(I)V" + } + +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 val searchQueryClassFingerprint = legacyFingerprint( + name = "searchQueryClassFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = listOf("L", "Ljava/util/Map;"), + opcodes = listOf( + Opcode.CHECK_CAST, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + ), + strings = listOf("force_enable_sticky_browsy_bars"), +) + +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..4fef422ff --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playertype/PlayerTypeHookPatch.kt @@ -0,0 +1,171 @@ +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.matchOrThrow().let { + it.method.apply { + val searchQueryIndex = it.patternMatch!!.startIndex + 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..71cecfc5c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/playservice/VersionCheckPatch.kt @@ -0,0 +1,60 @@ +@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_15_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_28_or_greater = false + private set +var is_19_32_or_greater = false + private set +var is_19_44_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_15_or_greater = 241602000 <= playStoreServicesVersion + is_19_23_or_greater = 242402000 <= playStoreServicesVersion + is_19_25_or_greater = 242599000 <= playStoreServicesVersion + is_19_28_or_greater = 242905000 <= playStoreServicesVersion + is_19_32_or_greater = 243305000 <= playStoreServicesVersion + is_19_44_or_greater = 244505000 <= playStoreServicesVersion + } +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/utils/recyclerview/BottomSheetRecyclerViewPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/recyclerview/BottomSheetRecyclerViewPatch.kt similarity index 52% rename from src/main/kotlin/app/revanced/patches/youtube/utils/recyclerview/BottomSheetRecyclerViewPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/utils/recyclerview/BottomSheetRecyclerViewPatch.kt index 560fd7437..1fa445e42 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/recyclerview/BottomSheetRecyclerViewPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/recyclerview/BottomSheetRecyclerViewPatch.kt @@ -1,45 +1,38 @@ package app.revanced.patches.youtube.utils.recyclerview -import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.InstructionExtensions.addInstruction -import app.revanced.patcher.patch.BytecodePatch +import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patches.youtube.utils.recyclerview.fingerprints.BottomSheetRecyclerViewBuilderFingerprint -import app.revanced.patches.youtube.utils.recyclerview.fingerprints.RecyclerViewTreeObserverFingerprint +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.resolvable import app.revanced.util.getReference import app.revanced.util.indexOfFirstInstructionOrThrow import app.revanced.util.indexOfFirstInstructionReversedOrThrow -import app.revanced.util.injectLiteralInstructionBooleanCall -import app.revanced.util.resultOrThrow import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.reference.FieldReference -object BottomSheetRecyclerViewPatch : BytecodePatch( - setOf( - BottomSheetRecyclerViewBuilderFingerprint, - RecyclerViewTreeObserverFingerprint - ) +private lateinit var recyclerViewTreeObserverMutableMethod: MutableMethod +private var recyclerViewTreeObserverInsertIndex = 0 + +val bottomSheetRecyclerViewPatch = bytecodePatch( + description = "bottomSheetRecyclerViewPatch" ) { - private lateinit var recyclerViewTreeObserverMutableMethod: MutableMethod - - private var recyclerViewTreeObserverInsertIndex = 0 - - override fun execute(context: BytecodeContext) { - + execute { /** * If this value is false, OldQualityLayoutPatch and OldSpeedLayoutPatch will not work. * 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. */ - BottomSheetRecyclerViewBuilderFingerprint.result?.let { - BottomSheetRecyclerViewBuilderFingerprint.injectLiteralInstructionBooleanCall( - 45382015, + if (bottomSheetRecyclerViewBuilderFingerprint.resolvable()) { + bottomSheetRecyclerViewBuilderFingerprint.injectLiteralInstructionBooleanCall( + 45382015L, "0x1" ) } - RecyclerViewTreeObserverFingerprint.resultOrThrow().mutableMethod.apply { + recyclerViewTreeObserverFingerprint.methodOrThrow().apply { recyclerViewTreeObserverMutableMethod = this val onDrawListenerIndex = indexOfFirstInstructionOrThrow { @@ -49,12 +42,11 @@ object BottomSheetRecyclerViewPatch : BytecodePatch( recyclerViewTreeObserverInsertIndex = indexOfFirstInstructionReversedOrThrow(onDrawListenerIndex, Opcode.CHECK_CAST) + 1 } - } - - internal fun injectCall(descriptor: String) = - recyclerViewTreeObserverMutableMethod.addInstruction( - recyclerViewTreeObserverInsertIndex++, - "invoke-static/range { p2 .. p2 }, $descriptor" - ) } + +fun bottomSheetRecyclerViewHook(descriptor: String) = + recyclerViewTreeObserverMutableMethod.addInstruction( + recyclerViewTreeObserverInsertIndex++, + "invoke-static/range { p2 .. p2 }, $descriptor" + ) 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..def021552 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/recyclerview/Fingerprints.kt @@ -0,0 +1,17 @@ +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 val bottomSheetRecyclerViewBuilderFingerprint = legacyFingerprint( + name = "bottomSheetRecyclerViewBuilderFingerprint", + literals = listOf(45382015L), +) + +internal val recyclerViewTreeObserverFingerprint = legacyFingerprint( + name = "recyclerViewTreeObserverFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + strings = listOf("LithoRVSLCBinder") +) 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..b813e6a84 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/resourceid/SharedResourceIdPatch.kt @@ -0,0 +1,656 @@ +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 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 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 notice = -1L + private set +var notificationBigPictureIconWidth = -1L + private set +var offlineActionsVideoDeletedUndoSnackbarText = -1L + private set +var playerCollapseButton = -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 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" + ] + 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" + ] + 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" + ] + notice = resourceMappings[ + ID, + "notice" + ] + notificationBigPictureIconWidth = resourceMappings[ + DIMEN, + "notification_big_picture_icon_width" + ] + offlineActionsVideoDeletedUndoSnackbarText = resourceMappings[ + STRING, + "offline_actions_video_deleted_undo_snackbar_text" + ] + playerCollapseButton = resourceMappings[ + ID, + "player_collapse_button" + ] + 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" + ] + 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..b73c633de --- /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( + returnYouTubeDislikeRollingNumberPatch, + returnYouTubeDislikeShortsPatch, + lithoFilterPatch, + settingsPatch, + 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..04c6cb02b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/returnyoutubeusername/ReturnYouTubeUsernamePatch.kt @@ -0,0 +1,36 @@ +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, + false, +) { + 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/src/main/kotlin/app/revanced/patches/youtube/utils/settings/ResourceUtils.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/ResourceUtils.kt similarity index 65% rename from src/main/kotlin/app/revanced/patches/youtube/utils/settings/ResourceUtils.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/ResourceUtils.kt index 01a6c30f3..7b84b5f93 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/settings/ResourceUtils.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/ResourceUtils.kt @@ -1,32 +1,46 @@ package app.revanced.patches.youtube.utils.settings -import app.revanced.patcher.data.ResourceContext +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 -@Suppress("DEPRECATION", "MemberVisibilityCanBePrivate") -object ResourceUtils { +internal object ResourceUtils { + private lateinit var context: ResourcePatchContext + private lateinit var youtubeSettingFile: File + private lateinit var rvxSettingFile: File - const val TARGET_PREFERENCE_PATH = "res/xml/revanced_prefs.xml" + 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 youtubePackageName = "com.google.android.youtube" + var youtubeMusicPackageName = YOUTUBE_MUSIC_PACKAGE_NAME + var youtubePackageName = YOUTUBE_PACKAGE_NAME private var iconType = "default" fun getIconType() = iconType - fun ResourceContext.updatePackageName( + fun updatePackageName( fromPackageName: String, - toPackageName: String + toPackageName: String, + musicPackageName: String ) { + youtubeMusicPackageName = musicPackageName youtubePackageName = toPackageName - val prefs = this[YOUTUBE_SETTINGS_PATH] - - prefs.writeText( - prefs.readText() + youtubeSettingFile.writeText( + youtubeSettingFile.readText() .replace( "android:targetPackage=\"$fromPackageName", "android:targetPackage=\"$toPackageName" @@ -34,14 +48,12 @@ object ResourceUtils { ) } - fun ResourceContext.updateGmsCorePackageName( + fun updateGmsCorePackageName( fromPackageName: String, toPackageName: String ) { - val prefs = this[TARGET_PREFERENCE_PATH] - - prefs.writeText( - prefs.readText() + rvxSettingFile.writeText( + rvxSettingFile.readText() .replace( "android:targetPackage=\"$fromPackageName", "android:targetPackage=\"$toPackageName" @@ -49,41 +61,44 @@ object ResourceUtils { ) } - fun ResourceContext.addPreference(settingArray: Array) { - val prefs = this[TARGET_PREFERENCE_PATH] + fun addPreference(patch: PatchList) { + patch.included = true + updatePatchStatus(patch.title.replace(" for YouTube", "")) + } + fun addPreference(settingArray: Array, patch: PatchList) { settingArray.forEach preferenceLoop@{ preference -> - prefs.writeText( - prefs.readText() + rvxSettingFile.writeText( + rvxSettingFile.readText() .replace("", "") ) } + + addPreference(patch) } - fun ResourceContext.updatePatchStatus(patchTitle: String) { + fun updatePatchStatus(patchTitle: String) { updatePatchStatusSettings(patchTitle, "@string/revanced_patches_included") } - fun ResourceContext.updatePatchStatusIcon(iconName: String) { + fun updatePatchStatusIcon(iconName: String) { iconType = iconName updatePatchStatusSettings("Icon", "@string/revanced_icon_$iconName") } - fun ResourceContext.updatePatchStatusLabel(appName: String) { + fun updatePatchStatusLabel(appName: String) = updatePatchStatusSettings("Label", appName) - } - fun ResourceContext.updatePatchStatusTheme(themeName: String) { + fun updatePatchStatusTheme(themeName: String) = updatePatchStatusSettings("Theme", themeName) - } - fun ResourceContext.updatePatchStatusSettings( + fun updatePatchStatusSettings( patchTitle: String, updateText: String - ) { - this.xmlEditor[TARGET_PREFERENCE_PATH].use { editor -> - editor.file.doRecursively loop@{ + ) = context.apply { + document(RVX_PREFERENCE_PATH).use { document -> + document.doRecursively loop@{ if (it !is Element) return@loop it.getAttributeNode("android:title")?.let { attribute -> @@ -95,12 +110,12 @@ object ResourceUtils { } } - fun ResourceContext.addPreferenceFragment(key: String, insertKey: String) { + fun addPreferenceFragment(key: String, insertKey: String) = context.apply { val targetClass = "com.google.android.apps.youtube.app.settings.videoquality.VideoQualitySettingsActivity" - this.xmlEditor[YOUTUBE_SETTINGS_PATH].use { editor -> - with(editor.file) { + document(YOUTUBE_SETTINGS_PATH).use { document -> + with(document) { val processedKeys = mutableSetOf() // To track processed keys doRecursively loop@{ node -> @@ -144,4 +159,4 @@ object ResourceUtils { } } } -} +} \ 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..14f371e81 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/settings/SettingsPatch.kt @@ -0,0 +1,268 @@ +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.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.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.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, + ) + + 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, + ) + + 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") + } + + 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) + } + } + } + } +} 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..2c1692bb3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/sponsorblock/SponsorBlockPatch.kt @@ -0,0 +1,282 @@ +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.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.ResourceGroup +import app.revanced.util.copyResources +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.indexOfFirstLiteralInstructionOrThrow +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 + +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") + } +} + +@Suppress("unused") +val sponsorBlockPatch = resourcePatch( + SPONSORBLOCK.title, + SPONSORBLOCK.summary, +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + playerControlsPatch, + sponsorBlockBytecodePatch, + settingsPatch + ) + + val outlineIcon by booleanOption( + key = "outlineIcon", + default = false, + title = "Outline icons", + description = "Apply the outline icon.", + 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_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) + } + } + + /** + * 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..dfaaf14a0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/Fingerprints.kt @@ -0,0 +1,33 @@ +package app.revanced.patches.youtube.utils.toolbar + +import app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.youtube.utils.resourceid.menuItemView +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 toolBarButtonFingerprint = legacyFingerprint( + name = "toolBarButtonFingerprint", + 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 + ), + literals = listOf(menuItemView), +) +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/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/ToolBarHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/ToolBarHookPatch.kt similarity index 52% rename from src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/ToolBarHookPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/ToolBarHookPatch.kt index f4e7ef890..250bd839f 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/ToolBarHookPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/utils/toolbar/ToolBarHookPatch.kt @@ -1,39 +1,33 @@ 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.patch.bytecodePatch 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 app.revanced.patches.youtube.utils.extension.Constants.UTILS_PATH +import app.revanced.patches.youtube.utils.resourceid.sharedResourceIdPatch +import app.revanced.util.fingerprint.matchOrThrow +import app.revanced.util.fingerprint.methodOrThrow 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 EXTENSION_CLASS_DESCRIPTOR = + "$UTILS_PATH/ToolBarPatch;" + +private lateinit var toolbarMethod: MutableMethod + +val toolBarHookPatch = bytecodePatch( + description = "toolBarHookPatch" ) { - private const val INTEGRATIONS_CLASS_DESCRIPTOR = - "$UTILS_PATH/ToolBarPatch;" + dependsOn(sharedResourceIdPatch) - 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 + execute { + toolBarButtonFingerprint.matchOrThrow().let { + it.method.apply { + val replaceIndex = it.patternMatch!!.startIndex + val freeIndex = it.patternMatch!!.endIndex - 1 val replaceReference = getInstruction(replaceIndex).reference val replaceRegister = @@ -48,7 +42,7 @@ object ToolBarHookPatch : BytecodePatch( 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-static {v$enumRegister, v$freeRegister}, $EXTENSION_CLASS_DESCRIPTOR->hookToolBar(Ljava/lang/Enum;Landroid/widget/ImageView;)V invoke-interface {v$replaceRegister, v$enumRegister}, $replaceReference """ ) @@ -56,15 +50,12 @@ object ToolBarHookPatch : BytecodePatch( } } - toolbarMethod = ToolBarPatchFingerprint.resultOrThrow().mutableMethod + toolbarMethod = toolBarPatchFingerprint.methodOrThrow() } +} - 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 +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..5fff83ae5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt @@ -0,0 +1,648 @@ +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): 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 + + 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;") + 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..d081d3bde --- /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.bottomSheetRecyclerViewHook +import app.revanced.patches.youtube.utils.recyclerview.bottomSheetRecyclerViewPatch +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( + bottomSheetRecyclerViewPatch, + customPlaybackSpeedPatch( + "$VIDEO_PATH/CustomPlaybackSpeedPatch;", + 8.0f + ), + flyoutMenuHookPatch, + lithoFilterPatch, + playerTypeHookPatch, + settingsPatch, + shortsPlaybackPatch, + videoIdPatch, + videoInformationPatch, + sharedResourceIdPatch, + ) + + execute { + + var settingArray = arrayOf( + "PREFERENCE_SCREEN: VIDEO" + ) + + // region patch for custom playback speed + + bottomSheetRecyclerViewHook("$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") + } + + bottomSheetRecyclerViewHook("$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/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/fingerprint/PlayerParameterBuilderFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt similarity index 51% rename from src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/fingerprint/PlayerParameterBuilderFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt index 6053c8064..02e4b048f 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/fingerprint/PlayerParameterBuilderFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt @@ -1,13 +1,30 @@ -package app.revanced.patches.youtube.video.playerresponse.fingerprint +package app.revanced.patches.youtube.video.playerresponse -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.fingerprint.legacyFingerprint +import app.revanced.util.or import app.revanced.util.parametersEqual import com.android.tools.smali.dexlib2.AccessFlags -internal object PlayerParameterBuilderFingerprint : MethodFingerprint( +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: @@ -40,8 +57,8 @@ internal object PlayerParameterBuilderFingerprint : MethodFingerprint( // "Z", // Appears to indicate if the video id is being opened or is currently playing. // "Z", // "Z" - customFingerprint = custom@{ methodDef, _ -> - val parameterTypes = methodDef.parameterTypes + customFingerprint = custom@{ method, _ -> + val parameterTypes = method.parameterTypes val parameterSize = parameterTypes.size if (parameterSize != 13 && parameterSize != 14) { return@custom false @@ -50,25 +67,13 @@ internal object PlayerParameterBuilderFingerprint : MethodFingerprint( 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/src/main/kotlin/app/revanced/patches/youtube/video/videoid/fingerprints/VideoIdFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/Fingerprints.kt similarity index 77% rename from src/main/kotlin/app/revanced/patches/youtube/video/videoid/fingerprints/VideoIdFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/Fingerprints.kt index 04ed1527a..3567bf0fd 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/video/videoid/fingerprints/VideoIdFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/Fingerprints.kt @@ -1,16 +1,17 @@ -package app.revanced.patches.youtube.video.videoid.fingerprints +package app.revanced.patches.youtube.video.videoid -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.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 object VideoIdFingerprint : MethodFingerprint( +internal val videoIdFingerprint = legacyFingerprint( + name = "videoIdFingerprint", returnType = "V", accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, parameters = listOf("L"), @@ -20,11 +21,11 @@ internal object VideoIdFingerprint : MethodFingerprint( Opcode.INVOKE_INTERFACE, Opcode.MOVE_RESULT_OBJECT ), - customFingerprint = custom@{ methodDef, classDef -> + customFingerprint = custom@{ method, classDef -> if (!classDef.fields.any { it.type == "Lcom/google/android/libraries/youtube/player/subtitles/model/SubtitleTrack;" }) { return@custom false } - val implementation = methodDef.implementation + val implementation = method.implementation ?: return@custom false val instructions = implementation.instructions val instructionCount = instructions.count() @@ -38,7 +39,7 @@ internal object VideoIdFingerprint : MethodFingerprint( return@custom false } - methodDef.indexOfFirstInstruction { + method.indexOfFirstInstruction { val methodReference = getReference() opcode == Opcode.INVOKE_INTERFACE && methodReference?.returnType == "Ljava/lang/String;" && @@ -46,4 +47,4 @@ internal object VideoIdFingerprint : MethodFingerprint( methodReference.definingClass == PLAYER_RESPONSE_MODEL_CLASS_DESCRIPTOR } >= 0 }, -) \ No newline at end of file +) 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/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt similarity index 57% rename from src/main/kotlin/app/revanced/util/BytecodeUtils.kt rename to patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index 0f4918862..ad3f5468f 100644 --- a/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -1,23 +1,25 @@ -@file:Suppress("unused") +@file:Suppress("CONTEXT_RECEIVERS_DEPRECATED") package app.revanced.util -import app.revanced.patcher.data.BytecodeContext +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.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import app.revanced.patcher.fingerprint.MethodFingerprintResult +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 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 app.revanced.patches.shared.mapping.get +import app.revanced.patches.shared.mapping.resourceMappingPatch +import app.revanced.patches.shared.mapping.resourceMappings import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.Method @@ -38,13 +40,6 @@ 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 @@ -57,32 +52,6 @@ fun parametersEqual( 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]. * @@ -93,17 +62,6 @@ 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. * @@ -111,7 +69,7 @@ fun MutableClass.transformFields(transform: MutableField.() -> MutableField) { */ fun MutableClass.transformMethods(transform: MutableMethod.() -> MutableMethod) { val transformedMethods = methods.map { it.transform() } - methods.removeIf { !MethodUtil.isConstructor(it) } + methods.clear() methods.addAll(transformedMethods) } @@ -127,318 +85,124 @@ fun MutableMethod.injectHideViewCall( insertIndex: Int, viewRegister: Int, classDescriptor: String, - targetMethod: String + targetMethod: String, ) = addInstruction( insertIndex, - "invoke-static { v$viewRegister }, $classDescriptor->$targetMethod(Landroid/view/View;)V" + "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 +/** + * 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, ) { - resultOrThrow().mutableMethod.apply { - val literalIndex = indexOfFirstWideLiteralInstructionValueOrThrow(literal) - val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT) - val targetRegister = getInstruction(targetIndex).registerA + // Duplicate original instruction and add to +1 index. + addInstruction(insertIndex + 1, getInstruction(insertIndex)) - 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 - """ + // Add patch code at same index as duplicated instruction, + // so it uses the original instruction control flow label. + addInstructionsWithLabels(insertIndex + 1, instructions) - addInstructions( - targetIndex + 1, - smaliInstruction - ) - } -} + // Remove original non duplicated instruction. + removeInstruction(insertIndex) -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) - ) - } - } - } - } - } + // 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] that matches the predicate, starting from [startIndex]. + * Get the index of the first instruction with the id of the given resource id name. * - * @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]. + * Requires [resourceMappingPatch] as a dependency. * - * @param startIndex Optional starting index to start searching from. - * @return -1 if the instruction is not found. - * @see indexOfFirstInstructionOrThrow + * @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.indexOfFirstInstruction(startIndex: Int = 0, predicate: Instruction.() -> Boolean): Int { - if (implementation == null) { +fun Method.indexOfFirstResourceId(resourceName: String): Int { + val resourceId = resourceMappings["id", resourceName] + if (resourceId == -1L) { + println("WARNING: Could not find resource type: id name: $name") 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 - } + return indexOfFirstLiteralInstruction(resourceId) } -fun Method.indexOfFirstInstructionOrThrow(opcode: Opcode): Int = - indexOfFirstInstructionOrThrow(0, opcode) - /** - * Get the index of the first [Instruction] that matches the predicate, starting from [startIndex]. + * Get the index of the first instruction with the id of the given resource name or throw a [PatchException]. * - * @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]. + * Requires [resourceMappingPatch] as a dependency. * - * @return the index of the instruction - * @throws PatchException - * @see indexOfFirstInstruction + * @throws [PatchException] if the resource is not found, or the method does not contain the resource id literal value. + * @see [indexOfFirstResourceId], [indexOfFirstLiteralInstructionReversedOrThrow] */ -fun Method.indexOfFirstInstructionOrThrow( - startIndex: Int = 0, - predicate: Instruction.() -> Boolean -): Int { - val index = indexOfFirstInstruction(startIndex, predicate) +fun Method.indexOfFirstResourceIdOrThrow(resourceName: String): Int { + val index = indexOfFirstResourceId(resourceName) 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") + throw PatchException("Found resource id for: '$resourceName' but method does not contain the id: $this") } 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. + * 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 indexOfFirstWideLiteralInstructionValueOrThrow + * @see indexOfFirstLiteralInstructionOrThrow */ -fun Method.indexOfFirstWideLiteralInstructionValue(literal: Long) = implementation?.let { +fun Method.indexOfFirstLiteralInstruction(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, + * 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.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() +fun Method.indexOfFirstLiteralInstructionOrThrow(literal: Long): Int { + val index = indexOfFirstLiteralInstruction(literal) + if (index < 0) throw PatchException("Could not find literal value: $literal") + return index +} - throw PatchException("Found literal value: '$value' but method does not contain the id: $this") +/** + * 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 } @@ -448,7 +212,6 @@ fun Method.indexOfFirstStringInstruction(str: String) = getReference()?.string == str } - fun Method.indexOfFirstStringInstructionOrThrow(str: String): Int { val index = indexOfFirstStringInstruction(str) if (index < 0) { @@ -463,8 +226,8 @@ fun Method.indexOfFirstStringInstructionOrThrow(str: String): Int { * * @return if the method contains a literal with the given value. */ -fun Method.containsWideLiteralInstructionValue(literal: Long) = - indexOfFirstWideLiteralInstructionValue(literal) >= 0 +fun Method.containsLiteralInstruction(literal: Long) = + indexOfFirstLiteralInstruction(literal) >= 0 /** * Traverse the class hierarchy starting from the given root class. @@ -472,16 +235,64 @@ fun Method.containsWideLiteralInstructionValue(literal: Long) = * @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( +fun BytecodePatchContext.traverseClassHierarchy( targetClass: MutableClass, callback: MutableClass.() -> Unit ) { callback(targetClass) - this.findClass(targetClass.superclass ?: return)?.mutableClass?.let { + + 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]. * @@ -493,18 +304,231 @@ fun BytecodeContext.traverseClassHierarchy( inline fun Instruction.getReference() = (this as? ReferenceInstruction)?.reference as? T -fun MethodFingerprintResult.getWalkerMethod(context: BytecodeContext, offset: Int) = - mutableMethod.getWalkerMethod(context, offset) +/** + * @return The index of the first opcode specified, or -1 if not found. + * @see indexOfFirstInstructionOrThrow + */ +fun Method.indexOfFirstInstruction(targetOpcode: Opcode): Int = + indexOfFirstInstruction(0, targetOpcode) /** - * MethodWalker can find the wrong class: - * https://github.com/ReVanced/revanced-patcher/issues/309 - * - * As a workaround, redefine MethodWalker here + * @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 MutableMethod.getWalkerMethod(context: BytecodeContext, offset: Int): MutableMethod { +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 context.findMethodOrThrow(newMethod.definingClass) { + return findMethodOrThrow(newMethod.definingClass) { MethodUtil.methodSignaturesMatch(this, newMethod) } } @@ -519,7 +543,8 @@ fun MutableMethod.getFiveRegisters(index: Int) = .take(registerCount).joinToString(",") { "v$it" } } -fun BytecodeContext.addStaticFieldToIntegration( +context(BytecodePatchContext) +fun addStaticFieldToExtension( className: String, methodName: String, fieldName: String, @@ -527,9 +552,9 @@ fun BytecodeContext.addStaticFieldToIntegration( smaliInstructions: String, shouldAddConstructor: Boolean = true ) { - val mutableClass = findClass { classDef -> classDef.type == className } - ?.mutableClass - ?: throw PatchException("No matching classes found: $className") + 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" @@ -584,25 +609,23 @@ fun BytecodeContext.addStaticFieldToIntegration( } } -fun BytecodeContext.findMethodOrThrow( +context(BytecodePatchContext) +fun 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") - } +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 } -fun BytecodeContext.updatePatchStatus( +context(BytecodePatchContext) +fun updatePatchStatus( className: String, methodName: String ) = findMethodOrThrow(className) { name == methodName } @@ -644,28 +667,60 @@ fun Method.cloneMutable( } /** - * Return the resolved methods of [MethodFingerprint]s early. + * Return the method early. */ -fun List.returnEarly(bool: Boolean = false) { +fun MutableMethod.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 - """ + val stringInstructions = when (returnType.first()) { + 'L' -> + """ + const/4 v0, $const + return-object v0 + """ - else -> throw PatchException("This case should never happen: ${fingerprint.javaClass.simpleName}") - } + 'V' -> "return-void" + 'I', 'Z' -> + """ + const/4 v0, $const + return v0 + """ - result.mutableMethod.addInstructions(0, stringInstructions) - } + 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/src/main/kotlin/app/revanced/util/ResourceUtils.kt b/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt similarity index 51% rename from src/main/kotlin/app/revanced/util/ResourceUtils.kt rename to patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt index f82f8c1a5..25d6257b6 100644 --- a/src/main/kotlin/app/revanced/util/ResourceUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt @@ -1,11 +1,11 @@ -@file:Suppress("DEPRECATION", "MemberVisibilityCanBePrivate", "SpellCheckingInspection") - package app.revanced.util -import app.revanced.patcher.data.ResourceContext +import app.revanced.patcher.patch.Option +import app.revanced.patcher.patch.Patch import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.patch.options.PatchOption -import app.revanced.patcher.util.DomFileEditor +import app.revanced.patcher.patch.ResourcePatchContext +import app.revanced.patcher.util.Document +import org.w3c.dom.Attr import org.w3c.dom.Element import org.w3c.dom.Node import org.w3c.dom.NodeList @@ -14,18 +14,22 @@ import java.io.InputStream import java.nio.file.Files import java.nio.file.StandardCopyOption -val classLoader: ClassLoader = object {}.javaClass.classLoader +private val classLoader = object {}.javaClass.classLoader -fun PatchOption.valueOrThrow() = value +@Suppress("UNCHECKED_CAST") +fun Patch<*>.getStringOptionValue(key: String) = + options[key] as Option + +fun Option.valueOrThrow() = value ?: throw PatchException("Invalid patch option: $title.") -fun PatchOption.valueOrThrow() = value +fun Option.valueOrThrow() = value ?: throw PatchException("Invalid patch option: $title.") -fun PatchOption.lowerCaseOrThrow() = valueOrThrow() +fun Option.lowerCaseOrThrow() = valueOrThrow() .lowercase() -fun PatchOption.underBarOrThrow() = lowerCaseOrThrow() +fun Option.underBarOrThrow() = lowerCaseOrThrow() .replace(" ", "_") fun Node.adoptChild(tagName: String, block: Element.() -> Unit) { @@ -40,6 +44,26 @@ fun Node.cloneNodes(parent: Node) { parent.removeChild(this) } +/** + * Returns a sequence for all child nodes. + */ +fun NodeList.asSequence() = (0 until this.length).asSequence().map { this.item(it) } + +/** + * Returns a sequence for all child nodes. + */ +@Suppress("UNCHECKED_CAST") +fun Node.childElementsSequence() = + this.childNodes.asSequence().filter { it.nodeType == Node.ELEMENT_NODE } as Sequence + +/** + * Performs the given [action] on each child element. + */ +inline fun Node.forEachChildElement(action: (Element) -> Unit) = + childElementsSequence().forEach { + action(it) + } + /** * Recursively traverse the DOM tree starting from the given root node. * @@ -50,12 +74,6 @@ fun Node.doRecursively(action: (Node) -> Unit) { 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)) @@ -70,7 +88,7 @@ fun List.getResourceGroup(fileNames: Array) = map { directory -> ) } -fun ResourceContext.appendAppVersion(appVersion: String) { +fun ResourcePatchContext.appendAppVersion(appVersion: String) { addEntryValues( "revanced_spoof_app_version_target_entries", "@string/revanced_spoof_app_version_target_entry_" + appVersion.replace(".", "_"), @@ -83,16 +101,15 @@ fun ResourceContext.appendAppVersion(appVersion: String) { ) } -fun ResourceContext.addEntryValues( +fun ResourcePatchContext.addEntryValues( attributeName: String, attributeValue: String, path: String = "res/values/arrays.xml", prepend: Boolean = true, ) { - xmlEditor[path].use { - with(it.file) { + 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 @@ -111,7 +128,7 @@ fun ResourceContext.addEntryValues( } } -fun ResourceContext.copyFile( +fun ResourcePatchContext.copyFile( resourceGroup: List, path: String, warning: String @@ -119,7 +136,7 @@ fun ResourceContext.copyFile( resourceGroup.let { resourceGroups -> try { val filePath = File(path) - val resourceDirectory = this["res"] + val resourceDirectory = get("res") resourceGroups.forEach { group -> val fromDirectory = filePath.resolve(group.resourceDirectoryName) @@ -141,38 +158,117 @@ fun ResourceContext.copyFile( 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. - * @param createDirectoryIfNotExist Whether to create a new directory if it does not exist. */ -fun ResourceContext.copyResources( +fun ResourcePatchContext.copyResources( sourceResourceDirectory: String, vararg resources: ResourceGroup, createDirectoryIfNotExist: Boolean = false, ) { - val targetResourceDirectory = this["res"] + val resourceDirectory = get("res") for (resourceGroup in resources) { resourceGroup.resources.forEach { resource -> val resourceDirectoryName = resourceGroup.resourceDirectoryName - if (createDirectoryIfNotExist) { - val targetDirectory = targetResourceDirectory.resolve(resourceDirectoryName) + val targetDirectory = resourceDirectory.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(), + resourceDirectory.resolve(resourceFile).toPath(), StandardCopyOption.REPLACE_EXISTING, ) } @@ -180,6 +276,12 @@ fun ResourceContext.copyResources( } } +internal fun inputStreamFromBundledResourceOrThrow( + sourceResourceDirectory: String, + resourceFile: String, +) = classLoader.getResourceAsStream("$sourceResourceDirectory/$resourceFile") + ?: throw PatchException("Could not find $resourceFile") + internal fun inputStreamFromBundledResource( sourceResourceDirectory: String, resourceFile: String, @@ -192,13 +294,28 @@ internal fun inputStreamFromBundledResource( */ class ResourceGroup(val resourceDirectoryName: String, vararg val resources: String) +/** + * Iterate through the children of a node by its tag. + * @param resource The xml resource. + * @param targetTag The target xml node. + * @param callback The callback to call when iterating over the nodes. + */ +fun ResourcePatchContext.iterateXmlNodeChildren( + resource: String, + targetTag: String, + callback: (node: Node) -> Unit, +) = document(classLoader.getResourceAsStream(resource)!!).use { document -> + val stringsNode = document.getElementsByTagName(targetTag).item(0).childNodes + for (i in 1 until stringsNode.length - 1) callback(stringsNode.item(i)) +} + /** * 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( +fun ResourcePatchContext.copyXmlNode( resourceDirectory: String, targetResource: String, elementTag: String @@ -208,26 +325,28 @@ fun ResourceContext.copyXmlNode( )?.let { inputStream -> // Copy nodes from the resources node to the real resource node elementTag.copyXmlNode( - this.xmlEditor[inputStream], - this.xmlEditor["res/$targetResource"] + document(inputStream), + document("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. + * 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: DomFileEditor, target: DomFileEditor): AutoCloseable { - val hostNodes = source.file.getElementsByTagName(this).item(0).childNodes +fun String.copyXmlNode( + source: Document, + target: Document, +): AutoCloseable { + val hostNodes = source.getElementsByTagName(this).item(0).childNodes - val destinationResourceFile = target.file - val destinationNode = destinationResourceFile.getElementsByTagName(this).item(0) + val destinationNode = target.getElementsByTagName(this).item(0) for (index in 0 until hostNodes.length) { val node = hostNodes.item(index).cloneNode(true) - destinationResourceFile.adoptNode(node) + target.adoptNode(node) destinationNode.appendChild(node) } @@ -237,6 +356,9 @@ fun String.copyXmlNode(source: DomFileEditor, target: DomFileEditor): AutoClosea } } +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) @@ -258,10 +380,15 @@ internal fun NodeList.findElementByAttributeValue(attributeName: String, value: return null } -internal fun NodeList.findElementByAttributeValueOrThrow( - attributeName: String, - value: String -): Element { - return findElementByAttributeValue(attributeName, value) +internal fun NodeList.findElementByAttributeValueOrThrow(attributeName: String, value: String) = + findElementByAttributeValue(attributeName, value) ?: throw PatchException("Could not find: $attributeName $value") -} \ No newline at end of file + +internal fun Element.copyAttributesFrom(oldContainer: Element) { + // Copy attributes from the old element to the new element + val attributes = oldContainer.attributes + for (i in 0 until attributes.length) { + val attr = attributes.item(i) as Attr + setAttribute(attr.name, attr.value) + } +} diff --git a/src/main/kotlin/app/revanced/util/Utils.kt b/patches/src/main/kotlin/app/revanced/util/Utils.kt similarity index 100% rename from src/main/kotlin/app/revanced/util/Utils.kt rename to patches/src/main/kotlin/app/revanced/util/Utils.kt 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..9e73e6257 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/util/fingerprint/LegacyFingerprint.kt @@ -0,0 +1,164 @@ +@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.methodOrThrow(): MutableMethod = + second.methodOrNull ?: throw first.exception + +context(BytecodePatchContext) +internal fun Pair.methodOrThrow(parentFingerprint: Pair): MutableMethod = + matchOrThrow(parentFingerprint).method + +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/src/main/resources/music/branding/afn_blue/settings/drawable/revanced_extended_settings_icon.xml b/patches/src/main/resources/music/branding/afn_blue/settings/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/branding/afn_blue/settings/drawable/revanced_extended_settings_icon.xml rename to patches/src/main/resources/music/branding/afn_blue/settings/drawable/revanced_extended_settings_icon.xml 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/src/main/resources/music/branding/afn_red/settings/drawable/revanced_extended_settings_icon.xml b/patches/src/main/resources/music/branding/afn_red/settings/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/branding/afn_red/settings/drawable/revanced_extended_settings_icon.xml rename to patches/src/main/resources/music/branding/afn_red/settings/drawable/revanced_extended_settings_icon.xml 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/src/main/resources/music/branding/mmt/settings/drawable/revanced_extended_settings_icon.xml b/patches/src/main/resources/music/branding/mmt/settings/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/branding/mmt/settings/drawable/revanced_extended_settings_icon.xml rename to patches/src/main/resources/music/branding/mmt/settings/drawable/revanced_extended_settings_icon.xml 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/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/src/main/resources/music/branding/revancify_blue/settings/drawable/revanced_extended_settings_icon.xml b/patches/src/main/resources/music/branding/revancify_blue/settings/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/branding/revancify_blue/settings/drawable/revanced_extended_settings_icon.xml rename to patches/src/main/resources/music/branding/revancify_blue/settings/drawable/revanced_extended_settings_icon.xml 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/src/main/resources/music/branding/revancify_red/settings/drawable/revanced_extended_settings_icon.xml b/patches/src/main/resources/music/branding/revancify_red/settings/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/branding/revancify_red/settings/drawable/revanced_extended_settings_icon.xml rename to patches/src/main/resources/music/branding/revancify_red/settings/drawable/revanced_extended_settings_icon.xml 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/youtube_music/settings/drawable/revanced_extended_settings_icon.xml b/patches/src/main/resources/music/branding/youtube_music/settings/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/branding/youtube_music/settings/drawable/revanced_extended_settings_icon.xml rename to patches/src/main/resources/music/branding/youtube_music/settings/drawable/revanced_extended_settings_icon.xml 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/src/main/resources/music/settings/host/values/arrays.xml b/patches/src/main/resources/music/settings/host/values/arrays.xml similarity index 100% rename from src/main/resources/music/settings/host/values/arrays.xml rename to patches/src/main/resources/music/settings/host/values/arrays.xml 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/src/main/resources/music/settings/host/values/strings.xml b/patches/src/main/resources/music/settings/host/values/strings.xml similarity index 98% rename from src/main/resources/music/settings/host/values/strings.xml rename to patches/src/main/resources/music/settings/host/values/strings.xml index a4bf4a33a..8761e31d6 100644 --- a/src/main/resources/music/settings/host/values/strings.xml +++ b/patches/src/main/resources/music/settings/host/values/strings.xml @@ -58,7 +58,10 @@ Please download %2$s from the website." Ads Hide fullscreen ads - Hides fullscreen ads. + "Hides fullscreen ads. + +Limitations: +• Sometimes you may see a blank black screen instead of the home feed." Hide general ads Hides general ads. Hide media ads @@ -439,6 +442,14 @@ This is required for the app to work." Tap on the continue button and disable battery optimizations." Continue + Spoof client + "Spoof the client to prevent playback issues. + +Limitations: +• OPUS audio codec may not be supported. +• Seekbar thumbnail may not be present. +• Watch history does not work with a brand account. + Sanitize sharing links Removes tracking query parameters from URLs when sharing links. 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_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_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_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_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_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_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_icon.xml b/patches/src/main/resources/music/settings/icons/revanced_colored/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/settings/icons/revanced_colored/drawable/revanced_extended_settings_icon.xml rename to patches/src/main/resources/music/settings/icons/revanced_colored/drawable/revanced_extended_settings_icon.xml diff --git a/src/main/resources/music/translations/bg-rBG/strings.xml b/patches/src/main/resources/music/translations/bg-rBG/strings.xml similarity index 99% rename from src/main/resources/music/translations/bg-rBG/strings.xml rename to patches/src/main/resources/music/translations/bg-rBG/strings.xml index 75d3328f1..21f9281a4 100644 --- a/src/main/resources/music/translations/bg-rBG/strings.xml +++ b/patches/src/main/resources/music/translations/bg-rBG/strings.xml @@ -48,7 +48,7 @@ Реклами Скриване на рекламите в режим на цял екран - Скриване на рекламите в режим на цял екран. + "Скриване на рекламите в режим на цял екран." Скриване на общите реклами Скриване на общите реклами. Скриване на музикални реклами diff --git a/src/main/resources/music/translations/bn/strings.xml b/patches/src/main/resources/music/translations/bn/strings.xml similarity index 100% rename from src/main/resources/music/translations/bn/strings.xml rename to patches/src/main/resources/music/translations/bn/strings.xml diff --git a/src/main/resources/music/translations/cs-rCZ/strings.xml b/patches/src/main/resources/music/translations/cs-rCZ/strings.xml similarity index 100% rename from src/main/resources/music/translations/cs-rCZ/strings.xml rename to patches/src/main/resources/music/translations/cs-rCZ/strings.xml diff --git a/src/main/resources/music/translations/el-rGR/strings.xml b/patches/src/main/resources/music/translations/el-rGR/strings.xml similarity index 98% rename from src/main/resources/music/translations/el-rGR/strings.xml rename to patches/src/main/resources/music/translations/el-rGR/strings.xml index 5023fac62..d78fe4a6a 100644 --- a/src/main/resources/music/translations/el-rGR/strings.xml +++ b/patches/src/main/resources/music/translations/el-rGR/strings.xml @@ -49,7 +49,7 @@ Διαφημίσεις Απόκρυψη διαφημίσεων πλήρους οθόνης - Απόκρυψη των ενδιάμεσων διαφημίσεων πλήρους οθόνης. + "Απόκρυψη των ενδιάμεσων διαφημίσεων πλήρους οθόνης." Απόκρυψη γενικών διαφημίσεων Απόκρυψη των γενικών διαφημίσεων. Απόκρυψη διαφημίσεων μουσικής @@ -220,7 +220,7 @@ Απόκρυψη των οδηγιών κοινότητας στην κορυφή της ενότητας σχολίων. Απόκρυψη κουμπιών χρονοσήμανσης & emoji Απόκρυψη των κουμπιών χρονοσήμανσης και επιλογής emoji κατά την πληκτρολόγηση σχολίου. - Φόντο διπλού πατήματος της οθόνης αναπαραγωγής + Απόκρυψη φόντου διπλού πατήματος της οθόνης αναπαραγωγής Απόκρυψη του σκοτεινού φόντου που εμφανίζεται στην οθόνη αναπαραγωγής όταν γίνεται διπλό πάτημα για αναζήτηση. Απόκρυψη κουμπιού κοινοποίησης στη λειτουργία πλήρους οθόνης Απόκρυψη του κουμπιού κοινοποίησης στην οθόνη αναπαραγωγής πλήρους οθόνης. @@ -390,6 +390,12 @@ Πατήστε το κουμπί «Συνέχεια» και απενεργοποιήστε τις βελτιστοποιήσεις μπαταρίας για το MicroG." Συνέχεια + Παραποίηση προγράμματος πελάτη + Παραποίηση του προγράμματος πελάτη για την αποφυγή προβλημάτων αναπαραγωγής. + +Περιορισμοί: +• Οι μικρογραφίες προεπισκόπησης στη γραμμή προόδου ενδέχεται να μην υπάρχουν. +• Το ιστορικό παρακολούθησης δεν λειτουργεί σε λογαριασμούς επωνυμίας (brand). Καθαρισμός συνδέσμων κοινοποίησης Αφαίρεση των παραμέτρων παρακολούθησης από τις διευθύνσεις URL κατά την κοινοποίηση συνδέσμων. Άνοιγμα ρυθμίσεων προεπιλεγμένων εφαρμογών diff --git a/src/main/resources/music/translations/es-rES/strings.xml b/patches/src/main/resources/music/translations/es-rES/strings.xml similarity index 99% rename from src/main/resources/music/translations/es-rES/strings.xml rename to patches/src/main/resources/music/translations/es-rES/strings.xml index f0ec43807..11c003032 100644 --- a/src/main/resources/music/translations/es-rES/strings.xml +++ b/patches/src/main/resources/music/translations/es-rES/strings.xml @@ -49,7 +49,7 @@ Descarga %2$s desde el sitio web." Anuncios Ocultar anuncios en pantalla completa - Oculta anuncios en pantalla completa. + "Oculta anuncios en pantalla completa." Ocultar anuncios generales Oculta anuncios generales. Ocultar anuncios de música diff --git a/src/main/resources/music/translations/fr-rFR/strings.xml b/patches/src/main/resources/music/translations/fr-rFR/strings.xml similarity index 92% rename from src/main/resources/music/translations/fr-rFR/strings.xml rename to patches/src/main/resources/music/translations/fr-rFR/strings.xml index 8fca9c77e..30186be91 100644 --- a/src/main/resources/music/translations/fr-rFR/strings.xml +++ b/patches/src/main/resources/music/translations/fr-rFR/strings.xml @@ -49,7 +49,7 @@ Veuillez télécharger %2$s à partir du site web." Publicités Masquer les publicités en plein écran - Masque les publicités en plein écran. + "Masque les publicités en plein écran." Masquer les publicités générales Masque les publicités générales. Masquer les publicités musicales @@ -273,6 +273,8 @@ Certaines fonctions peuvent ne pas fonctionner sur l'ancienne mise en page."Les \"Je n\'aime pas\" sont affichés en pourcentage plutôt qu\'en nombre. Bouton \"J\'aime\" compact Masque les séparateurs sur le bouton \"J\'aime\". + Afficher les \"J\'aime\" estimés + Affiche le nombre de \"J\'aime\" estimés sur les vidéos. Afficher un message si l\'API est indisponible Affiche un message si l\'API de Return YouTube Dislike n\'est pas disponible. À propos @@ -282,7 +284,26 @@ Certaines fonctions peuvent ne pas fonctionner sur l'ancienne mise en page."Les \"Je n\'aime pas\" sont indisponible (status %d). Les \"Je n\'aime pas\" sont indisponibles (le client a atteint la limite de l\'API). Les \"Je n\'aime pas\" sont indisponible (%s). + Masqué + Return YouTube Username + Activer Return YouTube Username + Remplace l\'identifiant par les noms d\'utilisateurs dans les commentaires. + Format d\'affichage + Sélectionnez le format d\'affichage des noms d\'utilisateurs. + Nom d\'utilisateur + Nom d\'utilisateur (@identifiant) + \@identifiant (Nom d\'utilisateur) + Clé API des données YouTube + La clé de développeur pour utiliser YouTube Data API v3. + À propos de la clé YouTube Data API + "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." + Obtenir une clé développeur pour YouTube Data API v3 + 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. SponsorBlock Activer Sponsorblock diff --git a/src/main/resources/music/translations/hu-rHU/strings.xml b/patches/src/main/resources/music/translations/hu-rHU/strings.xml similarity index 94% rename from src/main/resources/music/translations/hu-rHU/strings.xml rename to patches/src/main/resources/music/translations/hu-rHU/strings.xml index de62f4e43..868fb50e0 100644 --- a/src/main/resources/music/translations/hu-rHU/strings.xml +++ b/patches/src/main/resources/music/translations/hu-rHU/strings.xml @@ -2,6 +2,7 @@ ReVanced Extended + 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 @@ -48,7 +49,7 @@ Töltsd le a(z) %2$s weboldalról." Hirdetések Teljes képernyős hirdetések elrejtése - Teljes képernyős hirdetések elrejtése. + "Teljes képernyős hirdetések elrejtése." Általános hirdetések elrejtése Elrejti az általános hirdetéseket. Zenei hirdetések elrejtése @@ -234,14 +235,23 @@ Ez nem kerüli meg a korhatárkorlátozást. Csak automatikusan elfogadja azt."< "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." + Beállítások menü + Családi központ menü elrejtése + Általános menü elrejtése + Következő lejátszása menü elrejtése + Adatmegtakarító menü elejtése + Értesítések menü elrejtése + Névjegy menü elrejtése Videó Egyéni lejátszási sebességek szerkesztése Az elérhető lejátszási sebességek módosítása vagy hozzáadása. Lejátszási sebesség módosításainak megjegyzése Megjegyzi az utoljára kiválasztott lejátszási sebességet. + Mutass egy felugró értesítést Videó minőség megjegyzése Megjegyezi a legutolsó videó minőséget, amit kiválasztottál. + Mutass egy felugró értesítést 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. Megváltoztatva az alap sebességet %s-re. @@ -265,7 +275,14 @@ Előfordulhat, hogy egyes funkciók nem működnek megfelelően a régi lejátsz A nem tetszik funkció nem elérhető (állapot: %d). Nem tetszések nem érhetőek el (kliens API limit elérve). A nem tetszik funkció nem elérhető (%s). + Rejtett + Felhasználónév + Felhasználónév (@kezelő) + \@kezelő (felhasználónév) + YouTube adat API kulcs + A fejlesztői kulcs a YouTube Data API v3 használatához. + A YouTube Data API-kulcsról Szponzor Blokk SzponsorBlokk engedélyezése diff --git a/src/main/resources/music/translations/id-rID/strings.xml b/patches/src/main/resources/music/translations/id-rID/strings.xml similarity index 99% rename from src/main/resources/music/translations/id-rID/strings.xml rename to patches/src/main/resources/music/translations/id-rID/strings.xml index bf556e161..36c8d0017 100644 --- a/src/main/resources/music/translations/id-rID/strings.xml +++ b/patches/src/main/resources/music/translations/id-rID/strings.xml @@ -48,7 +48,7 @@ Download %2$s dari website." Iklan Sembunyikan iklan fullscreen - Menyembunyikan iklan fullscreen. + "Menyembunyikan iklan fullscreen." Sembunyikan Iklan Umum Menyembunyikan Iklan Umum. Sembunyikan iklan musik diff --git a/src/main/resources/music/translations/in/strings.xml b/patches/src/main/resources/music/translations/in/strings.xml similarity index 99% rename from src/main/resources/music/translations/in/strings.xml rename to patches/src/main/resources/music/translations/in/strings.xml index bf556e161..36c8d0017 100644 --- a/src/main/resources/music/translations/in/strings.xml +++ b/patches/src/main/resources/music/translations/in/strings.xml @@ -48,7 +48,7 @@ Download %2$s dari website." Iklan Sembunyikan iklan fullscreen - Menyembunyikan iklan fullscreen. + "Menyembunyikan iklan fullscreen." Sembunyikan Iklan Umum Menyembunyikan Iklan Umum. Sembunyikan iklan musik diff --git a/src/main/resources/music/translations/it-rIT/strings.xml b/patches/src/main/resources/music/translations/it-rIT/strings.xml similarity index 100% rename from src/main/resources/music/translations/it-rIT/strings.xml rename to patches/src/main/resources/music/translations/it-rIT/strings.xml diff --git a/src/main/resources/music/translations/ja-rJP/strings.xml b/patches/src/main/resources/music/translations/ja-rJP/strings.xml similarity index 93% rename from src/main/resources/music/translations/ja-rJP/strings.xml rename to patches/src/main/resources/music/translations/ja-rJP/strings.xml index 6151d75d9..3f2eb8c25 100644 --- a/src/main/resources/music/translations/ja-rJP/strings.xml +++ b/patches/src/main/resources/music/translations/ja-rJP/strings.xml @@ -2,6 +2,7 @@ ReVanced Extended + デフォルト値にリセット。 再起動してレイアウトを正常に読み込みます 再起動して更新 @@ -48,7 +49,7 @@ 広告 全画面広告を非表示 - 全画面広告を非表示にします。 + "全画面広告を非表示にします。" 一般広告を非表示 一般広告を非表示にします。 音楽の広告を非表示 @@ -244,6 +245,8 @@ 「通知」を非表示 「プライバシーとデータ」を非表示 「おすすめ」を非表示 + 「Music Premium の購入」を非表示 + 「YouTube Music について」を非表示 動画 カスタム再生速度の編集 @@ -270,6 +273,8 @@ 低評価はパーセンテージで表示されます。 コンパクトな高評価ボタン 高評価ボタンの区切りを非表示にします。 + 推定の高評価数を表示 + 動画の推定高評価数を表示します。 API が利用できない場合にメッセージを表示 RYDが利用できない場合、メッセージが表示されます。 Return YouTube Dislike について @@ -279,13 +284,26 @@ 低評価数は一時的に利用できません。(ステータス %d) 低評価数は利用できません (クライアント API 制限) 低評価数は一時的に利用できません。(%s) + 非表示 Return YouTube Username Return YouTube Username を有効化 + コメント中のハンドルネームをユーザー名に置き換えます。 + 表示形式 + ユーザーネームの表示形式を選択します。 ユーザーネーム + ユーザーネーム (@ハンドルネーム) + \@ハンドルネーム(ユーザーネーム) YouTube Data API キー YouTube Data API v3 を使用するための開発者キー。 YouTube Data API キーについて + "ハンドルネームをユーザーネームに置き換えるには、YouTube Data API v3 の開発者キーが必要です。 + +無料プランの API キーの1日の割り当ては 10,000 で、コメント1件につき1つの割り当てが使用されます。 + +API キーの発行方法については、ここをタップしてください。" + YouTube Data API v3 開発者キーの発行 + 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 キーは他人と共有してはならないため、インポート/エクスポート設定には含まれません。 SponsorBlock Sponsor Block を有効化 @@ -368,6 +386,8 @@ 続行 共有リンクのクリーンアップ リンクを共有する際に、URL からトラッキングクエリパラメーターを削除します。 + 「デフォルトで開く」の設定 + RVX Music でYouTube Music のURLを開くには、「対応リンクを開く」を有効にし、サポートされているURLを有効にします。 設定のインポート/エクスポート 設定をテキストとしてインポート/エクスポートします。 設定をファイルにエクスポート diff --git a/src/main/resources/music/translations/ko-rKR/strings.xml b/patches/src/main/resources/music/translations/ko-rKR/strings.xml similarity index 98% rename from src/main/resources/music/translations/ko-rKR/strings.xml rename to patches/src/main/resources/music/translations/ko-rKR/strings.xml index 7b481778b..19c004b3b 100644 --- a/src/main/resources/music/translations/ko-rKR/strings.xml +++ b/patches/src/main/resources/music/translations/ko-rKR/strings.xml @@ -49,7 +49,10 @@ 광고 전체 화면 광고 제거 - 전체 화면 광고를 숨깁니다. + "전체 화면 광고를 숨깁니다. + +알려진 문제점: +• 가끔씩 홈 피드 대신 검정 공백 화면이 표시될 수 있습니다." 일반 레이아웃 광고 제거 일반 레이아웃 광고를 숨깁니다. 음악 광고 제거 @@ -393,6 +396,13 @@ API Key를 발급받는 방법을 보려면 여기를 누르세요." 배터리 최적화 목록에서 제외하려면 '계속하기' 버튼을 누르세요." 계속하기 + 클라이언트 변경하기 + 클라이언트를 변경하여 재생 문제를 방지할 수 있습니다. + +알려진 문제점: +• OPUS 코덱이 지원되지 않을 수 있습니다. +• 재생바 썸네일이 표시되지 않을 수 있습니다. +• 브랜드 계정에서는 시청 기록이 작동되지 않습니다. 추적 쿼리를 제거한 링크 공유 링크를 공유할 때, URL에서 추적 쿼리 매개변수를 제거합니다. 기본 앱 설정 열기 diff --git a/src/main/resources/music/translations/nl-rNL/strings.xml b/patches/src/main/resources/music/translations/nl-rNL/strings.xml similarity index 99% rename from src/main/resources/music/translations/nl-rNL/strings.xml rename to patches/src/main/resources/music/translations/nl-rNL/strings.xml index 3a7d85dbd..422ca3724 100644 --- a/src/main/resources/music/translations/nl-rNL/strings.xml +++ b/patches/src/main/resources/music/translations/nl-rNL/strings.xml @@ -48,7 +48,7 @@ Advertenties Advertenties op volledig scherm verbergen - Verbergt advertenties op volledig scherm. + "Verbergt advertenties op volledig scherm." Algemene advertenties verbergen Verbergt algemene advertenties. Verberg muziek advertenties diff --git a/src/main/resources/music/translations/pl-rPL/strings.xml b/patches/src/main/resources/music/translations/pl-rPL/strings.xml similarity index 98% rename from src/main/resources/music/translations/pl-rPL/strings.xml rename to patches/src/main/resources/music/translations/pl-rPL/strings.xml index d843e742a..30ed1ac2f 100644 --- a/src/main/resources/music/translations/pl-rPL/strings.xml +++ b/patches/src/main/resources/music/translations/pl-rPL/strings.xml @@ -49,7 +49,10 @@ Pobierz %2$s ze strony." Reklamy Ukryj reklamy pełnoekranowe - Ukrywa reklamy pełnoekranowe + "Ukrywa reklamy pełnoekranowe. + +Ograniczenie: +• Czasem może pojawić się pusty, czarny ekran zamiast strony głównej" Ukryj ogólne reklamy Ukrywa ogólne reklamy. Ukryj reklamy multimedialne @@ -392,6 +395,13 @@ Jest to wymagane do działania aplikacji." Kontynuuj i wyłącz optymalizację baterii." Kontynuuj + Oszukuj klienta + Oszukuj klienta, by zapobiec problemom z odtwarzaniem + +Ograniczenia: +• Kodek audio OPUS może nie być wspierany +• Miniaturki podczas przewijania mogą nie być dostępne +• Historia oglądania nie działa na kontach firmowych Oczyść udostępniane linki Usuwa parametry śledzących zapytań z adresów URL podczas udostępniania linków. Otwórz systemowe ustawienia aplikacji diff --git a/src/main/resources/music/translations/pt-rBR/strings.xml b/patches/src/main/resources/music/translations/pt-rBR/strings.xml similarity index 99% rename from src/main/resources/music/translations/pt-rBR/strings.xml rename to patches/src/main/resources/music/translations/pt-rBR/strings.xml index 0f2e9105c..dd2e96937 100644 --- a/src/main/resources/music/translations/pt-rBR/strings.xml +++ b/patches/src/main/resources/music/translations/pt-rBR/strings.xml @@ -49,7 +49,7 @@ Por favor, baixe %2$s do site." Anúncios Ocultar anúncios em tela cheia - Oculta anúncios em tela cheia. + "Oculta anúncios em tela cheia." Ocultar anúncios gerais Oculta anúncios gerais. Ocultar anúncios de mídia @@ -392,6 +392,12 @@ Isto é necessário para o aplicativo funcionar." Toque no botão continuar e desative as otimizações da bateria." Continuar + Falsificar cliente + Falsificar o cliente para evitar problemas de reprodução. + +Limitações: +• Miniatura na barra de busca pode não estar presente. +• Histórico de exibição não funciona com uma conta de marca. Limpar links compartilhados Remove os parâmetros de consulta de rastreamento das URLs ao compartilhar os links. Abrir configurações padrão do aplicativo diff --git a/src/main/resources/music/translations/ro-rRO/strings.xml b/patches/src/main/resources/music/translations/ro-rRO/strings.xml similarity index 100% rename from src/main/resources/music/translations/ro-rRO/strings.xml rename to patches/src/main/resources/music/translations/ro-rRO/strings.xml diff --git a/src/main/resources/music/translations/ru-rRU/strings.xml b/patches/src/main/resources/music/translations/ru-rRU/strings.xml similarity index 99% rename from src/main/resources/music/translations/ru-rRU/strings.xml rename to patches/src/main/resources/music/translations/ru-rRU/strings.xml index f03db0782..19b012c48 100644 --- a/src/main/resources/music/translations/ru-rRU/strings.xml +++ b/patches/src/main/resources/music/translations/ru-rRU/strings.xml @@ -49,7 +49,7 @@ Реклама Полноэкранная реклама - Скрывает полноэкранную рекламу. + "Скрывает полноэкранную рекламу." Скрыть рекламу общего формата Скрывает рекламу общего формата. Скрыть музыкальную рекламу diff --git a/src/main/resources/music/translations/tr-rTR/strings.xml b/patches/src/main/resources/music/translations/tr-rTR/strings.xml similarity index 99% rename from src/main/resources/music/translations/tr-rTR/strings.xml rename to patches/src/main/resources/music/translations/tr-rTR/strings.xml index 7184df5c5..e08bb7456 100644 --- a/src/main/resources/music/translations/tr-rTR/strings.xml +++ b/patches/src/main/resources/music/translations/tr-rTR/strings.xml @@ -48,7 +48,7 @@ Lütfen web sitesinden %2$s dosyasını indirin." Reklamlar Tam ekran reklamlarını gizle - Tam ekran reklamlarını gizler. + "Tam ekran reklamlarını gizler." Genel reklamları gizle Genel reklamları gizler. Müzik reklamlarını gizle diff --git a/src/main/resources/music/translations/uk-rUA/strings.xml b/patches/src/main/resources/music/translations/uk-rUA/strings.xml similarity index 99% rename from src/main/resources/music/translations/uk-rUA/strings.xml rename to patches/src/main/resources/music/translations/uk-rUA/strings.xml index 6325aab61..8fc51cf2f 100644 --- a/src/main/resources/music/translations/uk-rUA/strings.xml +++ b/patches/src/main/resources/music/translations/uk-rUA/strings.xml @@ -49,7 +49,7 @@ Реклама Приховати повноекранну рекламу - Приховує повноекранну рекламу. + "Приховує повноекранну рекламу." Приховати загальну рекламу Приховує загальну рекламу. Приховати медіарекламу diff --git a/src/main/resources/music/translations/vi-rVN/strings.xml b/patches/src/main/resources/music/translations/vi-rVN/strings.xml similarity index 87% rename from src/main/resources/music/translations/vi-rVN/strings.xml rename to patches/src/main/resources/music/translations/vi-rVN/strings.xml index 8396cc763..268e082c5 100644 --- a/src/main/resources/music/translations/vi-rVN/strings.xml +++ b/patches/src/main/resources/music/translations/vi-rVN/strings.xml @@ -23,22 +23,22 @@ Ẩn các nút Thích/Không thích Ẩ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 nút Bình luận - Ẩn nút Bình luận trong bảng nút thao tác. + Ẩn nút Bình luận trong thanh thao tác. Ẩn nút Lưu - Ẩn nút Lưu trong bảng nút thao tác. + Ẩn nút Lưu trong thanh thao tác. Ẩn nút Tải xuống - Ẩn nút Tải xuống trong bảng nút thao tác. + Ẩn nút Tải xuống trong thanh thao tác. Ẩn nút Chia sẻ - Ẩn nút Chia sẻ trong bảng nút thao tác. + Ẩn nút Chia sẻ trong thanh thao tác. Ẩn nút Đài phát - Ẩn nút Đài phát trong bảng nút thao tác. + Ẩn nút Đài phát trong thanh thao tác. Ẩn tên nút - Ẩn tên nút trong bảng nút thao tác. + Ẩn tên nút trong thanh thao tác. Ghi đè nút tải xuống "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." +• 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." Tên gói ứng dụng trình tải xuống 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. Trình tải xuống bên ngoài @@ -49,7 +49,7 @@ Quảng cáo Ẩn quảng cáo toàn màn hình - Ẩn quảng cáo toàn màn hình. + "Ẩn quảng cáo toàn màn hình." Ẩn quảng cáo chung Ẩn quảng cáo xuất hiện trước khi phát. Ẩn quảng cáo @@ -107,9 +107,9 @@ Hạn chế: Tiếp tục phát Tiếp tục phát video từ thời điểm đã dừng lại khi chuyển sang YouTube. Xem trên YouTube - URL video không hợp lệ. + URL của video không hợp lệ. Thay thế mục Loại bỏ danh sách chờ - 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ờ\" bằng mục \"Xem trên YouTube\". Thay thế mục Báo vi phạm Thay thế mục Báo vi phạm bằng mục Tốc độ phát. Báo vi phạm trong phần bình luận @@ -126,7 +126,7 @@ Hạn chế: Tắt tự động hiển thị phụ đề Tắt tự động hiển thị phụ đề khi phát video nhạc có phụ đề. Tắt chuyển hướng khi nhấn nút Không thích - Ngăn chuyển đến bài hát tiếp theo khi nhấn nút Không thích. + Không chuyển đến bài hát tiếp theo khi nhấn vào nút Không thích. Tự động xoay màn hình Cho phép ứng dụng tự động xoay theo hướng màn hình mà thiết bị được giữ. Bộ lọc tuỳ chỉnh @@ -134,7 +134,7 @@ Hạn chế: Chỉnh sửa bộ lọc Nhập tên các mục mà bạn muốn lọc được phân cách bằng dòng. - Bộ lọc tuỳ chỉnh không hợp lệ: %s. + Tên mục đã nhập không hợp lệ: %s. Ẩn khối danh mục Ẩn khối danh mục ở cuối thẻ Trang chủ và đầu thẻ Khám phá. Ẩn các kệ được cá nhân hoá @@ -251,20 +251,20 @@ Lưu ý:\n- Tuỳ chọn này sẽ thay đổi giao diện ứng dụng, tuy nhi Video Chỉnh sửa tốc độ phát 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ó. - Lưu thay đổi tốc độ phát - Lưu giá trị tốc độ phát được chọn gần đây nhất. - Hiện một thông báo ngắn - Hiện một thông báo ngắn khi thay đổi tốc độ phát mặc định. - Lưu thay đổi chất lượng video - Lưu chất lượng video nhạc đã chọn gần đây nhất. - Hiện một thông báo ngắn - Hiện một thông báo ngắn khi thay đổi chất lượng mặc định của video. + Lưu lựa chọn tốc độ phát + Ghi nhớ lựa chọn tốc độ phát đã chọn gần nhất. + Thông báo ngắn + Hiển thị một thông báo ngắn khi thay đổi tốc độ phát mặc định. + Lưu lựa chọn chất lượng video + Ghi nhớ lựa chọn chất lượng video đã chọn gần nhất. + Thông báo ngắn + Hiển thị một thông báo ngắn khi thay đổi chất lượng video mặc định. 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ệ. Đã lưu tốc độ phát mặc định thành %s. - Đã lưu chất lượng video mặc định trên mạng di động thành %s. + Đã 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 trên mạng Wi-Fi thành %s. + Đã lưu chất lượng video mặc định khi sử dụng Wi-Fi thành %s. Return YouTube Dislike Kích hoạt Return YouTube Dislike @@ -303,15 +303,15 @@ Giới hạn truy cập hàng ngày cho các khoá API trên gói miễn phí l Nhấp vào đây để xem các bước phát hành khóa API." Phát hành mã khoá - 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. + 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. SponsorBlock 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. - 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. + 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. Thay đổi địa chỉ URL của API Đị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ì. Đã đặt lại địa chỉ URL của API SponsorBlock. @@ -319,33 +319,33 @@ Nhấp vào đây để xem các bước phát hành khóa API." Đã thay đổi địa chỉ URL của API SponsorBlock. Cài đặt phân đoạn 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. - 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. + 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. + 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ắ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. - Đ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. - 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. + 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. + Đ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ộ. + 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. Â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. - Đã bỏ qua nhà tài trợ. - Đã bỏ qua đoạn tự quảng cáo. - Đã 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 phần kết thúc. - Đã bỏ qua đoạn xem trước. - Đã bỏ qua đoạn xem trước. - Đã bỏ qua đoạn tóm tắt. - Đã bỏ qua cảnh phụ/nội dung lạc đề - hài hước. - Đã bỏ qua 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. + Đã bỏ qua Nhà tài trợ. + Đã bỏ qua Tự quảng cáo. + Đã 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 Phần Outro. + Đã bỏ qua Đoạn xem trước. + Đã bỏ qua Đoạn xem trước. + Đã bỏ qua Phần tóm tắt. + Đã bỏ qua Cảnh phụ - lạc đề. + Đã bỏ qua Phần không phải nhạc. Đã bỏ qua nhiều phân đoạn. Tự động bỏ qua Vô hiệu hoá @@ -372,14 +372,14 @@ Nhấp vào đây để xem các bước phát hành khóa API." Bật ghi nhật ký gỡ lỗi. Bật nhật ký bộ đệm gỡ lỗi Bao gồm bộ đệm trong nhật ký gỡ lỗi. - Codec OPUS + Kích hoạt Codec OPUS "Á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ũ." - Mở GmsCore - Mở GmsCore để kích hoạt Cloud Messaging để nhận thông báo đẩy và các cài đặt khác. + GmsCore + 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 chưa được cài đặt. Hãy cài đặt nó đi nào. Hành động cần thiết "Hiện GmsCore không có quyền chạy nền. @@ -392,6 +392,12 @@ Hãy làm theo hướng dẫn của 'Don't kill my app!' và tiến hành cài Nhấn vào nút Tiếp tục và tắt tối ưu hóa pin." Tiếp tục + Giả mạo ứng dụng khách + Giả mạo ứng dụng khách để khắc phục sự cố phát. + +Hạn chế: +• Hình thu nhỏ trên thanh tiến trình có thể không hiện hữu. +• Nhật ký xem không hoạt động đối với tài khoản thương hiệu. Liên kết sạch khi chia sẻ Loại bỏ các tham số truy vấn theo dõi khỏi URL khi chia sẻ liên kết. Mở theo mặc định diff --git a/src/main/resources/music/translations/zh-rCN/strings.xml b/patches/src/main/resources/music/translations/zh-rCN/strings.xml similarity index 99% rename from src/main/resources/music/translations/zh-rCN/strings.xml rename to patches/src/main/resources/music/translations/zh-rCN/strings.xml index 5a5c6ebcc..eaf52827f 100644 --- a/src/main/resources/music/translations/zh-rCN/strings.xml +++ b/patches/src/main/resources/music/translations/zh-rCN/strings.xml @@ -48,7 +48,7 @@ 广告 隐藏全屏广告 - 隐藏全屏广告 + "隐藏全屏广告" 隐藏一般广告 隐藏一般广告 音乐广告 diff --git a/src/main/resources/music/translations/zh-rTW/strings.xml b/patches/src/main/resources/music/translations/zh-rTW/strings.xml similarity index 99% rename from src/main/resources/music/translations/zh-rTW/strings.xml rename to patches/src/main/resources/music/translations/zh-rTW/strings.xml index 7efed854a..6ea62fa6d 100644 --- a/src/main/resources/music/translations/zh-rTW/strings.xml +++ b/patches/src/main/resources/music/translations/zh-rTW/strings.xml @@ -48,7 +48,7 @@ 廣告 隱藏全螢幕廣告 - 隱藏全螢幕廣告 + "隱藏全螢幕廣告" 隱藏一般廣告 隱藏一般廣告 隱藏音樂廣告 diff --git a/src/main/resources/music/visual/icons/extension/drawable/revanced_extended_settings_icon.xml b/patches/src/main/resources/music/visual/icons/extension/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/visual/icons/extension/drawable/revanced_extended_settings_icon.xml rename to patches/src/main/resources/music/visual/icons/extension/drawable/revanced_extended_settings_icon.xml diff --git a/src/main/resources/music/visual/icons/gear/drawable/revanced_extended_settings_icon.xml b/patches/src/main/resources/music/visual/icons/gear/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/visual/icons/gear/drawable/revanced_extended_settings_icon.xml rename to patches/src/main/resources/music/visual/icons/gear/drawable/revanced_extended_settings_icon.xml diff --git a/src/main/resources/music/visual/icons/revanced/drawable/revanced_extended_settings_icon.xml b/patches/src/main/resources/music/visual/icons/revanced/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/visual/icons/revanced/drawable/revanced_extended_settings_icon.xml rename to patches/src/main/resources/music/visual/icons/revanced/drawable/revanced_extended_settings_icon.xml diff --git a/src/main/resources/music/visual/icons/revanced_colored/drawable/revanced_extended_settings_icon.xml b/patches/src/main/resources/music/visual/icons/revanced_colored/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/visual/icons/revanced_colored/drawable/revanced_extended_settings_icon.xml rename to patches/src/main/resources/music/visual/icons/revanced_colored/drawable/revanced_extended_settings_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/pref_key_parent_tools_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/pref_key_parent_tools_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/pref_key_parent_tools_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/pref_key_parent_tools_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/revanced_extended_settings_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/revanced_extended_settings_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/revanced_extended_settings_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/revanced_extended_settings_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_account_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_account_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/revanced_preference_screen_account_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_account_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_action_bar_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_action_bar_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/revanced_preference_screen_action_bar_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_action_bar_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_ads_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_ads_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/revanced_preference_screen_ads_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_ads_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_flyout_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_flyout_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/revanced_preference_screen_flyout_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_flyout_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_general_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_general_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/revanced_preference_screen_general_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_general_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_misc_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_misc_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/revanced_preference_screen_misc_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_misc_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_navigation_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_navigation_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/revanced_preference_screen_navigation_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_navigation_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_player_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_player_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/revanced_preference_screen_player_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_player_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_return_youtube_username_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_return_youtube_username_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/revanced_preference_screen_return_youtube_username_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_return_youtube_username_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_ryd_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_ryd_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/revanced_preference_screen_ryd_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_ryd_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_sb_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_sb_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/revanced_preference_screen_sb_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_sb_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_settings_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_settings_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/revanced_preference_screen_settings_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_settings_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_video_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_video_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/revanced_preference_screen_video_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/revanced_preference_screen_video_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/settings_header_about_youtube_music_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/settings_header_about_youtube_music_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/settings_header_about_youtube_music_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/settings_header_about_youtube_music_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/settings_header_data_saving_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/settings_header_data_saving_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/settings_header_data_saving_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/settings_header_data_saving_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/settings_header_downloads_and_storage_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/settings_header_downloads_and_storage_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/settings_header_downloads_and_storage_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/settings_header_downloads_and_storage_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/settings_header_general_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/settings_header_general_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/settings_header_general_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/settings_header_general_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/settings_header_notifications_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/settings_header_notifications_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/settings_header_notifications_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/settings_header_notifications_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/settings_header_paid_memberships_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/settings_header_paid_memberships_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/settings_header_paid_memberships_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/settings_header_paid_memberships_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/settings_header_playback_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/settings_header_playback_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/settings_header_playback_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/settings_header_playback_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/settings_header_privacy_and_location_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/settings_header_privacy_and_location_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/settings_header_privacy_and_location_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/settings_header_privacy_and_location_icon.xml diff --git a/src/main/resources/music/visual/shared/drawable/settings_header_recommendations_icon.xml b/patches/src/main/resources/music/visual/shared/drawable/settings_header_recommendations_icon.xml similarity index 100% rename from src/main/resources/music/visual/shared/drawable/settings_header_recommendations_icon.xml rename to patches/src/main/resources/music/visual/shared/drawable/settings_header_recommendations_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_blue/splash/values-v31/styles.xml b/patches/src/main/resources/youtube/branding/afn_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/afn_blue/splash/values-v31/styles.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/afn_red/splash/values-v31/styles.xml b/patches/src/main/resources/youtube/branding/afn_red/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/afn_red/splash/values-v31/styles.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/splash/values-v31/styles.xml b/patches/src/main/resources/youtube/branding/mmt/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/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_blue/splash/values-v31/styles.xml b/patches/src/main/resources/youtube/branding/revancify_blue/splash/values-v31/styles.xml similarity index 100% rename from src/main/resources/youtube/branding/revancify_blue/splash/values-v31/styles.xml rename to patches/src/main/resources/youtube/branding/revancify_blue/splash/values-v31/styles.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/revancify_red/splash/values-v31/styles.xml b/patches/src/main/resources/youtube/branding/revancify_red/splash/values-v31/styles.xml similarity index 100% rename from src/main/resources/youtube/branding/revancify_red/splash/values-v31/styles.xml rename to patches/src/main/resources/youtube/branding/revancify_red/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/src/main/resources/youtube/branding/youtube/splash/values-v31/styles.xml b/patches/src/main/resources/youtube/branding/youtube/splash/values-v31/styles.xml similarity index 100% rename from src/main/resources/youtube/branding/youtube/splash/values-v31/styles.xml rename to patches/src/main/resources/youtube/branding/youtube/splash/values-v31/styles.xml 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/drawable-night-v31/new_content_dot_background.xml b/patches/src/main/resources/youtube/materialyou/drawable-night-v31/new_content_dot_background.xml similarity index 100% rename from src/main/resources/youtube/materialyou/drawable-night-v31/new_content_dot_background.xml rename to patches/src/main/resources/youtube/materialyou/drawable-night-v31/new_content_dot_background.xml diff --git a/src/main/resources/youtube/materialyou/drawable-v31/new_content_count_background.xml b/patches/src/main/resources/youtube/materialyou/drawable-v31/new_content_count_background.xml similarity index 100% rename from src/main/resources/youtube/materialyou/drawable-v31/new_content_count_background.xml rename to patches/src/main/resources/youtube/materialyou/drawable-v31/new_content_count_background.xml diff --git a/src/main/resources/youtube/materialyou/drawable-v31/new_content_dot_background.xml b/patches/src/main/resources/youtube/materialyou/drawable-v31/new_content_dot_background.xml similarity index 100% rename from src/main/resources/youtube/materialyou/drawable-v31/new_content_dot_background.xml rename to patches/src/main/resources/youtube/materialyou/drawable-v31/new_content_dot_background.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/src/main/resources/youtube/materialyou/layout-v31/new_content_count.xml b/patches/src/main/resources/youtube/materialyou/layout-v31/new_content_count.xml similarity index 100% rename from src/main/resources/youtube/materialyou/layout-v31/new_content_count.xml rename to patches/src/main/resources/youtube/materialyou/layout-v31/new_content_count.xml 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 dce423b9c..f184aac3b 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/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/src/main/resources/youtube/settings/host/values/arrays.xml b/patches/src/main/resources/youtube/settings/host/values/arrays.xml similarity index 86% rename from src/main/resources/youtube/settings/host/values/arrays.xml rename to patches/src/main/resources/youtube/settings/host/values/arrays.xml index 2c26e39ff..9468b28a6 100644 --- a/src/main/resources/youtube/settings/host/values/arrays.xml +++ b/patches/src/main/resources/youtube/settings/host/values/arrays.xml @@ -135,6 +135,34 @@ https://github.com/deniscerri/ytdlnis/releases/latest + + @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_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_1 @string/revanced_miniplayer_type_entry_2 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/src/main/resources/youtube/settings/host/values/strings.xml b/patches/src/main/resources/youtube/settings/host/values/strings.xml similarity index 98% rename from src/main/resources/youtube/settings/host/values/strings.xml rename to patches/src/main/resources/youtube/settings/host/values/strings.xml index 409b17e62..e73453db6 100644 --- a/src/main/resources/youtube/settings/host/values/strings.xml +++ b/patches/src/main/resources/youtube/settings/host/values/strings.xml @@ -1031,9 +1031,26 @@ Tap and hold to reset playback speed to 1.0x. Tap and hold again to reset back t "Tap to open whitelist dialog. Tap and hold to open whitelist setting dialog. - Show time-ordered playlist button - "Tap to generate a playlist of all videos from channel from oldest to newest. -Tap and hold to undo." + Show play all button + "Tap to generate a playlist of all videos from channel. +Tap and hold to undo. + +Info: +• May not work on livestreams." + Generate playlist mode + All contents (Sort by time) + 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 + Members only videos + Members only shorts + Members only livestreams + Unable to generate playlist due to channel id mismatch. Channel whitelist Check or remove the list of channels added to the whitelist. @@ -1819,35 +1836,25 @@ Tap on the continue button and disable battery optimizations." Turning off this setting may cause video playback issues. Default client iOS - Android - Android Creator - Android Embedded Player - Android Testsuite Android TV Android VR - TV HTML5 - Web Spoofing side effects - "• Movies or paid videos may not play. -• Livestreams start from the beginning. -• Videos may end 1 second early. -• Opus audio codec may not be supported." - "• Videos may end 1 second early. -• Opus audio codec may not be supported." + "• Livestreams start from the beginning. +• Videos may end 1 second early." + • Videos may end 1 second early. "• Audio track menu is missing. • Stable volume is not available." "• Audio track menu is missing. • Stable volume is not available." - • Video may not play. - iOS Compatibility mode - Only spoofed as iOS client if it is not a movie, paid video or livestream. - Always spoofed as iOS client. Force iOS AVC (H.264) iOS video codec is AVC (H.264). iOS video codec is AVC (H.264), VP9, or AV1. "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." + Skip iOS livestream playback + iOS client is not used for livestream playback. + iOS client is used for livestream playback. Show in Stats for nerds Client used to fetch streaming data is shown in Stats for nerds. Client used to fetch streaming data is hidden in Stats for nerds. @@ -1892,4 +1899,4 @@ AVC (H.264) has a maximum resolution of 1080p, and video playback will use more Excluded Included Stock - + \ 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/src/main/resources/youtube/settings/xml/revanced_prefs.xml b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml similarity index 88% rename from src/main/resources/youtube/settings/xml/revanced_prefs.xml rename to patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml index c5e4ac964..c5fdf74e5 100644 --- a/src/main/resources/youtube/settings/xml/revanced_prefs.xml +++ b/patches/src/main/resources/youtube/settings/xml/revanced_prefs.xml @@ -27,9 +27,9 @@ - + - + SETTINGS: ALTERNATIVE_THUMBNAILS --> @@ -53,7 +53,7 @@ - + @@ -68,7 +68,7 @@ + + SETTINGS: HOOK_DOWNLOAD_ACTIONS --> @@ -173,7 +173,7 @@ - + SETTINGS: MINIPLAYER_TYPE_MODERN --> + + SETTINGS: SPOOF_APP_VERSION --> @@ -386,7 +386,7 @@ - + @@ -395,7 +395,7 @@ + SETTINGS: KEEP_LANDSCAPE_MODE --> @@ -427,12 +427,13 @@ - - + + + + - - SETTINGS: OVERLAY_BUTTONS --> + SETTINGS: OVERLAY_BUTTONS --> @@ -443,7 +444,7 @@ - + @@ -481,18 +482,18 @@ - + SETTINGS: DESCRIPTION_INTERACTION --> + SETTINGS: SHORTS_TIME_STAMP --> @@ -584,6 +585,7 @@ + SETTINGS: SHORTS_COMPONENTS --> @@ -600,15 +602,15 @@ - - - - - + + + + + - - PREFERENCE_SCREEN: SWIPE_CONTROLS --> + + PREFERENCE_SCREEN: SWIPE_CONTROLS --> @@ -632,7 +634,7 @@ - + @@ -673,9 +675,9 @@ @@ -692,30 +694,30 @@ - - - - - - - - - + + + + + + + + + - + - - + + - + PREFERENCE_SCREEN: SPONSOR_BLOCK --> @@ -726,16 +728,16 @@ - + @@ -745,7 +747,7 @@ - + SETTINGS: WATCH_HISTORY --> - + Лента с инструменти Скрива или променя елементи, разположени в лентата с инструменти, като бутони на лентата с инструменти, лента за търсене, заглавия. @@ -507,6 +560,12 @@ Бутон за \"гласово търсене\" Бутон за гласово търсене на е скрит. Бутон за гласово търсене се показва. + YouTube Doodles + YouTube Doodles скрити. + YouTube Doodles се показват. + "YouTube Doodles се показват няколко дни в годината. + +Ако YouTube Doodles се показват във вашия регион и са скрити, лентата за филтриране под лентата за търсене също ще бъде скрита." Заменете бутона \"Създаване\" Заменете „Създаване“ с бутон за настройки. Тип действие за назначаване на бутона @@ -588,6 +647,9 @@ Интерфейс за мащабиране Интерфейс за мащабиране ескрит. Интерфейс за мащабиране се показва. + Филтриране на фрази в субтитрите + "Фрази като „#“, „Набиране на средства“, „Пазаруване“ и „продукти“ бяха скрити от субтитрите на видеоклипа." + "Фрази като „#“, „Набиране на средства“, „Пазаруване“ и „продукти“ бяха показани от субтитрите на видеоклипа." Бутони за действие Скриване или показване на бутони за действие под видеоклипове. @@ -654,6 +716,9 @@ Скриване на банер за коментари от членове Банера за коментари от членове е скрит. Банера за коментари от членове се показва. + Скриване на подчертаните връзки за търсене + Маркираните линкове за търсене са скрити. + Показани са маркираните линкове за търсене. Скриване на секцията с коментари Секцията с коментари е скрита. Секцията с коментари се показва. @@ -681,6 +746,9 @@ Промяна на типа превключване на настройките Използват се превключващи бутони с текст. Използват се бутони за превключване. + Скриване на 1080p Premium меню + 1080p Premium менюто е скрито. + 1080p Premium менюто се показва. Меню на аудио Менюто “Audio Track” е скрито. Менюто “Audio Track” се показва. @@ -874,8 +942,6 @@ Пок. бутон за \"Бял списък\" Натиснете - Отворете \"Бял списък\". Натиснете и задръжте - Отворете настройките на \"Бял списък\". - Бутон за показване на подредени по време плейлисти - "Докоснете, за да генерирате плейлист с всички видеоклипове в канала от най-старите до най-новите, натиснете продължително, за да отмените." Добавяне на канал към белия списък Проверка или премахване на листа с канали доб. в белия списък. Каналът %1$s е добавен в %2$s белия списък. @@ -920,12 +986,25 @@ Миниатюри на лентата за възпроизвеждане Миниатюрите са скрити. Миниатюрите се показват. + Деактивиране глави за лентата на напредъка + Главите са скрити. + Главите се показват. + Скриване на етикетите на главите в лентата на прогреса + Маркерите за глави в прогреса са скрити. + Маркерите за глави в прогреса се показват. Времево клеймо на видеоклипа Времето клеймо е скрито. Клеймо за време се показва. Стари миниатюри на времевата линия Над лентата за възпроизвеждане се появяват миниатюри. Миниатюрите се показват в режим на цял екран. + Висококачествени миниатюри + Миниатюри с високо качество в лентата на прогреса. + Миниатюри с средно качество в лентата на прогреса. + "Това ще възстанови миниатюри за потоци на живо, които нямат миниатюри в лентата за търсене. + +Използването на интернет данни може да е по-високо и миниатюрите в лентата за търсене ще имат малко забавяне преди показване. +Тази функция работи най-добре при много бърза интернет връзка." Лента за прогрес на тема Кайро "Лентата за прогрес на тема Кайро е активирана. Страничен ефект: @@ -937,6 +1016,9 @@ Анимация на числа в реално време Анимацията на числа в реално време е деактивирана. Анимацията на числа в реално време е активирана. + Скриване на секцията с резюме на видео, генерирано от AI + Резюме на видео, генерирано от AI, е скрито. + Показана се резюме на видеото, генерирано от AI. Раздел с функции Секциите „Популярни места“, „Игри“ и „Музика“ под описанието са скрити. Показват се секциите „Популярни места“, „Игри“ и „Музика“ под описанието. @@ -990,6 +1072,12 @@ "Скрива рафтовете за кратки видеа Известен проблем: Официалните заглавки в резултатите от търсенето са скрити." + Скрит в канала + "Скрито в канала. + +Информация: +• Скрити са само рафтовете със заглавката Shorts в началния раздел." + Показват се в канала. Скриване в „начало“ и „подобни видеоклипове“ Скрити в емисиите „начало“ и „подобни видеоклипове“. Показва се в емисиите „начало“ и „подобни видеоклипове“. @@ -1175,6 +1263,12 @@ Областта за плъзгане не може да бъде по-голяма от 50. Нулиране на стойността по подразбиране. Задръжка на плъзгащата контрола за показване Време за което плъзгащата контрола е видима. + Чувствителност при плъзгане на яркост + Конфигурирайте минималното разстояние за плъзгане на яркостта между 1 и 1000 (%).\nКолкото по-късо е минималното разстояние, толкова по-бързо се променя нивото на яркост. + Чувствителността на плъзгане за яркост трябва да бъде между 1-1000 (%). + Чувствителност при плъзгане на звука + Конфигурирайте минималното разстояние за плъзгане на силата на звука между 1 и 1000 (%).\n\nКолкото по-малко е минималното разстояние, толкова по-бързо се променя нивото на силата на звука.\n\nПрепоръчителната чувствителност на плъзгане на звука е 100% при 15 стъпки и 10% при 150 стъпки. + Чувствителността на силата на звука при плъзгане трябва да бъде между 1-1000 (%). Деактивирайте автоматичната HDR яркост Автоматичната яркост при HDR е изключена. Автоматичната яркост при HDR е включена. @@ -1274,6 +1368,9 @@ Компактен бутон за харесване Включен компактен бутон \"Харесва ми\". Компактният бутон „Харесва ми“ е деактивиран. + Съотношение на харесвания + Показано. + Скрито. Показване на известие, ако API не е наличен Показва известие, ако Return YouTube Dislike не е наличен. Не се показва известие, ако ReturnYouTube Dislike не е наличен. @@ -1286,7 +1383,26 @@ Нехаресванията не са достъпни (достигнат лимит на API). Нехаресванията не са налични (%s). Презареждане на видеото за гласуване чрез ReturnYouTubeDislike + Скрито + Връщане на потребителско име в YouTube + Активиране на връщане на потребителско име в YouTube + Потребителското име се използва. + Идентификаторът се използва. + Формат на дисплея + Потребителско име + Потребителско име (@handle) + \@handle (Потребителско име) + Ключ за API за данни на YouTube + Ключът на програмиста за използване на API за данни на YouTube v3. + Относно ключа за API за данни на YouTube + "За замяна на манипулатори с потребителски имена е необходим ключ за разработчици на YouTube Data API v3. + +Дневната квота за API ключове в безплатния план е 10 000 и 1 квота се използва за замяна на манипулатор с потребителско име за 1 коментар. + +Кликнете, за да видите как да издадете API ключ." + Издаване на ключ за разработчици на YouTube Data API v3 + 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 ключът никога не трябва да се споделя с други, така че не е включен в настройките за импортиране/експортиране. SponsorBlock Включване на SponsorBlock @@ -1541,19 +1657,12 @@ Изключването на тази настройка може да причини проблеми с възпроизвеждането на видео. Клиент по подразбиране iOS - Андроид - Създател на Android - Вграден Android плейър - Тестов пакет за Android Android TV Android VR - TV HTML5 - Web Ефекти от замяната "• Филми или платени видеоклипове може да не се възпроизвеждат." "• Липсва менюто за избор на аудио." "• Липсва менюто за избор на аудио." - • Видеото може да не се възпроизведе. Принудително AVC (H.264) за iOS видео кодекът на iOS е AVC (H.264). видео кодекът на iOS е AVC (H.264), VP9, or AV1. diff --git a/src/main/resources/youtube/translations/de-rDE/strings.xml b/patches/src/main/resources/youtube/translations/de-rDE/strings.xml similarity index 95% rename from src/main/resources/youtube/translations/de-rDE/strings.xml rename to patches/src/main/resources/youtube/translations/de-rDE/strings.xml index cf102dfed..91e395ff2 100644 --- a/src/main/resources/youtube/translations/de-rDE/strings.xml +++ b/patches/src/main/resources/youtube/translations/de-rDE/strings.xml @@ -6,6 +6,7 @@ ReVanced Extended Suche %s + Auf Standardwerte zurücksetzen. Experimentelle Flags Möchtest Du fortfahren? Neustarten um das Layout zu laden @@ -27,9 +28,15 @@ Bitte lade %2$s von der Webseite herunter." Merchandise-Abschnitt verstecken Merchandise-Abschnitte sind versteckt. Merchandise-Abschnitte werden angezeigt. + Player Shopping-Banner ausblenden + Shopping-Banner ist ausgeblendet. + Shopping-Banner wird angezeigt. Verstecke Label für bezahlte Promotion Label für bezahlte Promotion wird versteckt. Label für bezahlte Promotion wird angezeigt. + Werbe-Warnbanner ausblenden + Werbe-Warnbanner ist ausgeblendet. + Werbe-Warnbanner wird angezeigt. Verstecke selbstgesponserte Karten Selbstgesponserte Karten sind versteckt. Selbstgesponserte Karten werden angezeigt. @@ -54,6 +61,8 @@ Bitte lade %2$s von der Webseite herunter." Suchresultate Originale Vorschaubilder DeArrow & originale Vorschaubilder + DeArrow & erfasst immer noch + Erfasst immer noch Über DeArrow "DeArrow stellt Crowdsourcing-Thumbnails für YouTube-Videos bereit. Diese Thumbnails sind oft relevanter als die von YouTube bereitgestellten. @@ -65,6 +74,7 @@ Tippen Sie hier, um mehr über DeArrow zu erfahren." Keine Benachrichtigung wird angezeigt, wenn DeArrow nicht erreichbar ist. DeArrow API Endpunkt Die URL des Endpunkts der DeArrow-Thumbnail-Cache. Ändern Sie dies nicht, wenn Sie nicht wissen, was Sie machen. + Ungültige DeArrow API URL. Über Videoaufnahmen Aufnahmen werden von Anfang / Mitte / Ende jedes Videos aufgenommen. Diese Bilder sind in YouTube eingebaut und es wird keine externe API verwendet. Benutze schnelle Still-Aufnahmen @@ -108,6 +118,9 @@ Tippen Sie hier, um mehr über DeArrow zu erfahren." Feed-Umfragen verstecken Feed-Umfragen sind versteckt Feed-Umfragen werden angezeigt + Schwebende Taste ausblenden + Schwebende Taste ist ausgeblendet. + Schwebende Taste wird angezeigt. Verstecke Bildregale Bildregale sind versteckt Bildregale werden angezeigt @@ -220,7 +233,12 @@ 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." + Ganze Wörter zuordnen + Durch das Umschließen eines Keywords/Satzes mit doppelten Anführungszeichen wird verhindert, dass Teile von Videotitel und Kanalnamen betroffen sind.<br><br>Zum Beispiel,<br><b>\"ai\"</b> wird folgendes Video ausblenden: <b>Wie funktioniert AI?</b><br>aber es versteckt nicht: <b>Was bedeutet fair use?</b> Ungültiges Schlüsselwort. Kann \'%s\' nicht als Filter verwenden + Anführungszeichen hinzufügen, um Keyword zu verwenden: %s. + Keyword hat widersprüchliche Deklarationen: %s. + Keyword ist zu kurz und erfordert Anführungszeichen: %s. Schlüsselwort \'%1$s\' wird alle Videos ausblenden. Empfohlene Videos @@ -233,6 +251,15 @@ Einschränkungen: Verstecke Videos mit weniger als 1000 Aufrufen von nicht abonnierten Kanälen von der Startseite. Zählerfilter anzeigen + Videos auf der Startseite nach Aufrufen ausblenden + Videos auf der Startseite werden gefiltert. + Videos auf der Startseite werden nicht gefiltert. + Suchergebnisse nach Aufrufen ausblenden + Suchergebnisse werden gefiltert. + Suchergebnisse werden nicht gefiltert. + Abovideos nach Aufrufen ausblenden + Videos im Abofeed werden gefiltert. + Videos im Abofeed werden nicht gefiltert. Größer als Ansichten Videos mit Ansichten, die größer als diese Zahl sind, werden ausgeblendet. Weniger als Anrufe @@ -240,6 +267,18 @@ Einschränkungen: Schlüssel anzeigen 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 K -> 1 000\nM -> 1 000 000\nB -> 1 000 000 000\nviews -> Aufrufe + Über Aufrufzahl-Filterung + "Home / Abonnement / Suchergebnisse werden gefiltert, um Videos mit Aufrufen zu verstecken, die kleiner oder größer als eine angegebene Zahl sind. + +Beschränkungen: +• Shorts können nicht ausgeblendet werden. +• Videos mit 0 Ansichten werden nicht gefiltert." + Ähnliche Videos ausblenden + Ähnliche Videos sind ausgeblendet. + Ähnliche Videos werden angezeigt. + "Diese Einstellung begrenzt die maximale Anzahl von Layouts, die auf dem Wiedergabebildschirm geladen werden können. + +Wenn sich das Layout des Wiedergabebildschirms aufgrund serverseitiger Änderungen ändert, können unbeabsichtigte Layouts auf dem Wiedergabebildschirm ausgeblendet werden." Allgemein Startseite ändern @@ -259,6 +298,11 @@ Einschränkungen: Abonnements Beliebt Später ansehen + Startseitentyp ändern + "Startseite ändert sich immer. + +Einschränkung: Zurück-Taste in der Symbolleiste funktioniert möglicherweise nicht." + Startseite ändert sich nur einmal. Erzwungene automatische Audiospuren sind deaktivieren Erzwungene automatische Audiospuren sind deaktiviert. Erzwungene automatische Audiospuren sind aktiviert. @@ -283,6 +327,12 @@ Einschränkungen: Diskretion des Betrachters entfernen "Entfernt den Diskretionsdialog des Betrachters. Dies umgeht nicht die Altersbeschränkung. Es akzeptiert ihn nur automatisch." + Layout ändern + Original + Telefon + Telefon (max. 480 dp) + Tablet + Tablet (min. 600 dp) Spoof App Version Version gefälscht Version nicht gefälscht @@ -322,6 +372,8 @@ Manche Komponenten könnten nicht versteckt werden." Komponenten nach zeilengetrennten Namen filtern %s ist ein ungültiger benutzerdefinierter Filter. + Hook Buttons + Überschreibt die Klick-Aktion von In-App-Tasten. @@ -387,6 +439,9 @@ Manche Komponenten könnten nicht versteckt werden." Einstellungsmenü Elemente im YouTube-Einstellungsmenü verstecken + \"Im TV anschauen\"-Menü wird angezeigt. + + Privatsphäre-Menü wird angezeigt. Werkzeugleiste Verstecke oder ändere Toolbar-Komponenten, wie die Suchleiste, Buttons und Header. @@ -726,9 +781,6 @@ Tippen und halten um die Wiedergabegeschwindigkeit auf 1.0x zurückzusetzen. Hal Zeige Whitelist-Button \"Tippen, um den Whitelist-Dialog zu öffnen. Tippen und halten Sie, um den Einstellungsdialog für die Whitelist anzuzeigen. - Zeitgeordnete Wiedergabelistenschaltfläche anzeigen - "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." Kanal Whitelist Überprüfen oder die Liste der Kanäle entfernen, die zur Whitelist hinzugefügt wurden. Der Kanal \'%1$s\' wurde auf die Whitelist für %2$s gesetzt. diff --git a/src/main/resources/youtube/translations/el-rGR/strings.xml b/patches/src/main/resources/youtube/translations/el-rGR/strings.xml similarity index 98% rename from src/main/resources/youtube/translations/el-rGR/strings.xml rename to patches/src/main/resources/youtube/translations/el-rGR/strings.xml index a09c4ee6c..a5c400503 100644 --- a/src/main/resources/youtube/translations/el-rGR/strings.xml +++ b/patches/src/main/resources/youtube/translations/el-rGR/strings.xml @@ -547,7 +547,7 @@ Playlists Ευρεία γραμμή αναζήτησης Η ευρεία γραμμή αναζήτησης είναι ενεργοποιημένη. Η ευρεία γραμμή αναζήτησης είναι απενεργοποιημένη. - Συμπερίληψη της επικεφαλίδας + Ευρεία γραμμή αναζήτησης με επικεφαλίδα Η ευρεία γραμμή αναζήτησης περιλαμβάνει την επικεφαλίδα του YouTube. Η ευρεία γραμμή αναζήτησης δεν περιλαμβάνει την επικεφαλίδα του YouTube. Ευρεία γραμμή αναζήτησης στο «Εσείς» @@ -967,10 +967,27 @@ Playlists Κουμπί λίστας επιτρεπόμενων. Πατήστε για να ανοίξετε το παράθυρο λίστας επιτρεπόμενων. Πατήστε παρατεταμένα για να ανοίξετε το παράθυρο ρυθμίσεων λίστας επιτρεπόμενων. - Χρονικά-διατεταγμένη λίστα αναπαραγωγής - "Κουμπί δημιουργίας χρονικά-διατεταγμένης λίστας αναπαραγωγής. -Πατήστε για να δημιουργήσετε μια λίστα αναπαραγωγής όλων των βίντεο του καναλιού από το παλαιότερο στο νεότερο. -Πατήστε παρατεταμένα για αναίρεση." + Αναπαραγωγή όλων + "Κουμπί αναπαραγωγής όλων των βίντεο του καναλιού. +Πατήστε για να δημιουργήσετε μια λίστα αναπαραγωγής με όλα τα βίντεο από το κανάλι. +Πατήστε παρατεταμένα για αναίρεση. + +Πληροφορίες: +• Ενδέχεται να μη λειτουργεί σε ζωντανές μεταδόσεις." + Ταξινόμηση λίστας αναπαραγωγής + Όλο το περιεχόμενο (Ταξινόμηση κατά χρονο) + Όλο το περιεχόμενο (Ταξινόμηση κατά δημοφιλία) + Βίντεο μόνο (Ταξινόμηση κατά χρόνο) + Βίντεο μόνο (Ταξινόμηση κατά δημοφιλία) + Shorts μόνο (Ταξινόμηση κατά χρόνο) + Shorts μόνο (Ταξινόμηση κατά δημοφιλία) + Βίντεο ζωντανής μετάδοσης μόνο (Ταξινόμηση κατά χρόνο) + Βίντεο ζωντανής μετάδοσης μόνο (Ταξινόμηση κατά δημοφιλία) + Περιεχόμενο μόνο για μέλη + Βίντεο μόνο για μέλη + Shorts μόνο για μέλη + Βίντεο ζωντανής μετάδοσης μόνο για μέλη + Δεν είναι δυνατή η δημιουργία λίστας αναπαραγωγής λόγω αναντιστοιχίας id καναλιού. Λίστα επιτρεπόμενων καναλιών Ελέγξτε ή καταργήστε την λίστα των καναλιών που έχουν προστεθεί στη λίστα επιτρεπόμενων. Το κανάλι \'%1$s\' προστέθηκε στη λίστα επιτρεπόμενων %2$s. @@ -1709,35 +1726,26 @@ Playlists Η απενεργοποίηση αυτής της ρύθμισης ενδέχεται να προκαλέσει προβλήματα αναπαραγωγής βίντεο. Προεπιλογή iOS - Android - Android Creator - Ενσωματωμένο πρόγραμμα αναπαραγωγής Android - Android Testsuite Android TV Android VR - TV HTML5 - Ιστός (Web) Παρενέργειες παραποίησης "• Οι ταινίες ή τα επί πληρωμή βίντεο ενδέχεται να μην αναπαράγονται. • Οι ζωντανές μεταδόσεις ξεκινούν από την αρχή κατά την αναπαραγωγή. -• Τα βίντεο μπορεί να τελειώνουν 1 δευτερόλεπτο νωρίτερα. -• Ο κωδικοποιητής ήχου opus ενδέχεται να μην υποστηρίζεται." - "• Τα βίντεο μπορεί να τελειώνουν 1 δευτερόλεπτο νωρίτερα. -• Ο κωδικοποιητής ήχου Opus ενδέχεται να μην υποστηρίζεται." +• Τα βίντεο μπορεί να τελειώνουν 1 δευτερόλεπτο νωρίτερα." + • Τα βίντεο μπορεί να τελειώνουν 1 δευτερόλεπτο νωρίτερα. "• Το μενού «Κομμάτι ήχου» λείπει. • Η λειτουργία «Σταθερή ένταση» δεν είναι διαθέσιμη." "• Το μενού «Κομμάτι ήχου» λείπει. • Η λειτουργία «Σταθερή ένταση» δεν είναι διαθέσιμη." - • Τα βίντεο ενδέχεται να μην αναπαράγονται. - Λειτουργία συμβατότητας iOS - Το πρόγραμμα πελάτη παραποιείται ως iOS μόνο όταν αν το βίντεο δεν πρόκειται για ταινία, βίντεο επί πληρωμή ή ζωντανή μετάδοση. - Το πρόγραμμα πελάτη παραποιείται πάντα ως iOS. Εξαναγκασμός iOS AVC (H.264) Ο κωδικοποιητής βίντεο iOS είναι ο AVC (H.264). Ο κωδικοποιητής βίντεο iOS είναι ο AVC (H.264), ο VP9 ή ο AV1. "Ενεργοποιώντας αυτόν τον κωδικοποιητή ίσως βελτιωθεί η κατανάλωση ενέργειας και ίσως διορθωθούν μικροκολλήματα αναπαραγωγής. Ο AVC (H.264) ωστόσο έχει μέγιστη ανάλυση 1080p, και η αναπαραγωγή βίντεο καταναλώνει περισσότερα δεδομένα internet από τον VP9 ή τον AV1." + Παράλειψη αναπαραγωγής ζωντανών μεταδόσεων με iOS + Το πρόγραμμα πελάτη iOS δεν χρησιμοποιείται για την αναπαραγωγή ζωντανών μεταδόσεων. + Το πρόγραμμα πελάτη iOS χρησιμοποιείται για την αναπαραγωγή ζωντανών μεταδόσεων. Εμφάνιση στο «Στατιστικά για σπασίκλες» Το πρόγραμμα πελάτη που χρησιμοποιείται για τη λήψη δεδομένων ροής εμφανίζεται στο μενού «Στατιστικά για σπασίκλες». Το πρόγραμμα πελάτη που χρησιμοποιείται για τη λήψη δεδομένων ροής δεν εμφανίζεται στο μενού «Στατιστικά για σπασίκλες». diff --git a/src/main/resources/youtube/translations/es-rES/strings.xml b/patches/src/main/resources/youtube/translations/es-rES/strings.xml similarity index 99% rename from src/main/resources/youtube/translations/es-rES/strings.xml rename to patches/src/main/resources/youtube/translations/es-rES/strings.xml index 7b763cfa1..c228d63a1 100644 --- a/src/main/resources/youtube/translations/es-rES/strings.xml +++ b/patches/src/main/resources/youtube/translations/es-rES/strings.xml @@ -957,9 +957,12 @@ Mantén pulsado para establecer la velocidad de reproducción en 1.0x." Mostrar botón de lista blanca \"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 de reproducción ordenada - "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 reproducir todo + "Toca para generar una lista de reproducción de todos los vídeos del canal. +Toca y mantén para deshacer. + +Información: +• Puede que no funcione en transmisiones." Lista blanca de canales Verifique o elimine la lista de canales agregados a la lista blanca. El canal %1$s se agregó a la lista blanca %2$s. @@ -1680,21 +1683,14 @@ Pulsa el botón de continuar y desactiva las optimizaciones de la batería."Desactivar este ajuste puede causar problemas de reproducción de vídeo. Cliente predeterminado iOS - Android - Creador de Android - Reproductor integrado de Android - Android Testsuite Android TV Android VR - TV HTML5 - Web Efectos secundarios de falsificación "• Las películas o vídeos de pago no pueden reproducirse." "• Falta el menú \"Pista de audio\". • \"Regular volumen\" no está disponible." "• Falta el menú \"Pista de audio\". • \"Regular volumen\" no está disponible." - • El vídeo no puede reproducirse. Forzar iOS AVC (H.264) El códec de vídeo de iOS es AVC (H.264). El códec de vídeo de iOS es AVC (H.264), VP9 o AV1. diff --git a/src/main/resources/youtube/translations/fr-rFR/strings.xml b/patches/src/main/resources/youtube/translations/fr-rFR/strings.xml similarity index 93% rename from src/main/resources/youtube/translations/fr-rFR/strings.xml rename to patches/src/main/resources/youtube/translations/fr-rFR/strings.xml index 0a8544053..be7caa473 100644 --- a/src/main/resources/youtube/translations/fr-rFR/strings.xml +++ b/patches/src/main/resources/youtube/translations/fr-rFR/strings.xml @@ -27,15 +27,15 @@ Veuillez télécharger %2$s à partir du site web." Masquer les publicités générales Les publicités générales sont masquées. Les publicités générales sont affichées. - Masquer l\'étagère \"Effectuer des achats...\" - L\'étagère \"Effectuer des achats dans le magasin ...\" est masquée. - L\'étagère \"Effectuer des achats dans le magasin ...\" est affichée. + Masquer l\'étagère \'Effectuer des achats...\' + L\'étagère \'Effectuer des achats dans le magasin ...\' est masquée. + L\'étagère \'Effectuer des achats dans le magasin ...\'est affichée. Masquer l\'étagère des produits sur le lecteur L\'étagère des produits est masqué. L\'étagère des produits est affiché. - Masquer la bannière \"Communication commerciale\" - La bannière \"Inclut une communication commerciale\" est masqué. - La bannière \"Inclut une communication commerciale\" est affiché. + Masquer la bannière \'Communication commerciale\' + La bannière \'Inclut une communication commerciale\' est masqué. + La bannière \'Inclut une communication commerciale\' est affiché. Masquer la bannière d\'alerte de promotion La bannière d\'alerte de promotion est masquée. La bannière d\'alerte de promotion est affichée. @@ -45,9 +45,9 @@ Veuillez télécharger %2$s à partir du site web." Masquer les publicités vidéo Les publicités vidéos sont masquées. Les publicités vidéos sont affichées. - Masquer la bannière \"Afficher les produits\" - La bannière \"Afficher les produits\" est masquée. - La bannière \"Afficher les produits\" est affichée. + Masquer la bannière \'Afficher les produits\' + La bannière \'Afficher les produits\' est masquée. + La bannière \'Afficher les produits\' est affichée. Masquer les résultats web Les résultats web sont masqués. Les résultats web sont affichés. @@ -56,9 +56,9 @@ Veuillez télécharger %2$s à partir du site web." Publicités pour YouTube Premium affichées. Miniatures alternatives - Onglet \"Accueil\" - Onglet \"Abonnements\" - Onglet \"Vous\" + Onglet \'Accueil\' + Onglet \'Abonnements\' + Onglet \'Vous\' Listes de lecture, recommandations Résultats de recherche Miniatures originales @@ -107,18 +107,18 @@ Cliquez ici pour en savoir plus sur DeArrow." • Produits • Regarder à nouveau" Masquer des étagères - L\'étagère \"Vous pourriez aussi aimer\" est masquée. - L\'étagère \"Vous pourriez aussi aimer\" est affichée. + L\'étagère \'Vous pourriez aussi aimer\' est masquée. + L\'étagère \'Vous pourriez aussi aimer\' est affichée. Masquer les menus déroulants sous les vidéos Les menus déroulants sont masqués. Les menus déroulants sont affichés. Masquer les étagères coulissantes Les étagères coulissantes sont masqués. Les étagères coulissantes sont affichés. - Masquer le bouton \"Sous-titres\" - Le bouton \"Sous-titres\" est masqué. - Le bouton \"Sous-titres\" est affiché. - Masquer barre de recherche + Masquer le bouton \'Sous-titres\' + Le bouton \'Sous-titres\' est masqué. + Le bouton \'Sous-titres\' est affiché. + Masquer la barre de recherche La barre de recherche est masqué. La barre de recherche est affiché. Masquer les sondages @@ -133,27 +133,27 @@ Cliquez ici pour en savoir plus sur DeArrow." Masquer les posts récents Les posts récents sont masqués. Les posts récents sont affichés. - Masquer le bouton \"Vidéos récentes\" - Le bouton \"Vidéos récentes\" est masqué. - Le bouton \"Vidéos récentes\" est affiché. + Masquer le bouton \'Vidéos récentes\' + Le bouton \'Vidéos récentes\' est masqué. + Le bouton \'Vidéos récentes\' est affiché. Masquer les playlists mix Les playlists mix sont masqués. Les playlists mix sont affichés. - Masquer \"Vos films et séries\" - Les étagères \"Vos films et séries\" sont masqués. - Les étagères \"Vos films et séries\" sont affichés. - Masquer \"Recevoir une Notification\" - Le bouton \"Recevoir une Notification\" est masqué. - Le bouton \"Recevoir une Notification\" est affiché. - Masquer \"Jeux intégrés\" - Les \"Jeux intégrés\" sont masqués. - Les \"Jeux intégrés\" sont affichés. - Masquer le bouton \"Voir plus\" - Le bouton \"Voir plus\" est masqué. - Le bouton \"Voir plus\" est affiché. - Masquer la barre \"Abonnements\" - La barre \"Abonnements\" est masqué. - La barre \"Abonnements\" est affiché. + Masquer \'Vos films et séries\' + Les étagères \'Vos films et séries\' sont masqués. + Les étagères \'Vos films et séries\' sont affichés. + Masquer \'Recevoir une Notification\' + Le bouton \'Recevoir une Notification\' est masqué. + Le bouton \'Recevoir une Notification\' est affiché. + Masquer \'Jeux intégrés\' + Les \'Jeux intégrés\' sont masqués. + Les \'Jeux intégrés\' sont affichés. + Masquer le bouton \'Voir plus\' + Le bouton \'Voir plus\' est masqué. + Le bouton \'Voir plus\' est affiché. + Masquer la barre \'Abonnements\' + La barre \'Abonnements\' est masqué. + La barre \'Abonnements\' est affiché. Masquer les étagères à tickets Les étagères à tickets sont masqués. Les étagères à tickets sont affichés. @@ -180,30 +180,30 @@ Cliquez ici pour en savoir plus sur DeArrow." "Shorts Playlists Boutique" - Masquer le bouton \"Visiter la boutique\" - Le bouton \"Visiter la boutique\" est masqué. - Le bouton \"Visiter la boutique\" est affiché. + Masquer le bouton \'Visiter la boutique\' + Le bouton \'Visiter la boutique\' est masqué. + Le bouton \'Visiter la boutique\' est affiché. Masquer les membres de la chaîne Les membres de la chaîne sont masqués. Les membres de la chaîne sont affichés. Masquer les liens de la chaîne Les liens en haut de la chaîne sont masqués. Les liens en haut de la chaîne sont affichés. - Masquer la catégorie \"Pour vous\" - La catégorie \"Pour vous\" est masquée. - La catégorie \"Pour vous\" est affichée. + Masquer la catégorie \'Pour vous\' + La catégorie \'Pour vous\' est masquée. + La catégorie \'Pour vous\' est affichée. Posts communautaires Masque ou affiche les posts communautaires dans les flux et sur les chaînes. Masquer sur les chaînes Masqué sur les chaînes. Affiché sur les chaînes. - Masquer dans les flux \"accueil\" et \"vidéos similaires\" - Masqué dans les flux \"accueil\" et \"vidéos similaires\". - Affiché dans les flux \"accueil\" et \"vidéos similaires\". - Masquer dans le flux \"Abonnements\" - Masqué dans le flux \"Abonnements\". - Affiché dans le flux \"Abonnements\". + Masquer dans les flux \'Accueil\' et \'Vidéos similaires\' + Masqué dans les flux \'Accueil\' et \'Vidéos similaires\'. + Affiché dans les flux \'Accueil\' et \'Vidéos similaires\'. + Masquer dans le flux \'Abonnements\' + Masqué dans le flux \'Abonnements\'. + Affiché dans le flux \'Abonnements\'. Menu déroulant Masque ou Affiche des options du menu déroulant dans les flux. @@ -218,14 +218,14 @@ Boutique" Filtre par mots-clés Filtrer la page d\'accueil par mot-clés - Les vidéos dans le flux \"accueil\" sont filtrées. - Les vidéos dans le flux \"accueil\" ne sont pas filtrées. + Les vidéos dans le flux \'Accueil\' sont filtrées. + Les vidéos dans le flux \'Accueil\' ne sont pas filtrées. Filtrer les recherches par mots-clés Les résultats de recherche sont filtrés. Les résultats de recherche ne sont pas filtrés. - Filtrer \"Abonnements\" par mots-clés - Les vidéos dans le flux \"Abonnements\" sont filtrées. - Les vidéos dans le flux \"Abonnements\" ne sont pas filtrées. + Filtrer \'Abonnements\' par mots-clés + Les vidéos dans le flux \'Abonnements\' sont filtrées. + Les vidéos dans le flux \'Abonnements\' ne sont pas filtrées. Filtrer les commentaires par mot-clés Les commentaires sont filtrés. Les commentaires ne sont pas filtrés. @@ -236,7 +236,7 @@ Les mots-clés peuvent être des noms de chaînes ou tout texte figurant dans le Les mots comportant des majuscules au milieu doivent être saisis de la même façon (par exemple : iPhone, TikTok, TheoBabac)." À propos du filtrage par mots-clés - "Les onglets \"Pages d'accueil\" / \"Abonnements\" / Résultats de recherche sont filtrés pour masquer le contenu correspondant aux mots 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. @@ -325,17 +325,17 @@ Limitation : Le bouton Retour de la barre d'outils peut ne pas fonctionner."Activer dégradé pendant le chargement Le dégradé pendant l\'écran de chargement est activé. Le dégradé pendant l\'écran de chargement est désactivé. - Masquer le bouton \"Micro\" - Le bouton \"Micro\" est masqué. - Le bouton \"Micro\" est affiché. + Masquer le bouton \'Micro\' + Le bouton \'Micro\' est masqué. + Le bouton \'Micro\' est affiché. Masquer les séparateurs gris Les séparateurs gris sont masqués. Les séparateurs gris sont affichés. Masquer les barres d\'actions 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. 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é. - Suppr. Message \"Confirmer votre âge\" - "Supprime le message \"Confirmer votre âge\". + Suppr. Message \'Confirmer votre âge\' + "Supprime le message 'Confirmer votre âge'. Cela ne contourne pas la restriction d'âge, mais le confirme automatiquement." Modifier la mise en page Original @@ -354,17 +354,17 @@ Si désactivé ultérieurement, il est recommandé d'effacer les données de l'a Saisir la version à falsifier Saisissez la version de l\'application à falsifier. Choisir la version à falsifier - 17.41.37 - Restaure l\'ancien menu \"Playlist\" - 18.05.40 - Restaure l\'ancien menu \"Commentaires\" + 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 + 18.48.39 - Désactive les \'vues\' et \'j\'aime\' en temps réel Menu du compte - Masque ou affiche des éléments dans le menu du compte et dans l\'onglet \"Vous\". + Masque ou affiche des éléments dans le menu du compte et dans l\'onglet \'Vous\'. Masquer le menu du compte - "Masque les éléments du menu du compte et de l'onglet \"Vous\". + "Masque les éléments du menu du compte et de l'onglet 'Vous'. Certains composants peuvent ne pas être masqués." Filtre du menu du compte Liste de noms du menu de compte à filtrer, séparés par un saut de ligne. @@ -385,19 +385,19 @@ Certains composants peuvent ne pas être masqués." Boutons d\'action Remplace l\'action des boutons in-app. - Bouton \"Télécharger\" + Bouton \'Télécharger\' Remplacer le bouton de téléchargement de la vidéo - Le bouton \"Télécharger\" natif ouvre votre téléchargeur externe. - 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. + Le bouton \'Télécharger\' natif ouvre le téléchargeur de l\'appli. Remplacer le bouton de téléchargement de la playlist Le bouton de téléchargement natif de la playlist est toujours affiché, tandis que les playlists publiques utilisera votre téléchargeur externe. - Si affiché, le bouton \"Télécharger\" natif de la playlist ouvre le téléchargeur natif de l\'appli. + Si affiché, le bouton \'Télécharger\' natif de la playlist ouvre le téléchargeur natif de l\'appli. Nom du paquet du téléchargeur de la playlist Nom de package du téléchargeur externe installé, telle que YTDLnis. - Remplacer le bouton \"YouTube Music\" - Le bouton \"YouTube Music\" ouvre RVX Music. - Le bouton \"YouTube Music\" ouvre l\'appli natif. + Remplacer le bouton \'YouTube Music\' + Le bouton \'YouTube Music\' ouvre RVX Music. + Le bouton \'YouTube Music\' ouvre l\'appli natif. Nom du paquet de RVX Music Nom du paquet de RVX Music installé. RVX Music @@ -406,7 +406,7 @@ Certains composants peuvent ne pas être masqués." Prérequis YouTube Music est requis pour remplacer l\'action du bouton. Cliquez ici pour télécharger YouTube Music. - Minilecteur + Mini Lecteur Change le style du lecteur minimisé de l\'application. Style du minilecteur Original @@ -430,9 +430,9 @@ Certains composants peuvent ne pas être masqués." Masquer les sous-textes Les sous-textes sont masqués. Les sous-textes sont affichés. - Masquer les boutons \"Avancer\" et \"Reculer\" - Les boutons \"Avancer\" et \"Reculer\" sont masqués. - Les boutons \"Avancer\" et \"Reculer\" sont affichés. + Masquer les boutons \'Avancer\' et \'Reculer\' + Les boutons \'Avancer\' et \'Reculer\' sont masqués. + Les boutons \'Avancer\' et \'Reculer\' sont affichés. Opacité du mini lecteur Valeur d\'opacité entre 0-100, 0 étant transparent. L\'opacité du minilecteur doit être compris entre 0-100. @@ -442,32 +442,32 @@ Certains composants peuvent ne pas être masqués." Activer les boutons de navigation compacts L\'espacement entre les boutons de la barre de navigation sont réduit. L\'espacement entre les boutons de la barre de navigation sont normaux. - Masquer le bouton \"Créer\" - Le bouton \"Créer\" est masqué. - Le bouton \"Créer\" est affiché. - Masquer le bouton \"Accueil\" - Le bouton \"Accueil\" est masqué. - Le bouton \"Accueil\" est affiché. - Masquer le bouton \"Bibliothèque\" - Le bouton \"Bibliothèque\" est masqué. - Le bouton \"Bibliothèque\" est affiché. - Masquer le bouton \"Notifications\" - Le bouton \"Notifications\" est masqué. - Le bouton \"Notifications\" est affiché. - Masquer le bouton \"Shorts\" - Le bouton Shorts est masqué. - Le bouton \"Shorts\" est affiché. - Masquer le bouton \"Abonnements\" - Le bouton \"Abonnements\" est masqué. - Le bouton \"Abonnements\" est affiché. + Masquer le bouton \'Créer\' + Le bouton \'Créer\' est masqué. + Le bouton \'Créer\' est affiché. + Masquer le bouton \'Accueil\' + Le bouton \'Accueil\' est masqué. + Le bouton \'Accueil\' est affiché. + Masquer le bouton \'Bibliothèque\' + Le bouton \'Bibliothèque\' est masqué. + Le bouton \'Bibliothèque\' est affiché. + Masquer le bouton \'Notifications\' + Le bouton \'Notifications\' est masqué. + Le bouton \'Notifications\' est affiché. + Masquer le bouton \'Shorts\' + Le bouton \'Shorts\' est masqué. + Le bouton \'Shorts\' est affiché. + Masquer le bouton \'Abonnements\' + Le bouton \'Abonnements\' est masqué. + Le bouton \'Abonnements\' est affiché. Masquer les noms des catégories Le nom des catégories sont masqués. Le nom des catégories sont affichés. - Échanger \"Créer\" et \"Notifications\" - "Le bouton \"Créer\" est échangé avec le bouton \"Notification\". + Échanger \'Créer\' et \'Notifications\' + "Le bouton 'Créer' est échangé avec le bouton 'Notification'. Note : Activer ceci masquera également les publicités vidéos." - Le bouton \"Créer\" n\'est pas échangé avec le bouton \"Notification\". + Le bouton \'Créer\' n\'est pas échangé avec le bouton \'Notification\'. "Désactiver ceci pourrait charger plus de publicités depuis le serveur. Également, les publicités ne seront plus bloquées sur les Shorts. @@ -486,8 +486,8 @@ Si ce paramètre ne fait pas effet, essayer de passer en mode Incognito."Le menu \'Centre pour la famille\' est masqué. Le menu \'Centre pour la famille\' est affiché. Masquer le menu \'Paramètres généraux\' - Le menu \"Paramètres généraux\' est masqué. - Le menu \"Paramètres généraux\' est affiché. + Le menu \'Paramètres généraux\' est masqué. + Le menu \'Paramètres généraux\' est affiché. Masquer le menu \'Compte\' Le menu \'Compte\' est masqué. Le menu \'Compte\' est affiché. @@ -554,37 +554,37 @@ Si ce paramètre ne fait pas effet, essayer de passer en mode Incognito."Activ. Barre recherche large avec en-tête La barre de recherche large ne masque pas l\'en-tête YouTube. La barre de recherche large masque l\'en-tête YouTube. - Activer la barre de recherche large dans l\'onglet \"Vous\" - "Activer ce paramètre désactive le bouton \"Paramètres\" dans l'onglet \"Vous\". + Activer la barre de recherche large dans l\'onglet \'Vous\' + "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" - Masquer le bouton \"Caster\" - Le bouton \"Caster\" est masqué. - Le bouton \"Caster\" est affiché. - Masquer le bouton \"Créer\" - Le bouton \"Créer\" est masqué. - Le bouton \"Créer\" est affiché. - Masquer le bouton \"Notifications\" - Le bouton \"Notifications\" est masqué. - Le bouton \"Notifications\" est affiché. + Masquer le bouton \'Caster\' + Le bouton \'Caster\' est masqué. + Le bouton \'Caster\' est affiché. + Masquer le bouton \'Créer\' + Le bouton \'Créer\' est masqué. + Le bouton \'Créer\' est affiché. + Masquer le bouton \'Notifications\' + Le bouton \'Notifications\' est masqué. + Le bouton \'Notifications\' est affiché. Masquer les miniatures pendant la recherche Les miniatures sur la barre de recherche sont masquées. Les miniatures sur la barre de recherche sont affichées. - Masquer le bouton \"Recherche d\'images\" - Le bouton \"Recherche d\'images\" est masqué. - Le bouton \"Recherche d\'images\" est affiché. - Masquer le bouton \"Recherche vocale\" - Le bouton \"Recherche vocale \" est masqué. - Le bouton \"Recherche vocale \" est affiché. + Masquer le bouton \'Recherche d\'images\' + Le bouton \'Recherche d\'images\' est masqué. + Le bouton \'Recherche d\'images\' est affiché. + Masquer le bouton \'Recherche vocale\' + Le bouton \'Recherche vocale\' est masqué. + Le bouton \'Recherche vocale\' est affiché. Masquer les Doodles YouTube Les Doodles YouTube sont masqués. Les Doodles YouTube sont affichés. "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." - Remplacer le bouton \"Créer\" - Remplace le bouton \"Créer\" par le bouton \"Paramètre\". + Remplacer le bouton \'Créer\' + Remplace le bouton \'Créer\' par le bouton \'Paramètre\'. Action à attribuer au bouton "Appuyez pour ouvrir les paramètres RVX. Appuyez longuement pour ouvrir les paramètres YouTube." @@ -609,9 +609,9 @@ Paramètres → Lecture automatique → Lecture automatique de la vidéo suivant "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 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." - Valeur de \"Vitesse de lecture\" + Valeur de \'Vitesse de lecture\' La valeur de vitesse de lecture doit être comprise entre 0-8.0. La valeur de la vitesse de lecture doit être comprise entre 0-8.0. Masquer le filigrane de chaine @@ -644,9 +644,9 @@ Note : Masquer les bandeaux de messages Les bandeaux de messages sont masqués. Les bandeaux de messages sont affichés. - Masquer le bandeau \"Relâchez pour annuler\" - Le bandeau \"Relâchez pour annuler\' est masqué. - Le bandeau \"Relâchez pour annuler\" est affiché. + Masquer le bandeau \'Relâchez pour annuler\' + Le bandeau \'Relâchez pour annuler\' est masqué. + Le bandeau \'Relâchez pour annuler\' est affiché. Masquer les suggestions d\'actions Les actions suggérées sont masquées. Les actions suggérées sont affichées. @@ -671,25 +671,25 @@ La lecture automatique peut être modifiée dans les paramètres de YouTube : Boutons sous la vidéo Masque ou affiche les boutons sous les vidéos. - Désac. lueur des \"J\'aime\" et \"Je n\'aime pas\" - Les boutons \"J\'aime\" et \"Je n\'aime pas\" ne s\'illuminerons pas lorsqu\'ils sont mentionné. - Les boutons \"J\'aime\" et \"Je n\'aime pas\" s\'illuminerons lorsqu\'ils sont mentionné. - Masquer le bouton \"Extrait\" - Le bouton \"Extrait\" est masqué. - Le bouton \"Extrait\" est affiché. - Masquer le bouton \"Télécharger\" - Le bouton \"Télécharger\" est masqué. - Le bouton \"Télécharger\" est affiché. - Masquer les \"J\'aime\" et \"Je n\'aime pas\" - Les boutons \"J\'aime\" et \"Je n\'aime pas\" sont masqués. - Les boutons \"J\'aime\" et \"Je n\'aime pas\" sont affichés. - Masquer le bouton \"Remixer\" - Le bouton \"Remixer\" est masqué. - Le bouton \"Remixer\" est affiché. - Masquer le bouton \"Signaler\" - Le bouton \"Signaler\" est masqué. - Le bouton \"Signaler\" est affiché. - Masquer le bouton \"Récompense\" + Désac. lueur des \'J\'aime\' et \'Je n\'aime pas\' + Les boutons \'J\'aime\' et \'Je n\'aime pas\' ne s\'illuminerons pas lorsqu\'ils sont mentionné. + Les boutons \'J\'aime\' et \'Je n\'aime pas\' s\'illuminerons lorsqu\'ils sont mentionné. + Masquer le bouton \'Clip\' + Le bouton \'Clip\' est masqué. + Le bouton \'Clip\' est affiché. + Masquer le bouton \'Télécharger\' + Le bouton \'Télécharger\' est masqué. + Le bouton \'Télécharger\' est affiché. + Masquer les \'J\'aime\' et \'Je n\'aime pas\' + Les boutons \'J\'aime\' et \'Je n\'aime pas\' sont masqués. + Les boutons \'J\'aime\' et \'Je n\'aime pas\' sont affichés. + Masquer le bouton \'Remixer\' + Le bouton \'Remixer\' est masqué. + Le bouton \'Remixer\' est affiché. + Masquer le bouton \'Signaler\' + Le bouton \'Signaler\' est masqué. + Le bouton \'Signaler\' est affiché. + Masquer le bouton \'Récompense\' Le bouton \"Récompense\" est masqué. Le bouton \"Récompense\" est affiché. Masquer le bouton \"Enregistrer\" @@ -764,59 +764,59 @@ La lecture automatique peut être modifiée dans les paramètres de YouTube : Type de bouton à bascule Les boutons à bascule avec textes sont utilisés. Les boutons à bascule sont utilisés. - Masquer le menu 1080p Premium - Le menu 1080p Premium est masqué. - Le menu 1080p Premium est affiché. - Masquer le menu \"Piste audio\" - Le menu \"Piste audio\" est masqué. - Le menu \"Piste audio\" est affiché. - Masquer le menu \"Sous-titres\" - Le menu \"Sous-titres\" est masqué. - Le menu \"Sous-titres\" est affiché. + Masquer le menu \'1080p Premium\' + Le menu \'1080p Premium\' est masqué. + Le menu \'1080p Premium\' est affiché. + Masquer le menu \'Piste audio\' + Le menu \'Piste audio\' est masqué. + Le menu \'Piste audio\' est affiché. + Masquer le menu \'Sous-titres\' + Le menu \'Sous-titres\' est masqué. + Le menu \'Sous-titres\' est affiché. Masquer les conseils des sous-titres - Le message du menu \"sous-titre\" est masqué. - Le message du menu \"sous-titre\" est affiché. - Masquer le menu \"Verrouiller l\'écran\" - Le menu \"Verrouiller l\'écran\" est masqué. - Le menu \"Verrouiller l\'écran\" est affiché. - Masquer le menu \"Plus d’infos\" - Le menu \"Plus d\'infos\" est masqué. - Le menu \"Plus d\'infos\" est affiché. - Masquer le menu \"Vitesse de lecture\" - Le menu \"Vitesse de lecture\" est masqué. - Le menu \"Vitesse de lecture\" est affiché. - Masquer l\'en-tête du menu qualité - L\'en-tête du menu qualité est masqué. - L\'en-tête du menu qualité est affiché. - Masquer le message du menu \"Qualité\" - Le message du menu \"Qualité vidéo\" est masqué. - Le message du menu \"Qualité vidéo\" est affiché. - Masquer le menu \"Signaler\" - Le menu \"Signaler\" est masqué. - Le menu \"Signaler\" est affiché. + Le message du menu \'Sous-titre\' est masqué. + Le message du menu \'Sous-titre\' est affiché. + Masquer le menu \'Verrouiller l\'écran\' + Le menu \'Verrouiller l\'écran\' est masqué. + Le menu \'Verrouiller l\'écran\' est affiché. + Masquer le menu \'Plus d’infos\' + Le menu \'Plus d\'infos\' est masqué. + Le menu \'Plus d\'infos\' est affiché. + Masquer le menu \'Vitesse de lecture\' + Le menu \'Vitesse de lecture\' est masqué. + Le menu \'Vitesse de lecture\' est affiché. + Masquer l\'en-tête du menu \'Qualité\' + L\'en-tête du menu \'Qualité\' est masqué. + L\'en-tête du menu \'Qualité\' est affiché. + Masquer le message du menu \'Qualité\' + Le message du menu \'Qualité vidéo\' est masqué. + Le message du menu \'Qualité vidéo\' est affiché. + Masquer le menu \'Signaler\' + Le menu \'Signaler\' est masqué. + Le menu \'Signaler\' est affiché. Paramètres supplémentaires - Masquer le menu \"Mode Ambiant\" - Le menu \"Mode Ambiant\" est masqué. - Le menu \"Mode Ambiant\" est affiché. - Masquer le menu \"Aide et commentaires\" - Le menu \"Aide et commentaires\" est masqué. - Le menu \"Aide et commentaires\" est affiché. - Masquer le menu \"Écouter avec YouTube Music\" - Le menu \"Écouter avec YouTube Music\" est masqué. - Le menu \"Écouter avec YouTube Music\" est affiché. - Masquer le menu \"Lecture en boucle\" - Le menu \"Lecture en boucle\" est masqué. - Le menu \"Lecture en boucle\" est affiché. - Masquer le menu \"Picture-in-picture\" - Le menu \"Picture-in-picture\" est masqué. - Le menu \"Picture-in-picture\" est affiché. - Masquer le menu \"Commandes Premium\" - Le menu \"Commandes Premium\" est masqué. - Le menu \"Commandes Premium\" est affiché. - Masquer le menu \"Délai de mise en veille\" - Le menu \"Délai de mise en veille\" est masqué. - Le menu \"Délai de mise en veille\" est affiché. + Masquer le menu \'Mode Ambiant\' + Le menu \'Mode Ambiant\' est masqué. + Le menu \'Mode Ambiant\' est affiché. + Masquer le menu \'Aide et commentaires\' + Le menu \'Aide et commentaires\' est masqué. + Le menu \'Aide et commentaires\' est affiché. + Masquer le menu \'Écouter avec YouTube Music\' + Le menu \'Écouter avec YouTube Music\' est masqué. + Le menu \'Écouter avec YouTube Music\' est affiché. + Masquer le menu \'Lecture en boucle\' + Le menu \'Lecture en boucle\' est masqué. + Le menu \'Lecture en boucle\' est affiché. + Masquer le menu \'Picture-in-picture\' + Le menu \'Picture-in-picture\' est masqué. + Le menu \'Picture-in-picture\' est affiché. + Masquer le menu \'Commandes Premium\' + Le menu \'Commandes Premium\' est masqué. + Le menu \'Commandes Premium\' est affiché. + Masquer le menu \'Délai de mise en veille\' + Le menu \'Délai de mise en veille\' est masqué. + Le menu \'Délai de mise en veille\' est affiché. Masquer le menu \"Volume stable\" Le menu \"Volume stable\" est affiché. Le menu \"Volume stable\" est masqué. @@ -902,13 +902,13 @@ Limitation : Le titre de la vidéo disparaît lorsque vous cliquez dessus."Désact. vibration de sélection de chapitres La vibration est désactivée. La vibration est activée. - Desact. Vibration pendant le glissement + Désact. vibration pendant le glissement La vibration est désactivée. La vibration est activée. - Desact. vibration de la barre de progression + Désact. vibration de la barre de progression La vibration est désactivée. La vibration est activée. - Desact. vibration barre de progression relaché + Désact. vibration barre de progression relâchée La vibration est désactivée. La vibration est activée. Désact. vibration lors du zoom @@ -959,9 +959,6 @@ Appuyez longuement pour revenir à la vitesse de lecture à 1.0x. Appuyez longue Afficher le bouton \"Liste blanche\" Appuyez pour ouvrir la liste blanche. Appuyez longuement pour ouvrir les paramètres de la liste blanche. - Afficher bouton playlist chronologique - "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." Liste blanche des chaînes Vérifier ou supprimer les chaînes ajoutés à la liste blanche. La chaîne \'%1$s\' a été ajoutée à la liste blanche \'%2$s\'. @@ -1100,9 +1097,9 @@ Effet secondaire : Les fiches officielles dans les résultats de recherche sont Information : • Seules les étagères dont l'en-tête est Shorts dans l'onglet d'accueil sont masquées." Affiché sur les chaînes. - Masquer dans les flux \"accueil\" et \"vidéos similaires\" - Masqué dans les flux \"accueil\" et \"vidéos similaires\". - Affiché dans les flux \"accueil\" et \"vidéos similaires\". + Masquer dans les flux \"Accueil\" et \"Vidéos similaires\" + Masqué dans les flux \"Accueil\" et \"Vidéos similaires\". + Affiché dans les flux \"Accueil\" et \"Vidéos similaires\". Masquer dans le flux \"Abonnements\" Masquer dans le flux \"Abonnements\". Affiché dans le flux \"Abonnements\". @@ -1165,9 +1162,9 @@ Information : Le lien vers la vidéo complète est affiché. Actions suggérées - Masquer le bouton \'fond vert\' - Le bouton \'fond vert\' est masqué. - Le bouton \'fond vert\' est affiché. + Masquer le bouton \"Fond vert\" + Le bouton \"Fond vert\" est masqué. + Le bouton \"Fond vert\" est affiché. Masquer le bouton \"Enregistrer la musique\" Le bouton \"Enregistrer la musique\" est masqué. Le bouton \"Enregistrer la musique\" est affiché. @@ -1217,7 +1214,7 @@ Information : Désactiver l\'animation du bouton J\'aime L\'animation en fontaine est désactivé au-dessus du bouton j\'aime. L\'animation en fontaine est activé au-dessus du bouton j\'aime. - Masquer fond du bouton Lecture & Pause + Masquer fond du bouton \"Lecture\" & \"Pause\" Le fond du bouton est masqué. Le fond du bouton est affiché. Animation lors du double appui @@ -1310,7 +1307,7 @@ Limitations : Déaactiver les vidéos HDR Les vidéos en HDR sont désactivés. Les vidéos en HDR sont activés. - Desact. vitesse lecture des diffusions en direct + Désact. vitesse lecture des diffusions en direct La vitesse de lecture par défaut est désactivée pour les diffusions en direct. La vitesse de lecture par défaut est activée pour les diffusions en direct. Activer vitesses de lecture perso. @@ -1685,24 +1682,17 @@ Cliquez sur le bouton Continuer et désactivez les optimisations de la batterie. Désactiver ce paramètre peut entraîner des problèmes de lecture vidéo. Client par défaut iOS - Android - Android Creator - Lecteur intégré Android - Android Testsuite Android TV Android VR - TV HTML5 - Web Effets inconnus de la falsification "• 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." +• Les vidéos peuvent se terminer 1 seconde plus tôt. +• Le codec audio Opus peut ne pas être supporté." "• 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 vidéos peuvent ne pas être lus. Forcer iOS AVC (H.264) Le codec vidéo d\'iOS est AVC (H.264). Les codecs vidéos d\'iOS sont AVC (H.264), VP9, ou AV1. diff --git a/src/main/resources/youtube/translations/hu-rHU/strings.xml b/patches/src/main/resources/youtube/translations/hu-rHU/strings.xml similarity index 96% rename from src/main/resources/youtube/translations/hu-rHU/strings.xml rename to patches/src/main/resources/youtube/translations/hu-rHU/strings.xml index 51e7ce2a5..e4bf57716 100644 --- a/src/main/resources/youtube/translations/hu-rHU/strings.xml +++ b/patches/src/main/resources/youtube/translations/hu-rHU/strings.xml @@ -553,9 +553,9 @@ Ha ez a beállítás nem működik, váltson inkognító módra." 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" - Átküldés gomb elrejtése - Az átküldés gomb el van rejtve. - Az átküldés gomb látható. + Kivetítés gomb elrejtése + A kivetítés gomb el van rejtve. + A kivetítés gomb látható. Létrehozás gomb elrejtése A létrehozás gomb el van rejtve. A létrehozás gomb látható. @@ -645,7 +645,7 @@ Megjegyzés: Időzített reakciók elrejtése Az időzített reakciók el vannak rejtve. Az időzített reakciók láthatóak. - Javasolt videó végoldali képernyő elrejtése + Javasolt videó elrejtése a záró képernyőn "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ó: @@ -719,37 +719,37 @@ Beállítások → Automatikus lejátszás → Következő videó automatikus le A próba indítása gomb látható. Hozzászólások - Rejtse el vagy mutassa a hozzászólások rész elemeit. - Csatornák irányelveinek elrejtése - A csatorna irányelvei elrejtve - A csatorna irányelvei megjelenítve + Megjegyzések rész megjelenítése vagy elrejtése. + Csatorna irányelveinek elrejtése + A csatorna irányelvei el vannak rejtve. + A csatorna irányelvei láthatóak. Rejtse el a csartornatagok megjegyzései részt A csatornatagok megjegyzései rész el van rejtve. A csatornatagok megjegyzései rész látható. Kiemelt keresési hivatkozások elrejtése A kiemelt keresési hivatkozások el vannak rejtve. A kiemelt keresési hivatkozások láthatóak. - A megjegyzések szekció elrejtése - A megjegyzések szekció rejtett - A megjegyzések szekció megjelenik - Rejtse el a hozzászólások részét a kezdőlapon - A hozzászólások része el van rejtve a kezdőlapon. - A hozzászólások része látható a kezdőlapon. + Megjegyzések rész elrejtése + A megjegyzések rész el van rejtve. + A megjegyzések rész látható. + Hozzászólások elrejtése a kezdőlapon + A hozzászólások el van rejtve a kezdőlapon. + A hozzászólások láthatóak a kezdőlapon. Megjegyzés előnézet elrejtése A megjegyzés előnézet el van rejtve. A megjegyzés előnézet látható. Megjegyzés előnézet elrejtés típusa - 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. - 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ések rész méretét, így lehetséges a megjegyzések részben élő chat választ nyitni. + Ez megváltoztatja a megjegyzések rész méretét, így nem lehetséges a megjegyzések részben élő chat választ nyitni. Short létrehozás gomb elrejtése A \'Short létrehozása\' gomb el van rejtve. A \'Short létrehozása\' gomb látható. Köszönöm gomb elrejtése - A Köszönöm gomb el van rejtve. - A Köszönöm gomb megjelenik. - Időbélyeg és az emoji gombok elrejtése - A megjegyzés időbélyegzője és az emoji gombok el vannak rejtve - A megjegyzés időbélyegzője és a hangulatjelek gombjai megjelennek + A köszönöm gomb el van rejtve. + A köszönöm gomb megjelenik. + Időbélyeg és emoji gombok elrejtése + Az időbélyeg és emoji gombok el vannak rejtve. + Az időbélyeg és emoji gombok láthatók. Felugró menü A videólejátszóban található kinyíló menü elemeinek elrejtése vagy módosítása. @@ -759,33 +759,33 @@ Beállítások → Automatikus lejátszás → Következő videó automatikus le 1080p Premium menü elrejtése A 1080p Premium menü el van rejtve. A 1080p Premium menü látható. - Audió nyomkövető menü elrejtése - Az audiosáv menü el van rejtve. - Az audiosáv menü megjelenik. + Hangsáv menü elrejtése + A hangsáv menü el van rejtve. + A hangsáv menü látható. Feliratok menü elrejtése A feliratok menü el van rejtve. - A feliratok menü megjelenik. + A feliratok menü látható. Feliratok menü láblécének elrejtése A feliratok menü lábléce el van rejtve. - A feliratok menü lábléce megjelenik. - Zárolási képernyő menü elrejtése - A zárolás képernyő menü el van rejtve. - A zárolás képernyő menü megjelenik. - További információ menü elrejtése + A feliratok menü lábléc látható. + Záró képernyő menü elrejtése + A záró képernyő menü el van rejtve. + A záró képernyő menü látható. + További Információk menü elrejtése A további információk menü el van rejtve. - A további információk menü megjelenik. + A további információk menü látható. Lejátszási sebesség menü elrejtése A lejátszási sebesség menü el van rejtve. - A lejátszási sebesség menü megjelenik. + A lejátszási sebesség menü látható. Minőség menü fejléc elrejtése A minőség menü fejléce elrejtve. A minőség menü fejléce látható. - Minőség menü láblécének elrejtése - A minőség menü lábléce el van rejtve. - A minőség menü lábléce megjelenik. + Minőség menü lábléc elrejtése + A minőség menü lábléc el van rejtve. + A minőség menü lábléc látható. Jelentés menü elrejtése A jelentés menü el van rejtve. - A jelentés menü megjelenik. + A jelentés menü látható. További beállítások \'Mozifilmes világítás\' menü elrejtése @@ -799,7 +799,7 @@ Beállítások → Automatikus lejátszás → Következő videó automatikus le A YouTube Music-kal való hallgatás menü megjelenik. Videó ismétlés menü elrejtése A videó ismétlés menü el van rejtve. - A videó ismétlés menü megjelenik. + A videó ismétlése menü látható. Kép-a-képben menü elrejtése A kép-a-képben menü el van rejtve. A kép-a-képben menü látható. @@ -809,34 +809,34 @@ Beállítások → Automatikus lejátszás → Következő videó automatikus le Elalvási időzítő elrejtése Az elalvási időzítő el van rejtve. Az elalvási időzítő látható. - Rejtsd el a stabil hangerő menüt - A stabil hangerő menü megjelenik. + Stabil hangerő menü elrejtése + A stabil hangerő menü látható. A stabil hangerő menü el van rejtve. - Statisztikák a nagyoknak menü elrejtése - A statisztikák a kockáknak menü el van rejtve. - A statisztikák a kockáknak menü megjelenik. - Nézés VR-ban menü elrejtése - A VR-ben nézés menü el van rejtve. - A VR-ben nézés menü megjelenik. + Statisztikák menü elrejtése + A statisztikák menü el van rejtve. + A statisztikák menü látható. + Megtekintés VR-ban menü elrejtése + A megtekintés VR-ban menü el van rejtve. + A megtekintés VR-ban menü látható. Teljes képernyő Teljes képernyős móddal kapcsolatos elemek elrejtése vagy módosítása. Interakciós panel letiltása - Az interakciós panel letiltva. + Az interakciós panel le van tiltva. Az interakciós panel engedélyezve. - Videócím szakasz megjelenítése - "Megjeleníti a videócím szakaszt teljes képernyős módban. + Videócím rész megjelenítése + "Megjeleníti a videócím részt teljes képernyős módban. Korlátozás: A videó címe eltűnik, ha rákattint." - Automatikus lejátszás előnézeti tároló elrejtése - Az automatikus lejátszás előnézeti kerete el van rejtve. - Az automatikus lejátszás előnézeti kerete látható. + Automatikus lejátszás előnézet elrejtése + Az automatikus lejátszás előnézet el van rejtve. + Az automatikus lejátszás előnézet látható. Élő csevegés gomb elrejtése - Az élő csevegés visszajátszása gomb rejtve van.\n\nAz élő csevegés bezárásakor teljes képernyőn jelenik meg. - Az élő csevegés visszajátszása gomb látható.\n\nAz élő csevegés bezárásakor teljes képernyőn jelenik meg. - Kapcsolódó videó átfedésének elrejtése - A kapcsolódó videó átfedése el van rejtve. - A kapcsolódó videó átfedése látható. + Az élő csevegés gomb el van rejtve.\n\nAz élő csevegés bezárásakor teljes képernyőn jelenik meg. + Az élő csevegés gomb látható.\n\nAz élő csevegés bezárásakor teljes képernyőn jelenik meg. + Kapcsolódó videó elrejtése + A további videók rész a gyors menüben és a kapcsolódó videó el vannak rejtve. + A további videók rész a gyors menüben és a kapcsolódó videó láthatóak. Gyors műveletek Gyorsműveletek konténer elrejtése @@ -844,7 +844,7 @@ Korlátozás: A videó címe eltűnik, ha rákattint." A gyorsműveletek megjelennek. Hozzászólás gomb elrejtése A hozzászólás gomb el van rejtve. - A hozzászólás gomb megjelenik. + A hozzászólás gomb látható. Nem tetszik gomb elrejtése A nem tetszik gomb el van rejtve. A nem tetszik gomb látható. @@ -854,13 +854,13 @@ Korlátozás: A videó címe eltűnik, ha rákattint." Élő chat gomb elrejtése A élő chat gomb el van rejtve. A élő chat gomb látható. - További gomb elrejtése - A Tovább gomb el van rejtve. - A Tovább gomb megjelenik. - Mix lejátszási lista gomb elrejtése - A mix lejátszási lista megnyitása gomb el van rejtve. - A mix lejátszási lista megnyitása gomb látható. - Lejátszási lista gomb elrejtése + Bővebben gomb elrejtése + A bővebben gomb el van rejtve. + A bővebben gomb látható. + Lejátszási lista keverés gomb elrejtése + A lejátszási lista keverés gomb el van rejtve. + A lejátszási lista keverés gomb látható. + Lejátszási lista megnyitása gomb elrejtése A lejátszási lista megnyitása gomb el van rejtve. A lejátszási lista megnyitása gomb látható. Lejátszási listához mentés gomb elrejtése @@ -870,19 +870,19 @@ Korlátozás: A videó címe eltűnik, ha rákattint." A megosztás gomb el van rejtve. A megosztás gomb látható. Gyorsműveletek felső margó - Állítsa be a gyorsművelet-tároló és a csúszka közötti távolságot 0-32 között. + Állítsa be a keresősáv és a gyorsművelet rész közötti távolságot 0-32 között. A gyors műveletek felső margójának 0 és 32 között kell lennie. Visszaállítás alapértelmezettre. Fekvő mód letiltása - A videó tájolása portré mód fullscreenben. - A videó tájolása követi a készülék beállításait fullscreenben. - Kompakt vezérlők fedés engedélyezése - A vezérlők felülete nem tölti ki a teljes képernyőt. - A vezérlők felülete kitölti a teljes képernyőt. + A videó tájolása álló mód teljes képernyő esetén. + A videó tájolása követi a készülék beállításait teljes képernyő esetén. + Kompakt vezérlők engedélyezése + A vezérlők nem töltik ki a teljes képernyőt. + A vezérlők kitöltik a teljes képernyőt. Teljes képernyő erőltetése - "A videókat a következő esetekben váltjuk át teljes képernyős módba: + "A videók teljes képernyősre váltanak a következő esetekben: -• Amikor egy videót elindítanak. +• Amikor egy videó elindul. • Amikor egy időbélyegre kattintanak a hozzászólásokban." Tartsa a fekvő módot Megtartja a fekvő módot a képernyő ki- és bekapcsolásakor teljes képernyőn. @@ -891,33 +891,33 @@ Korlátozás: A videó címe eltűnik, ha rákattint." Haptikus visszajelzés Haptikus visszajelzés engedélyezése vagy letiltása. - Fejezetek haptikus jelzésének kikapcsolása - A haptikus visszajelzés ki van kapcsolva. - A haptikus visszajelzés be van kapcsolva. + Fejezetek haptikus visszajelzésének kikapcsolása + A haptikus visszajelzés le van tiltva. + A haptikus visszajelzés engedélyezve van. Súrolás haptikus visszajelzésének letiltása - A haptikus visszajelzés ki van kapcsolva. - A haptikus visszajelzés be van kapcsolva. + A haptikus visszajelzés le van tiltva. + A haptikus visszajelzés engedélyezve van. Lejátszó csúszka haptikus visszajelzésének letiltása - A haptikus visszajelzés ki van kapcsolva. - A haptikus visszajelzés be van kapcsolva. + A haptikus visszajelzés le van tiltva. + A haptikus visszajelzés engedélyezett. Keresés visszavonás haptikus visszajelzésének kikapcsolása - A haptikus visszajelzés ki van kapcsolva. - A haptikus visszajelzés be van kapcsolva. + A haptikus visszajelzés le van tiltva. + A haptikus visszajelzés engedélyezett. Nagyítás haptikus visszajelzésének letiltása - A haptikus visszajelzés ki van kapcsolva. - A haptikus visszajelzés be van kapcsolva. + A haptikus visszajelzés le van tiltva. + A haptikus visszajelzés engedélyezett. Lejátszó gombok A videólejátszó gombok elrejtése vagy megjelenítése. Automatikus lejátszás gomb elrejtése Az automatikus lejátszás gomb el van rejtve. - Az automatikus lejátszás gomb megjelenik. + Az automatikus lejátszás gomb látható. Feliratok gomb elrejtése - A Feliratok gomb el van rejtve. - A Feliratok gomb megjelenik. - Szereplők gomb elrejtése - A Cast gomb el van rejtve. - A Cast gomb megjelenik. + A feliratok gomb el van rejtve. + A feliratok gomb látható. + Kivetítés gomb elrejtése + A kivetítés gomb el van rejtve. + A kivetítés gomb látható. Összecsukás gomb elrejtése Az Összecsukás gomb el van rejtve. Az Összecsukás gomb megjelenik. @@ -933,8 +933,8 @@ Korlátozás: A videó címe eltűnik, ha rákattint." Felület gombok Folyamatos ismétlés gomb megjelenítése - "Tap to toggle always repeat states. -Tap and hold to toggle pause after repeat states." + "Koppintson a mindig ismétlődő állapotok váltásához. +Érintse meg és tartsa lenyomva az ismétlődő állapotok szüneteltetéséhez." Videó URL-jének másolása 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." @@ -951,9 +951,6 @@ Tartsa nyomva a sebesség alaphelyzetbe állításához." Kivétellista 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. - Időben rendezett lejátszási lista 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." Csatorna kivétellista Ellenőrizd vagy távolítsd el az kivétellistához hozzáadott csatornákat. \'%1$s\' csatorna hozzá lett adva a %2$s kivételekhez. @@ -976,7 +973,10 @@ Tartsa nyomva a sebesség alaphelyzetbe állításához." Keresősáv Szabja meg a keresősáv komponenseit. Időbélyegző információ hozzáfűzése - "Az információ hozzáadásra kerül az időbélyeghez." + "Az információ az időbélyeghez adva. + +Érintse meg a videó minőségének vagy lejátszási sebességének beállításához. +Érintse meg és tartsa lenyomva a hozzáfűzött információ típusának váltásához." Az információ nem kerül hozzáadásra az időbélyeghez. Információ típusának hozzáfűzése Videóminőség hozzáadása. @@ -1047,9 +1047,9 @@ Mellékhatás: a Cairo stílus az értesítési pontokra is alkalmazódik."Kulcs koncepciók rész elrejtése A kulcs koncepciók rész elrejtve. A kulcs koncepciók rész látható. - Podcast szakasz elrejtése - A podcast szakasz rejtve van - A podcast szakasz látható + Podcast rész elrejtése + A podcast rész el van rejtve. + A podcast rész látható. Vásárlási linkek elrejtése a videó leírásában A vásárlási linkek rejtve vannak A vásárlási linkek láthatóak @@ -1259,7 +1259,7 @@ Korlátozások: Érintse meg és tartsa nyomva a csúsztatás engedélyezéséhez. Érintse meg a csúsztatás engdélyezéséhez. Haptikus visszajelzés engedélyezése - A haptikus visszajelzés engedélyezve van. + A haptikus visszajelzés engedélyezett. A haptikus visszajelzés le van tiltva. A csúsztatási mozdulatok a \'Képernyő lezárása\' módban A csúsztatási mozdulatok engedélyezve vannak a \'Képernyő lezárása\' módban. @@ -1668,20 +1668,13 @@ Kattintson az API-kulcs kiadás folyamatának megtekintéséhez." A beállítás kikapcsolása videólejátszási problémákat okozhat. Alapértelmezett kliens iOS - Android - Android Creator - Android beágyazott lejátszó - Android tesztcsomag Android TV Android VR - TV HTML5 - Web Hamisítás mellékhatásai "• 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." "• Az audiosáv menü hiányzik." "• Az audiosáv menü hiányzik." - • A videó esetleg nem játszódik le. Kényszerített iOS AVC (H.264) Az iOS videokodek AVC (H.264). Az iOS videokodek AVC (H.264), VP9 vagy AV1. diff --git a/src/main/resources/youtube/translations/it-rIT/strings.xml b/patches/src/main/resources/youtube/translations/it-rIT/strings.xml similarity index 98% rename from src/main/resources/youtube/translations/it-rIT/strings.xml rename to patches/src/main/resources/youtube/translations/it-rIT/strings.xml index fbfd78e6e..e4675cf62 100644 --- a/src/main/resources/youtube/translations/it-rIT/strings.xml +++ b/patches/src/main/resources/youtube/translations/it-rIT/strings.xml @@ -145,9 +145,9 @@ Tocca qui per saperne di più su DeArrow." Nascondi il pulsante Avvisami Il pulsante Avvisami è nascosto. Il pulsante Avvisami è visibile. - Nascondi lo scaffale Giochi Interattivi - Lo scaffale Giochi Interattivi è nascosto. - Lo scaffale Giochi Interattivi è visibile. + Nascondi lo scaffale Sala Giochi + Lo scaffale Sala Giochi è nascosto. + Lo scaffale Sala Giochi è visibile. Nascondi il pulsante Mostra Altro Il pulsante Mostra Altro è nascosto. Il pulsante Mostra Altro è visibile. @@ -960,9 +960,26 @@ Tocca e tieni premuto di nuovo per ripristinare la velocità predefinita."Mostra il pulsante Whitelist Tocca per aprire la finestra della whitelist. Tocca e tieni premuto per aprire la finestra delle impostazioni della whitelist. - Mostra il pulsante Playlist Ordinato-per-Data - "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 Riproduci Tutto + "Tocca per generare una playlist di tutti i video del canale. +Tocca e tieni premuto per annullare. + +Nota: +• Potrebbe non funzionare con i video live." + Genera la modalità playlist + Tutti i contenuti (ordinato per tempo) + Tutti i contenuti (ordinato per popolarità) + Solo video (ordinato per tempo) + Solo video (ordinato per popolarità) + Solo Shorts (ordinato per tempo) + Solo Shorts (ordinato per popolarità) + Solo video live (ordinato per tempo) + Solo video live (ordinato per popolarità) + Tutti i contenuti riservati solo ai membri + Video riservato solo ai membri + Shorts riservato solo ai membri + Video live riservato solo ai membri + Impossibile generare la playlist a causa della mancata corrispondenza dell\'ID del canale Whitelist dei canali Controlla o rimuovi l\'elenco dei canali aggiunti alla whitelist. Il canale \"%1$s\" è stato aggiunto alla whitelist %2$s @@ -1336,7 +1353,7 @@ Note: Disattiva la velocità di riproduzione predefinita per la musica "La velocità di riproduzione predefinita è disattivata per la musica. -Nota: potrebbe non funzionare con i video che non hanno il banner \"Ascolta su YouTube Music\"." +Nota: potrebbe non funzionare con i video che non hanno il banner Ascolta su YouTube Music." La velocità di riproduzione predefinita per la musica è attivata. Attiva la velocità di riproduzione predefinita negli Shorts La velocità di riproduzione predefinita negli Shorts è attivata. @@ -1680,35 +1697,25 @@ Tocca il pulsante Continua e disattiva l'ottimizzazioni della batteria."La disattivazione di questa impostazione potrebbe causare problemi di riproduzione. Client predefinito iOS - Android - Android Creator - Riproduttore Android incorporato - Android Test Suite Android TV Android VR - TV HTML5 - Web Effetti collaterali del camuffamento - "• I film o i video a pagamento potrebbero non essere riprodotti. -• I video live iniziano dall'inizio. -• I video potrebbero terminare 1 secondo prima. -• Il codec audio Opus potrebbe non funzionare." - "• I video potrebbero terminare 1 secondo prima. -• Il codec audio Opus potrebbe non funzionare." + "• I video live iniziano dall'inizio. +• I video potrebbero terminare 1 secondo prima." + • I video potrebbero terminare 1 secondo prima. "• Il menù Traccia Audio è mancante. • Il menù Volume Stabile è mancante." "• Il menù Traccia Audio è mancante. • Il menù Volume Stabile è mancante." - • Il video potrebbe non essere riprodotto. - Modalità di compatibilità iOS - Camuffato come client iOS solo se non è un film, un video a pagamento o live. - Sempre camuffato come client iOS. Forza iOS AVC (H.264) Il codec video è AVC (H.264). Il codec video è AVC (H.264), VP9 o AV1. "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 userà più dati internet rispetto a VP9 o AV1." + Salta la riproduzione dei video live con il client iOS + Il client iOS non è usato per la riproduzione dei video live. + Il client iOS è usato per la riproduzione dei video live. Mostra nelle statistiche per nerd Il client usato per recuperare i dati in streaming è visibile nelle statistiche per nerd. Il client usato per recuperare i dati in streaming è nascosto nelle statistiche per nerd. diff --git a/src/main/resources/youtube/translations/ja-rJP/strings.xml b/patches/src/main/resources/youtube/translations/ja-rJP/strings.xml similarity index 98% rename from src/main/resources/youtube/translations/ja-rJP/strings.xml rename to patches/src/main/resources/youtube/translations/ja-rJP/strings.xml index 0fc8fb1d4..3467a55ff 100644 --- a/src/main/resources/youtube/translations/ja-rJP/strings.xml +++ b/patches/src/main/resources/youtube/translations/ja-rJP/strings.xml @@ -9,8 +9,8 @@ デフォルト値にリセットしました。 実験的な機能 続行しますか? - 再起動してレイアウトを正常に読み込みます - 再起動して設定を適用します + レイアウトを正常に読み込むためには YouTube を再起動する必要があります。\n\n続行しますか? + 設定を反映させるためには YouTube を再起動する必要があります。\n\n続行しますか? 通常 外部ダウンローダーのパッケージ名 NewPipe や YTDLnis などの、インストールされている外部ダウンローダーアプリのパッケージ名です。 @@ -37,8 +37,8 @@ プレーヤー上に表示される「プロモーションを含みます」の文章を非表示にします。 プレーヤー上に表示される「プロモーションを含みます」の文章を非表示にします。 プロモーションバナーを非表示 - YouTube Premium の価格の値上げなどのプロモーションバナーを非表示にします。 - YouTube Premium の価格の値上げなどのプロモーションバナーを非表示にします。 + メンバーシップの更新や YouTube Premium の価格の値上げなどのプロモーションバナーを非表示にします。 + メンバーシップの更新や YouTube Premium の価格の値上げなどのプロモーションバナーを非表示にします。 自己スポンサーカードを非表示 概要欄下部に表示されるセルフスポンサーカードを非表示にします。 概要欄下部に表示されるセルフスポンサーカードを非表示にします。 @@ -111,8 +111,8 @@ DeArrow の詳細については、ここをタップしてください。"検索結果の動画の下に表示される、展開可能なチップを非表示にします。 検索結果の動画の下に表示される、展開可能なチップを非表示にします。 展開可能な棚を非表示 - 検索結果に表示される、展開可能な棚を非表示にします。 - 検索結果に表示される、展開可能な棚を非表示にします。 + 検索結果にある、展開可能な棚を非表示にします。 + 検索結果にある、展開可能な棚を非表示にします。 字幕ボタンを非表示 フィードから字幕ボタンを非表示にします。 フィードから字幕ボタンを非表示にします。 @@ -956,9 +956,6 @@ DeArrow の詳細については、ここをタップしてください。"ホワイトリストボタンを表示 タップするとホワイトリストのダイアログが開きます。 長押しするとホワイトリストの設定のダイアログが開きます。 - 時間順のプレイリストボタンを表示 - "タップすると、チャンネルの古いものから新しいものまですべての動画の再生リストが生成されます。 -元に戻すには、長押しします。" チャンネルのホワイトリスト ホワイトリストに登録したチャンネルのリストを確認/削除します。 チャンネル「%1$s」を %2$s ホワイトリストに登録しました。 @@ -1009,8 +1006,8 @@ DeArrow の詳細については、ここをタップしてください。"シークバーからチャプターを非表示にします。 シークバーからチャプターを非表示にします。 チャプターのラベルを非表示 - タイムスタンプの横に表示されるチャプターのラベルを非表示にします。 - タイムスタンプの横に表示されるチャプターのラベルを非表示にします。 + タイムスタンプの横にあるチャプターのラベルを非表示にします。 + タイムスタンプの横にあるチャプターのラベルを非表示にします。 タイムスタンプを非表示 タイムスタンプを非表示にします。 タイムスタンプを非表示にします。 @@ -1034,8 +1031,8 @@ DeArrow の詳細については、ここをタップしてください。"概要欄 概要欄のコンポーネントを非表示または表示 数字の回転アニメーションを無効化 - 高評価数と視聴回数の回転アニメーションを無効にします。 - 高評価数と視聴回数の回転アニメーションを無効にします。 + 高評価数と再生回数の回転アニメーションを無効にします。 + 高評価数と再生回数の回転アニメーションを無効にします。 AI 生成の要約欄を非表示 概要欄にある「AI による動画の要約」欄を非表示にします。 概要欄にある「AI による動画の要約」欄を非表示にします。 @@ -1161,8 +1158,8 @@ DeArrow の詳細については、ここをタップしてください。" 推奨されるアクション グリーンスクリーンボタンを非表示 - プレーヤーの下部に表示される「グリーンスクリーン」ボタンを非表示にします。 - プレーヤーの下部に表示される「グリーンスクリーン」ボタンを非表示にします。 + プレーヤーの下部にある「グリーンスクリーン」ボタンを非表示にします。 + プレーヤーの下部にある「グリーンスクリーン」ボタンを非表示にします。 保存ボタンを非表示 楽曲の「保存」ボタンを非表示にします。 楽曲の「保存」ボタンを非表示にします。 @@ -1453,21 +1450,21 @@ API キーの発行方法については、ここをタップしてください スポンサー 有料プロモーション、有料紹介、直接広告が含まれます。セルフプロモーションや、個人の好きなクリエイター/ウェブサイト/商品に対する無償の活動は含まれません。 無報酬 / セルフプロモーション - 無報酬のプロモーションあるいはセルフプロモーションであるという点を除いては「スポンサー」と同様です。商品、寄付、コラボ情報に関する内容を含みます。 + 無報酬のプロモーションあるいはセルフプロモーションであるという点を除いては「スポンサー」と同様です。商品、寄付、コラボした人に関する内容を含みます。 行動を促すメッセージ (チャンネル登録等) - 動画の途中に挿入される高評価、チャンネル登録、フォローなどを促す短いリマインダーは、長いものや何か具体的なものは「セルフプロモーション」に分類するべきです。 + 動画の途中に挿入される高評価、チャンネル登録、フォローなどを促す短いリマインダーです。長いものや何か具体的なものは「セルフプロモーション」に分類するべきです。 ハイライト 動画の中で多くの人々が見たい部分 休憩 / イントロアニメーション 本編ではない部分。一時停止、静止画面、アニメーションの繰り返しが含まれます。情報を含んだ転換画面は含まれません。 エンドカード / クレジット - クレジットや動画のエンドカードが表示されている場面。情報を含む結論は含まれません。 + クレジットや動画のエンドカードが表示されている場面です。情報を含む結論は含まれません。 予告 / 要約 / フック - この動画やシリーズの他の動画で起きた、または今後起きる内容などをまとめたクリップのコレクション。すべての情報は、別の場所で繰り返し表示されます。 + この動画やシリーズの他の動画で起きた、または今後起きる内容などをまとめたクリップのコレクションです。すべての情報は、別の場所で繰り返し表示されます。 繋ぎの話 / 冗談 動画の本編を理解するのに必要のない繋ぎの話やユーモアなどの逸脱したシーン。コンテクストや背景情報の詳細は含まれません。 音楽ではない区間 - ミュージックビデオでのみ使用できます。他のカテゴリーに含まれていない、ミュージックビデオの音楽のない区間。 + ミュージックビデオでのみ使用できます。他のカテゴリーに含まれていない、ミュージックビデオの音楽のない区間です。 スキップ ハイライト スポンサーをスキップ @@ -1683,29 +1680,17 @@ API キーの発行方法については、ここをタップしてください この設定をオフにした場合、バッファリングの問題が発生する可能性があります。 偽装するクライアントの種類 iOS - Android - Android Creator - Android 埋め込みプレーヤー - Android Testsuite Android TV Android VR - TV HTML5 - Web ストリーミングデータを偽装することによる副作用 "・映画や有料動画が再生できない場合があります。 ・ライブは最初から再生されます。 ・動画が 1 秒早く終了することがあります。 ・Opus オーディオコーデックがサポートされない可能性があります。" - "・動画が 1 秒早く終了する可能性があります。 -・Opus オーディオコーデックはサポートされていない可能性があります。" "・「音声トラック」メニューは表示されません。 ・「一定音量」は使用できません。" "•「音声トラック」メニューは表示されません。 •「一定音量」は使用できません。" - • 動画が再生できない可能性があります。 - iOS 互換モード - 映画、有料動画、ライブを再生する時以外にのみ iOS クライアントに偽装します。 - 映画、有料動画、ライブを再生する時以外にのみ iOS クライアントに偽装します。 iOS クライアントで AVC (H.264) を強制 iOS クライアントで AVC コーデック (H.264) を強制します。 iOS クライアントで AVC コーデック (H.264) を強制します。 diff --git a/src/main/resources/youtube/translations/ko-rKR/strings.xml b/patches/src/main/resources/youtube/translations/ko-rKR/strings.xml similarity index 97% rename from src/main/resources/youtube/translations/ko-rKR/strings.xml rename to patches/src/main/resources/youtube/translations/ko-rKR/strings.xml index 17f807f08..d7fc61928 100644 --- a/src/main/resources/youtube/translations/ko-rKR/strings.xml +++ b/patches/src/main/resources/youtube/translations/ko-rKR/strings.xml @@ -336,7 +336,7 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요." 스낵바(팝업 메시지바)가 숨겨집니다. 스낵바(팝업 메시지바)가 표시됩니다. 시청 경고 다이얼로그 제거하기 - "시청 경고 다이얼로그를 제거합니다.\n• 이 설정은 다이얼로그를 자동으로 허용하기만 하며 연령 제한(성인인증 절차)을 우회할 수 없습니다.\n• 즉, 성인인증이 필요한 동영상에서 인증을 하려 할 때, 휴대폰 번호가 필요하다고 알려주는 소형 팝업창(다이얼로그) 없이 바로 휴대폰 번호 인증 페이지가 표시됩니다.\n• '당신은 혼자가 아닙니다' 페이지에서 '확인하기' 버튼이 표시되지 않는다면 이 설정이 아닌 플레이어 설정에서 '정보 패널 숨기기'를 비활성화해야 합니다." + "• 이 설정은 다이얼로그를 자동으로 허용하기만 하며 연령 제한(성인인증 절차)을 우회할 수 없습니다.\n• 즉, 성인인증이 필요한 동영상에서 인증을 하려 할 때, 휴대폰 번호가 필요하다고 알려주는 소형 팝업창(다이얼로그) 없이 바로 휴대폰 번호 인증 페이지가 표시됩니다.\n• '당신은 혼자가 아닙니다' 페이지는 제거할 수 없으며, 해당 페이지에서 '확인하기' 버튼이 표시되지 않는다면 이 설정이 아닌 플레이어 설정에서 '정보 패널 숨기기'를 비활성화해야 합니다." 레이아웃 변경하기 기기 기본값 사용 @@ -354,7 +354,7 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요." 18.05.40 - 이전 댓글 입력 상자로 복원합니다. 18.17.43 - 이전 플레이어 패널 구성요소로 복원합니다. 18.33.40 - 이전 Shorts 액션바로 복원합니다. - 18.38.45 - 이전 기본 동영상 화질 적용 방식을 복원합니다. + 18.38.45 - 이전 기본 동영상 화질 적용 방식으로 복원합니다. 18.48.39 - \'조회수\' & \'좋아요\'의 실시간 업데이트를 비활성화합니다. 계정 메뉴 @@ -662,9 +662,9 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요." 줌 오버레이 숨기기 줌 오버레이가 숨겨집니다. 줌 오버레이가 표시됩니다. - 동영상 부제목 제거하기 - "동영상 부제목에서 '#', '모금 행사', '쇼핑', '제품'과 같은 문구가 숨겨집니다." - "동영상 부제목에서 '#', '모금 행사', '쇼핑', '제품'과 같은 문구가 표시됩니다." + 동영상 제목 하단 정리하기 + "제목 하단에서 다음 문구들이 숨겨집니다:\n#(해시태그), 모금 행사, 쇼핑, n개 제품,\n인기 급상승 동영상 #순위, 회원 전용 ..." + "제목 하단에서 다음 문구들이 표시됩니다:\n#(해시태그), 모금 행사, 쇼핑, n개 제품,\n인기 급상승 동영상 #순위, 회원 전용 ..." 액션 버튼 플레이어 하단에 있는 액션 버튼을 숨기거나 표시할 수 있습니다. @@ -958,9 +958,26 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요." 화이트리스트 버튼 표시하기 버튼을 눌러서 화이트리스트 다이얼로그를 열 수 있습니다. 길게 누르면 화이트리스트 설정 다이얼로그가 열립니다. - 시간순 재생목록 버튼 표시하기 - "버튼을 눌러서 채널에서 가장 오래된 동영상부터 최신 동영상까지 모든 동영상의 재생목록을 생성할 수 있습니다. -길게 누르면 생성이 취소됩니다." + 전체 재생 버튼 표시하기 + "버튼을 눌러서 채널의 전체 동영상 재생목록을 생성할 수 있습니다. +길게 누르면 생성이 취소됩니다. + +알림: +• 실시간 스트림에서는 작동되지 않을 수 있습니다." + 재생목록 생성 유형 설정 + 전체 콘텐츠 (최신순) + 전체 콘텐츠 (인기순) + 동영상만 (최신순) + 동영상만 (인기순) + Shorts만 (최신순) + Shorts만 (인기순) + 실시간 스트림만 (최신순) + 실시간 스트림만 (인기순) + \'회원 전용\' 전체 콘텐츠 + \'회원 전용\' 동영상 + \'회원 전용\' Shorts + \'회원 전용\' 실시간 스트림 + 채널 ID가 일치하지 않아 재생목록을 생성할 수 없습니다. 채널 화이트리스트 화이트리스트에 추가된 채널을 확인 또는 제거할 수 있습니다. \'%1$s\' 채널을 %2$s 화이트리스트에 추가하였습니다. @@ -1041,12 +1058,12 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요." 롤링 넘버 애니메이션 비활성화하기 다음 롤링 넘버 애니메이션을 비활성화합니다.\n• 조회수, 시청자 수 롤링 애니메이션 (플레이어 하단)\n• 좋아요 수, 조회수 롤링 애니메이션 (동영상 설명) 다음 롤링 넘버 애니메이션을 활성화합니다.\n• 조회수, 시청자 수 롤링 애니메이션 (플레이어 하단)\n• 좋아요 수, 조회수 롤링 애니메이션 (동영상 설명) - AI-generated video summary 섹션 숨기기 - AI-generated video summary 섹션이 숨겨집니다. - AI-generated video summary 섹션이 표시됩니다. + AI 생성 동영상 요약 섹션 숨기기 + AI 생성 동영상 요약 섹션이 숨겨집니다. + AI 생성 동영상 요약 섹션이 표시됩니다. 속성 섹션 숨기기 - 게임 섹션, 음악 섹션 그리고 동영상 속 장소 섹션이 숨겨집니다. - 게임 섹션, 음악 섹션 그리고 동영상 속 장소 섹션이 표시됩니다. + 게임, 음악, 동영상 속 장소 그리고 언급된 인물 섹션이 숨겨집니다. + 게임, 음악, 동영상 속 장소 그리고 언급된 인물 섹션이 표시됩니다. 챕터 섹션 숨기기 챕터 섹션이 숨겨집니다. 챕터 섹션이 표시됩니다. @@ -1330,14 +1347,14 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요." 동영상 재생 속도 값을 변경할 때마다 저장합니다. 동영상 재생 속도 값을 변경할 때마다 저장하지 않습니다. 팝업 메시지 표시하기 - 기본 동영상 재생 속도 값으로 변경되었을 때, 팝업 메시지를 표시합니다. - 기본 동영상 재생 속도 값으로 변경되었을 때, 팝업 메시지를 표시하지 않습니다. + 기본 동영상 재생 속도 값을 변경하였을 때, 팝업 메시지를 표시합니다. + 기본 동영상 재생 속도 값을 변경하였을 때, 팝업 메시지를 표시하지 않습니다. 동영상 화질 저장 활성화하기 동영상 화질 값을 변경할 때마다 저장합니다. 동영상 화질 값을 변경할 때마다 저장하지 않습니다. 팝업 메시지 표시하기 - 기본 동영상 화질 값으로 변경되었을 때, 팝업 메시지를 표시합니다. - 기본 동영상 화질 값으로 변경되었을 때, 팝업 메시지를 표시하지 않습니다. + 기본 동영상 화질 값을 변경하였을 때, 팝업 메시지를 표시합니다. + 기본 동영상 화질 값을 변경하였을 때, 팝업 메시지를 표시하지 않습니다. 이전 동영상 화질 설정 메뉴 활성화하기 이전 동영상 화질 설정 메뉴를 활성화합니다. 이전 동영상 화질 설정을 비활성화합니다. @@ -1352,7 +1369,6 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요." 미리 로드된 버퍼를 건너뛰었습니다. 미리 로드된 버퍼 건너뛰기 "동영상을 시작할 때, 미리 로드된 버퍼를 건너뛰어 기본 동영상 화질 적용 지연을 우회합니다. - • 동영상이 시작되면 약 0.3초정도의 지연이 발생하지만 기본 동영상 화질이 즉시 적용됩니다. • HDR 동영상, 실시간 스트림, 15초 미만의 동영상에는 적용되지 않습니다." 이 설정을 활성화하면 동영상 재생 문제가 발생할 수 있습니다. @@ -1361,7 +1377,8 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요." 팝업 메시지를 표시하지 않습니다. 기기 크기 정보 변경하기 "기기 크기 정보를 최대값으로 변경합니다. -높은 기기 크기 정보가 필요한 일부 동영상에서는 고화질 동영상 값이 잠금 해제될 수 있지만 모든 동영상에는 적용되지 않습니다." +• 높은 기기 크기 정보가 필요한 일부 동영상에서는 고화질 동영상 값이 잠금 해제될 수 있지만 모든 동영상에는 적용되지 않습니다. +• 일부 스마트폰에서 이 설정을 활성화하면, 동영상 재생이 끊기거나 배터리 수명이 단축될 수 있으며 알려지지 않은 문제점도 발생할 수 있습니다." VP9 코덱 비활성화하기 "VP9 코덱을 비활성화합니다. • 재생 문제가 없는 계정이거나 iOS 클라이언트만 AV1 코덱을 지원하고 나머지 클라이언트는 VP9 코덱까지만 지원하기 때문에 iOS만 4K 동영상까지 재생될 수 있고, 나머지는 1080p까지 재생될 수 있습니다. @@ -1372,7 +1389,7 @@ DeArrow에 대해 자세히 알아보려면 여기를 누르세요." AV1 코덱을 VP9 코덱으로 변경합니다. AV1 코덱 응답 거부하기 "AV1 코덱 응답을 강제로 거부합니다. -약 20초정도의 버퍼링 후에 다른 코덱으로 전환됩니다." +• 약 20초정도의 버퍼링 후에 다른 코덱으로 전환됩니다." Fallback 프로세스로 인해 약 20초정도의 버퍼링이 발생합니다. 기본 동영상 재생 속도 값을 %s으로 변경합니다. 모바일 네트워크 이용 시 기본 동영상 화질 값을 %s로 변경합니다. @@ -1688,30 +1705,23 @@ API Key를 발급받는 방법을 보려면 여기를 누르세요." 이 설정을 비활성화하면 동영상 재생 문제가 발생할 수 있습니다. 기본 클라이언트 iOS - Android - Android Creator - Android Embedded Player - Android TestSuite Android TV Android VR - TV HTML5 - Web 알려진 문제점 - "• 영화 또는 회원 전용 동영상과 같은 유료 동영상이 재생되지 않을 수 있습니다.\n• 일부 실시간 스트림이 처음부터 재생될 수 있습니다.\n• 동영상이 1초 일찍 종료될 수 있습니다.\n• OPUS 코덱이 지원되지 않을 수 있습니다." - "• 동영상이 1초 일찍 종료될 수 있습니다. -• OPUS 코덱이 지원되지 않을 수 있습니다." + "• 일부 실시간 스트림이 처음부터 재생될 수 있습니다. +• 동영상이 1초 일찍 종료될 수 있습니다." + • 동영상이 1초 일찍 종료될 수 있습니다. "• 오디오 트랙 메뉴가 표시되지 않습니다.\n• 안정적인 볼륨 메뉴가 비활성화된 채로 잠겨있습니다." "• 오디오 트랙 메뉴가 표시되지 않습니다.\n• 안정적인 볼륨 메뉴가 비활성화된 채로 잠겨있습니다." - • 동영상이 재생되지 않을 수 있습니다. - iOS 호환성 모드 - 영화, 회원 전용 동영상과 같은 유료 동영상 또는 실시간 스트림이 아닌 경우에만 iOS 클라이언트로 변경됩니다. - 항상 iOS 클라이언트로 변경됩니다. iOS AVC (H.264) 강제로 활성화하기 iOS 동영상 코덱을 AVC (H.264)로 활성화합니다.\n\n• 일부 VP9 코덱 동영상에서 제거되었던 화질 값들이 표시될 수 있습니다.\n• 최대 화질 값이 1080p이므로 초고화질 동영상을 재생할 수 없습니다.\n• HDR 동영상을 재생할 수 없습니다 iOS 동영상 코덱을 AVC (H.264), VP9 또는 AV1으로 활성화합니다.\n\n• 예전에 업로드된 동영상을 재생했는데 VP9 코덱 응답을 받았을 경우, 일부 화질 값들이 제거되어 360p와 1080p(Premium 기능)만 선택할 수 있거나 화질 메뉴를 선택할 수 없을 수 있습니다. "이 설정을 활성화하면 배터리 수명이 향상되고 재생 끊김 현상이 해결될 수 있습니다. AVC (H.264)의 최대 화질 값은 1080p이며 동영상을 재생하면 VP9 또는 AV1보다 더 많은 모바일 데이터가 사용되오니 주의하세요." + iOS 실시간 스트림 재생 건너뛰기 + iOS 클라이언트가 실시간 스트림 재생에서 사용되지 않습니다. + iOS 클라이언트가 실시간 스트림 재생에서 사용됩니다. 전문 통계에서 표시하기 \'스트리밍 데이터를 가져오는 데 사용되는 클라이언트\'가 전문 통계에서 표시됩니다. \'스트리밍 데이터를 가져오는 데 사용되는 클라이언트\'가 전문 통계에서 숨겨집니다. diff --git a/src/main/resources/youtube/translations/pl-rPL/strings.xml b/patches/src/main/resources/youtube/translations/pl-rPL/strings.xml similarity index 98% rename from src/main/resources/youtube/translations/pl-rPL/strings.xml rename to patches/src/main/resources/youtube/translations/pl-rPL/strings.xml index d31126cf3..15b5b6370 100644 --- a/src/main/resources/youtube/translations/pl-rPL/strings.xml +++ b/patches/src/main/resources/youtube/translations/pl-rPL/strings.xml @@ -958,9 +958,23 @@ Stuknij i przytrzymaj, by zmienić prędkość odtwarzania na 1.0x. Stuknij i pr Przycisk od białej listy Stuknij, by otworzyć okno białej listy. Stuknij i przytrzymaj, by otworzyć okno ustawień białej listy. - Przycisk od czasowo uporządkowanych playlist - "Stuknij, by wygenerować playlistę ze wszystkimi filmami z kanału od najstarszego do najnowszego. -Stuknij i przytrzymaj, by cofnąć generowanie playlisty." + Przycisk od odtwarzania wszystkiego + "Stuknij, by wygenerować playlistę ze wszystkimi filmikami z kanału. +Stuknij i przytrzymaj, by to cofnąć." + Tryb generowania playlisty + Cała zawartość (sortowanie po czasie) + Cała zawartość (sortowanie po popularności) + Tylko filmy (sortowanie po czasie) + Tylko filmy (sortowanie po popularności) + Tylko Shortsy (sortowanie po czasie) + Tylko Shortsy (sortowanie po popularności) + Tylko streamowane filmy (sortowanie po czasie) + Tylko streamowane filmy (sortowanie po popularności) + Cała zawartość dla wspierających + Tylko filmy dla wspierających + Tylko shortsy dla wspierających + Tylko transmisje na żywo dla wspierających + Nie można wygenerować playlisty z powodu niedopasowania ID kanału. Biała lista kanałów Sprawdź lub usuń listę kanałów dodanych do białej listy Kanał \'%1$s\' został dodany do białej listy %2$s. @@ -1684,35 +1698,25 @@ Kontynuuj i wyłącz optymalizację baterii." Wyłączenie tej opcji może spowodować problemy z odtwarzaniem filmów. Domyślny klient iOS - Android - Android Creator - Wbudowany odtwarzacz Androida - Klient Testowy Androida Android TV Android VR - TV HTML5 - Przeglądarka Efekty uboczne oszukiwania - "• 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 audio opus może nie być wspierany" - "• Filmy mogą kończyć się o 1 sekundę wcześniej -• Kodek audio opus może nie być wspierany" + "• Transmisje na żywo rozpoczynają się od początku +• Filmy mogą kończyć się o 1 sekundę wcześniej" + • Filmy mogą kończyć się o 1 sekundę wcześniej "• 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 mogą się nie odtwarzać - Tryb kompatybilności iOS - Z wyjątkiem filmów kinowych, płatnych i transmisji na żywo - Zawsze Wymuś kodek iOS AVC (H.264) Włączone Wyłączone "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." + Pomiń iOS dla odtwarzania transmisji na żywo + Włączone + Wyłączone Informacja w statystykach dla nerdów Widoczna Ukryta diff --git a/src/main/resources/youtube/translations/pt-rBR/strings.xml b/patches/src/main/resources/youtube/translations/pt-rBR/strings.xml similarity index 98% rename from src/main/resources/youtube/translations/pt-rBR/strings.xml rename to patches/src/main/resources/youtube/translations/pt-rBR/strings.xml index 326963a6b..58dbdb437 100644 --- a/src/main/resources/youtube/translations/pt-rBR/strings.xml +++ b/patches/src/main/resources/youtube/translations/pt-rBR/strings.xml @@ -958,9 +958,26 @@ Toque e segure novamente para redefinir para a velocidade padrão." Exibir botão de lista branca 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 de reprodução ordenada por tempo - "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." + Mostrar botão de reproduzir tudo + "Toque para gerar uma playlist de todos os vídeos do canal. +Toque e segure para desfazer. + +Informação: +• Pode não funcionar em transmissões ao vivo." + Gerar modo playlist + Todo o conteúdo (Classificar por tempo) + Todo o conteúdo (Classificar por popular) + Somente vídeos (Classificar por tempo) + Somente vídeos (Classificar por popular) + Somente Shorts (Classificar por tempo) + Somente Shorts (Classificar por popular) + Somente vídeos transmitidos (Classificar por tempo) + Somente vídeos transmitidos (Classificar por popular) + Conteúdo somente para todos os membros + Vídeos somente para membros + Shorts somente para membros + Transmissões ao vivo somente para membros + Não é possível gerar a lista de reprodução devido a incompatibilidade de id do canal. Lista branca de canais Verifique ou remova a lista de canais adicionados à lista branca. O canal \'%1$s\' foi adicionado à lista branca de %2$s. @@ -1679,25 +1696,22 @@ Toque no botão continuar e desative as otimizações da bateria." Desativar esta configuração pode causar problemas de reprodução de vídeo. Cliente padrão iOS - Android - Criador Android - Reprodutor Incorporado Android - Suite de teste Android Android TV Android VR - TV HTML5 - Web Efeitos colaterais da falsificação "• Filmes ou vídeos pagos podem não reproduzir." + • Os vídeos podem acabar 1 segundo antes. "• O menu de faixa de áudio está faltando." "• O menu de faixa de áudio está faltando." - • O vídeo pode não reproduzir. Forçar iOS AVC (H.264) O codec de vídeo do iOS é AVC (H.264). O codec de vídeo do iOS é AVC (H.264), VP9 ou AV1. "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." + Pular a reprodução ao vivo do iOS + O cliente iOS não é usado para reprodução de transmissões ao vivo. + O cliente iOS é usado para reprodução de transmissões ao vivo. Exibir em Estatísticas para nerds O cliente usado para buscar dados de streaming é mostrado em Estatísticas para nerds. O cliente usado para buscar dados de streaming está oculto em Estatísticas para nerds. diff --git a/src/main/resources/youtube/translations/ru-rRU/strings.xml b/patches/src/main/resources/youtube/translations/ru-rRU/strings.xml similarity index 98% rename from src/main/resources/youtube/translations/ru-rRU/strings.xml rename to patches/src/main/resources/youtube/translations/ru-rRU/strings.xml index 8ff013892..0d389e3ff 100644 --- a/src/main/resources/youtube/translations/ru-rRU/strings.xml +++ b/patches/src/main/resources/youtube/translations/ru-rRU/strings.xml @@ -969,9 +969,26 @@ Shorts Кнопка \"Белый список\" Нажать - Открыть \"Белый список\". Нажать и удерживать - Открыть настройки \"Белый список\". - Кнопка создания плейлиста канала - "Нажмите, чтобы создать плейлист всех видео канала. -Нажмите и удерживайте, чтобы отменить действие." + Кнопка воспроизвести все + "Коснитесь, чтобы создать список воспроизведения всех видео из канала. +Коснитесь и удерживайте, для отмены. + +Инфо: +• Может не работать в трансляциях." + Сортировка создаваемого списка + Все (по времени) + Все (по популярности) + Только видео (по времени) + Только видео (по популярности) + Только Shorts (по времени) + Только Shorts (по популярности) + Только потоковое видео (по времени) + Только потоковое видео (по популярности) + Только содержимое всех участников + Только видео участников + Только Shorts участников + Только потоковые видео участников + Не удалось создать плейлист из-за несоответствия id канала. \"Белый список\" канала Управление каналами в белом списке. Канал \'%1$s\' добавлен в белый список %2$s. @@ -1704,25 +1721,22 @@ Shorts Отключение этой настройки вызовет проблемы с воспроизведением видео. Клиент по умолчанию iOS - Android - Android Создатель - Встроенный Android плеер - Android Тестовый набор Android TV Android VR - TV HTML5 - Веб Эффекты от подмены "• Фильмы или платные видео могут не проигрываться." + • Видео может закончиться 1 секунду ранее. "• Меню \"Звуковая дорожка\" не доступно." "• Меню \"Звуковая дорожка\" VR не доступно." - • Видео может не воспроизводиться. Принудительно подмена как iOS, AVC (H.264) Видео кодек подмены как iOS - AVC (H.264). Видео кодек подмены как iOS - VP9 или AV1. "Включение - может улучшить время работы батареи и исправить задержки воспроизведения. AVC (H.264) имеет максимальное разрешение 1080p, и будет использовать больше интернет данных, чем VP9 или AV1." + Пропустить трансляцию при подмене клиента как iOS + Пропуск трансляции включен. + Пропуск трансляции отключен. Показывать в Статистике для сисадминов Клиент, используемый для получения данных потока, отображается в Статистике для сисадминов. Клиент, используемый для получения данных потока, скрыт в Статистике для сисадминов. diff --git a/src/main/resources/youtube/translations/tr-rTR/strings.xml b/patches/src/main/resources/youtube/translations/tr-rTR/strings.xml similarity index 99% rename from src/main/resources/youtube/translations/tr-rTR/strings.xml rename to patches/src/main/resources/youtube/translations/tr-rTR/strings.xml index 46b9a0976..c59b0eb91 100644 --- a/src/main/resources/youtube/translations/tr-rTR/strings.xml +++ b/patches/src/main/resources/youtube/translations/tr-rTR/strings.xml @@ -787,9 +787,6 @@ Oynatma hızını 1,0x'e sıfırlamak için dokunun ve basılı tutun. Varsayıl Beyaz liste 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. - Zaman Sıralı Çalma Listesi Düğmesini göster - "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." Kanal beyaz listesi Beyaz listeye eklenen kanalların listesini kontrol edin veya kaldırın. \'%1$s\' kanalı \'%2$s\' beyaz listesine eklendi. diff --git a/src/main/resources/youtube/translations/uk-rUA/strings.xml b/patches/src/main/resources/youtube/translations/uk-rUA/strings.xml similarity index 98% rename from src/main/resources/youtube/translations/uk-rUA/strings.xml rename to patches/src/main/resources/youtube/translations/uk-rUA/strings.xml index f821c148d..ff56c06ac 100644 --- a/src/main/resources/youtube/translations/uk-rUA/strings.xml +++ b/patches/src/main/resources/youtube/translations/uk-rUA/strings.xml @@ -957,9 +957,26 @@ Показувати кнопку Білого списку Натисніть, щоб відкрити діалог білого списку. Натисніть і утримуйте, щоб відкрити діалог налаштування білого списку. - Показувати кнопку впорядкованого за часом списку відтворення - "Натисніть, щоб згенерувати список відтворення з усіх відео з каналу від найстарішого до найновішого. -Натисніть і утримуйте, щоб скасувати." + Показувати кнопку Відтворити все + "Натисніть, щоб згенерувати список відтворення з усіх відео з каналу. +Натисніть і отримуйте, щоб відмінити. + +Інформація: +• Може не працювати на прямих трансляціях." + Режим генерування списку відтворення + Весь контент (Сортування за часом) + Весь контент (Сортування за популярністю) + Лише відео (Сортування за часом) + Лише відео (Сортування за популярністю) + Лише YouTube Shorts (Сортування за часом) + Лише YouTube Shorts (Сортування за популярністю) + Лише відео Наживо (Сортування за часом) + Лише відео Наживо (Сортування за популярністю) + Контент тільки всіх спонсорів + Відео тільки спонсорів + YouTube Shots тільки спонсорів + Прямі трансляції тільки спонсорів + Не вдалося згенерувати список відтворення через невідповідність ідентифікатора каналу. Білий список каналів Перевірити чи вилучити список каналів, доданих до білого списку. Канал \'%1$s\' додано до білого списку %2$s. @@ -1677,30 +1694,25 @@ Вимикання цього налаштування може призвести до проблем відтворення відео. Основний клієнт iOS - Android - Розробник Android - Вбудований плеєр Android - Тестовий Android Android TV Android VR - TV HTML5 - Web Побічні ефекти імітування - "• Фільми чи платні відео можуть не відтворюватися. -• Прямі трансляції починаються з початку. -• Відео можуть закінчуватися на 1 секунду раніше. -• Немає аудіокодека opus." + "• Прямі трансляції починаються з початку. +• Відео можуть закінчуватися на 1 секунду раніше." + • Відео можуть закінчуватися на 1 секунду раніше. "• Меню звукової доріжки відсутнє. • Стабілізація гучності недоступна." "• Меню звукової доріжки відсутнє. • Стабілізація гучності недоступна." - • Відео може не відтворюватися. Примусово AVC (H.264) iOS AVC (H.264) кодек відео iOS. AVC (H.264), VP9, чи AV1 кодек відео iOS. "Увімкнення може зменшити споживання акумулятора та усунути затинання при відтворенні. AVC (H.264) має максимальну роздільну здатність 1080p, а для відтворення відео використовується більше інтернет-даних, ніж VP9 або AV1." + Пропускати відтворення прямих трансляцій iOS + Клієнт iOS не використовується для відтворення прямих трансляцій. + Клієнт iOS використовується для відтворення прямих трансляцій. Показувати в Статистика для сисадмінів Клієнт, що використовується для отримання даних трансляції показується у Статистика для сисадмінів. Клієнт, що використовується для отримання даних трансляції приховано у Статистика для сисадмінів. diff --git a/src/main/resources/youtube/translations/vi-rVN/strings.xml b/patches/src/main/resources/youtube/translations/vi-rVN/strings.xml similarity index 81% rename from src/main/resources/youtube/translations/vi-rVN/strings.xml rename to patches/src/main/resources/youtube/translations/vi-rVN/strings.xml index c9475b9f8..54e00d81b 100644 --- a/src/main/resources/youtube/translations/vi-rVN/strings.xml +++ b/patches/src/main/resources/youtube/translations/vi-rVN/strings.xml @@ -6,7 +6,7 @@ ReVanced Extended Tìm kiếm trong %s - Đặt lại về giá trị mặc định. + Đã đặt lại giá trị về mặc định. Tính năng thử nghiệm Bạn có muốn tiếp tục không? 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 @@ -30,15 +30,15 @@ Ẩn kệ Sản phẩm Kệ Sản phẩm đã ẩn. Kệ Sản phẩm được hiển thị. - Ẩn kệ cửa hàng bên dưới trình phát - Đã ẩn kệ cửa hàng. - Đã hiện kệ cửa hàng. - Ẩn nhãn quảng cáo được tài trợ - Nhãn Nội dung được trả tiền để quảng cáo đã ẩn. - Nhãn Nội dung được trả tiền để quảng cáo được hiển thị. + Ẩn kệ cửa hàng + Kệ cửa hàng được hiển thị bên dưới trình phát. + Kệ cửa hàng đã ẩn bên dưới trình phát. + Ẩn nhãn nội dung được trả tiền để quảng cáo + Nhãn nội dung được trả tiền để quảng cáo đã ẩn. + Nhãn nội dung được trả tiền để quảng cáo được hiển thị. Ẩn biểu ngữ thông báo khuyến mãi Biểu ngữ thông báo khuyến mãi đã ẩ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 được hiển thị. Ẩn thẻ được tài trợ Thẻ được tài trợ đã ẩn. Thẻ được tài trợ được hiển thị. @@ -59,7 +59,7 @@ Thẻ Trang chủ Thẻ Kênh đăng ký Thẻ Bạn - Danh sách phát của trình phát, đề xuất + Danh sách phát của trình phát, video đề xuất Kết quả tìm kiếm Hình thu nhỏ gốc DeArrow & Hình thu nhỏ gốc @@ -75,27 +75,27 @@ Nhấn vào đây để tìm hiểu thêm về DeArrow." Hiển thị thông báo ngắn nếu API DeArrow không khả dụng. Không hiện thông báo ngắn nếu API DeArrow không khả dụng. Điểm cuối API DeArrow - URL của điểm cuối của bộ nhớ đệm hình thu nhỏ DeArrow. + URL điểm cuối của bộ nhớ đệm hình thu nhỏ DeArrow. URL của API DeArrow không hợp lệ. Giới thiệu về 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. Hình thu nhỏ nhanh - Đ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. + Đ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 video phát trực tiếp, sắp diễn ra hoặc đã rất cũ có thể không hiển thị hình thu nhỏ. Đang sử dụng ảnh tĩnh chất lượng cao làm hình thu nhỏ video. Thời điểm để lấy ảnh tĩnh từ video Đầu video Giữa video Cuối video - 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. (mã trạng thái: %s) DeArrow tạm thời không khả dụng. - Hạn chế về hình ảnh do khu vực - Vượt qua hạn chế + Giới hạn khu vực cho hình ảnh + Bỏ qua giới hạn khu vực Sử dụng máy chủ hình ảnh yt4.ggpht.com. 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. Bảng tin - Ẩn thẻ album + Ẩn Đĩa nhạc Đĩa nhạc đã ẩn khỏi kết quả tìm kiếm. Đĩa nhạc được hiển thị trong kết quả tìm kiếm. Ẩn các kệ được cá nhân hoá @@ -103,31 +103,31 @@ Nhấn vào đây để tìm hiểu thêm về DeArrow." • Tin nổi bật • Tiếp tục xem -• Khám phá thêm kênh +• Khám phá các chủ đề khác • Nghe lại • Mua sắm • Xem lại" Ẩn kệ danh mục được đề xuất - Kệ danh mục đề xuất đã ẩn. - Kệ danh mục đề xuất được hiển thị. + Kệ danh mục được đề xuất đã ẩn. + Kệ danh mục được đề xuất được hiển thị. Ẩn bảng giới thiệu mở rộng Bảng giới thiệu mở rộng đã ẩn bên dưới video. Bảng giới thiệu mở rộng được hiển thị bên dưới video. Ẩn kệ mở rộng Kệ mở rộng đã ẩn. - Kệ mở rộng đã hiển thị. + Kệ mở rộng được hiển thị. Ẩn nút Phụ đề Nút Phụ đề đã ẩn. Nút Phụ đề được hiển thị. Ẩn thanh tìm kiếm - Thanh tìm kiếm đã bị ẩn. + Thanh tìm kiếm đã ẩn. Thanh tìm kiếm được hiển thị. Ẩn khảo sát Khảo sát đã ẩn. Khảo sát được hiển thị. Ẩn nút nổi - Nút nổi như nút \"Điều chỉnh trang chủ của bạn\" đã bị ẩn. - 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\" đã ẩn. + Nút nổi như nút \"Điều chỉnh trang chủ của bạn\" được hiển thị. Ẩn kệ Hình ảnh từ web Kệ Hình ảnh từ web đã ẩn khỏi kết quả tìm kiếm. Kệ Hình ảnh từ web được hiển thị trong kết quả tìm kiếm. @@ -140,21 +140,21 @@ Nhấn vào đây để tìm hiểu thêm về DeArrow." Ẩn Danh sách kết hợp Danh sách kết hợp đã ẩn. Danh sách kết hợp được hiển thị. - Ẩn kệ Phim và chương trình truyền hình - Kệ phim và chương trình truyền hình đã ẩn. - Kệ phim và chương trình truyền hình được hiển thị. + Ẩn kệ Phim và chương trình + Kệ phim và chương trình đã ẩn. + Kệ phim và chương trình được hiển thị. Ẩn nút Thông báo cho tôi - Nút Thông báo cho tôi đã ẩn bên dưới video sắp diễn ra. - 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út \"Thông báo cho tôi\" được hiển thị bên dưới video sắp diễn ra. Ẩn kệ Chơi game trên YouTube Kệ Chơi game trên YouTube đã ẩn. Kệ Chơi game trên YouTube được hiển thị. Ẩn nút Hiện thêm Nút Hiện thêm đã ẩn. Nút Hiện thêm được hiển thị. - Ẩn danh sách cuộn Kênh đăng ký - Danh sách cuộn Kênh đăng ký đã ẩn. - Danh sách cuộn Kênh đăng ký đã hiển thị. + Ẩn băng chuyền Kênh đăng ký + Băng chuyền Kênh đăng ký đã ẩn. + Băng chuyền Kênh đăng ký được hiển thị. Ẩn kệ bán vé Kệ bán vé đã ẩn. Kệ bán vé được hiển thị. @@ -173,17 +173,17 @@ Nhấn vào đây để tìm hiểu thêm về DeArrow." Hồ sơ kênh Ẩn hoặc hiển thị các thành phần trong hồ sơ kênh. - Bộ lọc thẻ trên kênh - Bộ lọc thẻ trên kênh đã bật. - Bộ lọc thẻ trên kênh đã tắt. + Bộ lọc thẻ kênh + Bộ lọc thẻ kênh đã bật. + Bộ lọc thẻ kênh đã tắt. Cài đặt bộ lọc - 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. + Nhập tên các thẻ kênh mà bạn muốn lọc được phân cách bằng dòng. "Shorts Danh sách phát Cửa hàng" Ẩn nút Chuyển đến cửa hàng - Đã ẩn nút Chuyển đến cửa hàng. - Đã hiện nút Chuyển đến cửa hàng. + Nút Chuyển đến cửa hàng đã ẩn. + Nút Chuyển đến cửa hàng được hiển thị. Ẩn kệ ghi nhận hội viên của kênh Kệ ghi nhận hội viên của kênh đã ẩn. Kệ ghi nhận hội viên của kênh được hiển thị. @@ -236,7 +236,7 @@ Cửa hà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)." - Giới thiệu về lọc từ khoá + Giới thiệu về bộ lọc từ khoá "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ế: @@ -244,32 +244,32 @@ Bộ lọc có phân biệt chữ hoa chữ thường, vì vậy bạn cần 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." Khớp toàn bộ từ - 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ông thể sử dụng từ khoá: %s. + Đặ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> + Từ khoá không hợp lệ: %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. Từ khóa sẽ ẩn tất cả video: %s. Video được đề xuất - Ẩn video đề xuất - "Ẩn các video đề xuất sau: + Ẩn video được đề xuất + "Ẩn các video được đề 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 có lượt xem thấ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 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ủ. Bộ lọc số lượt xem Ẩn video trên thẻ Trang chủ theo số lượt xem - Các Video trên thẻ Trang chủ đã đượ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. Các Video trên thẻ Trang chủ không được lọc theo số lượt xem đã đặt. - Ẩn kết quả tìm kiếm theo lượt xem - Kết quả tìm kiếm đã được lọc theo số lượt xem đã đặt. - Kết quả tìm kiếm không được lọc theo số lượt xem đã đặt. + Ẩn các video trong kết quả tìm kiếm theo số lượt xem + Các video trong kết quả tìm kiếm được lọc theo số lượt xem đã đặt. + Các video trong kết quả tìm kiếm không được lọc theo số lượt xem đã đặt. Ẩn video trên thẻ Kênh đăng ký theo số lượt xem - Các Video trên thẻ Kênh đăng ký đã được lọc theo số lượt xem đã đặt. - 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. + Các video trên thẻ Kênh đăng ký không được lọc theo số lượt xem đã đặt. Cao hơn Nhập số lượt xem. Video có số lượt xem cao hơn mức này sẽ bị ẩn. Thấp hơn @@ -277,14 +277,14 @@ Bộ lọc có phân biệt chữ hoa chữ thường, vì vậy bạn cần nh Ký tự đại diện số 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. N -> 1 000\nTr -> 1 000 000\nT -> 1 000 000 000\nlượt xem -> lượt xem - Về việc lọc theo số lượt xem - "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. + Về bộ lọc theo số lượt xem + "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." +• Không ẩn đối với các video Short. +• Các video có 0 lượt xem cũng không bị lọc." Ẩn các video có liên quan - Các video có liên quan đã bị ẩn. + Các video có liên quan đã ẩn. Các Video có liên quan được hiển thị. "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. @@ -320,18 +320,18 @@ Hạn chế: Nút Quay lại trên thanh công cụ có thể không hoạt đ Tắt tự động hiển thị phụ đề Tự động hiển thị phụ đề khi phát video có phụ đề đã tắt. Tự động hiển thị phụ đề khi phát video có phụ đề đã bật. - Vô hiệu hoá hoạt ảnh khi ứng dụng khởi chạy - Hoạt ảnh khi ứng dụng khởi chạy đã bị vô hiệu hoá. - Đã kích hoạt hoạt ảnh khởi động. - Màn hình tải màu gradient - Đã kích hoạt màn hình tải màu Gradient. - Đã vô hiệu hoá màn hình tải màu Gradient. + Tắt hoạt ảnh khi ứng dụng khởi chạy + Hoạt ảnh khi ứng dụng khởi chạy đã tắt. + Hoạt ảnh khi ứng dụng khởi chạy được bật. + Màn hình tải hiệu ứng gradient + Màn hình tải hiệu ứng gradient đã bật. + Màn hình tải hiệu ứng gradient đã tắt. Ẩ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. - Ẩn dải phân cách màu xám - Đã ẩn dải phân cách màu xám. - Đã hiện dải phân cách màu xám. + Nút Tìm kiếm bằng giọng nói đã ẩn khi tìm kiếm. + Nút Tìm kiếm bằng giọng nói được hiển thị khi tìm kiếm. + Ẩn các dải phân cách màu xám + Các dải phân cách màu xám đã ẩn. + Các dải phân cách màu xám được hiển thị. Ẩn thanh thông báo nhanh Thanh thông báo nhanh đã ẩn. Thanh thông báo nhanh được hiển thị. @@ -347,7 +347,7 @@ Tuỳ chọn này chỉ tự động chấp nhận hộp thoại cảnh báo, ch Máy tính bảng (Tối thiểu 600 dpi) Giả mạo phiên bản ứng dụng - Phiên bản đã được giả mạo + Phiên bản được giả mạo Phiên bản không được giả mạo "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. @@ -359,16 +359,16 @@ Nếu muốn tắt tính năng này sau đó, bạn nên xóa dữ liệu ứng Phiên bản giả mạo 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.17.43 - Khôi phục trình đơn Cài đặt trình phát kiểu 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 Trình đơn Tài khoản - Ẩ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. + Ẩn hoặc hiển thị các mục của trình đơn Tài khoản và thẻ Bạn. 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 và thẻ Bạn. -Một số thành phần có thể không bị ẩn." + "Ẩn các mục của trình đơn Tài khoản và thẻ Bạn. +Một số mục có thể không bị ẩn." Cài đặt bộ lọc 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. Ẩn tên hiển thị @@ -419,23 +419,23 @@ Một số thành phần có thể không bị ẩn." Hiện đại 2 Hiện đại 3 Thao tác nhấn đúp - "Đã kích hoạt thao tác nhấn đúp. + "Thao tác nhấn đúp được bật. • 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." - Đã vô hiệu hoá thao tác nhấn đúp. + Thao tác nhấn đúp đã tắt. Kéo và thả - Đã kích hoạt kéo và thả. - Đã vô hiệu kéo và thả. + Thao tác kéo và thả được bật. + Thao tác kéo và thả đã tắt. Ẩ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) - Đã hiện các nút Mở rộng và Đóng. + Các nút Mở rộng và Đóng đã ẩn.\nVuốt trình phát thu nhỏ để mở rộng hoặc đóng. + Các nút Mở rộng và Đóng được hiển thị. Ẩn các văn bản phụ - Đã ẩn các văn bản phụ. - Đã hiện các văn bản phụ. - Ẩ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 nút tua tới và tua lùi. + Các văn bản phụ đã ẩn. + Các văn bản phụ được hiển thị. + Ẩn các nút tua nhanh và tua lại + Các nút tua nhanh và tua lại đã ẩn. + Các nút tua nhanh và tua lại được hiển thị. Độ mờ lớp phủ 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ờ của lớp phủ trình phát thu nhỏ phải nằm trong khoảng 0 - 100. @@ -444,10 +444,10 @@ Một số thành phần có thể không bị ẩn." Ẩn hoặc hiển thị các thành phần trên Thanh điều hướng. Thanh điều hướng thu gọn Khoảng cách giữa các nút trên thanh điều hướng sẽ hẹp hơn. - 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 như mặc định. Ẩn nút Tạo - Đã ẩn nút Tạo. - Đã hiện nút Tạo. + Nút Tạo đã ẩn. + Nút Tạo được hiển thị. Ẩn nút Trang chủ Nút Trang chủ đã ẩn. Nút Trang chủ được hiển thị. @@ -455,21 +455,21 @@ Một số thành phần có thể không bị ẩn." Nút Bạn đã ẩn. Nút Bạn được hiển thị. Ẩn nút Thông báo - Đã ẩn nút Thông báo. - Đã hiện nút Thông báo. + Nút Thông báo đã ẩn. + Nút Thông báo được hiển thị. Ẩn nút Shorts - Đã ẩn nút Shorts. - Đã hiện nút Shorts. + Nút Shorts đã ẩn. + Nút Shorts được hiển thị. Ẩn nút Kênh đăng ký Nút Kênh đăng ký đã ẩn. Nút Kênh đăng ký được hiển thị. Ẩn tên các thẻ - Đã ẩn tên các thẻ. - Đã hiện tên các thẻ. + Tên các thẻ đã ẩn. + Tên các thẻ được hiển thị. Đổi vị trí nút Tạo và nút Thông báo "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." +Lưu ý: Bật tuỳ chọn này cũng sẽ ẩn các quảng cáo dạng video." Nút Tạo và nút Thông báo như mặc định. "Tắt tùy chọn này có thể tải thêm quảng cáo từ máy chủ. @@ -477,90 +477,90 @@ 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." Thanh điều hướng trong suốt - Thanh điều hướng đã được làm trong suốt. - Thanh điều hướng đã được hiển thị. + Thanh điều hướng trong suốt đang được áp dụng. + Thanh điều hướng mặc định đang được áp dụng. Ẩn Thanh điều hướng - Thanh điều hướng đã bị ẩn. - Thanh điều hướng đã được hiển thị. + Thanh điều hướng đã ẩn. + Thanh điều hướng được hiển thị. Trình đơn Cài đặt - Ẩn các thành phần của trình đơn Cài đặt YouTube. + Ẩn các mục của trình đơn Cài đặt YouTube. Ẩ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 Trung tâm dành cho gia đình. + Mục Trung tâm dành cho gia đình đã ẩn. + Mục Trung tâm dành cho gia đình được hiển thị. Ẩn mục Chung - Đã ẩn mục Chung. - Đã hiện mục Chung. + Mục Chung đã ẩn. + Mục Chung được hiển thị. Ẩn mục Tài khoản - Đã ẩn mục Tài khoản. - Đã hiện mục Tài khoản. + Mục Tài khoản đã ẩn. + Mục Tài khoản được hiển thị. Ẩn mục Tiết kiệm dữ liệu - Đã ẩn mục Tiết kiệm dữ liệu. - Đã hiện mục Tiết kiệm dữ liệu. + Mục Tiết kiệm dữ liệu đã ẩn. + Mục Tiết kiệm dữ liệu được hiển thị. Ẩn mục Tự động phát - Đã ẩn mục Tự động phát. - Đã hiện mục Tự động phát. + Mục Tự động phát đã ẩn. + Mục Tự động phát được hiển thị. Ẩ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 Lựa chọn ưu tiên về chất lượng video. + 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 được hiển thị. Ẩ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 Phát trong nền và nội dung tải xuống. + 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 được hiển thị. Ẩn mục Xem trên TV - Đã ẩn mục Xem trên TV. - Đã hiện mục Xem trên TV. + Mục Xem trên TV đã ẩn. + Mục Xem trên TV được hiển thị. Ẩ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 Quản lý toàn bộ nhật ký hoạt động. + 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 được hiển thị. Ẩn mục Dữ liệu của bạn trong Youtube - Đã ẩn mục Dữ liệu của bạn trong Youtube. - Đã hiện mục Dữ liệu của bạn trong Youtube. + Mục Dữ liệu của bạn trong Youtube đã ẩn. + Mục Dữ liệu của bạn trong Youtube được hiển thị. Ẩn mục Quyền riêng tư - Đã ẩn mục Quyền riêng tư. - Đã hiện mục Quyền riêng tư. + Mục Quyền riêng tư đã ẩn. + Mục Quyền riêng tư được hiển thị. Ẩ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 Thử nghiệm các tính năng mới. + 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 được hiển thị. Ẩ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. - Đã hiện mục Giao dịch mua và gói thành viê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 được hiển thị. Ẩ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 Lập hoá đơn và thanh toán. + Mục Lập hoá đơn và thanh toán đã ẩn. + Mục Lập hoá đơn và thanh toán được hiển thị. Ẩn mục Thông báo - Đã ẩn mục Thông báo. - Đã hiện mục Thông báo. + Mục Thông báo đã ẩn. + Mục Thông báo được hiển thị. Ẩn mục Ứng dụng đã kết nối - Đã ẩn mục Ứng dụng đã kết nối. - Đã hiện mục Ứng dụng đã kết nối. + Mục Ứng dụng đã kết nối đã ẩn. + Mục Ứng dụng đã kết nối được hiển thị. Ẩn mục Trò chuyện trực tiếp - Đã ẩn mục Trò chuyện trực tiếp. - Đã hiện mục Trò chuyện trực tiếp. + Mục Trò chuyện trực tiếp đã ẩn. + Mục Trò chuyện trực tiếp được hiển thị. Ẩn mục Phụ đề - Đã ẩn mục Phụ đề. - Đã hiện mục Phụ đề. + Mục Phụ đề đã ẩn. + Mục Phụ đề được hiển thị. Ẩn mục Hỗ trợ tiếp cận - Đã ẩn mục Hỗ trợ tiếp cận. - Đã hiện mục Hỗ trợ tiếp cận. + Mục Hỗ trợ tiếp cận đã ẩn. + Mục Hỗ trợ tiếp cận được hiển thị. Ẩn mục Giới thiệu - Đã ẩn mục Giới thiệu. - Đã hiện mục Hỗ trợ tiếp cận. + Mục Giới thiệu đã ẩn. + Mục Giới thiệu được hiển thị. Thanh công cụ Ẩ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. Thay đổi tiêu đề YouTube - Tiêu đề Premium được kích hoạt. - Tiêu đề Youtube mặc định. + Tiêu đề Premium được áp dụng. + Tiêu đề Youtube mặc định được áp dụng. Thanh tìm kiếm rộng - Đang áp dụng thanh tìm kiếm rộng. - Đang áp dụng thanh tìm kiếm mặc định. + Thanh tìm kiếm rộng đang được áp dụng. + Thanh tìm kiếm mặc định đang được áp dụng. Thanh tìm kiếm rộng với 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 sẽ ẩn tiêu đề YouTube. + Thanh tìm kiếm rộng được hiển thị cùng với tiêu đề YouTube. + Thanh tìm kiếm rộng sẽ làm ẩn tiêu đề YouTube. Thanh tìm kiếm rộng trên thẻ Bạn - "Bật cài đặt này sẽ làm vô hiệu hoá nút Cài đặt trong thẻ Bạn. + "Bật cài đặt này sẽ làm ẩn 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: +Trong trường hợp đó, vui lòng làm theo các bước sau để vào Cài đặt: Thẻ Bạn → Xem kênh → Trình đơn → Cài đặt" Ẩn nút Truyền Nút Truyền đã ẩn. @@ -575,17 +575,17 @@ Thẻ Bạn → Xem kênh → Trình đơn → Cài đặt" Hình thu nhỏ của từ khoá tìm kiếm đã ẩn khỏi lịch sử tìm kiếm. Hình thu nhỏ của từ khoá tìm kiếm được hiển thị trong lịch sử tìm kiếm. Ẩn nút tìm kiếm bằng hình ảnh - Nút tìm kiếm bằng hình ảnh đã bị ẩn. + Nút tìm kiếm bằng hình ảnh đã ẩn. Nút tìm kiếm bằng hình ảnh được hiển thị. Ẩ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. - Đã hiệ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. + Nút Tìm kiếm bằng giọng nói được hiển thị. Ẩn YouTube Doodles - YouTube Doodles đã bị ẩn. - YouTube Doodles đã được hiển thị. - "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. + YouTube Doodles đã ẩn. + YouTube Doodles được hiển thị. + "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 trong 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." +Nếu YouTube Doodles đ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." Thay thế nút Tạo Thay thế nút Tạo bằng nút Cài đặt. Thao tác kích hoạt nút @@ -601,19 +601,19 @@ Nhấn và giữ để mở cài đặt RVX." Tắt bảng tự động bật lên khi phá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. 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. - Vô hiệu hoá chuyển sang danh sách kết hợp - Tự động chuyển sang danh sách phát kết hợp đã bị vô hiệu hóa. - "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ắt chuyển sang danh sách kết hợp + Tự động chuyển sang danh sách phát kết hợp đã tắt. + "Tự động chuyển sang danh sách kết hợp đã bật khi tính năng Tự động phát đang bậ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" - 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. + Bật tính năng này sẽ tắt việc tự động chuyển sang YouTube Mix khi phát nhạc trong khi chế độ phát tự động cũng đang bật. Tắt nhấn và giữ để phát 2x>> - "Tắt tính năng nhấn và giữ để \"2x>>\". + "Tắt tính năng nhấn và giữ để phát 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." +• Bật tùy chọn này sẽ khôi phục lại tính năng trượt để tua ở bố cục cũ. +• Tắt tùy chọn này cũng không ép 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ốc độ phát khi nhấn và giữ 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ữ phải nằm trong khoảng 0 - 8.0. @@ -625,10 +625,10 @@ Lưu ý: Chiến dịch gây quỹ được hiển thị. Ẩn lớp phủ khi nhấn đúp để tua Lớp phủ khi nhấn đúp để tua đã bị ẩn. - Lớp phủ khi nhấn đúp để tua đã được hiển thị. - Ẩn các thẻ màn hình kết thúc - Đã ẩn các thẻ màn hình kết thúc. - Đã hiện các thẻ màn hình kết thúc. + Lớp phủ khi nhấn đúp để tua được hiển thị. + Ẩn các thẻ liên kết video khác ở cuối video + Các thẻ liên kết đã ẩn. + Các thẻ liên kết được hiển thị. Tắt tua chính xác Tua chính xác đã tắt. Tua chính xác đã bật. @@ -662,21 +662,21 @@ Lưu ý: 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." Video đề xuất ở màn hình kết thúc được hiển thị. - Bỏ qua tự động đếm ngược trước khi phát - 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 thời gian đếm ngược trước khi phát + 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 mà không cần đếm ngược. 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 lớp phủ khi chụm để thu phóng - Lớp phủ khi chụm để thu phóng đã bị ẩn. - Lớp phủ khi chụm để thu phóng đã được hiển thị. - Làm sạch phụ đề video - "Các cụm từ như '#', 'Fundraiser', 'Shop' and 'products' đã bị ẩn khỏi phụ đề video." - "Các cụm từ như '#', 'Fundraiser', 'Shop' and 'products' đã được hiển thị trong phụ đề video." + Lớp phủ khi chụm để thu phóng đã ẩn. + Lớp phủ khi chụm để thu phóng được hiển thị. + Ẩn các tag bên dưới tiêu đề video + "Các tag có cụm từ như \"#\", \"Chiến dịch gây quỹ\", \"Cửa hàng\" và \"sản phẩm\" đã ẩn bên dưới tiêu đề video." + "Các tag có các cụm từ như \"#\", \"Chiến dịch gây quỹ\", \"Cửa hàng\" và \"sản phẩm\" được hiển thị trong phụ đề video." - Các nút thao tác + Nút thao tác Ẩn hoặc hiển thị các nút thao tác bên dưới video. Tắt hoạt ảnh các nút Thích và Không thích - Các nút Thích và Không thích sẽ không sáng lên khi được nhắc đến. - 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 nhấn vào. + Các nút Thích và Không thích sẽ sáng lên khi nhấn vào. Ẩn nút Tạo đoạn video Nút Tạo đoạn video đã ẩn. Nút Tạo đoạn video được hiển thị. @@ -710,15 +710,15 @@ Cài đặt → Tự động phát → Tự động phát video tiếp theo." Chế độ môi trường xung quanh Tắt hoặc bỏ qua các hạn chế của Chế độ môi trường xung quanh. - Không giới hạn Chế độ môi trường xung quanh - Chế độ môi trường xung quanh vẫn được kích hoạt trong Chế độ tiết kiệm pin. - Chế độ môi trường xung quanh sẽ bị vô hiệu hoá trong chế độ tiết kiệm pin. + Không giới hạn khi tiết kiệm pin + Chế độ môi trường xung quanh vẫn được bật trong Chế độ tiết kiệm pin. + Chế độ môi trường xung quanh sẽ tắt trong Chế độ tiết kiệm pin. Tắt chế độ môi trường xung quanh - Chế độ môi trường xung quanh đã được vô hiệu hoá. - Chế độ môi trường xung quanh đã được kích hoạt. - Tắt chế độ môi trường khi 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. - 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 đã tắt. + Chế độ môi trường xung quanh được bật. + Tắt chế độ môi trường xung quanh khi toàn màn hình + Chế độ môi trường xung quanh đã tắt ở chế độ toàn màn hình. + Chế độ môi trường xung quanh được bật ở chế độ toàn màn hình. Thanh kênh Ẩn hoặc hiển thị các thành phần của thanh kênh bên dưới video. @@ -737,9 +737,9 @@ Cài đặt → Tự động phát → Tự động phát video tiếp theo."Ẩn biểu ngữ Bình luận của hội viên Biểu ngữ Bình luận của hội viên đã ẩn. Biểu ngữ Bình luận của hội viên được hiển thị. - Ẩn các bình luận chữ xanh - Các bình luận chữ xanh đã bị ẩn. - Các bình luận chữ xanh đã được hiển thị. + Ẩn bình luận chữ xanh + Các bình luận chữ xanh đã ẩn. + Các bình luận chữ xanh được hiển thị. Ẩn phần Bình luận Phần Bình luận đã ẩn. Phần Bình luận được hiển thị. @@ -750,17 +750,17 @@ Cài đặt → Tự động phát → Tự động phát video tiếp theo."Phần Xem trước bình luận đã ẩn. Phần Xem trước bình luận được hiển thị. Ẩn nội dung 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. - 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 sẽ chỉ ẩn nội dung xem trước bình luận mà không làm thay đổi kích thước của phần Bình luận nên bạn có thể vuốt sang trái để mở phần Phát lại cuộc trò chuyện trực tiếp. + Tuỳ chọn này sẽ làm thay đổi kích thước của phần Bình luận, do đó bạn không thể mở phần Phát lại cuộc trò chuyện trực tiếp. Ẩn nút Tạo video ngắn - Đã ẩn nút Tạo video ngắn. - Đã hiện nút Tạo video ngắn. + Nút Tạo video ngắn đã ẩn. + Nút Tạo video ngắn được hiển thị. Ẩn nút Cảm ơn Nút Cảm ơn đã ẩn. Nút Cảm ơn được hiển thị. - Ẩn nút dấu thời gian và các Biểu tượng cảm xúc - Nút dấu thời gian và các Biểu tượng cảm xúc đã ẩn. - Nút dấu thời gian và các Biểu tượng cảm xúc được hiển thị. + Ẩn nút dấu thời gian và biểu tượng cảm xúc + Nút dấu thời gian và biểu tượng cảm xúc đã ẩn. + Nút dấu thời gian và biểu tượng cảm xúc được hiển thị. Trình đơn tuỳ chọn Ẩn hoặc thay đổi thành phần của trình đơn tuỳ chọn trong trình phát video. @@ -769,7 +769,7 @@ Cài đặt → Tự động phát → Tự động phát video tiếp theo."Đang sử dụng kiểu bật/tắt tuỳ chọn dạng công tắc. Ẩn mục 1080p Premium Mục 1080p Premium đã ẩn. - Mục 1080p Premium đã hiển thị. + Mục 1080p Premium được hiển thị. Ẩn mục Bản âm thanh Mục Bản âm thanh đã ẩn. Mục Bản âm thanh được hiển thị. @@ -806,20 +806,20 @@ Cài đặt → Tự động phát → Tự động phát video tiếp theo."Mục Trợ giúp & phản hồi đã ẩn. Mục Trợ giúp & phản hồi được hiển thị. Ẩn mục Nghe trên YouTube Music - Mục Nghe trên YouTube Music đã bị ẩn. - Mục Nghe trên YouTube Music được hiển thị. + Mục \"Nghe trên YouTube Music\" đã ẩn. + Mục \"Nghe trên YouTube Music\" được hiển thị. Ẩn mục Cho video lặp lại - Mục lặp lại video đã ẩn. - Mục lặp lại video được hiển thị. + Mục Cho video lặp lại đã ẩn. + Mục Cho video lặp lại được hiển thị. Ẩn mục Hình trong hình - Mục hình trong hình đã ẩn. - Mục hình trong hình được hiển thị. + Mục Hình trong hình đã ẩn. + Mục Hình trong hình được hiển thị. Ẩn mục Nút điều khiển cho gói Premium Mục Nút điều khiển cho gói Premium đã ẩn. Mục Nút điều khiển cho gói Premium được hiển thị. Ẩn mục Hẹn giờ ngủ Mục Hẹn giờ ngủ đã ẩn. - Mục Hẹn giờ ngủ đã hiển thị. + Mục Hẹn giờ ngủ được hiển thị. Ẩn mục Âm lượng ổn định Mục Âm lượng ổn định được hiển thị. Mục Âm lượng ổn định đã ẩn. @@ -832,9 +832,9 @@ Cài đặt → Tự động phát → Tự động phát video tiếp theo." Toàn màn hình Ẩn hoặc thay đổi các thành phần liên quan đến toàn màn hình. - Vô hiệu hóa bảng tương tác - Bảng tương tác đã vô hiệu hóa. - Bảng điều khiển tương tác đã được bật. + Tắt bảng tương tác + Bảng tương tác đã tắt. + Bảng điều khiển tương tác được bật. Hiển thị phần tiêu đề video "Hiển thị phần tiêu đề video ở chế độ toàn màn hình. @@ -843,11 +843,11 @@ Hạn chế: Tiêu đề video sẽ biến mất khi nhấn vào." Bảng video tiếp theo đã ẩn khỏi màn hình kết thúc. Bảng video tiếp theo được hiển thị ở màn hình kết thúc. Ẩn nút 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ú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 đã ẩn.\n\nNút sẽ xuất hiện ở chế độ toàn màn hình khi bạn đóng cuộc 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út sẽ xuất hiện ở chế độ toàn màn hình khi bạn đóng cuộc trò chuyện trực tiếp. Ẩn lớp phủ video liên quan - Phần video thêm trong bảng nút thao tác nhanh và lớp phủ video liên quan đã bị ẩn. - 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 đã ẩn. + 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ị. Thao tác nhanh Ẩn bảng nút thao tác nhanh @@ -857,26 +857,26 @@ Hạn chế: Tiêu đề video sẽ biến mất khi nhấn vào." Nút Bình luận đã ẩn. Nút Bình luận được hiển thị. Ẩn nút Không thích - Nút Không thích đã bị ẩn. - Nút Không thích đã được hiển thị. + Nút Không thích đã ẩn. + Nút Không thích được hiển thị. Ẩn nút Thích Nút Thích đã ẩn. - Nút Thích đã hiển thị. + Nút Thích được hiển thị. Ẩn nút Trò chuyện trực tiếp - Nút trò chuyện trực tiếp đã ẩn. - Nút trò chuyện trực tiếp được hiển thị. + Nút Trò chuyện trực tiếp đã ẩn. + Nút Trò chuyện trực tiếp được hiển thị. Ẩn nút thêm Nút thêm (...) đã ẩn. - Nút thêm (...) đã hiển thị. + Nút thêm (...) được hiển thị. Ẩn nút Danh sách kết hợp Nút Danh sách kết hợp đã ẩn. Nút Danh sách kết hợp được hiển thị. Ẩn nút Danh sách phát Nút Danh sách phát đã ẩn. Nút Danh sách phát được hiển thị. - Ẩn nút Lưu - Nút Lưu đã bị ẩn. - Nút Lưu được hiển thị. + Ẩn nút Lưu vào danh sách phát + Nút Lưu vào danh sách phát đã ẩn. + Nút Lưu vào danh sách phát được hiển thị. Ẩn nút Chia sẻ Nút Chia sẻ đã ẩn. Nút Chia sẻ được hiển thị. @@ -885,8 +885,8 @@ Hạn chế: Tiêu đề video sẽ biến mất khi nhấn vào." Lề trên bảng nút thao tác nhanh phải nằm trong khoảng 0 - 32. Xem ở chế độ toàn màn hình dọc - Đang phát video ở chế độ toàn màn hình dọc. - Đang phát video ở chế độ toàn màn hình mặc định. + Phát video ở chế độ toàn màn hình dọc được áp dụng. + Phát video ở chế độ toàn màn hình mặc định. Lớp phủ điều khiển trình phát thu gọn 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 đã tắt. @@ -894,17 +894,17 @@ Hạn chế: Tiêu đề video sẽ biến mất khi nhấn vào." "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." +• Khi nhấn vào dấu thời gian trong Phần bình luận." Giữ chế độ toàn màn hình 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. - Thời gian giữ chế độ toàn màn hình + Thời gian giữ chế độ toàn màn hình (mili giây) Số mili giây mà chế độ toàn màn hình được giữ. Phản hồi xúc giác Tắt hoặc bật phản hồi xúc giác. 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 đã tắt. - Phản hồi xúc giác đã bật. + Phản hồi xúc giác được bật. Tắt phản hồi xúc giác khi đăng ký kênh Phản hồi xúc giác đã tắt. Phản hồi xúc giác được bật. @@ -929,9 +929,9 @@ Hạn chế: Tiêu đề video sẽ biến mất khi nhấn vào." Ẩn nút Truyền Nút Truyền đã ẩn. Nút Truyền được hiển thị. - Ẩn nút Thu gọn - Nút Thu gọn đã ẩn. - Nút Thu gọn được hiển thị. + Ẩn nút thu gọn + Nút thu gọn đã ẩn. + Nút thu gọn được hiển thị. Ẩn nút Toàn màn hình Nút Toàn màn hình đã ẩn. Nút Toàn màn hình được hiển thị. @@ -943,28 +943,45 @@ Hạn chế: Tiêu đề video sẽ biến mất khi nhấn vào." Nút YouTube Music được hiển thị. Nút trên lớp phủ trình phát - Nút Phát lặp lại một video + Nút Phát lặp lại video "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." +Nhấn và giữ để dừng lại sau khi hết thời lượng video." Nút Sao chép URL 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 và giữ để sao chép URL video kèm theo dấu thời gian." Nút Sao chép URL video với dấu thời gian "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." +Nhấn và giữ để sao chép dấu thời gian." Nút Tắt tiếng 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ải xuống bên ngoài + Nút Tải xuống từ nguồn bên ngoài Nhấn để khởi chạy trình tải xuống bên ngoài. Nút Tốc độ phát "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." +Nhấn và giữ để đặt lại tốc độ phát video (1.0x). Nhấn và giữ lần nữa để đặt lại về tốc độ mặc định đã đặt." Nút Danh sách trắng 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 phát theo thứ tự thời gian - "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." +Nhấn và giữ để mở hộp thoại cài đặt Danh sách trắng. + Nút phát tất cả + "Nhấn để tạo danh sách phát bao gồm tất cả các video từ kênh. +Nhấn và giữ để hoàn tác. + +Hạn chế: +• Tính năng có thể không hoạt động đối với video phát trực tiếp." + Chế độ tạo danh sách phát + Tất cả nội dung (Sắp xếp theo thời gian) + Tất cả nội dung (Sắp xếp theo độ phổ biến) + Chỉ video (Sắp xếp theo thời gian) + Chỉ video (Sắp xếp theo độ phổ biến) + Chỉ video ngắn (Sắp xếp theo thời gian) + Chỉ video ngắn (Sắp xếp theo độ phổ biến) + Chỉ video đã phát trực tiếp (Sắp xếp theo thời gian) + Chỉ video đã phát trực tiếp (Sắp xếp theo độ phổ biến) + Tất cả nội dung chỉ dành cho hội viên + Video chỉ dành cho hội viên + Video ngắn chỉ dành cho hội viên + Video phát trực tiếp chỉ dành cho hội viên + Không thể tạo danh sách phát do ID kênh không trùng khớp. Danh sách trắng Kiểm tra hoặc xóa các kênh đã thêm vào Danh sách trắng. Kênh \'%1$s\' đã được thêm vào Danh sách trắng %2$s. @@ -979,7 +996,7 @@ Nhấn và giữ để hoàn tác." SponsorBlock Không tải được thông tin kênh. Đã đặt lại Tốc độ phát: %sx. - Nhấn giữ để thay đổi trạng thái nút. + Nhấn và giữ để thay đổi trạng thái nút. Đã 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. @@ -987,79 +1004,79 @@ Nhấn và giữ để hoàn tác." Thanh tiến trình Tùy chỉnh thanh tiến trình. Thêm thông tin vào dấu thời gian - "Thông tin đã được thêm vào dấu thời gian. + "Thông tin được thêm vào dấu thời gian. Nhấn vào để thiếp lập Chất lượng video hoặc Tốc độ phát. -Nhấn giữ để chuyển loại thông tin cần thêm vào." +Nhấn và giữ để chuyển loại thông tin cần thêm vào." Thông tin không còn được thêm vào dấu thời gian. Loại thông tin cần thêm Thêm Chất lượng video. Thêm Tốc độ phát. - Thay thế hành động của dấu thời gian + Thay thế chức năng của dấu thời gian Nhấn để mở mục Tốc độ phát hoặc Chất lượng video. Nhấn để hiển thị thời gian còn lại. - Màu thanh tiến trình tùy chỉnh - Đang sử dụng màu thanh tiến trình video tùy chỉnh. - Đang sử dụng màu thanh tiến trình video mặc định. + Màu thanh tiến trình tuỳ chỉnh + Màu thanh tiến trình tuỳ chỉnh được áp dụng. + Màu thanh tiến trình mặc định được áp dụng. Thay đổi màu thanh tiến trình - Nhập mã màu hex của thanh tiến trình video mà bạn muốn thay đổi. + Nhập mã màu hex của thanh tiến trình mà bạn muốn thay đổi. Chạm thanh tiến trình để tua Chạm thanh tiến trình video để tua đã bật. Chạm thanh tiến trình video để tua đã tắt. Ẩn thanh tiến trình trong trình phát Thanh tiến trình video đã ẩn khỏi trình phát. Thanh tiến trình video được hiển thị trong trình phát. - Ẩn thanh tiến trình trong trình phát thu nhỏ - Thanh tiến trình đã ẩn khỏi trình phát thu nhỏ video. - Thanh tiến trình được hiển thị trong trình phát thu nhỏ video. + Ẩn thanh tiến trình trong hình thu nhỏ + Thanh tiến trình đã ẩn khỏi hình thu nhỏ video. + Thanh tiến trình được hiển thị trong hình thu nhỏ video. Ẩn các phân cảnh trên thanh tiến trình Các phân cảnh đã bị ẩn trên thanh tiến trình. - Các phân cảnh đã được hiển thị trên thanh tiến trình. + Các phân cảnh được hiển thị trên thanh tiến trình. Ẩn tên phân cảnh trên thanh tiến trình Tên phân cảnh kế bên dấu thời gian đã ẩn. - 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 được hiển thị. Ẩn Dấu thời gian Dấu thời gian đã ẩn. Dấu thời gian được hiển thị. Khôi phục hình thu nhỏ trên thanh tiến trình kiểu cũ Hình thu nhỏ khi tua sẽ xuất hiện phía trên thanh tiến trình. - 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ẽ hiển thị toàn màn hình. Hình thu nhỏ chất lượng cao Hình thu nhỏ khi tua có chất lượng cao. Hình thu nhỏ khi tua có chất lượng trung bình. "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ỏ khi tua 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ị sau một độ trễ nhất định. +Đ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." Thanh tiến trình kiểu Cairo - "Thanh tiến trình kiểu Cairo đã được kích hoạt. + "Thanh tiến trình kiểu Cairo được bậ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 đã bị vô hiệu hoá. + Thanh tiến trình kiểu Cairo đã tắt. Mô tả video Ẩn hoặc hiển thị các thành phần mô tả video. - Vô hiệu hoá hoạt ảnh của các văn bản dạng Số cuộn - Hoạt ảnh của các văn bản dạng Số cuộn đã bị vô hiệu hoá. - Đã kích hoạt hoạt ảnh Số cuộn. + Tắt hoạt ảnh của các văn bản dạng Số cuộn + Hoạt ảnh cuộn số đã tắt. + Hoạt ảnh cuộn số được bật. Ẩn phần Bản tóm tắt video do AI tạo - Phần Bản tóm tắt video do AI tạo đã bị ẩn. - Phần Bản tóm tắt video do AI tạo đã được hiển thị. + Phần \"Bản tóm tắt video do AI tạo\" đã ẩn. + Phần \"Bản tóm tắt video do AI tạo\" được hiển thị. Ẩn phần Thuộc tính Phần Địa điểm nổi bật, Trò chơi và Âm nhạc đã ẩn. Phần Địa điểm nổi bật, Trò chơi và Âm nhạc được hiển thị. Ẩn phần Phân cảnh Phần Phân cảnh đã ẩn. - Phần Phân cảnh đã hiển thị. + Phần Phân cảnh được hiển thị. Ẩn phần Nội dung - Phần Cách nội dung này được tạo ra đã bị ẩn. - Phần Cách nội dung này được tạo ra đã được hiển thị. + Phần \"Cách nội dung này được tạo ra\" đã ẩn. + Phần \"Cách nội dung này được tạo ra\" được hiển thị. Ẩn phần thẻ thông tin Phần thẻ thông tin đã ẩn. Phần thẻ thông tin được hiển thị. Ẩn phần Khái niệm chính - Phần Khái niệm chính bị ẩn. + Phần Khái niệm chính đã ẩn. Phần Khái niệm chính được hiển thị. Ẩn phần Khám phá podcast Phần Khám phá podcast đã ẩn. @@ -1084,7 +1101,7 @@ Hạn chế: Chủ đề Cairo cũng sẽ áp dụng cho dấu chấm thông bá 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." Mô tả - Trình Shorts + Shorts Tắt tính năng tiếp tục phát video Shorts Trinh phát Shorts sẽ không tiếp tục khi ứng dụng khởi chạy. Trinh phát Shorts sẽ tiếp tục khi ứng dụng khởi chạy. @@ -1100,7 +1117,7 @@ Mở rộng mô tả video có thể không hoạt động nếu bạn nhập n Cụ thể: • Chỉ những kệ có tiêu đề Shorts trên thẻ trang chủ mới bị ẩn." - Đã hiện trong hồ sơ kênh. + Được hiển thị trong hồ sơ kênh. Ẩ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 thẻ Trang chủ và các video có liên quan. @@ -1123,104 +1140,104 @@ Cụ thể: Trình phát Shorts Ẩn hoặc hiển thị các thành phần trong trình phát Shorts. Ẩn nút Tham gia - Đã ẩn nút Tham gia. - Đã hiện nút Tham gia. + Nút tham gia đã ẩn. + Nút tham gia được hiển thị. Ẩn nút Đăng ký - Đã ẩn nút Đăng ký. - Đã hiện nút Đăng ký. + Nút đăng ký đã ẩn. + Nút đăng ký được hiển thị. Ẩn tiêu đề tạm dừng - Đã ẩn Tiêu đề tạm dừng. - Đã hiện Tiêu đề tạm dừng. + Tiêu đề tạm dừng đã ẩn. + Tiêu đề tạm dừng được hiển thị. Ẩ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 các nút phủ lên khi tạm dừng. + 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 được hiển thị. Ẩn nút Thịnh hành - Đã ẩn nút Thịnh hành. - Đã hiện nút Thịnh hành. + Nút Thịnh hành đã ẩn. + Nút Thịnh hành được hiển thị. Ẩn nút Mua sắm - Đã ẩn nút Mua sắm. - Đã hiện nút Mua sắm. + Nút Mua sắm đã ẩn. + Nút Mua sắm được hiển thị. Ẩn nhãn dán - Đã ẩn nhãn dán. - Đã hiện nhãn dán. - Ẩn nhãn quảng cáo được tài trợ - Đã ẩn Nhãn quảng cáo được tài trợ. - Đã hiện Nhãn quảng cáo được tài trợ. - Ẩn Bảng thông tin - Đã ẩn Bảng thông tin. - Đã hiện Bảng thông tin. + Nhãn dán đã ẩn. + Nhãn dán được hiển thị. + Ẩn nhãn nội dung được trả tiền để quảng cáo + Nhãn nội dung được trả tiền để quảng cáo đã ẩn. + Nhãn nội dung được trả tiền để quảng cáo được hiển thị. + Ẩn bảng thông tin + Bảng thông tin đã ẩn. + Bảng thông tin được hiển thị. Ẩn tiêu đề Trò chuyện trực tiếp - Đã ẩn tiêu đề Trò chuyện trực tiếp.\n\nNút Quay lại trong tiêu đề sẽ không bị ẩn. - Đã 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. + Tiêu đề Trò chuyện trực tiếp đã ẩn.\n\nNút Quay lại trong tiêu đề sẽ không bị ẩn. + Tiêu đề Trò chuyện trực tiếp được hiển thị.\n\nNút Quay lại trong tiêu đề sẽ không bị ẩn. Ẩn Thanh kênh - Đã ẩn Thanh kênh. - Đã hiện Thanh kênh. - Ẩn Tiêu đề video - Đã ẩn Tiêu đề. - Đã hiện Tiêu đề. - Ẩn nhãn Siêu dữ liệu âm thanh - Đã ẩn nhãn Siêu dữ liệu. - Đã hiện nhãn Siêu dữ liệu. - Ẩn nhãn Liên kết toàn video - Đã ẩn nhãn Liên kết video. - Đã hiện nhãn Liên kết video. + Thanh kênh đã ẩn. + Thanh kênh được hiển thị. + Ẩn tiêu đề video + Tiêu đề đã ẩn. + Tiêu đề được hiển thị. + Ẩn nhãn siêu dữ liệu âm thanh + Nhãn siêu dữ liệu đã ẩn. + Nhãn siêu dữ liệu được hiển thị. + Ẩn nhãn liên kết toàn video + Nhãn liên kết video đã ẩn. + Nhãn liên kết video được hiển thị. Hành động đề xuất Ẩn nút Phông xanh - Đã ẩn nút Phông xanh. - Đã hiện nút Phông xanh. + Nút Phông xanh đã ẩn. + Nút Phông xanh được hiển thị. Ẩn nút Lưu nhạc - Đã ẩn nút Lưu nhạc. - Đã hiện nút lưu nhạc. - Nút Cửa hàng - Đã ẩn nút Cửa hàng. - Đã hiện nút Cửa hàng. + Nút Lưu nhạc đã ẩn. + Nút Lưu nhạc được hiển thị. + Ẩn nút Cửa hàng + Nút Cửa hàng đã ẩn. + Nút Cửa hàng được hiển thị. Ẩn nút Super Thanks - Đã ẩn nút Super Thanks. - Đã hiện nút Super Thanks. + Nút Super Thanks đã ẩn. + Nút Super Thanks được hiển thị. Ẩn nút Dùng âm thanh này - Đã ẩn nút Dùng âm thanh này. - Đã hiện nút Dùng âm thanh này. + Nút \"Dùng âm thanh này\" đã ẩn. + Nút \"Dùng âm thanh này\" được hiển thị. Ẩn nút Sử dụng mẫu - Đã ẩn nút Sử dụng mẫu. - Đã hiện nút Sử dụng mẫu. + Nút \"Sử dụng mẫu\" đã ẩn. + Nút \"Sử dụng mẫu\" được hiển thị. Ẩn nút Vị trí - Đã ẩn nút Vị trí. - Đã hiện nút Vị trí. + Nút Vị trí đã ẩn. + Nút Vị trí được hiển thị. Ẩn nút Gợi ý tìm kiếm - Đã ẩn nút Gợi ý tìm kiếm. - Đã hiện nút Gợi ý tìm kiếm. + Nút Gợi ý tìm kiếm đã ẩn. + Nút Gợi ý tìm kiếm được hiển thị. Ẩn sản phẩm được gắn thẻ - Đã ẩn Sản phẩm được gắn thẻ. - Đã hiện Sản phẩm được gắn thẻ. + Sản phẩm được gắn thẻ đã ẩn. + Sản phẩm được gắn thẻ được hiển thị. Nút thao tác Ẩn nút Thích - Đã ẩn nút Thích. - Đã hiện nút Thích. + Nút Thích đã ẩn. + Nút Thích được hiển thị. Ẩn nút Không thích - Đã ẩn nút Không thích. - Đã hiện nút Không thích. + Nút Không thích đã ẩn. + Nút Không thích được hiển thị. Ẩn nút Bình luận - Đã ẩn nút Bình luận. - Đã hiện nút Bình luận. + Nút Bình luận đã ẩn. + Nút Bình luận được hiển thị. Ẩn nút Phối lại - Đã ẩn nút Phối lại. - Đã hiện nút Phối lại. + Nút Phối lại đã ẩn. + Nút Phối lại được hiển thị. Ẩn nút Chia sẻ - Đã ẩn nút Chia sẻ. - Đã hiện nút Chia sẻ. + Nút Chia sẻ đã ẩn. + Nút Chia sẻ được hiển thị. Nút Âm thanh - Đã ẩn nút Âm thanh. - Đã hiện nút Âm thanh. + Nút âm thanh đã ẩn. + Nút âm thanh được hiển thị. - Hoạt ảnh / Phản hồi - Vô hiệu hoá hiệu ứng nút Thích - Đã vô hiệu hoá hiệu ứng phun nước trên nút Thích. - Đã kích hoạt hiệu ứng phun nước trên nút Thích. + Hoạt ảnh/Phản hồi + Tắt hiệu ứng nút Thích + Hiệu ứng phun nước trên nút Thích đã tắt. + Hiệu ứng phun nước trên nút Thích đã bật. Ẩn nền các nút Phát & Tạm dừng - Đã ẩn nền của nút. - Đã hiển thị nền của nút. + Nền nút đã ẩn. + Nền nút được hiển thị. Hoạt ảnh nhấn đúp Gốc Thích @@ -1230,52 +1247,52 @@ Cụ thể: Ẩn Dấu thời gian - "Dấu thời gian đã được bậ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 đã bị tắt. + Dấu thời gian đã tắt. Nhấn và giữ Dấu thời gian 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. Lề dưới của bảng Meta - 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. + 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 phải nằm trong khoảng từ 0 đến 64. Ẩn thanh công cụ - Đã ẩn Thanh công cụ. - Đã hiện Thanh công cụ. + Thanh công cụ đã ẩn. + Thanh công cụ được hiển thị. Ẩn Thanh điều hướng - Đã ẩn Thanh điều hướng. - Đã hiện Thanh điều hướng. + Thanh điều hướng đã ẩn. + Thanh điều hướng được hiển thị. Chiều cao của khoảng trống 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 phải nằm trong khoảng từ 0 đến 100 (%). Thay thế tên hiển thị của kênh - Đang áp dụng tên kênh. - Đang áp dụng tên hiển thị của kênh (@handle). + Tên kênh đang được áp dụng. + Tên hiển thị của kênh (@handle) đang được áp dụng. Cử chỉ vuốt Cử chỉ điều chỉnh độ sáng tự động Chế độ độ sáng tự động sẽ được bật khi vuốt độ sáng về mức tổi thiểu. Chế độ độ sáng tự động sẽ không được bật khi vuốt độ sáng về mức tổi thiểu. Vuốt điều chỉnh độ sáng - Đã kích hoạt cử chỉ vuốt điều chỉnh độ sáng. - Đã vô hiệu hoá cử chỉ vuốt điều chỉnh độ sáng. + Cử chỉ vuốt điều chỉnh độ sáng đã bật. + Cử chỉ vuốt điều chỉnh độ sáng đã tắt. Vuốt điều chỉnh âm lượng - Đã kích hoạt cử chỉ vuốt điều chỉnh âm lượng. - Đã vô hiệu hoá cử chỉ vuốt điều chỉnh âm lượng. + Cử chỉ vuốt điều chỉnh âm lượng đã bật. + Cử chỉ vuốt điều chỉnh âm lượng đã tắt. Lưu độ sáng Lưu độ sáng khi thoát ra hoặc vào chế độ toàn màn hình. Không lưu độ sáng khi thoát ra hoặc vào chế độ toàn màn hình. Nhấn và giữ để vuốt - Nhấn và giữ để vuốt đã được kích hoạt. - Nhấn và giữ để vuốt đã bị vô hiệu hoá. + Nhấn và giữ để vuốt đã bật. + Nhấn và giữ để vuốt đã tắt. Phản hồi xúc giác Phản hồi xúc giác đã bật. Phản hồi xúc giác đã tắt. Vuốt ở chế độ Khoá màn hình - Đã kích hoạt cử chỉ vuốt ở chế độ Khóa màn hình. - Đã vô hiệu hoá cử chỉ vuốt ở chế độ Khóa màn hình. + Cử chỉ vuốt đã bật ở chế độ Khoá màn hình. + Vuốt ở chế độ Khoá màn hình đã tắt. Độ trong suốt lớp phủ Độ trong suốt của nền khi thực hiện cử chỉ vuốt. Độ rộng ngưỡng vuốt @@ -1297,11 +1314,11 @@ Hạn chế: Độ sáng HDR tự động đã tắt. Độ sáng HDR tự động đã bật. Cử chỉ bên dưới trình phát - Đã 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. - Đã 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. + Cử chỉ khi vuốt xuống từ khu vực bên dưới trình phát đã bật để xem ở chế độ toàn màn hình dọc. + Cử chỉ vuốt xuống từ khu vực bên dưới trình phát đã tắt để xem ở chế độ toàn màn hình dọc. Vuốt để chuyển video - Vuốt lên/xuống sẽ phát video tiếp theo hoặc trước đó. - 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 trước đó hoặc tiếp theo. + Vuốt lên/xuống sẽ không phát video trước đó hoặc tiếp theo. Tự động Video @@ -1314,37 +1331,37 @@ Hạn chế: Tắt tùy chọn tốc độ phát khi xem trực tiếp 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ố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 tùy chỉnh - Đang áp dụng các giá trị tốc độ phát video tùy chỉnh. + Tốc độ phát tuỳ chỉnh + Đang áp dụng các giá trị tốc độ phát video tuỳ chỉnh. Đang áp dụng các giá trị tốc độ phát video mặc định. Kiểu mục tốc độ phát tùy chỉnh Mục tốc độ phát dạng hộp thoại được sử dụng. Mục tốc độ phát kiểu cũ được sử dụng. Chỉnh sửa tốc độ phát Thêm hoặc thay đổi tốc độ phát lại có sẵn. - Lưu thay đổi tốc độ phát - Thay đổi tốc độ phát áp dụng cho tất cả video. - Thay đổi tốc độ phát chỉ áp dụng cho video hiện tại. - Hiện một thông báo ngắn - Thông báo ngắn sẽ được hiển thị khi thay đổi tốc độ phát mặc định. - Thông báo ngắn sẽ không hiển thị khi thay đổi tốc độ phát mặc định. - Lưu thay đổi chất lượng video - Thay đổi chất lượng áp dụng cho tất cả video. - Thay đổi chất lượng chỉ áp dụng cho video hiện tại. - Hiện một thông báo ngắn - Thông báo ngắn sẽ được hiển thị khi thay đổi chất lượng mặc định của 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. + Lưu lựa chọn tốc độ phát + Lựa chọn tốc độ phát đã chọn sẽ áp dụng cho tất cả video. + Lựa chọn tốc độ phát đã chọn chỉ áp dụng cho video hiện tại. + Thông báo ngắn + Hiển thị một thông báo ngắn khi thay đổi tốc độ phát mặc định. + Không hiện một thông báo ngắn khi thay đổi tốc độ phát mặc định. + Lưu lựa chọn chất lượng video + Lựa chọn chất lượng video đã chọn sẽ áp dụng cho tất cả video. + Lựa chọn chất lượng video đã chọn chỉ áp dụng cho video hiện tại. + Thông báo ngắn + Hiển thị một thông báo ngắn khi thay đổi chất lượng video mặc định. + Không hiện một thông báo ngắn khi thay đổi chất lượng video mặc định. Khôi phục mục chất lượng video kiểu cũ Mục chất lượng video kiểu cũ được hiển thị. Mục chất lượng video kiểu cũ không được hiển thị. Tắt tùy chọn tốc độ phá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ốc độ phát mặc định đã được kích hoạt khi phát nhạc. +Hạn chế: Cài đặt này có thể không áp dụng cho các video không bao gồm biểu ngữ \"Nghe trên YouTube Music\"." + Tốc độ phát mặc định được kích hoạt khi phát nhạc. Tốc độ phát mặc định cho video ngắn - Đang áp dụng tốc độ phát mặc định (bạn đã đặt) khi xem Shorts. - Tốc độ phát mặc định không áp dụng cho Shorts. + Tốc độ phát mặc định đã đặt đang được áp dụng khi xem Shorts. + Tốc độ phát mặc định đã đặt không áp dụng cho Shorts. Đã bỏ qua bộ đệm tải trước. Bỏ qua bộ đệm tải trước "Bỏ qua bộ đệm tải trước ở đầu video để áp dụng ngay chất lượng video mặc định. @@ -1352,65 +1369,65 @@ Hạn chế: Cài đặt này có thể sẽ không áp dụng cho các video kh 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." - Việc bật cài đặt này có thể gây ra sự cố phát video. - Hiện thông báo ngắn khi bỏ qua + Bật cài đặt này có thể gây ra sự cố phát video. + Thông báo ngắn khi bỏ qua Thông báo ngắn được hiển thị. Thông báo ngắn đã ẩn. Giả mạo kích thước thiết bị "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." Vô hiệu hoá codec VP9 - "Codec VP9 đã bị vô hiệu hoá. + "Codec VP9 đã 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. +• Độ phân giải tối đa lúc này là 1080p. +• 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." - Codec VP9 đã được kích hoạt. + Codec VP9 được kích hoạt. Thay thế codec AV1 phần mềm Thay thế codec AV1 phần mềm bằng codec VP9. Từ chối phản hồi codec AV1 phần mềm "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." - 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ưu tốc độ phát mặc định thành %s. - 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. + Quá trình dự phòng khiến việc tải bộ đệm mất khoảng 20 giây trước khi bắt đầu. + Đã lưu lựa chọn tốc độ phát mặc định thành %s. + Đã lưu lựa chọn chất lượng video mặc định khi sử dụng dữ liệu di động thành %s. + Thay đổi lựa chọn chất lượng video thất bại. + Đã lưu lựa chọn chất lượng video mặc định khi sử dụng Wi-Fi thành %s. 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ệ. Return YouTube Dislike Hiện số lượt không thích Số lượt không thích được hiển thị. - Số lượt Không thích đã bị ẩn. + Số lượt không thích đã ẩn. Hiện số lượt Không thích trong 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." Số lượt không thích đã ẩn khỏi trình phát Shorts. - Hiện số lượt không thích theo % + Hiện số lượt không thích theo phần trăm Số lượt không thích được hiển thị dưới dạng phần trăm. Số lượt không thích được hiển thị dưới dạng số. Nút Thích thu gọn Nút Thích được thiết kế để tối ưu kích thước hiển thị. Nút Thích được thiết kế để đồng bộ khả năng hiển thị với nút Không thích. Hiển thị số lượt thích ước tính - Số lượt thích ước tính đã hiển thị. + Số lượt thích ước tính được hiển thị. Số lượt thích ước tính đã ẩ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 Return YouTube Dislike không khả dụng. - 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 Return YouTube Dislike không khả dụng. + Không hiện thông báo ngắn nếu Return YouTube Dislike không khả dụng. Giới thiệu ReturnYouTubeDislike.com 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. 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 (trạng thái %d). - 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 (đã đạt đến giới hạn API ứng dụng khá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 - Đã ẩn + Tải lại video để bình chọn bằng Return YouTube Dislike + Ẩn Return YouTube Username Kích hoạt Return YouTube Username @@ -1425,11 +1442,11 @@ Hạn chế: Lượt không thích có thể không hiển thị nếu người Giới thiệu về khoá YouTube Data API "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. +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ượt, 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." Phát hành mã khoá - 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. + 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. SponsorBlock Kích hoạt SponsorBlock @@ -1438,69 +1455,69 @@ Nhấp vào đây để xem các bước phát hành khóa API." Giao diện Nút Bình chọn phân đoạn Nút Bình chọn phân đoạn được hiển thị. - Nút Bình chọn phân đoạn đã ẩn. + Nút Bình chọn phân đoạn không được hiển thị. 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ế để tối ưu kích thước hiển thị. Nút Bỏ qua phân đoạn được thiết kế với giao diện tốt nhất. Tự động ẩn nút Bỏ qua Nút Bỏ qua sẽ ẩn sau vài giây. Nút Bỏ qua được hiển thị cho toàn bộ phân đoạn. - Hiện thông báo ngắn - 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ụ. - Thông báo không được hiển thị. Nhấn vào đây để xem ví dụ. + Thông báo ngắn khi tự động bỏ qua + Hiển thị 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ụ. + Không 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 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 đủ. Độ dài video đầy đủ được hiển thị. Cài đặt phân đoạn - 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. - 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ợ + 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. + 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ắ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. - Khúc nổi bật + 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. + Khoảnh khắc nổi bật Phần video được nhiều người tìm kiếm nhất. - Đ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. - Đ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. - Lạc đề/Hài hước + Đoạn tạm dừng/Phần giới thiệu + 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. + Đoạn kết thúc/Phần danh đề + 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ộ. + 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. - Â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. + Â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. Bỏ qua - Khúc Nổi bật + Khoảnh khắc nổi bật Bỏ qua Nhà tài trợ - Bỏ qua khuyến mãi - Bỏ qua Nhắc Tương Tác - Bỏ qua Khúc Nổi bật - 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 Kết - Bỏ qua Phần Xem Trước - Bỏ qua Phần Xem Trước - Bỏ qua Phần Tóm Tắt - Bỏ qua Đoạn Trống - Bỏ qua Phần Không Nhạc - Bỏ qua Phân Đoạn - Đã bỏ qua Nhà Tài Trợ. - Đã bỏ qua Tự Quảng Cáo. - Đã bỏ qua Nhắc Tương Tác. - Đã bỏ qua Khúc Nổi bật. - Đã 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 Kết. - Đã bỏ qua Phần Xem Trước. - Đã bỏ qua Phần Xem Trước. - Đã bỏ qua Phần Tóm Tắt. - Đã bỏ qua Đoạn Trống. - Đã bỏ qua Phần không chứa âm nhạc. - Đã bỏ qua phân đoạn chưa gửi. + Bỏ qua Tự quảng cáo + Bỏ qua Nhắc nhở tương tác + Bỏ qua Khoảnh khắc nổi bật + 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 Phần kết thúc + Bỏ qua Phần xem trước + Bỏ qua Phần xem trước + Bỏ qua Phần tóm tắt + Bỏ qua Cảnh phụ + Bỏ qua Phần không phải nhạc + Bỏ qua Phân đoạn + Đã bỏ qua Nhà tài trợ. + Đã bỏ qua Tự quảng cáo. + Đã bỏ qua Nhắc nhở tương tác. + Đã bỏ qua Khoảnh khắc nổi bật. + Đã 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 Phần kết thúc. + Đã bỏ qua Phần xem trước. + Đã bỏ qua Phần xem trước. + Đã bỏ qua Phần tóm tắt. + Đã bỏ qua Cảnh phụ. + Đã bỏ qua Phần không phải nhạc. + Đã bỏ qua phân đoạn chưa được gửi. Đã bỏ qua nhiều phân đoạn. Tự động bỏ qua Tự động bỏ qua một lần @@ -1517,11 +1534,11 @@ Nhấp vào đây để xem các bước phát hành khóa API." Tạo phân đoạn mới Nút tạo phân đoạn mới - Đã hiện nút Tạo phân đoạn mới. - Đã ẩn nút Tạo phân đoạn mới. + Nút tạo phân đoạn mới được hiển thị. + Nút tạo phân đoạn mới không được hiển thị. Điều chỉnh phân đoạn mới 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. - Giá trị nhập vào phải là một số dương. + Giá trị nhập phải là một số dương. Xem 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. Thực hiện theo các nguyên tắc @@ -1530,23 +1547,23 @@ Nhấp vào đây để xem các bước phát hành khóa API." Xem ngay Chung - Thông báo nếu API không khả dụng + 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 SponsorBlock không khả dụng. - Không hiện thông báo nếu SponsorBlock không khả dụng. - Bật theo dõi số lần bỏ qua + Không hiện thông báo ngắn nếu SponsorBlock không khả dụng. + Theo dõi số lần bỏ qua 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. Theo dõi số lần bỏ qua không được bật. Thời lượng phân đoạn tối thiểu 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. Thời lượng không hợp lệ. - Id người dùng riêng tư của bạn - 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. + ID người dùng riêng tư của bạn + 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. ID người dùng riêng tư phải dài ít nhất 30 ký tự. - Thay đổi URL API + Thay đổi URL của API Địa chỉ SponsorBlock sử dụng để liên lạc đến máy chủ. - Đặt lại URL API. - URL API không hợp lệ. - Đã thay đổi API URL. + Đã đặt lại URL của API. + URL của API không hợp lệ. + URL của API đã thay đổi. Sao chép 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. @@ -1555,26 +1572,26 @@ Nhấp vào đây để xem các bước phát hành khóa API." Nhập cài đặt thất bại: %s. Xuất cài đặt thất bại: %s. 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 + Không hiển thị lại lần nữa 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). Không thể gửi phân đoạn: %s. - SponsorBlock tạm thời ngưng hoạt động. + SponsorBlock tạm thời không hoạt động. Không thể gửi phân đoạn (trạng thái: %1$d %2$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). Không thể gửi phân đoạn: %s. Không thể gửi phân đoạn.\nĐã tồn tại. Đã gửi phân đoạn thành công. - 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 (hết thời gian chờ API). Không thể bỏ phiếu cho phân đoạn (trạng thái: %1$d %2$s). Không thể bỏ phiếu cho phân đoạn: %s. - Ủng hộ + Tán thành Phản đối Đổi danh mục Không có phân đoạn nào để bình chọn. Chọn danh mục phân đoạn - Danh mục đã tắt trong cài đặt. Cho phép danh mục để gửi. + Danh mục đã tắt trong cài đặt. Bật danh mục để gửi. Đoạn SponsorBlock mới Đặt %s là bắt đầu hoặc là kết thúc của một phân đoạn mới? bắt đầu @@ -1582,19 +1599,19 @@ Nhấp vào đây để xem các bước phát hành khóa API." ngay lúc này Phân đoạn bắt đầu lúc Phân đoạn kết thúc lúc - Thời lượng phân đoạn đã chính xác chưa? + Thời lượng phân đoạn đã chính xác? 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 gian bắt đầu phải trước thời gian kết thúc. + Thời gian bắt đầu phải nằm trước thời gian 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. - Hãy xem trước phân đoạn để đảm bảo rằng nó bỏ qua suôn sẻ. - Chỉnh sửa thời gian của phân đoạn theo cách thủ công - Bạn muốn thay đổi thời gian bắt đầu hay kết thúc của phân đoạn? + Hãy xem trước phân đoạn để đảm bảo rằng nó được bỏ qua suôn sẻ. + Chỉnh sửa thời gian của phân đoạn bằng cách thủ công + Bạn muốn chỉnh sửa 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ệ. 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... - Đã vô hiệu hoá SponsorBlock. + SponsorBlock đã vô hiệu hóa. 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. @@ -1605,7 +1622,7 @@ Nhấp vào đây để xem các bước phát hành khóa API." Bảng xếp hạng SponsorBlock Bạn đã hỗ trợ mọi người <b>%s</b> phân đoạn 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. - Đó là <b>%s</b> của mọi người.<br>Nhấn vào để xem bảng xếp hạng + Đó là <b>%s</b> của mọi người.<br>Nhấn vào đây để xem bảng xếp hạng. Bạn đã bỏ qua <b>%s</b> phân đoạn Đó là <b>%s</b>. Đặt lại bộ đếm phân đoạn đã bỏ qua? @@ -1631,9 +1648,9 @@ Nhấp vào đây để xem các bước phát hành khóa API." Đang bỏ qua chuyển hướng URL khi mở các liên kết xuất hiện trên YouTube. Đang chuyển hướng URL khi mở các liên kết xuất hiện trên YouTube. Mở theo mặc đị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ở GmsCore - Kích hoạt Cloud Messaging để nhận thông báo đẩy và các cài đặt khác. + Để mở liên kết YouTube trong RVX, hãy bậ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ợ. + GmsCore + Chuyển hướng tới cài đặt GmsCore và bật Cloud Messaging để nhận thông báo đẩy. GmsCore chưa được cài đặt. Hãy cài đặt nó đi nào. Hành động cần thiết "Hiện GmsCore không có quyền chạy nền. @@ -1667,7 +1684,7 @@ Nhấn vào Tiếp tục và tắt tối ưu hóa pin." Nhập/Xuất dưới dạng văn bản Nhập/Xuất dưới dạng văn bản - 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 hoặc xuất toàn bộ cài đặt của bạn dưới dạng văn bản. Xuất cài đặt thất bại. Cài đặt đã được xuất thành công. Nhập @@ -1678,79 +1695,69 @@ Nhấn vào Tiếp tục và tắt tối ưu hóa pin." Đặt lại Đã sao chép cài đặt sang bảng nhớ tạm. - Giả mạo dữ liệu phát trực tiếp - 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 - Đã giả mạo dữ liệu phát trực tiếp. - "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." - Việc tắt cài đặt này có thể gây ra sự cố khi phát video. - Máy khách mặc định + Giả mạo luồng dữ liệu trực tuyến + Giả mạo luồng dữ liệu trực tuyến để khắc phục sự cố phát video. + Giả mạo luồng dữ liệu trực tuyến + Luồng dữ liệu trực tuyến được giả mạo. + "Luồng dữ liệu trực tuyến chưa được giả mạo. Khi phát video có thể gặp sự cố đứng hình." + Tắt cài đặt này có thể gây ra sự cố phát video. + Ứng dụng khách mặc định iOS - Android - Android Creator - Trình phát nhúng Android - Thử nghiệm Android Android TV Android VR - TV HTML5 - Trang Web - Những hạn chế khi giả mạo - "• 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. -• Codec âm thanh opus có thể không được hỗ trợ." - "• Video có thể kết thúc sớm 1 giây. -• Codec âm thanh opus có thể không được hỗ trợ." + Hạn chế + "• Video phát trực tiếp sẽ khởi chạy từ đầu. +• Video có thể kết thúc sớm hơn 1 giây." + • Video có thể kết thúc sớm hơn 1 giây. "• Mục Bản âm thanh bị thiếu. -• Mục Âm lượng ổn định không khả dụng." +• Â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." - • Video có thể không phát được. - Chế độ tương thích iOS - Chỉ giả mạo máy khách sang iOS nếu đó không phải là phim, video trả phí hoặc phát trực tiếp. - Luôn giả mạo máy khách sang iOS. +• Âm lượng ổn định không khả dụng." Buộc iOS sử dụng AVC (H.264) Codec video trên iOS là AVC (H.264). Codec video trên iOS là AVC (H.264), VP9, hoặc là AV1. - "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. + "Bật tuỳ chọn này có thể tăng cường thời lượng pin và khắc phục tình trạng đứng hình 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." +AVC (H.264) có độ phân giải tối đa 1080p, và dùng nhiều dữ liệu di động hơn với VP9 hoặc AV1." + Bỏ qua video phát trực tiếp trên iOS + Ứng dụng khách iOS không được sử dụng cho video phát trực tiếp. + Ứng dụng khách iOS được sử dụng cho video phát trực tiếp. Hiển thị trong Thống kê chi tiết - Máy khách được sử dụng để lấy dữ liệu phát trực tiếp sẽ được hiển thị trong Thống kê chi tiết. - 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. + Ứng dụng khách sử dụng để nạp luồng dữ liệu trực tuyến được hiển thị trong Thống kê chi tiết. + Ứng dụng khách sử dụng để nạp luồng dữ liệu trực tuyến đã ẩn trong Thống kê chi tiết. Nhật ký xem - Thay đổi cài đặt liên quan đến Nhật ký xem. - Quản lý toàn bộ lịch sử - Nhấn để mở mục quản lý Nhật ký xem trên YouTube. + Thay đổi cài đặt liên quan đến nhật ký xem. + Quản lý toàn bộ lịch sử hoạt động + Nhấn để mở mục quản lý nhật ký hoạt động trên YouTube. Kiểu nhật ký Gốc Thay thế miền Chặn nhật ký Trạng thái • Nhật ký xem bị chặn. - • Tuân theo cài đặt Nhật ký xem của tài khoản Google. - "• Tuân theo cài đặt Nhật ký xem của tài khoản Google. + • Tuân theo cài đặt nhật ký xem của tài khoản Google. + "• 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." Thông tin bản vá Thông tin bản vá - Thông tin về các bản vá đã được áp dụng. + Thông tin về các bản vá được áp dụng. Công cụ được sử dụng - Nhiều hơn + Khác Tùy chỉnh - Stock + Nguyên bản AFN Blue AFN Red MMT Revancify Blue Revancify Red YouTube - Nguyên gốc - Không bao gồm + Nguyên bản + Đã loại trừ Đã bao gồm - Nguyên gốc + Nguyên bản diff --git a/src/main/resources/youtube/translations/zh-rCN/strings.xml b/patches/src/main/resources/youtube/translations/zh-rCN/strings.xml similarity index 99% rename from src/main/resources/youtube/translations/zh-rCN/strings.xml rename to patches/src/main/resources/youtube/translations/zh-rCN/strings.xml index b1f4b26b6..61414febc 100644 --- a/src/main/resources/youtube/translations/zh-rCN/strings.xml +++ b/patches/src/main/resources/youtube/translations/zh-rCN/strings.xml @@ -955,9 +955,6 @@ 显示白名单按钮 点按打开白名单对话框 长按打开白名单设置对话框 - 显示按时间排序的播放列表按钮 - "点按生成频道中从最旧到最新的所有视频的播放列表 -长按撤消" 频道白名单 检查或删除添加到白名单的频道列表 频道 \'%1$s\'已添加到 %2$s 白名单 @@ -1033,6 +1030,9 @@ 禁用滚动数字动画 滚动动画已禁用 滚动动画已启用 + 隐藏 AI 生成的视频摘要 + AI 生成的视频摘要已隐藏 + AI 生成的视频摘要已显示 隐藏属性部分 精选位置、游戏和音乐部分已隐藏 精选位置、游戏和音乐部分已显示 @@ -1670,19 +1670,12 @@ 关闭此选项可能会导致视频不能正常播放 默认客户端 iOS - Android - Android 创建者 - Android 嵌入式播放器 - Android 测试套件 Android TV Android VR - TV HTML5 - 网址 伪装副作用 "• 电影或付费视频可能无法播放" "• 音轨菜单缺失" "• 音轨菜单缺失" - • 视频可能无法播放 强制使用 iOS AVC (H.264) iOS 视频编解码器是 AVC (H.264) iOS 视频编解码器是 AVC (H.264), VP9, 或 AV1 diff --git a/src/main/resources/youtube/translations/zh-rTW/strings.xml b/patches/src/main/resources/youtube/translations/zh-rTW/strings.xml similarity index 99% rename from src/main/resources/youtube/translations/zh-rTW/strings.xml rename to patches/src/main/resources/youtube/translations/zh-rTW/strings.xml index ef0337150..9b47c1fdc 100644 --- a/src/main/resources/youtube/translations/zh-rTW/strings.xml +++ b/patches/src/main/resources/youtube/translations/zh-rTW/strings.xml @@ -2,9 +2,9 @@ 是否啟用影片播放器的無障礙控制? - 由於已啟用無障礙服務,因此您的控制項被修改。 + 由於已啟用無障礙服務,因此您的控制選項已被修改。 - ReVanced Extended + ReVanced 擴充功能 搜尋設定 重設為預設值。 實驗性功能 @@ -94,7 +94,7 @@ 使用圖像主機 yt4.ggpht.com 使用原圖主機。\n\n啟用此功能可以修復某些區域被封鎖的遺失影像。 - 首頁 + 探索 隱藏專輯卡片 專輯卡片已隱藏 專輯卡片已顯示 @@ -106,8 +106,8 @@ ・再次收聽 ・購物 ・再看一次" - 隱藏 Chips 影片欄 - Chips 影片欄已隱藏 + 隱藏剪輯 + 剪輯已隱藏 剪輯已顯示 隱藏影片下方的章節選擇欄 影片下方的章節選擇欄已隱藏 @@ -956,9 +956,6 @@ 顯示白名單按鈕 點選可開啟白名單對話框。 點選並按住可開啟白名單設定對話框。 - 顯示按時間排序的播放清單按鈕 - "點擊可產生頻道中從最舊到最新的所有影片的播放清單。 - 點選並按住可撤銷。" 頻道白名單 查看或刪除已新增至白名單的頻道清單。 頻道 %1$s 已加入 %2$s 白名單。 @@ -1680,19 +1677,12 @@ 關閉此設定可能會導致影片播放問題。 預設客戶端 iOS - 安卓 - 安卓創作者 - Android 嵌入式播放器 - Android 測試套件 Android 電視 Android VR - TV HTML5 - Web 偽裝副作用 "• 電影或付費影片可能無法播放。" "• 音軌選單遺失。" "• 音軌選單遺失。" - • 影片可能無法播放。 強制 iOS AVC (H.264) iOS 影片編解碼器為 AVC (H.264)、VP9 或 AV1。 iOS 視訊編解碼器是 AVC (H.264)、VP9 或 AV1。 diff --git a/src/main/resources/youtube/visual/icons/extension/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/visual/icons/extension/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/icons/extension/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/visual/icons/extension/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/visual/icons/gear/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/visual/icons/gear/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/icons/gear/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/visual/icons/gear/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/visual/icons/revanced/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/visual/icons/revanced/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/icons/revanced/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/visual/icons/revanced/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/visual/icons/revanced_colored/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/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/youtube/visual/icons/revanced_colored/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/visual/icons/yt_alt/drawable/revanced_extended_settings_key_icon.xml b/patches/src/main/resources/youtube/visual/icons/yt_alt/drawable/revanced_extended_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/icons/yt_alt/drawable/revanced_extended_settings_key_icon.xml rename to patches/src/main/resources/youtube/visual/icons/yt_alt/drawable/revanced_extended_settings_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable-xxhdpi/empty_icon.png b/patches/src/main/resources/youtube/visual/shared/drawable-xxhdpi/empty_icon.png similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable-xxhdpi/empty_icon.png rename to patches/src/main/resources/youtube/visual/shared/drawable-xxhdpi/empty_icon.png diff --git a/src/main/resources/youtube/visual/shared/drawable/about_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/about_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/about_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/about_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/accessibility_settings_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/accessibility_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/accessibility_settings_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/accessibility_settings_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/account_switcher_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/account_switcher_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/account_switcher_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/account_switcher_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/auto_play_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/auto_play_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/auto_play_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/auto_play_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/billing_and_payment_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/billing_and_payment_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/billing_and_payment_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/billing_and_payment_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/captions_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/captions_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/captions_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/captions_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/connected_accounts_browse_page_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/connected_accounts_browse_page_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/connected_accounts_browse_page_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/connected_accounts_browse_page_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/data_saving_settings_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/data_saving_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/data_saving_settings_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/data_saving_settings_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/general_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/general_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/general_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/general_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/gms_core_settings_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/gms_core_settings_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/gms_core_settings_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/gms_core_settings_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/history_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/history_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/history_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/history_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/live_chat_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/live_chat_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/live_chat_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/live_chat_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/notification_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/notification_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/notification_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/notification_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/offline_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/offline_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/offline_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/offline_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/pair_with_tv_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/pair_with_tv_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/pair_with_tv_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/pair_with_tv_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/parent_tools_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/parent_tools_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/parent_tools_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/parent_tools_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/premium_early_access_browse_page_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/premium_early_access_browse_page_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/premium_early_access_browse_page_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/premium_early_access_browse_page_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/privacy_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/privacy_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/privacy_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/privacy_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_change_player_flyout_menu_toggle_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_change_player_flyout_menu_toggle_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_change_player_flyout_menu_toggle_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_change_player_flyout_menu_toggle_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_change_shorts_repeat_state_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_change_shorts_repeat_state_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_change_shorts_repeat_state_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_change_shorts_repeat_state_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_default_video_quality_wifi_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_default_video_quality_wifi_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_default_video_quality_wifi_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_default_video_quality_wifi_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_disable_default_playback_speed_music_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_disable_default_playback_speed_music_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_disable_default_playback_speed_music_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_disable_default_playback_speed_music_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_disable_hdr_video_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_disable_hdr_video_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_disable_hdr_video_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_disable_hdr_video_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_disable_quic_protocol_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_disable_quic_protocol_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_disable_quic_protocol_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_disable_quic_protocol_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_enable_debug_logging_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_debug_logging_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_enable_debug_logging_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_debug_logging_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_enable_default_playback_speed_shorts_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_default_playback_speed_shorts_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_enable_default_playback_speed_shorts_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_default_playback_speed_shorts_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_enable_external_browser_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_external_browser_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_enable_external_browser_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_external_browser_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_enable_open_links_directly_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_open_links_directly_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_enable_open_links_directly_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_open_links_directly_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_enable_opus_codec_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_opus_codec_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_enable_opus_codec_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_opus_codec_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_enable_save_and_restore_brightness_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_save_and_restore_brightness_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_enable_save_and_restore_brightness_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_save_and_restore_brightness_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_brightness_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_brightness_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_brightness_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_brightness_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_haptic_feedback_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_haptic_feedback_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_haptic_feedback_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_haptic_feedback_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_lowest_value_auto_brightness_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_lowest_value_auto_brightness_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_lowest_value_auto_brightness_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_lowest_value_auto_brightness_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_press_to_engage_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_press_to_engage_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_press_to_engage_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_press_to_engage_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_to_switch_video_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_to_switch_video_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_to_switch_video_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_to_switch_video_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_volume_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_volume_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_volume_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_enable_swipe_volume_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_clip_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_clip_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_clip_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_clip_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_navigation_create_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_navigation_create_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_navigation_create_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_navigation_create_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_navigation_home_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_navigation_home_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_navigation_home_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_navigation_home_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_navigation_subscriptions_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_navigation_subscriptions_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_navigation_subscriptions_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_navigation_subscriptions_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_cast_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_cast_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_cast_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_cast_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_collapse_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_collapse_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_collapse_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_collapse_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_audio_track_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_audio_track_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_audio_track_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_audio_track_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_help_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_help_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_help_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_help_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_lock_screen_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_lock_screen_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_lock_screen_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_lock_screen_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_playback_speed_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_playback_speed_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_playback_speed_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_playback_speed_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_stable_volume_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_stable_volume_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_stable_volume_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_stable_volume_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_stats_for_nerds_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_stats_for_nerds_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_stats_for_nerds_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_stats_for_nerds_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_watch_in_vr_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_watch_in_vr_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_watch_in_vr_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_flyout_menu_watch_in_vr_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_previous_next_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_previous_next_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_previous_next_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_previous_next_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_youtube_music_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_youtube_music_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_youtube_music_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_player_youtube_music_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_playlist_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_playlist_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_playlist_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_playlist_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_quick_actions_comment_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_quick_actions_comment_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_quick_actions_comment_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_quick_actions_comment_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_quick_actions_like_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_quick_actions_like_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_quick_actions_like_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_quick_actions_like_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_quick_actions_more_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_quick_actions_more_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_quick_actions_more_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_quick_actions_more_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_report_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_report_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_report_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_report_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_rewards_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_rewards_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_rewards_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_rewards_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shop_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shop_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_shop_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shop_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_remix_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_remix_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_remix_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_remix_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_share_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_share_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_share_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_share_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_shelf_history_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_shelf_history_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_shelf_history_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_shelf_history_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_shelf_search_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_shelf_search_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_shelf_search_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_shorts_shelf_search_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_hide_thanks_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_thanks_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_hide_thanks_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_hide_thanks_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_always_repeat_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_always_repeat_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_always_repeat_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_always_repeat_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_copy_video_url_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_copy_video_url_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_copy_video_url_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_copy_video_url_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_copy_video_url_timestamp_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_copy_video_url_timestamp_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_copy_video_url_timestamp_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_copy_video_url_timestamp_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_external_downloader_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_external_downloader_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_external_downloader_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_external_downloader_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_mute_volume_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_mute_volume_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_mute_volume_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_mute_volume_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_time_ordered_playlist_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_play_all_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_time_ordered_playlist_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_play_all_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_speed_dialog_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_speed_dialog_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_speed_dialog_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_speed_dialog_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_whitelist_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_whitelist_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_whitelist_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_overlay_button_whitelist_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_action_buttons_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_action_buttons_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_action_buttons_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_action_buttons_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_ads_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_ads_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_ads_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_ads_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_alt_thumbnails_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_alt_thumbnails_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_alt_thumbnails_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_alt_thumbnails_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_ambient_mode_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_ambient_mode_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_ambient_mode_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_ambient_mode_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_category_bar_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_category_bar_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_category_bar_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_category_bar_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_community_posts_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_community_posts_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_community_posts_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_community_posts_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_custom_filter_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_custom_filter_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_custom_filter_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_custom_filter_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_feed_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_feed_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_feed_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_feed_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_fullscreen_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_fullscreen_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_fullscreen_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_fullscreen_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_import_export_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_import_export_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_import_export_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_import_export_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_misc_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_misc_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_misc_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_misc_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_navigation_bar_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_navigation_bar_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_navigation_bar_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_navigation_bar_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_player_buttons_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_player_buttons_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_player_buttons_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_player_buttons_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_player_flyout_menu_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_player_flyout_menu_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_player_flyout_menu_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_player_flyout_menu_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_player_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_player_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_player_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_player_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_return_youtube_username_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_return_youtube_username_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_return_youtube_username_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_return_youtube_username_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_ryd_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_ryd_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_ryd_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_ryd_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_seekbar_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_seekbar_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_seekbar_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_seekbar_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_settings_menu_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_settings_menu_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_settings_menu_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_settings_menu_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_shorts_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_shorts_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_shorts_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_shorts_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_spoof_streaming_data_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_spoof_streaming_data_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_spoof_streaming_data_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_spoof_streaming_data_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_swipe_controls_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_swipe_controls_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_swipe_controls_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_swipe_controls_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_toolbar_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_toolbar_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_toolbar_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_toolbar_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_video_description_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_video_description_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_video_description_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_video_description_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_video_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_video_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_video_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_preference_screen_video_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_sanitize_sharing_links_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_sanitize_sharing_links_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_sanitize_sharing_links_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_sanitize_sharing_links_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_magnitude_threshold_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_magnitude_threshold_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_swipe_magnitude_threshold_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_magnitude_threshold_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_background_alpha_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_background_alpha_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_background_alpha_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_background_alpha_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_rect_size_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_rect_size_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_rect_size_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_rect_size_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_text_size_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_text_size_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_text_size_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_text_size_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_timeout_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_timeout_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_timeout_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_swipe_overlay_timeout_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/revanced_switch_create_with_notifications_button_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/revanced_switch_create_with_notifications_button_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/revanced_switch_create_with_notifications_button_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/revanced_switch_create_with_notifications_button_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/sb_enable_create_segment_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/sb_enable_create_segment_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/sb_enable_create_segment_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/sb_enable_create_segment_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/sb_enable_voting_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/sb_enable_voting_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/sb_enable_voting_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/sb_enable_voting_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/subscription_product_setting_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/subscription_product_setting_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/subscription_product_setting_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/subscription_product_setting_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/video_quality_settings_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/video_quality_settings_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/video_quality_settings_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/video_quality_settings_key_icon.xml diff --git a/src/main/resources/youtube/visual/shared/drawable/your_data_key_icon.xml b/patches/src/main/resources/youtube/visual/shared/drawable/your_data_key_icon.xml similarity index 100% rename from src/main/resources/youtube/visual/shared/drawable/your_data_key_icon.xml rename to patches/src/main/resources/youtube/visual/shared/drawable/your_data_key_icon.xml diff --git a/settings.gradle.kts b/settings.gradle.kts index 432b20014..bcff5f42d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,7 +1,22 @@ rootProject.name = "revanced-patches" -buildCache { - local { - isEnabled = "CI" !in System.getenv() +pluginManagement { + repositories { + mavenLocal() + gradlePluginPortal() + google() + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/revanced/registry") + credentials { + username = providers.gradleProperty("gpr.user").getOrElse(System.getenv("GITHUB_ACTOR")) + password = providers.gradleProperty("gpr.key").getOrElse(System.getenv("GITHUB_TOKEN")) + } + } } } + +plugins { + id("app.revanced.patches") version "1.0.0-dev.6" +} + diff --git a/src/main/kotlin/app/revanced/generator/JsonPatchesFileGenerator.kt b/src/main/kotlin/app/revanced/generator/JsonPatchesFileGenerator.kt deleted file mode 100644 index c609e0bcb..000000000 --- a/src/main/kotlin/app/revanced/generator/JsonPatchesFileGenerator.kt +++ /dev/null @@ -1,49 +0,0 @@ -package app.revanced.generator - -import app.revanced.patcher.PatchSet -import app.revanced.patcher.patch.Patch -import com.google.gson.GsonBuilder -import java.io.File - -internal class JsonPatchesFileGenerator : PatchesFileGenerator { - override fun generate(patches: PatchSet) = patches.sortedBy { it.name }.map { - JsonPatch( - it.name!!, - it.description, - it.compatiblePackages, - it.use, - it.requiresIntegrations, - it.options.values.map { option -> - JsonPatch.Option( - option.key, - option.default, - option.values, - option.title, - option.description, - option.required, - ) - }, - ) - }.let { - File("patches.json").writeText(GsonBuilder().serializeNulls().create().toJson(it)) - } - - @Suppress("unused") - private class JsonPatch( - val name: String? = null, - val description: String? = null, - val compatiblePackages: Set? = null, - val use: Boolean = true, - val requiresIntegrations: Boolean = false, - val options: List