diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 250871bcc..193a26af0 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -16,6 +16,12 @@ jobs: with: fetch-depth: 0 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + - name: Cache Gradle uses: burrunan/gradle-cache-action@v1 diff --git a/.github/workflows/open_pull_request.yml b/.github/workflows/open_pull_request.yml index 2afa0596c..33c8a7211 100644 --- a/.github/workflows/open_pull_request.yml +++ b/.github/workflows/open_pull_request.yml @@ -20,13 +20,12 @@ jobs: - name: Open pull request uses: repo-sync/pull-request@v2 with: - destination_branch: "main" - pr_title: "chore: ${{ env.MESSAGE }}" + destination_branch: main + pr_title: 'chore: ${{ env.MESSAGE }}' pr_body: | This pull request will ${{ env.MESSAGE }}. ## Before merging this PR - - [ ] Remember about https://github.com/revanced/revanced-integrations - [ ] Pull translations from Crowdin pr_draft: true diff --git a/.github/workflows/pull_strings.yml b/.github/workflows/pull_strings.yml index fee4fcea4..b27d9166a 100644 --- a/.github/workflows/pull_strings.yml +++ b/.github/workflows/pull_strings.yml @@ -2,8 +2,6 @@ name: Pull strings on: workflow_dispatch: - schedule: - - cron: 0 0 1 * * jobs: pull: diff --git a/.github/workflows/push_strings.yml b/.github/workflows/push_strings.yml index a04af2499..0cd3b4eb6 100644 --- a/.github/workflows/push_strings.yml +++ b/.github/workflows/push_strings.yml @@ -6,7 +6,7 @@ on: branches: - dev paths: - - src/main/resources/addresources/values/strings.xml + - patches/src/main/resources/addresources/values/strings.xml jobs: push: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b210aad5c..498cca413 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,13 +23,19 @@ jobs: persist-credentials: false fetch-depth: 0 + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "17" + - name: Cache Gradle uses: burrunan/gradle-cache-action@v1 - name: Build env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: ./gradlew generateMeta clean + run: ./gradlew build clean - name: Setup Node.js uses: actions/setup-node@v4 diff --git a/.gitignore b/.gitignore index 501541766..62f6eb424 100644 --- a/.gitignore +++ b/.gitignore @@ -122,5 +122,8 @@ gradle-app.setting # Dependency directories node_modules/ -# gradle properties, due to Github token +# Gradle properties, due to Github token ./gradle.properties + +# One package is called the same as the Gradle build folder +!**/src/**/build/ \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index e086a70c4..f1854e4b5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -4,5 +4,5 @@ - + \ No newline at end of file diff --git a/.releaserc b/.releaserc index ff81c99ad..b3d61b10b 100644 --- a/.releaserc +++ b/.releaserc @@ -23,7 +23,6 @@ "assets": [ "CHANGELOG.md", "gradle.properties", - "patches.json" ], "message": "chore: Release v${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" } @@ -33,11 +32,8 @@ { "assets": [ { - "path": "build/libs/revanced-patches*" + "path": "patches/build/libs/patches-!(*sources*|*javadoc*).rvp?(.asc)" }, - { - "path": "patches.json" - } ], successComment: false } diff --git a/CHANGELOG.md b/CHANGELOG.md index a841566e6..3a022d197 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,110 @@ +# [5.0.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v5.0.0-dev.3...v5.0.0-dev.4) (2024-11-09) + + +### Bug Fixes + +* **YouTube - Remember video quality:** Correctly set default quality when changing from a low quality video ([#3879](https://github.com/ReVanced/revanced-patches/issues/3879)) ([ddb73e8](https://github.com/ReVanced/revanced-patches/commit/ddb73e857d7c26fd27ea995a27f53f5660d3f71c)) + +# [5.0.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v5.0.0-dev.2...v5.0.0-dev.3) (2024-11-09) + + +### Bug Fixes + +* Add missing dependency to patch ([97f5240](https://github.com/ReVanced/revanced-patches/commit/97f5240d53b9978fb3745170fe03619c7c90274a)) + +# [5.0.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v5.0.0-dev.1...v5.0.0-dev.2) (2024-11-09) + + +### Bug Fixes + +* **YouTube - Return YouTube Dislike:** Show Shorts dislikes with new A/B button icons ([084e0a5](https://github.com/ReVanced/revanced-patches/commit/084e0a527b1c75d1ef15dc706c429aa48d0ffe6b)) + +# [5.0.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.6...v5.0.0-dev.1) (2024-11-06) + + +### Bug Fixes + +* **MyFitnessPal - Hide ads:** Constrain patch to last working version ([#3847](https://github.com/ReVanced/revanced-patches/issues/3847)) ([f9fa526](https://github.com/ReVanced/revanced-patches/commit/f9fa526b04c2848175c389d6bb911aa5a245b60f)) +* **YouTube - Copy video URL:** Support A/B player layout ([0f42574](https://github.com/ReVanced/revanced-patches/commit/0f42574b7f4b1c9a48df8550c7d710093f76ce8c)) +* **YouTube - Custom branding:** Change icon correctly on 19.34+ ([#3866](https://github.com/ReVanced/revanced-patches/issues/3866)) ([2e47903](https://github.com/ReVanced/revanced-patches/commit/2e4790382546256e106a5842cd8c530f41b161e5)) +* **YouTube - Hide ads:** Hide new types of ads ([454281a](https://github.com/ReVanced/revanced-patches/commit/454281ac2108648832b7f0203f5fb7e814887835)) +* **YouTube - Hide layout components:** Remove obsolete 'Hide gray separator' ([a697701](https://github.com/ReVanced/revanced-patches/commit/a697701c5f1f9510b51e310b1ff212b609f38519)) +* **YouTube - Playback speed:** Restore old playback speed menu ([#3817](https://github.com/ReVanced/revanced-patches/issues/3817)) ([806b210](https://github.com/ReVanced/revanced-patches/commit/806b21093e3251697f03cd8804e5d5cd26070716)) +* **YouTube - Remove background playback restrictions:** Enable for Shorts as well ([#3671](https://github.com/ReVanced/revanced-patches/issues/3671)) ([7db1a77](https://github.com/ReVanced/revanced-patches/commit/7db1a7751dc47c4e36096fbdc2b3761b0ae11ccb)) +* **YouTube - Return YouTube Dislike:** Use latest separator height ([ae160a3](https://github.com/ReVanced/revanced-patches/commit/ae160a37985cc96c6de7e1a2fe5a1c83bc523046)) +* **YouTube - Seekbar:** Use latest shade of YouTube red ([4b77648](https://github.com/ReVanced/revanced-patches/commit/4b77648607a84eb29f4cae9ddb42b87084be7cd0)) +* **YouTube - Settings:** Use multiline preference title for localized languages ([#3821](https://github.com/ReVanced/revanced-patches/issues/3821)) ([ff85d49](https://github.com/ReVanced/revanced-patches/commit/ff85d490887de64eb6c6fd42e385a3e75969ff10)) +* **YouTube - SponsorBlock:** Show correct segment behavior in settings UI after importing ([e3f25a0](https://github.com/ReVanced/revanced-patches/commit/e3f25a03cd314eeae786e7660a6beacb275a6a76)) +* **YouTube - Spoof app version:** Remove obsolete 17.33.42 spoof target ([#3825](https://github.com/ReVanced/revanced-patches/issues/3825)) ([33aeba2](https://github.com/ReVanced/revanced-patches/commit/33aeba2a0895e9ecaba27ba4a3b22b86c9f1a51c)) +* **YouTube:** Merge `Restore old seekbar thumbnails` into `Seekbar thumbnails` ([#3860](https://github.com/ReVanced/revanced-patches/issues/3860)) ([e377b1e](https://github.com/ReVanced/revanced-patches/commit/e377b1e6ad93dea8e5f3829cd3894f71851887a3)) + + +### Build System + +* Bump ReVanced Patcher ([eee1692](https://github.com/ReVanced/revanced-patches/commit/eee16922779f994f5752190a20a9016ea98ec4cb)) + + +### Features + +* **YouTube - Hide player flyout menu items:** Hide stable volume ([#3827](https://github.com/ReVanced/revanced-patches/issues/3827)) ([b91e932](https://github.com/ReVanced/revanced-patches/commit/b91e932e65c04b1c1aee9a2f3dc3a73772d9c225)) +* **YouTube - Miniplayer:** Add horizontal drag gesture ([#3859](https://github.com/ReVanced/revanced-patches/issues/3859)) ([e32b19e](https://github.com/ReVanced/revanced-patches/commit/e32b19e170a5571b23547c3211b497089d0cd441)) +* **YouTube - Player flyout menu:** Hide sleep timer ([#3637](https://github.com/ReVanced/revanced-patches/issues/3637)) ([7e1bdab](https://github.com/ReVanced/revanced-patches/commit/7e1bdab520dba65682f018f819c0b7d9783f94ca)) +* **YouTube:** Add `Seekbar thumbnails` patch ([#3813](https://github.com/ReVanced/revanced-patches/issues/3813)) ([5988b75](https://github.com/ReVanced/revanced-patches/commit/5988b759752b944b6999b401faa394e2089e4003)) +* **YouTube:** Support version `19.43.41` ([#3854](https://github.com/ReVanced/revanced-patches/issues/3854)) ([85de5c7](https://github.com/ReVanced/revanced-patches/commit/85de5c7d96ce2d67f6386d1438e43620d31cc645)) + + +### BREAKING CHANGES + +* Various APIs have been changed or removed. + +# [4.18.0-dev.6](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.5...v4.18.0-dev.6) (2024-10-24) + + +### Bug Fixes + +* **YouTube - Playback speed:** Remember playback speed with new speed menu ([#3810](https://github.com/ReVanced/revanced-patches/issues/3810)) ([c3a5e14](https://github.com/ReVanced/revanced-patches/commit/c3a5e14a0a24973a0f9956845c9e0f99c1301d42)) + +# [4.18.0-dev.5](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.4...v4.18.0-dev.5) (2024-10-23) + + +### Features + +* **YouTube:** Hide player shopping shelf in playlists ([#3806](https://github.com/ReVanced/revanced-patches/issues/3806)) ([a553a13](https://github.com/ReVanced/revanced-patches/commit/a553a13c0326ef2fff7f785fed592d553a7963ce)) + +# [4.18.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.3...v4.18.0-dev.4) (2024-10-23) + + +### Features + +* **YouTube - Hide layout components:** Hide player shopping shelf ([#3804](https://github.com/ReVanced/revanced-patches/issues/3804)) ([1952f3b](https://github.com/ReVanced/revanced-patches/commit/1952f3b3c4bca08ed0f6e5b1117e0a6c51f00ed2)) + +# [4.18.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.2...v4.18.0-dev.3) (2024-10-22) + + +### Features + +* **YouTube:** Merge multiple layout patches into `Hide Layout Components` ([#3799](https://github.com/ReVanced/revanced-patches/issues/3799)) ([bbcb57a](https://github.com/ReVanced/revanced-patches/commit/bbcb57a32dfc8f031886f98b1b9701285105c579)) +* **YouTube:** Merge multiple player overlay patches into `Hide player overlay buttons` ([#3800](https://github.com/ReVanced/revanced-patches/issues/3800)) ([4ba0300](https://github.com/ReVanced/revanced-patches/commit/4ba0300590dd988bdcaa0761c4e606c1d7f86ce5)) + +# [4.18.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.18.0-dev.1...v4.18.0-dev.2) (2024-10-22) + + +### Bug Fixes + +* **Twitter - Change link sharing domain:** Support latest app version ([#3786](https://github.com/ReVanced/revanced-patches/issues/3786)) ([b54592c](https://github.com/ReVanced/revanced-patches/commit/b54592cf9c5d859e1af2f02e8e6aaad7d47ab760)) + +# [4.18.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.17.0...v4.18.0-dev.1) (2024-10-21) + + +### Bug Fixes + +* **YouTube - Hide layout components:** Move hide chips settings to Feed menu ([1ed677f](https://github.com/ReVanced/revanced-patches/commit/1ed677f7b8ba561b2bb173dcaf5d6123c22179c4)) + + +### Features + +* **YouTube:** Add `Shorts autoplay` patch ([#3794](https://github.com/ReVanced/revanced-patches/issues/3794)) ([96b5aed](https://github.com/ReVanced/revanced-patches/commit/96b5aede482f7a69d6df17864a2e17568b0da880)) + # [4.17.0](https://github.com/ReVanced/revanced-patches/compare/v4.16.0...v4.17.0) (2024-10-20) diff --git a/api/revanced-patches.api b/api/revanced-patches.api deleted file mode 100644 index ed2f78f37..000000000 --- a/api/revanced-patches.api +++ /dev/null @@ -1,2296 +0,0 @@ -public final class app/revanced/generator/MainKt { - public static synthetic fun main ([Ljava/lang/String;)V -} - -public final class app/revanced/patches/all/activity/exportall/ExportAllActivitiesPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/all/activity/exportall/ExportAllActivitiesPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/all/connectivity/wifi/spoof/SpoofWifiPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/all/connectivity/wifi/spoof/SpoofWifiPatch; - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Triple; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Triple;)V -} - -public final class app/revanced/patches/all/directory/ChangeDataDirectoryLocationPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/all/directory/ChangeDataDirectoryLocationPatch; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Integer; - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;I)V - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V -} - -public final class app/revanced/patches/all/interaction/gestures/PredictiveBackGesturePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/all/interaction/gestures/PredictiveBackGesturePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/all/location/hide/HideMockLocationPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/all/location/hide/HideMockLocationPatch; - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Pair; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Pair;)V -} - -public abstract class app/revanced/patches/all/misc/build/BaseSpoofBuildInfoPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public fun ()V - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Pair; - protected fun getBoard ()Ljava/lang/String; - protected fun getBootloader ()Ljava/lang/String; - protected fun getBrand ()Ljava/lang/String; - protected fun getCpuAbi ()Ljava/lang/String; - protected fun getCpuAbi2 ()Ljava/lang/String; - protected fun getDevice ()Ljava/lang/String; - protected fun getDisplay ()Ljava/lang/String; - protected fun getFingerprint ()Ljava/lang/String; - protected fun getHardware ()Ljava/lang/String; - protected fun getHost ()Ljava/lang/String; - protected fun getId ()Ljava/lang/String; - protected fun getManufacturer ()Ljava/lang/String; - protected fun getModel ()Ljava/lang/String; - protected fun getOdmSku ()Ljava/lang/String; - protected fun getProduct ()Ljava/lang/String; - protected fun getRadio ()Ljava/lang/String; - protected fun getSerial ()Ljava/lang/String; - protected fun getSku ()Ljava/lang/String; - protected fun getSocManufacturer ()Ljava/lang/String; - protected fun getSocModel ()Ljava/lang/String; - protected fun getTags ()Ljava/lang/String; - protected fun getTime ()Ljava/lang/Long; - protected fun getType ()Ljava/lang/String; - protected fun getUser ()Ljava/lang/String; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Pair;)V -} - -public final class app/revanced/patches/all/misc/build/SpoofBuildInfoPatch : app/revanced/patches/all/misc/build/BaseSpoofBuildInfoPatch { - public fun ()V -} - -public final class app/revanced/patches/all/misc/debugging/EnableAndroidDebuggingPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/all/misc/debugging/EnableAndroidDebuggingPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/all/misc/hex/HexPatch : app/revanced/patches/shared/misc/hex/BaseHexPatch { - public fun ()V - public fun getReplacements ()Ljava/util/List; -} - -public final class app/revanced/patches/all/misc/network/OverrideCertificatePinningPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/all/misc/network/OverrideCertificatePinningPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/all/misc/packagename/ChangePackageNamePatch : app/revanced/patcher/patch/ResourcePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/all/misc/packagename/ChangePackageNamePatch; - public fun close ()V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public final fun setOrGetFallbackPackageName (Ljava/lang/String;)Ljava/lang/String; -} - -public final class app/revanced/patches/all/misc/resources/AddResourcesPatch : app/revanced/patcher/patch/ResourcePatch, java/io/Closeable, java/util/Map, kotlin/jvm/internal/markers/KMutableMap { - public static final field INSTANCE Lapp/revanced/patches/all/misc/resources/AddResourcesPatch; - public fun clear ()V - public fun close ()V - public final fun containsKey (Ljava/lang/Object;)Z - public fun containsKey (Ljava/lang/String;)Z - public final fun containsValue (Ljava/lang/Object;)Z - public fun containsValue (Ljava/util/Set;)Z - public final fun entrySet ()Ljava/util/Set; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public final synthetic fun get (Ljava/lang/Object;)Ljava/lang/Object; - public final fun get (Ljava/lang/Object;)Ljava/util/Set; - public fun get (Ljava/lang/String;)Ljava/util/Set; - public fun getEntries ()Ljava/util/Set; - public fun getKeys ()Ljava/util/Set; - public fun getSize ()I - public fun getValues ()Ljava/util/Collection; - public final fun invoke (Ljava/lang/String;Lapp/revanced/util/resource/BaseResource;)Z - public final fun invoke (Ljava/lang/String;Ljava/lang/Iterable;)Z - public final fun invoke (Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)Z - public final fun invoke (Ljava/lang/String;Ljava/util/List;)Z - public final fun invoke (Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;)Z - public static synthetic fun invoke$default (Lapp/revanced/patches/all/misc/resources/AddResourcesPatch;Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;ILjava/lang/Object;)Z - public static synthetic fun invoke$default (Lapp/revanced/patches/all/misc/resources/AddResourcesPatch;Lkotlin/reflect/KClass;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Z - public fun isEmpty ()Z - public final fun keySet ()Ljava/util/Set; - public synthetic fun put (Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; - public fun put (Ljava/lang/String;Ljava/util/Set;)Ljava/util/Set; - public fun putAll (Ljava/util/Map;)V - public final synthetic fun remove (Ljava/lang/Object;)Ljava/lang/Object; - public final fun remove (Ljava/lang/Object;)Ljava/util/Set; - public fun remove (Ljava/lang/String;)Ljava/util/Set; - public final fun size ()I - public final fun values ()Ljava/util/Collection; -} - -public abstract class app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch : app/revanced/patcher/patch/BytecodePatch { - public fun ()V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public abstract fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public final fun findPatchIndices (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;)Lkotlin/sequences/Sequence; - public abstract fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V -} - -public abstract interface class app/revanced/patches/all/misc/transformation/IMethodCall { - public abstract fun getDefinedClassName ()Ljava/lang/String; - public abstract fun getMethodName ()Ljava/lang/String; - public abstract fun getMethodParams ()[Ljava/lang/String; - public abstract fun getReturnType ()Ljava/lang/String; - public abstract fun replaceInvokeVirtualWithIntegrations (Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lcom/android/tools/smali/dexlib2/iface/instruction/formats/Instruction35c;I)V -} - -public final class app/revanced/patches/all/misc/transformation/IMethodCall$DefaultImpls { - public static fun replaceInvokeVirtualWithIntegrations (Lapp/revanced/patches/all/misc/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/all/misc/versioncode/ChangeVersionCodePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/all/misc/versioncode/ChangeVersionCodePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch; - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Triple; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Triple;)V -} - -public final class app/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch$MethodCall : java/lang/Enum, app/revanced/patches/all/misc/transformation/IMethodCall { - public static final field SetAllowedCapturePolicyGlobal Lapp/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch$MethodCall; - public static final field SetAllowedCapturePolicySingle Lapp/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch$MethodCall; - public fun getDefinedClassName ()Ljava/lang/String; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public fun getMethodName ()Ljava/lang/String; - public fun getMethodParams ()[Ljava/lang/String; - public fun getReturnType ()Ljava/lang/String; - public fun replaceInvokeVirtualWithIntegrations (Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lcom/android/tools/smali/dexlib2/iface/instruction/formats/Instruction35c;I)V - public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch$MethodCall; - public static fun values ()[Lapp/revanced/patches/all/screencapture/removerestriction/RemoveCaptureRestrictionPatch$MethodCall; -} - -public final class app/revanced/patches/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Triple; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Triple;)V -} - -public final class app/revanced/patches/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch$MethodCall : java/lang/Enum, app/revanced/patches/all/misc/transformation/IMethodCall { - public static final field AddFlags Lapp/revanced/patches/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch$MethodCall; - public static final field SetFlags Lapp/revanced/patches/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch$MethodCall; - public fun getDefinedClassName ()Ljava/lang/String; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public fun getMethodName ()Ljava/lang/String; - public fun getMethodParams ()[Ljava/lang/String; - public fun getReturnType ()Ljava/lang/String; - public fun replaceInvokeVirtualWithIntegrations (Ljava/lang/String;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lcom/android/tools/smali/dexlib2/iface/instruction/formats/Instruction35c;I)V - public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch$MethodCall; - public static fun values ()[Lapp/revanced/patches/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch$MethodCall; -} - -public final class app/revanced/patches/all/shortcut/sharetargets/RemoveShareTargetsPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/all/shortcut/sharetargets/RemoveShareTargetsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/all/telephony/sim/spoof/SpoofSimCountryPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/all/telephony/sim/spoof/SpoofSimCountryPatch; - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Pair; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Pair;)V -} - -public final class app/revanced/patches/amazon/deeplinking/DeepLinkingPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/amazon/deeplinking/DeepLinkingPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/backdrops/misc/pro/ProUnlockPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/backdrops/misc/pro/ProUnlockPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/bandcamp/limitations/RemovePlayLimitsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/bandcamp/limitations/RemovePlayLimitsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/candylinkvpn/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/candylinkvpn/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/cieid/restrictions/root/BypassRootChecksPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/cieid/restrictions/root/BypassRootChecksPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/duolingo/ad/DisableAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/duolingo/ad/DisableAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/duolingo/debug/EnableDebugMenuPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/duolingo/debug/EnableDebugMenuPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/facebook/ads/mainfeed/HideSponsoredStoriesPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/facebook/ads/mainfeed/HideSponsoredStoriesPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/facebook/ads/story/HideStoryAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/facebook/ads/story/HideStoryAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/finanzonline/detection/bootloader/BootloaderDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/finanzonline/detection/bootloader/BootloaderDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/finanzonline/detection/root/RootDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/finanzonline/detection/root/RootDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/googlenews/customtabs/EnableCustomTabs : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/googlenews/customtabs/EnableCustomTabs; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/googlenews/misc/gms/GmsCoreSupportPatch : app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportPatch { - public static final field INSTANCE Lapp/revanced/patches/googlenews/misc/gms/GmsCoreSupportPatch; -} - -public final class app/revanced/patches/googlenews/misc/gms/GmsCoreSupportResourcePatch : app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/googlenews/misc/gms/GmsCoreSupportResourcePatch; -} - -public final class app/revanced/patches/googlenews/misc/integrations/IntegrationsPatch : app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/googlenews/misc/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/googlephotos/features/SpoofFeaturesPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/googlephotos/features/SpoofFeaturesPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/googlephotos/features/fingerprints/InitializeFeaturesEnumFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/googlephotos/features/fingerprints/InitializeFeaturesEnumFingerprint; -} - -public final class app/revanced/patches/googlephotos/misc/gms/GmsCoreSupportPatch : app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportPatch { - public static final field INSTANCE Lapp/revanced/patches/googlephotos/misc/gms/GmsCoreSupportPatch; -} - -public final class app/revanced/patches/googlephotos/misc/gms/GmsCoreSupportResourcePatch : app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/googlephotos/misc/gms/GmsCoreSupportResourcePatch; -} - -public final class app/revanced/patches/googlephotos/misc/integrations/IntegrationsPatch : app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/googlephotos/misc/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/googlephotos/preferences/RestoreHiddenBackUpWhileChargingTogglePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/googlephotos/preferences/RestoreHiddenBackUpWhileChargingTogglePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/googlerecorder/restrictions/RemoveDeviceRestrictions : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/googlerecorder/restrictions/RemoveDeviceRestrictions; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/hexeditor/ad/DisableAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/hexeditor/ad/DisableAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/iconpackstudio/misc/pro/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/iconpackstudio/misc/pro/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/idaustria/detection/root/RootDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/idaustria/detection/root/RootDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/idaustria/detection/signature/SpoofSignaturePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/idaustria/detection/signature/SpoofSignaturePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/inshorts/ad/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/inshorts/ad/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/instagram/patches/ad/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/instagram/patches/ad/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/instagram/patches/ads/timeline/HideTimelineAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/instagram/patches/ads/timeline/HideTimelineAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/irplus/ad/RemoveAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/irplus/ad/RemoveAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/memegenerator/detection/license/LicenseValidationPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/memegenerator/detection/license/LicenseValidationPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/memegenerator/detection/signature/SignatureVerificationPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/memegenerator/detection/signature/SignatureVerificationPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/memegenerator/misc/pro/UnlockProVersionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/memegenerator/misc/pro/UnlockProVersionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/messenger/inbox/HideInboxAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/messenger/inbox/HideInboxAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/messenger/inbox/HideInboxSubtabsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/messenger/inbox/HideInboxSubtabsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/messenger/inputfield/DisableSwitchingEmojiToStickerPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/messenger/inputfield/DisableSwitchingEmojiToStickerPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/messenger/inputfield/DisableTypingIndicatorPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/messenger/inputfield/DisableTypingIndicatorPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/mifitness/misc/locale/ForceEnglishLocalePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/mifitness/misc/locale/ForceEnglishLocalePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/mifitness/misc/login/FixLoginPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/mifitness/misc/login/FixLoginPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/moneymanager/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/moneymanager/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/ad/video/HideMusicVideoAds : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/ad/video/HideMusicVideoAds; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/ad/video/HideVideoAds : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/ad/video/HideVideoAds; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/ad/video/MusicVideoAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/ad/video/MusicVideoAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/audio/codecs/CodecsUnlockPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/audio/codecs/CodecsUnlockPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/audio/exclusiveaudio/ExclusiveAudioPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/audio/exclusiveaudio/ExclusiveAudioPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/interaction/permanentshuffle/PermanentShufflePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/interaction/permanentshuffle/PermanentShufflePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/interaction/permanentshuffle/PermanentShuffleTogglePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/interaction/permanentshuffle/PermanentShuffleTogglePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/layout/compactheader/CompactHeaderPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/compactheader/CompactHeaderPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/layout/compactheader/HideCategoryBar : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/compactheader/HideCategoryBar; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/layout/minimizedplayback/MinimizedPlaybackPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/minimizedplayback/MinimizedPlaybackPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/layout/premium/HideGetPremiumPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/premium/HideGetPremiumPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/layout/upgradebutton/RemoveUpgradeButtonPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/layout/upgradebutton/RemoveUpgradeButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/music/misc/gms/Constants { - public static final field INSTANCE Lapp/revanced/patches/music/misc/gms/Constants; -} - -public final class app/revanced/patches/music/misc/gms/GmsCoreSupportPatch : app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportPatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/gms/GmsCoreSupportPatch; -} - -public final class app/revanced/patches/music/misc/gms/GmsCoreSupportResourcePatch : app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/gms/GmsCoreSupportResourcePatch; -} - -public final class app/revanced/patches/music/misc/integrations/IntegrationsPatch : app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/music/misc/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/music/premium/backgroundplay/BackgroundPlayPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/music/premium/backgroundplay/BackgroundPlayPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/myexpenses/misc/pro/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/myexpenses/misc/pro/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/myfitnesspal/ads/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/myfitnesspal/ads/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/myfitnesspal/ads/fingerprints/IsPremiumUseCaseImplFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/myfitnesspal/ads/fingerprints/IsPremiumUseCaseImplFingerprint; -} - -public final class app/revanced/patches/myfitnesspal/ads/fingerprints/MainActivityNavigateToNativePremiumUpsellFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/myfitnesspal/ads/fingerprints/MainActivityNavigateToNativePremiumUpsellFingerprint; -} - -public final class app/revanced/patches/netguard/broadcasts/removerestriction/RemoveBroadcastsRestrictionPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/netguard/broadcasts/removerestriction/RemoveBroadcastsRestrictionPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/nfctoolsse/misc/pro/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/nfctoolsse/misc/pro/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/nyx/misc/pro/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/nyx/misc/pro/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/openinghours/misc/fix/crash/FixCrashPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/openinghours/misc/fix/crash/FixCrashPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/photomath/detection/deviceid/SpoofDeviceIdPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/photomath/detection/deviceid/SpoofDeviceIdPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/photomath/detection/signature/SignatureDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/photomath/detection/signature/SignatureDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/photomath/misc/annoyances/HideUpdatePopupPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/photomath/misc/annoyances/HideUpdatePopupPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/photomath/misc/unlock/plus/UnlockPlusPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/photomath/misc/unlock/plus/UnlockPlusPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/piccomafr/misc/SpoofAndroidDeviceIdPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/piccomafr/misc/SpoofAndroidDeviceIdPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/piccomafr/tracking/DisableTrackingPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/piccomafr/tracking/DisableTrackingPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/pixiv/ads/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/pixiv/ads/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/rar/misc/annoyances/purchasereminder/HidePurchaseReminderPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/rar/misc/annoyances/purchasereminder/HidePurchaseReminderPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/rar/misc/annoyances/purchasereminder/fingerprints/ShowReminderFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/rar/misc/annoyances/purchasereminder/fingerprints/ShowReminderFingerprint; -} - -public final class app/revanced/patches/reddit/ad/banner/HideBannerPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/ad/banner/HideBannerPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/reddit/ad/comments/HideCommentAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/ad/comments/HideCommentAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/ad/general/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/ad/general/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/reddit/customclients/BaseFixSLinksPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Lapp/revanced/patcher/fingerprint/MethodFingerprint;Lapp/revanced/patcher/fingerprint/MethodFingerprint;Ljava/util/Set;Ljava/util/Set;)V - public synthetic fun (Lapp/revanced/patcher/fingerprint/MethodFingerprint;Lapp/revanced/patcher/fingerprint/MethodFingerprint;Ljava/util/Set;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - protected abstract fun getIntegrationsClassDescriptor ()Ljava/lang/String; - protected final fun getResolveSLinkMethod ()Ljava/lang/String; - protected final fun getSetAccessTokenMethod ()Ljava/lang/String; - protected abstract fun patchNavigationHandler (Lapp/revanced/patcher/fingerprint/MethodFingerprintResult;Lapp/revanced/patcher/data/BytecodeContext;)V - protected abstract fun patchSetAccessToken (Lapp/revanced/patcher/fingerprint/MethodFingerprintResult;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public abstract class app/revanced/patches/reddit/customclients/BaseSpoofClientPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun getClientId ()Ljava/lang/String; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public fun patchMiscellaneous (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public fun patchUserAgent (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public final fun setClientId (Ljava/lang/String;)V -} - -public final class app/revanced/patches/reddit/customclients/Constants { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/Constants; - public static final field OAUTH_USER_AGENT Ljava/lang/String; -} - -public abstract class app/revanced/patches/reddit/customclients/ads/BaseDisableAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/util/Set;Ljava/util/Set;)V - public synthetic fun (Ljava/util/Set;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/baconreader/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/baconreader/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/boostforreddit/ads/DisableAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/boostforreddit/ads/DisableAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/boostforreddit/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/boostforreddit/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public fun patchUserAgent (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/FixAudioMissingInDownloadsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/FixAudioMissingInDownloadsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/boostforreddit/fix/slink/FixSLinksPatch : app/revanced/patches/reddit/customclients/BaseFixSLinksPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/boostforreddit/fix/slink/FixSLinksPatch; -} - -public final class app/revanced/patches/reddit/customclients/boostforreddit/misc/integrations/IntegrationsPatch : app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/boostforreddit/misc/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/reddit/customclients/infinityforreddit/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/infinityforreddit/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/infinityforreddit/subscription/UnlockSubscriptionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/infinityforreddit/subscription/UnlockSubscriptionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/joeyforreddit/ads/DisableAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/joeyforreddit/ads/DisableAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/joeyforreddit/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/joeyforreddit/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public fun patchUserAgent (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/joeyforreddit/api/fingerprints/AuthUtilityUserAgent : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/joeyforreddit/api/fingerprints/AuthUtilityUserAgent; -} - -public final class app/revanced/patches/reddit/customclients/joeyforreddit/detection/piracy/DisablePiracyDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/joeyforreddit/detection/piracy/DisablePiracyDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/redditisfun/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/redditisfun/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public fun patchMiscellaneous (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public fun patchUserAgent (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/relayforreddit/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/relayforreddit/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public fun patchMiscellaneous (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/slide/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/slide/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/syncforlemmy/ads/DisableAdsPatch : app/revanced/patches/reddit/customclients/ads/BaseDisableAdsPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforlemmy/ads/DisableAdsPatch; -} - -public final class app/revanced/patches/reddit/customclients/syncforreddit/ads/DisableAdsPatch : app/revanced/patches/reddit/customclients/ads/BaseDisableAdsPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforreddit/ads/DisableAdsPatch; -} - -public final class app/revanced/patches/reddit/customclients/syncforreddit/annoyances/startup/DisableSyncForLemmyBottomSheetPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforreddit/annoyances/startup/DisableSyncForLemmyBottomSheetPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/syncforreddit/api/SpoofClientPatch : app/revanced/patches/reddit/customclients/BaseSpoofClientPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforreddit/api/SpoofClientPatch; - public fun patchClientId (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public fun patchMiscellaneous (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V - public fun patchUserAgent (Ljava/util/Set;Lapp/revanced/patcher/data/BytecodeContext;)V -} - -public final class app/revanced/patches/reddit/customclients/syncforreddit/detection/piracy/DisablePiracyDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforreddit/detection/piracy/DisablePiracyDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/syncforreddit/fix/slink/FixSLinksPatch : app/revanced/patches/reddit/customclients/BaseFixSLinksPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforreddit/fix/slink/FixSLinksPatch; -} - -public final class app/revanced/patches/reddit/customclients/syncforreddit/fix/user/UseUserEndpointPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforreddit/fix/user/UseUserEndpointPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/syncforreddit/fix/video/FixVideoDownloadsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforreddit/fix/video/FixVideoDownloadsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/customclients/syncforreddit/misc/integrations/IntegrationsPatch : app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/customclients/syncforreddit/misc/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/reddit/layout/disablescreenshotpopup/DisableScreenshotPopupPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/layout/disablescreenshotpopup/DisableScreenshotPopupPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/layout/premiumicon/UnlockPremiumIconPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/layout/premiumicon/UnlockPremiumIconPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/reddit/misc/tracking/url/SanitizeUrlQueryPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/reddit/misc/tracking/url/SanitizeUrlQueryPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/scbeasy/detection/debugging/RemoveDebuggingDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/scbeasy/detection/debugging/RemoveDebuggingDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/serviceportalbund/detection/root/RootDetectionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/serviceportalbund/detection/root/RootDetectionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/misc/checks/BaseCheckEnvironmentPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Lapp/revanced/patcher/fingerprint/MethodFingerprint;Ljava/util/Set;Lapp/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/shared/misc/fix/verticalscroll/VerticalScrollPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/shared/misc/fix/verticalscroll/VerticalScrollPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patcher/fingerprint/MethodFingerprint;Ljava/util/Set;Lapp/revanced/patcher/fingerprint/MethodFingerprint;Lkotlin/reflect/KClass;Lapp/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patcher/fingerprint/MethodFingerprint;Ljava/util/Set;Lapp/revanced/patcher/fingerprint/MethodFingerprint;Lkotlin/reflect/KClass;Lapp/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch;Ljava/util/Set;Ljava/util/Set;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - protected final fun getGmsCoreVendor ()Ljava/lang/String; - protected final fun getGmsCoreVendorGroupId ()Ljava/lang/String; -} - -public abstract class app/revanced/patches/shared/misc/hex/BaseHexPatch : app/revanced/patcher/patch/RawResourcePatch { - public fun ()V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public abstract fun getReplacements ()Ljava/util/List; -} - -public final class app/revanced/patches/shared/misc/hex/BaseHexPatch$Replacement { - public static final field Companion Lapp/revanced/patches/shared/misc/hex/BaseHexPatch$Replacement$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public final fun replacePattern ([B)V -} - -public final class app/revanced/patches/shared/misc/hex/BaseHexPatch$Replacement$Companion { -} - -public abstract class app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/lang/String;Ljava/util/Set;)V - public fun (Ljava/util/Set;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch$IntegrationsFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun invoke (Ljava/lang/String;)V -} - -public abstract interface class app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IHookInsertIndexResolver : kotlin/jvm/functions/Function1 { - public abstract fun invoke (Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/Integer; -} - -public final class app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IHookInsertIndexResolver$DefaultImpls { - public static fun invoke (Lapp/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IHookInsertIndexResolver;Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/Integer; -} - -public abstract interface class app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IRegisterResolver : kotlin/jvm/functions/Function1 { - public abstract fun invoke (Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/Integer; -} - -public final class app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IRegisterResolver$DefaultImpls { - public static fun invoke (Lapp/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch$IntegrationsFingerprint$IRegisterResolver;Lcom/android/tools/smali/dexlib2/iface/Method;)Ljava/lang/Integer; -} - -public final class app/revanced/patches/shared/misc/mapping/ResourceMappingPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/shared/misc/mapping/ResourceMappingPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public final fun get (Ljava/lang/String;Ljava/lang/String;)J -} - -public final class app/revanced/patches/shared/misc/mapping/ResourceMappingPatch$ResourceElement { - public fun (Ljava/lang/String;Ljava/lang/String;J)V - public final fun component1 ()Ljava/lang/String; - public final fun component2 ()Ljava/lang/String; - public final fun component3 ()J - public final fun copy (Ljava/lang/String;Ljava/lang/String;J)Lapp/revanced/patches/shared/misc/mapping/ResourceMappingPatch$ResourceElement; - public static synthetic fun copy$default (Lapp/revanced/patches/shared/misc/mapping/ResourceMappingPatch$ResourceElement;Ljava/lang/String;Ljava/lang/String;JILjava/lang/Object;)Lapp/revanced/patches/shared/misc/mapping/ResourceMappingPatch$ResourceElement; - public fun equals (Ljava/lang/Object;)Z - public final fun getId ()J - public final fun getName ()Ljava/lang/String; - public final fun getType ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public abstract class app/revanced/patches/shared/misc/settings/BaseSettingsResourcePatch : app/revanced/patcher/patch/ResourcePatch, java/io/Closeable, java/util/Set, kotlin/jvm/internal/markers/KMutableSet { - public fun ()V - public fun (Lkotlin/Pair;Ljava/util/Set;)V - public synthetic fun (Lkotlin/Pair;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun add (Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)Z - public synthetic fun add (Ljava/lang/Object;)Z - public fun addAll (Ljava/util/Collection;)Z - public fun clear ()V - public fun close ()V - public fun contains (Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)Z - public final fun contains (Ljava/lang/Object;)Z - public fun containsAll (Ljava/util/Collection;)Z - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V - public fun getSize ()I - public fun isEmpty ()Z - public fun iterator ()Ljava/util/Iterator; - public fun remove (Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)Z - public final fun remove (Ljava/lang/Object;)Z - public fun removeAll (Ljava/util/Collection;)Z - public fun retainAll (Ljava/util/Collection;)Z - public final fun size ()I - public fun toArray ()[Ljava/lang/Object; - public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; -} - -public abstract class app/revanced/patches/shared/misc/settings/preference/BasePreference { - public static final field Companion Lapp/revanced/patches/shared/misc/settings/preference/BasePreference$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun equals (Ljava/lang/Object;)Z - public final fun getKey ()Ljava/lang/String; - public final fun getSummaryKey ()Ljava/lang/String; - public final fun getTag ()Ljava/lang/String; - public final fun getTitleKey ()Ljava/lang/String; - public fun hashCode ()I - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/patches/shared/misc/settings/preference/BasePreference$Companion { - public final fun addSummary (Lorg/w3c/dom/Element;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/SummaryType;)V - public static synthetic fun addSummary$default (Lapp/revanced/patches/shared/misc/settings/preference/BasePreference$Companion;Lorg/w3c/dom/Element;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/SummaryType;ILjava/lang/Object;)V -} - -public abstract class app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen : java/io/Closeable { - public fun ()V - public fun (Ljava/util/Set;)V - public synthetic fun (Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun close ()V - public abstract fun commit (Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen;)V -} - -public abstract class app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$BasePreferenceCollection { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getKey ()Ljava/lang/String; - public final fun getPreferences ()Ljava/util/Set; - public final fun getTitleKey ()Ljava/lang/String; - public abstract fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreference; -} - -public class app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen : app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$BasePreferenceCollection { - public fun (Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting;)V - public synthetic fun (Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun addPreferences ([Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)V - public final fun getCategories ()Ljava/util/Set; - public synthetic fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreference; - public fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen; -} - -public class app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen$Category : app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$BasePreferenceCollection { - public fun (Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun addPreferences ([Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)V - public synthetic fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreference; - public fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/PreferenceCategory; -} - -public final class app/revanced/patches/shared/misc/settings/preference/InputType : java/lang/Enum { - public static final field NUMBER Lapp/revanced/patches/shared/misc/settings/preference/InputType; - public static final field TEXT Lapp/revanced/patches/shared/misc/settings/preference/InputType; - public static final field TEXT_CAP_CHARACTERS Lapp/revanced/patches/shared/misc/settings/preference/InputType; - public static final field TEXT_MULTI_LINE Lapp/revanced/patches/shared/misc/settings/preference/InputType; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public final fun getType ()Ljava/lang/String; - public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/shared/misc/settings/preference/InputType; - public static fun values ()[Lapp/revanced/patches/shared/misc/settings/preference/InputType; -} - -public final class app/revanced/patches/shared/misc/settings/preference/IntentPreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun equals (Ljava/lang/Object;)Z - public final fun getIntent ()Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent; - public fun hashCode ()I - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent { - public fun (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V - public final fun copy (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent; - public static synthetic fun copy$default (Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent; - public fun equals (Ljava/lang/Object;)Z - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class app/revanced/patches/shared/misc/settings/preference/ListPreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/util/resource/ArrayResource;Lapp/revanced/util/resource/ArrayResource;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/util/resource/ArrayResource;Lapp/revanced/util/resource/ArrayResource;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getEntries ()Lapp/revanced/util/resource/ArrayResource; - public final fun getEntriesKey ()Ljava/lang/String; - public final fun getEntryValues ()Lapp/revanced/util/resource/ArrayResource; - public final fun getEntryValuesKey ()Ljava/lang/String; - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/patches/shared/misc/settings/preference/NonInteractivePreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getSelectable ()Z - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public class app/revanced/patches/shared/misc/settings/preference/PreferenceCategory : app/revanced/patches/shared/misc/settings/preference/BasePreference { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getPreferences ()Ljava/util/Set; - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public class app/revanced/patches/shared/misc/settings/preference/PreferenceScreen : app/revanced/patches/shared/misc/settings/preference/BasePreference { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting;Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getPreferences ()Ljava/util/Set; - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting : java/lang/Enum { - public static final field BY_KEY Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting; - public static final field BY_TITLE Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting; - public static final field UNSORTED Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public final fun getKeySuffix ()Ljava/lang/String; - public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting; - public static fun values ()[Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen$Sorting; -} - -public final class app/revanced/patches/shared/misc/settings/preference/SummaryType : java/lang/Enum { - public static final field DEFAULT Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; - public static final field OFF Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; - public static final field ON Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; - public static fun getEntries ()Lkotlin/enums/EnumEntries; - public final fun getType ()Ljava/lang/String; - public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; - public static fun values ()[Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; -} - -public final class app/revanced/patches/shared/misc/settings/preference/SwitchPreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getSummaryOffKey ()Ljava/lang/String; - public final fun getSummaryOnKey ()Ljava/lang/String; - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/patches/shared/misc/settings/preference/TextPreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { - public fun ()V - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/InputType;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/InputType;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getInputType ()Lapp/revanced/patches/shared/misc/settings/preference/InputType; - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/patches/solidexplorer2/functionality/filesize/RemoveFileSizeLimitPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/solidexplorer2/functionality/filesize/RemoveFileSizeLimitPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/songpal/badge/BadgeTabPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field ACTIVITY_TAB_DESCRIPTOR Ljava/lang/String; - public static final field INSTANCE Lapp/revanced/patches/songpal/badge/BadgeTabPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/songpal/badge/RemoveNotificationBadgePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/songpal/badge/RemoveNotificationBadgePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/soundcloud/ad/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/soundcloud/ad/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/soundcloud/analytics/DisableTelemetryPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/soundcloud/analytics/DisableTelemetryPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/soundcloud/offlinesync/EnableOfflineSyncPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/soundcloud/offlinesync/EnableOfflineSyncPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/spotify/layout/theme/CustomThemePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/spotify/layout/theme/CustomThemePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/spotify/lite/ondemand/OnDemandPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/spotify/lite/ondemand/OnDemandPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/spotify/navbar/PremiumNavbarTabPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/spotify/navbar/PremiumNavbarTabPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/spotify/navbar/PremiumNavbarTabResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/spotify/navbar/PremiumNavbarTabResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/stocard/layout/HideOffersTabPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/stocard/layout/HideOffersTabPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/stocard/layout/HideStoryBubblesPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/stocard/layout/HideStoryBubblesPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/strava/subscription/UnlockSubscriptionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/strava/subscription/UnlockSubscriptionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/strava/upselling/DisableSubscriptionSuggestionsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/strava/upselling/DisableSubscriptionSuggestionsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/swissid/integritycheck/RemoveGooglePlayIntegrityCheck : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/swissid/integritycheck/RemoveGooglePlayIntegrityCheck; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/ticktick/misc/themeunlock/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/ticktick/misc/themeunlock/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/feedfilter/FeedFilterPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/feedfilter/FeedFilterPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/interaction/cleardisplay/RememberClearDisplayPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/interaction/cleardisplay/RememberClearDisplayPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/interaction/downloads/DownloadsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/interaction/downloads/DownloadsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/interaction/seekbar/ShowSeekbarPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/interaction/seekbar/ShowSeekbarPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/interaction/speed/PlaybackSpeedPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/interaction/speed/PlaybackSpeedPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/misc/integrations/IntegrationsPatch : app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/misc/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/tiktok/misc/login/disablerequirement/DisableLoginRequirementPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/misc/login/disablerequirement/DisableLoginRequirementPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/misc/login/fixgoogle/FixGoogleLoginPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/misc/login/fixgoogle/FixGoogleLoginPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/misc/settings/SettingsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/misc/settings/SettingsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tiktok/misc/spoof/sim/SpoofSimPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tiktok/misc/spoof/sim/SpoofSimPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/trakt/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/trakt/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tudortmund/lockscreen/patch/ShowOnLockscreenPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tudortmund/lockscreen/patch/ShowOnLockscreenPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/ads/DisableDashboardAds : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/ads/DisableDashboardAds; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/annoyances/adfree/DisableAdFreeBannerPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/annoyances/adfree/DisableAdFreeBannerPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/annoyances/inappupdate/DisableInAppUpdatePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/annoyances/inappupdate/DisableInAppUpdatePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/annoyances/notifications/DisableBlogNotificationReminderPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/annoyances/notifications/DisableBlogNotificationReminderPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/annoyances/popups/DisableGiftMessagePopupPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/annoyances/popups/DisableGiftMessagePopupPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/featureflags/OverrideFeatureFlagsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/featureflags/OverrideFeatureFlagsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/fixes/FixOldVersionsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/fixes/FixOldVersionsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/live/DisableTumblrLivePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/live/DisableTumblrLivePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/tumblr/timelinefilter/TimelineFilterPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/tumblr/timelinefilter/TimelineFilterPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitch/ad/audio/AudioAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/ad/audio/AudioAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitch/ad/embedded/EmbeddedAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/ad/embedded/EmbeddedAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/twitch/ad/shared/util/BaseAdPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - protected final fun blockMethods (Lapp/revanced/patcher/data/BytecodeContext;Ljava/lang/String;[Ljava/lang/String;Lapp/revanced/patches/twitch/ad/shared/util/BaseAdPatch$ReturnMethod;)Z - public static synthetic fun blockMethods$default (Lapp/revanced/patches/twitch/ad/shared/util/BaseAdPatch;Lapp/revanced/patcher/data/BytecodeContext;Ljava/lang/String;[Ljava/lang/String;Lapp/revanced/patches/twitch/ad/shared/util/BaseAdPatch$ReturnMethod;ILjava/lang/Object;)Z - protected final fun createConditionInstructions (Ljava/lang/String;)Ljava/lang/String; - public static synthetic fun createConditionInstructions$default (Lapp/revanced/patches/twitch/ad/shared/util/BaseAdPatch;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String; - public final fun getConditionCall ()Ljava/lang/String; - public final fun getSkipLabelName ()Ljava/lang/String; -} - -protected final class app/revanced/patches/twitch/ad/shared/util/BaseAdPatch$ReturnMethod { - public fun ()V - public fun (CLjava/lang/String;)V - public synthetic fun (CLjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun component1 ()C - public final fun component2 ()Ljava/lang/String; - public final fun copy (CLjava/lang/String;)Lapp/revanced/patches/twitch/ad/shared/util/BaseAdPatch$ReturnMethod; - public static synthetic fun copy$default (Lapp/revanced/patches/twitch/ad/shared/util/BaseAdPatch$ReturnMethod;CLjava/lang/String;ILjava/lang/Object;)Lapp/revanced/patches/twitch/ad/shared/util/BaseAdPatch$ReturnMethod; - public fun equals (Ljava/lang/Object;)Z - public final fun getReturnType ()C - public final fun getValue ()Ljava/lang/String; - public fun hashCode ()I - public fun toString ()Ljava/lang/String; -} - -public final class app/revanced/patches/twitch/ad/video/VideoAdsPatch : app/revanced/patches/twitch/ad/shared/util/BaseAdPatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/ad/video/VideoAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitch/chat/antidelete/ShowDeletedMessagesPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/chat/antidelete/ShowDeletedMessagesPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitch/chat/autoclaim/AutoClaimChannelPointsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/chat/autoclaim/AutoClaimChannelPointsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitch/debug/DebugModePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/debug/DebugModePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitch/misc/integrations/IntegrationsPatch : app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/misc/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/twitch/misc/settings/SettingsPatch : app/revanced/patcher/patch/BytecodePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/twitch/misc/settings/SettingsPatch; - public fun close ()V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitch/misc/settings/SettingsResourcePatch : app/revanced/patches/shared/misc/settings/BaseSettingsResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/twitch/misc/settings/SettingsResourcePatch; -} - -public final class app/revanced/patches/twitter/interaction/downloads/UnlockDownloadsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitter/interaction/downloads/UnlockDownloadsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitter/layout/viewcount/HideViewCountPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitter/layout/viewcount/HideViewCountPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitter/misc/dynamiccolor/DynamicColorPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/twitter/misc/dynamiccolor/DynamicColorPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/twitter/misc/hook/json/JsonHookPatch : app/revanced/patcher/patch/BytecodePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/twitter/misc/hook/json/JsonHookPatch; - public fun close ()V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public abstract class app/revanced/patches/twitter/misc/hook/patch/BaseHookPatch : app/revanced/patcher/patch/BytecodePatch { - public fun (Ljava/lang/String;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitter/misc/hook/patch/ads/HideAdsHookPatch : app/revanced/patches/twitter/misc/hook/patch/BaseHookPatch { - public static final field INSTANCE Lapp/revanced/patches/twitter/misc/hook/patch/ads/HideAdsHookPatch; -} - -public final class app/revanced/patches/twitter/misc/hook/patch/recommendation/HideRecommendedUsersPatch : app/revanced/patches/twitter/misc/hook/patch/BaseHookPatch { - public static final field INSTANCE Lapp/revanced/patches/twitter/misc/hook/patch/recommendation/HideRecommendedUsersPatch; -} - -public final class app/revanced/patches/twitter/misc/links/ChangeLinkSharingDomainPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitter/misc/links/ChangeLinkSharingDomainPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitter/misc/links/OpenLinksWithAppChooserPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitter/misc/links/OpenLinksWithAppChooserPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/twitter/misc/links/SanitizeSharingLinksPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/twitter/misc/links/SanitizeSharingLinksPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/vsco/misc/pro/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/vsco/misc/pro/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/warnwetter/misc/firebasegetcert/FirebaseGetCertPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/warnwetter/misc/firebasegetcert/FirebaseGetCertPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/warnwetter/misc/promocode/PromoCodeUnlockPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/warnwetter/misc/promocode/PromoCodeUnlockPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/willhaben/ads/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/willhaben/ads/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/windyapp/misc/unlockpro/UnlockProPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/windyapp/misc/unlockpro/UnlockProPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/ad/general/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/ad/general/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/ad/general/HideAdsResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/ad/general/HideAdsResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/ad/getpremium/HideGetPremiumPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/ad/getpremium/HideGetPremiumPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/ad/video/VideoAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/ad/video/VideoAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoUrlBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/copyvideourl/CopyVideoUrlBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/interaction/downloads/DownloadsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/downloads/DownloadsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/interaction/seekbar/DisablePreciseSeekingGesturePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/seekbar/DisablePreciseSeekingGesturePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/interaction/seekbar/EnableSeekbarTappingPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/seekbar/EnableSeekbarTappingPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/branding/CustomBrandingPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/branding/CustomBrandingPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/branding/header/PremiumHeadingPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/branding/header/PremiumHeadingPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/buttons/action/HideButtonsPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/buttons/action/HideButtonsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/buttons/autoplay/HideAutoplayButtonPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/buttons/autoplay/HideAutoplayButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/buttons/captions/HideCaptionsButtonPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/buttons/captions/HideCaptionsButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/buttons/cast/HideCastButtonPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/buttons/cast/HideCastButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/buttons/navigation/NavigationButtonsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/buttons/navigation/NavigationButtonsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/buttons/player/hide/HidePlayerButtonsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/buttons/player/hide/HidePlayerButtonsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/albumcards/AlbumCardsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/albumcards/AlbumCardsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/breakingnews/BreakingNewsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/breakingnews/BreakingNewsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/comments/CommentsPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/comments/CommentsPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/hide/crowdfundingbox/CrowdfundingBoxPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/crowdfundingbox/CrowdfundingBoxPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/endscreencards/HideEndscreenCardsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/endscreencards/HideEndscreenCardsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/filterbar/HideFilterBarPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/filterbar/HideFilterBarPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/floatingmicrophone/HideFloatingMicrophoneButtonPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/floatingmicrophone/HideFloatingMicrophoneButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/infocards/HideInfocardsResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/infocards/HideInfocardsResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/hide/loadmorebutton/HideLoadMoreButtonPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/loadmorebutton/HideLoadMoreButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/player/flyoutmenupanel/HidePlayerFlyoutMenuPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/player/flyoutmenupanel/HidePlayerFlyoutMenuPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/seekbar/HideSeekbarPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/seekbar/HideSeekbarPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/hide/suggestedvideoendscreen/DisableSuggestedVideoEndScreenPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/suggestedvideoendscreen/DisableSuggestedVideoEndScreenPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/hide/time/HideTimestampPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/hide/time/HideTimestampPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/panels/popup/PlayerPopupPanelsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/panels/popup/PlayerPopupPanelsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/player/background/PlayerControlsBackgroundPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/player/background/PlayerControlsBackgroundPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/searchbar/WideSearchbarPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/searchbar/WideSearchbarPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/seekbar/RestoreOldSeekbarThumbnailsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/seekbar/RestoreOldSeekbarThumbnailsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/sponsorblock/SponsorBlockBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/startupshortsreset/fingerprints/UserWasInShortsFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/startupshortsreset/fingerprints/UserWasInShortsFingerprint; -} - -public final class app/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/tabletminiplayer/TabletMiniPlayerPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/tabletminiplayer/TabletMiniPlayerPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/theme/ThemeBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/theme/ThemeBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/layout/thumbnails/BypassImageRegionRestrictions : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/layout/thumbnails/BypassImageRegionRestrictions; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/announcements/AnnouncementsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/autorepeat/AutoRepeatPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/autorepeat/AutoRepeatPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/check/CheckEnvironmentPatch : app/revanced/patches/shared/misc/checks/BaseCheckEnvironmentPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/check/CheckEnvironmentPatch; -} - -public final class app/revanced/patches/youtube/misc/debugging/DebuggingPatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/debugging/DebuggingPatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/fix/playback/SpoofClientPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/fix/playback/SpoofClientPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/fix/playback/SpoofSignaturePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/fix/playback/SpoofSignaturePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/fix/playback/SpoofSignatureResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/fix/playback/SpoofSignatureResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/fix/playback/SpoofVideoStreamsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/fix/playback/SpoofVideoStreamsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/fix/playback/UserAgentClientSpoofPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/fix/playback/UserAgentClientSpoofPatch; - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Triple; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Triple;)V -} - -public final class app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch : app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch; -} - -public final class app/revanced/patches/youtube/misc/gms/GmsCoreSupportResourcePatch : app/revanced/patches/shared/misc/gms/BaseGmsCoreSupportResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/gms/GmsCoreSupportResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHook : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHook; - public final fun addImageUrlErrorCallbackHook (Ljava/lang/String;)V - public final fun addImageUrlHook (Ljava/lang/String;Z)V - public static synthetic fun addImageUrlHook$default (Lapp/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHook;Ljava/lang/String;ZILjava/lang/Object;)V - public final fun addImageUrlSuccessCallbackHook (Ljava/lang/String;)V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/integrations/IntegrationsPatch : app/revanced/patches/shared/misc/integrations/BaseIntegrationsPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/integrations/IntegrationsPatch; -} - -public final class app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch : app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public synthetic fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Ljava/lang/Object; - public fun filterMap (Lcom/android/tools/smali/dexlib2/iface/ClassDef;Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/iface/instruction/Instruction;I)Lkotlin/Pair; - public synthetic fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Ljava/lang/Object;)V - public fun transform (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;Lkotlin/Pair;)V -} - -public final class app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch : app/revanced/patcher/patch/BytecodePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch; - public fun close ()V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/minimizedplayback/MinimizedPlaybackPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/minimizedplayback/MinimizedPlaybackPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun getHookNavigationButtonCreated ()Lkotlin/jvm/functions/Function1; -} - -public final class app/revanced/patches/youtube/misc/playercontrols/BottomControlsResourcePatch : app/revanced/patcher/patch/ResourcePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/playercontrols/BottomControlsResourcePatch; - public final fun addControls (Ljava/lang/String;)V - public fun close ()V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/playercontrols/PlayerControlsBytecodePatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/playercontrols/PlayerControlsBytecodePatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun initializeBottomControl (Ljava/lang/String;)V - public final fun initializeControl (Ljava/lang/String;)V - public final fun injectVisibilityCheckCall (Ljava/lang/String;)V -} - -public final class app/revanced/patches/youtube/misc/playercontrols/PlayerControlsResourcePatch : app/revanced/patcher/patch/ResourcePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/playercontrols/PlayerControlsResourcePatch; - public final fun addBottomControls (Ljava/lang/String;)V - public fun close ()V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/playeroverlay/PlayerOverlaysHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/playeroverlay/PlayerOverlaysHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/playertype/PlayerTypeHookPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/playertype/PlayerTypeHookPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/privacy/RemoveTrackingQueryParameterPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/privacy/RemoveTrackingQueryParameterPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/misc/settings/SettingsPatch : app/revanced/patcher/patch/BytecodePatch, java/io/Closeable { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/settings/SettingsPatch; - public fun close ()V - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun newIntent (Ljava/lang/String;)Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent; -} - -public final class app/revanced/patches/youtube/misc/settings/SettingsPatch$PreferenceScreen : app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/settings/SettingsPatch$PreferenceScreen; - public fun commit (Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreen;)V - public final fun getADS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getALTERNATIVE_THUMBNAILS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getFEED ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getGENERAL_LAYOUT ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getMISC ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getPLAYER ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getSEEKBAR ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getSHORTS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getSWIPE_CONTROLS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; - public final fun getVIDEO ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; -} - -public final class app/revanced/patches/youtube/misc/settings/SettingsResourcePatch : app/revanced/patches/shared/misc/settings/BaseSettingsResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/settings/SettingsResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtube/misc/zoomhaptics/ZoomHapticsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/misc/zoomhaptics/ZoomHapticsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/hdrbrightness/HDRBrightnessPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/hdrbrightness/HDRBrightnessPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/information/VideoInformationPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/information/VideoInformationPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch : app/revanced/patcher/patch/BytecodePatch, java/io/Closeable, java/util/Set, kotlin/jvm/internal/markers/KMutableSet { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch; - public fun add (Lapp/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch$Hook;)Z - public synthetic fun add (Ljava/lang/Object;)Z - public fun addAll (Ljava/util/Collection;)Z - public fun clear ()V - public fun close ()V - public fun contains (Lapp/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch$Hook;)Z - public final fun contains (Ljava/lang/Object;)Z - public fun containsAll (Ljava/util/Collection;)Z - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun getSize ()I - public fun isEmpty ()Z - public fun iterator ()Ljava/util/Iterator; - public fun remove (Lapp/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch$Hook;)Z - public final fun remove (Ljava/lang/Object;)Z - public fun removeAll (Ljava/util/Collection;)Z - public fun retainAll (Ljava/util/Collection;)Z - public final fun size ()I - public fun toArray ()[Ljava/lang/Object; - public fun toArray ([Ljava/lang/Object;)[Ljava/lang/Object; -} - -public final class app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/quality/RememberVideoQualityPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/speed/PlaybackSpeedPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/speed/button/PlaybackSpeedButtonPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/speed/button/PlaybackSpeedButtonPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/videoid/VideoIdPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/videoid/VideoIdPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public final fun hookBackgroundPlayVideoId (Ljava/lang/String;)V - public final fun hookPlayerResponseVideoId (Ljava/lang/String;)V - public final fun hookVideoId (Ljava/lang/String;)V -} - -public final class app/revanced/patches/youtube/video/videoqualitymenu/RestoreOldVideoQualityMenuPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/videoqualitymenu/RestoreOldVideoQualityMenuPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/youtube/video/videoqualitymenu/RestoreOldVideoQualityMenuResourcePatch : app/revanced/patcher/patch/ResourcePatch { - public static final field INSTANCE Lapp/revanced/patches/youtube/video/videoqualitymenu/RestoreOldVideoQualityMenuResourcePatch; - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V - public fun execute (Lapp/revanced/patcher/data/ResourceContext;)V -} - -public final class app/revanced/patches/youtubevanced/ad/general/HideAdsPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/youtubevanced/ad/general/HideAdsPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/patches/yuka/misc/unlockpremium/UnlockPremiumPatch : app/revanced/patcher/patch/BytecodePatch { - public static final field INSTANCE Lapp/revanced/patches/yuka/misc/unlockpremium/UnlockPremiumPatch; - public fun execute (Lapp/revanced/patcher/data/BytecodeContext;)V - public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V -} - -public final class app/revanced/util/BytecodeUtilsKt { - public static final fun alsoResolve (Lapp/revanced/patcher/fingerprint/MethodFingerprint;Lapp/revanced/patcher/data/BytecodeContext;Lapp/revanced/patcher/fingerprint/MethodFingerprint;)Lapp/revanced/patcher/fingerprint/MethodFingerprintResult; - public static final fun containsWideLiteralInstructionValue (Lcom/android/tools/smali/dexlib2/iface/Method;J)Z - public static final fun findMutableMethodOf (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lcom/android/tools/smali/dexlib2/iface/Method;)Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod; - public static final fun findOpcodeIndicesReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)Ljava/util/List; - public static final fun findOpcodeIndicesReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Lkotlin/jvm/functions/Function1;)Ljava/util/List; - public static final fun forEachLiteralValueInstruction (Lapp/revanced/patcher/data/BytecodeContext;JLkotlin/jvm/functions/Function2;)V - public static final fun getException (Lapp/revanced/patcher/fingerprint/MethodFingerprint;)Lapp/revanced/patcher/patch/PatchException; - 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 final fun indexOfFirstInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;Lkotlin/jvm/functions/Function1;)I - public static synthetic fun indexOfFirstInstruction$default (Lcom/android/tools/smali/dexlib2/iface/Method;ILcom/android/tools/smali/dexlib2/Opcode;ILjava/lang/Object;)I - public static synthetic fun indexOfFirstInstruction$default (Lcom/android/tools/smali/dexlib2/iface/Method;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)I - public static final fun indexOfFirstInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;ILcom/android/tools/smali/dexlib2/Opcode;)I - public static final fun indexOfFirstInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;ILkotlin/jvm/functions/Function1;)I - public static final fun indexOfFirstInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)I - public static synthetic fun indexOfFirstInstructionOrThrow$default (Lcom/android/tools/smali/dexlib2/iface/Method;ILcom/android/tools/smali/dexlib2/Opcode;ILjava/lang/Object;)I - public static synthetic fun indexOfFirstInstructionOrThrow$default (Lcom/android/tools/smali/dexlib2/iface/Method;ILkotlin/jvm/functions/Function1;ILjava/lang/Object;)I - public static final fun indexOfFirstInstructionReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lcom/android/tools/smali/dexlib2/Opcode;)I - public static final fun indexOfFirstInstructionReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)I - public static synthetic fun indexOfFirstInstructionReversed$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lcom/android/tools/smali/dexlib2/Opcode;ILjava/lang/Object;)I - public static synthetic fun indexOfFirstInstructionReversed$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)I - public static final fun indexOfFirstInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;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 indexOfFirstWideLiteralInstructionValue (Lcom/android/tools/smali/dexlib2/iface/Method;J)I - public static final fun indexOfFirstWideLiteralInstructionValueOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;J)I - public static final fun indexOfFirstWideLiteralInstructionValueReversed (Lcom/android/tools/smali/dexlib2/iface/Method;J)I - public static final fun indexOfFirstWideLiteralInstructionValueReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;J)I - public static final fun indexOfIdResource (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;)I - public static final fun indexOfIdResourceOrThrow (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 resultOrThrow (Lapp/revanced/patcher/fingerprint/MethodFingerprint;)Lapp/revanced/patcher/fingerprint/MethodFingerprintResult; - public static final fun returnEarly (Lapp/revanced/patcher/fingerprint/MethodFingerprint;Z)V - public static final fun returnEarly (Ljava/lang/Iterable;Z)V - public static final fun returnEarly (Ljava/util/List;Z)V - public static synthetic fun returnEarly$default (Lapp/revanced/patcher/fingerprint/MethodFingerprint;ZILjava/lang/Object;)V - public static synthetic fun returnEarly$default (Ljava/lang/Iterable;ZILjava/lang/Object;)V - public static synthetic fun returnEarly$default (Ljava/util/List;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/data/BytecodeContext;Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lkotlin/jvm/functions/Function1;)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 asSequence (Lorg/w3c/dom/NodeList;)Lkotlin/sequences/Sequence; - public static final fun childElementsSequence (Lorg/w3c/dom/Node;)Lkotlin/sequences/Sequence; - public static final fun copyResources (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;[Lapp/revanced/util/ResourceGroup;)V - public static final fun copyXmlNode (Ljava/lang/String;Lapp/revanced/patcher/util/DomFileEditor;Lapp/revanced/patcher/util/DomFileEditor;)Ljava/lang/AutoCloseable; - public static final fun doRecursively (Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V - public static final fun forEachChildElement (Lorg/w3c/dom/Node;Lkotlin/jvm/functions/Function1;)V - public static final fun insertFirst (Lorg/w3c/dom/Node;Lorg/w3c/dom/Node;)V - public static final fun iterateXmlNodeChildren (Lapp/revanced/patcher/data/ResourceContext;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V -} - -public abstract class app/revanced/util/patch/LiteralValueFingerprint : app/revanced/patcher/fingerprint/MethodFingerprint { - public fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function0;)V - public synthetic fun (Ljava/lang/String;Ljava/lang/Integer;Ljava/lang/Iterable;Ljava/lang/Iterable;Ljava/lang/Iterable;Lkotlin/jvm/functions/Function0;ILkotlin/jvm/internal/DefaultConstructorMarker;)V -} - -public final class app/revanced/util/resource/ArrayResource : app/revanced/util/resource/BaseResource { - public static final field Companion Lapp/revanced/util/resource/ArrayResource$Companion; - public fun (Ljava/lang/String;Ljava/util/List;)V - public final fun getItems ()Ljava/util/List; - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/util/resource/ArrayResource$Companion { - public final fun fromNode (Lorg/w3c/dom/Node;)Lapp/revanced/util/resource/ArrayResource; -} - -public abstract class app/revanced/util/resource/BaseResource { - public fun (Ljava/lang/String;Ljava/lang/String;)V - public fun equals (Ljava/lang/Object;)Z - public final fun getName ()Ljava/lang/String; - public final fun getTag ()Ljava/lang/String; - public fun hashCode ()I - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; - public static synthetic fun serialize$default (Lapp/revanced/util/resource/BaseResource;Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/util/resource/StringResource : app/revanced/util/resource/BaseResource { - public static final field Companion Lapp/revanced/util/resource/StringResource$Companion; - public fun (Ljava/lang/String;Ljava/lang/String;Z)V - public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V - public final fun getFormatted ()Z - public final fun getValue ()Ljava/lang/String; - public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; -} - -public final class app/revanced/util/resource/StringResource$Companion { - public final fun fromNode (Lorg/w3c/dom/Node;)Lapp/revanced/util/resource/StringResource; -} - diff --git a/build.gradle.kts b/build.gradle.kts deleted file mode 100644 index 9ed141678..000000000 --- a/build.gradle.kts +++ /dev/null @@ -1,155 +0,0 @@ -import org.gradle.kotlin.dsl.support.listFilesOrdered -import org.jetbrains.kotlin.gradle.dsl.JvmTarget - -plugins { - alias(libs.plugins.kotlin) - alias(libs.plugins.binary.compatibility.validator) - `maven-publish` - signing -} - -group = "app.revanced" - -repositories { - mavenCentral() - mavenLocal() - google() - maven { - // A repository must be specified for some reason. "registry" is a dummy. - url = uri("https://maven.pkg.github.com/revanced/registry") - credentials { - username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") - password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") - } - } -} - -dependencies { - implementation(libs.revanced.patcher) - implementation(libs.smali) - // TODO: Required because build fails without it. Find a way to remove this dependency. - implementation(libs.guava) - // Used in JsonGenerator. - implementation(libs.gson) - // Android API stubs defined here. - compileOnly(project(":stub")) -} - -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } -} - -java { - targetCompatibility = JavaVersion.VERSION_11 -} - -tasks { - withType(Jar::class) { - exclude("app/revanced/meta") - - manifest { - attributes["Name"] = "ReVanced Patches" - attributes["Description"] = "Patches for ReVanced." - attributes["Version"] = version - attributes["Timestamp"] = System.currentTimeMillis().toString() - attributes["Source"] = "git@github.com:revanced/revanced-patches.git" - attributes["Author"] = "ReVanced" - attributes["Contact"] = "contact@revanced.app" - attributes["Origin"] = "https://revanced.app" - attributes["License"] = "GNU General Public License v3.0" - } - } - - register("buildDexJar") { - description = "Build and add a DEX to the JAR file" - group = "build" - - dependsOn(build) - - doLast { - val d8 = File(System.getenv("ANDROID_HOME")).resolve("build-tools") - .listFilesOrdered().last().resolve("d8").absolutePath - - val patchesJar = configurations.archives.get().allArtifacts.files.files.first().absolutePath - val workingDirectory = layout.buildDirectory.dir("libs").get().asFile - - exec { - workingDir = workingDirectory - commandLine = listOf(d8, "--release", patchesJar) - } - - exec { - workingDir = workingDirectory - commandLine = listOf("zip", "-u", patchesJar, "classes.dex") - } - } - } - - register("generatePatchesFiles") { - description = "Generate patches files" - - dependsOn(build) - - classpath = sourceSets["main"].runtimeClasspath - mainClass.set("app.revanced.generator.MainKt") - } - - // Needed by gradle-semantic-release-plugin. - // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 - publish { - dependsOn("buildDexJar") - dependsOn("generatePatchesFiles") - } -} - -publishing { - repositories { - maven { - name = "GitHubPackages" - url = uri("https://maven.pkg.github.com/revanced/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/crowdin.yml b/crowdin.yml index 4ac3cb98b..148f321cd 100644 --- a/crowdin.yml +++ b/crowdin.yml @@ -3,6 +3,6 @@ api_token_env: "CROWDIN_PERSONAL_TOKEN" preserve_hierarchy: false files: - - source: src/main/resources/addresources/values/strings.xml - translation: src/main/resources/addresources/values-%android_code%/strings.xml + - source: patches/src/main/resources/addresources/values/strings.xml + translation: patches/src/main/resources/addresources/values-%android_code%/strings.xml skip_untranslated_strings: true diff --git a/extensions/remove-screen-capture-restriction/build.gradle.kts b/extensions/remove-screen-capture-restriction/build.gradle.kts new file mode 100644 index 000000000..46f94dac8 --- /dev/null +++ b/extensions/remove-screen-capture-restriction/build.gradle.kts @@ -0,0 +1,11 @@ +extension { + name = "extensions/all/screencapture/remove-screen-capture-restriction.rve" +} + +android { + namespace = "app.revanced.extension" +} + +dependencies { + compileOnly(libs.annotation) +} diff --git a/extensions/remove-screen-capture-restriction/src/main/AndroidManifest.xml b/extensions/remove-screen-capture-restriction/src/main/AndroidManifest.xml new file mode 100644 index 000000000..15e7c2ae6 --- /dev/null +++ b/extensions/remove-screen-capture-restriction/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/remove-screen-capture-restriction/src/main/java/app/revanced/extension/all/screencapture/removerestriction/RemoveScreencaptureRestrictionPatch.java b/extensions/remove-screen-capture-restriction/src/main/java/app/revanced/extension/all/screencapture/removerestriction/RemoveScreencaptureRestrictionPatch.java new file mode 100644 index 000000000..1dac34144 --- /dev/null +++ b/extensions/remove-screen-capture-restriction/src/main/java/app/revanced/extension/all/screencapture/removerestriction/RemoveScreencaptureRestrictionPatch.java @@ -0,0 +1,21 @@ +package app.revanced.extension.all.screencapture.removerestriction; + +import android.media.AudioAttributes; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +public final class RemoveScreencaptureRestrictionPatch { + // Member of AudioAttributes.Builder + @RequiresApi(api = Build.VERSION_CODES.Q) + public static AudioAttributes.Builder setAllowedCapturePolicy(final AudioAttributes.Builder builder, final int capturePolicy) { + builder.setAllowedCapturePolicy(AudioAttributes.ALLOW_CAPTURE_BY_ALL); + + return builder; + } + + // Member of AudioManager static class + public static void setAllowedCapturePolicy(final int capturePolicy) { + // Ignore request + } +} diff --git a/extensions/remove-screenshot-restriction/build.gradle.kts b/extensions/remove-screenshot-restriction/build.gradle.kts new file mode 100644 index 000000000..cdbad5e1e --- /dev/null +++ b/extensions/remove-screenshot-restriction/build.gradle.kts @@ -0,0 +1,7 @@ +extension { + name = "extensions/all/screenshot/remove-screenshot-restriction.rve" +} + +android { + namespace = "app.revanced.extension" +} diff --git a/extensions/remove-screenshot-restriction/src/main/AndroidManifest.xml b/extensions/remove-screenshot-restriction/src/main/AndroidManifest.xml new file mode 100644 index 000000000..15e7c2ae6 --- /dev/null +++ b/extensions/remove-screenshot-restriction/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/extensions/remove-screenshot-restriction/src/main/java/app/revanced/extension/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch.java b/extensions/remove-screenshot-restriction/src/main/java/app/revanced/extension/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch.java new file mode 100644 index 000000000..fd5c427d3 --- /dev/null +++ b/extensions/remove-screenshot-restriction/src/main/java/app/revanced/extension/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch.java @@ -0,0 +1,15 @@ +package app.revanced.extension.all.screenshot.removerestriction; + +import android.view.Window; +import android.view.WindowManager; + +public class RemoveScreenshotRestrictionPatch { + + public static void addFlags(Window window, int flags) { + window.addFlags(flags & ~WindowManager.LayoutParams.FLAG_SECURE); + } + + public static void setFlags(Window window, int flags, int mask) { + window.setFlags(flags & ~WindowManager.LayoutParams.FLAG_SECURE, mask & ~WindowManager.LayoutParams.FLAG_SECURE); + } +} diff --git a/extensions/shared/build.gradle.kts b/extensions/shared/build.gradle.kts new file mode 100644 index 000000000..5abaf83ab --- /dev/null +++ b/extensions/shared/build.gradle.kts @@ -0,0 +1,22 @@ +extension { + name = "extensions/shared.rve" +} + +android { + namespace = "app.revanced.extension" + + buildTypes { + release { + isMinifyEnabled = true + } + } +} + +dependencies { + compileOnly(libs.appcompat) + compileOnly(libs.annotation) + compileOnly(libs.okhttp) + compileOnly(libs.retrofit) + + 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/boostforreddit/FixSLinksPatch.java b/extensions/shared/src/main/java/app/revanced/extension/boostforreddit/FixSLinksPatch.java new file mode 100644 index 000000000..b7a150fb1 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/boostforreddit/FixSLinksPatch.java @@ -0,0 +1,24 @@ +package app.revanced.extension.boostforreddit; + +import com.rubenmayayo.reddit.ui.activities.WebViewActivity; + +import app.revanced.extension.shared.fixes.slink.BaseFixSLinksPatch; + +/** @noinspection unused*/ +public class FixSLinksPatch extends BaseFixSLinksPatch { + static { + INSTANCE = new FixSLinksPatch(); + } + + private FixSLinksPatch() { + webViewActivityClass = WebViewActivity.class; + } + + public static boolean patchResolveSLink(String link) { + return INSTANCE.resolveSLink(link); + } + + public static void patchSetAccessToken(String accessToken) { + INSTANCE.setAccessToken(accessToken); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/FilterPromotedLinksPatch.java b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/FilterPromotedLinksPatch.java new file mode 100644 index 000000000..7534d6928 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/reddit/patches/FilterPromotedLinksPatch.java @@ -0,0 +1,23 @@ +package app.revanced.extension.reddit.patches; + +import com.reddit.domain.model.ILink; + +import java.util.ArrayList; +import java.util.List; + +public final class FilterPromotedLinksPatch { + /** + * Filters list from promoted links. + **/ + public static List filterChildren(final Iterable links) { + final List filteredList = new ArrayList<>(); + + for (Object item : links) { + if (item instanceof ILink && ((ILink) item).getPromoted()) continue; + + filteredList.add(item); + } + + return filteredList; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java b/extensions/shared/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java new file mode 100644 index 000000000..1e2586b2b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/GmsCoreSupport.java @@ -0,0 +1,158 @@ +package app.revanced.extension.shared; + +import static app.revanced.extension.shared.StringRef.str; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.SearchManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.PowerManager; +import android.provider.Settings; + +import androidx.annotation.RequiresApi; + +import java.net.MalformedURLException; +import java.net.URL; + +/** + * @noinspection unused + */ +public class GmsCoreSupport { + public static final String ORIGINAL_UNPATCHED_PACKAGE_NAME = "com.google.android.youtube"; + 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 void open(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); + Utils.getContext().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) { + // Do not set cancelable to false, to allow using back button to skip the action, + // just in case the check can never be satisfied. + var dialog = new AlertDialog.Builder(context) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setTitle(str("gms_core_dialog_title")) + .setMessage(str(dialogMessageRef)) + .setPositiveButton(str(positiveButtonStringRef), onPositiveClickListener) + .create(); + Utils.showDialog(context, dialog); + } + + /** + * Injection point. + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static void checkGmsCore(Activity context) { + 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 (context.getPackageName().equals(ORIGINAL_UNPATCHED_PACKAGE_NAME)) { + 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 = context.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(getGmsCoreDownload()); + return; + } + + // Check if GmsCore is running in the background. + try (var client = context.getContentResolver().acquireContentProviderClient(GMS_CORE_PROVIDER)) { + if (client == null) { + Logger.printInfo(() -> "GmsCore is not running in the background"); + + showBatteryOptimizationDialog(context, + "gms_core_dialog_not_whitelisted_not_allowed_in_background_message", + "gms_core_dialog_open_website_text", + (dialog, id) -> open(DONT_KILL_MY_APP_LINK)); + return; + } + } + + // Check if GmsCore is whitelisted from battery optimizations. + if (batteryOptimizationsEnabled(context)) { + Logger.printInfo(() -> "GmsCore is not whitelisted from battery optimizations"); + showBatteryOptimizationDialog(context, + "gms_core_dialog_not_whitelisted_using_battery_optimizations_message", + "gms_core_dialog_continue_text", + (dialog, id) -> openGmsCoreDisableBatteryOptimizationsIntent(context)); + } + } catch (Exception ex) { + Logger.printException(() -> "checkGmsCore failure", ex); + } + } + + @SuppressLint("BatteryLife") // Permission is part of GmsCore + private static void openGmsCoreDisableBatteryOptimizationsIntent(Activity activity) { + Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); + intent.setData(Uri.fromParts("package", GMS_CORE_PACKAGE_NAME, null)); + activity.startActivityForResult(intent, 0); + } + + /** + * @return If GmsCore is not whitelisted from battery optimizations. + */ + private static boolean batteryOptimizationsEnabled(Context context) { + var powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE); + return !powerManager.isIgnoringBatteryOptimizations(GMS_CORE_PACKAGE_NAME); + } + + private static String getGmsCoreDownload() { + final var vendorGroupId = getGmsCoreVendorGroupId(); + //noinspection SwitchStatementWithTooFewBranches + switch (vendorGroupId) { + case "app.revanced": + return "https://github.com/revanced/gmscore/releases/latest"; + default: + return vendorGroupId + ".android.gms"; + } + } + + // Modified by a patch. Do not touch. + private static String getGmsCoreVendorGroupId() { + return "app.revanced"; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/Logger.java b/extensions/shared/src/main/java/app/revanced/extension/shared/Logger.java new file mode 100644 index 000000000..9df38ac99 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/Logger.java @@ -0,0 +1,156 @@ +package app.revanced.extension.shared; + +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import app.revanced.extension.shared.settings.BaseSettings; + +import java.io.PrintWriter; +import java.io.StringWriter; + +import static app.revanced.extension.shared.settings.BaseSettings.*; + +public class Logger { + + /** + * Log messages using lambdas. + */ + @FunctionalInterface + 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 + * + */ + private String findOuterClassSimpleName() { + var 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 = "revanced: "; + + /** + * Logs debug messages under the outer class name of the code calling this method. + * Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()} + * so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled. + */ + public static void printDebug(@NonNull LogMessage message) { + printDebug(message, null); + } + + /** + * Logs debug messages under the outer class name of the code calling this method. + * Whenever possible, the log string should be constructed entirely inside {@link LogMessage#buildMessageString()} + * so the performance cost of building strings is paid only if {@link BaseSettings#DEBUG} is enabled. + */ + public static void printDebug(@NonNull LogMessage message, @Nullable Exception ex) { + if (DEBUG.get()) { + String logMessage = message.buildMessageString(); + String logTag = REVANCED_LOG_PREFIX + message.findOuterClassSimpleName(); + + if (DEBUG_STACKTRACE.get()) { + var builder = new StringBuilder(logMessage); + var sw = new StringWriter(); + new Throwable().printStackTrace(new PrintWriter(sw)); + + builder.append('\n').append(sw); + logMessage = builder.toString(); + } + + if (ex == null) { + Log.d(logTag, logMessage); + } else { + Log.d(logTag, logMessage, ex); + } + } + } + + /** + * 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); + } + if (DEBUG_TOAST_ON_ERROR.get()) { + Utils.showToastLong(outerClassSimpleName + ": " + messageString); + } + } + + /** + * Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized. + * Normally this method should not be used. + */ + public static void initializationInfo(@NonNull Class callingClass, @NonNull String message) { + Log.i(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message); + } + + /** + * Logging to use if {@link BaseSettings#DEBUG} or {@link Utils#getContext()} may not be initialized. + * Normally this method should not be used. + */ + public static void initializationException(@NonNull Class callingClass, @NonNull String message, + @Nullable Exception ex) { + Log.e(REVANCED_LOG_PREFIX + callingClass.getSimpleName(), message, ex); + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/StringRef.java b/extensions/shared/src/main/java/app/revanced/extension/shared/StringRef.java new file mode 100644 index 000000000..4390137de --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/StringRef.java @@ -0,0 +1,122 @@ +package app.revanced.extension.shared; + +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; + +public class StringRef { + private static Resources resources; + private static String packageName; + + // 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 + */ + @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) { + if (resources == null || packageName == null) { + Context context = Utils.getContext(); + resources = context.getResources(); + packageName = context.getPackageName(); + } + resolved = true; + if (resources != null) { + final int identifier = resources.getIdentifier(value, "string", packageName); + if (identifier == 0) + Logger.printException(() -> "Resource not found: " + value); + else + value = resources.getString(identifier); + } else { + Logger.printException(() -> "Could not resolve resources!"); + } + } + return value; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/Utils.java b/extensions/shared/src/main/java/app/revanced/extension/shared/Utils.java new file mode 100644 index 000000000..aed89670c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/Utils.java @@ -0,0 +1,770 @@ +package app.revanced.extension.shared; + +import android.annotation.SuppressLint; +import android.app.*; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.os.Build; +import android.os.Bundle; +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.view.animation.Animation; +import android.view.animation.AnimationUtils; +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.text.Bidi; +import java.util.*; +import java.util.regex.Pattern; +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 app.revanced.extension.shared.settings.preference.ReVancedAboutPreference; + +public class Utils { + + @SuppressLint("StaticFieldLeak") + private static Context context; + + private static String versionName; + + private Utils() { + } // utility class + + /** + * Injection point. + * + * @return The manifest 'Version' entry of the patches.jar used during patching. + */ + @SuppressWarnings("SameReturnValue") + public static String getPatchesReleaseVersion() { + return ""; // Value is replaced during patching. + } + + /** + * @return The version name of the app, such as 19.11.43 + */ + public static String getAppVersionName() { + if (versionName == null) { + try { + final var packageName = Objects.requireNonNull(getContext()).getPackageName(); + + PackageManager packageManager = context.getPackageManager(); + PackageInfo packageInfo; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + packageInfo = packageManager.getPackageInfo( + packageName, + PackageManager.PackageInfoFlags.of(0) + ); + } else { + packageInfo = packageManager.getPackageInfo( + packageName, + 0 + ); + } + versionName = packageInfo.versionName; + } catch (Exception ex) { + Logger.printException(() -> "Failed to get package info", ex); + versionName = "Unknown"; + } + } + + return versionName; + } + + + /** + * 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) { + if (hideViewBy0dpUnderCondition(condition.get(), view)) { + Logger.printDebug(() -> "View hidden by setting: " + condition); + } + } + + /** + * Hide a view by setting its layout height and width to 0dp. + * + * @param condition The setting to check for hiding the view. + * @param view The view to hide. + */ + public static boolean hideViewBy0dpUnderCondition(boolean condition, View view) { + if (condition) { + hideViewByLayoutParams(view); + return true; + } + + return false; + } + + /** + * 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) { + if (hideViewUnderCondition(condition.get(), view)) { + Logger.printDebug(() -> "View hidden by setting: " + condition); + } + } + + /** + * 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 boolean hideViewUnderCondition(boolean condition, View view) { + if (condition) { + view.setVisibility(View.GONE); + return true; + } + + return false; + } + + public static void hideViewByRemovingFromParentUnderCondition(BooleanSetting condition, View view) { + if (hideViewByRemovingFromParentUnderCondition(condition.get(), view)) { + Logger.printDebug(() -> "View hidden by setting: " + condition); + } + } + + public static boolean hideViewByRemovingFromParentUnderCondition(boolean setting, View view) { + if (setting) { + ViewParent parent = view.getParent(); + if (parent instanceof ViewGroup) { + ((ViewGroup) parent).removeView(view); + return true; + } + } + + return false; + } + + /** + * 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); + } + + /** + * Simulates a delay by doing meaningless calculations. + * Used for debugging to verify UI timeout logic. + */ + @SuppressWarnings("UnusedReturnValue") + public static long doNothingForDuration(long amountOfTimeToWaste) { + final long timeCalculationStarted = System.currentTimeMillis(); + Logger.printDebug(() -> "Artificially creating delay of: " + amountOfTimeToWaste + "ms"); + + long meaninglessValue = 0; + while (System.currentTimeMillis() - timeCalculationStarted < amountOfTimeToWaste) { + // could do a thread sleep, but that will trigger an exception if the thread is interrupted + meaninglessValue += Long.numberOfLeadingZeros((long) Math.exp(Math.random())); + } + // return the value, otherwise the compiler or VM might optimize and remove the meaningless time wasting work, + // leaving an empty loop that hammers on the System.currentTimeMillis native call + return meaninglessValue; + } + + + 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; + } + + /** + * @return zero, if the resource is not found + */ + @SuppressLint("DiscouragedApi") + public static int getResourceIdentifier(@NonNull Context context, @NonNull String resourceIdentifierName, @NonNull String type) { + return context.getResources().getIdentifier(resourceIdentifierName, type, context.getPackageName()); + } + + /** + * @return zero, if the resource is not found + */ + public static int getResourceIdentifier(@NonNull String resourceIdentifierName, @NonNull String type) { + return getResourceIdentifier(getContext(), resourceIdentifierName, type); + } + + public static int getResourceInteger(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + return getContext().getResources().getInteger(getResourceIdentifier(resourceIdentifierName, "integer")); + } + + @NonNull + public static Animation getResourceAnimation(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + return AnimationUtils.loadAnimation(getContext(), getResourceIdentifier(resourceIdentifierName, "anim")); + } + + public static int getResourceColor(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + //noinspection deprecation + return getContext().getResources().getColor(getResourceIdentifier(resourceIdentifierName, "color")); + } + + public static int getResourceDimensionPixelSize(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + return getContext().getResources().getDimensionPixelSize(getResourceIdentifier(resourceIdentifierName, "dimen")); + } + + public static float getResourceDimension(@NonNull String resourceIdentifierName) throws Resources.NotFoundException { + return getContext().getResources().getDimension(getResourceIdentifier(resourceIdentifierName, "dimen")); + } + + public interface MatchFilter { + boolean matches(T object); + } + + /** + * Includes sub children. + * + * @noinspection unchecked + */ + public static R getChildViewByResourceName(@NonNull View view, @NonNull String str) { + var child = view.findViewById(Utils.getResourceIdentifier(str, "id")); + if (child != null) { + return (R) child; + } + + throw new IllegalArgumentException("View with resource 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; + } + + @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 currentDepthLog = currentDepth; + Logger.printDebug(() -> "Could not find parent view of depth: " + nthParent + + " and instead found at: " + currentDepthLog + " view: " + view); + return null; + } + + public static void restartApp(@NonNull Context context) { + String packageName = context.getPackageName(); + Intent intent = context.getPackageManager().getLaunchIntentForPackage(packageName); + 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); + context.startActivity(mainIntent); + System.exit(0); + } + + public static Context getContext() { + if (context == null) { + Logger.initializationException(Utils.class, "Context is null, returning null!", null); + } + return context; + } + + public static void setContext(Context appContext) { + 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) { + android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + android.content.ClipData clip = android.content.ClipData.newPlainText("ReVanced", text); + clipboard.setPrimaryClip(clip); + } + + public static boolean isTablet() { + return context.getResources().getConfiguration().smallestScreenWidthDp >= 600; + } + + @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; + } + + /** + * Ignore this class. It must be public to satisfy Android requirements. + */ + @SuppressWarnings("deprecation") + public static final class DialogFragmentWrapper extends DialogFragment { + + private Dialog dialog; + @Nullable + private DialogFragmentOnStartAction onStartAction; + + @Override + public void onSaveInstanceState(Bundle outState) { + // Do not call super method to prevent state saving. + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + return dialog; + } + + @Override + public void onStart() { + try { + super.onStart(); + + if (onStartAction != null) { + onStartAction.onStart((AlertDialog) getDialog()); + } + } catch (Exception ex) { + Logger.printException(() -> "onStart failure: " + dialog.getClass().getSimpleName(), ex); + } + } + } + + /** + * Interface for {@link #showDialog(Activity, AlertDialog, boolean, DialogFragmentOnStartAction)}. + */ + @FunctionalInterface + public interface DialogFragmentOnStartAction { + void onStart(AlertDialog dialog); + } + + public static void showDialog(Activity activity, AlertDialog dialog) { + showDialog(activity, dialog, true, null); + } + + /** + * Utility method to allow showing an AlertDialog on top of other alert dialogs. + * Calling this will always display the dialog on top of all other dialogs + * previously called using this method. + *
+ * Be aware the on start action can be called multiple times for some situations, + * such as the user switching apps without dismissing the dialog then switching back to this app. + *
+ * This method is only useful during app startup and multiple patches may show their own dialog, + * and the most important dialog can be called last (using a delay) so it's always on top. + *
+ * For all other situations it's better to not use this method and + * call {@link AlertDialog#show()} on the dialog. + */ + @SuppressWarnings("deprecation") + public static void showDialog(Activity activity, + AlertDialog dialog, + boolean isCancelable, + @Nullable DialogFragmentOnStartAction onStartAction) { + verifyOnMainThread(); + + DialogFragmentWrapper fragment = new DialogFragmentWrapper(); + fragment.dialog = dialog; + fragment.onStartAction = onStartAction; + fragment.setCancelable(isCancelable); + + fragment.show(activity.getFragmentManager(), null); + } + + /** + * 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() { + return Looper.getMainLooper().isCurrentThread(); + } + + /** + * @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 { + NONE, + MOBILE, + OTHER, + } + + public static boolean isNetworkConnected() { + NetworkType networkType = getNetworkType(); + return networkType == NetworkType.MOBILE + || networkType == NetworkType.OTHER; + } + + @SuppressLint("MissingPermission") // permission already included in YouTube + public static NetworkType getNetworkType() { + Context networkContext = getContext(); + if (networkContext == null) { + return NetworkType.NONE; + } + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + var networkInfo = cm.getActiveNetworkInfo(); + + if (networkInfo == null || !networkInfo.isConnected()) { + return NetworkType.NONE; + } + var type = networkInfo.getType(); + return (type == ConnectivityManager.TYPE_MOBILE) + || (type == ConnectivityManager.TYPE_BLUETOOTH) ? NetworkType.MOBILE : NetworkType.OTHER; + } + + /** + * Hide a view by setting its layout params to 0x0 + * @param view The view to hide. + */ + public static void hideViewByLayoutParams(View view) { + 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); + } + } + + /** + * {@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 Pattern punctuationPattern = Pattern.compile("\\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 punctuationPattern.matcher(original).replaceAll("").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) { + sortPreferenceGroups((PreferenceGroup) preference); + 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()); + break; + case BY_KEY: + sortValue = preference.getKey(); + break; + 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++; + + // Move any screens, intents, and the one off About preference to the top. + if (pref instanceof PreferenceScreen || pref instanceof ReVancedAboutPreference + || pref.getIntent() != null) { + // Arbitrary high number. + order -= 1000; + } + + pref.setOrder(order); + } + } + + /** + * Set all preferences to multiline titles if the device is not using an English variant. + * The English strings are heavily scrutinized and all titles fit on screen + * except 2 or 3 preference strings and those do not affect readability. + * + * Allowing multiline for those 2 or 3 English preferences looks weird and out of place, + * and visually it looks better to clip the text and keep all titles 1 line. + */ + @SuppressWarnings("deprecation") + public static void setPreferenceTitlesToMultiLineIfNeeded(PreferenceGroup group) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return; + } + + String deviceLanguage = Utils.getContext().getResources().getConfiguration().locale.getLanguage(); + if (deviceLanguage.equals("en")) { + return; + } + + for (int i = 0, prefCount = group.getPreferenceCount(); i < prefCount; i++) { + Preference pref = group.getPreference(i); + pref.setSingleLineTitle(false); + + if (pref instanceof PreferenceGroup) { + setPreferenceTitlesToMultiLineIfNeeded((PreferenceGroup) pref); + } + } + } + + /** + * 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]. + */ + public static void setEditTextDialogTheme(AlertDialog.Builder builder) { + final int editTextDialogStyle = getResourceIdentifier( + "revanced_edit_text_dialog_style", "style"); + if (editTextDialogStyle != 0) { + builder.getContext().setTheme(editTextDialogStyle); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/checks/Check.java b/extensions/shared/src/main/java/app/revanced/extension/shared/checks/Check.java new file mode 100644 index 000000000..855e6003b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/checks/Check.java @@ -0,0 +1,164 @@ +package app.revanced.extension.shared.checks; + +import static android.text.Html.FROM_HTML_MODE_COMPACT; +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.Utils.DialogFragmentOnStartAction; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.text.Html; +import android.widget.Button; + +import androidx.annotation.Nullable; + +import java.util.Collection; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.settings.Settings; + +abstract class Check { + private static final int NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING = 2; + + private static final int SECONDS_BEFORE_SHOWING_IGNORE_BUTTON = 15; + private static final int SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON = 10; + + private static final Uri GOOD_SOURCE = Uri.parse("https://revanced.app"); + + /** + * @return If the check conclusively passed or failed. A null value indicates it neither passed nor failed. + */ + @Nullable + protected abstract Boolean check(); + + protected abstract String failureReason(); + + /** + * Specifies a sorting order for displaying the checks that failed. + * A lower value indicates to show first before other checks. + */ + public abstract int uiSortingValue(); + + /** + * For debugging and development only. + * Forces all checks to be performed and the check failed dialog to be shown. + * Can be enabled by importing settings text with {@link Settings#CHECK_ENVIRONMENT_WARNINGS_ISSUED} + * set to -1. + */ + static boolean debugAlwaysShowWarning() { + final boolean alwaysShowWarning = Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() < 0; + if (alwaysShowWarning) { + Logger.printInfo(() -> "Debug forcing environment check warning to show"); + } + + return alwaysShowWarning; + } + + static boolean shouldRun() { + return Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get() + < NUMBER_OF_TIMES_TO_IGNORE_WARNING_BEFORE_DISABLING; + } + + static void disableForever() { + Logger.printInfo(() -> "Environment checks disabled forever"); + + Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(Integer.MAX_VALUE); + } + + @SuppressLint("NewApi") + static void issueWarning(Activity activity, Collection failedChecks) { + final var reasons = new StringBuilder(); + + reasons.append("

    "); + for (var check : failedChecks) { + // Add a non breaking space to fix bullet points spacing issue. + reasons.append("
  •  ").append(check.failureReason()); + } + reasons.append("
"); + + var message = Html.fromHtml( + str("revanced_check_environment_failed_message", reasons.toString()), + FROM_HTML_MODE_COMPACT + ); + + Utils.runOnMainThreadDelayed(() -> { + AlertDialog alert = new AlertDialog.Builder(activity) + .setCancelable(false) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setTitle(str("revanced_check_environment_failed_title")) + .setMessage(message) + .setPositiveButton( + " ", + (dialog, which) -> { + final var intent = new Intent(Intent.ACTION_VIEW, GOOD_SOURCE); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + activity.startActivity(intent); + + // Shutdown to prevent the user from navigating back to this app, + // which is no longer showing a warning dialog. + activity.finishAffinity(); + System.exit(0); + } + ).setNegativeButton( + " ", + (dialog, which) -> { + // Cleanup data if the user incorrectly imported a huge negative number. + final int current = Math.max(0, Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.get()); + Settings.CHECK_ENVIRONMENT_WARNINGS_ISSUED.save(current + 1); + + dialog.dismiss(); + } + ).create(); + + Utils.showDialog(activity, alert, false, new DialogFragmentOnStartAction() { + boolean hasRun; + @Override + public void onStart(AlertDialog dialog) { + // Only run this once, otherwise if the user changes to a different app + // then changes back, this handler will run again and disable the buttons. + if (hasRun) { + return; + } + hasRun = true; + + var openWebsiteButton = dialog.getButton(DialogInterface.BUTTON_POSITIVE); + openWebsiteButton.setEnabled(false); + + var dismissButton = dialog.getButton(DialogInterface.BUTTON_NEGATIVE); + dismissButton.setEnabled(false); + + getCountdownRunnable(dismissButton, openWebsiteButton).run(); + } + }); + }, 1000); // Use a delay, so this dialog is shown on top of any other startup dialogs. + } + + private static Runnable getCountdownRunnable(Button dismissButton, Button openWebsiteButton) { + return new Runnable() { + private int secondsRemaining = SECONDS_BEFORE_SHOWING_IGNORE_BUTTON; + + @Override + public void run() { + Utils.verifyOnMainThread(); + + if (secondsRemaining > 0) { + if (secondsRemaining - SECONDS_BEFORE_SHOWING_WEBSITE_BUTTON == 0) { + openWebsiteButton.setText(str("revanced_check_environment_dialog_open_official_source_button")); + openWebsiteButton.setEnabled(true); + } + + secondsRemaining--; + + Utils.runOnMainThreadDelayed(this, 1000); + } else { + dismissButton.setText(str("revanced_check_environment_dialog_ignore_button")); + dismissButton.setEnabled(true); + } + } + }; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java new file mode 100644 index 000000000..d63f8b7e3 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/checks/CheckEnvironmentPatch.java @@ -0,0 +1,341 @@ +package app.revanced.extension.shared.checks; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.util.Base64; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.shared.checks.Check.debugAlwaysShowWarning; +import static app.revanced.extension.shared.checks.PatchInfo.Build.*; + +/** + * This class is used to check if the app was patched by the user + * and not downloaded pre-patched, because pre-patched apps are difficult to trust. + *
+ * Various indicators help to detect if the app was patched by the user. + */ +@SuppressWarnings("unused") +public final class CheckEnvironmentPatch { + private static final boolean DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG = debugAlwaysShowWarning(); + + private enum InstallationType { + /** + * CLI patching, manual installation of a previously patched using adb, + * or root installation if stock app is first installed using adb. + */ + ADB((String) null), + ROOT_MOUNT_ON_APP_STORE("com.android.vending"), + MANAGER("app.revanced.manager.flutter", + "app.revanced.manager", + "app.revanced.manager.debug"); + + @Nullable + static InstallationType installTypeFromPackageName(@Nullable String packageName) { + for (InstallationType type : values()) { + for (String installPackageName : type.packageNames) { + if (Objects.equals(installPackageName, packageName)) { + return type; + } + } + } + + return null; + } + + /** + * Array elements can be null. + */ + final String[] packageNames; + + InstallationType(String... packageNames) { + this.packageNames = packageNames; + } + } + + /** + * Check if the app is installed by the manager, the app store, or through adb/CLI. + *
+ * Does not conclusively + * If the app is installed by the manager or the app store, it is likely, the app was patched using the manager, + * or installed manually via ADB (in the case of ReVanced CLI for example). + *
+ * If the app is not installed by the manager or the app store, then the app was likely downloaded pre-patched + * and installed by the browser or another unknown app. + */ + private static class CheckExpectedInstaller extends Check { + @Nullable + InstallationType installerFound; + + @NonNull + @Override + protected Boolean check() { + final var context = Utils.getContext(); + + final var installerPackageName = + context.getPackageManager().getInstallerPackageName(context.getPackageName()); + + Logger.printInfo(() -> "Installed by: " + installerPackageName); + + installerFound = InstallationType.installTypeFromPackageName(installerPackageName); + final boolean passed = (installerFound != null); + + Logger.printInfo(() -> passed + ? "Apk was not installed from an unknown source" + : "Apk was installed from an unknown source"); + + return passed; + } + + @Override + protected String failureReason() { + return str("revanced_check_environment_manager_not_expected_installer"); + } + + @Override + public int uiSortingValue() { + return -100; // Show first. + } + } + + /** + * Check if the build properties are the same as during the patch. + *
+ * If the build properties are the same as during the patch, it is likely, the app was patched on the same device. + *
+ * If the build properties are different, the app was likely downloaded pre-patched or patched on another device. + */ + private static class CheckWasPatchedOnSameDevice extends Check { + @SuppressLint({"NewApi", "HardwareIds"}) + @Override + protected Boolean check() { + if (PATCH_BOARD.isEmpty()) { + // Did not patch with Manager, and cannot conclusively say where this was from. + Logger.printInfo(() -> "APK does not contain a hardware signature and cannot compare to current device"); + return null; + } + + //noinspection deprecation + final var passed = buildFieldEqualsHash("BOARD", Build.BOARD, PATCH_BOARD) & + buildFieldEqualsHash("BOOTLOADER", Build.BOOTLOADER, PATCH_BOOTLOADER) & + buildFieldEqualsHash("BRAND", Build.BRAND, PATCH_BRAND) & + buildFieldEqualsHash("CPU_ABI", Build.CPU_ABI, PATCH_CPU_ABI) & + buildFieldEqualsHash("CPU_ABI2", Build.CPU_ABI2, PATCH_CPU_ABI2) & + buildFieldEqualsHash("DEVICE", Build.DEVICE, PATCH_DEVICE) & + buildFieldEqualsHash("DISPLAY", Build.DISPLAY, PATCH_DISPLAY) & + buildFieldEqualsHash("FINGERPRINT", Build.FINGERPRINT, PATCH_FINGERPRINT) & + buildFieldEqualsHash("HARDWARE", Build.HARDWARE, PATCH_HARDWARE) & + buildFieldEqualsHash("HOST", Build.HOST, PATCH_HOST) & + buildFieldEqualsHash("ID", Build.ID, PATCH_ID) & + buildFieldEqualsHash("MANUFACTURER", Build.MANUFACTURER, PATCH_MANUFACTURER) & + buildFieldEqualsHash("MODEL", Build.MODEL, PATCH_MODEL) & + buildFieldEqualsHash("PRODUCT", Build.PRODUCT, PATCH_PRODUCT) & + buildFieldEqualsHash("RADIO", Build.RADIO, PATCH_RADIO) & + buildFieldEqualsHash("TAGS", Build.TAGS, PATCH_TAGS) & + buildFieldEqualsHash("TYPE", Build.TYPE, PATCH_TYPE) & + buildFieldEqualsHash("USER", Build.USER, PATCH_USER); + + Logger.printInfo(() -> passed + ? "Device hardware signature matches current device" + : "Device hardware signature does not match current device"); + + return passed; + } + + @Override + protected String failureReason() { + return str("revanced_check_environment_not_same_patching_device"); + } + + @Override + public int uiSortingValue() { + return 0; // Show in the middle. + } + } + + /** + * Check if the app was installed within the last 30 minutes after being patched. + *
+ * If the app was installed within the last 30 minutes, it is likely, the app was patched by the user. + *
+ * If the app was installed much later than the patch time, it is likely the app was + * downloaded pre-patched or the user waited too long to install the app. + */ + private static class CheckIsNearPatchTime extends Check { + /** + * How soon after patching the app must be installed to pass. + */ + static final int INSTALL_AFTER_PATCHING_DURATION_THRESHOLD = 30 * 60 * 1000; // 30 minutes. + + /** + * Milliseconds between the time the app was patched, and when it was installed/updated. + */ + long durationBetweenPatchingAndInstallation; + + @NonNull + @Override + protected Boolean check() { + try { + Context context = Utils.getContext(); + PackageManager packageManager = context.getPackageManager(); + PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0); + + // Duration since initial install or last update, which ever is sooner. + durationBetweenPatchingAndInstallation = packageInfo.lastUpdateTime - PatchInfo.PATCH_TIME; + Logger.printInfo(() -> "App was installed/updated: " + + (durationBetweenPatchingAndInstallation / (60 * 1000) + " minutes after patching")); + + if (durationBetweenPatchingAndInstallation < 0) { + // Patch time is in the future and clearly wrong. + return false; + } + + if (durationBetweenPatchingAndInstallation < INSTALL_AFTER_PATCHING_DURATION_THRESHOLD) { + return true; + } + } catch (PackageManager.NameNotFoundException ex) { + Logger.printException(() -> "Package name not found exception", ex); // Will never happen. + } + + // User installed more than 30 minutes after patching. + return false; + } + + @Override + protected String failureReason() { + if (durationBetweenPatchingAndInstallation < 0) { + // Could happen if the user has their device clock incorrectly set in the past, + // but assume that isn't the case and the apk was patched on a device with the wrong system time. + return str("revanced_check_environment_not_near_patch_time_invalid"); + } + + // If patched over 1 day ago, show how old this pre-patched apk is. + // Showing the age can help convey it's better to patch yourself and know it's the latest. + final long oneDay = 24 * 60 * 60 * 1000; + final long daysSincePatching = durationBetweenPatchingAndInstallation / oneDay; + if (daysSincePatching > 1) { // Use over 1 day to avoid singular vs plural strings. + return str("revanced_check_environment_not_near_patch_time_days", daysSincePatching); + } + + return str("revanced_check_environment_not_near_patch_time"); + } + + @Override + public int uiSortingValue() { + return 100; // Show last. + } + } + + /** + * Injection point. + */ + public static void check(Activity context) { + // If the warning was already issued twice, or if the check was successful in the past, + // do not run the checks again. + if (!Check.shouldRun() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + Logger.printDebug(() -> "Environment checks are disabled"); + return; + } + + Utils.runOnBackgroundThread(() -> { + try { + Logger.printInfo(() -> "Running environment checks"); + List failedChecks = new ArrayList<>(); + + CheckWasPatchedOnSameDevice sameHardware = new CheckWasPatchedOnSameDevice(); + Boolean hardwareCheckPassed = sameHardware.check(); + if (hardwareCheckPassed != null) { + if (hardwareCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + // Patched on the same device using Manager, + // and no further checks are needed. + Check.disableForever(); + return; + } + + failedChecks.add(sameHardware); + } + + CheckExpectedInstaller installerCheck = new CheckExpectedInstaller(); + if (installerCheck.check() && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + // If the installer package is Manager but this code is reached, + // that means it must not be the right Manager otherwise the hardware hash + // signatures would be present and this check would not have run. + if (installerCheck.installerFound == InstallationType.MANAGER) { + failedChecks.add(installerCheck); + // Also could not have been patched on this device. + failedChecks.add(sameHardware); + } else if (failedChecks.isEmpty()) { + // ADB install of CLI build. Allow even if patched a long time ago. + Check.disableForever(); + return; + } + } else { + failedChecks.add(installerCheck); + } + + CheckIsNearPatchTime nearPatchTime = new CheckIsNearPatchTime(); + Boolean timeCheckPassed = nearPatchTime.check(); + if (timeCheckPassed && !DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + // Allow installing recently patched apks, + // even if the install source is not Manager or ADB. + Check.disableForever(); + return; + } else { + failedChecks.add(nearPatchTime); + } + + if (DEBUG_ALWAYS_SHOW_CHECK_FAILED_DIALOG) { + // Show all failures for debugging layout. + failedChecks = Arrays.asList( + sameHardware, + nearPatchTime, + installerCheck + ); + } + + //noinspection ComparatorCombinators + Collections.sort(failedChecks, (o1, o2) -> o1.uiSortingValue() - o2.uiSortingValue()); + + Check.issueWarning( + context, + failedChecks + ); + } catch (Exception ex) { + Logger.printException(() -> "check failure", ex); + } + }); + } + + private static boolean buildFieldEqualsHash(String buildFieldName, String buildFieldValue, @Nullable String hash) { + try { + final var sha1 = MessageDigest.getInstance("SHA-1") + .digest(buildFieldValue.getBytes(StandardCharsets.UTF_8)); + + // Must be careful to use same base64 encoding Kotlin uses. + String runtimeHash = new String(Base64.encode(sha1, Base64.NO_WRAP), StandardCharsets.ISO_8859_1); + final boolean equals = runtimeHash.equals(hash); + if (!equals) { + Logger.printInfo(() -> "Hashes do not match. " + buildFieldName + ": '" + buildFieldValue + + "' runtimeHash: '" + runtimeHash + "' patchTimeHash: '" + hash + "'"); + } + + return equals; + } catch (NoSuchAlgorithmException ex) { + Logger.printException(() -> "buildFieldEqualsHash failure", ex); // Will never happen. + + return false; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/checks/PatchInfo.java b/extensions/shared/src/main/java/app/revanced/extension/shared/checks/PatchInfo.java new file mode 100644 index 000000000..62144d753 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/checks/PatchInfo.java @@ -0,0 +1,28 @@ +package app.revanced.extension.shared.checks; + +// Fields are set by the patch. Do not modify. +// Fields are not final, because the compiler is inlining them. +final class PatchInfo { + static long PATCH_TIME = 0L; + + final static class Build { + static String PATCH_BOARD = ""; + static String PATCH_BOOTLOADER = ""; + static String PATCH_BRAND = ""; + static String PATCH_CPU_ABI = ""; + static String PATCH_CPU_ABI2 = ""; + static String PATCH_DEVICE = ""; + static String PATCH_DISPLAY = ""; + static String PATCH_FINGERPRINT = ""; + static String PATCH_HARDWARE = ""; + static String PATCH_HOST = ""; + static String PATCH_ID = ""; + static String PATCH_MANUFACTURER = ""; + static String PATCH_MODEL = ""; + static String PATCH_PRODUCT = ""; + static String PATCH_RADIO = ""; + static String PATCH_TAGS = ""; + static String PATCH_TYPE = ""; + static String PATCH_USER = ""; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/fixes/slink/BaseFixSLinksPatch.java b/extensions/shared/src/main/java/app/revanced/extension/shared/fixes/slink/BaseFixSLinksPatch.java new file mode 100644 index 000000000..a8a6bf504 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/fixes/slink/BaseFixSLinksPatch.java @@ -0,0 +1,208 @@ +package app.revanced.extension.shared.fixes.slink; + + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import androidx.annotation.NonNull; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.URL; +import java.util.Objects; + +import static app.revanced.extension.shared.Utils.getContext; + + +/** + * Base class to implement /s/ link resolution in 3rd party Reddit apps. + *
+ *
+ * Usage: + *
+ *
+ * An implementation of this class must have two static methods that are called by the app: + *
    + *
  • public static boolean patchResolveSLink(String link)
  • + *
  • public static void patchSetAccessToken(String accessToken)
  • + *
+ * The static methods must call the instance methods of the base class. + *
+ * The singleton pattern can be used to access the instance of the class: + *
+ * {@code
+ * {
+ *     INSTANCE = new FixSLinksPatch();
+ * }
+ * }
+ * 
+ * Set the app's web view activity class as a fallback to open /s/ links if the resolution fails: + *
+ * {@code
+ * private FixSLinksPatch() {
+ *     webViewActivityClass = WebViewActivity.class;
+ * }
+ * }
+ * 
+ * Hook the app's navigation handler to call this method before doing any of its own resolution: + *
+ * {@code
+ * public static boolean patchResolveSLink(Context context, String link) {
+ *     return INSTANCE.resolveSLink(context, link);
+ * }
+ * }
+ * 
+ * If this method returns true, the app should early return and not do any of its own resolution. + *
+ *
+ * Hook the app's access token so that this class can use it to resolve /s/ links: + *
+ * {@code
+ * public static void patchSetAccessToken(String accessToken) {
+ *     INSTANCE.setAccessToken(access_token);
+ * }
+ * }
+ * 
+ */ +public abstract class BaseFixSLinksPatch { + /** + * The class of the activity used to open links in a web view if resolving them fails. + */ + protected Class webViewActivityClass; + + /** + * The access token used to resolve the /s/ link. + */ + protected String accessToken; + + /** + * The URL that was trying to be resolved before the access token was set. + * If this is not null, the URL will be resolved right after the access token is set. + */ + protected String pendingUrl; + + /** + * The singleton instance of the class. + */ + protected static BaseFixSLinksPatch INSTANCE; + + public boolean resolveSLink(String link) { + switch (resolveLink(link)) { + case ACCESS_TOKEN_START: { + pendingUrl = link; + return true; + } + case DO_NOTHING: + return true; + default: + return false; + } + } + + private ResolveResult resolveLink(String link) { + Context context = getContext(); + if (link.matches(".*reddit\\.com/r/[^/]+/s/[^/]+")) { + // A link ends with #bypass if it failed to resolve below. + // resolveLink is called with the same link again but this time with #bypass + // so that the link is opened in the app browser instead of trying to resolve it again. + if (link.endsWith("#bypass")) { + openInAppBrowser(context, link); + + return ResolveResult.DO_NOTHING; + } + + Logger.printDebug(() -> "Resolving " + link); + + if (accessToken == null) { + // This is not optimal. + // However, an accessToken is necessary to make an authenticated request to Reddit. + // in case Reddit has banned the IP - e.g. VPN. + Intent startIntent = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()); + context.startActivity(startIntent); + + return ResolveResult.ACCESS_TOKEN_START; + } + + + Utils.runOnBackgroundThread(() -> { + String bypassLink = link + "#bypass"; + + String finalLocation = bypassLink; + try { + HttpURLConnection connection = getHttpURLConnection(link, accessToken); + connection.connect(); + String location = connection.getHeaderField("location"); + connection.disconnect(); + + Objects.requireNonNull(location, "Location is null"); + + finalLocation = location; + Logger.printDebug(() -> "Resolved " + link + " to " + location); + } catch (SocketTimeoutException e) { + Logger.printException(() -> "Timeout when trying to resolve " + link, e); + finalLocation = bypassLink; + } catch (Exception e) { + Logger.printException(() -> "Failed to resolve " + link, e); + finalLocation = bypassLink; + } finally { + Intent startIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(finalLocation)); + startIntent.setPackage(context.getPackageName()); + startIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(startIntent); + } + }); + + return ResolveResult.DO_NOTHING; + } + + return ResolveResult.CONTINUE; + } + + public void setAccessToken(String accessToken) { + Logger.printDebug(() -> "Setting access token"); + + this.accessToken = accessToken; + + // In case a link was trying to be resolved before access token was set. + // The link is resolved now, after the access token is set. + if (pendingUrl != null) { + String link = pendingUrl; + pendingUrl = null; + + Logger.printDebug(() -> "Opening pending URL"); + + resolveLink(link); + } + } + + private void openInAppBrowser(Context context, String link) { + Intent intent = new Intent(context, webViewActivityClass); + intent.putExtra("url", link); + context.startActivity(intent); + } + + @NonNull + private HttpURLConnection getHttpURLConnection(String link, String accessToken) throws IOException { + URL url = new URL(link); + + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + connection.setInstanceFollowRedirects(false); + connection.setRequestMethod("HEAD"); + connection.setConnectTimeout(2000); + connection.setReadTimeout(2000); + + if (accessToken != null) { + Logger.printDebug(() -> "Setting access token to make /s/ request"); + + connection.setRequestProperty("Authorization", "Bearer " + accessToken); + } else { + Logger.printDebug(() -> "Not setting access token to make /s/ request, because it is null"); + } + + return connection; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/shared/fixes/slink/ResolveResult.java b/extensions/shared/src/main/java/app/revanced/extension/shared/fixes/slink/ResolveResult.java new file mode 100644 index 000000000..8026c2058 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/fixes/slink/ResolveResult.java @@ -0,0 +1,10 @@ +package app.revanced.extension.shared.fixes.slink; + +public enum ResolveResult { + // Let app handle rest of stuff + CONTINUE, + // Start app, to make it cache its access_token + ACCESS_TOKEN_START, + // Don't do anything - we started resolving + DO_NOTHING +} 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..70d7589e8 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BaseSettings.java @@ -0,0 +1,17 @@ +package app.revanced.extension.shared.settings; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; +import static app.revanced.extension.shared.settings.Setting.parent; + +/** + * 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 DEBUG = new BooleanSetting("revanced_debug", FALSE); + public static final BooleanSetting DEBUG_STACKTRACE = new BooleanSetting("revanced_debug_stacktrace", FALSE, parent(DEBUG)); + public static final BooleanSetting DEBUG_TOAST_ON_ERROR = new BooleanSetting("revanced_debug_toast_on_error", TRUE, "revanced_debug_toast_on_error_user_dialog_message"); +} 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..7e84034d0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/BooleanSetting.java @@ -0,0 +1,79 @@ +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); + } + + @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..a2b82dd21 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/EnumSetting.java @@ -0,0 +1,117 @@ +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.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); + } + + @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..7419741e0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/FloatSetting.java @@ -0,0 +1,69 @@ +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); + } + + @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..58f39a910 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/IntegerSetting.java @@ -0,0 +1,69 @@ +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); + } + + @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..4d7f8114f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/LongSetting.java @@ -0,0 +1,69 @@ +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); + } + + @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..7507d802a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/Setting.java @@ -0,0 +1,437 @@ +package app.revanced.extension.shared.settings; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.StringRef; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.preference.SharedPrefCategory; +import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings; +import org.jetbrains.annotations.NotNull; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.*; + +import static app.revanced.extension.shared.StringRef.str; + +@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 extension 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_prefs"); + + @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() { + Collections.sort(SETTINGS, (Setting o1, Setting 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.showToastLong(this.getClass().getSimpleName() + + " error: Duplicate Setting key found: " + key); + } + + load(); + } + + /** + * Migrate a setting value if the path is renamed but otherwise the old and new settings are identical. + */ + public static void migrateOldSettingToNew(@NonNull Setting oldSetting, @NonNull Setting newSetting) { + if (oldSetting == newSetting) throw new IllegalArgumentException(); + + if (!oldSetting.isSetToDefault()) { + Logger.printInfo(() -> "Migrating old setting value: " + oldSetting + " into replacement setting: " + newSetting); + newSetting.save(oldSetting.value); + oldSetting.resetToDefault(); + } + } + + /** + * Migrate an old Setting value previously stored in a different SharedPreference. + * + * This method will be deleted in the future. + */ + public static void migrateFromOldPreferences(@NonNull SharedPrefCategory oldPrefs, @NonNull Setting setting, String settingKey) { + if (!oldPrefs.preferences.contains(settingKey)) { + return; // Nothing to do. + } + + Object newValue = setting.get(); + final Object migratedValue; + if (setting instanceof BooleanSetting) { + migratedValue = oldPrefs.getBoolean(settingKey, (Boolean) newValue); + } else if (setting instanceof IntegerSetting) { + migratedValue = oldPrefs.getIntegerString(settingKey, (Integer) newValue); + } else if (setting instanceof LongSetting) { + migratedValue = oldPrefs.getLongString(settingKey, (Long) newValue); + } else if (setting instanceof FloatSetting) { + migratedValue = oldPrefs.getFloatString(settingKey, (Float) newValue); + } else if (setting instanceof StringSetting) { + migratedValue = oldPrefs.getString(settingKey, (String) newValue); + } else { + Logger.printException(() -> "Unknown setting: " + setting); + // Remove otherwise it'll show a toast on every launch + oldPrefs.preferences.edit().remove(settingKey).apply(); + return; + } + + oldPrefs.preferences.edit().remove(settingKey).apply(); // Remove the old setting. + if (migratedValue.equals(newValue)) { + Logger.printDebug(() -> "Value does not need migrating: " + settingKey); + return; // Old value is already equal to the new setting value. + } + + Logger.printDebug(() -> "Migrating old preference value into current preference: " + settingKey); + //noinspection unchecked + setting.save(migratedValue); + } + + /** + * Sets, but does _not_ persistently save the value. + * This method is only to be used by the Settings preference code. + * + * This intentionally is a static method to deter + * accidental usage when {@link #save(Object)} was intended. + */ + public static void privateSetValueFromString(@NonNull Setting setting, @NonNull String newValue) { + setting.setValueFromString(newValue); + } + + /** + * Sets the value of {@link #value}, but do not save to {@link #preferences}. + */ + protected abstract void setValueFromString(@NonNull String newValue); + + /** + * Load and set the value of {@link #value}. + */ + protected abstract void load(); + + /** + * Persistently saves the value. + */ + public abstract void save(@NonNull T newValue); + + @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} + */ + 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); + } + } + 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 ""; + } + } + + /** + * @return if any settings that require a reboot were changed. + */ + public static boolean importFromJSON(@NonNull String settingsJsonString) { + try { + if (!settingsJsonString.matches("[\\s\\S]*\\{")) { + settingsJsonString = '{' + settingsJsonString + '}'; // Restore outer JSON braces + } + JSONObject json = new JSONObject(settingsJsonString); + + boolean rebootSettingChanged = false; + int numberOfSettingsImported = 0; + for (Setting setting : SETTINGS) { + String key = setting.getImportExportKey(); + if (json.has(key)) { + Object value = setting.readFromJSON(json, key); + if (!setting.get().equals(value)) { + rebootSettingChanged |= setting.rebootApp; + //noinspection unchecked + setting.save(value); + } + numberOfSettingsImported++; + } else if (setting.includeWithImportExport && !setting.isSetToDefault()) { + Logger.printDebug(() -> "Resetting to default: " + setting); + rebootSettingChanged |= setting.rebootApp; + setting.resetToDefault(); + } + } + + // 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. + SponsorBlockSettings.updateFromImportedSettings(); + + Utils.showToastLong(numberOfSettingsImported == 0 + ? str("revanced_settings_import_reset") + : str("revanced_settings_import_success", numberOfSettingsImported)); + + return rebootSettingChanged; + } catch (JSONException | IllegalArgumentException ex) { + Utils.showToastLong(str("revanced_settings_import_failure_parse", 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..0fa5e03fc --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/StringSetting.java @@ -0,0 +1,69 @@ +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); + } + + @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..b7be999ff --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/AbstractPreferenceFragment.java @@ -0,0 +1,276 @@ +package app.revanced.extension.shared.settings.preference; + +import android.annotation.SuppressLint; +import android.app.AlertDialog; +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.preference.*; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.settings.Setting; + +import static app.revanced.extension.shared.StringRef.str; + +@SuppressWarnings("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 restartDialogButtonText, restartDialogTitle, confirmDialogTitle; + + /** + * Used to prevent showing reboot dialog, if user cancels a setting user dialog. + */ + private boolean showingUserDialogMessage; + + private final SharedPreferences.OnSharedPreferenceChangeListener listener = (sharedPreferences, str) -> { + try { + Setting setting = Setting.getSettingFromPath(str); + if (setting == null) { + return; + } + Preference pref = findPreference(str); + if (pref == null) { + return; + } + Logger.printDebug(() -> "Preference changed: " + setting.key); + + // 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(getContext()); + } + } + + } 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 var identifier = Utils.getResourceIdentifier("revanced_prefs", "xml"); + if (identifier == 0) return; + addPreferencesFromResource(identifier); + + PreferenceScreen screen = getPreferenceScreen(); + Utils.sortPreferenceGroups(screen); + Utils.setPreferenceTitlesToMultiLineIfNeeded(screen); + } + + private void showSettingUserDialogConfirmation(SwitchPreference switchPref, BooleanSetting setting) { + Utils.verifyOnMainThread(); + + final var context = getContext(); + if (confirmDialogTitle == null) { + confirmDialogTitle = str("revanced_settings_confirm_user_dialog_title"); + } + showingUserDialogMessage = true; + new AlertDialog.Builder(context) + .setTitle(confirmDialogTitle) + .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) { + updatePreferenceScreen((PreferenceScreen) pref, syncSettingValue, applySettingToPreference); + } else if (pref.hasKey()) { + String key = pref.getKey(); + Setting setting = Setting.getSettingFromPath(key); + + if (setting != null) { + updatePreference(pref, setting, syncSettingValue, applySettingToPreference); + } else if (BaseSettings.DEBUG.get() && (pref instanceof SwitchPreference + || pref instanceof EditTextPreference || pref instanceof ListPreference)) { + // Probably a typo in the patches preference declaration. + Logger.printException(() -> "Preference key has no setting: " + key); + } + } + } + } + + /** + * 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 switchPref = (SwitchPreference) pref; + BooleanSetting boolSetting = (BooleanSetting) setting; + if (applySettingToPreference) { + switchPref.setChecked(boolSetting.get()); + } else { + BooleanSetting.privateSetValue(boolSetting, switchPref.isChecked()); + } + } else if (pref instanceof EditTextPreference) { + EditTextPreference editPreference = (EditTextPreference) pref; + if (applySettingToPreference) { + editPreference.setText(setting.get().toString()); + } else { + Setting.privateSetValueFromString(setting, editPreference.getText()); + } + } else if (pref instanceof ListPreference) { + ListPreference listPref = (ListPreference) pref; + if (applySettingToPreference) { + listPref.setValue(setting.get().toString()); + } else { + Setting.privateSetValueFromString(setting, listPref.getValue()); + } + updateListPreferenceSummary(listPref, 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()); + } + + protected void updateListPreferenceSummary(ListPreference listPreference, Setting setting) { + String objectStringValue = setting.get().toString(); + final int entryIndex = listPreference.findIndexOfValue(objectStringValue); + if (entryIndex >= 0) { + listPreference.setSummary(listPreference.getEntries()[entryIndex]); + } else { + // Value is not an available option. + // User manually edited import data, or options changed and current selection is no longer available. + // Still show the value in the summary, so it's clear that something is selected. + listPreference.setSummary(objectStringValue); + } + } + + public static void showRestartDialog(@NonNull final Context context) { + Utils.verifyOnMainThread(); + if (restartDialogTitle == null) { + restartDialogTitle = str("revanced_settings_restart_title"); + } + if (restartDialogButtonText == null) { + restartDialogButtonText = str("revanced_settings_restart"); + } + new AlertDialog.Builder(context) + .setMessage(restartDialogTitle) + .setPositiveButton(restartDialogButtonText, (dialog, id) + -> Utils.restartApp(context)) + .setNegativeButton(android.R.string.cancel, null) + .setCancelable(false) + .show(); + } + + @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 onDestroy() { + getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(listener); + super.onDestroy(); + } +} 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..c750ca3f1 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/ImportExportPreference.java @@ -0,0 +1,99 @@ +package app.revanced.extension.shared.settings.preference; + +import android.app.AlertDialog; +import android.content.Context; +import android.os.Build; +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.Logger; +import app.revanced.extension.shared.Utils; + +import static app.revanced.extension.shared.StringRef.str; + +@SuppressWarnings({"unused", "deprecation"}) +public class ImportExportPreference extends EditTextPreference implements Preference.OnPreferenceClickListener { + + private String existingSettings; + + private void init() { + setSelectable(true); + + EditText editText = getEditText(); + editText.setTextIsSelectable(true); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + 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); + + // Show the user the settings in JSON format. + builder.setNeutralButton(str("revanced_settings_import_copy"), (dialog, which) -> { + Utils.setClipboard(getEditText().getText().toString()); + }).setPositiveButton(str("revanced_settings_import"), (dialog, which) -> { + importSettings(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/ReVancedAboutPreference.java b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java new file mode 100644 index 000000000..89fbe80e9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/ReVancedAboutPreference.java @@ -0,0 +1,325 @@ +package app.revanced.extension.shared.settings.preference; + +import static app.revanced.extension.shared.StringRef.sf; +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.youtube.requests.Route.Method.GET; + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.Window; +import android.webkit.WebView; +import android.webkit.WebViewClient; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.util.ArrayList; +import java.util.List; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.requests.Requester; +import app.revanced.extension.youtube.requests.Route; + +/** + * Opens a dialog showing the links from {@link SocialLinksRoutes}. + */ +@SuppressWarnings({"unused", "deprecation"}) +public class ReVancedAboutPreference extends Preference { + + private static String useNonBreakingHyphens(String text) { + // Replace any dashes with non breaking dashes, so the English text 'pre-release' + // and the dev release number does not break and cover two lines. + return text.replace("-", "‑"); // #8209 = non breaking hyphen. + } + + private static String getColorHexString(int color) { + return String.format("#%06X", (0x00FFFFFF & color)); + } + + protected boolean isDarkModeEnabled() { + Configuration config = getContext().getResources().getConfiguration(); + final int currentNightMode = config.uiMode & Configuration.UI_MODE_NIGHT_MASK; + return currentNightMode == Configuration.UI_MODE_NIGHT_YES; + } + + /** + * Subclasses can override this and provide a themed color. + */ + protected int getLightColor() { + return Color.WHITE; + } + + /** + * Subclasses can override this and provide a themed color. + */ + protected int getDarkColor() { + return Color.BLACK; + } + + private String createDialogHtml(WebLink[] socialLinks) { + final boolean isNetworkConnected = Utils.isNetworkConnected(); + + StringBuilder builder = new StringBuilder(); + builder.append(""); + builder.append(""); + + final boolean isDarkMode = isDarkModeEnabled(); + String backgroundColorHex = getColorHexString(isDarkMode ? getDarkColor() : getLightColor()); + String foregroundColorHex = getColorHexString(isDarkMode ? getLightColor() : getDarkColor()); + // Apply light/dark mode colors. + builder.append(String.format( + "", + backgroundColorHex, foregroundColorHex, foregroundColorHex)); + + if (isNetworkConnected) { + builder.append(""); + } + + String patchesVersion = Utils.getPatchesReleaseVersion(); + + // Add the title. + builder.append("

") + .append("ReVanced") + .append("

"); + + builder.append("

") + // Replace hyphens with non breaking dashes so the version number does not break lines. + .append(useNonBreakingHyphens(str("revanced_settings_about_links_body", patchesVersion))) + .append("

"); + + // Add a disclaimer if using a dev release. + if (patchesVersion.contains("dev")) { + builder.append("

") + // English text 'Pre-release' can break lines. + .append(useNonBreakingHyphens(str("revanced_settings_about_links_dev_header"))) + .append("

"); + + builder.append("

") + .append(str("revanced_settings_about_links_dev_body")) + .append("

"); + } + + builder.append("

") + .append(str("revanced_settings_about_links_header")) + .append("

"); + + builder.append("
"); + for (WebLink social : socialLinks) { + builder.append("
"); + builder.append(String.format("%s", social.url, social.name)); + builder.append("
"); + } + builder.append("
"); + + builder.append(""); + return builder.toString(); + } + + { + setOnPreferenceClickListener(pref -> { + // Show a progress spinner if the social links are not fetched yet. + if (!SocialLinksRoutes.hasFetchedLinks() && Utils.isNetworkConnected()) { + ProgressDialog progress = new ProgressDialog(getContext()); + progress.setProgressStyle(ProgressDialog.STYLE_SPINNER); + progress.show(); + Utils.runOnBackgroundThread(() -> fetchLinksAndShowDialog(progress)); + } else { + // No network call required and can run now. + fetchLinksAndShowDialog(null); + } + + return false; + }); + } + + private void fetchLinksAndShowDialog(@Nullable ProgressDialog progress) { + WebLink[] socialLinks = SocialLinksRoutes.fetchSocialLinks(); + String htmlDialog = createDialogHtml(socialLinks); + + Utils.runOnMainThreadNowOrLater(() -> { + if (progress != null) { + progress.dismiss(); + } + new WebViewDialog(getContext(), htmlDialog).show(); + }); + } + + public ReVancedAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ReVancedAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ReVancedAboutPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ReVancedAboutPreference(Context context) { + super(context); + } +} + +/** + * Displays html content as a dialog. Any links a user taps on are opened in an external browser. + */ +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; + } + } +} + +class WebLink { + final boolean preferred; + final String name; + final String url; + + WebLink(JSONObject json) throws JSONException { + this(json.getBoolean("preferred"), + json.getString("name"), + json.getString("url") + ); + } + + WebLink(boolean preferred, String name, String url) { + this.preferred = preferred; + this.name = name; + this.url = url; + } + + @NonNull + @Override + public String toString() { + return "ReVancedSocialLink{" + + "preferred=" + preferred + + ", name='" + name + '\'' + + ", url='" + url + '\'' + + '}'; + } +} + +class SocialLinksRoutes { + /** + * Simple link to the website donate page, + * rather than fetching and parsing the donation links using the API. + */ + public static final WebLink DONATE_LINK = new WebLink(true, + sf("revanced_settings_about_links_donate").toString(), + "https://revanced.app/donate"); + + /** + * Links to use if fetch links api call fails. + */ + private static final WebLink[] NO_CONNECTION_STATIC_LINKS = { + new WebLink(true, "ReVanced.app", "https://revanced.app"), + DONATE_LINK, + }; + + private static final String SOCIAL_LINKS_PROVIDER = "https://api.revanced.app/v2"; + private static final Route.CompiledRoute GET_SOCIAL = new Route(GET, "/socials").compile(); + + @Nullable + private static volatile WebLink[] fetchedLinks; + + static boolean hasFetchedLinks() { + return fetchedLinks != null; + } + + static WebLink[] fetchSocialLinks() { + try { + if (hasFetchedLinks()) return fetchedLinks; + + // Check if there is no internet connection. + if (!Utils.isNetworkConnected()) return NO_CONNECTION_STATIC_LINKS; + + HttpURLConnection connection = Requester.getConnectionFromCompiledRoute(SOCIAL_LINKS_PROVIDER, GET_SOCIAL); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + Logger.printDebug(() -> "Fetching social links from: " + connection.getURL()); + + // Do not show an exception toast if the server is down + final int responseCode = connection.getResponseCode(); + if (responseCode != 200) { + Logger.printDebug(() -> "Failed to get social links. Response code: " + responseCode); + return NO_CONNECTION_STATIC_LINKS; + } + + JSONObject json = Requester.parseJSONObjectAndDisconnect(connection); + JSONArray socials = json.getJSONArray("socials"); + + List links = new ArrayList<>(); + + links.add(DONATE_LINK); // Show donate link first. + for (int i = 0, length = socials.length(); i < length; i++) { + WebLink link = new WebLink(socials.getJSONObject(i)); + links.add(link); + } + + Logger.printDebug(() -> "links: " + links); + + return fetchedLinks = links.toArray(new WebLink[0]); + + } catch (SocketTimeoutException ex) { + Logger.printInfo(() -> "Could not fetch social links", ex); // No toast. + } catch (JSONException ex) { + Logger.printException(() -> "Could not parse about information", ex); + } catch (Exception ex) { + Logger.printException(() -> "Failed to get about information", ex); + } + + return NO_CONNECTION_STATIC_LINKS; + } +} 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..3e9a96961 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/ResettableEditTextPreference.java @@ -0,0 +1,67 @@ +package app.revanced.extension.shared.settings.preference; + +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 app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.Logger; + +import java.util.Objects; + +import static app.revanced.extension.shared.StringRef.str; + +@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) { + super.onPrepareDialogBuilder(builder); + Utils.setEditTextDialogTheme(builder); + + Setting setting = Setting.getSettingFromPath(getKey()); + if (setting != null) { + builder.setNeutralButton(str("revanced_settings_reset"), null); + } + } + + @Override + protected void showDialog(Bundle state) { + super.showDialog(state); + + // Override the button click listener to prevent dismissing the dialog. + Button button = ((AlertDialog) getDialog()).getButton(AlertDialog.BUTTON_NEUTRAL); + if (button == null) { + return; + } + button.setOnClickListener(v -> { + try { + 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..4e9c1f2e0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/shared/settings/preference/SharedPrefCategory.java @@ -0,0 +1,190 @@ +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 app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; + +import java.util.Objects; + +/** + * 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); + } + } 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/syncforreddit/FixRedditVideoDownloadPatch.java b/extensions/shared/src/main/java/app/revanced/extension/syncforreddit/FixRedditVideoDownloadPatch.java new file mode 100644 index 000000000..e006e31e6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/syncforreddit/FixRedditVideoDownloadPatch.java @@ -0,0 +1,77 @@ +package app.revanced.extension.syncforreddit; + +import android.util.Pair; +import androidx.annotation.Nullable; +import org.w3c.dom.Element; +import org.xml.sax.SAXException; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; + +/** + * @noinspection unused + */ +public class FixRedditVideoDownloadPatch { + private static @Nullable Pair getBestMpEntry(Element element) { + var representations = element.getElementsByTagName("Representation"); + var entries = new ArrayList>(); + + for (int i = 0; i < representations.getLength(); i++) { + Element representation = (Element) representations.item(i); + var bandwidthStr = representation.getAttribute("bandwidth"); + try { + var bandwidth = Integer.parseInt(bandwidthStr); + var baseUrl = representation.getElementsByTagName("BaseURL").item(0); + if (baseUrl != null) { + entries.add(new Pair<>(bandwidth, baseUrl.getTextContent())); + } + } catch (NumberFormatException ignored) { + } + } + + if (entries.isEmpty()) { + return null; + } + + Collections.sort(entries, (e1, e2) -> e2.first - e1.first); + return entries.get(0); + } + + private static String[] parse(byte[] data) throws ParserConfigurationException, IOException, SAXException { + var adaptionSets = DocumentBuilderFactory + .newInstance() + .newDocumentBuilder() + .parse(new ByteArrayInputStream(data)) + .getElementsByTagName("AdaptationSet"); + + String videoUrl = null; + String audioUrl = null; + + for (int i = 0; i < adaptionSets.getLength(); i++) { + Element element = (Element) adaptionSets.item(i); + var contentType = element.getAttribute("contentType"); + var bestEntry = getBestMpEntry(element); + if (bestEntry == null) continue; + + if (contentType.equalsIgnoreCase("video")) { + videoUrl = bestEntry.second; + } else if (contentType.equalsIgnoreCase("audio")) { + audioUrl = bestEntry.second; + } + } + + return new String[]{videoUrl, audioUrl}; + } + + public static String[] getLinks(byte[] data) { + try { + return parse(data); + } catch (ParserConfigurationException | IOException | SAXException e) { + return new String[]{null, null}; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/syncforreddit/FixSLinksPatch.java b/extensions/shared/src/main/java/app/revanced/extension/syncforreddit/FixSLinksPatch.java new file mode 100644 index 000000000..de6a96c12 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/syncforreddit/FixSLinksPatch.java @@ -0,0 +1,24 @@ +package app.revanced.extension.syncforreddit; + +import com.laurencedawson.reddit_sync.ui.activities.WebViewActivity; + +import app.revanced.extension.shared.fixes.slink.BaseFixSLinksPatch; + +/** @noinspection unused*/ +public class FixSLinksPatch extends BaseFixSLinksPatch { + static { + INSTANCE = new FixSLinksPatch(); + } + + private FixSLinksPatch() { + webViewActivityClass = WebViewActivity.class; + } + + public static boolean patchResolveSLink(String link) { + return INSTANCE.resolveSLink(link); + } + + public static void patchSetAccessToken(String accessToken) { + INSTANCE.setAccessToken(accessToken); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/Utils.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/Utils.java new file mode 100644 index 000000000..277965921 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/Utils.java @@ -0,0 +1,25 @@ +package app.revanced.extension.tiktok; + +import app.revanced.extension.shared.settings.StringSetting; + +public class Utils { + + // Edit: This could be handled using a custom Setting class + // that saves its value to preferences and JSON using the formatted String created here. + public static long[] parseMinMax(StringSetting setting) { + final String[] minMax = setting.get().split("-"); + if (minMax.length == 2) { + try { + final long min = Long.parseLong(minMax[0]); + final long max = Long.parseLong(minMax[1]); + + if (min <= max && min >= 0) return new long[]{min, max}; + + } catch (NumberFormatException ignored) { + } + } + + setting.save("0-" + Long.MAX_VALUE); + return new long[]{0L, Long.MAX_VALUE}; + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/cleardisplay/RememberClearDisplayPatch.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/cleardisplay/RememberClearDisplayPatch.java new file mode 100644 index 000000000..e436b5dcd --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/cleardisplay/RememberClearDisplayPatch.java @@ -0,0 +1,13 @@ +package app.revanced.extension.tiktok.cleardisplay; + +import app.revanced.extension.tiktok.settings.Settings; + +@SuppressWarnings("unused") +public class RememberClearDisplayPatch { + public static boolean getClearDisplayState() { + return Settings.CLEAR_DISPLAY.get(); + } + public static void rememberClearDisplayState(boolean newState) { + Settings.CLEAR_DISPLAY.save(newState); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/download/DownloadsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/download/DownloadsPatch.java new file mode 100644 index 000000000..c55d62878 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/download/DownloadsPatch.java @@ -0,0 +1,14 @@ +package app.revanced.extension.tiktok.download; + +import app.revanced.extension.tiktok.settings.Settings; + +@SuppressWarnings("unused") +public class DownloadsPatch { + public static String getDownloadPath() { + return Settings.DOWNLOAD_PATH.get(); + } + + public static boolean shouldRemoveWatermark() { + return Settings.DOWNLOAD_WATERMARK.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/AdsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/AdsFilter.java new file mode 100644 index 000000000..31a982c68 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/AdsFilter.java @@ -0,0 +1,16 @@ +package app.revanced.extension.tiktok.feedfilter; + +import app.revanced.extension.tiktok.settings.Settings; +import com.ss.android.ugc.aweme.feed.model.Aweme; + +public class AdsFilter implements IFilter { + @Override + public boolean getEnabled() { + return Settings.REMOVE_ADS.get(); + } + + @Override + public boolean getFiltered(Aweme item) { + return item.isAd() || item.isWithPromotionalMusic(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/FeedItemsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/FeedItemsFilter.java new file mode 100644 index 000000000..e1e0add8e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/FeedItemsFilter.java @@ -0,0 +1,34 @@ +package app.revanced.extension.tiktok.feedfilter; + +import com.ss.android.ugc.aweme.feed.model.Aweme; +import com.ss.android.ugc.aweme.feed.model.FeedItemList; + +import java.util.Iterator; +import java.util.List; + +public final class FeedItemsFilter { + private static final List FILTERS = List.of( + new AdsFilter(), + new LiveFilter(), + new StoryFilter(), + new ImageVideoFilter(), + new ViewCountFilter(), + new LikeCountFilter() + ); + + public static void filter(FeedItemList feedItemList) { + Iterator feedItemListIterator = feedItemList.items.iterator(); + while (feedItemListIterator.hasNext()) { + Aweme item = feedItemListIterator.next(); + if (item == null) continue; + + for (IFilter filter : FILTERS) { + boolean enabled = filter.getEnabled(); + if (enabled && filter.getFiltered(item)) { + feedItemListIterator.remove(); + break; + } + } + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/IFilter.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/IFilter.java new file mode 100644 index 000000000..57639258d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/IFilter.java @@ -0,0 +1,9 @@ +package app.revanced.extension.tiktok.feedfilter; + +import com.ss.android.ugc.aweme.feed.model.Aweme; + +public interface IFilter { + boolean getEnabled(); + + boolean getFiltered(Aweme item); +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/ImageVideoFilter.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/ImageVideoFilter.java new file mode 100644 index 000000000..ed3e7cdb9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/ImageVideoFilter.java @@ -0,0 +1,16 @@ +package app.revanced.extension.tiktok.feedfilter; + +import app.revanced.extension.tiktok.settings.Settings; +import com.ss.android.ugc.aweme.feed.model.Aweme; + +public class ImageVideoFilter implements IFilter { + @Override + public boolean getEnabled() { + return Settings.HIDE_IMAGE.get(); + } + + @Override + public boolean getFiltered(Aweme item) { + return item.isImage() || item.isPhotoMode(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/LikeCountFilter.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/LikeCountFilter.java new file mode 100644 index 000000000..57eb665ea --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/LikeCountFilter.java @@ -0,0 +1,32 @@ +package app.revanced.extension.tiktok.feedfilter; + +import app.revanced.extension.tiktok.settings.Settings; +import com.ss.android.ugc.aweme.feed.model.Aweme; +import com.ss.android.ugc.aweme.feed.model.AwemeStatistics; + +import static app.revanced.extension.tiktok.Utils.parseMinMax; + +public final class LikeCountFilter implements IFilter { + final long minLike; + final long maxLike; + + LikeCountFilter() { + long[] minMax = parseMinMax(Settings.MIN_MAX_LIKES); + minLike = minMax[0]; + maxLike = minMax[1]; + } + + @Override + public boolean getEnabled() { + return true; + } + + @Override + public boolean getFiltered(Aweme item) { + AwemeStatistics statistics = item.getStatistics(); + if (statistics == null) return false; + + long likeCount = statistics.getDiggCount(); + return likeCount < minLike || likeCount > maxLike; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/LiveFilter.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/LiveFilter.java new file mode 100644 index 000000000..db6ab0af0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/LiveFilter.java @@ -0,0 +1,16 @@ +package app.revanced.extension.tiktok.feedfilter; + +import app.revanced.extension.tiktok.settings.Settings; +import com.ss.android.ugc.aweme.feed.model.Aweme; + +public class LiveFilter implements IFilter { + @Override + public boolean getEnabled() { + return Settings.HIDE_LIVE.get(); + } + + @Override + public boolean getFiltered(Aweme item) { + return item.isLive() || item.isLiveReplay(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/StoryFilter.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/StoryFilter.java new file mode 100644 index 000000000..85d0a7088 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/StoryFilter.java @@ -0,0 +1,16 @@ +package app.revanced.extension.tiktok.feedfilter; + +import app.revanced.extension.tiktok.settings.Settings; +import com.ss.android.ugc.aweme.feed.model.Aweme; + +public class StoryFilter implements IFilter { + @Override + public boolean getEnabled() { + return Settings.HIDE_STORY.get(); + } + + @Override + public boolean getFiltered(Aweme item) { + return item.getIsTikTokStory(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/ViewCountFilter.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/ViewCountFilter.java new file mode 100644 index 000000000..ca9156f84 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/feedfilter/ViewCountFilter.java @@ -0,0 +1,32 @@ +package app.revanced.extension.tiktok.feedfilter; + +import app.revanced.extension.tiktok.settings.Settings; +import com.ss.android.ugc.aweme.feed.model.Aweme; +import com.ss.android.ugc.aweme.feed.model.AwemeStatistics; + +import static app.revanced.extension.tiktok.Utils.parseMinMax; + +public class ViewCountFilter implements IFilter { + final long minView; + final long maxView; + + ViewCountFilter() { + long[] minMax = parseMinMax(Settings.MIN_MAX_VIEWS); + minView = minMax[0]; + maxView = minMax[1]; + } + + @Override + public boolean getEnabled() { + return true; + } + + @Override + public boolean getFiltered(Aweme item) { + AwemeStatistics statistics = item.getStatistics(); + if (statistics == null) return false; + + long playCount = statistics.getPlayCount(); + return playCount < minView || playCount > maxView; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/AdPersonalizationActivityHook.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/AdPersonalizationActivityHook.java new file mode 100644 index 000000000..11304eb1e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/AdPersonalizationActivityHook.java @@ -0,0 +1,82 @@ +package app.revanced.extension.tiktok.settings; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.preference.PreferenceFragment; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.tiktok.settings.preference.ReVancedPreferenceFragment; +import com.bytedance.ies.ugc.aweme.commercialize.compliance.personalization.AdPersonalizationActivity; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; + +/** + * Hooks AdPersonalizationActivity. + *

+ * This class is responsible for injecting our own fragment by replacing the AdPersonalizationActivity. + * + * @noinspection unused + */ +public class AdPersonalizationActivityHook { + public static Object createSettingsEntry(String entryClazzName, String entryInfoClazzName) { + try { + Class entryClazz = Class.forName(entryClazzName); + Class entryInfoClazz = Class.forName(entryInfoClazzName); + Constructor entryConstructor = entryClazz.getConstructor(entryInfoClazz); + Constructor entryInfoConstructor = entryInfoClazz.getDeclaredConstructors()[0]; + Object buttonInfo = entryInfoConstructor.newInstance("ReVanced settings", null, (View.OnClickListener) view -> startSettingsActivity(), "revanced"); + return entryConstructor.newInstance(buttonInfo); + } catch (ClassNotFoundException | NoSuchMethodException | InvocationTargetException | IllegalAccessException | + InstantiationException e) { + throw new RuntimeException(e); + } + } + + /*** + * Initialize the settings menu. + * @param base The activity to initialize the settings menu on. + * @return Whether the settings menu should be initialized. + */ + public static boolean initialize(AdPersonalizationActivity base) { + Bundle extras = base.getIntent().getExtras(); + if (extras != null && !extras.getBoolean("revanced", false)) return false; + + SettingsStatus.load(); + + LinearLayout linearLayout = new LinearLayout(base); + linearLayout.setLayoutParams(new LinearLayout.LayoutParams(-1, -1)); + linearLayout.setOrientation(LinearLayout.VERTICAL); + linearLayout.setFitsSystemWindows(true); + linearLayout.setTransitionGroup(true); + + FrameLayout fragment = new FrameLayout(base); + fragment.setLayoutParams(new FrameLayout.LayoutParams(-1, -1)); + int fragmentId = View.generateViewId(); + fragment.setId(fragmentId); + + linearLayout.addView(fragment); + base.setContentView(linearLayout); + + PreferenceFragment preferenceFragment = new ReVancedPreferenceFragment(); + base.getFragmentManager().beginTransaction().replace(fragmentId, preferenceFragment).commit(); + + return true; + } + + private static void startSettingsActivity() { + Context appContext = Utils.getContext(); + if (appContext != null) { + Intent intent = new Intent(appContext, AdPersonalizationActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra("revanced", true); + appContext.startActivity(intent); + } else { + Logger.printDebug(() -> "Utils.getContext() return null"); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/Settings.java new file mode 100644 index 000000000..22a2d84d9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/Settings.java @@ -0,0 +1,26 @@ +package app.revanced.extension.tiktok.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; +import app.revanced.extension.shared.settings.FloatSetting; +import app.revanced.extension.shared.settings.StringSetting; + +public class Settings extends BaseSettings { + public static final BooleanSetting REMOVE_ADS = new BooleanSetting("remove_ads", TRUE, true); + public static final BooleanSetting HIDE_LIVE = new BooleanSetting("hide_live", FALSE, true); + public static final BooleanSetting HIDE_STORY = new BooleanSetting("hide_story", FALSE, true); + public static final BooleanSetting HIDE_IMAGE = new BooleanSetting("hide_image", FALSE, true); + public static final StringSetting MIN_MAX_VIEWS = new StringSetting("min_max_views", "0-" + Long.MAX_VALUE, true); + public static final StringSetting MIN_MAX_LIKES = new StringSetting("min_max_likes", "0-" + Long.MAX_VALUE, true); + public static final StringSetting DOWNLOAD_PATH = new StringSetting("down_path", "DCIM/TikTok"); + public static final BooleanSetting DOWNLOAD_WATERMARK = new BooleanSetting("down_watermark", TRUE); + public static final BooleanSetting CLEAR_DISPLAY = new BooleanSetting("clear_display", FALSE); + public static final FloatSetting REMEMBERED_SPEED = new FloatSetting("REMEMBERED_SPEED", 1.0f); + public static final BooleanSetting SIM_SPOOF = new BooleanSetting("simspoof", TRUE, true); + public static final StringSetting SIM_SPOOF_ISO = new StringSetting("simspoof_iso", "us"); + public static final StringSetting SIMSPOOF_MCCMNC = new StringSetting("simspoof_mccmnc", "310160"); + public static final StringSetting SIMSPOOF_OP_NAME = new StringSetting("simspoof_op_name", "T-Mobile"); +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/SettingsStatus.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/SettingsStatus.java new file mode 100644 index 000000000..7333b1798 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/SettingsStatus.java @@ -0,0 +1,23 @@ +package app.revanced.extension.tiktok.settings; + +public class SettingsStatus { + public static boolean feedFilterEnabled = false; + public static boolean downloadEnabled = false; + public static boolean simSpoofEnabled = false; + + public static void enableFeedFilter() { + feedFilterEnabled = true; + } + + public static void enableDownload() { + downloadEnabled = true; + } + + public static void enableSimSpoof() { + simSpoofEnabled = true; + } + + public static void load() { + + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/DownloadPathPreference.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/DownloadPathPreference.java new file mode 100644 index 000000000..ae4759c79 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/DownloadPathPreference.java @@ -0,0 +1,124 @@ +package app.revanced.extension.tiktok.settings.preference; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Environment; +import android.preference.DialogPreference; +import android.text.Editable; +import android.text.InputType; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.View; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RadioGroup; + +import app.revanced.extension.shared.settings.StringSetting; + +@SuppressWarnings("deprecation") +public class DownloadPathPreference extends DialogPreference { + private final Context context; + private final String[] entryValues = {"DCIM", "Movies", "Pictures"}; + private String mValue; + + private boolean mValueSet; + private int mediaPathIndex; + private String childDownloadPath; + + public DownloadPathPreference(Context context, String title, StringSetting setting) { + super(context); + this.context = context; + this.setTitle(title); + this.setSummary(Environment.getExternalStorageDirectory().getPath() + "/" + setting.get()); + this.setKey(setting.key); + this.setValue(setting.get()); + } + + public String getValue() { + return this.mValue; + } + + public void setValue(String value) { + final boolean changed = !TextUtils.equals(mValue, value); + if (changed || !mValueSet) { + mValue = value; + mValueSet = true; + persistString(value); + if (changed) { + notifyDependencyChange(shouldDisableDependents()); + notifyChanged(); + } + } + } + + @Override + protected View onCreateDialogView() { + String currentMedia = getValue().split("/")[0]; + childDownloadPath = getValue().substring(getValue().indexOf("/") + 1); + mediaPathIndex = findIndexOf(currentMedia); + + LinearLayout dialogView = new LinearLayout(context); + RadioGroup mediaPath = new RadioGroup(context); + mediaPath.setLayoutParams(new RadioGroup.LayoutParams(-1, -2)); + for (String entryValue : entryValues) { + RadioButton radioButton = new RadioButton(context); + radioButton.setText(entryValue); + radioButton.setId(View.generateViewId()); + mediaPath.addView(radioButton); + } + mediaPath.setOnCheckedChangeListener((radioGroup, id) -> { + RadioButton radioButton = radioGroup.findViewById(id); + mediaPathIndex = findIndexOf(radioButton.getText().toString()); + }); + mediaPath.check(mediaPath.getChildAt(mediaPathIndex).getId()); + EditText downloadPath = new EditText(context); + downloadPath.setInputType(InputType.TYPE_CLASS_TEXT); + downloadPath.setText(childDownloadPath); + downloadPath.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void afterTextChanged(Editable editable) { + childDownloadPath = editable.toString(); + } + }); + dialogView.setLayoutParams(new LinearLayout.LayoutParams(-1, -1)); + dialogView.setOrientation(LinearLayout.VERTICAL); + dialogView.addView(mediaPath); + dialogView.addView(downloadPath); + return dialogView; + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + builder.setTitle("Download Path"); + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> this.onClick(dialog, DialogInterface.BUTTON_POSITIVE)); + builder.setNegativeButton(android.R.string.cancel, null); + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + if (positiveResult && mediaPathIndex >= 0) { + String newValue = entryValues[mediaPathIndex] + "/" + childDownloadPath; + setSummary(Environment.getExternalStorageDirectory().getPath() + "/" + newValue); + setValue(newValue); + } + } + + private int findIndexOf(String str) { + for (int i = 0; i < entryValues.length; i++) { + if (str.equals(entryValues[i])) return i; + } + return -1; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/InputTextPreference.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/InputTextPreference.java new file mode 100644 index 000000000..b80380e51 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/InputTextPreference.java @@ -0,0 +1,17 @@ +package app.revanced.extension.tiktok.settings.preference; + +import android.content.Context; +import android.preference.EditTextPreference; + +import app.revanced.extension.shared.settings.StringSetting; + +public class InputTextPreference extends EditTextPreference { + + public InputTextPreference(Context context, String title, String summary, StringSetting setting) { + super(context); + this.setTitle(title); + this.setSummary(summary); + this.setKey(setting.key); + this.setText(setting.get()); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/RangeValuePreference.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/RangeValuePreference.java new file mode 100644 index 000000000..8eaf98ac5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/RangeValuePreference.java @@ -0,0 +1,130 @@ +package app.revanced.extension.tiktok.settings.preference; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.preference.DialogPreference; +import android.text.Editable; +import android.text.InputType; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.View; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.TextView; + +import app.revanced.extension.shared.settings.StringSetting; + +@SuppressWarnings("deprecation") +public class RangeValuePreference extends DialogPreference { + private final Context context; + + private String minValue; + + private String maxValue; + + private String mValue; + + private boolean mValueSet; + + public RangeValuePreference(Context context, String title, String summary, StringSetting setting) { + super(context); + this.context = context; + setTitle(title); + setSummary(summary); + setKey(setting.key); + setValue(setting.get()); + } + + public void setValue(String value) { + final boolean changed = !TextUtils.equals(mValue, value); + if (changed || !mValueSet) { + mValue = value; + mValueSet = true; + persistString(value); + if (changed) { + notifyDependencyChange(shouldDisableDependents()); + notifyChanged(); + } + } + } + + public String getValue() { + return mValue; + } + + @Override + protected View onCreateDialogView() { + minValue = getValue().split("-")[0]; + maxValue = getValue().split("-")[1]; + LinearLayout dialogView = new LinearLayout(context); + dialogView.setOrientation(LinearLayout.VERTICAL); + LinearLayout minView = new LinearLayout(context); + minView.setOrientation(LinearLayout.HORIZONTAL); + TextView min = new TextView(context); + min.setText("Min: "); + minView.addView(min); + EditText minEditText = new EditText(context); + minEditText.setInputType(InputType.TYPE_CLASS_NUMBER); + minEditText.setText(minValue); + minView.addView(minEditText); + dialogView.addView(minView); + LinearLayout maxView = new LinearLayout(context); + maxView.setOrientation(LinearLayout.HORIZONTAL); + TextView max = new TextView(context); + max.setText("Max: "); + maxView.addView(max); + EditText maxEditText = new EditText(context); + maxEditText.setInputType(InputType.TYPE_CLASS_NUMBER); + maxEditText.setText(maxValue); + maxView.addView(maxEditText); + dialogView.addView(maxView); + minEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void afterTextChanged(Editable editable) { + minValue = editable.toString(); + } + }); + maxEditText.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { + + } + + @Override + public void afterTextChanged(Editable editable) { + maxValue = editable.toString(); + } + }); + return dialogView; + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + builder.setPositiveButton(android.R.string.ok, (dialog, which) -> this.onClick(dialog, DialogInterface.BUTTON_POSITIVE)); + builder.setNegativeButton(android.R.string.cancel, null); + } + + @Override + protected void onDialogClosed(boolean positiveResult) { + if (positiveResult) { + String newValue = minValue + "-" + maxValue; + setValue(newValue); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/ReVancedPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/ReVancedPreferenceFragment.java new file mode 100644 index 000000000..43ab69297 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/ReVancedPreferenceFragment.java @@ -0,0 +1,54 @@ +package app.revanced.extension.tiktok.settings.preference; + +import android.preference.Preference; +import android.preference.PreferenceScreen; +import androidx.annotation.NonNull; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment; +import app.revanced.extension.tiktok.settings.preference.categories.DownloadsPreferenceCategory; +import app.revanced.extension.tiktok.settings.preference.categories.FeedFilterPreferenceCategory; +import app.revanced.extension.tiktok.settings.preference.categories.ExtensionPreferenceCategory; +import app.revanced.extension.tiktok.settings.preference.categories.SimSpoofPreferenceCategory; +import org.jetbrains.annotations.NotNull; + +/** + * 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) { + if (pref instanceof RangeValuePreference) { + RangeValuePreference rangeValuePref = (RangeValuePreference) pref; + Setting.privateSetValueFromString(setting, rangeValuePref.getValue()); + } else if (pref instanceof DownloadPathPreference) { + DownloadPathPreference downloadPathPref = (DownloadPathPreference) pref; + Setting.privateSetValueFromString(setting, downloadPathPref.getValue()); + } else { + super.syncSettingWithPreference(pref, setting, applySettingToPreference); + } + } + + @Override + protected void initialize() { + final var context = getContext(); + + // Currently no resources can be compiled for TikTok (fails with aapt error). + // So all TikTok Strings are hard coded in the extension. + restartDialogTitle = "Refresh and restart"; + restartDialogButtonText = "Restart"; + confirmDialogTitle = "Do you wish to proceed?"; + + PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(context); + setPreferenceScreen(preferenceScreen); + + // Custom categories reference app specific Settings class. + new FeedFilterPreferenceCategory(context, preferenceScreen); + new DownloadsPreferenceCategory(context, preferenceScreen); + new SimSpoofPreferenceCategory(context, preferenceScreen); + new ExtensionPreferenceCategory(context, preferenceScreen); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/TogglePreference.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/TogglePreference.java new file mode 100644 index 000000000..788b0d67d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/TogglePreference.java @@ -0,0 +1,17 @@ +package app.revanced.extension.tiktok.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/tiktok/settings/preference/categories/ConditionalPreferenceCategory.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/ConditionalPreferenceCategory.java new file mode 100644 index 000000000..d9f865ee9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/ConditionalPreferenceCategory.java @@ -0,0 +1,22 @@ +package app.revanced.extension.tiktok.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/tiktok/settings/preference/categories/DownloadsPreferenceCategory.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/DownloadsPreferenceCategory.java new file mode 100644 index 000000000..1ba3defa4 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/DownloadsPreferenceCategory.java @@ -0,0 +1,35 @@ +package app.revanced.extension.tiktok.settings.preference.categories; + +import android.content.Context; +import android.preference.PreferenceScreen; +import app.revanced.extension.tiktok.settings.Settings; +import app.revanced.extension.tiktok.settings.SettingsStatus; +import app.revanced.extension.tiktok.settings.preference.DownloadPathPreference; +import app.revanced.extension.tiktok.settings.preference.TogglePreference; + +@SuppressWarnings("deprecation") +public class DownloadsPreferenceCategory extends ConditionalPreferenceCategory { + public DownloadsPreferenceCategory(Context context, PreferenceScreen screen) { + super(context, screen); + setTitle("Downloads"); + } + + @Override + public boolean getSettingsStatus() { + return SettingsStatus.downloadEnabled; + } + + @Override + public void addPreferences(Context context) { + addPreference(new DownloadPathPreference( + context, + "Download path", + Settings.DOWNLOAD_PATH + )); + addPreference(new TogglePreference( + context, + "Remove watermark", "", + Settings.DOWNLOAD_WATERMARK + )); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/ExtensionPreferenceCategory.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/ExtensionPreferenceCategory.java new file mode 100644 index 000000000..ad49df688 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/ExtensionPreferenceCategory.java @@ -0,0 +1,29 @@ +package app.revanced.extension.tiktok.settings.preference.categories; + +import android.content.Context; +import android.preference.PreferenceScreen; + +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.tiktok.settings.preference.TogglePreference; + +@SuppressWarnings("deprecation") +public class ExtensionPreferenceCategory extends ConditionalPreferenceCategory { + public ExtensionPreferenceCategory(Context context, PreferenceScreen screen) { + super(context, screen); + setTitle("Extension"); + } + + @Override + public boolean getSettingsStatus() { + return true; + } + + @Override + public void addPreferences(Context context) { + addPreference(new TogglePreference(context, + "Enable debug log", + "Show extension debug log.", + BaseSettings.DEBUG + )); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/FeedFilterPreferenceCategory.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/FeedFilterPreferenceCategory.java new file mode 100644 index 000000000..bcd56bc7e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/FeedFilterPreferenceCategory.java @@ -0,0 +1,55 @@ +package app.revanced.extension.tiktok.settings.preference.categories; + +import android.content.Context; +import android.preference.PreferenceScreen; +import app.revanced.extension.tiktok.settings.preference.RangeValuePreference; +import app.revanced.extension.tiktok.settings.Settings; +import app.revanced.extension.tiktok.settings.SettingsStatus; +import app.revanced.extension.tiktok.settings.preference.TogglePreference; + +@SuppressWarnings("deprecation") +public class FeedFilterPreferenceCategory extends ConditionalPreferenceCategory { + public FeedFilterPreferenceCategory(Context context, PreferenceScreen screen) { + super(context, screen); + setTitle("Feed filter"); + } + + @Override + public boolean getSettingsStatus() { + return SettingsStatus.feedFilterEnabled; + } + + @Override + public void addPreferences(Context context) { + addPreference(new TogglePreference( + context, + "Remove feed ads", "Remove ads from feed.", + Settings.REMOVE_ADS + )); + addPreference(new TogglePreference( + context, + "Hide livestreams", "Hide livestreams from feed.", + Settings.HIDE_LIVE + )); + addPreference(new TogglePreference( + context, + "Hide story", "Hide story from feed.", + Settings.HIDE_STORY + )); + addPreference(new TogglePreference( + context, + "Hide image video", "Hide image video from feed.", + Settings.HIDE_IMAGE + )); + addPreference(new RangeValuePreference( + context, + "Min/Max views", "The minimum or maximum views of a video to show.", + Settings.MIN_MAX_VIEWS + )); + addPreference(new RangeValuePreference( + context, + "Min/Max likes", "The minimum or maximum likes of a video to show.", + Settings.MIN_MAX_LIKES + )); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/SimSpoofPreferenceCategory.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/SimSpoofPreferenceCategory.java new file mode 100644 index 000000000..0a820dc39 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/settings/preference/categories/SimSpoofPreferenceCategory.java @@ -0,0 +1,47 @@ +package app.revanced.extension.tiktok.settings.preference.categories; + +import android.content.Context; +import android.preference.PreferenceScreen; +import app.revanced.extension.tiktok.settings.Settings; +import app.revanced.extension.tiktok.settings.SettingsStatus; +import app.revanced.extension.tiktok.settings.preference.InputTextPreference; +import app.revanced.extension.tiktok.settings.preference.TogglePreference; + +@SuppressWarnings("deprecation") +public class SimSpoofPreferenceCategory extends ConditionalPreferenceCategory { + public SimSpoofPreferenceCategory(Context context, PreferenceScreen screen) { + super(context, screen); + setTitle("Bypass regional restriction"); + } + + + @Override + public boolean getSettingsStatus() { + return SettingsStatus.simSpoofEnabled; + } + + @Override + public void addPreferences(Context context) { + addPreference(new TogglePreference( + context, + "Fake sim card info", + "Bypass regional restriction by fake sim card information.", + Settings.SIM_SPOOF + )); + addPreference(new InputTextPreference( + context, + "Country ISO", "us, uk, jp, ...", + Settings.SIM_SPOOF_ISO + )); + addPreference(new InputTextPreference( + context, + "Operator mcc+mnc", "mcc+mnc", + Settings.SIMSPOOF_MCCMNC + )); + addPreference(new InputTextPreference( + context, + "Operator name", "Name of the operator.", + Settings.SIMSPOOF_OP_NAME + )); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/speed/PlaybackSpeedPatch.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/speed/PlaybackSpeedPatch.java new file mode 100644 index 000000000..3b078ab89 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/speed/PlaybackSpeedPatch.java @@ -0,0 +1,13 @@ +package app.revanced.extension.tiktok.speed; + +import app.revanced.extension.tiktok.settings.Settings; + +public class PlaybackSpeedPatch { + public static void rememberPlaybackSpeed(float newSpeed) { + Settings.REMEMBERED_SPEED.save(newSpeed); + } + + public static float getPlaybackSpeed() { + return Settings.REMEMBERED_SPEED.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tiktok/spoof/sim/SpoofSimPatch.java b/extensions/shared/src/main/java/app/revanced/extension/tiktok/spoof/sim/SpoofSimPatch.java new file mode 100644 index 000000000..94910bdb6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tiktok/spoof/sim/SpoofSimPatch.java @@ -0,0 +1,37 @@ +package app.revanced.extension.tiktok.spoof.sim; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.tiktok.settings.Settings; + +@SuppressWarnings("unused") +public class SpoofSimPatch { + + private static final boolean ENABLED = Settings.SIM_SPOOF.get(); + + public static String getCountryIso(String value) { + if (ENABLED) { + String iso = Settings.SIM_SPOOF_ISO.get(); + Logger.printDebug(() -> "Spoofing sim ISO from: " + value + " to: " + iso); + return iso; + } + return value; + } + + public static String getOperator(String value) { + if (ENABLED) { + String mcc_mnc = Settings.SIMSPOOF_MCCMNC.get(); + Logger.printDebug(() -> "Spoofing sim MCC-MNC from: " + value + " to: " + mcc_mnc); + return mcc_mnc; + } + return value; + } + + public static String getOperatorName(String value) { + if (ENABLED) { + String operator = Settings.SIMSPOOF_OP_NAME.get(); + Logger.printDebug(() -> "Spoofing sim operator from: " + value + " to: " + operator); + return operator; + } + return value; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tudortmund/lockscreen/ShowOnLockscreenPatch.java b/extensions/shared/src/main/java/app/revanced/extension/tudortmund/lockscreen/ShowOnLockscreenPatch.java new file mode 100644 index 000000000..f2868cf4e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tudortmund/lockscreen/ShowOnLockscreenPatch.java @@ -0,0 +1,46 @@ +package app.revanced.extension.tudortmund.lockscreen; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.view.Display; +import android.view.Window; +import androidx.appcompat.app.AppCompatActivity; + +import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD; +import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; + +public class ShowOnLockscreenPatch { + /** + * @noinspection deprecation + */ + public static Window getWindow(AppCompatActivity activity, float brightness) { + Window window = activity.getWindow(); + + if (brightness >= 0) { + // High brightness set, therefore show on lockscreen. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) activity.setShowWhenLocked(true); + else window.addFlags(FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD); + } else { + // Ignore brightness reset when the screen is turned off. + DisplayManager displayManager = (DisplayManager) activity.getSystemService(Context.DISPLAY_SERVICE); + + boolean isScreenOn = false; + for (Display display : displayManager.getDisplays()) { + if (display.getState() == Display.STATE_OFF) continue; + + isScreenOn = true; + break; + } + + if (isScreenOn) { + // Hide on lockscreen. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) activity.setShowWhenLocked(false); + else window.clearFlags(FLAG_SHOW_WHEN_LOCKED | FLAG_DISMISS_KEYGUARD); + } + } + + return window; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/tumblr/patches/TimelineFilterPatch.java b/extensions/shared/src/main/java/app/revanced/extension/tumblr/patches/TimelineFilterPatch.java new file mode 100644 index 000000000..bb3a2473d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/tumblr/patches/TimelineFilterPatch.java @@ -0,0 +1,32 @@ +package app.revanced.extension.tumblr.patches; + +import com.tumblr.rumblr.model.TimelineObject; +import com.tumblr.rumblr.model.Timelineable; + +import java.util.HashSet; +import java.util.List; + +public final class TimelineFilterPatch { + private static final HashSet blockedObjectTypes = new HashSet<>(); + + static { + // This dummy gets removed by the TimelineFilterPatch and in its place, + // equivalent instructions with a different constant string + // will be inserted for each Timeline object type filter. + // Modifying this line may break the patch. + blockedObjectTypes.add("BLOCKED_OBJECT_DUMMY"); + } + + // Calls to this method are injected where the list of Timeline objects is first received. + // We modify the list filter out elements that we want to hide. + public static void filterTimeline(final List> timelineObjects) { + final var iterator = timelineObjects.iterator(); + while (iterator.hasNext()) { + var timelineElement = iterator.next(); + if (timelineElement == null) continue; + + String elementType = timelineElement.getData().getTimelineObjectType().toString(); + if (blockedObjectTypes.contains(elementType)) iterator.remove(); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/Utils.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/Utils.java new file mode 100644 index 000000000..73c363ff8 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/Utils.java @@ -0,0 +1,14 @@ +package app.revanced.extension.twitch; + +public class Utils { + + /* Called from SettingsPatch smali */ + public static int getStringId(String name) { + return app.revanced.extension.shared.Utils.getResourceIdentifier(name, "string"); + } + + /* Called from SettingsPatch smali */ + public static int getDrawableId(String name) { + return app.revanced.extension.shared.Utils.getResourceIdentifier(name, "drawable"); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/adblock/IAdblockService.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/adblock/IAdblockService.java new file mode 100644 index 000000000..457ecdf97 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/adblock/IAdblockService.java @@ -0,0 +1,26 @@ +package app.revanced.extension.twitch.adblock; + +import okhttp3.Request; + +public interface IAdblockService { + String friendlyName(); + + Integer maxAttempts(); + + Boolean isAvailable(); + + Request rewriteHlsRequest(Request originalRequest); + + static boolean isVod(Request request){ + return request.url().pathSegments().contains("vod"); + } + + static String channelName(Request request) { + for (String pathSegment : request.url().pathSegments()) { + if (pathSegment.endsWith(".m3u8")) { + return pathSegment.replace(".m3u8", ""); + } + } + return null; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/adblock/LuminousService.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/adblock/LuminousService.java new file mode 100644 index 000000000..ef217daf0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/adblock/LuminousService.java @@ -0,0 +1,47 @@ +package app.revanced.extension.twitch.adblock; + +import app.revanced.extension.shared.Logger; +import okhttp3.HttpUrl; +import okhttp3.Request; + +import static app.revanced.extension.shared.StringRef.str; + +public class LuminousService implements IAdblockService { + @Override + public String friendlyName() { + return str("revanced_proxy_luminous"); + } + + @Override + public Integer maxAttempts() { + return 2; + } + + @Override + public Boolean isAvailable() { + return true; + } + + @Override + public Request rewriteHlsRequest(Request originalRequest) { + var type = IAdblockService.isVod(originalRequest) ? "vod" : "playlist"; + var url = HttpUrl.parse("https://eu.luminous.dev/" + + type + + "/" + + IAdblockService.channelName(originalRequest) + + ".m3u8" + + "%3Fallow_source%3Dtrue%26allow_audio_only%3Dtrue%26fast_bread%3Dtrue" + ); + + if (url == null) { + Logger.printException(() -> "Failed to parse rewritten URL"); + return null; + } + + // Overwrite old request + return new Request.Builder() + .get() + .url(url) + .build(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/adblock/PurpleAdblockService.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/adblock/PurpleAdblockService.java new file mode 100644 index 000000000..ba1bd183a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/adblock/PurpleAdblockService.java @@ -0,0 +1,96 @@ +package app.revanced.extension.twitch.adblock; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.twitch.api.RetrofitClient; +import okhttp3.HttpUrl; +import okhttp3.Request; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import static app.revanced.extension.shared.StringRef.str; + +public class PurpleAdblockService implements IAdblockService { + private final Map tunnels = new HashMap<>() {{ + put("https://eu1.jupter.ga", false); + put("https://eu2.jupter.ga", false); + }}; + + @Override + public String friendlyName() { + return str("revanced_proxy_purpleadblock"); + } + + @Override + public Integer maxAttempts() { + return 3; + } + + @Override + public Boolean isAvailable() { + for (String tunnel : tunnels.keySet()) { + var success = true; + + try { + var response = RetrofitClient.getInstance().getPurpleAdblockApi().ping(tunnel).execute(); + if (!response.isSuccessful()) { + Logger.printException(() -> + "PurpleAdBlock tunnel $tunnel returned an error: HTTP code " + response.code() + ); + Logger.printDebug(response::message); + + try (var errorBody = response.errorBody()) { + if (errorBody != null) { + Logger.printDebug(() -> { + try { + return errorBody.string(); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + } + + success = false; + } + } catch (Exception ex) { + Logger.printException(() -> "PurpleAdBlock tunnel $tunnel is unavailable", ex); + success = false; + } + + // Cache availability data + tunnels.put(tunnel, success); + + if (success) + return true; + } + + return false; + } + + @Override + public Request rewriteHlsRequest(Request originalRequest) { + for (Map.Entry entry : tunnels.entrySet()) { + if (!entry.getValue()) continue; + + var server = entry.getKey(); + + // Compose new URL + var url = HttpUrl.parse(server + "/channel/" + IAdblockService.channelName(originalRequest)); + if (url == null) { + Logger.printException(() -> "Failed to parse rewritten URL"); + return null; + } + + // Overwrite old request + return new Request.Builder() + .get() + .url(url) + .build(); + } + + Logger.printException(() -> "No tunnels are available"); + return null; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/api/PurpleAdblockApi.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/api/PurpleAdblockApi.java new file mode 100644 index 000000000..519a3fe8a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/api/PurpleAdblockApi.java @@ -0,0 +1,12 @@ +package app.revanced.extension.twitch.api; + +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Url; + +/* only used for service pings */ +public interface PurpleAdblockApi { + @GET /* root */ + Call ping(@Url String baseUrl); +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/api/RequestInterceptor.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/api/RequestInterceptor.java new file mode 100644 index 000000000..2309bcf64 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/api/RequestInterceptor.java @@ -0,0 +1,120 @@ +package app.revanced.extension.twitch.api; + +import androidx.annotation.NonNull; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.twitch.adblock.IAdblockService; +import app.revanced.extension.twitch.adblock.LuminousService; +import app.revanced.extension.twitch.adblock.PurpleAdblockService; +import app.revanced.extension.twitch.settings.Settings; +import okhttp3.Interceptor; +import okhttp3.Response; + +import java.io.IOException; + +import static app.revanced.extension.shared.StringRef.str; + +public class RequestInterceptor implements Interceptor { + private IAdblockService activeService = null; + + private static final String PROXY_DISABLED = str("revanced_block_embedded_ads_entry_1"); + private static final String LUMINOUS_SERVICE = str("revanced_block_embedded_ads_entry_2"); + private static final String PURPLE_ADBLOCK_SERVICE = str("revanced_block_embedded_ads_entry_3"); + + + @NonNull + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + var originalRequest = chain.request(); + + if (Settings.BLOCK_EMBEDDED_ADS.get().equals(PROXY_DISABLED)) { + return chain.proceed(originalRequest); + } + + Logger.printDebug(() -> "Intercepted request to URL:" + originalRequest.url()); + + // Skip if not HLS manifest request + if (!originalRequest.url().host().contains("usher.ttvnw.net")) { + return chain.proceed(originalRequest); + } + + final String isVod; + if (IAdblockService.isVod(originalRequest)) isVod = "yes"; + else isVod = "no"; + + Logger.printDebug(() -> "Found HLS manifest request. Is VOD? " + + isVod + + "; Channel: " + + IAdblockService.channelName(originalRequest) + ); + + // None of the services support VODs currently + if (IAdblockService.isVod(originalRequest)) return chain.proceed(originalRequest); + + updateActiveService(); + + if (activeService != null) { + var available = activeService.isAvailable(); + var rewritten = activeService.rewriteHlsRequest(originalRequest); + + + if (!available || rewritten == null) { + Utils.showToastShort(String.format( + str("revanced_embedded_ads_service_unavailable"), activeService.friendlyName() + )); + return chain.proceed(originalRequest); + } + + Logger.printDebug(() -> "Rewritten HLS stream URL: " + rewritten.url()); + + var maxAttempts = activeService.maxAttempts(); + + for (var i = 1; i <= maxAttempts; i++) { + // Execute rewritten request and close body to allow multiple proceed() calls + var response = chain.proceed(rewritten); + response.close(); + + if (!response.isSuccessful()) { + int attempt = i; + Logger.printException(() -> "Request failed (attempt " + + attempt + + "/" + maxAttempts + "): HTTP error " + + response.code() + + " (" + response.message() + ")" + ); + + try { + Thread.sleep(50); + } catch (InterruptedException e) { + Logger.printException(() -> "Failed to sleep", e); + } + } else { + // Accept response from ad blocker + Logger.printDebug(() -> "Ad-blocker used"); + return chain.proceed(rewritten); + } + } + + // maxAttempts exceeded; giving up on using the ad blocker + Utils.showToastLong(String.format( + str("revanced_embedded_ads_service_failed"), + activeService.friendlyName()) + ); + } + + // Adblock disabled + return chain.proceed(originalRequest); + + } + + private void updateActiveService() { + var current = Settings.BLOCK_EMBEDDED_ADS.get(); + + if (current.equals(LUMINOUS_SERVICE) && !(activeService instanceof LuminousService)) + activeService = new LuminousService(); + else if (current.equals(PURPLE_ADBLOCK_SERVICE) && !(activeService instanceof PurpleAdblockService)) + activeService = new PurpleAdblockService(); + else if (current.equals(PROXY_DISABLED)) + activeService = null; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/api/RetrofitClient.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/api/RetrofitClient.java new file mode 100644 index 000000000..24f4060b6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/api/RetrofitClient.java @@ -0,0 +1,25 @@ +package app.revanced.extension.twitch.api; + +import retrofit2.Retrofit; + +public class RetrofitClient { + + private static RetrofitClient instance = null; + private final PurpleAdblockApi purpleAdblockApi; + + private RetrofitClient() { + Retrofit retrofit = new Retrofit.Builder().baseUrl("http://localhost" /* dummy */).build(); + purpleAdblockApi = retrofit.create(PurpleAdblockApi.class); + } + + public static synchronized RetrofitClient getInstance() { + if (instance == null) { + instance = new RetrofitClient(); + } + return instance; + } + + public PurpleAdblockApi getPurpleAdblockApi() { + return purpleAdblockApi; + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/AudioAdsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/AudioAdsPatch.java new file mode 100644 index 000000000..77b7cbd5a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/AudioAdsPatch.java @@ -0,0 +1,10 @@ +package app.revanced.extension.twitch.patches; + +import app.revanced.extension.twitch.settings.Settings; + +@SuppressWarnings("unused") +public class AudioAdsPatch { + public static boolean shouldBlockAudioAds() { + return Settings.BLOCK_AUDIO_ADS.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/AutoClaimChannelPointsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/AutoClaimChannelPointsPatch.java new file mode 100644 index 000000000..55c32c7ea --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/AutoClaimChannelPointsPatch.java @@ -0,0 +1,10 @@ +package app.revanced.extension.twitch.patches; + +import app.revanced.extension.twitch.settings.Settings; + +@SuppressWarnings("unused") +public class AutoClaimChannelPointsPatch { + public static boolean shouldAutoClaim() { + return Settings.AUTO_CLAIM_CHANNEL_POINTS.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/DebugModePatch.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/DebugModePatch.java new file mode 100644 index 000000000..dc4ab8094 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/DebugModePatch.java @@ -0,0 +1,10 @@ +package app.revanced.extension.twitch.patches; + +import app.revanced.extension.twitch.settings.Settings; + +@SuppressWarnings("unused") +public class DebugModePatch { + public static boolean isDebugModeEnabled() { + return Settings.TWITCH_DEBUG_MODE.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/EmbeddedAdsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/EmbeddedAdsPatch.java new file mode 100644 index 000000000..bb172d1a8 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/EmbeddedAdsPatch.java @@ -0,0 +1,10 @@ +package app.revanced.extension.twitch.patches; + +import app.revanced.extension.twitch.api.RequestInterceptor; + +@SuppressWarnings("unused") +public class EmbeddedAdsPatch { + public static RequestInterceptor createRequestInterceptor() { + return new RequestInterceptor(); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/ShowDeletedMessagesPatch.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/ShowDeletedMessagesPatch.java new file mode 100644 index 000000000..747a6b94d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/ShowDeletedMessagesPatch.java @@ -0,0 +1,51 @@ +package app.revanced.extension.twitch.patches; + +import static app.revanced.extension.shared.StringRef.str; + +import android.graphics.Color; +import android.graphics.Typeface; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.SpannedString; +import android.text.style.ForegroundColorSpan; +import android.text.style.StrikethroughSpan; +import android.text.style.StyleSpan; + +import androidx.annotation.Nullable; + +import app.revanced.extension.twitch.settings.Settings; +import tv.twitch.android.shared.chat.util.ClickableUsernameSpan; + +@SuppressWarnings("unused") +public class ShowDeletedMessagesPatch { + + /** + * Injection point. + */ + public static boolean shouldUseSpoiler() { + return "spoiler".equals(Settings.SHOW_DELETED_MESSAGES.get()); + } + + public static boolean shouldCrossOut() { + return "cross-out".equals(Settings.SHOW_DELETED_MESSAGES.get()); + } + + @Nullable + public static Spanned reformatDeletedMessage(Spanned original) { + if (!shouldCrossOut()) + return null; + + SpannableStringBuilder ssb = new SpannableStringBuilder(original); + ssb.setSpan(new StrikethroughSpan(), 0, original.length(), 0); + ssb.append(" (").append(str("revanced_deleted_msg")).append(")"); + ssb.setSpan(new StyleSpan(Typeface.ITALIC), original.length(), ssb.length(), 0); + + // Gray-out username + ClickableUsernameSpan[] usernameSpans = original.getSpans(0, original.length(), ClickableUsernameSpan.class); + if (usernameSpans.length > 0) { + ssb.setSpan(new ForegroundColorSpan(Color.parseColor("#ADADB8")), 0, original.getSpanEnd(usernameSpans[0]), 0); + } + + return new SpannedString(ssb); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/VideoAdsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/VideoAdsPatch.java new file mode 100644 index 000000000..6c7b739af --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/patches/VideoAdsPatch.java @@ -0,0 +1,10 @@ +package app.revanced.extension.twitch.patches; + +import app.revanced.extension.twitch.settings.Settings; + +@SuppressWarnings("unused") +public class VideoAdsPatch { + public static boolean shouldBlockVideoAds() { + return Settings.BLOCK_VIDEO_ADS.get(); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/settings/AppCompatActivityHook.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/settings/AppCompatActivityHook.java new file mode 100644 index 000000000..e617cf9b2 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/settings/AppCompatActivityHook.java @@ -0,0 +1,112 @@ +package app.revanced.extension.twitch.settings; + +import android.content.Intent; +import android.os.Bundle; +import androidx.appcompat.app.ActionBar; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.twitch.settings.preference.ReVancedPreferenceFragment; +import tv.twitch.android.feature.settings.menu.SettingsMenuGroup; +import tv.twitch.android.settings.SettingsActivity; + +import java.util.ArrayList; +import java.util.List; + +/** + * Hooks AppCompatActivity. + *

+ * This class is responsible for injecting our own fragment by replacing the AppCompatActivity. + * @noinspection unused + */ +public class AppCompatActivityHook { + private static final int REVANCED_SETTINGS_MENU_ITEM_ID = 0x7; + private static final String EXTRA_REVANCED_SETTINGS = "app.revanced.twitch.settings"; + + /** + * Launches SettingsActivity and show ReVanced settings + */ + public static void startSettingsActivity() { + Logger.printDebug(() -> "Launching ReVanced settings"); + + final var context = Utils.getContext(); + + if (context != null) { + Intent intent = new Intent(context, SettingsActivity.class); + Bundle bundle = new Bundle(); + bundle.putBoolean(EXTRA_REVANCED_SETTINGS, true); + intent.putExtras(bundle); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + } + + /** + * Helper for easy access in smali + * @return Returns string resource id + */ + public static int getReVancedSettingsString() { + return app.revanced.extension.twitch.Utils.getStringId("revanced_settings"); + } + + /** + * Intercepts settings menu group list creation in SettingsMenuPresenter$Event.MenuGroupsUpdated + * @return Returns a modified list of menu groups + */ + public static List handleSettingMenuCreation(List settingGroups, Object revancedEntry) { + List groups = new ArrayList<>(settingGroups); + + if (groups.isEmpty()) { + // Create new menu group if none exist yet + List items = new ArrayList<>(); + items.add(revancedEntry); + groups.add(new SettingsMenuGroup(items)); + } else { + // Add to last menu group + int groupIdx = groups.size() - 1; + List items = new ArrayList<>(groups.remove(groupIdx).getSettingsMenuItems()); + items.add(revancedEntry); + groups.add(new SettingsMenuGroup(items)); + } + + Logger.printDebug(() -> settingGroups.size() + " menu groups in list"); + return groups; + } + + /** + * Intercepts settings menu group onclick events + * @return Returns true if handled, otherwise false + */ + @SuppressWarnings("rawtypes") + public static boolean handleSettingMenuOnClick(Enum item) { + Logger.printDebug(() -> "item " + item.ordinal() + " clicked"); + if (item.ordinal() != REVANCED_SETTINGS_MENU_ITEM_ID) { + return false; + } + + startSettingsActivity(); + return true; + } + + /** + * Intercepts fragment loading in SettingsActivity.onCreate + * @return Returns true if the revanced settings have been requested by the user, otherwise false + */ + public static boolean handleSettingsCreation(androidx.appcompat.app.AppCompatActivity base) { + if (!base.getIntent().getBooleanExtra(EXTRA_REVANCED_SETTINGS, false)) { + Logger.printDebug(() -> "Revanced settings not requested"); + return false; // User wants to enter another settings fragment + } + Logger.printDebug(() -> "ReVanced settings requested"); + + ReVancedPreferenceFragment fragment = new ReVancedPreferenceFragment(); + ActionBar supportActionBar = base.getSupportActionBar(); + if (supportActionBar != null) + supportActionBar.setTitle(app.revanced.extension.twitch.Utils.getStringId("revanced_settings")); + + base.getFragmentManager() + .beginTransaction() + .replace(Utils.getResourceIdentifier("fragment_container", "id"), fragment) + .commit(); + return true; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/settings/Settings.java new file mode 100644 index 000000000..aa5fed4b2 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/settings/Settings.java @@ -0,0 +1,25 @@ +package app.revanced.extension.twitch.settings; + +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.shared.settings.StringSetting; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + +public class Settings extends BaseSettings { + /* Ads */ + public static final BooleanSetting BLOCK_VIDEO_ADS = new BooleanSetting("revanced_block_video_ads", TRUE); + public static final BooleanSetting BLOCK_AUDIO_ADS = new BooleanSetting("revanced_block_audio_ads", TRUE); + public static final StringSetting BLOCK_EMBEDDED_ADS = new StringSetting("revanced_block_embedded_ads", "luminous"); + + /* Chat */ + public static final StringSetting SHOW_DELETED_MESSAGES = new StringSetting("revanced_show_deleted_messages", "cross-out"); + public static final BooleanSetting AUTO_CLAIM_CHANNEL_POINTS = new BooleanSetting("revanced_auto_claim_channel_points", TRUE); + + /* Misc */ + /** + * Not to be confused with {@link BaseSettings#DEBUG}. + */ + public static final BooleanSetting TWITCH_DEBUG_MODE = new BooleanSetting("revanced_twitch_debug_mode", FALSE, true); +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/settings/preference/CustomPreferenceCategory.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/settings/preference/CustomPreferenceCategory.java new file mode 100644 index 000000000..5cef4a0b7 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/settings/preference/CustomPreferenceCategory.java @@ -0,0 +1,23 @@ +package app.revanced.extension.twitch.settings.preference; + +import android.content.Context; +import android.graphics.Color; +import android.preference.PreferenceCategory; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +public class CustomPreferenceCategory extends PreferenceCategory { + public CustomPreferenceCategory(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onBindView(View rootView) { + super.onBindView(rootView); + + if(rootView instanceof TextView) { + ((TextView) rootView).setTextColor(Color.parseColor("#8161b3")); + } + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitch/settings/preference/ReVancedPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/twitch/settings/preference/ReVancedPreferenceFragment.java new file mode 100644 index 000000000..d77d06e19 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitch/settings/preference/ReVancedPreferenceFragment.java @@ -0,0 +1,21 @@ +package app.revanced.extension.twitch.settings.preference; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment; +import app.revanced.extension.twitch.settings.Settings; + +/** + * Preference fragment for ReVanced settings + */ +public class ReVancedPreferenceFragment extends AbstractPreferenceFragment { + + @Override + protected void initialize() { + super.initialize(); + + // Do anything that forces this apps Settings bundle to load. + if (Settings.BLOCK_VIDEO_ADS.get()) { + Logger.printDebug(() -> "Block video ads enabled"); // Any statement that references the app settings. + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/json/BaseJsonHook.kt b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/json/BaseJsonHook.kt new file mode 100644 index 000000000..306b58e0a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/json/BaseJsonHook.kt @@ -0,0 +1,9 @@ +package app.revanced.extension.twitter.patches.hook.json + +import org.json.JSONObject + +abstract class BaseJsonHook : JsonHook { + abstract fun apply(json: JSONObject) + + override fun transform(json: JSONObject) = json.apply { apply(json) } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/json/JsonHook.kt b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/json/JsonHook.kt new file mode 100644 index 000000000..2d6441be7 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/json/JsonHook.kt @@ -0,0 +1,15 @@ +package app.revanced.extension.twitter.patches.hook.json + +import app.revanced.extension.twitter.patches.hook.patch.Hook +import org.json.JSONObject + +interface JsonHook : Hook { + /** + * Transform a JSONObject. + * + * @param json The JSONObject. + */ + fun transform(json: JSONObject): JSONObject + + override fun hook(type: JSONObject) = transform(type) +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/json/JsonHookPatch.kt b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/json/JsonHookPatch.kt new file mode 100644 index 000000000..4d82c8b4e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/json/JsonHookPatch.kt @@ -0,0 +1,30 @@ +package app.revanced.extension.twitter.patches.hook.json + +import app.revanced.extension.twitter.patches.hook.patch.dummy.DummyHook +import app.revanced.extension.twitter.utils.json.JsonUtils.parseJson +import app.revanced.extension.twitter.utils.stream.StreamUtils +import org.json.JSONException +import java.io.IOException +import java.io.InputStream + +object JsonHookPatch { + // Additional hooks added by corresponding patch. + private val hooks = buildList { + add(DummyHook) + } + + @JvmStatic + fun parseJsonHook(jsonInputStream: InputStream): InputStream { + var jsonObject = try { + parseJson(jsonInputStream) + } catch (ignored: IOException) { + return jsonInputStream // Unreachable. + } catch (ignored: JSONException) { + return jsonInputStream + } + + for (hook in hooks) jsonObject = hook.hook(jsonObject) + + return StreamUtils.fromString(jsonObject.toString()) + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/patch/Hook.kt b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/patch/Hook.kt new file mode 100644 index 000000000..3211e40e8 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/patch/Hook.kt @@ -0,0 +1,9 @@ +package app.revanced.extension.twitter.patches.hook.patch + +interface Hook { + /** + * Hook the given type. + * @param type The type to hook + */ + fun hook(type: T): T +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/patch/ads/HideAdsHook.kt b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/patch/ads/HideAdsHook.kt new file mode 100644 index 000000000..de2f7b2fa --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/patch/ads/HideAdsHook.kt @@ -0,0 +1,15 @@ +package app.revanced.extension.twitter.patches.hook.patch.ads + +import app.revanced.extension.twitter.patches.hook.json.BaseJsonHook +import app.revanced.extension.twitter.patches.hook.twifucker.TwiFucker +import org.json.JSONObject + +@Suppress("unused") +object HideAdsHook : BaseJsonHook() { + /** + * Strips JSONObject from promoted ads. + * + * @param json The JSONObject. + */ + override fun apply(json: JSONObject) = TwiFucker.hidePromotedAds(json) +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/patch/dummy/DummyHook.kt b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/patch/dummy/DummyHook.kt new file mode 100644 index 000000000..9dd620d91 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/patch/dummy/DummyHook.kt @@ -0,0 +1,14 @@ +package app.revanced.extension.twitter.patches.hook.patch.dummy + +import app.revanced.extension.twitter.patches.hook.json.BaseJsonHook +import app.revanced.extension.twitter.patches.hook.json.JsonHookPatch +import org.json.JSONObject + +/** + * Dummy hook to reserve a register in [JsonHookPatch.hooks] list. + */ +object DummyHook : BaseJsonHook() { + override fun apply(json: JSONObject) { + // Do nothing. + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/patch/recommendation/RecommendedUsersHook.kt b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/patch/recommendation/RecommendedUsersHook.kt new file mode 100644 index 000000000..161801dc2 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/patch/recommendation/RecommendedUsersHook.kt @@ -0,0 +1,14 @@ +package app.revanced.extension.twitter.patches.hook.patch.recommendation + +import app.revanced.extension.twitter.patches.hook.json.BaseJsonHook +import app.revanced.extension.twitter.patches.hook.twifucker.TwiFucker +import org.json.JSONObject + +object RecommendedUsersHook : BaseJsonHook() { + /** + * Strips JSONObject from recommended users. + * + * @param json The JSONObject. + */ + override fun apply(json: JSONObject) = TwiFucker.hideRecommendedUsers(json) +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/twifucker/TwiFucker.kt b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/twifucker/TwiFucker.kt new file mode 100644 index 000000000..af5b0312e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/twifucker/TwiFucker.kt @@ -0,0 +1,218 @@ +package app.revanced.extension.twitter.patches.hook.twifucker + +import android.util.Log +import app.revanced.extension.twitter.patches.hook.twifucker.TwiFuckerUtils.forEach +import app.revanced.extension.twitter.patches.hook.twifucker.TwiFuckerUtils.forEachIndexed +import org.json.JSONArray +import org.json.JSONObject + +// https://raw.githubusercontent.com/Dr-TSNG/TwiFucker/880cdf1c1622e54ab45561ffcb4f53d94ed97bae/app/src/main/java/icu/nullptr/twifucker/hook/JsonHook.kt +internal object TwiFucker { + // root + private fun JSONObject.jsonGetInstructions(): JSONArray? = optJSONObject("timeline")?.optJSONArray("instructions") + + private fun JSONObject.jsonGetData(): JSONObject? = optJSONObject("data") + + private fun JSONObject.jsonHasRecommendedUsers(): Boolean = has("recommended_users") + + private fun JSONObject.jsonRemoveRecommendedUsers() { + remove("recommended_users") + } + + private fun JSONObject.jsonCheckAndRemoveRecommendedUsers() { + if (jsonHasRecommendedUsers()) { + Log.d("ReVanced", "Handle recommended users: $this") + jsonRemoveRecommendedUsers() + } + } + + private fun JSONObject.jsonHasThreads(): Boolean = has("threads") + + private fun JSONObject.jsonRemoveThreads() { + remove("threads") + } + + private fun JSONObject.jsonCheckAndRemoveThreads() { + if (jsonHasThreads()) { + Log.d("ReVanced", "Handle threads: $this") + jsonRemoveThreads() + } + } + + // data + private fun JSONObject.dataGetInstructions(): JSONArray? { + val timeline = + optJSONObject("user_result")?.optJSONObject("result") + ?.optJSONObject("timeline_response")?.optJSONObject("timeline") + ?: optJSONObject("timeline_response")?.optJSONObject("timeline") + ?: optJSONObject("search")?.optJSONObject("timeline_response")?.optJSONObject("timeline") + ?: optJSONObject("timeline_response") + return timeline?.optJSONArray("instructions") + } + + private fun JSONObject.dataCheckAndRemove() { + dataGetInstructions()?.forEach { instruction -> + instruction.instructionCheckAndRemove { it.entriesRemoveAnnoyance() } + } + } + + private fun JSONObject.dataGetLegacy(): JSONObject? = + optJSONObject("tweet_result")?.optJSONObject("result")?.let { + if (it.has("tweet")) { + it.optJSONObject("tweet") + } else { + it + } + }?.optJSONObject("legacy") + + // entry + private fun JSONObject.entryHasPromotedMetadata(): Boolean = + optJSONObject("content")?.optJSONObject("item")?.optJSONObject("content") + ?.optJSONObject("tweet") + ?.has("promotedMetadata") == true || optJSONObject("content")?.optJSONObject("content") + ?.has("tweetPromotedMetadata") == true || optJSONObject("item")?.optJSONObject("content") + ?.has("tweetPromotedMetadata") == true + + private fun JSONObject.entryGetContentItems(): JSONArray? = + optJSONObject("content")?.optJSONArray("items") + ?: optJSONObject("content")?.optJSONObject("timelineModule")?.optJSONArray("items") + + private fun JSONObject.entryIsTweetDetailRelatedTweets(): Boolean = optString("entryId").startsWith("tweetdetailrelatedtweets-") + + private fun JSONObject.entryGetTrends(): JSONArray? = optJSONObject("content")?.optJSONObject("timelineModule")?.optJSONArray("items") + + // trend + private fun JSONObject.trendHasPromotedMetadata(): Boolean = + optJSONObject("item")?.optJSONObject("content")?.optJSONObject("trend") + ?.has("promotedMetadata") == true + + private fun JSONArray.trendRemoveAds() { + val trendRemoveIndex = mutableListOf() + forEachIndexed { trendIndex, trend -> + if (trend.trendHasPromotedMetadata()) { + Log.d("ReVanced", "Handle trends ads $trendIndex $trend") + trendRemoveIndex.add(trendIndex) + } + } + for (i in trendRemoveIndex.asReversed()) { + remove(i) + } + } + + // instruction + private fun JSONObject.instructionTimelineAddEntries(): JSONArray? = optJSONArray("entries") + + private fun JSONObject.instructionGetAddEntries(): JSONArray? = optJSONObject("addEntries")?.optJSONArray("entries") + + private fun JSONObject.instructionCheckAndRemove(action: (JSONArray) -> Unit) { + instructionTimelineAddEntries()?.let(action) + instructionGetAddEntries()?.let(action) + } + + // entries + private fun JSONArray.entriesRemoveTimelineAds() { + val removeIndex = mutableListOf() + forEachIndexed { entryIndex, entry -> + entry.entryGetTrends()?.trendRemoveAds() + + if (entry.entryHasPromotedMetadata()) { + Log.d("ReVanced", "Handle timeline ads $entryIndex $entry") + removeIndex.add(entryIndex) + } + + val innerRemoveIndex = mutableListOf() + val contentItems = entry.entryGetContentItems() + contentItems?.forEachIndexed inner@{ itemIndex, item -> + if (item.entryHasPromotedMetadata()) { + Log.d("ReVanced", "Handle timeline replies ads $entryIndex $entry") + if (contentItems.length() == 1) { + removeIndex.add(entryIndex) + } else { + innerRemoveIndex.add(itemIndex) + } + return@inner + } + } + for (i in innerRemoveIndex.asReversed()) { + contentItems?.remove(i) + } + } + for (i in removeIndex.reversed()) { + remove(i) + } + } + + private fun JSONArray.entriesRemoveTweetDetailRelatedTweets() { + val removeIndex = mutableListOf() + forEachIndexed { entryIndex, entry -> + + if (entry.entryIsTweetDetailRelatedTweets()) { + Log.d("ReVanced", "Handle tweet detail related tweets $entryIndex $entry") + removeIndex.add(entryIndex) + } + } + for (i in removeIndex.reversed()) { + remove(i) + } + } + + private fun JSONArray.entriesRemoveAnnoyance() { + entriesRemoveTimelineAds() + entriesRemoveTweetDetailRelatedTweets() + } + + private fun JSONObject.entryIsWhoToFollow(): Boolean = + optString("entryId").let { + it.startsWith("whoToFollow-") || it.startsWith("who-to-follow-") || it.startsWith("connect-module-") + } + + private fun JSONObject.itemContainsPromotedUser(): Boolean = + optJSONObject("item")?.optJSONObject("content") + ?.has("userPromotedMetadata") == true || optJSONObject("item")?.optJSONObject("content") + ?.optJSONObject("user") + ?.has("userPromotedMetadata") == true || optJSONObject("item")?.optJSONObject("content") + ?.optJSONObject("user")?.has("promotedMetadata") == true + + fun JSONArray.entriesRemoveWhoToFollow() { + val entryRemoveIndex = mutableListOf() + forEachIndexed { entryIndex, entry -> + if (!entry.entryIsWhoToFollow()) return@forEachIndexed + + Log.d("ReVanced", "Handle whoToFollow $entryIndex $entry") + entryRemoveIndex.add(entryIndex) + + val items = entry.entryGetContentItems() + val userRemoveIndex = mutableListOf() + items?.forEachIndexed { index, item -> + item.itemContainsPromotedUser().let { + if (it) { + Log.d("ReVanced", "Handle whoToFollow promoted user $index $item") + userRemoveIndex.add(index) + } + } + } + for (i in userRemoveIndex.reversed()) { + items?.remove(i) + } + } + for (i in entryRemoveIndex.reversed()) { + remove(i) + } + } + + fun hideRecommendedUsers(json: JSONObject) { + json.filterInstructions { it.entriesRemoveWhoToFollow() } + json.jsonCheckAndRemoveRecommendedUsers() + } + + fun hidePromotedAds(json: JSONObject) { + json.filterInstructions { it.entriesRemoveAnnoyance() } + json.jsonGetData()?.dataCheckAndRemove() + } + + private fun JSONObject.filterInstructions(action: (JSONArray) -> Unit) { + jsonGetInstructions()?.forEach { instruction -> + instruction.instructionCheckAndRemove(action) + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/twifucker/TwiFuckerUtils.kt b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/twifucker/TwiFuckerUtils.kt new file mode 100644 index 000000000..4872d95aa --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/hook/twifucker/TwiFuckerUtils.kt @@ -0,0 +1,22 @@ +package app.revanced.extension.twitter.patches.hook.twifucker + +import org.json.JSONArray +import org.json.JSONObject + +internal object TwiFuckerUtils { + inline fun JSONArray.forEach(action: (JSONObject) -> Unit) { + (0 until this.length()).forEach { i -> + if (this[i] is JSONObject) { + action(this[i] as JSONObject) + } + } + } + + inline fun JSONArray.forEachIndexed(action: (index: Int, JSONObject) -> Unit) { + (0 until this.length()).forEach { i -> + if (this[i] is JSONObject) { + action(i, this[i] as JSONObject) + } + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/links/ChangeLinkSharingDomainPatch.java b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/links/ChangeLinkSharingDomainPatch.java new file mode 100644 index 000000000..808a8de03 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/links/ChangeLinkSharingDomainPatch.java @@ -0,0 +1,16 @@ +package app.revanced.extension.twitter.patches.links; + +public final class ChangeLinkSharingDomainPatch { + private static final String DOMAIN_NAME = "https://fxtwitter.com"; + private static final String LINK_FORMAT = "%s/%s/status/%s"; + + public static String formatResourceLink(Object... formatArgs) { + String username = (String) formatArgs[0]; + String tweetId = (String) formatArgs[1]; + return String.format(LINK_FORMAT, DOMAIN_NAME, username, tweetId); + } + + public static String formatLink(long tweetId, String username) { + return String.format(LINK_FORMAT, DOMAIN_NAME, username, tweetId); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/links/OpenLinksWithAppChooserPatch.java b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/links/OpenLinksWithAppChooserPatch.java new file mode 100644 index 000000000..2b4bdc124 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitter/patches/links/OpenLinksWithAppChooserPatch.java @@ -0,0 +1,15 @@ +package app.revanced.extension.twitter.patches.links; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public final class OpenLinksWithAppChooserPatch { + public static void openWithChooser(final Context context, final Intent intent) { + Log.d("ReVanced", "Opening intent with chooser: " + intent); + + intent.setAction("android.intent.action.VIEW"); + + context.startActivity(Intent.createChooser(intent, null)); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitter/utils/json/JsonUtils.kt b/extensions/shared/src/main/java/app/revanced/extension/twitter/utils/json/JsonUtils.kt new file mode 100644 index 000000000..d046c6370 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitter/utils/json/JsonUtils.kt @@ -0,0 +1,13 @@ +package app.revanced.extension.twitter.utils.json + +import app.revanced.extension.twitter.utils.stream.StreamUtils +import org.json.JSONException +import org.json.JSONObject +import java.io.IOException +import java.io.InputStream + +object JsonUtils { + @JvmStatic + @Throws(IOException::class, JSONException::class) + fun parseJson(jsonInputStream: InputStream) = JSONObject(StreamUtils.toString(jsonInputStream)) +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/twitter/utils/stream/StreamUtils.kt b/extensions/shared/src/main/java/app/revanced/extension/twitter/utils/stream/StreamUtils.kt new file mode 100644 index 000000000..ff33c4409 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/twitter/utils/stream/StreamUtils.kt @@ -0,0 +1,24 @@ +package app.revanced.extension.twitter.utils.stream + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream + +object StreamUtils { + @Throws(IOException::class) + fun toString(inputStream: InputStream): String { + ByteArrayOutputStream().use { result -> + val buffer = ByteArray(1024) + var length: Int + while (inputStream.read(buffer).also { length = it } != -1) { + result.write(buffer, 0, length) + } + return result.toString() + } + } + + fun fromString(string: String): InputStream { + return ByteArrayInputStream(string.toByteArray()) + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/ByteTrieSearch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/ByteTrieSearch.java new file mode 100644 index 000000000..162e0b040 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/ByteTrieSearch.java @@ -0,0 +1,45 @@ +package app.revanced.extension.youtube; + +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/youtube/Event.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/Event.kt new file mode 100644 index 000000000..72323949c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/Event.kt @@ -0,0 +1,29 @@ +package app.revanced.extension.youtube + +/** + * 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) + } + + 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/youtube/StringTrieSearch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/StringTrieSearch.java new file mode 100644 index 000000000..fbff9beba --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/StringTrieSearch.java @@ -0,0 +1,34 @@ +package app.revanced.extension.youtube; + +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/youtube/ThemeHelper.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/ThemeHelper.java new file mode 100644 index 000000000..6e0ea5974 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/ThemeHelper.java @@ -0,0 +1,85 @@ +package app.revanced.extension.youtube; + +import android.app.Activity; +import android.graphics.Color; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; + +public class ThemeHelper { + @Nullable + private static Integer darkThemeColor, lightThemeColor; + private static int themeValue; + + /** + * Injection point. + */ + @SuppressWarnings("unused") + public static void setTheme(Enum value) { + final int newOrdinalValue = value.ordinal(); + if (themeValue != newOrdinalValue) { + themeValue = newOrdinalValue; + Logger.printDebug(() -> "Theme value: " + newOrdinalValue); + } + } + + public static boolean isDarkTheme() { + return themeValue == 1; + } + + public static void setActivityTheme(Activity activity) { + final var theme = isDarkTheme() + ? "Theme.YouTube.Settings.Dark" + : "Theme.YouTube.Settings"; + activity.setTheme(Utils.getResourceIdentifier(theme, "style")); + } + + /** + * Injection point. + */ + @SuppressWarnings("SameReturnValue") + private static String darkThemeResourceName() { + // Value is changed by Theme patch, if included. + return "@color/yt_black3"; + } + + /** + * @return The dark theme color as specified by the Theme patch (if included), + * or the dark mode background color unpatched YT uses. + */ + public static int getDarkThemeColor() { + if (darkThemeColor == null) { + darkThemeColor = getColorInt(darkThemeResourceName()); + } + return darkThemeColor; + } + + /** + * Injection point. + */ + @SuppressWarnings("SameReturnValue") + private static String lightThemeResourceName() { + // Value is changed by Theme patch, if included. + return "@color/yt_white1"; + } + + /** + * @return The light theme color as specified by the Theme patch (if included), + * or the non dark mode background color unpatched YT uses. + */ + public static int getLightThemeColor() { + if (lightThemeColor == null) { + lightThemeColor = getColorInt(lightThemeResourceName()); + } + return lightThemeColor; + } + + private static int getColorInt(String colorString) { + if (colorString.startsWith("#")) { + return Color.parseColor(colorString); + } + return Utils.getResourceColor(colorString); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/TrieSearch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/TrieSearch.java new file mode 100644 index 000000000..74fb4685d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/TrieSearch.java @@ -0,0 +1,412 @@ +package app.revanced.extension.youtube; + +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); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java new file mode 100644 index 000000000..92be08433 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/AlternativeThumbnailsPatch.java @@ -0,0 +1,710 @@ +package app.revanced.extension.youtube.patches; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.youtube.settings.Settings.*; +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 app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.NavigationBar; +import app.revanced.extension.youtube.shared.PlayerType; + +/** + * 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. + */ +@SuppressWarnings("unused") +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. + + /** + * 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("Invalid DeArrow API URL. Using default"); + 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 (PlayerType.getCurrent().isMaximizedOrFullscreen()) { + return ALT_THUMBNAIL_PLAYER.get(); + } + + // Must check second, as search can be from any tab. + if (NavigationBar.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 if not available NULL. + */ + @Nullable + 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 null; + } + + /** + * 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. + String fallbackUrl = null; + if (option.useStillImages) { + fallbackUrl = buildYouTubeVideoStillURL(decodedUrl, qualityToUse); + } + if (fallbackUrl == null) { + fallbackUrl = decodedUrl.sanitizedUrl; + } + + sanitizedReplacementUrl = buildDeArrowThumbnailURL(decodedUrl.videoId, fallbackUrl); + } else if (option.useStillImages) { + includeTracking = true; // Include view tracking parameters if present. + sanitizedReplacementUrl = buildYouTubeVideoStillURL(decodedUrl, qualityToUse); + if (sanitizedReplacementUrl == null) { + return originalUrl; // Still capture is not available. Return the untouched original url. + } + } 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: " + decodedUrl.sanitizedUrl); + + 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 through the extension 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 { + private static final String YOUTUBE_THUMBNAIL_DOMAIN = "https://i.ytimg.com/"; + + @Nullable + static DecodedThumbnailUrl decodeImageUrl(String url) { + final int urlPathStartIndex = url.indexOf('/', "https://".length()) + 1; + if (urlPathStartIndex <= 0) return null; + + final int urlPathEndIndex = url.indexOf('/', urlPathStartIndex); + if (urlPathEndIndex < 0) return null; + + final int videoIdStartIndex = url.indexOf('/', urlPathEndIndex) + 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, urlPathStartIndex, urlPathEndIndex, videoIdStartIndex, videoIdEndIndex, + imageSizeStartIndex, imageSizeEndIndex, imageExtensionEndIndex); + } + + final String originalFullUrl; + /** Full usable url, but stripped of any tracking information. */ + final String sanitizedUrl; + /** Url path, such as 'vi' or 'vi_webp' */ + final String urlPath; + 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 urlPathStartIndex, int urlPathEndIndex, int videoIdStartIndex, int videoIdEndIndex, + int imageSizeStartIndex, int imageSizeEndIndex, int imageExtensionEndIndex) { + originalFullUrl = fullUrl; + sanitizedUrl = fullUrl.substring(0, imageExtensionEndIndex); + urlPath = fullUrl.substring(urlPathStartIndex, urlPathEndIndex); + 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 network response has been observed, despite the alt webp image being a smaller file). + StringBuilder builder = new StringBuilder(originalFullUrl.length() + 2); + // Many different "i.ytimage.com" domains exist such as "i9.ytimg.com", + // but still captures are frequently not available on the other domains (especially newly uploaded videos). + // So always use the primary domain for a higher success rate. + builder.append(YOUTUBE_THUMBNAIL_DOMAIN).append(urlPath).append('/'); + 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/AutoRepeatPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/AutoRepeatPatch.java new file mode 100644 index 000000000..21409e739 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/AutoRepeatPatch.java @@ -0,0 +1,11 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class AutoRepeatPatch { + //Used by app.revanced.patches.youtube.layout.autorepeat.patch.AutoRepeatPatch + public static boolean shouldAutoRepeat() { + return Settings.AUTO_REPEAT.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/BackgroundPlaybackPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/BackgroundPlaybackPatch.java new file mode 100644 index 000000000..7e5f59a6e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/BackgroundPlaybackPatch.java @@ -0,0 +1,44 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; + +@SuppressWarnings("unused") +public class BackgroundPlaybackPatch { + + /** + * Injection point. + */ + public static boolean isBackgroundPlaybackAllowed(boolean original) { + if (original) return true; + + // Steps to verify most edge cases (with Shorts background playback set to off): + // 1. Open a regular video + // 2. Minimize app (PIP should appear) + // 3. Reopen app + // 4. Open a Short (without closing the regular video) + // (try opening both Shorts in the video player suggestions AND Shorts from the home feed) + // 5. Minimize the app (PIP should not appear) + // 6. Reopen app + // 7. Close the Short + // 8. Resume playing the regular video + // 9. Minimize the app (PIP should appear) + if (!VideoInformation.lastVideoIdIsShort()) { + return true; // Definitely is not a Short. + } + + // TODO: Add better hook. + // Might be a Shorts, or might be a prior regular video on screen again after a Shorts was closed. + // This incorrectly prevents PIP if player is in WATCH_WHILE_MINIMIZED after closing a Shorts, + // But there's no way around this unless an additional hook is added to definitively detect + // the Shorts player is on screen. This use case is unusual anyways so it's not a huge concern. + return !PlayerType.getCurrent().isNoneHiddenOrMinimized(); + } + + /** + * Injection point. + */ + public static boolean isBackgroundShortsPlaybackAllowed(boolean original) { + return !Settings.DISABLE_SHORTS_BACKGROUND_PLAYBACK.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/BypassImageRegionRestrictionsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/BypassImageRegionRestrictionsPatch.java new file mode 100644 index 000000000..ccc853d4c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/BypassImageRegionRestrictionsPatch.java @@ -0,0 +1,46 @@ +package app.revanced.extension.youtube.patches; + +import static app.revanced.extension.youtube.settings.Settings.BYPASS_IMAGE_REGION_RESTRICTIONS; + +import java.util.regex.Pattern; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class BypassImageRegionRestrictionsPatch { + + private static final boolean BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED = BYPASS_IMAGE_REGION_RESTRICTIONS.get(); + + private static final String REPLACEMENT_IMAGE_DOMAIN = "https://yt4.ggpht.com"; + + /** + * 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("^https://(yt3|lh[3-6]|play-lh)\\.(ggpht|googleusercontent)\\.com"); + + /** + * Injection point. Called off the main thread and by multiple threads at the same time. + * + * @param originalUrl Image url for all image urls loaded. + */ + public static String overrideImageURL(String originalUrl) { + try { + if (BYPASS_IMAGE_REGION_RESTRICTIONS_ENABLED) { + String replacement = YOUTUBE_STATIC_IMAGE_DOMAIN_PATTERN + .matcher(originalUrl).replaceFirst(REPLACEMENT_IMAGE_DOMAIN); + + if (Settings.DEBUG.get() && !replacement.equals(originalUrl)) { + Logger.printDebug(() -> "Replaced: '" + originalUrl + "' with: '" + replacement + "'"); + } + + return replacement; + } + } catch (Exception ex) { + Logger.printException(() -> "overrideImageURL failure", ex); + } + + return originalUrl; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/BypassURLRedirectsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/BypassURLRedirectsPatch.java new file mode 100644 index 000000000..ebce7cd35 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/BypassURLRedirectsPatch.java @@ -0,0 +1,31 @@ +package app.revanced.extension.youtube.patches; + +import android.net.Uri; + +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public class BypassURLRedirectsPatch { + private static final String YOUTUBE_REDIRECT_PATH = "/redirect"; + + /** + * Convert the YouTube redirect URI string to the redirect query URI. + * + * @param uri The YouTube redirect URI string. + * @return The redirect query URI. + */ + public static Uri parseRedirectUri(String uri) { + final var parsed = Uri.parse(uri); + + if (Settings.BYPASS_URL_REDIRECTS.get() && parsed.getPath().equals(YOUTUBE_REDIRECT_PATH)) { + var query = Uri.parse(Uri.decode(parsed.getQueryParameter("q"))); + + Logger.printDebug(() -> "Bypassing YouTube redirect URI: " + query); + + return query; + } + + return parsed; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ChangeStartPagePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ChangeStartPagePatch.java new file mode 100644 index 000000000..350e5787d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ChangeStartPagePatch.java @@ -0,0 +1,129 @@ +package app.revanced.extension.youtube.patches; + +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 app.revanced.extension.shared.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 TRUE.equals(isBrowseId); + } + + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + private boolean isIntentAction() { + return FALSE.equals(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(); + + /** + * 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 (appLaunched) { + Logger.printDebug(() -> "Ignore override browseId as the app already launched"); + return original; + } + appLaunched = true; + + Logger.printDebug(() -> "Changing browseId to " + START_PAGE.id); + return START_PAGE.id; + } + + public static void overrideIntentAction(@NonNull Intent intent) { + if (!START_PAGE.isIntentAction()) { + return; + } + + if (!ACTION_MAIN.equals(intent.getAction())) { + Logger.printDebug(() -> "Ignore override intent action" + + " as the current activity is not the entry point of the application"); + return; + } + + if (appLaunched) { + Logger.printDebug(() -> "Ignore override intent action as the app already launched"); + return; + } + appLaunched = true; + + final String intentAction = START_PAGE.id; + Logger.printDebug(() -> "Changing intent action to " + intentAction); + intent.setAction(intentAction); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java new file mode 100644 index 000000000..ccc2cc8c9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch.java @@ -0,0 +1,84 @@ +package app.revanced.extension.youtube.patches; + +import static app.revanced.extension.shared.StringRef.str; + +import android.app.Activity; +import android.text.Html; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class CheckWatchHistoryDomainNameResolutionPatch { + + private static final String HISTORY_TRACKING_ENDPOINT = "s.youtube.com"; + + private static final String SINKHOLE_IPV4 = "0.0.0.0"; + private static final String SINKHOLE_IPV6 = "::"; + + private static boolean domainResolvesToValidIP(String host) { + try { + InetAddress address = InetAddress.getByName(host); + String hostAddress = address.getHostAddress(); + + if (address.isLoopbackAddress()) { + Logger.printDebug(() -> host + " resolves to localhost"); + } else if (SINKHOLE_IPV4.equals(hostAddress) || SINKHOLE_IPV6.equals(hostAddress)) { + Logger.printDebug(() -> host + " resolves to sinkhole ip"); + } else { + return true; // Domain is not blocked. + } + } catch (UnknownHostException e) { + Logger.printDebug(() -> host + " failed to resolve"); + } + + return false; + } + + /** + * Injection point. + * + * Checks if s.youtube.com is blacklisted and playback history will fail to work. + */ + public static void checkDnsResolver(Activity context) { + if (!Utils.isNetworkConnected() || !Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.get()) return; + + Utils.runOnBackgroundThread(() -> { + try { + // If the user has a flaky DNS server, or they just lost internet connectivity + // and the isNetworkConnected() check has not detected it yet (it can take a few + // seconds after losing connection), then the history tracking endpoint will + // show a resolving error but it's actually an internet connection problem. + // + // Prevent this false positive by verify youtube.com resolves. + // If youtube.com does not resolve, then it's not a watch history domain resolving error + // because the entire app will not work since no domains are resolving. + if (domainResolvesToValidIP(HISTORY_TRACKING_ENDPOINT) + || !domainResolvesToValidIP("youtube.com")) { + return; + } + + Utils.runOnMainThread(() -> { + var alert = new android.app.AlertDialog.Builder(context) + .setTitle(str("revanced_check_watch_history_domain_name_dialog_title")) + .setMessage(Html.fromHtml(str("revanced_check_watch_history_domain_name_dialog_message"))) + .setIconAttribute(android.R.attr.alertDialogIcon) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + dialog.dismiss(); + }).setNegativeButton(str("revanced_check_watch_history_domain_name_dialog_ignore"), (dialog, which) -> { + Settings.CHECK_WATCH_HISTORY_DOMAIN_NAME.save(false); + dialog.dismiss(); + }).create(); + + Utils.showDialog(context, alert, false, null); + }); + } catch (Exception ex) { + Logger.printException(() -> "checkDnsResolver failure", ex); + } + }); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/CopyVideoUrlPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/CopyVideoUrlPatch.java new file mode 100644 index 000000000..db3338f52 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/CopyVideoUrlPatch.java @@ -0,0 +1,47 @@ +package app.revanced.extension.youtube.patches; + +import static app.revanced.extension.shared.StringRef.str; + +import android.os.Build; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; + +public class CopyVideoUrlPatch { + + public static void copyUrl(boolean withTimestamp) { + try { + StringBuilder builder = new StringBuilder("https://youtu.be/"); + builder.append(VideoInformation.getVideoId()); + final long currentVideoTimeInSeconds = VideoInformation.getVideoTime() / 1000; + if (withTimestamp && currentVideoTimeInSeconds > 0) { + final long hour = currentVideoTimeInSeconds / (60 * 60); + final long minute = (currentVideoTimeInSeconds / 60) % 60; + final long second = currentVideoTimeInSeconds % 60; + builder.append("?t="); + if (hour > 0) { + builder.append(hour).append("h"); + } + if (minute > 0) { + builder.append(minute).append("m"); + } + if (second > 0) { + builder.append(second).append("s"); + } + } + + Utils.setClipboard(builder.toString()); + // 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 (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2 || (withTimestamp && currentVideoTimeInSeconds > 0)) { + Utils.showToastShort(withTimestamp && currentVideoTimeInSeconds > 0 + ? str("revanced_share_copy_url_timestamp_success") + : str("revanced_share_copy_url_success")); + } + } catch (Exception e) { + Logger.printException(() -> "Failed to generate video url", e); + } + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/CustomPlayerOverlayOpacityPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/CustomPlayerOverlayOpacityPatch.java new file mode 100644 index 000000000..4f13deaac --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/CustomPlayerOverlayOpacityPatch.java @@ -0,0 +1,33 @@ +package app.revanced.extension.youtube.patches; + +import static app.revanced.extension.shared.StringRef.str; + +import android.widget.ImageView; + +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class CustomPlayerOverlayOpacityPatch { + + private static final int PLAYER_OVERLAY_OPACITY_LEVEL; + + static { + int opacity = Settings.PLAYER_OVERLAY_OPACITY.get(); + + if (opacity < 0 || opacity > 100) { + Utils.showToastLong(str("revanced_player_overlay_opacity_invalid_toast")); + Settings.PLAYER_OVERLAY_OPACITY.resetToDefault(); + opacity = Settings.PLAYER_OVERLAY_OPACITY.defaultValue; + } + + PLAYER_OVERLAY_OPACITY_LEVEL = (opacity * 255) / 100; + } + + /** + * Injection point. + */ + public static void changeOpacity(ImageView imageView) { + imageView.setImageAlpha(PLAYER_OVERLAY_OPACITY_LEVEL); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableAutoCaptionsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableAutoCaptionsPatch.java new file mode 100644 index 000000000..9d43159a5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableAutoCaptionsPatch.java @@ -0,0 +1,20 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; + +@SuppressWarnings("unused") +public class DisableAutoCaptionsPatch { + + /** + * Used by injected code. Do not delete. + */ + public static boolean captionsButtonDisabled; + + public static boolean autoCaptionsEnabled() { + return Settings.AUTO_CAPTIONS.get() + // Do not use auto captions for Shorts. + && !PlayerType.getCurrent().isNoneHiddenOrSlidingMinimized(); + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableFullscreenAmbientModePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableFullscreenAmbientModePatch.java new file mode 100644 index 000000000..356634294 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableFullscreenAmbientModePatch.java @@ -0,0 +1,25 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +/** @noinspection unused*/ +public final class DisableFullscreenAmbientModePatch { + + private static final boolean DISABLE_FULLSCREEN_AMBIENT_MODE = Settings.DISABLE_FULLSCREEN_AMBIENT_MODE.get(); + + /** + * Constant found in: androidx.window.embedding.DividerAttributes + */ + private static final int DIVIDER_ATTRIBUTES_COLOR_SYSTEM_DEFAULT = -16777216; + + /** + * Injection point. + */ + public static int getFullScreenBackgroundColor(int originalColor) { + if (DISABLE_FULLSCREEN_AMBIENT_MODE) { + return DIVIDER_ATTRIBUTES_COLOR_SYSTEM_DEFAULT; + } + + return originalColor; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisablePlayerPopupPanelsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisablePlayerPopupPanelsPatch.java new file mode 100644 index 000000000..dd2e0ca8f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisablePlayerPopupPanelsPatch.java @@ -0,0 +1,11 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class DisablePlayerPopupPanelsPatch { + //Used by app.revanced.patches.youtube.layout.playerpopuppanels.patch.PlayerPopupPanelsPatch + public static boolean disablePlayerPopupPanels() { + return Settings.PLAYER_POPUP_PANELS.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisablePreciseSeekingGesturePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisablePreciseSeekingGesturePatch.java new file mode 100644 index 000000000..c6a80c6a0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisablePreciseSeekingGesturePatch.java @@ -0,0 +1,10 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class DisablePreciseSeekingGesturePatch { + public static boolean isGestureDisabled() { + return Settings.DISABLE_PRECISE_SEEKING_GESTURE.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableResumingStartupShortsPlayerPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableResumingStartupShortsPlayerPatch.java new file mode 100644 index 000000000..938ac5458 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableResumingStartupShortsPlayerPatch.java @@ -0,0 +1,14 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +/** @noinspection unused*/ +public class DisableResumingStartupShortsPlayerPatch { + + /** + * Injection point. + */ + public static boolean disableResumingStartupShortsPlayer() { + return Settings.DISABLE_RESUMING_SHORTS_PLAYER.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableRollingNumberAnimationsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableRollingNumberAnimationsPatch.java new file mode 100644 index 000000000..f600f391a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableRollingNumberAnimationsPatch.java @@ -0,0 +1,13 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class DisableRollingNumberAnimationsPatch { + /** + * Injection point. + */ + public static boolean disableRollingNumberAnimations() { + return Settings.DISABLE_ROLLING_NUMBER_ANIMATIONS.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableSuggestedVideoEndScreenPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableSuggestedVideoEndScreenPatch.java new file mode 100644 index 000000000..7cd91d9c2 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DisableSuggestedVideoEndScreenPatch.java @@ -0,0 +1,24 @@ +package app.revanced.extension.youtube.patches; + +import android.annotation.SuppressLint; +import android.widget.ImageView; + +import app.revanced.extension.youtube.settings.Settings; + +/** @noinspection unused*/ +public final class DisableSuggestedVideoEndScreenPatch { + @SuppressLint("StaticFieldLeak") + private static ImageView lastView; + + public static void closeEndScreen(final ImageView imageView) { + if (!Settings.DISABLE_SUGGESTED_VIDEO_END_SCREEN.get()) return; + + // Prevent adding the listener multiple times. + if (lastView == imageView) return; + lastView = imageView; + + imageView.getViewTreeObserver().addOnGlobalLayoutListener(() -> { + if (imageView.isShown()) imageView.callOnClick(); + }); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DownloadsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DownloadsPatch.java new file mode 100644 index 000000000..6da31b6a4 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/DownloadsPatch.java @@ -0,0 +1,103 @@ +package app.revanced.extension.youtube.patches; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; + +import androidx.annotation.NonNull; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.StringRef; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class DownloadsPatch { + + private static WeakReference activityRef = new WeakReference<>(null); + + /** + * Injection point. + */ + public static void activityCreated(Activity mainActivity) { + activityRef = new WeakReference<>(mainActivity); + } + + /** + * 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 inAppDownloadButtonOnClick(@NonNull String videoId) { + try { + if (!Settings.EXTERNAL_DOWNLOADER_ACTION_BUTTON.get()) { + return false; + } + + // If possible, use the main activity as the context. + // Otherwise fall back on using the application context. + Context context = activityRef.get(); + boolean isActivityContext = true; + if (context == null) { + // Utils context is the application context, and not an activity context. + context = Utils.getContext(); + isActivityContext = false; + } + + launchExternalDownloader(videoId, context, isActivityContext); + return true; + } catch (Exception ex) { + Logger.printException(() -> "inAppDownloadButtonOnClick failure", ex); + } + return false; + } + + /** + * @param isActivityContext If the context parameter is for an Activity. If this is false, then + * the downloader is opened as a new task (which forces YT to minimize). + */ + public static void launchExternalDownloader(@NonNull String videoId, + @NonNull Context context, boolean isActivityContext) { + try { + Objects.requireNonNull(videoId); + Logger.printDebug(() -> "Launching external downloader with context: " + context); + + // Trim string to avoid any accidental whitespace. + var downloaderPackageName = Settings.EXTERNAL_DOWNLOADER_PACKAGE_NAME.get().trim(); + + boolean packageEnabled = false; + try { + packageEnabled = context.getPackageManager().getApplicationInfo(downloaderPackageName, 0).enabled; + } catch (PackageManager.NameNotFoundException error) { + Logger.printDebug(() -> "External downloader could not be found: " + error); + } + + // If the package is not installed, show the toast + if (!packageEnabled) { + Utils.showToastLong(StringRef.str("revanced_external_downloader_not_installed_warning", downloaderPackageName)); + return; + } + + String content = "https://youtu.be/" + videoId; + Intent intent = new Intent("android.intent.action.SEND"); + intent.setType("text/plain"); + intent.setPackage(downloaderPackageName); + intent.putExtra("android.intent.extra.TEXT", content); + if (!isActivityContext) { + Logger.printDebug(() -> "Using new task intent"); + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + } + context.startActivity(intent); + } catch (Exception ex) { + Logger.printException(() -> "launchExternalDownloader failure", ex); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/EnableDebuggingPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/EnableDebuggingPatch.java new file mode 100644 index 000000000..60ffa53ed --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/EnableDebuggingPatch.java @@ -0,0 +1,56 @@ +package app.revanced.extension.youtube.patches; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.settings.BaseSettings; + +@SuppressWarnings("unused") +public final class EnableDebuggingPatch { + + private static final ConcurrentMap featureFlags + = new ConcurrentHashMap<>(300, 0.75f, 1); + + /** + * Injection point. + */ + public static boolean isBooleanFeatureFlagEnabled(boolean value, long flag) { + if (value && BaseSettings.DEBUG.get()) { + if (featureFlags.putIfAbsent(flag, true) == null) { + Logger.printDebug(() -> "boolean feature is enabled: " + flag); + } + } + + return value; + } + + /** + * Injection point. + */ + public static double isDoubleFeatureFlagEnabled(double value, long flag, double defaultValue) { + if (defaultValue != value && BaseSettings.DEBUG.get()) { + if (featureFlags.putIfAbsent(flag, true) == null) { + // Align the log outputs to make post processing easier. + Logger.printDebug(() -> " double feature is enabled: " + flag + + " value: " + value + (defaultValue == 0 ? "" : " default: " + defaultValue)); + } + } + + return value; + } + + /** + * Injection point. + */ + public static long isLongFeatureFlagEnabled(long value, long flag, long defaultValue) { + if (defaultValue != value && BaseSettings.DEBUG.get()) { + if (featureFlags.putIfAbsent(flag, true) == null) { + Logger.printDebug(() -> " long feature is enabled: " + flag + + " value: " + value + (defaultValue == 0 ? "" : " default: " + defaultValue)); + } + } + + return value; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/FixBackToExitGesturePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/FixBackToExitGesturePatch.java new file mode 100644 index 000000000..8fe4dbb18 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/FixBackToExitGesturePatch.java @@ -0,0 +1,44 @@ +package app.revanced.extension.youtube.patches; + +import android.app.Activity; + +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public class FixBackToExitGesturePatch { + /** + * State whether the scroll position reaches the top. + */ + public static boolean isTopView = false; + + /** + * Handle the event after clicking the back button. + * + * @param activity The activity, the app is launched with to finish. + */ + public static void onBackPressed(Activity activity) { + if (!isTopView) return; + + Logger.printDebug(() -> "Activity is closed"); + + activity.finish(); + } + + /** + * Handle the event when the homepage list of views is being scrolled. + */ + public static void onScrollingViews() { + Logger.printDebug(() -> "Views are scrolling"); + + isTopView = false; + } + + /** + * Handle the event when the homepage list of views reached the top. + */ + public static void onTopView() { + Logger.printDebug(() -> "Scrolling reached the top"); + + isTopView = true; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/FullscreenPanelsRemoverPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/FullscreenPanelsRemoverPatch.java new file mode 100644 index 000000000..f454b361c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/FullscreenPanelsRemoverPatch.java @@ -0,0 +1,12 @@ +package app.revanced.extension.youtube.patches; + +import android.view.View; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class FullscreenPanelsRemoverPatch { + public static int getFullscreenPanelsVisibility() { + return Settings.HIDE_FULLSCREEN_PANELS.get() ? View.GONE : View.VISIBLE; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideEndscreenCardsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideEndscreenCardsPatch.java new file mode 100644 index 000000000..89261d119 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideEndscreenCardsPatch.java @@ -0,0 +1,14 @@ +package app.revanced.extension.youtube.patches; + +import android.view.View; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class HideEndscreenCardsPatch { + //Used by app.revanced.patches.youtube.layout.hideendscreencards.bytecode.patch.HideEndscreenCardsPatch + public static void hideEndscreen(View view) { + if (!Settings.HIDE_ENDSCREEN_CARDS.get()) return; + view.setVisibility(View.GONE); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideGetPremiumPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideGetPremiumPatch.java new file mode 100644 index 000000000..35592c0ff --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideGetPremiumPatch.java @@ -0,0 +1,13 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class HideGetPremiumPatch { + /** + * Injection point. + */ + public static boolean hideGetPremiumView() { + return Settings.HIDE_GET_PREMIUM.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideInfoCardsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideInfoCardsPatch.java new file mode 100644 index 000000000..e01c4a394 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideInfoCardsPatch.java @@ -0,0 +1,17 @@ +package app.revanced.extension.youtube.patches; + +import android.view.View; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class HideInfoCardsPatch { + public static void hideInfoCardsIncognito(View view) { + if (!Settings.HIDE_INFO_CARDS.get()) return; + view.setVisibility(View.GONE); + } + + public static boolean hideInfoCardsMethodCall() { + return Settings.HIDE_INFO_CARDS.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HidePlayerOverlayButtonsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HidePlayerOverlayButtonsPatch.java new file mode 100644 index 000000000..c1a0065db --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HidePlayerOverlayButtonsPatch.java @@ -0,0 +1,72 @@ +package app.revanced.extension.youtube.patches; + +import android.view.View; +import android.widget.ImageView; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class HidePlayerOverlayButtonsPatch { + + private static final boolean HIDE_AUTOPLAY_BUTTON_ENABLED = Settings.HIDE_AUTOPLAY_BUTTON.get(); + + /** + * Injection point. + */ + public static boolean hideAutoPlayButton() { + return HIDE_AUTOPLAY_BUTTON_ENABLED; + } + + /** + * Injection point. + */ + public static int getCastButtonOverrideV2(int original) { + return Settings.HIDE_CAST_BUTTON.get() ? View.GONE : original; + } + + /** + * Injection point. + */ + public static void hideCaptionsButton(ImageView imageView) { + imageView.setVisibility(Settings.HIDE_CAPTIONS_BUTTON.get() ? ImageView.GONE : ImageView.VISIBLE); + } + + private static final boolean HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS_ENABLED + = Settings.HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS.get(); + + private static final int PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID = + Utils.getResourceIdentifier("player_control_previous_button_touch_area", "id"); + + private static final int PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID = + Utils.getResourceIdentifier("player_control_next_button_touch_area", "id"); + + /** + * Injection point. + */ + public static void hidePreviousNextButtons(View parentView) { + if (!HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS_ENABLED) { + return; + } + + // Must use a deferred call to main thread to hide the button. + // Otherwise the layout crashes if set to hidden now. + Utils.runOnMainThread(() -> { + hideView(parentView, PLAYER_CONTROL_PREVIOUS_BUTTON_TOUCH_AREA_ID); + hideView(parentView, PLAYER_CONTROL_NEXT_BUTTON_TOUCH_AREA_ID); + }); + } + + private static void hideView(View parentView, int resourceId) { + View nextPreviousButton = parentView.findViewById(resourceId); + + if (nextPreviousButton == null) { + Logger.printException(() -> "Could not find player previous/next button"); + return; + } + + Logger.printDebug(() -> "Hiding previous/next button"); + Utils.hideViewByRemovingFromParentUnderCondition(true, nextPreviousButton); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideSeekbarPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideSeekbarPatch.java new file mode 100644 index 000000000..98065d7ec --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideSeekbarPatch.java @@ -0,0 +1,10 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class HideSeekbarPatch { + public static boolean hideSeekbar() { + return Settings.HIDE_SEEKBAR.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideTimestampPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideTimestampPatch.java new file mode 100644 index 000000000..a502bb690 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/HideTimestampPatch.java @@ -0,0 +1,10 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class HideTimestampPatch { + public static boolean hideTimestamp() { + return Settings.HIDE_TIMESTAMP.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java new file mode 100644 index 000000000..33bdf26b2 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/MiniplayerPatch.java @@ -0,0 +1,322 @@ +package app.revanced.extension.youtube.patches; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.*; +import static app.revanced.extension.youtube.patches.VersionCheckPatch.*; + +import android.util.DisplayMetrics; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings({"unused", "SpellCheckingInspection"}) +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), + /** + * Half broken miniplayer, that might be work in progress or left over abandoned code. + * Can force this type by editing the import/export settings. + */ + MODERN_4(null, 4); + + /** + * Legacy tablet hook value. + */ + @Nullable + final Boolean legacyTabletOverride; + + /** + * Modern player type used by YT. + */ + @Nullable + final Integer modernPlayerType; + + MiniplayerType(@Nullable Boolean legacyTabletOverride, @Nullable Integer modernPlayerType) { + this.legacyTabletOverride = legacyTabletOverride; + this.modernPlayerType = modernPlayerType; + } + + public boolean isModern() { + return modernPlayerType != null; + } + } + + private static final int MINIPLAYER_SIZE; + + static { + // YT appears to use the device screen dip width, plus an unknown fixed horizontal padding size. + DisplayMetrics displayMetrics = Utils.getContext().getResources().getDisplayMetrics(); + final int deviceDipWidth = (int) (displayMetrics.widthPixels / displayMetrics.density); + + // YT seems to use a minimum height to calculate the minimum miniplayer width based on the video. + // 170 seems to be the smallest that can be used and using less makes no difference. + final int WIDTH_DIP_MIN = 170; // Seems to be the smallest that works. + final int HORIZONTAL_PADDING_DIP = 15; // Estimated padding. + // Round down to the nearest 5 pixels, to keep any error toasts easier to read. + final int WIDTH_DIP_MAX = 5 * ((deviceDipWidth - HORIZONTAL_PADDING_DIP) / 5); + Logger.printDebug(() -> "Screen dip width: " + deviceDipWidth + " maxWidth: " + WIDTH_DIP_MAX); + + int dipWidth = Settings.MINIPLAYER_WIDTH_DIP.get(); + + if (dipWidth < WIDTH_DIP_MIN || dipWidth > WIDTH_DIP_MAX) { + Utils.showToastLong(str("revanced_miniplayer_width_dip_invalid_toast", + WIDTH_DIP_MIN, WIDTH_DIP_MAX)); + + // Instead of resetting, clamp the size at the bounds. + dipWidth = Math.max(WIDTH_DIP_MIN, Math.min(dipWidth, WIDTH_DIP_MAX)); + Settings.MINIPLAYER_WIDTH_DIP.save(dipWidth); + } + + MINIPLAYER_SIZE = dipWidth; + } + + /** + * 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 + = Utils.getResourceIdentifier("modern_miniplayer_subtitle_text", "id"); + + private static final MiniplayerType CURRENT_TYPE = Settings.MINIPLAYER_TYPE.get(); + + /** + * Cannot turn off double tap with modern 2 or 3 with later targets, + * as forcing it off breakings tapping the miniplayer. + */ + private static final boolean DOUBLE_TAP_ACTION_ENABLED = + // 19.29+ is very broken if double tap is not enabled. + IS_19_29_OR_GREATER || + (CURRENT_TYPE.isModern() && Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get()); + + private static final boolean DRAG_AND_DROP_ENABLED = + CURRENT_TYPE.isModern() && Settings.MINIPLAYER_DRAG_AND_DROP.get(); + + private static final boolean HIDE_EXPAND_CLOSE_ENABLED = + Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.get() + && Settings.MINIPLAYER_HIDE_EXPAND_CLOSE.isAvailable(); + + private static final boolean HIDE_SUBTEXT_ENABLED = + (CURRENT_TYPE == MODERN_1 || CURRENT_TYPE == MODERN_3) && Settings.MINIPLAYER_HIDE_SUBTEXT.get(); + + private static final boolean HIDE_REWIND_FORWARD_ENABLED = + CURRENT_TYPE == MODERN_1 && Settings.MINIPLAYER_HIDE_REWIND_FORWARD.get(); + + private static final boolean MINIPLAYER_ROUNDED_CORNERS_ENABLED = + Settings.MINIPLAYER_ROUNDED_CORNERS.get(); + + private static final boolean MINIPLAYER_HORIZONTAL_DRAG_ENABLED = + DRAG_AND_DROP_ENABLED && Settings.MINIPLAYER_HORIZONTAL_DRAG.get(); + + /** + * Remove a broken and always present subtitle text that is only + * present with {@link MiniplayerType#MODERN_2}. Bug was fixed in 19.21. + */ + private static final boolean HIDE_BROKEN_MODERN_2_SUBTITLE = + CURRENT_TYPE == MODERN_2 && !IS_19_21_OR_GREATER; + + private static final int OPACITY_LEVEL; + + public static final class MiniplayerHorizontalDragAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return Settings.MINIPLAYER_TYPE.get().isModern() && Settings.MINIPLAYER_DRAG_AND_DROP.get(); + } + } + + public static final class MiniplayerHideExpandCloseAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + MiniplayerType type = Settings.MINIPLAYER_TYPE.get(); + return (!IS_19_20_OR_GREATER && (type == MODERN_1 || type == MODERN_3)) + || (!IS_19_26_OR_GREATER && type == MODERN_1 + && !Settings.MINIPLAYER_DOUBLE_TAP_ACTION.get() && !Settings.MINIPLAYER_DRAG_AND_DROP.get()) + || (IS_19_29_OR_GREATER && type == MODERN_3); + } + } + + static { + int opacity = Settings.MINIPLAYER_OPACITY.get(); + + if (opacity < 0 || opacity > 100) { + Utils.showToastLong(str("revanced_miniplayer_opacity_invalid_toast")); + Settings.MINIPLAYER_OPACITY.resetToDefault(); + opacity = Settings.MINIPLAYER_OPACITY.defaultValue; + } + + 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 getModernFeatureFlagsActiveOverride(boolean original) { + if (CURRENT_TYPE == ORIGINAL) { + return original; + } + + return CURRENT_TYPE.isModern(); + } + + /** + * Injection point. + */ + public static boolean enableMiniplayerDoubleTapAction(boolean original) { + if (CURRENT_TYPE == ORIGINAL) { + return original; + } + + return DOUBLE_TAP_ACTION_ENABLED; + } + + /** + * Injection point. + */ + public static boolean enableMiniplayerDragAndDrop(boolean original) { + if (CURRENT_TYPE == ORIGINAL) { + return original; + } + + return DRAG_AND_DROP_ENABLED; + } + + + /** + * Injection point. + */ + public static boolean setRoundedCorners(boolean original) { + if (CURRENT_TYPE.isModern()) { + return MINIPLAYER_ROUNDED_CORNERS_ENABLED; + } + + return original; + } + + /** + * Injection point. + */ + public static int setMiniplayerDefaultSize(int original) { + if (CURRENT_TYPE.isModern()) { + return MINIPLAYER_SIZE; + } + + return original; + } + + + /** + * Injection point. + */ + public static boolean setHorizontalDrag(boolean original) { + if (CURRENT_TYPE.isModern()) { + return MINIPLAYER_HORIZONTAL_DRAG_ENABLED; + } + + return original; + } + + /** + * Injection point. + */ + public static void hideMiniplayerExpandClose(ImageView view) { + Utils.hideViewByRemovingFromParentUnderCondition(HIDE_EXPAND_CLOSE_ENABLED, view); + } + + /** + * Injection point. + */ + public static void hideMiniplayerRewindForward(ImageView view) { + Utils.hideViewByRemovingFromParentUnderCondition(HIDE_REWIND_FORWARD_ENABLED, view); + } + + /** + * Injection point. + */ + public static void hideMiniplayerSubTexts(View view) { + try { + // Different subviews are passed in, but only TextView is of interest here. + if (HIDE_SUBTEXT_ENABLED && view instanceof TextView) { + Logger.printDebug(() -> "Hiding subtext view"); + Utils.hideViewByRemovingFromParentUnderCondition(true, view); + } + } catch (Exception ex) { + Logger.printException(() -> "hideMiniplayerSubTexts failure", ex); + } + } + + /** + * Injection point. + */ + public static void playerOverlayGroupCreated(View group) { + try { + if (HIDE_BROKEN_MODERN_2_SUBTITLE && MODERN_OVERLAY_SUBTITLE_TEXT != 0) { + if (group instanceof ViewGroup) { + View subtitleText = Utils.getChildView((ViewGroup) group, true, + view -> view.getId() == MODERN_OVERLAY_SUBTITLE_TEXT); + + if (subtitleText != null) { + subtitleText.setVisibility(View.GONE); + Logger.printDebug(() -> "Modern overlay subtitle view set to hidden"); + } + } + } + } catch (Exception ex) { + Logger.printException(() -> "playerOverlayGroupCreated failure", ex); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/NavigationButtonsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/NavigationButtonsPatch.java new file mode 100644 index 000000000..8c581fc1c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/NavigationButtonsPatch.java @@ -0,0 +1,51 @@ +package app.revanced.extension.youtube.patches; + +import static app.revanced.extension.shared.Utils.hideViewUnderCondition; +import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; + +import android.view.View; + +import java.util.EnumMap; +import java.util.Map; + +import android.widget.TextView; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class NavigationButtonsPatch { + + private static final Map shouldHideMap = new EnumMap<>(NavigationButton.class) { + { + put(NavigationButton.HOME, Settings.HIDE_HOME_BUTTON.get()); + put(NavigationButton.CREATE, Settings.HIDE_CREATE_BUTTON.get()); + put(NavigationButton.SHORTS, Settings.HIDE_SHORTS_BUTTON.get()); + put(NavigationButton.SUBSCRIPTIONS, Settings.HIDE_SUBSCRIPTIONS_BUTTON.get()); + } + }; + + private static final boolean SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON + = Settings.SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON.get(); + + /** + * Injection point. + */ + public static boolean switchCreateWithNotificationButton() { + return SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON; + } + + /** + * Injection point. + */ + public static void navigationTabCreated(NavigationButton button, View tabView) { + if (Boolean.TRUE.equals(shouldHideMap.get(button))) { + tabView.setVisibility(View.GONE); + } + } + + /** + * Injection point. + */ + public static void hideNavigationButtonLabels(TextView navigationLabelsView) { + hideViewUnderCondition(Settings.HIDE_NAVIGATION_BUTTON_LABELS, navigationLabelsView); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/OpenLinksExternallyPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/OpenLinksExternallyPatch.java new file mode 100644 index 000000000..1f5d52c5b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/OpenLinksExternallyPatch.java @@ -0,0 +1,18 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class OpenLinksExternallyPatch { + /** + * Return the intent to open links with. If empty, the link will be opened with the default browser. + * + * @param originalIntent The original intent to open links with. + * @return The intent to open links with. Empty means the link will be opened with the default browser. + */ + public static String getIntent(String originalIntent) { + if (Settings.EXTERNAL_BROWSER.get()) return ""; + + return originalIntent; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/PlayerControlsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/PlayerControlsPatch.java new file mode 100644 index 000000000..5dc3dde26 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/PlayerControlsPatch.java @@ -0,0 +1,44 @@ +package app.revanced.extension.youtube.patches; + +import android.view.View; +import android.view.ViewTreeObserver; +import android.widget.ImageView; + +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public class PlayerControlsPatch { + /** + * Injection point. + */ + public static void setFullscreenCloseButton(ImageView imageButton) { + // Add a global listener, since the protected method + // View#onVisibilityChanged() does not have any call backs. + imageButton.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { + int lastVisibility = View.VISIBLE; + + @Override + public void onGlobalLayout() { + try { + final int visibility = imageButton.getVisibility(); + if (lastVisibility != visibility) { + lastVisibility = visibility; + + Logger.printDebug(() -> "fullscreen button visibility: " + + (visibility == View.VISIBLE ? "VISIBLE" : + visibility == View.GONE ? "GONE" : "INVISIBLE")); + + fullscreenButtonVisibilityChanged(visibility == View.VISIBLE); + } + } catch (Exception ex) { + Logger.printDebug(() -> "OnGlobalLayoutListener failure", ex); + } + } + }); + } + + // noinspection EmptyMethod + public static void fullscreenButtonVisibilityChanged(boolean isVisible) { + // Code added during patching. + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/PlayerOverlaysHookPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/PlayerOverlaysHookPatch.java new file mode 100644 index 000000000..cf44c9dd5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/PlayerOverlaysHookPatch.java @@ -0,0 +1,15 @@ +package app.revanced.extension.youtube.patches; + +import android.view.ViewGroup; + +import app.revanced.extension.youtube.shared.PlayerOverlays; + +@SuppressWarnings("unused") +public class PlayerOverlaysHookPatch { + /** + * Injection point. + */ + public static void playerOverlayInflated(ViewGroup group) { + PlayerOverlays.attach(group); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/PlayerTypeHookPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/PlayerTypeHookPatch.java new file mode 100644 index 000000000..3f1591950 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/PlayerTypeHookPatch.java @@ -0,0 +1,27 @@ +package app.revanced.extension.youtube.patches; + +import androidx.annotation.Nullable; + +import app.revanced.extension.youtube.shared.PlayerType; +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()); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/RemoveTrackingQueryParameterPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/RemoveTrackingQueryParameterPatch.java new file mode 100644 index 000000000..3b05a239a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/RemoveTrackingQueryParameterPatch.java @@ -0,0 +1,17 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class RemoveTrackingQueryParameterPatch { + private static final String NEW_TRACKING_PARAMETER_REGEX = ".si=.+"; + private static final String OLD_TRACKING_PARAMETER_REGEX = ".feature=.+"; + + public static String sanitize(String url) { + if (!Settings.REMOVE_TRACKING_QUERY_PARAMETER.get()) return url; + + return url + .replaceAll(NEW_TRACKING_PARAMETER_REGEX, "") + .replaceAll(OLD_TRACKING_PARAMETER_REGEX, ""); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/RemoveViewerDiscretionDialogPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/RemoveViewerDiscretionDialogPatch.java new file mode 100644 index 000000000..0260b2c69 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/RemoveViewerDiscretionDialogPatch.java @@ -0,0 +1,19 @@ +package app.revanced.extension.youtube.patches; + +import android.app.AlertDialog; +import app.revanced.extension.youtube.settings.Settings; + +/** @noinspection unused*/ +public class RemoveViewerDiscretionDialogPatch { + public static void confirmDialog(AlertDialog dialog) { + if (!Settings.REMOVE_VIEWER_DISCRETION_DIALOG.get()) { + // Since the patch replaces the AlertDialog#show() method, we need to call the original method here. + dialog.show(); + return; + } + + final var button = dialog.getButton(AlertDialog.BUTTON_POSITIVE); + button.setSoundEffectsEnabled(false); + button.performClick(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java new file mode 100644 index 000000000..770316d81 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch.java @@ -0,0 +1,644 @@ +package app.revanced.extension.youtube.patches; + +import static app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike.Vote; + +import android.graphics.Rect; +import android.graphics.drawable.ShapeDrawable; +import android.os.Build; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.Spanned; +import android.view.View; +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.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.patches.components.ReturnYouTubeDislikeFilterPatch; +import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch; +import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike; +import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; + +/** + * 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 = + SpoofAppVersionPatch.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(boolean rydEnabled) { + ReturnYouTubeDislikeApi.resetRateLimits(); + // Must remove all values to protect against using stale data + // if the user enables RYD while a video is on screen. + clearData(); + } + + private static void clearData() { + currentVideoData = null; + lastLithoShortsVideoData = null; + 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); + } + + /** + * 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; + } + + 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; + } + // 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())) { + // 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() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // YouTube requires Android N or greater + 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); + + 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) { + if (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/SeekbarTappingPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/SeekbarTappingPatch.java new file mode 100644 index 000000000..dbbb363e1 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/SeekbarTappingPatch.java @@ -0,0 +1,10 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class SeekbarTappingPatch { + public static boolean seekbarTappingEnabled() { + return Settings.SEEKBAR_TAPPING.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/SeekbarThumbnailsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/SeekbarThumbnailsPatch.java new file mode 100644 index 000000000..e9a469026 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/SeekbarThumbnailsPatch.java @@ -0,0 +1,32 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class SeekbarThumbnailsPatch { + + public static final class SeekbarThumbnailsHighQualityAvailability implements Setting.Availability { + @Override + public boolean isAvailable() { + return VersionCheckPatch.IS_19_17_OR_GREATER || !Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get(); + } + } + + private static final boolean SEEKBAR_THUMBNAILS_HIGH_QUALITY_ENABLED + = Settings.SEEKBAR_THUMBNAILS_HIGH_QUALITY.get(); + + /** + * Injection point. + */ + public static boolean useHighQualityFullscreenThumbnails() { + return SEEKBAR_THUMBNAILS_HIGH_QUALITY_ENABLED; + } + + /** + * Injection point. + */ + public static boolean useFullscreenSeekbarThumbnails() { + return !Settings.RESTORE_OLD_SEEKBAR_THUMBNAILS.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ShortsAutoplayPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ShortsAutoplayPatch.java new file mode 100644 index 000000000..32576479d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ShortsAutoplayPatch.java @@ -0,0 +1,119 @@ +package app.revanced.extension.youtube.patches; + +import android.app.Activity; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class ShortsAutoplayPatch { + + private enum ShortsLoopBehavior { + UNKNOWN, + /** + * Repeat the same Short forever! + */ + REPEAT, + /** + * Play once, then advanced to the next Short. + */ + SINGLE_PLAY, + /** + * Pause playback after 1 play. + */ + END_SCREEN; + + static void setYTEnumValue(Enum ytBehavior) { + for (ShortsLoopBehavior rvBehavior : values()) { + if (ytBehavior.name().endsWith(rvBehavior.name())) { + rvBehavior.ytEnumValue = ytBehavior; + + Logger.printDebug(() -> rvBehavior + " set to YT enum: " + ytBehavior.name()); + return; + } + } + + Logger.printException(() -> "Unknown Shorts loop behavior: " + ytBehavior.name()); + } + + /** + * YouTube enum value of the obfuscated enum type. + */ + private Enum ytEnumValue; + } + + private static WeakReference mainActivityRef = new WeakReference<>(null); + + + public static void setMainActivity(Activity activity) { + mainActivityRef = new WeakReference<>(activity); + } + + /** + * @return If the app is currently in background PiP mode. + */ + @RequiresApi(api = Build.VERSION_CODES.N) + private static boolean isAppInBackgroundPiPMode() { + Activity activity = mainActivityRef.get(); + return activity != null && activity.isInPictureInPictureMode(); + } + + /** + * Injection point. + */ + public static void setYTShortsRepeatEnum(Enum ytEnum) { + try { + for (Enum ytBehavior : Objects.requireNonNull(ytEnum.getClass().getEnumConstants())) { + ShortsLoopBehavior.setYTEnumValue(ytBehavior); + } + } catch (Exception ex) { + Logger.printException(() -> "setYTShortsRepeatEnum failure", ex); + } + } + + /** + * Injection point. + */ + @RequiresApi(api = Build.VERSION_CODES.N) + public static Enum changeShortsRepeatBehavior(Enum original) { + try { + final boolean autoplay; + + if (isAppInBackgroundPiPMode()) { + if (!VersionCheckPatch.IS_19_34_OR_GREATER) { + // 19.34+ is required to set background play behavior. + Logger.printDebug(() -> "PiP Shorts not supported, using original repeat behavior"); + + return original; + } + + autoplay = Settings.SHORTS_AUTOPLAY_BACKGROUND.get(); + } else { + autoplay = Settings.SHORTS_AUTOPLAY.get(); + } + + final ShortsLoopBehavior behavior = autoplay + ? ShortsLoopBehavior.SINGLE_PLAY + : ShortsLoopBehavior.REPEAT; + + if (behavior.ytEnumValue != null) { + Logger.printDebug(() -> behavior.ytEnumValue == original + ? "Changing Shorts repeat behavior from: " + original.name() + " to: " + behavior.ytEnumValue + : "Behavior setting is same as original. Using original: " + original.name() + ); + + return behavior.ytEnumValue; + } + } catch (Exception ex) { + Logger.printException(() -> "changeShortsRepeatState failure", ex); + } + + return original; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/SlideToSeekPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/SlideToSeekPatch.java new file mode 100644 index 000000000..5ed4a8185 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/SlideToSeekPatch.java @@ -0,0 +1,14 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class SlideToSeekPatch { + private static final boolean SLIDE_TO_SEEK_DISABLED = !Settings.SLIDE_TO_SEEK.get(); + + public static boolean isSlideToSeekDisabled(boolean isDisabled) { + if (!isDisabled) return false; + + return SLIDE_TO_SEEK_DISABLED; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/TabletLayoutPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/TabletLayoutPatch.java new file mode 100644 index 000000000..f2ae03598 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/TabletLayoutPatch.java @@ -0,0 +1,16 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class TabletLayoutPatch { + + private static final boolean TABLET_LAYOUT_ENABLED = Settings.TABLET_LAYOUT.get(); + + /** + * Injection point. + */ + public static boolean getTabletLayoutEnabled() { + return TABLET_LAYOUT_ENABLED; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/VersionCheckPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/VersionCheckPatch.java new file mode 100644 index 000000000..74f082bf4 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/VersionCheckPatch.java @@ -0,0 +1,12 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.shared.Utils; + +public class VersionCheckPatch { + public static final boolean IS_19_17_OR_GREATER = Utils.getAppVersionName().compareTo("19.17.00") >= 0; + public static final boolean IS_19_20_OR_GREATER = Utils.getAppVersionName().compareTo("19.20.00") >= 0; + public static final boolean IS_19_21_OR_GREATER = Utils.getAppVersionName().compareTo("19.21.00") >= 0; + public static final boolean IS_19_26_OR_GREATER = Utils.getAppVersionName().compareTo("19.26.00") >= 0; + public static final boolean IS_19_29_OR_GREATER = Utils.getAppVersionName().compareTo("19.29.00") >= 0; + public static final boolean IS_19_34_OR_GREATER = Utils.getAppVersionName().compareTo("19.34.00") >= 0; +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/VideoAdsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/VideoAdsPatch.java new file mode 100644 index 000000000..9950de5e0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/VideoAdsPatch.java @@ -0,0 +1,14 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class VideoAdsPatch { + + // Used by app.revanced.patches.youtube.ad.general.video.patch.VideoAdsPatch + // depends on Whitelist patch (still needs to be written) + public static boolean shouldShowAds() { + return !Settings.HIDE_VIDEO_ADS.get(); // TODO && Whitelist.shouldShowAds(); + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/VideoInformation.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/VideoInformation.java new file mode 100644 index 000000000..6b64ade12 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/VideoInformation.java @@ -0,0 +1,359 @@ +package app.revanced.extension.youtube.patches; + +import androidx.annotation.NonNull; +import app.revanced.extension.youtube.patches.playback.speed.RememberPlaybackSpeedPatch; +import app.revanced.extension.youtube.shared.VideoState; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +/** + * Hooking class for the current playing video. + * @noinspection unused + */ +public final class VideoInformation { + + public interface PlaybackController { + // Methods are added to YT classes during patching. + boolean seekTo(long videoTime); + void seekToRelative(long videoTimeOffset); + } + + private static final float DEFAULT_YOUTUBE_PLAYBACK_SPEED = 1.0f; + /** + * Prefix present in all Short player parameters signature. + */ + private static final String SHORTS_PLAYER_PARAMETERS = "8AEB"; + + private static WeakReference playerControllerRef = new WeakReference<>(null); + private static WeakReference mdxPlayerDirectorRef = new WeakReference<>(null); + + @NonNull + private static String videoId = ""; + private static long videoLength = 0; + private static long videoTime = -1; + + @NonNull + private static volatile String playerResponseVideoId = ""; + private static volatile boolean playerResponseVideoIdIsShort; + private static volatile boolean videoIdIsShort; + + /** + * The current playback speed + */ + private static float playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED; + + /** + * Injection point. + * + * @param playerController player controller object. + */ + public static void initialize(@NonNull PlaybackController playerController) { + try { + playerControllerRef = new WeakReference<>(Objects.requireNonNull(playerController)); + videoTime = -1; + videoLength = 0; + playbackSpeed = DEFAULT_YOUTUBE_PLAYBACK_SPEED; + } catch (Exception ex) { + Logger.printException(() -> "Failed to initialize", ex); + } + } + + /** + * Injection point. + * + * @param mdxPlayerDirector MDX player director object (casting mode). + */ + public static void initializeMdx(@NonNull PlaybackController mdxPlayerDirector) { + try { + mdxPlayerDirectorRef = new WeakReference<>(Objects.requireNonNull(mdxPlayerDirector)); + } catch (Exception ex) { + Logger.printException(() -> "Failed to initialize MDX", ex); + } + } + + /** + * Injection point. + * + * @param newlyLoadedVideoId id of the current video + */ + public static void setVideoId(@NonNull String newlyLoadedVideoId) { + if (!videoId.equals(newlyLoadedVideoId)) { + Logger.printDebug(() -> "New video id: " + newlyLoadedVideoId); + videoId = newlyLoadedVideoId; + } + } + + /** + * @return If the player parameters are for a Short. + */ + public static boolean playerParametersAreShort(@NonNull String parameters) { + return parameters.startsWith(SHORTS_PLAYER_PARAMETERS); + } + + /** + * Injection point. + */ + public static String newPlayerResponseSignature(@NonNull String signature, String videoId, boolean isShortAndOpeningOrPlaying) { + final boolean isShort = playerParametersAreShort(signature); + playerResponseVideoIdIsShort = isShort; + if (!isShort || isShortAndOpeningOrPlaying) { + if (videoIdIsShort != isShort) { + videoIdIsShort = isShort; + Logger.printDebug(() -> "videoIdIsShort: " + isShort); + } + } + return signature; // 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)) { + Logger.printDebug(() -> "New player response video id: " + videoId); + playerResponseVideoId = videoId; + } + } + + /** + * Injection point. + * Called when user selects a playback speed. + * + * @param userSelectedPlaybackSpeed The playback speed the user selected + */ + public static void userSelectedPlaybackSpeed(float userSelectedPlaybackSpeed) { + Logger.printDebug(() -> "User selected playback speed: " + userSelectedPlaybackSpeed); + playbackSpeed = userSelectedPlaybackSpeed; + } + + /** + * Overrides the current playback speed. + *

+ * Used exclusively by {@link RememberPlaybackSpeedPatch} + */ + public static void overridePlaybackSpeed(float speedOverride) { + if (playbackSpeed != speedOverride) { + Logger.printDebug(() -> "Overriding playback speed to: " + speedOverride); + playbackSpeed = speedOverride; + } + } + + /** + * Injection point. + * + * @param length The length of the video in milliseconds. + */ + public static void setVideoLength(final long length) { + if (videoLength != length) { + Logger.printDebug(() -> "Current video length: " + length); + videoLength = length; + } + } + + /** + * 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; + } + + /** + * 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) { + Utils.verifyOnMainThread(); + try { + final long videoTime = getVideoTime(); + final long videoLength = getVideoLength(); + + // Prevent issues such as play/ pause button or autoplay not working. + final long adjustedSeekTime = Math.min(seekTime, videoLength - 250); + if (videoTime <= seekTime && videoTime >= adjustedSeekTime) { + // Both the current video time and the seekTo are in the last 250ms of the video. + // Ignore this seek call, otherwise if a video ends with multiple closely timed segments + // then seeking here can create an infinite loop of skip attempts. + Logger.printDebug(() -> "Ignoring seekTo call as video playback is almost finished. " + + " videoTime: " + videoTime + " videoLength: " + videoLength + " seekTo: " + seekTime); + return false; + } + + Logger.printDebug(() -> "Seeking to: " + adjustedSeekTime); + + // Try regular playback controller first, and it will not succeed if casting. + PlaybackController controller = playerControllerRef.get(); + if (controller == null) { + Logger.printDebug(() -> "Cannot seekTo because player controller is null"); + } else { + if (controller.seekTo(adjustedSeekTime)) return true; + Logger.printDebug(() -> "seekTo did not succeeded. Trying MXD."); + // Else the video is loading or changing videos, or video is casting to a different device. + } + + // Try calling the seekTo method of the MDX player director (called when casting). + // The difference has to be a different second mark in order to avoid infinite skip loops + // as the Lounge API only supports seconds. + if (adjustedSeekTime / 1000 == videoTime / 1000) { + Logger.printDebug(() -> "Skipping seekTo for MDX because seek time is too small " + + "(" + (adjustedSeekTime - videoTime) + "ms)"); + return false; + } + + controller = mdxPlayerDirectorRef.get(); + if (controller == null) { + Logger.printDebug(() -> "Cannot seekTo MXD because player controller is null"); + return false; + } + + return controller.seekTo(adjustedSeekTime); + } catch (Exception ex) { + Logger.printException(() -> "Failed to seek", ex); + return false; + } + } + + /** + * Seeks a relative amount. Should always be used over {@link #seekTo(long)} + * when the desired seek time is an offset of the current time. + */ + public static void seekToRelative(long seekTime) { + Utils.verifyOnMainThread(); + try { + Logger.printDebug(() -> "Seeking relative to: " + seekTime); + + // 19.39+ does not have a boolean return type for relative seek. + // But can call both methods and it works correctly for both situations. + PlaybackController controller = playerControllerRef.get(); + if (controller == null) { + Logger.printDebug(() -> "Cannot seek relative as player controller is null"); + } else { + controller.seekToRelative(seekTime); + } + + // 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; + if (seekTime < 0) { + adjustedSeekTime = Math.min(seekTime, -1000); + } else { + adjustedSeekTime = Math.max(seekTime, 1000); + } + + controller = mdxPlayerDirectorRef.get(); + if (controller == null) { + Logger.printDebug(() -> "Cannot seek relative as MXD player controller is null"); + } else { + controller.seekToRelative(adjustedSeekTime); + } + } catch (Exception ex) { + Logger.printException(() -> "Failed to seek relative", 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; + } + + /** + * 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 The current playback speed. + */ + public static float getPlaybackSpeed() { + return playbackSpeed; + } + + /** + * 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; + } + + /** + * @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 + */ + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public static boolean isAtEndOfVideo() { + return videoTime >= videoLength && videoLength > 0; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java new file mode 100644 index 000000000..57ec7442f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/WideSearchbarPatch.java @@ -0,0 +1,11 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class WideSearchbarPatch { + + public static boolean enableWideSearchbar(boolean original) { + return Settings.WIDE_SEARCHBAR.get() || original; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ZoomHapticsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ZoomHapticsPatch.java new file mode 100644 index 000000000..0367eb868 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/ZoomHapticsPatch.java @@ -0,0 +1,10 @@ +package app.revanced.extension.youtube.patches; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class ZoomHapticsPatch { + public static boolean shouldVibrate() { + return !Settings.DISABLE_ZOOM_HAPTICS.get(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java new file mode 100644 index 000000000..10309da96 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/announcements/AnnouncementsPatch.java @@ -0,0 +1,172 @@ +package app.revanced.extension.youtube.patches.announcements; + +import static android.text.Html.FROM_HTML_MODE_COMPACT; +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENTS; +import static app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes.GET_LATEST_ANNOUNCEMENT_IDS; + +import android.app.Activity; +import android.app.AlertDialog; +import android.os.Build; +import android.text.Html; +import android.text.method.LinkMovementMethod; +import android.widget.TextView; + +import androidx.annotation.RequiresApi; + +import org.json.JSONArray; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.time.LocalDateTime; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.patches.announcements.requests.AnnouncementsRoutes; +import app.revanced.extension.youtube.requests.Requester; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class AnnouncementsPatch { + private AnnouncementsPatch() { + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private static boolean isLatestAlready() throws IOException { + HttpURLConnection connection = + AnnouncementsRoutes.getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENT_IDS); + + Logger.printDebug(() -> "Get latest announcement IDs route connection url: " + connection.getURL()); + + try { + // Do not show the announcement if the request failed. + if (connection.getResponseCode() != 200) { + if (Settings.ANNOUNCEMENT_LAST_ID.isSetToDefault()) + return true; + + Settings.ANNOUNCEMENT_LAST_ID.resetToDefault(); + Utils.showToastLong(str("revanced_announcements_connection_failed")); + + return true; + } + } catch (IOException ex) { + Logger.printException(() -> "Could not connect to announcements provider", ex); + return true; + } + + var jsonString = Requester.parseStringAndDisconnect(connection); + + // Parse the ID. Fall-back to raw string if it fails. + int id = Settings.ANNOUNCEMENT_LAST_ID.defaultValue; + try { + final var announcementIds = new JSONArray(jsonString); + id = announcementIds.getJSONObject(0).getInt("id"); + + } catch (Throwable ex) { + Logger.printException(() -> "Failed to parse announcement IDs", ex); + } + + // Do not show the announcement, if the last announcement id is the same as the current one. + return Settings.ANNOUNCEMENT_LAST_ID.get() == id; + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public static void showAnnouncement(final Activity context) { + if (!Settings.ANNOUNCEMENTS.get()) return; + + // Check if there is internet connection + if (!Utils.isNetworkConnected()) return; + + Utils.runOnBackgroundThread(() -> { + try { + if (isLatestAlready()) return; + + HttpURLConnection connection = AnnouncementsRoutes + .getAnnouncementsConnectionFromRoute(GET_LATEST_ANNOUNCEMENTS); + + Logger.printDebug(() -> "Get latest announcements route connection url: " + connection.getURL()); + + var jsonString = Requester.parseStringAndDisconnect(connection); + + // Parse the announcement. Fall-back to raw string if it fails. + int id = Settings.ANNOUNCEMENT_LAST_ID.defaultValue; + String title; + String message; + LocalDateTime archivedAt = LocalDateTime.MAX; + Level level = Level.INFO; + try { + final var announcement = new JSONArray(jsonString).getJSONObject(0); + + id = announcement.getInt("id"); + title = announcement.getString("title"); + message = announcement.getString("content"); + if (!announcement.isNull("archived_at")) { + archivedAt = LocalDateTime.parse(announcement.getString("archived_at")); + } + if (!announcement.isNull("level")) { + level = Level.fromInt(announcement.getInt("level")); + } + } catch (Throwable ex) { + Logger.printException(() -> "Failed to parse announcement. Fall-backing to raw string", ex); + + title = "Announcement"; + message = jsonString; + } + + // If the announcement is archived, do not show it. + if (archivedAt.isBefore(LocalDateTime.now())) { + Settings.ANNOUNCEMENT_LAST_ID.save(id); + return; + } + + int finalId = id; + final var finalTitle = title; + final var finalMessage = Html.fromHtml(message, FROM_HTML_MODE_COMPACT); + final Level finalLevel = level; + + Utils.runOnMainThread(() -> { + // Show the announcement. + var alert = new AlertDialog.Builder(context) + .setTitle(finalTitle) + .setMessage(finalMessage) + .setIcon(finalLevel.icon) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + Settings.ANNOUNCEMENT_LAST_ID.save(finalId); + dialog.dismiss(); + }).setNegativeButton(str("revanced_announcements_dialog_dismiss"), (dialog, which) -> { + dialog.dismiss(); + }) + .setCancelable(false) + .create(); + + Utils.showDialog(context, alert, false, (AlertDialog dialog) -> { + // Make links clickable. + ((TextView) dialog.findViewById(android.R.id.message)) + .setMovementMethod(LinkMovementMethod.getInstance()); + }); + }); + } catch (Exception e) { + final var message = "Failed to get announcement"; + + Logger.printException(() -> message, e); + } + }); + } + + // TODO: Use better icons. + private enum Level { + INFO(android.R.drawable.ic_dialog_info), + WARNING(android.R.drawable.ic_dialog_alert), + SEVERE(android.R.drawable.ic_dialog_alert); + + public final int icon; + + Level(int icon) { + this.icon = icon; + } + + public static Level fromInt(int value) { + return values()[Math.min(value, values().length - 1)]; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/announcements/requests/AnnouncementsRoutes.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/announcements/requests/AnnouncementsRoutes.java new file mode 100644 index 000000000..ea54e1bd6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/announcements/requests/AnnouncementsRoutes.java @@ -0,0 +1,22 @@ +package app.revanced.extension.youtube.patches.announcements.requests; + +import app.revanced.extension.youtube.requests.Requester; +import app.revanced.extension.youtube.requests.Route; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import static app.revanced.extension.youtube.requests.Route.Method.GET; + +public class AnnouncementsRoutes { + private static final String ANNOUNCEMENTS_PROVIDER = "https://api.revanced.app/v4"; + public static final Route GET_LATEST_ANNOUNCEMENT_IDS = new Route(GET, "/announcements/latest/id?tag=youtube"); + public static final Route GET_LATEST_ANNOUNCEMENTS = new Route(GET, "/announcements/latest?tag=youtube"); + + private AnnouncementsRoutes() { + } + + public static HttpURLConnection getAnnouncementsConnectionFromRoute(Route route, String... params) throws IOException { + return Requester.getConnectionFromRoute(ANNOUNCEMENTS_PROVIDER, route, params); + } +} \ No newline at end of file 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..8b2c5465b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/AdsFilter.java @@ -0,0 +1,242 @@ +package app.revanced.extension.youtube.patches.components; + +import static app.revanced.extension.shared.StringRef.str; + +import android.app.Instrumentation; +import android.view.KeyEvent; +import android.view.View; + +import androidx.annotation.Nullable; + +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.StringTrieSearch; + +@SuppressWarnings("unused") +public final class AdsFilter extends Filter { + // region Fullscreen ad + private static volatile long lastTimeClosedFullscreenAd; + private static final Instrumentation instrumentation = new Instrumentation(); + private final StringFilterGroup fullscreenAd; + + // endregion + + private final StringTrieSearch exceptions = new StringTrieSearch(); + + private final StringFilterGroup playerShoppingShelf; + private final ByteArrayFilterGroup playerShoppingShelfBuffer; + + private final StringFilterGroup channelProfile; + private final ByteArrayFilterGroup visitStoreButton; + + private final StringFilterGroup shoppingLinks; + + public AdsFilter() { + exceptions.addPatterns( + "home_video_with_context", // Don't filter anything in the home page video component. + "related_video_with_context", // Don't filter anything in the related video component. + "comment_thread", // Don't filter anything in the comments. + "|comment.", // Don't filter anything in the comments replies. + "library_recent_shelf" + ); + + // Identifiers. + + + final var carouselAd = new StringFilterGroup( + Settings.HIDE_GENERAL_ADS, + "carousel_ad" + ); + addIdentifierCallbacks(carouselAd); + + // Paths. + + fullscreenAd = new StringFilterGroup( + Settings.HIDE_FULLSCREEN_ADS, + "_interstitial" + ); + + final var buttonedAd = new StringFilterGroup( + Settings.HIDE_BUTTONED_ADS, + "_ad_with", + "_buttoned_layout", + // text_image_button_group_layout, landscape_image_button_group_layout, full_width_square_image_button_group_layout + "image_button_group_layout", + "full_width_square_image_layout", + "video_display_button_group_layout", + "landscape_image_wide_button_layout", + "video_display_carousel_button_group_layout" + ); + + final var generalAds = new StringFilterGroup( + Settings.HIDE_GENERAL_ADS, + "ads_video_with_context", + "banner_text_icon", + "square_image_layout", + "watch_metadata_app_promo", + "video_display_full_layout", + "hero_promo_image", + "statement_banner", + "carousel_footered_layout", + "text_image_button_layout", + "primetime_promo", + "product_details", + "composite_concurrent_carousel_layout", + "carousel_headered_layout", + "full_width_portrait_image_layout", + "brand_video_shelf" + ); + + final var movieAds = new StringFilterGroup( + Settings.HIDE_MOVIES_SECTION, + "browsy_bar", + "compact_movie", + "horizontal_movie_shelf", + "movie_and_show_upsell_card", + "compact_tvfilm_item", + "offer_module_root" + ); + + final var viewProducts = new StringFilterGroup( + Settings.HIDE_PRODUCTS_BANNER, + "product_item", + "products_in_video", + "shopping_overlay.eml", // Video player overlay shopping links. + "shopping_carousel.eml" // Channel profile shopping shelf. + ); + + shoppingLinks = new StringFilterGroup( + Settings.HIDE_SHOPPING_LINKS, + "expandable_list" + ); + + channelProfile = new StringFilterGroup( + null, + "channel_profile.eml" + ); + + playerShoppingShelf = new StringFilterGroup( + null, + "horizontal_shelf.eml" + ); + + playerShoppingShelfBuffer = new ByteArrayFilterGroup( + Settings.HIDE_PLAYER_STORE_SHELF, + "shopping_item_card_list.eml" + ); + + visitStoreButton = new ByteArrayFilterGroup( + Settings.HIDE_VISIT_STORE_BUTTON, + "header_store_button" + ); + + final var webLinkPanel = new StringFilterGroup( + Settings.HIDE_WEB_SEARCH_RESULTS, + "web_link_panel" + ); + + final var merchandise = new StringFilterGroup( + Settings.HIDE_MERCHANDISE_BANNERS, + "product_carousel" + ); + + final var selfSponsor = new StringFilterGroup( + Settings.HIDE_SELF_SPONSOR, + "cta_shelf_card" + ); + + addPathCallbacks( + generalAds, + buttonedAd, + merchandise, + viewProducts, + selfSponsor, + fullscreenAd, + channelProfile, + webLinkPanel, + shoppingLinks, + playerShoppingShelf, + movieAds + ); + } + + @Override + boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (matchedGroup == playerShoppingShelf) { + if (contentIndex == 0 && playerShoppingShelfBuffer.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } + + if (exceptions.matches(path)) + return false; + + if (matchedGroup == fullscreenAd) { + if (path.contains("|ImageType|")) closeFullscreenAd(); + + return false; // Do not actually filter the fullscreen ad otherwise it will leave a dimmed screen. + } + + if (matchedGroup == channelProfile) { + if (visitStoreButton.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + return false; + } + + // Check for the index because of likelihood of false positives. + if (matchedGroup == shoppingLinks && contentIndex != 0) + return false; + + return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + + /** + * Hide the view, which shows ads in the homepage. + * + * @param view The view, which shows ads. + */ + public static void hideAdAttributionView(View view) { + Utils.hideViewBy0dpUnderCondition(Settings.HIDE_GENERAL_ADS, view); + } + + /** + * Close the fullscreen ad. + *

+ * The strategy is to send a back button event to the app to close the fullscreen ad using the back button event. + */ + private static void closeFullscreenAd() { + final var currentTime = System.currentTimeMillis(); + + // Prevent spamming the back button. + if (currentTime - lastTimeClosedFullscreenAd < 10000) return; + lastTimeClosedFullscreenAd = currentTime; + + Logger.printDebug(() -> "Closing fullscreen ad"); + + Utils.runOnMainThreadDelayed(() -> { + // Must run off main thread (Odd, but whatever). + Utils.runOnBackgroundThread(() -> { + try { + instrumentation.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK); + } catch (Exception ex) { + // Injecting user events on Android 10+ requires the manifest to include + // INJECT_EVENTS, and it's usage is heavily restricted + // and requires the user to manually approve the permission in the device settings. + // + // And no matter what, permissions cannot be added for root installations + // as manifest changes are ignored for mount installations. + // + // Instead, catch the SecurityException and turn off hide full screen ads + // since this functionality does not work for these devices. + Logger.printInfo(() -> "Could not inject back button event", ex); + Settings.HIDE_FULLSCREEN_ADS.save(false); + Utils.showToastLong(str("revanced_hide_fullscreen_ads_feature_not_available_toast")); + } + }); + }, 1000); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ButtonsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ButtonsFilter.java new file mode 100644 index 000000000..35337bee0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ButtonsFilter.java @@ -0,0 +1,103 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +final class ButtonsFilter extends Filter { + private static final String VIDEO_ACTION_BAR_PATH = "video_action_bar.eml"; + + private final StringFilterGroup actionBarGroup; + private final StringFilterGroup bufferFilterPathGroup; + private final ByteArrayFilterGroupList bufferButtonsGroupList = new ByteArrayFilterGroupList(); + + public ButtonsFilter() { + actionBarGroup = new StringFilterGroup( + null, + VIDEO_ACTION_BAR_PATH + ); + addIdentifierCallbacks(actionBarGroup); + + + bufferFilterPathGroup = new StringFilterGroup( + null, + "|ContainerType|button.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_PLAYLIST_BUTTON, + "|save_to_playlist_button" + ), + new StringFilterGroup( + Settings.HIDE_CLIP_BUTTON, + "|clip_button.eml|" + ), + bufferFilterPathGroup + ); + + 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" + ), + // Check for clip button both here and using a path filter, + // as there's a chance the path is a generic action button and won't contain 'clip_button' + new ByteArrayFilterGroup( + Settings.HIDE_CLIP_BUTTON, + "yt_outline_scissors" + ), + new ByteArrayFilterGroup( + Settings.HIDE_THANKS_BUTTON, + "yt_outline_dollar_sign_heart" + ) + ); + } + + private boolean isEveryFilterGroupEnabled() { + for (var group : pathCallbacks) + if (!group.isEnabled()) return false; + + for (var group : bufferButtonsGroupList) + if (!group.isEnabled()) return false; + + return true; + } + + @Override + boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + // If the current matched group is the action bar group, + // in case every filter group is enabled, hide the action bar. + if (matchedGroup == actionBarGroup) { + if (!isEveryFilterGroupEnabled()) { + return false; + } + } else if (matchedGroup == bufferFilterPathGroup) { + // Make sure the current path is the right one + // to avoid false positives. + if (!path.startsWith(VIDEO_ACTION_BAR_PATH)) return false; + + // In case the group list has no match, return false. + if (!bufferButtonsGroupList.check(protobufBufferArray).isFiltered()) return false; + } + + return super.isFiltered(identifier, path, 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..0e7ebc440 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CommentsFilter.java @@ -0,0 +1,83 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +final class CommentsFilter extends Filter { + + private static final String TIMESTAMP_OR_EMOJI_BUTTONS_ENDS_WITH_PATH + = "|CellType|ContainerType|ContainerType|ContainerType|ContainerType|ContainerType|"; + + private final StringFilterGroup commentComposer; + private final ByteArrayFilterGroup emojiPickerBufferGroup; + + public CommentsFilter() { + var commentsByMembers = new StringFilterGroup( + Settings.HIDE_COMMENTS_BY_MEMBERS_HEADER, + "sponsorships_comments_header.eml", + "sponsorships_comments_footer.eml" + ); + + var comments = new StringFilterGroup( + Settings.HIDE_COMMENTS_SECTION, + "video_metadata_carousel", + "_comments" + ); + + var createAShort = new StringFilterGroup( + Settings.HIDE_COMMENTS_CREATE_A_SHORT_BUTTON, + "composer_short_creation_button.eml" + ); + + var previewComment = new StringFilterGroup( + Settings.HIDE_COMMENTS_PREVIEW_COMMENT, + "|carousel_item", + "comments_entry_point_teaser", + "comments_entry_point_simplebox" + ); + + var thanksButton = new StringFilterGroup( + Settings.HIDE_COMMENTS_THANKS_BUTTON, + "super_thanks_button.eml" + ); + + commentComposer = new StringFilterGroup( + Settings.HIDE_COMMENTS_TIMESTAMP_AND_EMOJI_BUTTONS, + "comment_composer.eml" + ); + + emojiPickerBufferGroup = new ByteArrayFilterGroup( + null, + "id.comment.quick_emoji.button" + ); + + addPathCallbacks( + commentsByMembers, + comments, + createAShort, + previewComment, + thanksButton, + commentComposer + ); + } + + @Override + boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (matchedGroup == commentComposer) { + // To completely hide the emoji buttons (and leave no empty space), the timestamp button is + // also hidden because the buffer is exactly the same and there's no way selectively hide. + if (contentIndex == 0 + && path.endsWith(TIMESTAMP_OR_EMOJI_BUTTONS_ENDS_WITH_PATH) + && emojiPickerBufferGroup.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + + return false; + } + + return super.isFiltered(identifier, path, 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..37062d6e2 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/CustomFilter.java @@ -0,0 +1,161 @@ +package app.revanced.extension.youtube.patches.components; + +import static app.revanced.extension.shared.StringRef.str; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Arrays; +import java.util.Collection; +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.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.ByteTrieSearch; +import app.revanced.extension.youtube.settings.Settings; + +/** + * Allows custom filtering using a path and optionally a proto buffer string. + */ +@SuppressWarnings("unused") +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 + boolean isFiltered(@Nullable String identifier, String path, 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(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/DescriptionComponentsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/DescriptionComponentsFilter.java new file mode 100644 index 000000000..2ddd8489c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/DescriptionComponentsFilter.java @@ -0,0 +1,88 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import app.revanced.extension.youtube.StringTrieSearch; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +final class DescriptionComponentsFilter extends Filter { + + private final StringTrieSearch exceptions = new StringTrieSearch(); + + private final ByteArrayFilterGroupList macroMarkersCarouselGroupList = new ByteArrayFilterGroupList(); + + private final StringFilterGroup macroMarkersCarousel; + + public DescriptionComponentsFilter() { + exceptions.addPatterns( + "compact_channel", + "description", + "grid_video", + "inline_expander", + "metadata" + ); + + final StringFilterGroup attributesSection = new StringFilterGroup( + Settings.HIDE_ATTRIBUTES_SECTION, + "gaming_section", + "music_section", + "video_attributes_section" + ); + + final StringFilterGroup infoCardsSection = new StringFilterGroup( + Settings.HIDE_INFO_CARDS_SECTION, + "infocards_section" + ); + + final StringFilterGroup podcastSection = new StringFilterGroup( + Settings.HIDE_PODCAST_SECTION, + "playlist_section" + ); + + final StringFilterGroup transcriptSection = new StringFilterGroup( + Settings.HIDE_TRANSCRIPT_SECTION, + "transcript_section" + ); + + macroMarkersCarousel = new StringFilterGroup( + null, + "macro_markers_carousel.eml" + ); + + macroMarkersCarouselGroupList.addAll( + new ByteArrayFilterGroup( + Settings.HIDE_CHAPTERS_SECTION, + "chapters_horizontal_shelf" + ), + new ByteArrayFilterGroup( + Settings.HIDE_KEY_CONCEPTS_SECTION, + "learning_concept_macro_markers_carousel_shelf" + ) + ); + + addPathCallbacks( + attributesSection, + infoCardsSection, + podcastSection, + transcriptSection, + macroMarkersCarousel + ); + } + + @Override + boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (exceptions.matches(path)) return false; + + if (matchedGroup == macroMarkersCarousel) { + if (contentIndex == 0 && macroMarkersCarouselGroupList.check(protobufBufferArray).isFiltered()) { + return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex); + } + + return false; + } + + return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/Filter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/Filter.java new file mode 100644 index 000000000..42b86d589 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/Filter.java @@ -0,0 +1,90 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.settings.BaseSettings; + +/** + * Filters litho based components. + * + * 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, 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. + */ +abstract class Filter { + + public enum FilterContentType { + IDENTIFIER, + PATH, + 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<>(); + + /** + * Adds callbacks to {@link #isFiltered(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, byte[], StringFilterGroup, FilterContentType, int)} + * if any of the groups are found. + */ + protected final void addPathCallbacks(StringFilterGroup... groups) { + pathCallbacks.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. + */ + boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray, + StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) { + if (BaseSettings.DEBUG.get()) { + String filterSimpleName = getClass().getSimpleName(); + if (contentType == FilterContentType.IDENTIFIER) { + Logger.printDebug(() -> filterSimpleName + " Filtered identifier: " + identifier); + } else { + Logger.printDebug(() -> filterSimpleName + " Filtered path: " + path); + } + } + return true; + } +} + diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FilterGroup.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FilterGroup.java new file mode 100644 index 000000000..4e20bc82a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FilterGroup.java @@ -0,0 +1,214 @@ +package app.revanced.extension.youtube.patches.components; + +import androidx.annotation.NonNull; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.settings.BooleanSetting; +import app.revanced.extension.youtube.ByteTrieSearch; + +abstract class FilterGroup { + 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); +} + +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); + } +} + +/** + * 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. + */ +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/youtube/patches/components/FilterGroupList.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FilterGroupList.java new file mode 100644 index 000000000..ac0e23ca8 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/FilterGroupList.java @@ -0,0 +1,85 @@ +package app.revanced.extension.youtube.patches.components; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import java.util.*; +import java.util.function.Consumer; + +import app.revanced.extension.youtube.ByteTrieSearch; +import app.revanced.extension.youtube.StringTrieSearch; +import app.revanced.extension.youtube.TrieSearch; + +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, matchedLength); + return true; + } + return false; + }); + } + } + } + + @NonNull + @Override + public Iterator iterator() { + return filterGroups.iterator(); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @Override + public void forEach(@NonNull Consumer action) { + filterGroups.forEach(action); + } + + @RequiresApi(api = Build.VERSION_CODES.N) + @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(); +} + +final class StringFilterGroupList extends FilterGroupList { + protected StringTrieSearch createSearchGraph() { + return new StringTrieSearch(); + } +} + +/** + * 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. + */ +final class ByteArrayFilterGroupList extends FilterGroupList { + protected ByteTrieSearch createSearchGraph() { + return new ByteTrieSearch(); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/HideInfoCardsFilterPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/HideInfoCardsFilterPatch.java new file mode 100644 index 000000000..ce92b592e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/HideInfoCardsFilterPatch.java @@ -0,0 +1,16 @@ +package app.revanced.extension.youtube.patches.components; + +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class HideInfoCardsFilterPatch extends Filter { + + public HideInfoCardsFilterPatch() { + addIdentifierCallbacks( + new StringFilterGroup( + Settings.HIDE_INFO_CARDS, + "info_card_teaser_overlay.eml" + ) + ); + } +} 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..b451fd282 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/KeywordContentFilter.java @@ -0,0 +1,597 @@ +package app.revanced.extension.youtube.patches.components; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton; +import static java.lang.Character.UnicodeBlock.*; + +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.ByteTrieSearch; +import app.revanced.extension.youtube.StringTrieSearch; +import app.revanced.extension.youtube.TrieSearch; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.NavigationBar; +import app.revanced.extension.youtube.shared.PlayerType; + +/** + *

+ * 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")
+@RequiresApi(api = Build.VERSION_CODES.N)
+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.
+            "home_video_with_context.eml",
+            "search_video_with_context.eml",
+            "video_with_context.eml", // Subscription tab videos.
+            "related_video_with_context.eml",
+            // A/B test for subscribed video, and sometimes when tablet layout is enabled.
+            "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 backoff is in effect.
+     */
+    private volatile long timeToResumeFiltering;
+
+    /**
+     * 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;
+
+    /**
+     * 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() {
+        // Keywords are parsed on first call to isFiltered()
+        addPathCallbacks(startsWithFilter, containsFilter);
+    }
+
+    private boolean hideKeywordSettingIsActive() {
+        if (timeToResumeFiltering != 0) {
+            if (System.currentTimeMillis() < timeToResumeFiltering) {
+                return false;
+            }
+
+            timeToResumeFiltering = 0;
+            filteredVideosPercentage = 0;
+            Logger.printDebug(() -> "Resuming keyword filtering");
+        }
+
+        // Must check player type first, as search bar can be active behind the player.
+        if (PlayerType.getCurrent().isMaximizedOrFullscreen()) {
+            // For now, consider the under video results the same as the home feed.
+            return Settings.HIDE_KEYWORD_CONTENT_HOME.get();
+        }
+
+        // Must check second, as search can be from any tab.
+        if (NavigationBar.isSearchBarActive()) {
+            return Settings.HIDE_KEYWORD_CONTENT_SEARCH.get();
+        }
+
+        // Avoid checking navigation button status if all other settings are off.
+        final boolean hideHome = Settings.HIDE_KEYWORD_CONTENT_HOME.get();
+        final boolean hideSubscriptions = Settings.HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS.get();
+        if (!hideHome && !hideSubscriptions) {
+            return false;
+        }
+
+        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
+    boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
+                       StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
+        if (contentIndex != 0 && matchedGroup == startsWithFilter) {
+            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 (!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(identifier, path, 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..bf10416d1
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/LayoutComponentsFilter.java
@@ -0,0 +1,468 @@
+package app.revanced.extension.youtube.patches.components;
+
+import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
+
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.youtube.StringTrieSearch;
+import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.youtube.shared.NavigationBar;
+import app.revanced.extension.youtube.shared.PlayerType;
+
+@SuppressWarnings("unused")
+public final class LayoutComponentsFilter extends Filter {
+    private static final String COMPACT_CHANNEL_BAR_PATH_PREFIX = "compact_channel_bar.eml";
+    private static final String VIDEO_ACTION_BAR_PATH_PREFIX = "video_action_bar.eml";
+    private static final String ANIMATED_VECTOR_TYPE_PATH = "AnimatedVectorType";
+
+    private static final StringTrieSearch mixPlaylistsExceptions = new StringTrieSearch(
+            "V.ED", // Playlist browse id.
+            "java.lang.ref.WeakReference"
+    );
+    private static final ByteArrayFilterGroup mixPlaylistsExceptions2 = new ByteArrayFilterGroup(
+            null,
+            "cell_description_body"
+    );
+    private static final ByteArrayFilterGroup mixPlaylists = new ByteArrayFilterGroup(
+            Settings.HIDE_MIX_PLAYLISTS,
+            "&list="
+    );
+
+    private final StringTrieSearch exceptions = new StringTrieSearch();
+    private final StringFilterGroup searchResultShelfHeader;
+    private final StringFilterGroup inFeedSurvey;
+    private final StringFilterGroup notifyMe;
+    private final StringFilterGroup expandableMetadata;
+    private final ByteArrayFilterGroup searchResultRecommendations;
+    private final StringFilterGroup searchResultVideo;
+    private final StringFilterGroup compactChannelBarInner;
+    private final StringFilterGroup compactChannelBarInnerButton;
+    private final ByteArrayFilterGroup joinMembershipButton;
+    private final StringFilterGroup likeSubscribeGlow;
+    private final StringFilterGroup horizontalShelves;
+
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    public LayoutComponentsFilter() {
+        exceptions.addPatterns(
+                "home_video_with_context",
+                "related_video_with_context",
+                "search_video_with_context",
+                "comment_thread", // Whitelist comments
+                "|comment.", // Whitelist comment replies
+                "library_recent_shelf"
+        );
+
+        // Identifiers.
+
+        final var chipsShelf = new StringFilterGroup(
+                Settings.HIDE_CHIPS_SHELF,
+                "chips_shelf"
+        );
+
+        addIdentifierCallbacks(
+                chipsShelf
+        );
+
+        // Paths.
+
+        final var communityPosts = new StringFilterGroup(
+                Settings.HIDE_COMMUNITY_POSTS,
+                "post_base_wrapper",
+                "text_post_root.eml",
+                "images_post_root.eml",
+                "images_post_slim.eml",
+                "images_post_root_slim.eml",
+                "text_post_root_slim.eml",
+                "post_base_wrapper_slim.eml"
+        );
+
+        final var communityGuidelines = new StringFilterGroup(
+                Settings.HIDE_COMMUNITY_GUIDELINES,
+                "community_guidelines"
+        );
+
+        final var subscribersCommunityGuidelines = new StringFilterGroup(
+                Settings.HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES,
+                "sponsorships_comments_upsell"
+        );
+
+        final var channelMemberShelf = new StringFilterGroup(
+                Settings.HIDE_CHANNEL_MEMBER_SHELF,
+                "member_recognition_shelf"
+        );
+
+        final var compactBanner = new StringFilterGroup(
+                Settings.HIDE_COMPACT_BANNER,
+                "compact_banner"
+        );
+
+        inFeedSurvey = new StringFilterGroup(
+                Settings.HIDE_FEED_SURVEY,
+                "in_feed_survey",
+                "slimline_survey"
+        );
+
+        final var medicalPanel = new StringFilterGroup(
+                Settings.HIDE_MEDICAL_PANELS,
+                "medical_panel"
+        );
+
+        final var paidPromotion = new StringFilterGroup(
+                Settings.HIDE_PAID_PROMOTION_LABEL,
+                "paid_content_overlay"
+        );
+
+        final var infoPanel = new StringFilterGroup(
+                Settings.HIDE_HIDE_INFO_PANELS,
+                "publisher_transparency_panel",
+                "single_item_information_panel"
+        );
+
+        final var latestPosts = new StringFilterGroup(
+                Settings.HIDE_HIDE_LATEST_POSTS,
+                "post_shelf"
+        );
+
+        final var channelGuidelines = new StringFilterGroup(
+                Settings.HIDE_HIDE_CHANNEL_GUIDELINES,
+                "channel_guidelines_entry_banner"
+        );
+
+        final var emergencyBox = new StringFilterGroup(
+                Settings.HIDE_EMERGENCY_BOX,
+                "emergency_onebox"
+        );
+
+        // The player audio track button does the exact same function as the audio track flyout menu option.
+        // Previously this was a setting to show/hide the player button.
+        // But it was decided it's simpler to always hide this button because:
+        // - 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 var audioTrackButton = new StringFilterGroup(
+                null,
+                "multi_feed_icon_button"
+        );
+
+        final var artistCard = new StringFilterGroup(
+                Settings.HIDE_ARTIST_CARDS,
+                "official_card"
+        );
+
+        expandableMetadata = new StringFilterGroup(
+                Settings.HIDE_EXPANDABLE_CHIP,
+                "inline_expander"
+        );
+
+        final var channelBar = new StringFilterGroup(
+                Settings.HIDE_CHANNEL_BAR,
+                "channel_bar"
+        );
+
+        final var relatedVideos = new StringFilterGroup(
+                Settings.HIDE_RELATED_VIDEOS,
+                "fullscreen_related_videos"
+        );
+
+        final var playables = new StringFilterGroup(
+                Settings.HIDE_PLAYABLES,
+                "horizontal_gaming_shelf.eml",
+                "mini_game_card.eml"
+        );
+
+        final var quickActions = new StringFilterGroup(
+                Settings.HIDE_QUICK_ACTIONS,
+                "quick_actions"
+        );
+
+        final var imageShelf = new StringFilterGroup(
+                Settings.HIDE_IMAGE_SHELF,
+                "image_shelf"
+        );
+
+
+        final var timedReactions = new StringFilterGroup(
+                Settings.HIDE_TIMED_REACTIONS,
+                "emoji_control_panel",
+                "timed_reaction"
+        );
+
+        searchResultShelfHeader = new StringFilterGroup(
+                Settings.HIDE_SEARCH_RESULT_SHELF_HEADER,
+                "shelf_header.eml"
+        );
+
+        notifyMe = new StringFilterGroup(
+                Settings.HIDE_NOTIFY_ME_BUTTON,
+                "set_reminder_button"
+        );
+
+        compactChannelBarInner = new StringFilterGroup(
+                Settings.HIDE_JOIN_MEMBERSHIP_BUTTON,
+                "compact_channel_bar_inner"
+        );
+
+        compactChannelBarInnerButton = new StringFilterGroup(
+                null,
+                "|button.eml|"
+        );
+
+        joinMembershipButton = new ByteArrayFilterGroup(
+                null,
+                "sponsorships"
+        );
+
+        likeSubscribeGlow = new StringFilterGroup(
+                Settings.DISABLE_LIKE_SUBSCRIBE_GLOW,
+                "animated_button_border.eml"
+        );
+
+        final var channelWatermark = new StringFilterGroup(
+                Settings.HIDE_VIDEO_CHANNEL_WATERMARK,
+                "featured_channel_watermark_overlay"
+        );
+
+        final var forYouShelf = new StringFilterGroup(
+                Settings.HIDE_FOR_YOU_SHELF,
+                "mixed_content_shelf"
+        );
+
+        searchResultVideo = new StringFilterGroup(
+                Settings.HIDE_SEARCH_RESULT_RECOMMENDATIONS,
+                "search_video_with_context.eml"
+        );
+
+        searchResultRecommendations = new ByteArrayFilterGroup(
+                Settings.HIDE_SEARCH_RESULT_RECOMMENDATIONS,
+                "endorsement_header_footer"
+        );
+
+        horizontalShelves = new StringFilterGroup(
+                Settings.HIDE_HORIZONTAL_SHELVES,
+                "horizontal_video_shelf.eml",
+                "horizontal_shelf.eml",
+                "horizontal_shelf_inline.eml",
+                "horizontal_tile_shelf.eml"
+        );
+
+        addPathCallbacks(
+                expandableMetadata,
+                inFeedSurvey,
+                notifyMe,
+                likeSubscribeGlow,
+                channelBar,
+                communityPosts,
+                paidPromotion,
+                searchResultVideo,
+                latestPosts,
+                channelWatermark,
+                communityGuidelines,
+                playables,
+                quickActions,
+                relatedVideos,
+                compactBanner,
+                compactChannelBarInner,
+                medicalPanel,
+                infoPanel,
+                emergencyBox,
+                subscribersCommunityGuidelines,
+                channelGuidelines,
+                audioTrackButton,
+                artistCard,
+                timedReactions,
+                imageShelf,
+                channelMemberShelf,
+                forYouShelf,
+                horizontalShelves
+        );
+    }
+
+    @Override
+    boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
+                       StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
+        if (matchedGroup == searchResultVideo) {
+            if (searchResultRecommendations.check(protobufBufferArray).isFiltered()) {
+                return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+            }
+            return false;
+        }
+
+        if (matchedGroup == likeSubscribeGlow) {
+            if ((path.startsWith(VIDEO_ACTION_BAR_PATH_PREFIX) || path.startsWith(COMPACT_CHANNEL_BAR_PATH_PREFIX))
+                    && path.contains(ANIMATED_VECTOR_TYPE_PATH)) {
+                return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+            }
+
+            return false;
+        }
+
+        // The groups are excluded from the filter due to the exceptions list below.
+        // Filter them separately here.
+        if (matchedGroup == notifyMe || matchedGroup == inFeedSurvey || matchedGroup == expandableMetadata)
+        {
+            return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+        }
+
+        if (exceptions.matches(path)) return false; // Exceptions are not filtered.
+
+        if (matchedGroup == compactChannelBarInner) {
+            if (compactChannelBarInnerButton.check(path).isFiltered()) {
+                // The filter may be broad, but in the context of a compactChannelBarInnerButton,
+                // it's safe to assume that the button is the only thing that should be hidden.
+                if (joinMembershipButton.check(protobufBufferArray).isFiltered()) {
+                    return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+                }
+            }
+
+            return false;
+        }
+
+        // TODO: This also hides the feed Shorts shelf header
+        if (matchedGroup == searchResultShelfHeader && contentIndex != 0) return false;
+
+        if (matchedGroup == horizontalShelves) {
+            if (contentIndex == 0 && hideShelves()) {
+                return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
+            }
+
+            return false;
+        }
+
+        return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+    }
+
+    /**
+     * Injection point.
+     * Called from a different place then the other filters.
+     */
+    public static boolean filterMixPlaylists(final Object conversionContext, @Nullable final byte[] bytes) {
+        try {
+            if (bytes == null) {
+                Logger.printDebug(() -> "bytes is null");
+                return false;
+            }
+
+            // Prevent playlist items being hidden, if a mix playlist is present in it.
+            if (mixPlaylistsExceptions.matches(conversionContext.toString())) {
+                return false;
+            }
+
+            // Prevent hiding the description of some videos accidentally.
+            if (mixPlaylistsExceptions2.check(bytes).isFiltered()) {
+                return false;
+            }
+
+            if (mixPlaylists.check(bytes).isFiltered()) {
+                Logger.printDebug(() -> "Filtered mix playlist");
+                return true;
+            }
+        } catch (Exception ex) {
+            Logger.printException(() -> "filterMixPlaylists failure", ex);
+        }
+
+        return false;
+    }
+
+    /**
+     * Injection point.
+     */
+    public static boolean showWatermark() {
+        return !Settings.HIDE_VIDEO_CHANNEL_WATERMARK.get();
+    }
+
+    /**
+     * Injection point.
+     */
+    public static void hideAlbumCard(View view) {
+        Utils.hideViewBy0dpUnderCondition(Settings.HIDE_ALBUM_CARDS, view);
+    }
+
+    /**
+     * Injection point.
+     */
+    public static void hideCrowdfundingBox(View view) {
+        Utils.hideViewBy0dpUnderCondition(Settings.HIDE_CROWDFUNDING_BOX, view);
+    }
+
+    /**
+     * Injection point.
+     */
+    public static boolean hideFloatingMicrophoneButton(final boolean original) {
+        return original || Settings.HIDE_FLOATING_MICROPHONE_BUTTON.get();
+    }
+
+    /**
+     * Injection point.
+     */
+    public static int hideInFeed(final int height) {
+        return Settings.HIDE_FILTER_BAR_FEED_IN_FEED.get()
+                ? 0
+                : height;
+    }
+
+    /**
+     * Injection point.
+     */
+    public static int hideInSearch(int height) {
+        return Settings.HIDE_FILTER_BAR_FEED_IN_SEARCH.get()
+                ? 0
+                : height;
+    }
+
+    /**
+     * Injection point.
+     */
+    public static void hideInRelatedVideos(View chipView) {
+        Utils.hideViewBy0dpUnderCondition(Settings.HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS, chipView);
+    }
+
+    private static final boolean HIDE_DOODLES_ENABLED = Settings.HIDE_DOODLES.get();
+
+    /**
+     * Injection point.
+     */
+    @Nullable
+    public static Drawable hideYoodles(Drawable animatedYoodle) {
+        if (HIDE_DOODLES_ENABLED) {
+            return null;
+        }
+
+        return animatedYoodle;
+    }
+
+    private static final boolean HIDE_SHOW_MORE_BUTTON_ENABLED = Settings.HIDE_SHOW_MORE_BUTTON.get();
+
+    /**
+     * Injection point.
+     */
+    public static void hideShowMoreButton(View view) {
+        if (HIDE_SHOW_MORE_BUTTON_ENABLED
+                && NavigationBar.isSearchBarActive()
+                // Search bar can be active but behind the player.
+                && !PlayerType.getCurrent().isMaximizedOrFullscreen()) {
+            Utils.hideViewByLayoutParams(view);
+        }
+    }
+
+    private static boolean hideShelves() {
+        // If the player is opened while library is selected,
+        // then filter any recommendations below the player.
+        if (PlayerType.getCurrent().isMaximizedOrFullscreen()
+                // Or if the search is active while library is selected, then also filter.
+                || NavigationBar.isSearchBarActive()) {
+            return true;
+        }
+
+        // Check navigation button last.
+        // Only filter if the library tab is not selected.
+        // This check is important as the shelf layout is used for the library tab playlists.
+        return NavigationButton.getSelectedNavigationButton() != NavigationButton.LIBRARY;
+    }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/LithoFilterPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/LithoFilterPatch.java
new file mode 100644
index 000000000..1edd27509
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/LithoFilterPatch.java
@@ -0,0 +1,188 @@
+package app.revanced.extension.youtube.patches.components;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.nio.ByteBuffer;
+import java.util.List;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.youtube.StringTrieSearch;
+import app.revanced.extension.youtube.settings.Settings;
+
+@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 byte[] protoBuffer;
+
+        LithoFilterParameters(@Nullable String lithoIdentifier, String lithoPath, byte[] protoBuffer) {
+            this.identifier = lithoIdentifier;
+            this.path = lithoPath;
+            this.protoBuffer = protoBuffer;
+        }
+
+        @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( "ID: ");
+            builder.append(identifier);
+            builder.append(" Path: ");
+            builder.append(path);
+            if (Settings.DEBUG_PROTOBUFFER.get()) {
+                builder.append(" BufferStrings: ");
+                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 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);
+        }
+
+        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.identifier, parameters.path, parameters.protoBuffer,
+                                    group, type, matchedStartIndex);
+                        }
+                );
+            }
+        }
+    }
+
+    /**
+     * Injection point.  Called off the main thread.
+     */
+    @SuppressWarnings("unused")
+    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.
+        if (protobufBuffer == null) {
+            // It appears the buffer can be cleared out just before the call to #filter()
+            // Ignore this null value and retain the last buffer that was set.
+            Logger.printDebug(() -> "Ignoring null protobuffer");
+        } else {
+            bufferThreadLocal.set(protobufBuffer);
+        }
+    }
+
+    /**
+     * Injection point.  Called off the main thread, and commonly called by multiple threads at the same time.
+     */
+    @SuppressWarnings("unused")
+    public static boolean filter(@Nullable String lithoIdentifier, @NonNull StringBuilder pathBuilder) {
+        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/path filters still work correctly.
+            if (protobufBuffer == null) {
+                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(lithoIdentifier,
+                    pathBuilder.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;
+            }
+        } 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/youtube/patches/components/PlaybackSpeedMenuFilterPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilterPatch.java
new file mode 100644
index 000000000..d630ee9ed
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilterPatch.java
@@ -0,0 +1,51 @@
+package app.revanced.extension.youtube.patches.components;
+
+import androidx.annotation.Nullable;
+
+import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch;
+import app.revanced.extension.youtube.settings.Settings;
+
+/**
+ * Abuse LithoFilter for {@link CustomPlaybackSpeedPatch}.
+ */
+public final class PlaybackSpeedMenuFilterPatch extends Filter {
+
+    /**
+     * Old litho based speed selection menu.
+     */
+    public static volatile boolean isOldPlaybackSpeedMenuVisible;
+
+    /**
+     * 0.05x speed selection menu.
+     */
+    public static volatile boolean isPlaybackRateSelectorMenuVisible;
+
+    private final StringFilterGroup oldPlaybackMenuGroup;
+
+    public PlaybackSpeedMenuFilterPatch() {
+        // 0.05x litho speed menu.
+        var playbackRateSelectorGroup = new StringFilterGroup(
+                Settings.CUSTOM_SPEED_MENU,
+                "playback_rate_selector_menu_sheet.eml-js"
+        );
+
+        // Old litho based speed menu.
+        oldPlaybackMenuGroup = new StringFilterGroup(
+                Settings.CUSTOM_SPEED_MENU,
+                "playback_speed_sheet_content.eml-js");
+
+        addPathCallbacks(playbackRateSelectorGroup, oldPlaybackMenuGroup);
+    }
+
+    @Override
+    boolean isFiltered(@Nullable String identifier, String path, 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/PlayerFlyoutMenuItemsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java
new file mode 100644
index 000000000..888687d8c
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter.java
@@ -0,0 +1,112 @@
+package app.revanced.extension.youtube.patches.components;
+
+import android.os.Build;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+
+import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.youtube.shared.PlayerType;
+
+@SuppressWarnings("unused")
+public class PlayerFlyoutMenuItemsFilter extends Filter {
+
+    private final ByteArrayFilterGroupList flyoutFilterGroupList = new ByteArrayFilterGroupList();
+
+    private final ByteArrayFilterGroup exception;
+    private final StringFilterGroup videoQualityMenuFooter;
+
+    @RequiresApi(api = Build.VERSION_CODES.N)
+    public PlayerFlyoutMenuItemsFilter() {
+        exception = new ByteArrayFilterGroup(
+                // Whitelist Quality menu item when "Hide Additional settings menu" is enabled
+                Settings.HIDE_PLAYER_FLYOUT_ADDITIONAL_SETTINGS,
+                "quality_sheet"
+        );
+
+        videoQualityMenuFooter = new StringFilterGroup(
+                Settings.HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER,
+                "quality_sheet_footer"
+        );
+
+        addPathCallbacks(
+                videoQualityMenuFooter,
+                new StringFilterGroup(null, "overflow_menu_item.eml|")
+        );
+
+        flyoutFilterGroupList.addAll(
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_PLAYER_FLYOUT_CAPTIONS,
+                        "closed_caption"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_PLAYER_FLYOUT_ADDITIONAL_SETTINGS,
+                        "yt_outline_gear"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_PLAYER_FLYOUT_LOOP_VIDEO,
+                        "yt_outline_arrow_repeat_1_"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_PLAYER_FLYOUT_AMBIENT_MODE,
+                        "yt_outline_screen_light"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_PLAYER_FLYOUT_STABLE_VOLUME,
+                        "volume_stable"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_PLAYER_FLYOUT_HELP,
+                        "yt_outline_question_circle"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_PLAYER_FLYOUT_MORE_INFO,
+                        "yt_outline_info_circle"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_PLAYER_FLYOUT_LOCK_SCREEN,
+                        "yt_outline_lock"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_PLAYER_FLYOUT_SPEED,
+                        "yt_outline_play_arrow_half_circle"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_PLAYER_FLYOUT_AUDIO_TRACK,
+                        "yt_outline_person_radar"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_PLAYER_FLYOUT_SLEEP_TIMER,
+                        "yt_outline_moon_z_"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_PLAYER_FLYOUT_WATCH_IN_VR,
+                        "yt_outline_vr"
+                )
+        );
+    }
+
+    @Override
+    boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
+                       StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
+        if (matchedGroup == videoQualityMenuFooter) {
+            return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+        }
+
+        if (contentIndex != 0) {
+            return false; // Overflow menu is always the start of the path.
+        }
+
+        // Shorts also use this player flyout panel
+        if (PlayerType.getCurrent().isNoneOrHidden() || exception.check(protobufBufferArray).isFiltered()) {
+            return false;
+        }
+
+        if (flyoutFilterGroupList.check(protobufBufferArray).isFiltered()) {
+            // Super class handles logging.
+            return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+        }
+
+        return false;
+    }
+}
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..0d1504e40
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ReturnYouTubeDislikeFilterPatch.java
@@ -0,0 +1,139 @@
+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.youtube.patches.ReturnYouTubeDislikePatch;
+import app.revanced.extension.youtube.patches.VideoInformation;
+import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.youtube.TrieSearch;
+
+/**
+ * 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(Entry eldest) {
+            return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK;
+        }
+    };
+
+    /**
+     * Injection point.
+     */
+    @SuppressWarnings("unused")
+    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);
+        }
+    }
+
+    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")
+        );
+    }
+
+    @Override
+    boolean isFiltered(@Nullable String identifier, String path, 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;
+        }
+    }
+
+    /**
+     * 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;
+    }
+}
\ No newline at end of file
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsFilter.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsFilter.java
new file mode 100644
index 000000000..ee7b78952
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/ShortsFilter.java
@@ -0,0 +1,443 @@
+package app.revanced.extension.youtube.patches.components;
+
+import static app.revanced.extension.shared.Utils.hideViewUnderCondition;
+import static app.revanced.extension.youtube.shared.NavigationBar.NavigationButton;
+
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.google.android.libraries.youtube.rendering.ui.pivotbar.PivotBar;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.List;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.youtube.shared.NavigationBar;
+import app.revanced.extension.youtube.shared.PlayerType;
+
+@SuppressWarnings("unused")
+public final class ShortsFilter extends Filter {
+    private static final boolean HIDE_SHORTS_NAVIGATION_BAR = Settings.HIDE_SHORTS_NAVIGATION_BAR.get();
+    private final static String REEL_CHANNEL_BAR_PATH = "reel_channel_bar.eml";
+
+    /**
+     * For paid promotion label and subscribe button that appears in the channel bar.
+     */
+    private final static String REEL_METAPANEL_PATH = "reel_metapanel.eml";
+
+    /**
+     * Tags that appears when opening the Shorts player.
+     */
+    private static final List REEL_WATCH_FRAGMENT_INIT_PLAYBACK = Arrays.asList("r_fs", "r_ts");
+
+    /**
+     * Vertical padding between the bottom of the screen and the seekbar, when the Shorts navigation bar is hidden.
+     */
+    public static final int HIDDEN_NAVIGATION_BAR_VERTICAL_HEIGHT = 100;
+
+    private static WeakReference pivotBarRef = new WeakReference<>(null);
+
+    private final StringFilterGroup shortsCompactFeedVideoPath;
+    private final ByteArrayFilterGroup shortsCompactFeedVideoBuffer;
+
+    private final StringFilterGroup subscribeButton;
+    private final StringFilterGroup joinButton;
+    private final StringFilterGroup paidPromotionButton;
+    private final StringFilterGroup shelfHeader;
+
+    private final StringFilterGroup suggestedAction;
+    private final ByteArrayFilterGroupList suggestedActionsGroupList = new ByteArrayFilterGroupList();
+
+    private final StringFilterGroup actionBar;
+    private final ByteArrayFilterGroupList videoActionButtonGroupList = new ByteArrayFilterGroupList();
+
+    public ShortsFilter() {
+        //
+        // Identifier components.
+        //
+
+        var shortsIdentifiers = new StringFilterGroup(
+                null, // Setting is based on navigation state.
+                "shorts_shelf",
+                "inline_shorts",
+                "shorts_grid",
+                "shorts_video_cell",
+                "shorts_pivot_item"
+        );
+
+        // Feed Shorts shelf header.
+        // Use a different filter group for this pattern, as it requires an additional check after matching.
+        shelfHeader = new StringFilterGroup(
+                null,
+                "shelf_header.eml"
+        );
+
+        addIdentifierCallbacks(shortsIdentifiers, shelfHeader);
+
+        //
+        // Path components.
+        //
+
+        shortsCompactFeedVideoPath = new StringFilterGroup(null,
+                // Shorts that appear in the feed/search when the device is using tablet layout.
+                "compact_video.eml",
+                // 'video_lockup_with_attachment.eml' is shown instead of 'compact_video.eml' for some users
+                "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.
+        shortsCompactFeedVideoBuffer = new ByteArrayFilterGroup(null, "/frame0.jpg");
+
+        // Shorts player components.
+        StringFilterGroup pausedOverlayButtons = new StringFilterGroup(
+                Settings.HIDE_SHORTS_PAUSED_OVERLAY_BUTTONS,
+                "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 soundButton = new StringFilterGroup(
+                Settings.HIDE_SHORTS_SOUND_BUTTON,
+                "reel_pivot_button"
+        );
+
+        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 likeFountain = new StringFilterGroup(
+                Settings.HIDE_SHORTS_LIKE_FOUNTAIN,
+                "like_fountain.eml"
+        );
+
+        joinButton = new StringFilterGroup(
+                Settings.HIDE_SHORTS_JOIN_BUTTON,
+                "sponsor_button"
+        );
+
+        subscribeButton = new StringFilterGroup(
+                Settings.HIDE_SHORTS_SUBSCRIBE_BUTTON,
+                "subscribe_button"
+        );
+
+        paidPromotionButton = new StringFilterGroup(
+                Settings.HIDE_PAID_PROMOTION_LABEL,
+                "reel_player_disclosure.eml"
+        );
+
+        actionBar = new StringFilterGroup(
+                null,
+                "shorts_action_bar"
+        );
+
+        suggestedAction = new StringFilterGroup(
+                null,
+                "suggested_action.eml"
+        );
+
+        addPathCallbacks(
+                shortsCompactFeedVideoPath, suggestedAction, actionBar, joinButton, subscribeButton,
+                paidPromotionButton, pausedOverlayButtons, channelBar, fullVideoLinkLabel, videoTitle,
+                reelSoundMetadata, soundButton, infoPanel, stickers, likeFountain
+        );
+
+        //
+        // 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"
+                )
+        );
+
+        //
+        // Suggested actions.
+        //
+        suggestedActionsGroupList.addAll(
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_SHORTS_SHOP_BUTTON,
+                        "yt_outline_bag_"
+                ),
+                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_LOCATION_LABEL,
+                        "yt_outline_location_point_"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_SHORTS_SAVE_SOUND_BUTTON,
+                        "yt_outline_bookmark_",
+                        // 'Save sound' button. It seems this has been removed and only 'Save music' is used.
+                        // Still hide this in case it's still present.
+                        "yt_outline_list_add_",
+                        // 'Use this sound' button. It seems this has been removed and only 'Save music' is used.
+                        // Still hide this in case it's still present.
+                        "yt_outline_camera_"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_SHORTS_SEARCH_SUGGESTIONS,
+                        "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_UPCOMING_BUTTON,
+                        "yt_outline_bell_"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_SHORTS_GREEN_SCREEN_BUTTON,
+                        "greenscreen_temp"
+                ),
+                new ByteArrayFilterGroup(
+                        Settings.HIDE_SHORTS_HASHTAG_BUTTON,
+                        "yt_outline_hashtag_"
+                )
+        );
+    }
+
+    private boolean isEverySuggestedActionFilterEnabled() {
+        for (ByteArrayFilterGroup group : suggestedActionsGroupList) {
+            if (!group.isEnabled()) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    @Override
+    boolean isFiltered(@Nullable String identifier, String path, byte[] protobufBufferArray,
+                       StringFilterGroup matchedGroup, FilterContentType contentType, int contentIndex) {
+        if (contentType == FilterContentType.PATH) {
+            if (matchedGroup == subscribeButton || matchedGroup == joinButton || matchedGroup == paidPromotionButton) {
+                // Selectively filter to avoid false positive filtering of other subscribe/join buttons.
+                if (path.startsWith(REEL_CHANNEL_BAR_PATH) || path.startsWith(REEL_METAPANEL_PATH)) {
+                    return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+                }
+                return false;
+            }
+
+            if (matchedGroup == shortsCompactFeedVideoPath) {
+                if (shouldHideShortsFeedItems() && shortsCompactFeedVideoBuffer.check(protobufBufferArray).isFiltered()) {
+                    return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+                }
+                return false;
+            }
+
+            // Video action buttons (like, dislike, comment, share, remix) have the same path.
+            if (matchedGroup == actionBar) {
+                if (videoActionButtonGroupList.check(protobufBufferArray).isFiltered()) {
+                    return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+                }
+                return false;
+            }
+
+            if (matchedGroup == suggestedAction) {
+                // Skip searching the buffer if all suggested actions are set to hidden.
+                // This has a secondary effect of hiding all new un-identified actions
+                // under the assumption that the user wants all actions hidden.
+                if (isEverySuggestedActionFilterEnabled()) {
+                    return super.isFiltered(path, identifier, protobufBufferArray, matchedGroup, contentType, contentIndex);
+                }
+
+                if (suggestedActionsGroupList.check(protobufBufferArray).isFiltered()) {
+                    return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+                }
+                return false;
+            }
+
+        } else {
+            // Feed/search identifier components.
+            if (matchedGroup == shelfHeader) {
+                // Because the header is used in watch history and possibly other places, check for the index,
+                // which is 0 when the shelf header is used for Shorts.
+                if (contentIndex != 0) return false;
+            }
+
+            if (!shouldHideShortsFeedItems()) return false;
+        }
+
+        // Super class handles logging.
+        return super.isFiltered(identifier, path, protobufBufferArray, matchedGroup, contentType, contentIndex);
+    }
+
+    private static boolean shouldHideShortsFeedItems() {
+        final boolean hideHome = Settings.HIDE_SHORTS_HOME.get();
+        final boolean hideSubscriptions = Settings.HIDE_SHORTS_SUBSCRIPTIONS.get();
+        final boolean hideSearch = Settings.HIDE_SHORTS_SEARCH.get();
+
+        if (hideHome && hideSubscriptions && hideSearch) {
+            // 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 (PlayerType.getCurrent().isMaximizedOrFullscreen()) {
+            // 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 (NavigationBar.isSearchBarActive()) {
+            return hideSearch;
+        }
+
+        // Avoid checking navigation button status if all other Shorts should show.
+        if (!hideHome && !hideSubscriptions) {
+            return false;
+        }
+
+        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 must be in the library tab.  Don't hide the history or any playlists here.
+        return false;
+    }
+
+    public static void hideShortsShelf(final View shortsShelfView) {
+        if (shouldHideShortsFeedItems()) {
+            Utils.hideViewByLayoutParams(shortsShelfView);
+        }
+    }
+
+    public static int getSoundButtonSize(int original) {
+        if (Settings.HIDE_SHORTS_SOUND_BUTTON.get()) {
+            return 0;
+        }
+
+        return original;
+    }
+
+    // region Hide the buttons in older versions of YouTube. New versions use Litho.
+
+    public static void hideLikeButton(final View likeButtonView) {
+        // Cannot set the visibility to gone for like/dislike,
+        // as some other unknown YT code also sets the visibility after this hook.
+        //
+        // Setting the view to 0dp works, but that leaves a blank space where
+        // the button was (only relevant for dislikes button).
+        //
+        // Instead remove the view from the parent.
+        Utils.hideViewByRemovingFromParentUnderCondition(Settings.HIDE_SHORTS_LIKE_BUTTON, likeButtonView);
+    }
+
+    public static void hideDislikeButton(final View dislikeButtonView) {
+        Utils.hideViewByRemovingFromParentUnderCondition(Settings.HIDE_SHORTS_DISLIKE_BUTTON, dislikeButtonView);
+    }
+
+    public static void hideShortsCommentsButton(final View commentsButtonView) {
+        hideViewUnderCondition(Settings.HIDE_SHORTS_COMMENTS_BUTTON, commentsButtonView);
+    }
+
+    public static void hideShortsRemixButton(final View remixButtonView) {
+        hideViewUnderCondition(Settings.HIDE_SHORTS_REMIX_BUTTON, remixButtonView);
+    }
+
+    public static void hideShortsShareButton(final View shareButtonView) {
+        hideViewUnderCondition(Settings.HIDE_SHORTS_SHARE_BUTTON, shareButtonView);
+    }
+
+    // endregion
+
+    public static void setNavigationBar(PivotBar view) {
+        pivotBarRef = new WeakReference<>(view);
+    }
+
+    public static void hideNavigationBar(String tag) {
+        if (HIDE_SHORTS_NAVIGATION_BAR) {
+            if (REEL_WATCH_FRAGMENT_INIT_PLAYBACK.contains(tag)) {
+                var pivotBar = pivotBarRef.get();
+                if (pivotBar == null) return;
+
+                Logger.printDebug(() -> "Hiding navbar by setting to GONE");
+                pivotBar.setVisibility(View.GONE);
+            } else {
+                Logger.printDebug(() -> "Ignoring tag: " + tag);
+            }
+        }
+    }
+
+    public static int getNavigationBarHeight(int original) {
+        if (HIDE_SHORTS_NAVIGATION_BAR) {
+            return HIDDEN_NAVIGATION_BAR_VERTICAL_HEIGHT;
+        }
+
+        return original;
+    }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/VideoQualityMenuFilterPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/VideoQualityMenuFilterPatch.java
new file mode 100644
index 000000000..7ee3cab77
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/components/VideoQualityMenuFilterPatch.java
@@ -0,0 +1,30 @@
+package app.revanced.extension.youtube.patches.components;
+
+import androidx.annotation.Nullable;
+
+import app.revanced.extension.youtube.patches.playback.quality.RestoreOldVideoQualityMenuPatch;
+import app.revanced.extension.youtube.settings.Settings;
+
+/**
+ * Abuse LithoFilter for {@link RestoreOldVideoQualityMenuPatch}.
+ */
+public final class VideoQualityMenuFilterPatch 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 VideoQualityMenuFilterPatch() {
+        addPathCallbacks(new StringFilterGroup(
+                Settings.RESTORE_OLD_VIDEO_QUALITY_MENU,
+                "quick_quality_sheet_content.eml-js"
+        ));
+    }
+
+    @Override
+    boolean isFiltered(@Nullable String identifier, String path, 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/playback/quality/RememberVideoQualityPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java
new file mode 100644
index 000000000..f55b36d61
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch.java
@@ -0,0 +1,167 @@
+package app.revanced.extension.youtube.patches.playback.quality;
+
+import static app.revanced.extension.shared.StringRef.str;
+import static app.revanced.extension.shared.Utils.NetworkType;
+
+import androidx.annotation.Nullable;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.IntegerSetting;
+import app.revanced.extension.youtube.patches.VideoInformation;
+import app.revanced.extension.youtube.settings.Settings;
+
+@SuppressWarnings("unused")
+public class RememberVideoQualityPatch {
+    private static final int AUTOMATIC_VIDEO_QUALITY_VALUE = -2;
+    private static final IntegerSetting wifiQualitySetting = Settings.VIDEO_QUALITY_DEFAULT_WIFI;
+    private static final IntegerSetting mobileQualitySetting = Settings.VIDEO_QUALITY_DEFAULT_MOBILE;
+
+    private static boolean qualityNeedsUpdating;
+
+    /**
+     * If the user selected a new quality from the flyout menu,
+     * and {@link Settings#REMEMBER_VIDEO_QUALITY_LAST_SELECTED} is enabled.
+     */
+    private static boolean userChangedDefaultQuality;
+
+    /**
+     * Index of the video quality chosen by the user from the flyout menu.
+     */
+    private static int userSelectedQualityIndex;
+
+    /**
+     * The available qualities of the current video in human readable form: [1080, 720, 480]
+     */
+    @Nullable
+    private static List videoQualities;
+
+    private static void changeDefaultQuality(int defaultQuality) {
+        String networkTypeMessage;
+        if (Utils.getNetworkType() == NetworkType.MOBILE) {
+            mobileQualitySetting.save(defaultQuality);
+            networkTypeMessage = str("revanced_remember_video_quality_mobile");
+        } else {
+            wifiQualitySetting.save(defaultQuality);
+            networkTypeMessage = str("revanced_remember_video_quality_wifi");
+        }
+        Utils.showToastShort(
+                str("revanced_remember_video_quality_toast", networkTypeMessage, (defaultQuality + "p")));
+    }
+
+    /**
+     * Injection point.
+     *
+     * @param qualities Video qualities available, ordered from largest to smallest, with index 0 being the 'automatic' value of -2
+     * @param originalQualityIndex quality index to use, as chosen by YouTube
+     */
+    public static int setVideoQuality(Object[] qualities, final int originalQualityIndex, Object qInterface, String qIndexMethod) {
+        try {
+            final int preferredQuality = Utils.getNetworkType() == NetworkType.MOBILE
+                    ? mobileQualitySetting.get()
+                    : wifiQualitySetting.get();
+
+            if (!userChangedDefaultQuality && preferredQuality == AUTOMATIC_VIDEO_QUALITY_VALUE) {
+                return originalQualityIndex; // Nothing to do.
+            }
+
+            if (videoQualities == null || videoQualities.size() != qualities.length) {
+                videoQualities = new ArrayList<>(qualities.length);
+                for (Object streamQuality : qualities) {
+                    for (Field field : streamQuality.getClass().getFields()) {
+                        if (field.getType().isAssignableFrom(Integer.TYPE)
+                                && field.getName().length() <= 2) {
+                            videoQualities.add(field.getInt(streamQuality));
+                        }
+                    }
+                }
+                
+                // After changing videos the qualities can initially be for the prior video.
+                // So if the qualities have changed an update is needed.
+                qualityNeedsUpdating = true;
+                Logger.printDebug(() -> "VideoQualities: " + videoQualities);
+            }
+
+            if (userChangedDefaultQuality) {
+                userChangedDefaultQuality = false;
+                final int quality = videoQualities.get(userSelectedQualityIndex);
+                Logger.printDebug(() -> "User changed default quality to: " + quality);
+                changeDefaultQuality(quality);
+                return userSelectedQualityIndex;
+            }
+
+            if (!qualityNeedsUpdating) {
+                return originalQualityIndex;
+            }
+            qualityNeedsUpdating = false;
+
+            // Find the highest quality that is equal to or less than the preferred.
+            int qualityToUse = videoQualities.get(0); // first element is automatic mode
+            int qualityIndexToUse = 0;
+            int i = 0;
+            for (Integer quality : videoQualities) {
+                if (quality <= preferredQuality && qualityToUse < quality)  {
+                    qualityToUse = quality;
+                    qualityIndexToUse = i;
+                }
+                i++;
+            }
+
+            // If the desired quality index is equal to the original index,
+            // then the video is already set to the desired default quality.
+            final int qualityToUseFinal = qualityToUse;
+            if (qualityIndexToUse == originalQualityIndex) {
+                // On first load of a new video, if the UI video quality flyout menu
+                // is not updated then it will still show 'Auto' (ie: Auto (480p)),
+                // even though it's already set to the desired resolution.
+                //
+                // To prevent confusion, set the video index anyways (even if it matches the existing index)
+                // as that will force the UI picker to not display "Auto".
+                Logger.printDebug(() -> "Video is already preferred quality: " + qualityToUseFinal);
+            } else {
+                Logger.printDebug(() -> "Changing video quality from: "
+                        + videoQualities.get(originalQualityIndex) + " to: " + qualityToUseFinal);
+            }
+
+            Method m = qInterface.getClass().getMethod(qIndexMethod, Integer.TYPE);
+            m.invoke(qInterface, qualityToUse);
+            return qualityIndexToUse;
+        } catch (Exception ex) {
+            Logger.printException(() -> "Failed to set quality", ex);
+            return originalQualityIndex;
+        }
+    }
+
+    /**
+     * Injection point.  Old quality menu.
+     */
+    public static void userChangedQuality(int selectedQualityIndex) {
+        if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get()) return;
+
+        userSelectedQualityIndex = selectedQualityIndex;
+        userChangedDefaultQuality = true;
+    }
+
+    /**
+     * Injection point.  New quality menu.
+     */
+    public static void userChangedQualityInNewFlyout(int selectedQuality) {
+        if (!Settings.REMEMBER_VIDEO_QUALITY_LAST_SELECTED.get()) return;
+
+        changeDefaultQuality(selectedQuality); // Quality is human readable resolution (ie: 1080).
+    }
+
+    /**
+     * Injection point.
+     */
+    public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) {
+        Logger.printDebug(() -> "newVideoStarted");
+        qualityNeedsUpdating = true;
+        videoQualities = null;
+    }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RestoreOldVideoQualityMenuPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RestoreOldVideoQualityMenuPatch.java
new file mode 100644
index 000000000..ac74bc810
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/quality/RestoreOldVideoQualityMenuPatch.java
@@ -0,0 +1,110 @@
+package app.revanced.extension.youtube.patches.playback.quality;
+
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.widget.ListView;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.youtube.patches.components.VideoQualityMenuFilterPatch;
+import app.revanced.extension.youtube.settings.Settings;
+
+/**
+ * This patch contains the logic to show the old video quality menu.
+ * Two methods are required, because the quality menu is a RecyclerView in the new YouTube version
+ * and a ListView in the old one.
+ */
+@SuppressWarnings("unused")
+public final class RestoreOldVideoQualityMenuPatch {
+
+    /**
+     * Injection point.
+     */
+    public static void onFlyoutMenuCreate(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 (!VideoQualityMenuFilterPatch.isVideoQualityMenuVisible || recyclerView.getChildCount() == 0) {
+                    return;
+                }
+                VideoQualityMenuFilterPatch.isVideoQualityMenuVisible = false;
+
+                ViewParent quickQualityViewParent = Utils.getParentView(recyclerView, 3);
+                if (!(quickQualityViewParent instanceof ViewGroup)) {
+                    return;
+                }
+
+                View firstChild = recyclerView.getChildAt(0);
+                if (!(firstChild instanceof ViewGroup)) {
+                    return;
+                }
+
+                ViewGroup advancedQualityParentView = (ViewGroup) firstChild;
+                if (advancedQualityParentView.getChildCount() < 4) {
+                    return;
+                }
+
+                View advancedQualityView = advancedQualityParentView.getChildAt(3);
+                if (advancedQualityView == null) {
+                    return;
+                }
+
+                ((ViewGroup) quickQualityViewParent).setVisibility(View.GONE);
+
+                // Click the "Advanced" quality menu to show the "old" quality menu.
+                advancedQualityView.setSoundEffectsEnabled(false);
+                advancedQualityView.performClick();
+            } catch (Exception ex) {
+                Logger.printException(() -> "onFlyoutMenuCreate failure", ex);
+            }
+        });
+    }
+
+
+    /**
+     * Injection point.
+     *
+     * Used to force the creation of the advanced menu item for the Shorts quality flyout.
+     */
+    public static boolean forceAdvancedVideoQualityMenuCreation(boolean original) {
+        return Settings.RESTORE_OLD_VIDEO_QUALITY_MENU.get() || original;
+    }
+
+    /**
+     * Injection point.
+     *
+     * Used if spoofing to an old app version, and also used for the Shorts video quality flyout.
+     */
+    public static void showOldVideoQualityMenu(final ListView listView) {
+        if (!Settings.RESTORE_OLD_VIDEO_QUALITY_MENU.get()) return;
+
+        listView.setOnHierarchyChangeListener(new ViewGroup.OnHierarchyChangeListener() {
+            @Override
+            public void onChildViewAdded(View parent, View child) {
+                try {
+                    parent.setVisibility(View.GONE);
+
+                    final var indexOfAdvancedQualityMenuItem = 4;
+                    if (listView.indexOfChild(child) != indexOfAdvancedQualityMenuItem) return;
+
+                    Logger.printDebug(() -> "Found advanced menu item in old type of quality menu");
+
+                    listView.setSoundEffectsEnabled(false);
+                    final var qualityItemMenuPosition = 4;
+                    listView.performItemClick(null, qualityItemMenuPosition, 0);
+
+                } catch (Exception ex) {
+                    Logger.printException(() -> "showOldVideoQualityMenu failure", ex);
+                }
+            }
+
+            @Override
+            public void onChildViewRemoved(View parent, View child) {
+            }
+        });
+    }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java
new file mode 100644
index 000000000..d015c192a
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch.java
@@ -0,0 +1,199 @@
+package app.revanced.extension.youtube.patches.playback.speed;
+
+import static app.revanced.extension.shared.StringRef.str;
+
+import android.preference.ListPreference;
+import android.support.v7.widget.RecyclerView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+import androidx.annotation.NonNull;
+
+import app.revanced.extension.youtube.patches.components.PlaybackSpeedMenuFilterPatch;
+import app.revanced.extension.youtube.settings.Settings;
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+
+import java.util.Arrays;
+
+@SuppressWarnings("unused")
+public class CustomPlaybackSpeedPatch {
+    /**
+     * 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 MAXIMUM_PLAYBACK_SPEED = 8;
+
+    /**
+     * Custom playback speeds.
+     */
+    public static float[] customPlaybackSpeeds;
+
+    /**
+     * The last time the old playback menu was forcefully called.
+     */
+    private static long lastTimeOldPlaybackMenuInvoked;
+
+    /**
+     * PreferenceList entries and values, of all available playback speeds.
+     */
+    private static String[] preferenceListEntries, preferenceListEntryValues;
+
+    static {
+        loadCustomSpeeds();
+    }
+
+    private static void resetCustomSpeeds(@NonNull String toastMessage) {
+        Utils.showToastLong(toastMessage);
+        Settings.CUSTOM_PLAYBACK_SPEEDS.resetToDefault();
+    }
+
+    private 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];
+
+            int i = 0;
+            for (String speedString : speedStrings) {
+                final float speedFloat = Float.parseFloat(speedString);
+                if (speedFloat <= 0 || arrayContains(customPlaybackSpeeds, speedFloat)) {
+                    throw new IllegalArgumentException();
+                }
+
+                if (speedFloat >= MAXIMUM_PLAYBACK_SPEED) {
+                    resetCustomSpeeds(str("revanced_custom_playback_speeds_invalid", MAXIMUM_PLAYBACK_SPEED));
+                    loadCustomSpeeds();
+                    return;
+                }
+
+                customPlaybackSpeeds[i] = speedFloat;
+                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;
+    }
+
+    /**
+     * Initialize a settings preference list with the available playback speeds.
+     */
+    @SuppressWarnings("deprecation")
+    public static void initializeListPreference(ListPreference preference) {
+        if (preferenceListEntries == null) {
+            preferenceListEntries = new String[customPlaybackSpeeds.length];
+            preferenceListEntryValues = new String[customPlaybackSpeeds.length];
+
+            int i = 0;
+            for (float speed : customPlaybackSpeeds) {
+                String speedString = String.valueOf(speed);
+                preferenceListEntries[i] = speedString + "x";
+                preferenceListEntryValues[i] = speedString;
+                i++;
+            }
+        }
+
+        preference.setEntries(preferenceListEntries);
+        preference.setEntryValues(preferenceListEntryValues);
+    }
+
+    /**
+     * Injection point.
+     */
+    public static void onFlyoutMenuCreate(RecyclerView recyclerView) {
+        recyclerView.getViewTreeObserver().addOnDrawListener(() -> {
+            try {
+                if (PlaybackSpeedMenuFilterPatch.isPlaybackRateSelectorMenuVisible) {
+                    if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 5)) {
+                        PlaybackSpeedMenuFilterPatch.isPlaybackRateSelectorMenuVisible = false;
+                    }
+                    return;
+                }
+            } catch (Exception ex) {
+                Logger.printException(() -> "isPlaybackRateSelectorMenuVisible failure", ex);
+            }
+
+            try {
+                if (PlaybackSpeedMenuFilterPatch.isOldPlaybackSpeedMenuVisible) {
+                    if (hideLithoMenuAndShowOldSpeedMenu(recyclerView, 8)) {
+                        PlaybackSpeedMenuFilterPatch.isOldPlaybackSpeedMenuVisible = false;
+                    }
+                }
+            } catch (Exception ex) {
+                Logger.printException(() -> "isOldPlaybackSpeedMenuVisible failure", ex);
+            }
+        });
+    }
+
+    private static boolean hideLithoMenuAndShowOldSpeedMenu(RecyclerView recyclerView, int expectedChildCount) {
+        if (recyclerView.getChildCount() == 0) {
+            return false;
+        }
+
+        View firstChild = recyclerView.getChildAt(0);
+        if (!(firstChild instanceof ViewGroup)) {
+            return false;
+        }
+
+        ViewGroup PlaybackSpeedParentView = (ViewGroup) firstChild;
+        if (PlaybackSpeedParentView.getChildCount() != expectedChildCount) {
+            return false;
+        }
+
+        ViewParent parentView3rd = Utils.getParentView(recyclerView, 3);
+        if (!(parentView3rd instanceof ViewGroup)) {
+            return true;
+        }
+
+        ViewParent parentView4th = parentView3rd.getParent();
+        if (!(parentView4th instanceof ViewGroup)) {
+            return true;
+        }
+
+        // Dismiss View [R.id.touch_outside] is the 1st ChildView of the 4th ParentView.
+        // This only shows in phone layout.
+        final var touchInsidedView = ((ViewGroup) parentView4th).getChildAt(0);
+        touchInsidedView.setSoundEffectsEnabled(false);
+        touchInsidedView.performClick();
+
+        // In tablet layout there is no Dismiss View, instead we just hide all two parent views.
+        ((ViewGroup) parentView3rd).setVisibility(View.GONE);
+        ((ViewGroup) parentView4th).setVisibility(View.GONE);
+
+        // Close the litho speed menu and show the old one.
+        showOldPlaybackSpeedMenu();
+
+        return true;
+    }
+
+    public static void showOldPlaybackSpeedMenu() {
+        // This method is sometimes used multiple times.
+        // To prevent this, ignore method reuse within 1 second.
+        final long now = System.currentTimeMillis();
+        if (now - lastTimeOldPlaybackMenuInvoked < 1000) {
+            Logger.printDebug(() -> "Ignoring call to showOldPlaybackSpeedMenu");
+            return;
+        }
+        lastTimeOldPlaybackMenuInvoked = now;
+        Logger.printDebug(() -> "Old video quality menu shown");
+
+        // Rest of the implementation added by patch.
+    }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java
new file mode 100644
index 000000000..87237ae6f
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch.java
@@ -0,0 +1,70 @@
+package app.revanced.extension.youtube.patches.playback.speed;
+
+import static app.revanced.extension.shared.StringRef.str;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.youtube.patches.VideoInformation;
+import app.revanced.extension.youtube.settings.Settings;
+
+@SuppressWarnings("unused")
+public final class RememberPlaybackSpeedPatch {
+
+    private static final long TOAST_DELAY_MILLISECONDS = 750;
+
+    private static long lastTimeSpeedChanged;
+
+    /**
+     * Injection point.
+     */
+    public static void newVideoStarted(VideoInformation.PlaybackController ignoredPlayerController) {
+        Logger.printDebug(() -> "newVideoStarted");
+        VideoInformation.overridePlaybackSpeed(Settings.PLAYBACK_SPEED_DEFAULT.get());
+    }
+
+    /**
+     * Injection point.
+     * Called when user selects a playback speed.
+     *
+     * @param playbackSpeed The playback speed the user selected
+     */
+    public static void userSelectedPlaybackSpeed(float playbackSpeed) {
+        if (Settings.REMEMBER_PLAYBACK_SPEED_LAST_SELECTED.get()) {
+            // With the 0.05x menu, if the speed is set by integrations to higher than 2.0x
+            // then the menu will allow increasing without bounds but the max speed is
+            // still capped to under 8.0x.
+            playbackSpeed = Math.min(playbackSpeed, CustomPlaybackSpeedPatch.MAXIMUM_PLAYBACK_SPEED - 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.PLAYBACK_SPEED_DEFAULT.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.PLAYBACK_SPEED_DEFAULT.save(finalPlaybackSpeed);
+
+                Utils.showToastLong(str("revanced_remember_playback_speed_toast", (finalPlaybackSpeed + "x")));
+            }, TOAST_DELAY_MILLISECONDS);
+        }
+    }
+
+    /**
+     * Injection point.
+     * Overrides the video speed.  Called after video loads, and immediately after user selects a different playback speed
+     */
+    public static float getPlaybackSpeedOverride() {
+        return VideoInformation.getPlaybackSpeed();
+    }
+
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/ClientType.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/ClientType.java
new file mode 100644
index 000000000..de6a2a12c
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/ClientType.java
@@ -0,0 +1,79 @@
+package app.revanced.extension.youtube.patches.spoof;
+
+import static app.revanced.extension.youtube.patches.spoof.DeviceHardwareSupport.allowAV1;
+import static app.revanced.extension.youtube.patches.spoof.DeviceHardwareSupport.allowVP9;
+
+import android.os.Build;
+
+import androidx.annotation.Nullable;
+
+public enum ClientType {
+    // https://dumps.tadiphone.dev/dumps/oculus/eureka
+    IOS(5,
+            // iPhone 15 supports AV1 hardware decoding.
+            // Only use if this Android device also has hardware decoding.
+            allowAV1()
+                    ? "iPhone16,2"  // 15 Pro Max
+                    : "iPhone11,4", // XS Max
+            // iOS 14+ forces VP9.
+            allowVP9()
+                    ? "17.5.1.21F90"
+                    : "13.7.17H35",
+            allowVP9()
+                    ? "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 17_5_1 like Mac OS X)"
+                    : "com.google.ios.youtube/19.10.7 (iPhone; U; CPU iOS 13_7 like Mac OS X)",
+            null,
+            // Version number should be a valid iOS release.
+            // https://www.ipa4fun.com/history/185230
+            "19.10.7"
+    ),
+    ANDROID_VR(28,
+            "Quest 3",
+            "12",
+            "com.google.android.apps.youtube.vr.oculus/1.56.21 (Linux; U; Android 12; GB) gzip",
+            "32", // Android 12.1
+            "1.56.21"
+    );
+
+    /**
+     * YouTube
+     * client type
+     */
+    public final int id;
+
+    /**
+     * Device model, equivalent to {@link Build#MODEL} (System property: ro.product.model)
+     */
+    public final String model;
+
+    /**
+     * Device OS version.
+     */
+    public final String osVersion;
+
+    /**
+     * Player user-agent.
+     */
+    public final String userAgent;
+
+    /**
+     * Android SDK version, equivalent to {@link Build.VERSION#SDK} (System property: ro.build.version.sdk)
+     * Field is null if not applicable.
+     */
+    @Nullable
+    public final String androidSdkVersion;
+
+    /**
+     * App version.
+     */
+    public final String appVersion;
+
+    ClientType(int id, String model, String osVersion, String userAgent, @Nullable String androidSdkVersion, String appVersion) {
+        this.id = id;
+        this.model = model;
+        this.osVersion = osVersion;
+        this.userAgent = userAgent;
+        this.androidSdkVersion = androidSdkVersion;
+        this.appVersion = appVersion;
+    }
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/DeviceHardwareSupport.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/DeviceHardwareSupport.java
new file mode 100644
index 000000000..3adc6befb
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/DeviceHardwareSupport.java
@@ -0,0 +1,53 @@
+package app.revanced.extension.youtube.patches.spoof;
+
+import android.media.MediaCodecInfo;
+import android.media.MediaCodecList;
+import android.os.Build;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.youtube.settings.Settings;
+
+public class DeviceHardwareSupport {
+    public static final boolean DEVICE_HAS_HARDWARE_DECODING_VP9;
+    public 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 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
+
+        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_VIDEO_STREAMS_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/spoof/SpoofAppVersionPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofAppVersionPatch.java
new file mode 100644
index 000000000..25ad35d64
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofAppVersionPatch.java
@@ -0,0 +1,23 @@
+package app.revanced.extension.youtube.patches.spoof;
+
+import app.revanced.extension.youtube.settings.Settings;
+
+@SuppressWarnings("unused")
+public class SpoofAppVersionPatch {
+
+    private static final boolean SPOOF_APP_VERSION_ENABLED = Settings.SPOOF_APP_VERSION.get();
+    private static final String SPOOF_APP_VERSION_TARGET = Settings.SPOOF_APP_VERSION_TARGET.get();
+
+    /**
+     * Injection point
+     */
+    public static String getYouTubeVersionOverride(String version) {
+        if (SPOOF_APP_VERSION_ENABLED) return SPOOF_APP_VERSION_TARGET;
+        return version;
+    }
+
+    public static boolean isSpoofingToLessThan(String version) {
+        return SPOOF_APP_VERSION_ENABLED && SPOOF_APP_VERSION_TARGET.compareTo(version) < 0;
+    }
+
+}
diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofDeviceDimensionsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofDeviceDimensionsPatch.java
new file mode 100644
index 000000000..6df52a4a3
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofDeviceDimensionsPatch.java
@@ -0,0 +1,17 @@
+package app.revanced.extension.youtube.patches.spoof;
+
+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/spoof/SpoofVideoStreamsPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java
new file mode 100644
index 000000000..b50e3f4ff
--- /dev/null
+++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch.java
@@ -0,0 +1,168 @@
+package app.revanced.extension.youtube.patches.spoof;
+
+import android.net.Uri;
+
+import androidx.annotation.Nullable;
+
+import java.nio.ByteBuffer;
+import java.util.Map;
+import java.util.Objects;
+
+import app.revanced.extension.shared.Logger;
+import app.revanced.extension.shared.Utils;
+import app.revanced.extension.shared.settings.BaseSettings;
+import app.revanced.extension.shared.settings.Setting;
+import app.revanced.extension.youtube.patches.spoof.requests.StreamingDataRequest;
+import app.revanced.extension.youtube.settings.Settings;
+
+@SuppressWarnings("unused")
+public class SpoofVideoStreamsPatch {
+    public static final class ForceiOSAVCAvailability implements Setting.Availability {
+        @Override
+        public boolean isAvailable() {
+            return Settings.SPOOF_VIDEO_STREAMS.get() && Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get() == ClientType.IOS;
+        }
+    }
+
+    private static final boolean SPOOF_STREAMING_DATA = Settings.SPOOF_VIDEO_STREAMS.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 (BaseSettings.DEBUG.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; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/requests/PlayerRoutes.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/requests/PlayerRoutes.java new file mode 100644 index 000000000..364dc173a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/requests/PlayerRoutes.java @@ -0,0 +1,74 @@ +package app.revanced.extension.youtube.patches.spoof.requests; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.youtube.patches.spoof.ClientType; +import app.revanced.extension.youtube.requests.Requester; +import app.revanced.extension.youtube.requests.Route; + +final class PlayerRoutes { + private static final String YT_API_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(); + + /** + * TCP connection and HTTP read timeout + */ + private static final int CONNECTION_TIMEOUT_MILLISECONDS = 10 * 1000; // 10 Seconds. + + private PlayerRoutes() { + } + + static String createInnertubeBody(ClientType clientType) { + JSONObject innerTubeBody = new JSONObject(); + + try { + JSONObject context = new JSONObject(); + + JSONObject client = new JSONObject(); + client.put("clientName", clientType.name()); + client.put("clientVersion", clientType.appVersion); + client.put("deviceModel", clientType.model); + client.put("osVersion", clientType.osVersion); + if (clientType.androidSdkVersion != null) { + client.put("androidSdkVersion", clientType.androidSdkVersion); + } + + context.put("client", client); + + innerTubeBody.put("context", context); + innerTubeBody.put("contentCheckOk", true); + innerTubeBody.put("racyCheckOk", true); + innerTubeBody.put("videoId", "%s"); + } 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(YT_API_URL, route); + + connection.setRequestProperty("Content-Type", "application/json"); + connection.setRequestProperty("User-Agent", clientType.userAgent); + + connection.setUseCaches(false); + connection.setDoOutput(true); + + connection.setConnectTimeout(CONNECTION_TIMEOUT_MILLISECONDS); + connection.setReadTimeout(CONNECTION_TIMEOUT_MILLISECONDS); + return connection; + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/requests/StreamingDataRequest.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/requests/StreamingDataRequest.java new file mode 100644 index 000000000..e66f4d885 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/spoof/requests/StreamingDataRequest.java @@ -0,0 +1,223 @@ +package app.revanced.extension.youtube.patches.spoof.requests; + +import static app.revanced.extension.youtube.patches.spoof.requests.PlayerRoutes.GET_STREAMING_DATA; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.io.BufferedInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.*; +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.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.youtube.patches.spoof.ClientType; +import app.revanced.extension.youtube.settings.Settings; + +/** + * Video streaming data. Fetching is tied to the behavior YT uses, + * where this class fetches the streams only when YT fetches. + * + * Effectively the cache expiration of these fetches is the same as the stock app, + * since the stock app would not use expired streams and therefor + * the extension replace stream hook is called only if YT + * did use its own client streams. + */ +public class StreamingDataRequest { + + private static final ClientType[] CLIENT_ORDER_TO_USE; + + static { + ClientType[] allClientTypes = ClientType.values(); + ClientType preferredClient = Settings.SPOOF_VIDEO_STREAMS_CLIENT_TYPE.get(); + + CLIENT_ORDER_TO_USE = new ClientType[allClientTypes.length]; + CLIENT_ORDER_TO_USE[0] = preferredClient; + + int i = 1; + for (ClientType c : allClientTypes) { + if (c != preferredClient) { + CLIENT_ORDER_TO_USE[i++] = c; + } + } + } + + private static final String[] REQUEST_HEADER_KEYS = { + "Authorization", // Available only to logged in users. + "X-GOOG-API-FORMAT-VERSION", + "X-Goog-Visitor-Id" + }; + + /** + * TCP connection and HTTP read timeout. + */ + private static final int HTTP_TIMEOUT_MILLISECONDS = 10 * 1000; + + /** + * Any arbitrarily large value, but must be at least twice {@link #HTTP_TIMEOUT_MILLISECONDS} + */ + private static final int MAX_MILLISECONDS_TO_WAIT_FOR_FETCH = 20 * 1000; + + private static final Map cache = Collections.synchronizedMap( + new LinkedHashMap<>(100) { + /** + * Cache limit must be greater than the maximum number of videos open at once, + * which theoretically is more than 4 (3 Shorts + one regular minimized video). + * But instead use a much larger value, to handle if a video viewed a while ago + * is somehow still referenced. Each stream is a small array of Strings + * so memory usage is not a concern. + */ + private static final int CACHE_LIMIT = 50; + + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > CACHE_LIMIT; // Evict the oldest entry if over the cache limit. + } + }); + + public static void fetchRequest(String videoId, Map fetchHeaders) { + // Always fetch, even if there is a existing request for the same video. + cache.put(videoId, new StreamingDataRequest(videoId, fetchHeaders)); + } + + @Nullable + public static StreamingDataRequest getRequestForVideoId(String videoId) { + return cache.get(videoId); + } + + private static void handleConnectionError(String toastMessage, @Nullable Exception ex, boolean showToast) { + if (showToast) Utils.showToastShort(toastMessage); + Logger.printInfo(() -> toastMessage, ex); + } + + @Nullable + private static HttpURLConnection send(ClientType clientType, String videoId, + Map playerHeaders, + boolean showErrorToasts) { + 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); + connection.setConnectTimeout(HTTP_TIMEOUT_MILLISECONDS); + connection.setReadTimeout(HTTP_TIMEOUT_MILLISECONDS); + + for (String key : REQUEST_HEADER_KEYS) { + String value = playerHeaders.get(key); + if (value != null) { + connection.setRequestProperty(key, value); + } + } + + String innerTubeBody = String.format(PlayerRoutes.createInnertubeBody(clientType), videoId); + byte[] requestBody = innerTubeBody.getBytes(StandardCharsets.UTF_8); + connection.setFixedLengthStreamingMode(requestBody.length); + connection.getOutputStream().write(requestBody); + + final int responseCode = connection.getResponseCode(); + if (responseCode == 200) return connection; + + handleConnectionError(clientTypeName + " not available with response code: " + + responseCode + " message: " + connection.getResponseMessage(), + null, showErrorToasts); + } catch (SocketTimeoutException ex) { + handleConnectionError("Connection timeout", ex, showErrorToasts); + } catch (IOException ex) { + handleConnectionError("Network error", ex, showErrorToasts); + } catch (Exception ex) { + Logger.printException(() -> "send failed", ex); + } finally { + Logger.printDebug(() -> "video: " + videoId + " took: " + (System.currentTimeMillis() - startTime) + "ms"); + } + + return null; + } + + private static ByteBuffer fetch(String videoId, Map playerHeaders) { + final boolean debugEnabled = BaseSettings.DEBUG.get(); + + // Retry with different client if empty response body is received. + int i = 0; + for (ClientType clientType : CLIENT_ORDER_TO_USE) { + // Show an error if the last client type fails, or if the debug is enabled then show for all attempts. + final boolean showErrorToast = (++i == CLIENT_ORDER_TO_USE.length) || debugEnabled; + + HttpURLConnection connection = send(clientType, videoId, playerHeaders, showErrorToast); + if (connection != null) { + try { + // gzip encoding doesn't response with content length (-1), + // but empty response body does. + if (connection.getContentLength() != 0) { + try (InputStream inputStream = new BufferedInputStream(connection.getInputStream())) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + byte[] buffer = new byte[2048]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) >= 0) { + baos.write(buffer, 0, bytesRead); + } + + 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, debugEnabled); + 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 + '\'' + '}'; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/theme/ProgressBarDrawable.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/theme/ProgressBarDrawable.java new file mode 100644 index 000000000..bf0284f79 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/theme/ProgressBarDrawable.java @@ -0,0 +1,48 @@ +package app.revanced.extension.youtube.patches.theme; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.drawable.Drawable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import app.revanced.extension.youtube.patches.HideSeekbarPatch; +import app.revanced.extension.youtube.settings.Settings; + +/** + * Used by {@link SeekbarColorPatch} change the color of the seekbar. + * and {@link HideSeekbarPatch} to hide the seekbar of the feed and watch history. + */ +@SuppressWarnings("unused") +public class ProgressBarDrawable extends Drawable { + + private final Paint paint = new Paint(); + + @Override + public void draw(@NonNull Canvas canvas) { + if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) { + return; + } + paint.setColor(SeekbarColorPatch.getSeekbarColor()); + canvas.drawRect(getBounds(), paint); + } + + @Override + public void setAlpha(int alpha) { + paint.setAlpha(alpha); + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + paint.setColorFilter(colorFilter); + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java new file mode 100644 index 000000000..aea5c227c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/theme/SeekbarColorPatch.java @@ -0,0 +1,192 @@ +package app.revanced.extension.youtube.patches.theme; + +import static app.revanced.extension.shared.StringRef.str; + +import android.graphics.Color; + +import java.util.Arrays; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class SeekbarColorPatch { + + private static final boolean SEEKBAR_CUSTOM_COLOR_ENABLED = Settings.SEEKBAR_CUSTOM_COLOR.get(); + + /** + * Default color of the seekbar. + */ + private static final int ORIGINAL_SEEKBAR_COLOR = 0xFFFF0000; + + /** + * Default colors of the gradient seekbar. + */ + private static final int[] ORIGINAL_SEEKBAR_GRADIENT_COLORS = { 0xFFFF0033, 0xFFFF2791 }; + + /** + * Default positions of the gradient seekbar. + */ + private static final float[] ORIGINAL_SEEKBAR_GRADIENT_POSITIONS = { 0.8f, 1.0f }; + + /** + * Default YouTube seekbar color brightness. + */ + private static final float ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS; + + /** + * If {@link Settings#SEEKBAR_CUSTOM_COLOR} is enabled, + * this is the color value of {@link Settings#SEEKBAR_CUSTOM_COLOR_VALUE}. + * Otherwise this is {@link #ORIGINAL_SEEKBAR_COLOR}. + */ + private static int seekbarColor = ORIGINAL_SEEKBAR_COLOR; + + /** + * Custom seekbar hue, saturation, and brightness values. + */ + private static final float[] customSeekbarColorHSV = new float[3]; + + static { + float[] hsv = new float[3]; + Color.colorToHSV(ORIGINAL_SEEKBAR_COLOR, hsv); + ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS = hsv[2]; + + if (SEEKBAR_CUSTOM_COLOR_ENABLED) { + loadCustomSeekbarColor(); + } + } + + private static void loadCustomSeekbarColor() { + try { + seekbarColor = Color.parseColor(Settings.SEEKBAR_CUSTOM_COLOR_VALUE.get()); + Color.colorToHSV(seekbarColor, customSeekbarColorHSV); + } catch (Exception ex) { + Utils.showToastShort(str("revanced_seekbar_custom_color_invalid")); + Settings.SEEKBAR_CUSTOM_COLOR_VALUE.resetToDefault(); + loadCustomSeekbarColor(); + } + } + + public static int getSeekbarColor() { + return seekbarColor; + } + + public static boolean playerSeekbarGradientEnabled(boolean original) { + if (SEEKBAR_CUSTOM_COLOR_ENABLED) return false; + + return original; + } + + /** + * Injection point. + * + * Overrides all Litho components that use the YouTube seekbar color. + * Used only for the video thumbnails seekbar. + * + * If {@link Settings#HIDE_SEEKBAR_THUMBNAIL} is enabled, this returns a fully transparent color. + */ + public static int getLithoColor(int colorValue) { + if (colorValue == ORIGINAL_SEEKBAR_COLOR) { + if (Settings.HIDE_SEEKBAR_THUMBNAIL.get()) { + return 0x00000000; + } + + return getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR); + } + return colorValue; + } + + /** + * Injection point. + */ + public static void setLinearGradient(int[] colors, float[] positions) { + final boolean hideSeekbar = Settings.HIDE_SEEKBAR_THUMBNAIL.get(); + + if (SEEKBAR_CUSTOM_COLOR_ENABLED || hideSeekbar) { + // Most litho usage of linear gradients is hooked here, + // so must only change if the values are those for the seekbar. + if (Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_COLORS, colors) + && Arrays.equals(ORIGINAL_SEEKBAR_GRADIENT_POSITIONS, positions)) { + Arrays.fill(colors, hideSeekbar + ? 0x00000000 + : seekbarColor); + return; + } + + Logger.printDebug(() -> "Ignoring gradient colors: " + Arrays.toString(colors) + + " positions: " + Arrays.toString(positions)); + } + } + + /** + * Injection point. + * + * Overrides color when video player seekbar is clicked. + */ + public static int getVideoPlayerSeekbarClickedColor(int colorValue) { + if (!SEEKBAR_CUSTOM_COLOR_ENABLED) { + return colorValue; + } + + return colorValue == ORIGINAL_SEEKBAR_COLOR + ? getSeekbarColorValue(ORIGINAL_SEEKBAR_COLOR) + : colorValue; + } + + /** + * Injection point. + * + * Overrides color used for the video player seekbar. + */ + public static int getVideoPlayerSeekbarColor(int originalColor) { + if (!SEEKBAR_CUSTOM_COLOR_ENABLED) { + return originalColor; + } + + return getSeekbarColorValue(originalColor); + } + + /** + * Color parameter is changed to the custom seekbar color, while retaining + * the brightness and alpha changes of the parameter value compared to the original seekbar color. + */ + private static int getSeekbarColorValue(int originalColor) { + try { + if (!SEEKBAR_CUSTOM_COLOR_ENABLED || originalColor == seekbarColor) { + return originalColor; // nothing to do + } + + final int alphaDifference = Color.alpha(originalColor) - Color.alpha(ORIGINAL_SEEKBAR_COLOR); + + // The seekbar uses the same color but different brightness for different situations. + float[] hsv = new float[3]; + Color.colorToHSV(originalColor, hsv); + final float brightnessDifference = hsv[2] - ORIGINAL_SEEKBAR_COLOR_BRIGHTNESS; + + // Apply the brightness difference to the custom seekbar color. + hsv[0] = customSeekbarColorHSV[0]; + hsv[1] = customSeekbarColorHSV[1]; + hsv[2] = clamp(customSeekbarColorHSV[2] + brightnessDifference, 0, 1); + + final int replacementAlpha = clamp(Color.alpha(seekbarColor) + alphaDifference, 0, 255); + final int replacementColor = Color.HSVToColor(replacementAlpha, hsv); + Logger.printDebug(() -> String.format("Original color: #%08X replacement color: #%08X", + originalColor, replacementColor)); + return replacementColor; + } catch (Exception ex) { + Logger.printException(() -> "getSeekbarColorValue failure", ex); + return originalColor; + } + } + + /** @noinspection SameParameterValue */ + private static int clamp(int value, int lower, int upper) { + return Math.max(lower, Math.min(value, upper)); + } + + /** @noinspection SameParameterValue */ + private static float clamp(float value, float lower, float upper) { + return Math.max(lower, Math.min(value, upper)); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/theme/ThemePatch.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/theme/ThemePatch.java new file mode 100644 index 000000000..0f8fb3304 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/patches/theme/ThemePatch.java @@ -0,0 +1,61 @@ +package app.revanced.extension.youtube.patches.theme; + +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.ThemeHelper; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class ThemePatch { + // color constants used in relation with litho components + 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, // explore drawer 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 final int WHITE_COLOR = Utils.getResourceColor("yt_white1"); + private static final int BLACK_COLOR = Utils.getResourceColor("yt_black1"); + + private static final boolean GRADIENT_LOADING_SCREEN_ENABLED = Settings.GRADIENT_LOADING_SCREEN.get(); + + /** + * Injection point. + * + * Change the color of Litho components. + * If the color of the component matches one of the values, return the background color . + * + * @param originalValue The original color value. + * @return The new or original color value + */ + public static int getValue(int originalValue) { + if (ThemeHelper.isDarkTheme()) { + if (anyEquals(originalValue, DARK_VALUES)) return BLACK_COLOR; + } else { + if (anyEquals(originalValue, WHITE_VALUES)) return WHITE_COLOR; + } + + return originalValue; + } + + private static boolean anyEquals(int value, int... of) { + for (int v : of) if (value == v) return true; + + return false; + } + + /** + * Injection point. + */ + public static boolean gradientLoadingScreenEnabled() { + return GRADIENT_LOADING_SCREEN_ENABLED; + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/requests/Requester.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/requests/Requester.java new file mode 100644 index 000000000..69d43a4be --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/requests/Requester.java @@ -0,0 +1,145 @@ +package app.revanced.extension.youtube.requests; + +import app.revanced.extension.shared.Utils; +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; + +public class Requester { + private Requester() { + } + + public static HttpURLConnection getConnectionFromRoute(String apiUrl, Route route, String... params) throws IOException { + return getConnectionFromCompiledRoute(apiUrl, route.compile(params)); + } + + public static HttpURLConnection getConnectionFromCompiledRoute(String apiUrl, Route.CompiledRoute route) throws IOException { + String url = apiUrl + route.getCompiledRoute(); + HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(); + // Request data is in the URL parameters and no body is sent. + // The calling code must set a length if using a request body. + connection.setFixedLengthStreamingMode(0); + connection.setRequestMethod(route.getMethod().name()); + String agentString = System.getProperty("http.agent") + + "; ReVanced/" + Utils.getAppVersionName() + + " (" + Utils.getPatchesReleaseVersion() + ")"; + connection.setRequestProperty("User-Agent", agentString); + + return connection; + } + + /** + * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. + */ + private static String parseInputStreamAndClose(InputStream inputStream) throws IOException { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + StringBuilder jsonBuilder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + jsonBuilder.append(line); + jsonBuilder.append('\n'); + } + return jsonBuilder.toString(); + } + } + + /** + * Parse the {@link HttpURLConnection} response as a String. + * This does not close the url connection. If further requests to this host are unlikely + * in the near future, then instead use {@link #parseStringAndDisconnect(HttpURLConnection)}. + */ + public static String parseString(HttpURLConnection connection) throws IOException { + return parseInputStreamAndClose(connection.getInputStream()); + } + + /** + * Parse the {@link HttpURLConnection} response as a String, and disconnect. + * + * Should only be used if other requests to the server in the near future are unlikely + * + * @see #parseString(HttpURLConnection) + */ + public static String parseStringAndDisconnect(HttpURLConnection connection) throws IOException { + String result = parseString(connection); + connection.disconnect(); + return result; + } + + /** + * Parse the {@link HttpURLConnection} error stream as a String. + * If the server sent no error response data, this returns an empty string. + */ + public static String parseErrorString(HttpURLConnection connection) throws IOException { + InputStream errorStream = connection.getErrorStream(); + if (errorStream == null) { + return ""; + } + return parseInputStreamAndClose(errorStream); + } + + /** + * Parse the {@link HttpURLConnection} error stream as a String, and disconnect. + * If the server sent no error response data, this returns an empty string. + * + * Should only be used if other requests to the server are unlikely in the near future. + * + * @see #parseErrorString(HttpURLConnection) + */ + public static String parseErrorStringAndDisconnect(HttpURLConnection connection) throws IOException { + String result = parseErrorString(connection); + connection.disconnect(); + return result; + } + + /** + * Parse the {@link HttpURLConnection} response into a JSONObject. + * This does not close the url connection. If further requests to this host are unlikely + * in the near future, then instead use {@link #parseJSONObjectAndDisconnect(HttpURLConnection)}. + */ + public static JSONObject parseJSONObject(HttpURLConnection connection) throws JSONException, IOException { + return new JSONObject(parseString(connection)); + } + + /** + * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. + * + * Should only be used if other requests to the server in the near future are unlikely + * + * @see #parseJSONObject(HttpURLConnection) + */ + public static JSONObject parseJSONObjectAndDisconnect(HttpURLConnection connection) throws JSONException, IOException { + JSONObject object = parseJSONObject(connection); + connection.disconnect(); + return object; + } + + /** + * Parse the {@link HttpURLConnection}, and closes the underlying InputStream. + * This does not close the url connection. If further requests to this host are unlikely + * in the near future, then instead use {@link #parseJSONArrayAndDisconnect(HttpURLConnection)}. + */ + public static JSONArray parseJSONArray(HttpURLConnection connection) throws JSONException, IOException { + return new JSONArray(parseString(connection)); + } + + /** + * Parse the {@link HttpURLConnection}, close the underlying InputStream, and disconnect. + * + * Should only be used if other requests to the server in the near future are unlikely + * + * @see #parseJSONArray(HttpURLConnection) + */ + public static JSONArray parseJSONArrayAndDisconnect(HttpURLConnection connection) throws JSONException, IOException { + JSONArray array = parseJSONArray(connection); + connection.disconnect(); + return array; + } + +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/requests/Route.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/requests/Route.java new file mode 100644 index 000000000..c25d108b9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/requests/Route.java @@ -0,0 +1,66 @@ +package app.revanced.extension.youtube.requests; + +public class Route { + private final String route; + private final Method method; + private final int paramCount; + + public Route(Method method, String route) { + this.method = method; + this.route = route; + this.paramCount = countMatches(route, '{'); + + if (paramCount != countMatches(route, '}')) + throw new IllegalArgumentException("Not enough parameters"); + } + + public Method getMethod() { + return method; + } + + public CompiledRoute compile(String... params) { + if (params.length != paramCount) + throw new IllegalArgumentException("Error compiling route [" + route + "], incorrect amount of parameters provided. " + + "Expected: " + paramCount + ", provided: " + params.length); + + StringBuilder compiledRoute = new StringBuilder(route); + for (int i = 0; i < paramCount; i++) { + int paramStart = compiledRoute.indexOf("{"); + int paramEnd = compiledRoute.indexOf("}"); + compiledRoute.replace(paramStart, paramEnd + 1, params[i]); + } + return new CompiledRoute(this, compiledRoute.toString()); + } + + public static class CompiledRoute { + private final Route baseRoute; + private final String compiledRoute; + + private CompiledRoute(Route baseRoute, String compiledRoute) { + this.baseRoute = baseRoute; + this.compiledRoute = compiledRoute; + } + + public String getCompiledRoute() { + return compiledRoute; + } + + public Method getMethod() { + return baseRoute.method; + } + } + + private int countMatches(CharSequence seq, char c) { + int count = 0; + for (int i = 0; i < seq.length(); i++) { + if (seq.charAt(i) == c) + count++; + } + return count; + } + + public enum Method { + GET, + POST + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java new file mode 100644 index 000000000..02893537c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/ReturnYouTubeDislike.java @@ -0,0 +1,730 @@ +package app.revanced.extension.youtube.returnyoutubedislike; + +import static app.revanced.extension.shared.StringRef.str; + +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.os.Build; +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.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.*; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.ThemeHelper; +import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch; +import app.revanced.extension.youtube.returnyoutubedislike.requests.RYDVoteData; +import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; + +/** + * 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 { + + public enum Vote { + LIKE(1), + DISLIKE(-1), + LIKE_REMOVE(0); + + public final int value; + + Vote(int value) { + this.value = value; + } + } + + /** + * 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: + * https://developer.android.com/topic/performance/vitals/anr + */ + 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' + + private static final boolean IS_SPOOFING_TO_OLD_SEPARATOR_COLOR + = SpoofAppVersionPatch.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; + + static { + DisplayMetrics dp = Objects.requireNonNull(Utils.getContext()).getResources().getDisplayMetrics(); + + leftSeparatorBounds = new Rect(0, 0, + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1.2f, dp), + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14, dp)); + final int middleSeparatorSize = + (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3.7f, dp); + 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); + } + + 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 ThemeHelper.isDarkTheme() + ? 0x29AAAAAA // transparent dark gray + : 0xFFD9D9D9; // light gray + } + return ThemeHelper.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)) { + // 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()); + } + + 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) { + return (SpannableString) sourceStyle; // 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize + if (dislikeCountFormatter == null) { + Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().locale; + 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + synchronized (ReturnYouTubeDislike.class) { // number formatter is not thread safe, must synchronize + if (dislikePercentageFormatter == null) { + Locale locale = Objects.requireNonNull(Utils.getContext()).getResources().getConfiguration().locale; + dislikePercentageFormatter = NumberFormat.getPercentInstance(locale); + + // Want to set the digit strings, and the simplest way is to cast to the implementation NumberFormat returns. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P + && dislikePercentageFormatter instanceof DecimalFormat) { + DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale); + symbols.setDigitStrings(DecimalFormatSymbols.getInstance(Locale.ENGLISH).getDigitStrings()); + ((DecimalFormat) dislikePercentageFormatter).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. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + final long now = System.currentTimeMillis(); + fetchCache.values().removeIf(value -> { + final boolean expired = value.isExpired(now); + if (expired) + Logger.printDebug(() -> "Removing expired fetch: " + value.videoId); + return expired; + }); + } + + 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; + } + + @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; + } + + 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 { + PlayerType currentType = PlayerType.getCurrent(); + if (isShort != currentType.isNoneHiddenOrMinimized()) { + Logger.printDebug(() -> "Cannot vote for video: " + videoId + + " as current player type does not match: " + currentType); + + // 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(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/returnyoutubedislike/requests/RYDVoteData.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/RYDVoteData.java new file mode 100644 index 000000000..b57eadcfd --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/RYDVoteData.java @@ -0,0 +1,179 @@ +package app.revanced.extension.youtube.returnyoutubedislike.requests; + +import static app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike.Vote; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.json.JSONException; +import org.json.JSONObject; + +import app.revanced.extension.shared.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. + */ +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/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java new file mode 100644 index 000000000..a933f0828 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeApi.java @@ -0,0 +1,610 @@ +package app.revanced.extension.youtube.returnyoutubedislike.requests; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeRoutes.getRYDConnectionFromRoute; + +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.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.requests.Requester; +import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike; +import app.revanced.extension.youtube.settings.Settings; + +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; + + /** + * 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; + + /** + * Number of times {@link #HTTP_STATUS_CODE_RATE_LIMIT} was requested by RYD api. + * Does not include network calls attempted while rate limit is in effect, + * and does not include rate limit imposed if a fetch fails. + */ + private static volatile int numberOfRateLimitRequestsEncountered; + + /** + * Number of network calls made in {@link #fetchVotes(String)} + */ + private static volatile int fetchCallCount; + + /** + * Number of times {@link #fetchVotes(String)} failed due to timeout or any other error. + * This does not include when rate limit requests are encountered. + */ + private static volatile int fetchCallNumberOfFailures; + + /** + * Total time spent waiting for {@link #fetchVotes(String)} network call to complete. + * Value does does not persist on app shut down. + */ + private static volatile long fetchCallResponseTimeTotal; + + /** + * Round trip network time for the most recent call to {@link #fetchVotes(String)} + */ + private static volatile long fetchCallResponseTimeLast; + private static volatile long fetchCallResponseTimeMin; + private static volatile long fetchCallResponseTimeMax; + + public static final int FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT = -1; + + /** + * If rate limit was hit, this returns {@link #FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT} + */ + public static long getFetchCallResponseTimeLast() { + return fetchCallResponseTimeLast; + } + public static long getFetchCallResponseTimeMin() { + return fetchCallResponseTimeMin; + } + public static long getFetchCallResponseTimeMax() { + return fetchCallResponseTimeMax; + } + public static long getFetchCallResponseTimeAverage() { + return fetchCallCount == 0 ? 0 : (fetchCallResponseTimeTotal / fetchCallCount); + } + public static int getFetchCallCount() { + return fetchCallCount; + } + public static int getFetchCallNumberOfFailures() { + return fetchCallNumberOfFailures; + } + public static int getNumberOfRateLimitRequestsEncountered() { + return numberOfRateLimitRequestsEncountered; + } + + private ReturnYouTubeDislikeApi() { + } // utility class + + /** + * Simulates a slow response by doing meaningless calculations. + * Used to debug the app UI and verify UI timeout logic works + */ + private static void randomlyWaitIfLocallyDebugging() { + final boolean DEBUG_RANDOMLY_DELAY_NETWORK_CALLS = false; // set true to debug UI + if (DEBUG_RANDOMLY_DELAY_NETWORK_CALLS) { + final long amountOfTimeToWaste = (long) (Math.random() + * (API_GET_VOTES_TCP_TIMEOUT_MILLISECONDS + API_GET_VOTES_HTTP_TIMEOUT_MILLISECONDS)); + Utils.doNothingForDuration(amountOfTimeToWaste); + } + } + + /** + * 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) { + final boolean DEBUG_RATE_LIMIT = false; // set to true, to verify rate limit works + if (DEBUG_RATE_LIMIT) { + final double RANDOM_RATE_LIMIT_PERCENTAGE = 0.2; // 20% chance of a triggering a rate limit + if (Math.random() < RANDOM_RATE_LIMIT_PERCENTAGE) { + Logger.printDebug(() -> "Artificially triggering rate limit for debug purposes"); + httpResponseCode = HTTP_STATUS_CODE_RATE_LIMIT; + } + } + return httpResponseCode == HTTP_STATUS_CODE_RATE_LIMIT; + } + + @SuppressWarnings("NonAtomicOperationOnVolatileField") // Don't care, fields are only estimates. + private static void updateRateLimitAndStats(long timeNetworkCallStarted, boolean connectionError, boolean rateLimitHit) { + if (connectionError && rateLimitHit) { + throw new IllegalArgumentException(); + } + final long responseTimeOfFetchCall = System.currentTimeMillis() - timeNetworkCallStarted; + fetchCallResponseTimeTotal += responseTimeOfFetchCall; + fetchCallResponseTimeMin = (fetchCallResponseTimeMin == 0) ? responseTimeOfFetchCall : Math.min(responseTimeOfFetchCall, fetchCallResponseTimeMin); + fetchCallResponseTimeMax = Math.max(responseTimeOfFetchCall, fetchCallResponseTimeMax); + fetchCallCount++; + if (connectionError) { + timeToResumeAPICalls = System.currentTimeMillis() + BACKOFF_CONNECTION_ERROR_MILLISECONDS; + fetchCallResponseTimeLast = responseTimeOfFetchCall; + fetchCallNumberOfFailures++; + 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; + numberOfRateLimitRequestsEncountered++; + fetchCallResponseTimeLast = FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT; + if (!lastApiCallFailed && Settings.RYD_TOAST_ON_CONNECTION_ERROR.get()) { + Utils.showToastLong(str("revanced_ryd_failure_client_rate_limit_requested")); + } + lastApiCallFailed = true; + } else { + fetchCallResponseTimeLast = responseTimeOfFetchCall; + lastApiCallFailed = false; + } + } + + private static void handleConnectionError(@NonNull String toastMessage, + @Nullable Exception ex, + boolean showLongToast) { + if (!lastApiCallFailed && Settings.RYD_TOAST_ON_CONNECTION_ERROR.get()) { + if (showLongToast) { + Utils.showToastLong(toastMessage); + } else { + Utils.showToastShort(toastMessage); + } + } + lastApiCallFailed = true; + + 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); + final long timeNetworkCallStarted = System.currentTimeMillis(); + + 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 + + randomlyWaitIfLocallyDebugging(); + + final int responseCode = connection.getResponseCode(); + if (checkIfRateLimitWasHit(responseCode)) { + connection.disconnect(); // rate limit hit, should disconnect + updateRateLimitAndStats(timeNetworkCallStarted, 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(timeNetworkCallStarted, 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 { + // Unexpected response code. Most likely RYD is temporarily broken. + handleConnectionError(str("revanced_ryd_failure_connection_status_code", responseCode), + null, true); + } + connection.disconnect(); // Something went wrong, might as well disconnect. + } catch (SocketTimeoutException ex) { + handleConnectionError((str("revanced_ryd_failure_connection_timeout")), ex, false); + } catch (IOException ex) { + handleConnectionError((str("revanced_ryd_failure_generic", ex.getMessage())), ex, true); + } catch (Exception ex) { + // should never happen + Logger.printException(() -> "fetchVotes failure", ex); + } + + updateRateLimitAndStats(timeNetworkCallStarted, 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(36); + 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, true); + connection.disconnect(); + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); + } catch (IOException ex) { + handleConnectionError(str("revanced_ryd_failure_generic", "registration failed"), ex, true); + } 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, true); + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); + } catch (IOException ex) { + handleConnectionError(str("revanced_ryd_failure_generic", "confirm registration failed"), + ex, true); + } catch (Exception ex) { + Logger.printException(() -> "Failed to confirm registration for user: " + userId + + "solution: " + solution, ex); + } + return 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 = registerAsNewUser(); + if (userId != null) { + Settings.RYD_USER_ID.save(userId); + } + return userId; + } + + public static boolean sendVote(String videoId, ReturnYouTubeDislike.Vote vote) { + Utils.verifyOffMainThread(); + Objects.requireNonNull(videoId); + Objects.requireNonNull(vote); + + try { + String userId = getUserId(); + if (userId == null) return false; + + if (checkIfRateLimitInEffect("sendVote")) { + return false; + } + 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 false; + } + 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 confirmVote(videoId, userId, solution); + } + + 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, true); + connection.disconnect(); // something went wrong, might as well disconnect + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); + } catch (IOException ex) { + handleConnectionError(str("revanced_ryd_failure_generic", "send vote failed"), ex, true); + } catch (Exception ex) { + // should never happen + Logger.printException(() -> "Failed to send vote for video: " + videoId + " vote: " + vote, ex); + } + return false; + } + + private static boolean confirmVote(String videoId, String userId, String solution) { + Utils.verifyOffMainThread(); + Objects.requireNonNull(videoId); + Objects.requireNonNull(userId); + Objects.requireNonNull(solution); + + try { + if (checkIfRateLimitInEffect("confirmVote")) { + return false; + } + 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 false; + } + if (responseCode == HTTP_STATUS_CODE_SUCCESS) { + Logger.printDebug(() -> "Vote confirm successful for video: " + videoId); + return true; + } + + // 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, true); + } catch (SocketTimeoutException ex) { + handleConnectionError(str("revanced_ryd_failure_connection_timeout"), ex, false); + } catch (IOException ex) { + handleConnectionError(str("revanced_ryd_failure_generic", "confirm vote failed"), + ex, true); + } catch (Exception ex) { + Logger.printException(() -> "Failed to confirm vote for video: " + videoId + + " solution: " + solution, ex); // should never happen + } + return false; + } + + 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) { + final long timeSolveStarted = System.currentTimeMillis(); + 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) { + String solution = Base64.encodeToString(new byte[]{buffer[0], buffer[1], buffer[2], buffer[3]}, Base64.NO_WRAP); + Logger.printDebug(() -> "Found puzzle solution: " + solution + " of difficulty: " + difficulty + + " in: " + (System.currentTimeMillis() - timeSolveStarted) + " ms"); + return solution; + } + } + + // should never be reached + throw new IllegalStateException("Failed to solve puzzle challenge: " + challenge + " difficulty: " + difficulty); + } + + // https://stackoverflow.com/a/157202 + private static String randomString(int len) { + String AB = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + SecureRandom rnd = new SecureRandom(); + + StringBuilder sb = new StringBuilder(len); + for (int i = 0; i < len; i++) + sb.append(AB.charAt(rnd.nextInt(AB.length()))); + return sb.toString(); + } + + private static int countLeadingZeroes(byte[] uInt8View) { + int zeroes = 0; + for (byte b : uInt8View) { + int 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/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java new file mode 100644 index 000000000..2c2ae7255 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/returnyoutubedislike/requests/ReturnYouTubeDislikeRoutes.java @@ -0,0 +1,28 @@ +package app.revanced.extension.youtube.returnyoutubedislike.requests; + +import static app.revanced.extension.youtube.requests.Route.Method.GET; +import static app.revanced.extension.youtube.requests.Route.Method.POST; + +import java.io.IOException; +import java.net.HttpURLConnection; + +import app.revanced.extension.youtube.requests.Requester; +import app.revanced.extension.youtube.requests.Route; + +class ReturnYouTubeDislikeRoutes { + static final String RYD_API_URL = "https://returnyoutubedislikeapi.com/"; + + static final Route SEND_VOTE = new Route(POST, "interact/vote"); + static final Route CONFIRM_VOTE = new Route(POST, "interact/confirmVote"); + static final Route GET_DISLIKES = new Route(GET, "votes?videoId={video_id}"); + static final Route GET_REGISTRATION = new Route(GET, "puzzle/registration?userId={user_id}"); + static final Route CONFIRM_REGISTRATION = new Route(POST, "puzzle/registration?userId={user_id}"); + + private ReturnYouTubeDislikeRoutes() { + } + + 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/youtube/settings/LicenseActivityHook.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java new file mode 100644 index 000000000..acf565712 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/LicenseActivityHook.java @@ -0,0 +1,99 @@ +package app.revanced.extension.youtube.settings; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.preference.PreferenceFragment; +import android.view.ViewGroup; +import android.widget.ImageButton; +import android.widget.TextView; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.youtube.ThemeHelper; +import app.revanced.extension.youtube.settings.preference.ReVancedPreferenceFragment; +import app.revanced.extension.youtube.settings.preference.ReturnYouTubeDislikePreferenceFragment; +import app.revanced.extension.youtube.settings.preference.SponsorBlockPreferenceFragment; + +import java.util.Objects; + +import static app.revanced.extension.shared.Utils.getChildView; +import static app.revanced.extension.shared.Utils.getResourceIdentifier; + +/** + * Hooks LicenseActivity. + *

+ * This class is responsible for injecting our own fragment by replacing the LicenseActivity. + */ +@SuppressWarnings("unused") +public class LicenseActivityHook { + + /** + * Injection point. + *

+ * Hooks LicenseActivity#onCreate in order to inject our own fragment. + */ + public static void initialize(Activity licenseActivity) { + try { + ThemeHelper.setActivityTheme(licenseActivity); + licenseActivity.setContentView( + getResourceIdentifier("revanced_settings_with_toolbar", "layout")); + setBackButton(licenseActivity); + + PreferenceFragment fragment; + String toolbarTitleResourceName; + String dataString = licenseActivity.getIntent().getDataString(); + switch (dataString) { + case "revanced_sb_settings_intent": + toolbarTitleResourceName = "revanced_sb_settings_title"; + fragment = new SponsorBlockPreferenceFragment(); + break; + case "revanced_ryd_settings_intent": + toolbarTitleResourceName = "revanced_ryd_settings_title"; + fragment = new ReturnYouTubeDislikePreferenceFragment(); + break; + case "revanced_settings_intent": + toolbarTitleResourceName = "revanced_settings_title"; + fragment = new ReVancedPreferenceFragment(); + break; + default: + Logger.printException(() -> "Unknown setting: " + dataString); + return; + } + + setToolbarTitle(licenseActivity, toolbarTitleResourceName); + licenseActivity.getFragmentManager() + .beginTransaction() + .replace(getResourceIdentifier("revanced_settings_fragments", "id"), fragment) + .commit(); + } catch (Exception ex) { + Logger.printException(() -> "onCreate failure", ex); + } + } + + private static void setToolbarTitle(Activity activity, String toolbarTitleResourceName) { + ViewGroup toolbar = activity.findViewById(getToolbarResourceId()); + TextView toolbarTextView = Objects.requireNonNull(getChildView(toolbar, false, + view -> view instanceof TextView)); + toolbarTextView.setText(getResourceIdentifier(toolbarTitleResourceName, "string")); + } + + @SuppressLint("UseCompatLoadingForDrawables") + private static void setBackButton(Activity activity) { + ViewGroup toolbar = activity.findViewById(getToolbarResourceId()); + ImageButton imageButton = Objects.requireNonNull(getChildView(toolbar, false, + view -> view instanceof ImageButton)); + final int backButtonResource = getResourceIdentifier(ThemeHelper.isDarkTheme() + ? "yt_outline_arrow_left_white_24" + : "yt_outline_arrow_left_black_24", + "drawable"); + imageButton.setImageDrawable(activity.getResources().getDrawable(backButtonResource)); + imageButton.setOnClickListener(view -> activity.onBackPressed()); + } + + private static int getToolbarResourceId() { + final int toolbarResourceId = getResourceIdentifier("revanced_toolbar", "id"); + if (toolbarResourceId == 0) { + throw new IllegalStateException("Could not find back button resource"); + } + return toolbarResourceId; + } + +} 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..dc0d15d18 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/Settings.java @@ -0,0 +1,393 @@ +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.*; +import static app.revanced.extension.youtube.patches.ChangeStartPagePatch.StartPage; +import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerHideExpandCloseAvailability; +import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerHorizontalDragAvailability; +import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType; +import static app.revanced.extension.youtube.patches.MiniplayerPatch.MiniplayerType.*; +import static app.revanced.extension.youtube.patches.SeekbarThumbnailsPatch.SeekbarThumbnailsHighQualityAvailability; +import static app.revanced.extension.youtube.sponsorblock.objects.CategoryBehaviour.*; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.settings.*; +import app.revanced.extension.shared.settings.preference.SharedPrefCategory; +import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.DeArrowAvailability; +import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.StillImagesAvailability; +import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.ThumbnailOption; +import app.revanced.extension.youtube.patches.AlternativeThumbnailsPatch.ThumbnailStillTime; +import app.revanced.extension.youtube.patches.spoof.ClientType; +import app.revanced.extension.youtube.patches.spoof.SpoofAppVersionPatch; +import app.revanced.extension.youtube.patches.spoof.SpoofVideoStreamsPatch; +import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings; + +@SuppressWarnings("deprecation") +public class Settings extends BaseSettings { + // Video + public static final BooleanSetting RESTORE_OLD_VIDEO_QUALITY_MENU = new BooleanSetting("revanced_restore_old_video_quality_menu", TRUE); + public static final BooleanSetting REMEMBER_VIDEO_QUALITY_LAST_SELECTED = new BooleanSetting("revanced_remember_video_quality_last_selected", FALSE); + public static final IntegerSetting VIDEO_QUALITY_DEFAULT_WIFI = new IntegerSetting("revanced_video_quality_default_wifi", -2); + public static final IntegerSetting VIDEO_QUALITY_DEFAULT_MOBILE = new IntegerSetting("revanced_video_quality_default_mobile", -2); + // Speed + public static final BooleanSetting REMEMBER_PLAYBACK_SPEED_LAST_SELECTED = new BooleanSetting("revanced_remember_playback_speed_last_selected", FALSE); + public static final BooleanSetting CUSTOM_SPEED_MENU = new BooleanSetting("revanced_custom_speed_menu", TRUE); + public static final FloatSetting PLAYBACK_SPEED_DEFAULT = new FloatSetting("revanced_playback_speed_default", 1.0f); + public static final StringSetting CUSTOM_PLAYBACK_SPEEDS = new StringSetting("revanced_custom_playback_speeds", + "0.25\n0.5\n0.75\n0.9\n0.95\n1.0\n1.05\n1.1\n1.25\n1.5\n1.75\n2.0\n3.0\n4.0\n5.0", true); + + // Ads + public static final BooleanSetting HIDE_FULLSCREEN_ADS = new BooleanSetting("revanced_hide_fullscreen_ads", TRUE); + public static final BooleanSetting HIDE_BUTTONED_ADS = new BooleanSetting("revanced_hide_buttoned_ads", TRUE); + 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); + public static final BooleanSetting HIDE_HIDE_LATEST_POSTS = new BooleanSetting("revanced_hide_latest_posts_ads", TRUE); + public static final BooleanSetting HIDE_MERCHANDISE_BANNERS = new BooleanSetting("revanced_hide_merchandise_banners", TRUE); + public static final BooleanSetting HIDE_PAID_PROMOTION_LABEL = new BooleanSetting("revanced_hide_paid_promotion_label", TRUE); + public static final BooleanSetting HIDE_PRODUCTS_BANNER = new BooleanSetting("revanced_hide_products_banner", TRUE); + public static final BooleanSetting HIDE_PLAYER_STORE_SHELF = new BooleanSetting("revanced_hide_player_store_shelf", TRUE); + public static final BooleanSetting HIDE_SHOPPING_LINKS = new BooleanSetting("revanced_hide_shopping_links", TRUE); + public static final BooleanSetting HIDE_SELF_SPONSOR = new BooleanSetting("revanced_hide_self_sponsor_ads", TRUE); + public static final BooleanSetting HIDE_VIDEO_ADS = new BooleanSetting("revanced_hide_video_ads", TRUE, true); + public static final BooleanSetting HIDE_VISIT_STORE_BUTTON = new BooleanSetting("revanced_hide_visit_store_button", TRUE); + public static final BooleanSetting HIDE_WEB_SEARCH_RESULTS = new BooleanSetting("revanced_hide_web_search_results", TRUE); + + // Feed + public static final BooleanSetting HIDE_ALBUM_CARDS = new BooleanSetting("revanced_hide_album_cards", FALSE, true); + public static final BooleanSetting HIDE_ARTIST_CARDS = new BooleanSetting("revanced_hide_artist_cards", FALSE); + public static final BooleanSetting HIDE_EXPANDABLE_CHIP = new BooleanSetting("revanced_hide_expandable_chip", TRUE); + public static final BooleanSetting HIDE_DOODLES = new BooleanSetting("revanced_hide_doodles", FALSE, true, "revanced_hide_doodles_user_dialog_message"); + + // 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_subscription", 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", TRUE, 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()); + + // Hide keyword content + public static final BooleanSetting HIDE_KEYWORD_CONTENT_HOME = new BooleanSetting("revanced_hide_keyword_content_home", FALSE); + public static final BooleanSetting HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_keyword_content_subscriptions", FALSE); + public static final BooleanSetting HIDE_KEYWORD_CONTENT_SEARCH = new BooleanSetting("revanced_hide_keyword_content_search", FALSE); + public static final StringSetting HIDE_KEYWORD_CONTENT_PHRASES = new StringSetting("revanced_hide_keyword_content_phrases", "", + parentsAny(HIDE_KEYWORD_CONTENT_HOME, HIDE_KEYWORD_CONTENT_SUBSCRIPTIONS, HIDE_KEYWORD_CONTENT_SEARCH)); + + // Uncategorized layout related settings. Do not add to this section, and instead move these out and categorize them. + public static final BooleanSetting DISABLE_SUGGESTED_VIDEO_END_SCREEN = new BooleanSetting("revanced_disable_suggested_video_end_screen", FALSE, true); + public static final BooleanSetting GRADIENT_LOADING_SCREEN = new BooleanSetting("revanced_gradient_loading_screen", FALSE, true); + public static final BooleanSetting HIDE_HORIZONTAL_SHELVES = new BooleanSetting("revanced_hide_horizontal_shelves", TRUE); + public static final BooleanSetting HIDE_CAPTIONS_BUTTON = new BooleanSetting("revanced_hide_captions_button", FALSE); + public static final BooleanSetting HIDE_CHANNEL_BAR = new BooleanSetting("revanced_hide_channel_bar", FALSE); + public static final BooleanSetting HIDE_CHANNEL_MEMBER_SHELF = new BooleanSetting("revanced_hide_channel_member_shelf", TRUE); + public static final BooleanSetting HIDE_CHIPS_SHELF = new BooleanSetting("revanced_hide_chips_shelf", TRUE); + public static final BooleanSetting HIDE_COMMUNITY_GUIDELINES = new BooleanSetting("revanced_hide_community_guidelines", TRUE); + public static final BooleanSetting HIDE_COMMUNITY_POSTS = new BooleanSetting("revanced_hide_community_posts", FALSE); + public static final BooleanSetting HIDE_COMPACT_BANNER = new BooleanSetting("revanced_hide_compact_banner", TRUE); + public static final BooleanSetting HIDE_CROWDFUNDING_BOX = new BooleanSetting("revanced_hide_crowdfunding_box", FALSE, true); + public static final BooleanSetting HIDE_EMERGENCY_BOX = new BooleanSetting("revanced_hide_emergency_box", TRUE); + public static final BooleanSetting HIDE_ENDSCREEN_CARDS = new BooleanSetting("revanced_hide_endscreen_cards", FALSE); + public static final BooleanSetting HIDE_FEED_SURVEY = new BooleanSetting("revanced_hide_feed_survey", TRUE); + public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_FEED = new BooleanSetting("revanced_hide_filter_bar_feed_in_feed", FALSE, true); + public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_RELATED_VIDEOS = new BooleanSetting("revanced_hide_filter_bar_feed_in_related_videos", FALSE, true); + public static final BooleanSetting HIDE_FILTER_BAR_FEED_IN_SEARCH = new BooleanSetting("revanced_hide_filter_bar_feed_in_search", FALSE, true); + public static final BooleanSetting HIDE_FLOATING_MICROPHONE_BUTTON = new BooleanSetting("revanced_hide_floating_microphone_button", TRUE, true); + public static final BooleanSetting HIDE_FULLSCREEN_PANELS = new BooleanSetting("revanced_hide_fullscreen_panels", TRUE, true); + public static final BooleanSetting HIDE_HIDE_CHANNEL_GUIDELINES = new BooleanSetting("revanced_hide_channel_guidelines", TRUE); + public static final BooleanSetting HIDE_HIDE_INFO_PANELS = new BooleanSetting("revanced_hide_info_panels", TRUE); + public static final BooleanSetting HIDE_IMAGE_SHELF = new BooleanSetting("revanced_hide_image_shelf", TRUE); + public static final BooleanSetting HIDE_INFO_CARDS = new BooleanSetting("revanced_hide_info_cards", FALSE); + public static final BooleanSetting HIDE_JOIN_MEMBERSHIP_BUTTON = new BooleanSetting("revanced_hide_join_membership_button", TRUE); + public static final BooleanSetting HIDE_SHOW_MORE_BUTTON = new BooleanSetting("revanced_hide_show_more_button", TRUE, true); + public static final BooleanSetting HIDE_MEDICAL_PANELS = new BooleanSetting("revanced_hide_medical_panels", TRUE); + public static final BooleanSetting HIDE_MIX_PLAYLISTS = new BooleanSetting("revanced_hide_mix_playlists", TRUE); + public static final BooleanSetting HIDE_MOVIES_SECTION = new BooleanSetting("revanced_hide_movies_section", TRUE); + public static final BooleanSetting HIDE_NOTIFY_ME_BUTTON = new BooleanSetting("revanced_hide_notify_me_button", TRUE); + public static final BooleanSetting HIDE_PLAYABLES = new BooleanSetting("revanced_hide_playables", TRUE); + public static final BooleanSetting HIDE_QUICK_ACTIONS = new BooleanSetting("revanced_hide_quick_actions", FALSE); + public static final BooleanSetting HIDE_RELATED_VIDEOS = new BooleanSetting("revanced_hide_related_videos", FALSE); + public static final BooleanSetting HIDE_SEARCH_RESULT_SHELF_HEADER = new BooleanSetting("revanced_hide_search_result_shelf_header", FALSE); + public static final BooleanSetting HIDE_SUBSCRIBERS_COMMUNITY_GUIDELINES = new BooleanSetting("revanced_hide_subscribers_community_guidelines", TRUE); + public static final BooleanSetting HIDE_TIMED_REACTIONS = new BooleanSetting("revanced_hide_timed_reactions", TRUE); + public static final BooleanSetting HIDE_TIMESTAMP = new BooleanSetting("revanced_hide_timestamp", FALSE); + public static final BooleanSetting HIDE_VIDEO_CHANNEL_WATERMARK = new BooleanSetting("revanced_hide_channel_watermark", TRUE); + public static final BooleanSetting HIDE_FOR_YOU_SHELF = new BooleanSetting("revanced_hide_for_you_shelf", TRUE); + public static final BooleanSetting HIDE_SEARCH_RESULT_RECOMMENDATIONS = new BooleanSetting("revanced_hide_search_result_recommendations", TRUE); + public static final IntegerSetting PLAYER_OVERLAY_OPACITY = new IntegerSetting("revanced_player_overlay_opacity",100, true); + public static final BooleanSetting PLAYER_POPUP_PANELS = new BooleanSetting("revanced_hide_player_popup_panels", FALSE); + + // Player + public static final BooleanSetting DISABLE_FULLSCREEN_AMBIENT_MODE = new BooleanSetting("revanced_disable_fullscreen_ambient_mode", TRUE, true); + public static final BooleanSetting DISABLE_ROLLING_NUMBER_ANIMATIONS = new BooleanSetting("revanced_disable_rolling_number_animations", FALSE); + public static final BooleanSetting DISABLE_LIKE_SUBSCRIBE_GLOW = new BooleanSetting("revanced_disable_like_subscribe_glow", FALSE); + public static final BooleanSetting HIDE_AUTOPLAY_BUTTON = new BooleanSetting("revanced_hide_autoplay_button", TRUE, true); + public static final BooleanSetting HIDE_CAST_BUTTON = new BooleanSetting("revanced_hide_cast_button", TRUE, true); + public static final BooleanSetting HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS = new BooleanSetting("revanced_hide_player_previous_next_buttons", FALSE, true); + @Deprecated + public static final BooleanSetting HIDE_PLAYER_BUTTONS = new BooleanSetting("revanced_hide_player_buttons", FALSE, true); + public static final BooleanSetting COPY_VIDEO_URL = new BooleanSetting("revanced_copy_video_url", FALSE); + public static final BooleanSetting COPY_VIDEO_URL_TIMESTAMP = new BooleanSetting("revanced_copy_video_url_timestamp", TRUE); + public static final BooleanSetting PLAYBACK_SPEED_DIALOG_BUTTON = new BooleanSetting("revanced_playback_speed_dialog_button", FALSE); + + // Miniplayer + public static final EnumSetting MINIPLAYER_TYPE = new EnumSetting<>("revanced_miniplayer_type", MiniplayerType.ORIGINAL, true); + private static final Availability MINIPLAYER_ANY_MODERN = MINIPLAYER_TYPE.availability(MODERN_1, MODERN_2, MODERN_3, MODERN_4); + public static final BooleanSetting MINIPLAYER_DOUBLE_TAP_ACTION = new BooleanSetting("revanced_miniplayer_double_tap_action", TRUE, true, MINIPLAYER_ANY_MODERN); + public static final BooleanSetting MINIPLAYER_DRAG_AND_DROP = new BooleanSetting("revanced_miniplayer_drag_and_drop", TRUE, true, MINIPLAYER_ANY_MODERN); + public static final BooleanSetting MINIPLAYER_HORIZONTAL_DRAG = new BooleanSetting("revanced_miniplayer_horizontal_drag", FALSE, true, new MiniplayerHorizontalDragAvailability()); + public static final BooleanSetting MINIPLAYER_HIDE_EXPAND_CLOSE = new BooleanSetting("revanced_miniplayer_hide_expand_close", FALSE, true, new MiniplayerHideExpandCloseAvailability()); + public static final BooleanSetting MINIPLAYER_HIDE_SUBTEXT = new BooleanSetting("revanced_miniplayer_hide_subtext", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1, MODERN_3)); + public static final BooleanSetting MINIPLAYER_HIDE_REWIND_FORWARD = new BooleanSetting("revanced_miniplayer_hide_rewind_forward", FALSE, true, MINIPLAYER_TYPE.availability(MODERN_1)); + public static final BooleanSetting MINIPLAYER_ROUNDED_CORNERS = new BooleanSetting("revanced_miniplayer_rounded_corners", TRUE, true, MINIPLAYER_ANY_MODERN); + public static final IntegerSetting MINIPLAYER_WIDTH_DIP = new IntegerSetting("revanced_miniplayer_width_dip", 192, true, MINIPLAYER_ANY_MODERN); + public static final IntegerSetting MINIPLAYER_OPACITY = new IntegerSetting("revanced_miniplayer_opacity", 100, true, MINIPLAYER_TYPE.availability(MODERN_1)); + + // External downloader + public static final BooleanSetting EXTERNAL_DOWNLOADER = new BooleanSetting("revanced_external_downloader", FALSE); + public static final BooleanSetting EXTERNAL_DOWNLOADER_ACTION_BUTTON = new BooleanSetting("revanced_external_downloader_action_button", FALSE); + public static final StringSetting EXTERNAL_DOWNLOADER_PACKAGE_NAME = new StringSetting("revanced_external_downloader_name", + "org.schabi.newpipe" /* NewPipe */, parentsAny(EXTERNAL_DOWNLOADER, EXTERNAL_DOWNLOADER_ACTION_BUTTON)); + + // Comments + public static final BooleanSetting HIDE_COMMENTS_BY_MEMBERS_HEADER = new BooleanSetting("revanced_hide_comments_by_members_header", FALSE); + public static final BooleanSetting HIDE_COMMENTS_SECTION = new BooleanSetting("revanced_hide_comments_section", FALSE); + public static final BooleanSetting HIDE_COMMENTS_CREATE_A_SHORT_BUTTON = new BooleanSetting("revanced_hide_comments_create_a_short_button", TRUE); + public static final BooleanSetting HIDE_COMMENTS_PREVIEW_COMMENT = new BooleanSetting("revanced_hide_comments_preview_comment", FALSE); + public static final BooleanSetting HIDE_COMMENTS_THANKS_BUTTON = new BooleanSetting("revanced_hide_comments_thanks_button", TRUE); + public static final BooleanSetting HIDE_COMMENTS_TIMESTAMP_AND_EMOJI_BUTTONS = new BooleanSetting("revanced_hide_comments_timestamp_and_emoji_buttons", TRUE); + + // Description + public static final BooleanSetting HIDE_ATTRIBUTES_SECTION = new BooleanSetting("revanced_hide_attributes_section", FALSE); + public static final BooleanSetting HIDE_CHAPTERS_SECTION = new BooleanSetting("revanced_hide_chapters_section", TRUE); + public static final BooleanSetting HIDE_INFO_CARDS_SECTION = new BooleanSetting("revanced_hide_info_cards_section", TRUE); + 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", TRUE); + public static final BooleanSetting HIDE_TRANSCRIPT_SECTION = new BooleanSetting("revanced_hide_transcript_section", TRUE); + + // Action buttons + public static final BooleanSetting HIDE_LIKE_DISLIKE_BUTTON = new BooleanSetting("revanced_hide_like_dislike_button", FALSE); + public static final BooleanSetting HIDE_SHARE_BUTTON = new BooleanSetting("revanced_hide_share_button", FALSE); + public static final BooleanSetting HIDE_REPORT_BUTTON = new BooleanSetting("revanced_hide_report_button", FALSE); + public static final BooleanSetting HIDE_REMIX_BUTTON = new BooleanSetting("revanced_hide_remix_button", TRUE); + public static final BooleanSetting HIDE_DOWNLOAD_BUTTON = new BooleanSetting("revanced_hide_download_button", FALSE); + public static final BooleanSetting HIDE_THANKS_BUTTON = new BooleanSetting("revanced_hide_thanks_button", TRUE); + public static final BooleanSetting HIDE_CLIP_BUTTON = new BooleanSetting("revanced_hide_clip_button", TRUE); + public static final BooleanSetting HIDE_PLAYLIST_BUTTON = new BooleanSetting("revanced_hide_playlist_button", FALSE); + + // Player flyout menu items + public static final BooleanSetting HIDE_PLAYER_FLYOUT_CAPTIONS = new BooleanSetting("revanced_hide_player_flyout_captions", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_ADDITIONAL_SETTINGS = new BooleanSetting("revanced_hide_player_flyout_additional_settings", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_LOOP_VIDEO = new BooleanSetting("revanced_hide_player_flyout_loop_video", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_AMBIENT_MODE = new BooleanSetting("revanced_hide_player_flyout_ambient_mode", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_HELP = new BooleanSetting("revanced_hide_player_flyout_help", TRUE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_SPEED = new BooleanSetting("revanced_hide_player_flyout_speed", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_MORE_INFO = new BooleanSetting("revanced_hide_player_flyout_more_info", TRUE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_LOCK_SCREEN = new BooleanSetting("revanced_hide_player_flyout_lock_screen", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_AUDIO_TRACK = new BooleanSetting("revanced_hide_player_flyout_audio_track", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_SLEEP_TIMER = new BooleanSetting("revanced_hide_player_flyout_sleep_timer", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_STABLE_VOLUME = new BooleanSetting("revanced_hide_player_flyout_stable_volume", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_WATCH_IN_VR = new BooleanSetting("revanced_hide_player_flyout_watch_in_vr", TRUE); + @Deprecated + private static final BooleanSetting DEPRECATED_HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER = new BooleanSetting("revanced_hide_video_quality_menu_footer", FALSE); + public static final BooleanSetting HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER = new BooleanSetting("revanced_hide_player_flyout_video_quality_footer", FALSE); + + // General layout + public static final EnumSetting CHANGE_START_PAGE = new EnumSetting<>("revanced_change_start_page", StartPage.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", "17.41.37", true, parent(SPOOF_APP_VERSION)); + public static final BooleanSetting TABLET_LAYOUT = new BooleanSetting("revanced_tablet_layout", FALSE, true, "revanced_tablet_layout_user_dialog_message"); + public static final BooleanSetting WIDE_SEARCHBAR = new BooleanSetting("revanced_wide_searchbar", FALSE, true); + public static final BooleanSetting BYPASS_IMAGE_REGION_RESTRICTIONS = new BooleanSetting("revanced_bypass_image_region_restrictions", FALSE, true); + public static final BooleanSetting REMOVE_VIEWER_DISCRETION_DIALOG = new BooleanSetting("revanced_remove_viewer_discretion_dialog", FALSE, + "revanced_remove_viewer_discretion_dialog_user_dialog_message"); + + // 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)); + + // Navigation buttons + public static final BooleanSetting HIDE_HOME_BUTTON = new BooleanSetting("revanced_hide_home_button", FALSE, true); + public static final BooleanSetting HIDE_CREATE_BUTTON = new BooleanSetting("revanced_hide_create_button", TRUE, true); + public static final BooleanSetting HIDE_SHORTS_BUTTON = new BooleanSetting("revanced_hide_shorts_button", TRUE, true); + public static final BooleanSetting HIDE_SUBSCRIPTIONS_BUTTON = new BooleanSetting("revanced_hide_subscriptions_button", FALSE, true); + public static final BooleanSetting HIDE_NAVIGATION_BUTTON_LABELS = new BooleanSetting("revanced_hide_navigation_button_labels", FALSE, true); + public static final BooleanSetting SWITCH_CREATE_WITH_NOTIFICATIONS_BUTTON = new BooleanSetting("revanced_switch_create_with_notifications_button", TRUE, true); + + // Shorts + public static final BooleanSetting DISABLE_SHORTS_BACKGROUND_PLAYBACK = new BooleanSetting("revanced_shorts_disable_background_playback", FALSE); + public static final BooleanSetting DISABLE_RESUMING_SHORTS_PLAYER = new BooleanSetting("revanced_disable_resuming_shorts_player", FALSE); + public static final BooleanSetting HIDE_SHORTS_HOME = new BooleanSetting("revanced_hide_shorts_home", FALSE); + public static final BooleanSetting HIDE_SHORTS_SUBSCRIPTIONS = new BooleanSetting("revanced_hide_shorts_subscriptions", FALSE); + public static final BooleanSetting HIDE_SHORTS_SEARCH = new BooleanSetting("revanced_hide_shorts_search", FALSE); + 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_OVERLAY_BUTTONS = new BooleanSetting("revanced_hide_shorts_paused_overlay_buttons", FALSE); + public static final BooleanSetting HIDE_SHORTS_SHOP_BUTTON = new BooleanSetting("revanced_hide_shorts_shop_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_TAGGED_PRODUCTS = new BooleanSetting("revanced_hide_shorts_tagged_products", TRUE); + public static final BooleanSetting HIDE_SHORTS_LOCATION_LABEL = new BooleanSetting("revanced_hide_shorts_location_label", FALSE); + public static final BooleanSetting HIDE_SHORTS_SAVE_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_save_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_UPCOMING_BUTTON = new BooleanSetting("revanced_hide_shorts_upcoming_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_GREEN_SCREEN_BUTTON = new BooleanSetting("revanced_hide_shorts_green_screen_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_HASHTAG_BUTTON = new BooleanSetting("revanced_hide_shorts_hashtag_button", TRUE); + public static final BooleanSetting HIDE_SHORTS_SEARCH_SUGGESTIONS = new BooleanSetting("revanced_hide_shorts_search_suggestions", FALSE); + public static final BooleanSetting HIDE_SHORTS_STICKERS = new BooleanSetting("revanced_hide_shorts_stickers", 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_LIKE_FOUNTAIN = new BooleanSetting("revanced_hide_shorts_like_fountain", TRUE); + 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_INFO_PANEL = new BooleanSetting("revanced_hide_shorts_info_panel", TRUE); + public static final BooleanSetting HIDE_SHORTS_SOUND_BUTTON = new BooleanSetting("revanced_hide_shorts_sound_button", 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", FALSE); + public static final BooleanSetting HIDE_SHORTS_FULL_VIDEO_LINK_LABEL = new BooleanSetting("revanced_hide_shorts_full_video_link_label", FALSE); + public static final BooleanSetting HIDE_SHORTS_NAVIGATION_BAR = new BooleanSetting("revanced_hide_shorts_navigation_bar", FALSE, true); + public static final BooleanSetting SHORTS_AUTOPLAY = new BooleanSetting("revanced_shorts_autoplay", FALSE); + public static final BooleanSetting SHORTS_AUTOPLAY_BACKGROUND = new BooleanSetting("revanced_shorts_autoplay_background", TRUE); + + // Seekbar + public static final BooleanSetting DISABLE_PRECISE_SEEKING_GESTURE = new BooleanSetting("revanced_disable_precise_seeking_gesture", TRUE); + public static final BooleanSetting SEEKBAR_TAPPING = new BooleanSetting("revanced_seekbar_tapping", TRUE); + public static final BooleanSetting SLIDE_TO_SEEK = new BooleanSetting("revanced_slide_to_seek", FALSE, true); + public static final BooleanSetting RESTORE_OLD_SEEKBAR_THUMBNAILS = new BooleanSetting("revanced_restore_old_seekbar_thumbnails", TRUE); + public static final BooleanSetting SEEKBAR_THUMBNAILS_HIGH_QUALITY = new BooleanSetting("revanced_seekbar_thumbnails_high_quality", FALSE, true, + "revanced_seekbar_thumbnails_high_quality_dialog_message", new SeekbarThumbnailsHighQualityAvailability()); + 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 SEEKBAR_CUSTOM_COLOR = new BooleanSetting("revanced_seekbar_custom_color", FALSE, true); + public static final StringSetting SEEKBAR_CUSTOM_COLOR_VALUE = new StringSetting("revanced_seekbar_custom_color_value", "#FF0033", true, parent(SEEKBAR_CUSTOM_COLOR)); + + // Misc + public static final BooleanSetting AUTO_CAPTIONS = new BooleanSetting("revanced_auto_captions", FALSE); + public static final BooleanSetting DISABLE_ZOOM_HAPTICS = new BooleanSetting("revanced_disable_zoom_haptics", TRUE); + public static final BooleanSetting EXTERNAL_BROWSER = new BooleanSetting("revanced_external_browser", TRUE, true); + public static final BooleanSetting AUTO_REPEAT = new BooleanSetting("revanced_auto_repeat", FALSE); + public static final BooleanSetting SPOOF_DEVICE_DIMENSIONS = new BooleanSetting("revanced_spoof_device_dimensions", FALSE, true, + "revanced_spoof_device_dimensions_user_dialog_message"); + public static final BooleanSetting BYPASS_URL_REDIRECTS = new BooleanSetting("revanced_bypass_url_redirects", TRUE); + public static final BooleanSetting ANNOUNCEMENTS = new BooleanSetting("revanced_announcements", TRUE); + public static final BooleanSetting SPOOF_VIDEO_STREAMS = new BooleanSetting("revanced_spoof_video_streams", TRUE, true,"revanced_spoof_video_streams_user_dialog_message"); + public static final BooleanSetting SPOOF_VIDEO_STREAMS_IOS_FORCE_AVC = new BooleanSetting("revanced_spoof_video_streams_ios_force_avc", FALSE, true, + "revanced_spoof_video_streams_ios_force_avc_user_dialog_message", new SpoofVideoStreamsPatch.ForceiOSAVCAvailability()); + public static final EnumSetting SPOOF_VIDEO_STREAMS_CLIENT_TYPE = new EnumSetting<>("revanced_spoof_video_streams_client", ClientType.ANDROID_VR, true, parent(SPOOF_VIDEO_STREAMS)); + public static final IntegerSetting ANNOUNCEMENT_LAST_ID = new IntegerSetting("revanced_announcement_last_id", -1); + public static final BooleanSetting CHECK_WATCH_HISTORY_DOMAIN_NAME = new BooleanSetting("revanced_check_watch_history_domain_name", TRUE, false, false); + public static final BooleanSetting REMOVE_TRACKING_QUERY_PARAMETER = new BooleanSetting("revanced_remove_tracking_query_parameter", TRUE); + public static final IntegerSetting CHECK_ENVIRONMENT_WARNINGS_ISSUED = new IntegerSetting("revanced_check_environment_warnings_issued", 0, true, false); + + // Debugging + /** + * When enabled, share the debug logs with care. + * The buffer contains select user data, including the client ip address and information that could identify the end user. + */ + public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, parent(BaseSettings.DEBUG)); + + // Swipe controls + public static final BooleanSetting SWIPE_BRIGHTNESS = new BooleanSetting("revanced_swipe_brightness", TRUE); + public static final BooleanSetting SWIPE_VOLUME = new BooleanSetting("revanced_swipe_volume", TRUE); + public static final BooleanSetting SWIPE_PRESS_TO_ENGAGE = new BooleanSetting("revanced_swipe_press_to_engage", FALSE, true, + parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); + public static final BooleanSetting SWIPE_HAPTIC_FEEDBACK = new BooleanSetting("revanced_swipe_haptic_feedback", TRUE, true, + parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); + public static final IntegerSetting SWIPE_MAGNITUDE_THRESHOLD = new IntegerSetting("revanced_swipe_threshold", 30, true, + parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); + public static final IntegerSetting SWIPE_OVERLAY_BACKGROUND_ALPHA = new IntegerSetting("revanced_swipe_overlay_background_alpha", 127, true, + parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); + public static final IntegerSetting SWIPE_OVERLAY_TEXT_SIZE = new IntegerSetting("revanced_swipe_text_overlay_size", 22, true, + parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); + public static final LongSetting SWIPE_OVERLAY_TIMEOUT = new LongSetting("revanced_swipe_overlay_timeout", 500L, true, + parentsAny(SWIPE_BRIGHTNESS, SWIPE_VOLUME)); + public static final BooleanSetting SWIPE_SAVE_AND_RESTORE_BRIGHTNESS = new BooleanSetting("revanced_swipe_save_and_restore_brightness", TRUE, true, parent(SWIPE_BRIGHTNESS)); + public static final FloatSetting SWIPE_BRIGHTNESS_VALUE = new FloatSetting("revanced_swipe_brightness_value", -1f); + public static final BooleanSetting SWIPE_LOWEST_VALUE_ENABLE_AUTO_BRIGHTNESS = new BooleanSetting("revanced_swipe_lowest_value_enable_auto_brightness", FALSE, true, parent(SWIPE_BRIGHTNESS)); + + // ReturnYoutubeDislike + public static final BooleanSetting RYD_ENABLED = new BooleanSetting("ryd_enabled", TRUE); + public static final StringSetting RYD_USER_ID = new StringSetting("ryd_user_id", "", false, false); + public static final BooleanSetting RYD_SHORTS = new BooleanSetting("ryd_shorts", TRUE, parent(RYD_ENABLED)); + public static final BooleanSetting RYD_DISLIKE_PERCENTAGE = new BooleanSetting("ryd_dislike_percentage", FALSE, parent(RYD_ENABLED)); + public static final BooleanSetting RYD_COMPACT_LAYOUT = new BooleanSetting("ryd_compact_layout", FALSE, parent(RYD_ENABLED)); + public static final BooleanSetting RYD_TOAST_ON_CONNECTION_ERROR = new BooleanSetting("ryd_toast_on_connection_error", TRUE, parent(RYD_ENABLED)); + + // SponsorBlock + public static final BooleanSetting SB_ENABLED = new BooleanSetting("sb_enabled", TRUE); + /** + * Do not use directly, instead use {@link SponsorBlockSettings} + */ + public static final StringSetting SB_PRIVATE_USER_ID = new StringSetting("sb_private_user_id_Do_Not_Share", ""); + public static final StringSetting DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING = new StringSetting("uuid", ""); // Delete sometime in 2024 + 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", TRUE, 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", TRUE, parent(SB_ENABLED)); + public static final StringSetting SB_API_URL = new StringSetting("sb_api_url","https://sponsor.ajay.app"); + 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 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); + + public static final StringSetting SB_CATEGORY_SPONSOR = new StringSetting("sb_sponsor", SKIP_AUTOMATICALLY_ONCE.reVancedKeyValue); + public static final StringSetting SB_CATEGORY_SPONSOR_COLOR = new StringSetting("sb_sponsor_color","#00D400"); + public static final StringSetting SB_CATEGORY_SELF_PROMO = new StringSetting("sb_selfpromo", MANUAL_SKIP.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", MANUAL_SKIP.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", MANUAL_SKIP.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", MANUAL_SKIP.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", IGNORE.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", IGNORE.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"); + + static { + // region Migration + + // Do _not_ delete this SB private user id migration property until sometime in early 2025. + // This is the only setting that cannot be reconfigured if lost, + // and more time should be given for users who rarely upgrade. + SharedPrefCategory sbPrefs = new SharedPrefCategory("sponsor-block"); + // Remove the "sb_" prefix, as old settings are saved without it. + String key = DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING.key.substring(3); + migrateFromOldPreferences(sbPrefs, DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING, key); + + migrateOldSettingToNew(DEPRECATED_SB_UUID_OLD_MIGRATION_SETTING, SB_PRIVATE_USER_ID); + + // Old spoof versions that no longer work reliably. + if (SpoofAppVersionPatch.isSpoofingToLessThan("17.33.00")) { + Logger.printInfo(() -> "Resetting spoof app version target"); + Settings.SPOOF_APP_VERSION_TARGET.resetToDefault(); + } + + migrateOldSettingToNew(HIDE_PLAYER_BUTTONS, HIDE_PLAYER_PREVIOUS_NEXT_BUTTONS); + + migrateOldSettingToNew(DEPRECATED_HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER, HIDE_PLAYER_FLYOUT_VIDEO_QUALITY_FOOTER); + + // endregion + } +} + 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..5ca2e65dc --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/AlternativeThumbnailsAboutDeArrowPreference.java @@ -0,0 +1,35 @@ +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/ForceAVCSpoofingPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ForceAVCSpoofingPreference.java new file mode 100644 index 000000000..558175675 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ForceAVCSpoofingPreference.java @@ -0,0 +1,61 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.StringRef.str; +import static app.revanced.extension.youtube.patches.spoof.DeviceHardwareSupport.DEVICE_HAS_HARDWARE_DECODING_VP9; + +import android.content.Context; +import android.preference.SwitchPreference; +import android.util.AttributeSet; + +@SuppressWarnings({"unused", "deprecation"}) +public class ForceAVCSpoofingPreference extends SwitchPreference { + { + if (!DEVICE_HAS_HARDWARE_DECODING_VP9) { + setSummaryOn(str("revanced_spoof_video_streams_ios_force_avc_no_hardware_vp9_summary_on")); + } + } + + public ForceAVCSpoofingPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ForceAVCSpoofingPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ForceAVCSpoofingPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ForceAVCSpoofingPreference(Context context) { + super(context); + } + + private void updateUI() { + if (DEVICE_HAS_HARDWARE_DECODING_VP9) { + return; + } + + // Temporarily remove the preference key to allow changing this preference without + // causing the settings UI listeners from showing reboot dialogs by the changes made here. + String key = getKey(); + setKey(null); + + // This setting cannot be changed by the user. + super.setEnabled(false); + super.setChecked(true); + + setKey(key); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + + updateUI(); + } + + @Override + public void setChecked(boolean checked) { + super.setChecked(checked); + + updateUI(); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/HtmlPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/HtmlPreference.java new file mode 100644 index 000000000..bd9db08f5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/HtmlPreference.java @@ -0,0 +1,35 @@ +package app.revanced.extension.youtube.settings.preference; + +import static android.text.Html.FROM_HTML_MODE_COMPACT; + +import android.content.Context; +import android.os.Build; +import android.preference.Preference; +import android.text.Html; +import android.util.AttributeSet; + +import androidx.annotation.RequiresApi; + +/** + * Allows using basic html for the summary text. + */ +@SuppressWarnings({"unused", "deprecation"}) +@RequiresApi(api = Build.VERSION_CODES.O) +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); + } +} 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..a22206f22 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedPreferenceFragment.java @@ -0,0 +1,36 @@ +package app.revanced.extension.youtube.settings.preference; + +import android.os.Build; +import android.preference.ListPreference; +import android.preference.Preference; + +import androidx.annotation.RequiresApi; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.settings.preference.AbstractPreferenceFragment; +import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch; +import app.revanced.extension.youtube.settings.Settings; + +/** + * Preference fragment for ReVanced settings. + * + * @noinspection deprecation + */ +public class ReVancedPreferenceFragment extends AbstractPreferenceFragment { + + @RequiresApi(api = Build.VERSION_CODES.O) + @Override + protected void initialize() { + super.initialize(); + + try { + // If the preference was included, then initialize it based on the available playback speed. + Preference defaultSpeedPreference = findPreference(Settings.PLAYBACK_SPEED_DEFAULT.key); + if (defaultSpeedPreference instanceof ListPreference) { + CustomPlaybackSpeedPatch.initializeListPreference((ListPreference) defaultSpeedPreference); + } + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedYouTubeAboutPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedYouTubeAboutPreference.java new file mode 100644 index 000000000..17b667e78 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReVancedYouTubeAboutPreference.java @@ -0,0 +1,32 @@ +package app.revanced.extension.youtube.settings.preference; + +import android.content.Context; +import android.util.AttributeSet; + +import app.revanced.extension.shared.settings.preference.ReVancedAboutPreference; +import app.revanced.extension.youtube.ThemeHelper; + +@SuppressWarnings("unused") +public class ReVancedYouTubeAboutPreference extends ReVancedAboutPreference { + + public int getLightColor() { + return ThemeHelper.getLightThemeColor(); + } + + public int getDarkColor() { + return ThemeHelper.getDarkThemeColor(); + } + + public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + public ReVancedYouTubeAboutPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + public ReVancedYouTubeAboutPreference(Context context) { + super(context); + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReturnYouTubeDislikePreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReturnYouTubeDislikePreferenceFragment.java new file mode 100644 index 000000000..66bcf29e2 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/ReturnYouTubeDislikePreferenceFragment.java @@ -0,0 +1,237 @@ +package app.revanced.extension.youtube.settings.preference; + +import static app.revanced.extension.shared.StringRef.str; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceCategory; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.youtube.patches.ReturnYouTubeDislikePatch; +import app.revanced.extension.youtube.returnyoutubedislike.ReturnYouTubeDislike; +import app.revanced.extension.youtube.returnyoutubedislike.requests.ReturnYouTubeDislikeApi; +import app.revanced.extension.youtube.settings.Settings; + +/** @noinspection deprecation*/ +public class ReturnYouTubeDislikePreferenceFragment extends PreferenceFragment { + + /** + * If dislikes are shown on Shorts. + */ + private SwitchPreference shortsPreference; + + /** + * If dislikes are shown as percentage. + */ + private SwitchPreference percentagePreference; + + /** + * If segmented like/dislike button uses smaller compact layout. + */ + private SwitchPreference compactLayoutPreference; + + /** + * If segmented like/dislike button uses smaller compact layout. + */ + private SwitchPreference toastOnRYDNotAvailable; + + private void updateUIState() { + shortsPreference.setEnabled(Settings.RYD_SHORTS.isAvailable()); + percentagePreference.setEnabled(Settings.RYD_DISLIKE_PERCENTAGE.isAvailable()); + compactLayoutPreference.setEnabled(Settings.RYD_COMPACT_LAYOUT.isAvailable()); + toastOnRYDNotAvailable.setEnabled(Settings.RYD_TOAST_ON_CONNECTION_ERROR.isAvailable()); + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + try { + Activity context = getActivity(); + PreferenceManager manager = getPreferenceManager(); + manager.setSharedPreferencesName(Setting.preferences.name); + PreferenceScreen preferenceScreen = manager.createPreferenceScreen(context); + setPreferenceScreen(preferenceScreen); + + SwitchPreference enabledPreference = new SwitchPreference(context); + enabledPreference.setChecked(Settings.RYD_ENABLED.get()); + enabledPreference.setTitle(str("revanced_ryd_enable_title")); + enabledPreference.setSummaryOn(str("revanced_ryd_enable_summary_on")); + enabledPreference.setSummaryOff(str("revanced_ryd_enable_summary_off")); + enabledPreference.setOnPreferenceChangeListener((pref, newValue) -> { + final Boolean rydIsEnabled = (Boolean) newValue; + Settings.RYD_ENABLED.save(rydIsEnabled); + ReturnYouTubeDislikePatch.onRYDStatusChange(rydIsEnabled); + + updateUIState(); + return true; + }); + preferenceScreen.addPreference(enabledPreference); + + shortsPreference = new SwitchPreference(context); + shortsPreference.setChecked(Settings.RYD_SHORTS.get()); + shortsPreference.setTitle(str("revanced_ryd_shorts_title")); + String shortsSummary = ReturnYouTubeDislikePatch.IS_SPOOFING_TO_NON_LITHO_SHORTS_PLAYER + ? str("revanced_ryd_shorts_summary_on") + : str("revanced_ryd_shorts_summary_on_disclaimer"); + shortsPreference.setSummaryOn(shortsSummary); + shortsPreference.setSummaryOff(str("revanced_ryd_shorts_summary_off")); + shortsPreference.setOnPreferenceChangeListener((pref, newValue) -> { + Settings.RYD_SHORTS.save((Boolean) newValue); + updateUIState(); + return true; + }); + preferenceScreen.addPreference(shortsPreference); + + percentagePreference = new SwitchPreference(context); + percentagePreference.setChecked(Settings.RYD_DISLIKE_PERCENTAGE.get()); + percentagePreference.setTitle(str("revanced_ryd_dislike_percentage_title")); + percentagePreference.setSummaryOn(str("revanced_ryd_dislike_percentage_summary_on")); + percentagePreference.setSummaryOff(str("revanced_ryd_dislike_percentage_summary_off")); + percentagePreference.setOnPreferenceChangeListener((pref, newValue) -> { + Settings.RYD_DISLIKE_PERCENTAGE.save((Boolean) newValue); + ReturnYouTubeDislike.clearAllUICaches(); + updateUIState(); + return true; + }); + preferenceScreen.addPreference(percentagePreference); + + compactLayoutPreference = new SwitchPreference(context); + compactLayoutPreference.setChecked(Settings.RYD_COMPACT_LAYOUT.get()); + compactLayoutPreference.setTitle(str("revanced_ryd_compact_layout_title")); + compactLayoutPreference.setSummaryOn(str("revanced_ryd_compact_layout_summary_on")); + compactLayoutPreference.setSummaryOff(str("revanced_ryd_compact_layout_summary_off")); + compactLayoutPreference.setOnPreferenceChangeListener((pref, newValue) -> { + Settings.RYD_COMPACT_LAYOUT.save((Boolean) newValue); + ReturnYouTubeDislike.clearAllUICaches(); + updateUIState(); + return true; + }); + preferenceScreen.addPreference(compactLayoutPreference); + + toastOnRYDNotAvailable = new SwitchPreference(context); + toastOnRYDNotAvailable.setChecked(Settings.RYD_TOAST_ON_CONNECTION_ERROR.get()); + toastOnRYDNotAvailable.setTitle(str("revanced_ryd_toast_on_connection_error_title")); + toastOnRYDNotAvailable.setSummaryOn(str("revanced_ryd_toast_on_connection_error_summary_on")); + toastOnRYDNotAvailable.setSummaryOff(str("revanced_ryd_toast_on_connection_error_summary_off")); + toastOnRYDNotAvailable.setOnPreferenceChangeListener((pref, newValue) -> { + Settings.RYD_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue); + updateUIState(); + return true; + }); + preferenceScreen.addPreference(toastOnRYDNotAvailable); + + updateUIState(); + + + // About category + + PreferenceCategory aboutCategory = new PreferenceCategory(context); + aboutCategory.setTitle(str("revanced_ryd_about")); + preferenceScreen.addPreference(aboutCategory); + + // ReturnYouTubeDislike Website + + Preference aboutWebsitePreference = new Preference(context); + aboutWebsitePreference.setTitle(str("revanced_ryd_attribution_title")); + aboutWebsitePreference.setSummary(str("revanced_ryd_attribution_summary")); + aboutWebsitePreference.setOnPreferenceClickListener(pref -> { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse("https://returnyoutubedislike.com")); + pref.getContext().startActivity(i); + return false; + }); + aboutCategory.addPreference(aboutWebsitePreference); + + // RYD API connection statistics + + if (BaseSettings.DEBUG.get()) { + PreferenceCategory emptyCategory = new PreferenceCategory(context); // vertical padding + preferenceScreen.addPreference(emptyCategory); + + PreferenceCategory statisticsCategory = new PreferenceCategory(context); + statisticsCategory.setTitle(str("revanced_ryd_statistics_category_title")); + preferenceScreen.addPreference(statisticsCategory); + + Preference statisticPreference; + + statisticPreference = new Preference(context); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeAverage_title")); + statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeAverage())); + preferenceScreen.addPreference(statisticPreference); + + statisticPreference = new Preference(context); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeMin_title")); + statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMin())); + preferenceScreen.addPreference(statisticPreference); + + statisticPreference = new Preference(context); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeMax_title")); + statisticPreference.setSummary(createMillisecondStringFromNumber(ReturnYouTubeDislikeApi.getFetchCallResponseTimeMax())); + preferenceScreen.addPreference(statisticPreference); + + String fetchCallTimeWaitingLastSummary; + final long fetchCallTimeWaitingLast = ReturnYouTubeDislikeApi.getFetchCallResponseTimeLast(); + if (fetchCallTimeWaitingLast == ReturnYouTubeDislikeApi.FETCH_CALL_RESPONSE_TIME_VALUE_RATE_LIMIT) { + fetchCallTimeWaitingLastSummary = str("revanced_ryd_statistics_getFetchCallResponseTimeLast_rate_limit_summary"); + } else { + fetchCallTimeWaitingLastSummary = createMillisecondStringFromNumber(fetchCallTimeWaitingLast); + } + statisticPreference = new Preference(context); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallResponseTimeLast_title")); + statisticPreference.setSummary(fetchCallTimeWaitingLastSummary); + preferenceScreen.addPreference(statisticPreference); + + statisticPreference = new Preference(context); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallCount_title")); + statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getFetchCallCount(), + "revanced_ryd_statistics_getFetchCallCount_zero_summary", + "revanced_ryd_statistics_getFetchCallCount_non_zero_summary")); + preferenceScreen.addPreference(statisticPreference); + + statisticPreference = new Preference(context); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str("revanced_ryd_statistics_getFetchCallNumberOfFailures_title")); + statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getFetchCallNumberOfFailures(), + "revanced_ryd_statistics_getFetchCallNumberOfFailures_zero_summary", + "revanced_ryd_statistics_getFetchCallNumberOfFailures_non_zero_summary")); + preferenceScreen.addPreference(statisticPreference); + + statisticPreference = new Preference(context); + statisticPreference.setSelectable(false); + statisticPreference.setTitle(str("revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_title")); + statisticPreference.setSummary(createSummaryText(ReturnYouTubeDislikeApi.getNumberOfRateLimitRequestsEncountered(), + "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_zero_summary", + "revanced_ryd_statistics_getNumberOfRateLimitRequestsEncountered_non_zero_summary")); + preferenceScreen.addPreference(statisticPreference); + } + } catch (Exception ex) { + Logger.printException(() -> "onCreate failure", ex); + } + } + + private static String createSummaryText(int value, String summaryStringZeroKey, String summaryStringOneOrMoreKey) { + if (value == 0) { + return str(summaryStringZeroKey); + } + return String.format(str(summaryStringOneOrMoreKey), value); + } + + private static String createMillisecondStringFromNumber(long number) { + return String.format(str("revanced_ryd_statistics_millisecond_text"), number); + } + +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockPreferenceFragment.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockPreferenceFragment.java new file mode 100644 index 000000000..9fa4a942a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/settings/preference/SponsorBlockPreferenceFragment.java @@ -0,0 +1,602 @@ +package app.revanced.extension.youtube.settings.preference; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.preference.*; +import android.text.Html; +import android.text.InputType; +import android.util.TypedValue; +import android.widget.EditText; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController; +import app.revanced.extension.youtube.sponsorblock.SponsorBlockSettings; +import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils; +import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategory; +import app.revanced.extension.youtube.sponsorblock.objects.SegmentCategoryListPreference; +import app.revanced.extension.youtube.sponsorblock.objects.UserStats; +import app.revanced.extension.youtube.sponsorblock.requests.SBRequester; +import app.revanced.extension.youtube.sponsorblock.ui.SponsorBlockViewController; + +import static android.text.Html.fromHtml; +import static app.revanced.extension.shared.StringRef.str; + +@SuppressWarnings("deprecation") +public class SponsorBlockPreferenceFragment extends PreferenceFragment { + + private SwitchPreference sbEnabled; + private SwitchPreference addNewSegment; + private SwitchPreference votingEnabled; + private SwitchPreference compactSkipButton; + private SwitchPreference autoHideSkipSegmentButton; + private SwitchPreference showSkipToast; + private SwitchPreference trackSkips; + private SwitchPreference showTimeWithoutSegments; + private SwitchPreference toastOnConnectionError; + + private EditTextPreference newSegmentStep; + private EditTextPreference minSegmentDuration; + private EditTextPreference privateUserId; + private EditTextPreference importExport; + private Preference apiUrl; + + private PreferenceCategory statsCategory; + private PreferenceCategory segmentCategory; + + private void updateUI() { + try { + final boolean enabled = Settings.SB_ENABLED.get(); + if (!enabled) { + SponsorBlockViewController.hideAll(); + SegmentPlaybackController.setCurrentVideoId(null); + } else if (!Settings.SB_CREATE_NEW_SEGMENT.get()) { + SponsorBlockViewController.hideNewSegmentLayout(); + } + // Voting and add new segment buttons automatically shows/hide themselves. + + sbEnabled.setChecked(enabled); + + addNewSegment.setChecked(Settings.SB_CREATE_NEW_SEGMENT.get()); + addNewSegment.setEnabled(enabled); + + votingEnabled.setChecked(Settings.SB_VOTING_BUTTON.get()); + votingEnabled.setEnabled(enabled); + + compactSkipButton.setChecked(Settings.SB_COMPACT_SKIP_BUTTON.get()); + compactSkipButton.setEnabled(enabled); + + autoHideSkipSegmentButton.setChecked(Settings.SB_AUTO_HIDE_SKIP_BUTTON.get()); + autoHideSkipSegmentButton.setEnabled(enabled); + + showSkipToast.setChecked(Settings.SB_TOAST_ON_SKIP.get()); + showSkipToast.setEnabled(enabled); + + toastOnConnectionError.setChecked(Settings.SB_TOAST_ON_CONNECTION_ERROR.get()); + toastOnConnectionError.setEnabled(enabled); + + trackSkips.setChecked(Settings.SB_TRACK_SKIP_COUNT.get()); + trackSkips.setEnabled(enabled); + + showTimeWithoutSegments.setChecked(Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get()); + showTimeWithoutSegments.setEnabled(enabled); + + newSegmentStep.setText((Settings.SB_CREATE_NEW_SEGMENT_STEP.get()).toString()); + newSegmentStep.setEnabled(enabled); + + minSegmentDuration.setText((Settings.SB_SEGMENT_MIN_DURATION.get()).toString()); + minSegmentDuration.setEnabled(enabled); + + privateUserId.setText(Settings.SB_PRIVATE_USER_ID.get()); + privateUserId.setEnabled(enabled); + + // If the user has a private user id, then include a subtext that mentions not to share it. + String importExportSummary = SponsorBlockSettings.userHasSBPrivateId() + ? str("revanced_sb_settings_ie_sum_warning") + : str("revanced_sb_settings_ie_sum"); + importExport.setSummary(importExportSummary); + + apiUrl.setEnabled(enabled); + importExport.setEnabled(enabled); + segmentCategory.setEnabled(enabled); + statsCategory.setEnabled(enabled); + } catch (Exception ex) { + Logger.printException(() -> "update settings UI failure", ex); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + try { + Activity context = getActivity(); + PreferenceManager manager = getPreferenceManager(); + manager.setSharedPreferencesName(Setting.preferences.name); + PreferenceScreen preferenceScreen = manager.createPreferenceScreen(context); + setPreferenceScreen(preferenceScreen); + + SponsorBlockSettings.initialize(); + + sbEnabled = new SwitchPreference(context); + sbEnabled.setTitle(str("revanced_sb_enable_sb")); + sbEnabled.setSummary(str("revanced_sb_enable_sb_sum")); + preferenceScreen.addPreference(sbEnabled); + sbEnabled.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_ENABLED.save((Boolean) newValue); + updateUI(); + return true; + }); + + addAppearanceCategory(context, preferenceScreen); + + segmentCategory = new PreferenceCategory(context); + segmentCategory.setTitle(str("revanced_sb_diff_segments")); + preferenceScreen.addPreference(segmentCategory); + updateSegmentCategories(); + + addCreateSegmentCategory(context, preferenceScreen); + + addGeneralCategory(context, preferenceScreen); + + statsCategory = new PreferenceCategory(context); + statsCategory.setTitle(str("revanced_sb_stats")); + preferenceScreen.addPreference(statsCategory); + fetchAndDisplayStats(); + + addAboutCategory(context, preferenceScreen); + + updateUI(); + } catch (Exception ex) { + Logger.printException(() -> "onCreate failure", ex); + } + } + + private void addAppearanceCategory(Context context, PreferenceScreen screen) { + PreferenceCategory category = new PreferenceCategory(context); + screen.addPreference(category); + category.setTitle(str("revanced_sb_appearance_category")); + + votingEnabled = new SwitchPreference(context); + votingEnabled.setTitle(str("revanced_sb_enable_voting")); + votingEnabled.setSummaryOn(str("revanced_sb_enable_voting_sum_on")); + votingEnabled.setSummaryOff(str("revanced_sb_enable_voting_sum_off")); + category.addPreference(votingEnabled); + votingEnabled.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_VOTING_BUTTON.save((Boolean) newValue); + updateUI(); + return true; + }); + + compactSkipButton = new SwitchPreference(context); + compactSkipButton.setTitle(str("revanced_sb_enable_compact_skip_button")); + compactSkipButton.setSummaryOn(str("revanced_sb_enable_compact_skip_button_sum_on")); + compactSkipButton.setSummaryOff(str("revanced_sb_enable_compact_skip_button_sum_off")); + category.addPreference(compactSkipButton); + compactSkipButton.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_COMPACT_SKIP_BUTTON.save((Boolean) newValue); + updateUI(); + return true; + }); + + autoHideSkipSegmentButton = new SwitchPreference(context); + autoHideSkipSegmentButton.setTitle(str("revanced_sb_enable_auto_hide_skip_segment_button")); + autoHideSkipSegmentButton.setSummaryOn(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_on")); + autoHideSkipSegmentButton.setSummaryOff(str("revanced_sb_enable_auto_hide_skip_segment_button_sum_off")); + category.addPreference(autoHideSkipSegmentButton); + autoHideSkipSegmentButton.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_AUTO_HIDE_SKIP_BUTTON.save((Boolean) newValue); + updateUI(); + return true; + }); + + showSkipToast = new SwitchPreference(context); + showSkipToast.setTitle(str("revanced_sb_general_skiptoast")); + showSkipToast.setSummaryOn(str("revanced_sb_general_skiptoast_sum_on")); + showSkipToast.setSummaryOff(str("revanced_sb_general_skiptoast_sum_off")); + showSkipToast.setOnPreferenceClickListener(preference1 -> { + Utils.showToastShort(str("revanced_sb_skipped_sponsor")); + return false; + }); + showSkipToast.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_TOAST_ON_SKIP.save((Boolean) newValue); + updateUI(); + return true; + }); + category.addPreference(showSkipToast); + + showTimeWithoutSegments = new SwitchPreference(context); + showTimeWithoutSegments.setTitle(str("revanced_sb_general_time_without")); + showTimeWithoutSegments.setSummaryOn(str("revanced_sb_general_time_without_sum_on")); + showTimeWithoutSegments.setSummaryOff(str("revanced_sb_general_time_without_sum_off")); + showTimeWithoutSegments.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.save((Boolean) newValue); + updateUI(); + return true; + }); + category.addPreference(showTimeWithoutSegments); + } + + private void addCreateSegmentCategory(Context context, PreferenceScreen screen) { + PreferenceCategory category = new PreferenceCategory(context); + screen.addPreference(category); + category.setTitle(str("revanced_sb_create_segment_category")); + + addNewSegment = new SwitchPreference(context); + addNewSegment.setTitle(str("revanced_sb_enable_create_segment")); + addNewSegment.setSummaryOn(str("revanced_sb_enable_create_segment_sum_on")); + addNewSegment.setSummaryOff(str("revanced_sb_enable_create_segment_sum_off")); + category.addPreference(addNewSegment); + addNewSegment.setOnPreferenceChangeListener((preference1, o) -> { + Boolean newValue = (Boolean) o; + if (newValue && !Settings.SB_SEEN_GUIDELINES.get()) { + new AlertDialog.Builder(preference1.getContext()) + .setTitle(str("revanced_sb_guidelines_popup_title")) + .setMessage(str("revanced_sb_guidelines_popup_content")) + .setNegativeButton(str("revanced_sb_guidelines_popup_already_read"), null) + .setPositiveButton(str("revanced_sb_guidelines_popup_open"), (dialogInterface, i) -> openGuidelines()) + .setOnDismissListener(dialog -> Settings.SB_SEEN_GUIDELINES.save(true)) + .setCancelable(false) + .show(); + } + Settings.SB_CREATE_NEW_SEGMENT.save(newValue); + updateUI(); + return true; + }); + + newSegmentStep = new EditTextPreference(context); + newSegmentStep.setTitle(str("revanced_sb_general_adjusting")); + newSegmentStep.setSummary(str("revanced_sb_general_adjusting_sum")); + newSegmentStep.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER); + newSegmentStep.setOnPreferenceChangeListener((preference1, newValue) -> { + try { + final int newAdjustmentValue = Integer.parseInt(newValue.toString()); + if (newAdjustmentValue != 0) { + Settings.SB_CREATE_NEW_SEGMENT_STEP.save(newAdjustmentValue); + return true; + } + } catch (NumberFormatException ex) { + Logger.printInfo(() -> "Invalid new segment step", ex); + } + + Utils.showToastLong(str("revanced_sb_general_adjusting_invalid")); + updateUI(); + return false; + }); + category.addPreference(newSegmentStep); + + Preference guidelinePreferences = new Preference(context); + guidelinePreferences.setTitle(str("revanced_sb_guidelines_preference_title")); + guidelinePreferences.setSummary(str("revanced_sb_guidelines_preference_sum")); + guidelinePreferences.setOnPreferenceClickListener(preference1 -> { + openGuidelines(); + return true; + }); + category.addPreference(guidelinePreferences); + } + + private void addGeneralCategory(final Context context, PreferenceScreen screen) { + PreferenceCategory category = new PreferenceCategory(context); + screen.addPreference(category); + category.setTitle(str("revanced_sb_general")); + + toastOnConnectionError = new SwitchPreference(context); + toastOnConnectionError.setTitle(str("revanced_sb_toast_on_connection_error_title")); + toastOnConnectionError.setSummaryOn(str("revanced_sb_toast_on_connection_error_summary_on")); + toastOnConnectionError.setSummaryOff(str("revanced_sb_toast_on_connection_error_summary_off")); + toastOnConnectionError.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_TOAST_ON_CONNECTION_ERROR.save((Boolean) newValue); + updateUI(); + return true; + }); + category.addPreference(toastOnConnectionError); + + trackSkips = new SwitchPreference(context); + trackSkips.setTitle(str("revanced_sb_general_skipcount")); + trackSkips.setSummaryOn(str("revanced_sb_general_skipcount_sum_on")); + trackSkips.setSummaryOff(str("revanced_sb_general_skipcount_sum_off")); + trackSkips.setOnPreferenceChangeListener((preference1, newValue) -> { + Settings.SB_TRACK_SKIP_COUNT.save((Boolean) newValue); + updateUI(); + return true; + }); + category.addPreference(trackSkips); + + minSegmentDuration = new EditTextPreference(context); + minSegmentDuration.setTitle(str("revanced_sb_general_min_duration")); + minSegmentDuration.setSummary(str("revanced_sb_general_min_duration_sum")); + minSegmentDuration.getEditText().setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); + minSegmentDuration.setOnPreferenceChangeListener((preference1, newValue) -> { + try { + Float minTimeDuration = Float.valueOf(newValue.toString()); + Settings.SB_SEGMENT_MIN_DURATION.save(minTimeDuration); + return true; + } catch (NumberFormatException ex) { + Logger.printInfo(() -> "Invalid minimum segment duration", ex); + } + + Utils.showToastLong(str("revanced_sb_general_min_duration_invalid")); + updateUI(); + return false; + }); + category.addPreference(minSegmentDuration); + + privateUserId = new EditTextPreference(context); + privateUserId.setTitle(str("revanced_sb_general_uuid")); + privateUserId.setSummary(str("revanced_sb_general_uuid_sum")); + privateUserId.setOnPreferenceChangeListener((preference1, newValue) -> { + String newUUID = newValue.toString(); + if (!SponsorBlockSettings.isValidSBUserId(newUUID)) { + Utils.showToastLong(str("revanced_sb_general_uuid_invalid")); + return false; + } + + Settings.SB_PRIVATE_USER_ID.save(newUUID); + updateUI(); + fetchAndDisplayStats(); + return true; + }); + category.addPreference(privateUserId); + + apiUrl = new Preference(context); + apiUrl.setTitle(str("revanced_sb_general_api_url")); + apiUrl.setSummary(Html.fromHtml(str("revanced_sb_general_api_url_sum"))); + apiUrl.setOnPreferenceClickListener(preference1 -> { + EditText editText = new EditText(context); + editText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI); + editText.setText(Settings.SB_API_URL.get()); + + DialogInterface.OnClickListener urlChangeListener = (dialog, buttonPressed) -> { + if (buttonPressed == DialogInterface.BUTTON_NEUTRAL) { + Settings.SB_API_URL.resetToDefault(); + Utils.showToastLong(str("revanced_sb_api_url_reset")); + } else if (buttonPressed == DialogInterface.BUTTON_POSITIVE) { + String serverAddress = editText.getText().toString(); + if (!SponsorBlockSettings.isValidSBServerAddress(serverAddress)) { + Utils.showToastLong(str("revanced_sb_api_url_invalid")); + } else if (!serverAddress.equals(Settings.SB_API_URL.get())) { + Settings.SB_API_URL.save(serverAddress); + Utils.showToastLong(str("revanced_sb_api_url_changed")); + } + } + }; + new AlertDialog.Builder(context) + .setTitle(apiUrl.getTitle()) + .setView(editText) + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(str("revanced_sb_reset"), urlChangeListener) + .setPositiveButton(android.R.string.ok, urlChangeListener) + .show(); + return true; + }); + category.addPreference(apiUrl); + + importExport = new EditTextPreference(context) { + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + Utils.setEditTextDialogTheme(builder); + + builder.setNeutralButton(str("revanced_sb_settings_copy"), (dialog, which) -> { + Utils.setClipboard(getEditText().getText().toString()); + }); + } + }; + importExport.setTitle(str("revanced_sb_settings_ie")); + // Summary is set in updateUI() + importExport.getEditText().setInputType(InputType.TYPE_CLASS_TEXT + | InputType.TYPE_TEXT_FLAG_MULTI_LINE + | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + importExport.getEditText().setAutofillHints((String) null); + } + importExport.getEditText().setTextSize(TypedValue.COMPLEX_UNIT_PT, 8); + importExport.setOnPreferenceClickListener(preference1 -> { + importExport.getEditText().setText(SponsorBlockSettings.exportDesktopSettings()); + return true; + }); + importExport.setOnPreferenceChangeListener((preference1, newValue) -> { + SponsorBlockSettings.importDesktopSettings((String) newValue); + updateSegmentCategories(); + fetchAndDisplayStats(); + updateUI(); + return true; + }); + category.addPreference(importExport); + } + + private void updateSegmentCategories() { + try { + segmentCategory.removeAll(); + + Activity activity = getActivity(); + for (SegmentCategory category : SegmentCategory.categoriesWithoutUnsubmitted()) { + segmentCategory.addPreference(new SegmentCategoryListPreference(activity, category)); + } + } catch (Exception ex) { + Logger.printException(() -> "updateSegmentCategories failure", ex); + } + } + + private void addAboutCategory(Context context, PreferenceScreen screen) { + PreferenceCategory category = new PreferenceCategory(context); + screen.addPreference(category); + category.setTitle(str("revanced_sb_about")); + + { + Preference preference = new Preference(context); + category.addPreference(preference); + preference.setTitle(str("revanced_sb_about_api")); + preference.setSummary(str("revanced_sb_about_api_sum")); + preference.setOnPreferenceClickListener(preference1 -> { + Intent i = new Intent(Intent.ACTION_VIEW); + i.setData(Uri.parse("https://sponsor.ajay.app")); + preference1.getContext().startActivity(i); + return false; + }); + } + } + + private void openGuidelines() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("https://wiki.sponsor.ajay.app/w/Guidelines")); + getActivity().startActivity(intent); + } + + private void fetchAndDisplayStats() { + try { + statsCategory.removeAll(); + if (!SponsorBlockSettings.userHasSBPrivateId()) { + // User has never voted or created any segments. No stats to show. + addLocalUserStats(); + return; + } + + Preference loadingPlaceholderPreference = new Preference(this.getActivity()); + loadingPlaceholderPreference.setEnabled(false); + statsCategory.addPreference(loadingPlaceholderPreference); + if (Settings.SB_ENABLED.get()) { + loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_loading")); + Utils.runOnBackgroundThread(() -> { + UserStats stats = SBRequester.retrieveUserStats(); + Utils.runOnMainThread(() -> { // get back on main thread to modify UI elements + addUserStats(loadingPlaceholderPreference, stats); + addLocalUserStats(); + }); + }); + } else { + loadingPlaceholderPreference.setTitle(str("revanced_sb_stats_sb_disabled")); + } + } catch (Exception ex) { + Logger.printException(() -> "fetchAndDisplayStats failure", ex); + } + } + + private void addUserStats(@NonNull Preference loadingPlaceholder, @Nullable UserStats stats) { + Utils.verifyOnMainThread(); + try { + if (stats == null) { + loadingPlaceholder.setTitle(str("revanced_sb_stats_connection_failure")); + return; + } + statsCategory.removeAll(); + Context context = statsCategory.getContext(); + + if (stats.totalSegmentCountIncludingIgnored > 0) { + // If user has not created any segments, there's no reason to set a username. + EditTextPreference preference = new EditTextPreference(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 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/shared/NavigationBar.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java new file mode 100644 index 000000000..960df3bf0 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/NavigationBar.java @@ -0,0 +1,309 @@ +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.lang.ref.WeakReference; +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.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.BaseSettings; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public final class NavigationBar { + + // + // Search bar + // + + private static volatile WeakReference searchBarResultsRef = new WeakReference<>(null); + + /** + * Injection point. + */ + public static void searchBarResultsViewLoaded(View searchbarResults) { + searchBarResultsRef = new WeakReference<>(searchbarResults); + } + + /** + * @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 PlayerType#isMaximizedOrFullscreen()}. + */ + public static boolean isSearchBarActive() { + View searchbarResults = searchBarResultsRef.get(); + return searchbarResults != null && searchbarResults.getParent() != null; + } + + // + // Navigation bar buttons + // + + /** + * 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.DEBUG.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 (BaseSettings.DEBUG.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/PlayerControlsVisibilityObserver.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibilityObserver.kt new file mode 100644 index 000000000..26745755d --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerControlsVisibilityObserver.kt @@ -0,0 +1,84 @@ +package app.revanced.extension.youtube.shared + +import android.app.Activity +import android.view.View +import android.view.ViewGroup +import app.revanced.extension.shared.Utils +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.youtube_controls_overlay + */ + private val controlsLayoutParentId = + Utils.getResourceIdentifier(activity, "youtube_controls_overlay", "id") + + /** + * id of R.id.controls_layout + */ + private val controlsLayoutId = + Utils.getResourceIdentifier(activity, "controls_layout", "id") + + /** + * 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 +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerOverlays.kt b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerOverlays.kt new file mode 100644 index 000000000..ec82053fa --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerOverlays.kt @@ -0,0 +1,97 @@ +package app.revanced.extension.youtube.shared + +import android.view.View +import android.view.ViewGroup +import app.revanced.extension.youtube.Event +import app.revanced.extension.youtube.swipecontrols.misc.Rectangle + +/** + * hooking class for player overlays + */ +@Suppress("MemberVisibilityCanBePrivate") +object PlayerOverlays { + + /** + * called when the overlays finished inflating + */ + val onInflate = Event() + + /** + * called when new children are added or removed from the overlay + */ + val onChildrenChange = Event() + + /** + * called when the overlay layout changes + */ + val onLayoutChange = Event() + + /** + * start listening for events on the provided view group + * + * @param overlaysLayout the overlays view group + */ + @JvmStatic + fun attach(overlaysLayout: ViewGroup) { + onInflate.invoke(overlaysLayout) + overlaysLayout.setOnHierarchyChangeListener(object : + ViewGroup.OnHierarchyChangeListener { + override fun onChildViewAdded(parent: View?, child: View?) { + if (parent is ViewGroup && child is View) { + onChildrenChange( + ChildrenChangeEventArgs( + parent, + child, + false, + ), + ) + } + } + + override fun onChildViewRemoved(parent: View?, child: View?) { + if (parent is ViewGroup && child is View) { + onChildrenChange( + ChildrenChangeEventArgs( + parent, + child, + true, + ), + ) + } + } + }) + overlaysLayout.addOnLayoutChangeListener { view, newLeft, newTop, newRight, newBottom, oldLeft, oldTop, oldRight, oldBottom -> + if (view is ViewGroup) { + onLayoutChange( + LayoutChangeEventArgs( + view, + Rectangle( + oldLeft, + oldTop, + oldRight - oldLeft, + oldBottom - oldTop, + ), + Rectangle( + newLeft, + newTop, + newRight - newLeft, + newBottom - newTop, + ), + ), + ) + } + } + } +} + +data class ChildrenChangeEventArgs( + val overlaysLayout: ViewGroup, + val childView: View, + val wasChildRemoved: Boolean, +) + +data class LayoutChangeEventArgs( + val overlaysLayout: ViewGroup, + val oldRect: Rectangle, + val newRect: Rectangle, +) 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..dc3fd8ca2 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/PlayerType.kt @@ -0,0 +1,139 @@ +package app.revanced.extension.youtube.shared + +import app.revanced.extension.shared.Logger +import app.revanced.extension.youtube.Event +import app.revanced.extension.youtube.patches.VideoInformation + +/** + * Main 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 = 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 = 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]. + * + * @see VideoInformation + */ + 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. + * @see VideoInformation + */ + 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). + * @see VideoInformation + */ + fun isNoneHiddenOrMinimized(): Boolean { + return isNoneHiddenOrSlidingMinimized() || this == WATCH_WHILE_MINIMIZED + } + + fun isMaximizedOrFullscreen(): Boolean { + return this == WATCH_WHILE_MAXIMIZED || this == WATCH_WHILE_FULLSCREEN + } +} 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..e01cb0249 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/shared/VideoState.kt @@ -0,0 +1,51 @@ +package app.revanced.extension.youtube.shared + +import app.revanced.extension.shared.Logger +import app.revanced.extension.youtube.patches.VideoInformation + +/** + * VideoState playback state. + */ +enum class VideoState { + NEW, + PLAYING, + PAUSED, + RECOVERABLE_ERROR, + UNRECOVERABLE_ERROR, + + /** + * @see [VideoInformation.isAtEndOfVideo] + */ + ENDED, + + ; + + companion object { + + private val nameToVideoState = values().associateBy { it.name } + + @JvmStatic + fun setFromString(enumName: String) { + val state = nameToVideoState[enumName] + if (state == null) { + Logger.printException { "Unknown VideoState encountered: $enumName" } + } else if (currentVideoState != state) { + Logger.printDebug { "VideoState changed to: $state" } + currentVideoState = 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: VideoState? + get() = currentVideoState + private set(value) { + currentVideoState = value + } + + private var currentVideoState: VideoState? = null + } +} 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..3f48930e3 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SegmentPlaybackController.java @@ -0,0 +1,771 @@ +package app.revanced.extension.youtube.sponsorblock; + +import static app.revanced.extension.shared.StringRef.str; + +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.*; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.patches.VideoInformation; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; +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; + +/** + * 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. + */ +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; + + @Nullable + private static String currentVideoId; + @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; + + /** + * 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<>(); + + /** + * 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; + + @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. + */ + private static void clearData() { + currentVideoId = null; + 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(VideoInformation.PlaybackController ignoredPlayerController) { + 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 setCurrentVideoId(@Nullable String videoId) { + try { + if (Objects.equals(currentVideoId, videoId)) { + return; + } + clearData(); + if (videoId == null || !Settings.SB_ENABLED.get()) { + return; + } + if (PlayerType.getCurrent().isNoneOrHidden()) { + Logger.printDebug(() -> "ignoring Short"); + return; + } + if (!Utils.isNetworkConnected()) { + 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); + + 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 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() + || PlayerType.getCurrent().isNoneOrHidden() // Shorts playback. + || segments == null || segments.length == 0) { + return; + } + Logger.printDebug(() -> "setVideoTime: " + 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 * 1200); + 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 SponsorSegment lastSegmentSkipped; + private static long lastSegmentSkippedTime; + + 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); + 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 int toastNumberOfSegmentsSkipped; + @Nullable + private static SponsorSegment toastSegmentSkipped; + + 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 + */ + @SuppressWarnings("unused") + 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) { + 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 + */ + @SuppressWarnings("unused") + public static void setSponsorBarThickness(int thickness) { + sponsorBarThickness = thickness; + } + + /** + * Injection point. + */ + @SuppressWarnings("unused") + 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; + } + + private static void calculateTimeWithoutSegments() { + final long currentVideoLength = VideoInformation.getVideoLength(); + if (!Settings.SB_VIDEO_LENGTH_WITHOUT_SEGMENTS.get() || currentVideoLength <= 0 + || segments == null || segments.length == 0) { + timeWithoutSegments = null; + return; + } + + boolean foundNonhighlightSegments = false; + long timeWithoutSegmentsValue = currentVideoLength; + + 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 highlightSegmentTimeBarScreenWidth = -1; // actual pixel width to use + 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. + */ + @SuppressWarnings("unused") + 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; + 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..0edc054c7 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockSettings.java @@ -0,0 +1,244 @@ +package app.revanced.extension.youtube.sponsorblock; + +import static app.revanced.extension.shared.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.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.Setting; +import app.revanced.extension.youtube.settings.Settings; +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(); + } +} 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..d3f851f6e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/SponsorBlockUtils.java @@ -0,0 +1,503 @@ +package app.revanced.extension.youtube.sponsorblock; + +import static app.revanced.extension.shared.StringRef.str; + +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.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.patches.VideoInformation; +import app.revanced.extension.youtube.settings.Settings; +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. + */ +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) { + case DialogInterface.BUTTON_NEGATIVE: + // start + newSponsorSegmentStartMillis = newSponsorSegmentDialogShownMillis; + break; + case DialogInterface.BUTTON_POSITIVE: + // end + newSponsorSegmentEndMillis = newSponsorSegmentDialogShownMillis; + break; + } + 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: + case DOWNVOTE: + SBRequester.voteForSegmentOnBackgroundThread(segment, voteOption); + break; + case CATEGORY_CHANGE: + onNewCategorySelect(segment, context); + break; + } + }) + .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 = VideoInformation.getVideoId(); + final long videoLength = VideoInformation.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_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_content", + 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); + } + } 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 = VideoInformation.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); + } + + public static String getTimeSavedString(long totalSecondsSaved) { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + 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); + } + return "error"; // will never be reached. YouTube requires Android O or greater + } + + 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); + } + } + } +} 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..7cd4a44c5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/CategoryBehaviour.java @@ -0,0 +1,122 @@ +package app.revanced.extension.youtube.sponsorblock.objects; + +import static app.revanced.extension.shared.StringRef.sf; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.Objects; + +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.StringRef; + +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++; + } + } + } + + static String[] getBehaviorKeyValues() { + if (behaviorKeyValues == null) { + createNameAndKeyArrays(); + } + return behaviorKeyValues; + } + static String[] getBehaviorKeyValuesWithoutSkipOnce() { + if (behaviorKeyValuesWithoutSkipOnce == null) { + createNameAndKeyArrays(); + } + return behaviorKeyValuesWithoutSkipOnce; + } + + static String[] getBehaviorDescriptions() { + if (behaviorDescriptions == null) { + createNameAndKeyArrays(); + } + return behaviorDescriptions; + } + 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..5be3755ae --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategory.java @@ -0,0 +1,332 @@ +package app.revanced.extension.youtube.sponsorblock.objects; + +import static app.revanced.extension.youtube.settings.Settings.*; +import static app.revanced.extension.shared.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.Utils; +import app.revanced.extension.shared.settings.StringSetting; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.StringRef; + +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), + /** + * Unique category that is treated differently than the rest. + */ + HIGHLIGHT("poi_highlight", sf("revanced_sb_segments_highlight"), sf("revanced_sb_segments_highlight_sum"), 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_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), + UNSUBMITTED("unsubmitted", StringRef.empty, 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; + @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.IGNORE; + + 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); + } + + @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/SegmentCategoryListPreference.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java new file mode 100644 index 000000000..ec83d9122 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/objects/SegmentCategoryListPreference.java @@ -0,0 +1,159 @@ +package app.revanced.extension.youtube.sponsorblock.objects; + +import static app.revanced.extension.shared.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.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.Logger; +import app.revanced.extension.shared.Utils; + +@SuppressWarnings("deprecation") +public class SegmentCategoryListPreference extends ListPreference { + private final SegmentCategory category; + private EditText mEditText; + private int mClickedDialogEntryIndex; + + public SegmentCategoryListPreference(Context context, SegmentCategory category) { + super(context); + final boolean isHighlightCategory = category == SegmentCategory.HIGHLIGHT; + this.category = Objects.requireNonNull(category); + + // Edit: Using preferences to sync together multiple pieces + // of code together is messy and should be rethought. + setKey(category.behaviorSetting.key); + setDefaultValue(category.behaviorSetting.defaultValue); + setEntries(isHighlightCategory + ? CategoryBehaviour.getBehaviorDescriptionsWithoutSkipOnce() + : CategoryBehaviour.getBehaviorDescriptions()); + setEntryValues(isHighlightCategory + ? CategoryBehaviour.getBehaviorKeyValuesWithoutSkipOnce() + : CategoryBehaviour.getBehaviorKeyValues()); + + setSummary(category.description.toString()); + updateTitle(); + } + + @Override + protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { + try { + Utils.setEditTextDialogTheme(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(category.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(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) -> { + onClick(dialog, DialogInterface.BUTTON_POSITIVE); + }); + builder.setNeutralButton(str("revanced_sb_reset_color"), (dialog, which) -> { + try { + category.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); + category.setBehaviour(Objects.requireNonNull(CategoryBehaviour.byReVancedKeyValue(value))); + 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")); + } + updateTitle(); + } + } catch (Exception ex) { + Logger.printException(() -> "onDialogClosed failure", ex); + } + } + + private void updateTitle() { + setTitle(category.getTitleWithColorDot()); + } +} \ No newline at end of file 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..811cb87c4 --- /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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import app.revanced.extension.youtube.patches.VideoInformation; +import app.revanced.extension.shared.StringRef; + +import java.util.Objects; + +import static app.revanced.extension.shared.StringRef.sf; + +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, VideoInformation.getVideoLength()).toString(); + } + + /** + * @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)) return false; + SponsorSegment other = (SponsorSegment) o; + 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..4889c7671 --- /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..7cb9122d9 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRequester.java @@ -0,0 +1,315 @@ +package app.revanced.extension.youtube.sponsorblock.requests; + +import static app.revanced.extension.shared.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.youtube.requests.Requester; +import app.revanced.extension.youtube.requests.Route; +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; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; + +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); + } + + // Crude debug tests to verify random features + // Could benefit from: + // 1) collection of YouTube videos with test segment times (verify client skip timing matches the video, verify seekbar draws correctly) + // 2) unit tests (verify everything else) + if (false) { + segments.clear(); + // Test auto-hide skip button: + // Button should appear only once + segments.add(new SponsorSegment(SegmentCategory.INTRO, "debug", 5000, 120000, false)); + // Button should appear only once + segments.add(new SponsorSegment(SegmentCategory.SELF_PROMO, "debug", 10000, 60000, false)); + // Button should appear only once + segments.add(new SponsorSegment(SegmentCategory.INTERACTION, "debug", 15000, 20000, false)); + // Button should appear _twice_ (at 21s and 27s) + segments.add(new SponsorSegment(SegmentCategory.SPONSOR, "debug", 21000, 30000, false)); + // Button should appear only once + segments.add(new SponsorSegment(SegmentCategory.OUTRO, "debug", 24000, 27000, false)); + + + // Test seekbar visibility: + // All three segments should be viewable on the seekbar + segments.add(new SponsorSegment(SegmentCategory.MUSIC_OFFTOPIC, "debug", 200000, 300000, false)); + segments.add(new SponsorSegment(SegmentCategory.SPONSOR, "debug", 200000, 250000, false)); + segments.add(new SponsorSegment(SegmentCategory.SELF_PROMO, "debug", 200000, 330000, false)); + } + + 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: + messageToToast = str("revanced_sb_submit_succeeded"); + break; + case 409: + messageToToast = str("revanced_sb_submit_failed_duplicate"); + break; + case 403: + messageToToast = str("revanced_sb_submit_failed_forbidden", Requester.parseErrorStringAndDisconnect(connection)); + break; + case 429: + messageToToast = str("revanced_sb_submit_failed_rate_limit"); + break; + case 400: + messageToToast = str("revanced_sb_submit_failed_invalid", Requester.parseErrorStringAndDisconnect(connection)); + break; + default: + messageToToast = str("revanced_sb_submit_failed_unknown_error", responseCode, connection.getResponseMessage()); + break; + } + 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/requests/SBRoutes.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRoutes.java new file mode 100644 index 000000000..fe3403e56 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/requests/SBRoutes.java @@ -0,0 +1,20 @@ +package app.revanced.extension.youtube.sponsorblock.requests; + +import static app.revanced.extension.youtube.requests.Route.Method.GET; +import static app.revanced.extension.youtube.requests.Route.Method.POST; + +import app.revanced.extension.youtube.requests.Route; + +class SBRoutes { + static final Route IS_USER_VIP = new Route(GET, "/api/isUserVIP?userID={user_id}"); + static final Route GET_SEGMENTS = new Route(GET, "/api/skipSegments?videoID={video_id}&categories={categories}"); + static final Route VIEWED_SEGMENT = new Route(POST, "/api/viewedVideoSponsorTime?UUID={segment_id}"); + static final Route GET_USER_STATS = new Route(GET, "/api/userInfo?userID={user_id}&values=[\"userID\",\"userName\",\"reputation\",\"segmentCount\",\"ignoredSegmentCount\",\"viewCount\",\"minutesSaved\"]"); + static final Route CHANGE_USERNAME = new Route(POST, "/api/setUsername?userID={user_id}&username={username}"); + 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}"); + static final Route VOTE_ON_SEGMENT_QUALITY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&type={type}"); + static final Route VOTE_ON_SEGMENT_CATEGORY = new Route(POST, "/api/voteOnSponsorTime?userID={user_id}&UUID={segment_id}&category={category}"); + + private SBRoutes() { + } +} \ No newline at end of file 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..4ec6c35b7 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/CreateSegmentButtonController.java @@ -0,0 +1,111 @@ +package app.revanced.extension.youtube.sponsorblock.ui; + +import static app.revanced.extension.shared.Utils.getResourceIdentifier; + +import android.view.View; +import android.widget.ImageView; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +import app.revanced.extension.youtube.patches.VideoInformation; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.videoplayer.PlayerControlButton; + +// Edit: This should be a subclass of PlayerControlButton +public class CreateSegmentButtonController { + private static WeakReference buttonReference = new WeakReference<>(null); + private static boolean isShowing; + + /** + * injection point + */ + public static void initialize(View youtubeControlsLayout) { + try { + Logger.printDebug(() -> "initializing new segment button"); + ImageView imageView = Objects.requireNonNull(Utils.getChildViewByResourceName( + youtubeControlsLayout, "revanced_sb_create_segment_button")); + imageView.setVisibility(View.GONE); + imageView.setOnClickListener(v -> SponsorBlockViewController.toggleNewSegmentLayoutVisibility()); + + buttonReference = new WeakReference<>(imageView); + } catch (Exception ex) { + Logger.printException(() -> "initialize failure", ex); + } + } + + /** + * injection point + */ + public static void changeVisibilityImmediate(boolean visible) { + if (visible) { + // Fix button flickering, by pushing this call to the back of + // the main thread and letting other layout code run first. + Utils.runOnMainThread(() -> setVisibility(true, false)); + } else { + setVisibility(false, false); + } + } + + /** + * injection point + */ + public static void changeVisibility(boolean visible, boolean animated) { + // Ignore this call, otherwise with full screen thumbnails the buttons are visible while seeking. + if (visible && !animated) return; + + setVisibility(visible, animated); + } + + private static void setVisibility(boolean visible, boolean animated) { + try { + if (isShowing == visible) return; + isShowing = visible; + + ImageView iView = buttonReference.get(); + if (iView == null) return; + + if (visible) { + iView.clearAnimation(); + if (!shouldBeShown()) { + return; + } + if (animated) { + iView.startAnimation(PlayerControlButton.getButtonFadeIn()); + } + iView.setVisibility(View.VISIBLE); + return; + } + + if (iView.getVisibility() == View.VISIBLE) { + iView.clearAnimation(); + if (animated) { + iView.startAnimation(PlayerControlButton.getButtonFadeOut()); + } + iView.setVisibility(View.GONE); + } + } catch (Exception ex) { + Logger.printException(() -> "changeVisibility failure", ex); + } + } + + private static boolean shouldBeShown() { + return Settings.SB_ENABLED.get() && Settings.SB_CREATE_NEW_SEGMENT.get() + && !VideoInformation.isAtEndOfVideo(); + } + + public static void hide() { + if (!isShowing) { + return; + } + Utils.verifyOnMainThread(); + View v = buttonReference.get(); + if (v == null) { + return; + } + v.setVisibility(View.GONE); + isShowing = 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..38e1f113c --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/NewSegmentLayout.java @@ -0,0 +1,127 @@ +package app.revanced.extension.youtube.sponsorblock.ui; + +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.youtube.patches.VideoInformation; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils; +import app.revanced.extension.shared.Logger; + +import static app.revanced.extension.shared.Utils.getResourceDimensionPixelSize; +import static app.revanced.extension.shared.Utils.getResourceIdentifier; + +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; + + final int defaultBottomMargin; + final int ctaBottomMargin; + + 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( + getResourceIdentifier(context, "revanced_sb_new_segment", "layout"), 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" + ); + + defaultBottomMargin = getResourceDimensionPixelSize("brand_interaction_default_bottom_margin"); + ctaBottomMargin = getResourceDimensionPixelSize("brand_interaction_cta_bottom_margin"); + } + + /** + * 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) { + final ImageButton button = findViewById(getResourceIdentifier(context, resourceIdentifierName, "id")); + + // Add ripple effect + button.setBackgroundResource(rippleEffectId); + RippleDrawable rippleDrawable = (RippleDrawable) button.getBackground(); + rippleDrawable.setColor(rippleColorStateList); + + button.setOnClickListener((v) -> { + handler.apply(); + Logger.printDebug(() -> debugMessage); + }); + } + + @FunctionalInterface + public 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..11813aa84 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SkipSponsorButton.java @@ -0,0 +1,101 @@ +package app.revanced.extension.youtube.sponsorblock.ui; + +import static app.revanced.extension.shared.Utils.getResourceColor; +import static app.revanced.extension.shared.Utils.getResourceDimension; +import static app.revanced.extension.shared.Utils.getResourceDimensionPixelSize; +import static app.revanced.extension.shared.Utils.getResourceIdentifier; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +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 static final boolean highContrast = true; + private final LinearLayout skipSponsorBtnContainer; + private final TextView skipSponsorTextView; + private final Paint background; + private final Paint border; + private SponsorSegment segment; + final int defaultBottomMargin; + final int ctaBottomMargin; + + 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(getResourceIdentifier(context, "revanced_sb_skip_sponsor_button", "layout"), this, true); // layout:skip_ad_button + setMinimumHeight(getResourceDimensionPixelSize("ad_skip_ad_button_min_height")); // dimen:ad_skip_ad_button_min_height + skipSponsorBtnContainer = Objects.requireNonNull((LinearLayout) findViewById(getResourceIdentifier(context, "revanced_sb_skip_sponsor_button_container", "id"))); // id:skip_ad_button_container + background = new Paint(); + background.setColor(getResourceColor("skip_ad_button_background_color")); // color:skip_ad_button_background_color); + background.setStyle(Paint.Style.FILL); + border = new Paint(); + border.setColor(getResourceColor("skip_ad_button_border_color")); // color:skip_ad_button_border_color); + border.setStrokeWidth(getResourceDimension("ad_skip_ad_button_border_width")); // dimen:ad_skip_ad_button_border_width); + border.setStyle(Paint.Style.STROKE); + skipSponsorTextView = Objects.requireNonNull((TextView) findViewById(getResourceIdentifier(context, "revanced_sb_skip_sponsor_button_text", "id"))); // id:skip_ad_button_text; + defaultBottomMargin = getResourceDimensionPixelSize("skip_button_default_bottom_margin"); // dimen:skip_button_default_bottom_margin + ctaBottomMargin = getResourceDimensionPixelSize("skip_button_cta_bottom_margin"); // dimen:skip_button_cta_bottom_margin + + 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); + }); + } + + @Override // android.view.ViewGroup + protected final void dispatchDraw(Canvas canvas) { + final int left = skipSponsorBtnContainer.getLeft(); + final int top = skipSponsorBtnContainer.getTop(); + final int leftPlusWidth = (left + skipSponsorBtnContainer.getWidth()); + final int topPlusHeight = (top + skipSponsorBtnContainer.getHeight()); + canvas.drawRect(left, top, leftPlusWidth, topPlusHeight, background); + if (!highContrast) { + canvas.drawLines(new float[]{ + leftPlusWidth, top, left, top, + left, top, left, topPlusHeight, + left, topPlusHeight, leftPlusWidth, topPlusHeight}, + border); + } + + super.dispatchDraw(canvas); + } + + /** + * @return true, if this button state was changed + */ + public boolean updateSkipButtonText(@NonNull SponsorSegment segment) { + this.segment = segment; + CharSequence newText = segment.getSkipButtonText(); + if (newText.equals(skipSponsorTextView.getText())) { + 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..099f0d56e --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController.java @@ -0,0 +1,224 @@ +package app.revanced.extension.youtube.sponsorblock.ui; + +import static app.revanced.extension.shared.Utils.getResourceIdentifier; + +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.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.shared.PlayerType; +import app.revanced.extension.youtube.sponsorblock.objects.SponsorSegment; + +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; + + static { + PlayerType.getOnChange().addObserver((PlayerType type) -> { + playerTypeChanged(type); + return null; + }); + } + + 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(getResourceIdentifier("revanced_sb_inline_sponsor_overlay", "layout"), 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(layout.findViewById(getResourceIdentifier("revanced_sb_skip_highlight_button", "id")))); + skipSponsorButtonRef = new WeakReference<>( + Objects.requireNonNull(layout.findViewById(getResourceIdentifier("revanced_sb_skip_sponsor_button", "id")))); + newSegmentLayoutRef = new WeakReference<>( + Objects.requireNonNull(layout.findViewById(getResourceIdentifier("revanced_sb_new_segment_view", "id")))); + + 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, layout.defaultBottomMargin, layout.ctaBottomMargin); + } + } + private static void setSkipButtonMargins(@Nullable SkipSponsorButton button, boolean fullScreen) { + if (button != null) { + setLayoutMargins(button, fullScreen, button.defaultBottomMargin, button.ctaBottomMargin); + } + } + private static void setLayoutMargins(@NonNull View view, boolean fullScreen, + int defaultBottomMargin, int ctaBottomMargin) { + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) view.getLayoutParams(); + if (params == null) { + Logger.printException(() -> "Unable to setNewSegmentLayoutMargins (params are null)"); + return; + } + params.bottomMargin = fullScreen ? 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.AUTO_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..bad5f2484 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/sponsorblock/ui/VotingButtonController.java @@ -0,0 +1,116 @@ +package app.revanced.extension.youtube.sponsorblock.ui; + +import static app.revanced.extension.shared.Utils.getResourceIdentifier; + +import android.view.View; +import android.widget.ImageView; + +import java.lang.ref.WeakReference; +import java.util.Objects; + +import app.revanced.extension.youtube.patches.VideoInformation; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.youtube.sponsorblock.SegmentPlaybackController; +import app.revanced.extension.youtube.sponsorblock.SponsorBlockUtils; +import app.revanced.extension.shared.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.youtube.videoplayer.PlayerControlButton; + +// Edit: This should be a subclass of PlayerControlButton +public class VotingButtonController { + private static WeakReference buttonReference = new WeakReference<>(null); + private static boolean isShowing; + + /** + * injection point + */ + public static void initialize(View youtubeControlsLayout) { + try { + Logger.printDebug(() -> "initializing voting button"); + ImageView imageView = Objects.requireNonNull(Utils.getChildViewByResourceName( + 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(() -> "initialize failure", ex); + } + } + + /** + * injection point + */ + public static void changeVisibilityImmediate(boolean visible) { + if (visible) { + // Fix button flickering, by pushing this call to the back of + // the main thread and letting other layout code run first. + Utils.runOnMainThread(() -> setVisibility(true, false)); + } else { + setVisibility(false, false); + } + } + + /** + * injection point + */ + public static void changeVisibility(boolean visible, boolean animated) { + // Ignore this call, otherwise with full screen thumbnails the buttons are visible while seeking. + if (visible && !animated) return; + + setVisibility(visible, animated); + } + + /** + * injection point + */ + private static void setVisibility(boolean visible, boolean animated) { + try { + if (isShowing == visible) return; + isShowing = visible; + + ImageView iView = buttonReference.get(); + if (iView == null) return; + + if (visible) { + iView.clearAnimation(); + if (!shouldBeShown()) { + return; + } + if (animated) { + iView.startAnimation(PlayerControlButton.getButtonFadeIn()); + } + iView.setVisibility(View.VISIBLE); + return; + } + + if (iView.getVisibility() == View.VISIBLE) { + iView.clearAnimation(); + if (animated) { + iView.startAnimation(PlayerControlButton.getButtonFadeOut()); + } + iView.setVisibility(View.GONE); + } + } catch (Exception ex) { + Logger.printException(() -> "changeVisibility failure", ex); + } + } + + private static boolean shouldBeShown() { + return Settings.SB_ENABLED.get() && Settings.SB_VOTING_BUTTON.get() + && SegmentPlaybackController.videoHasSegments() && !VideoInformation.isAtEndOfVideo(); + } + + public static void hide() { + if (!isShowing) { + return; + } + Utils.verifyOnMainThread(); + View v = buttonReference.get(); + if (v == null) { + return; + } + v.setVisibility(View.GONE); + isShowing = 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..f9850d99a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/SwipeControlsConfigurationProvider.kt @@ -0,0 +1,120 @@ +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.PlayerType + +/** + * 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.SWIPE_VOLUME.get() + + /** + * should swipe controls for volume be enabled? + */ + val enableBrightnessControl: Boolean + get() = Settings.SWIPE_BRIGHTNESS.get() + + /** + * is the video player currently in fullscreen mode? + */ + private val isFullscreenVideo: Boolean + get() = PlayerType.current == PlayerType.WATCH_WHILE_FULLSCREEN +//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.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() +//endregion + +//region overlay adjustments + + /** + * should the overlay enable haptic feedback? + */ + val shouldEnableHapticFeedback: Boolean + get() = Settings.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.SWIPE_SAVE_AND_RESTORE_BRIGHTNESS.get() + + /** + * should auto-brightness be enabled at the lowest value of the brightness gesture + */ + val shouldLowestValueEnableAutoBrightness: Boolean + get() = Settings.SWIPE_LOWEST_VALUE_ENABLE_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 +} 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..afb55d74b --- /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.Logger.printDebug +import app.revanced.extension.shared.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/extension/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) + } else { + ClassicSwipeController(this) + } + + companion object { + /** + * the currently active swipe controls host. + * the reference may be null! + */ + @JvmStatic + var currentHost: WeakReference = WeakReference(null) + private set + } +} 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..c8b1884bd --- /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 android.os.Build +import app.revanced.extension.shared.Logger.printException +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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + 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..f291bcb41 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/ScreenBrightnessController.kt @@ -0,0 +1,73 @@ +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) + } + + /** + * is the screen brightness set to device- default? + */ + val isDefaultBrightness + get() = (rawScreenBrightness == WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE) + + /** + * 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] + */ + 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..2c2edb959 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/SwipeZonesController.kt @@ -0,0 +1,144 @@ +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 +import app.revanced.extension.youtube.swipecontrols.misc.Rectangle +import app.revanced.extension.youtube.swipecontrols.misc.applyDimension +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, +) { + /** + * 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 = Utils.getResourceIdentifier(host, "player_view", "id") + + /** + * 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 eRect = effectiveSwipeRect + val zoneWidth = (eRect.width * 3) / 8 + return Rectangle( + eRect.right - zoneWidth, + eRect.top, + zoneWidth, + eRect.height, + ) + } + + /** + * the rectangle of the screen brightness control zone + */ + val brightness: Rectangle + get() { + val zoneWidth = (effectiveSwipeRect.width * 3) / 8 + 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..d2b8788df --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/VolumeKeysController.kt @@ -0,0 +1,51 @@ +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..a0d2c6db6 --- /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.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, +) : 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 = 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 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 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..12c5b3025 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/PressToSwipeController.kt @@ -0,0 +1,78 @@ +package app.revanced.extension.youtube.swipecontrols.controller.gesture + +import android.view.MotionEvent +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, +) : 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 not in swipe session or vertical + if (!isInSwipeSession || 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/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..fca1da9f8 --- /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, + 10, + 1, + ) { + + /** + * 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..e398696df --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/controller/gesture/core/VolumeAndBrightnessScroller.kt @@ -0,0 +1,102 @@ +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: Int = 10, + brightnessDistance: Int = 1, +) : 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..67512f0f4 --- /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: Int, + 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..409d7ad4b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/misc/SwipeControlsUtils.kt @@ -0,0 +1,25 @@ +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() +} 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..9972c5233 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/swipecontrols/views/SwipeControlsOverlayLayout.kt @@ -0,0 +1,145 @@ +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.StringRef.str +import app.revanced.extension.shared.Utils +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( + Utils.getResourceIdentifier(context, name, "drawable"), + context.theme, + ).apply { + setTint(config.overlayForegroundColor) + setBounds( + 0, + 0, + width, + height, + ) + } + } + + init { + // init views + val feedbackTextViewPadding = 2.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( + feedbackTextViewPadding, + feedbackTextViewPadding, + feedbackTextViewPadding, + feedbackTextViewPadding, + ) + } + background = GradientDrawable().apply { + cornerRadius = 8f + 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("revanced_ic_sc_brightness_auto", iconHeight, iconHeight) + manualBrightnessIcon = getDrawable("revanced_ic_sc_brightness_manual", iconHeight, iconHeight) + mutedVolumeIcon = getDrawable("revanced_ic_sc_volume_mute", iconHeight, iconHeight) + normalVolumeIcon = getDrawable("revanced_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_enable_auto_brightness_overlay_text"), + autoBrightnessIcon, + ) + } else if (brightness >= 0) { + showFeedbackView("${round(brightness).toInt()}%", manualBrightnessIcon) + } + } + + override fun onEnterSwipeSession() { + if (config.shouldEnableHapticFeedback) { + @Suppress("DEPRECATION") + performHapticFeedback( + HapticFeedbackConstants.LONG_PRESS, + HapticFeedbackConstants.FLAG_IGNORE_GLOBAL_SETTING, + ) + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlButton.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlButton.java new file mode 100644 index 000000000..5b877cdc6 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlButton.java @@ -0,0 +1,54 @@ +package app.revanced.extension.youtube.videoplayer; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import app.revanced.extension.youtube.patches.CopyVideoUrlPatch; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public class CopyVideoUrlButton extends PlayerControlButton { + @Nullable + private static CopyVideoUrlButton instance; + + public CopyVideoUrlButton(ViewGroup viewGroup) { + super( + viewGroup, + "revanced_copy_video_url_button", + Settings.COPY_VIDEO_URL, + view -> CopyVideoUrlPatch.copyUrl(false), + view -> { + CopyVideoUrlPatch.copyUrl(true); + return true; + } + ); + } + + /** + * Injection point. + */ + public static void initializeButton(View view) { + try { + instance = new CopyVideoUrlButton((ViewGroup) view); + } catch (Exception ex) { + Logger.printException(() -> "initializeButton failure", ex); + } + } + + /** + * injection point + */ + public static void changeVisibilityImmediate(boolean visible) { + if (instance != null) instance.setVisibilityImmediate(visible); + } + + /** + * injection point + */ + public static void changeVisibility(boolean visible, boolean animated) { + if (instance != null) instance.setVisibility(visible, animated); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlTimestampButton.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlTimestampButton.java new file mode 100644 index 000000000..ebb3f518a --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/CopyVideoUrlTimestampButton.java @@ -0,0 +1,54 @@ +package app.revanced.extension.youtube.videoplayer; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import app.revanced.extension.youtube.patches.CopyVideoUrlPatch; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public class CopyVideoUrlTimestampButton extends PlayerControlButton { + @Nullable + private static CopyVideoUrlTimestampButton instance; + + public CopyVideoUrlTimestampButton(ViewGroup bottomControlsViewGroup) { + super( + bottomControlsViewGroup, + "revanced_copy_video_url_timestamp_button", + Settings.COPY_VIDEO_URL_TIMESTAMP, + view -> CopyVideoUrlPatch.copyUrl(true), + view -> { + CopyVideoUrlPatch.copyUrl(false); + return true; + } + ); + } + + /** + * Injection point. + */ + public static void initializeButton(View bottomControlsViewGroup) { + try { + instance = new CopyVideoUrlTimestampButton((ViewGroup) bottomControlsViewGroup); + } catch (Exception ex) { + Logger.printException(() -> "initializeButton failure", ex); + } + } + + /** + * injection point + */ + public static void changeVisibilityImmediate(boolean visible) { + if (instance != null) instance.setVisibilityImmediate(visible); + } + + /** + * injection point + */ + public static void changeVisibility(boolean visible, boolean animated) { + if (instance != null) instance.setVisibility(visible, animated); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/ExternalDownloadButton.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/ExternalDownloadButton.java new file mode 100644 index 000000000..bfd6d30e5 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/ExternalDownloadButton.java @@ -0,0 +1,60 @@ +package app.revanced.extension.youtube.videoplayer; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import app.revanced.extension.shared.Logger; +import app.revanced.extension.youtube.patches.DownloadsPatch; +import app.revanced.extension.youtube.patches.VideoInformation; +import app.revanced.extension.youtube.settings.Settings; + +@SuppressWarnings("unused") +public class ExternalDownloadButton extends PlayerControlButton { + @Nullable + private static ExternalDownloadButton instance; + + public ExternalDownloadButton(ViewGroup viewGroup) { + super( + viewGroup, + "revanced_external_download_button", + Settings.EXTERNAL_DOWNLOADER, + ExternalDownloadButton::onDownloadClick, + null + ); + } + + /** + * Injection point. + */ + public static void initializeButton(View view) { + try { + instance = new ExternalDownloadButton((ViewGroup) view); + } catch (Exception ex) { + Logger.printException(() -> "initializeButton failure", ex); + } + } + + /** + * injection point + */ + public static void changeVisibilityImmediate(boolean visible) { + if (instance != null) instance.setVisibilityImmediate(visible); + } + + /** + * injection point + */ + public static void changeVisibility(boolean visible, boolean animated) { + if (instance != null) instance.setVisibility(visible, animated); + } + + private static void onDownloadClick(View view) { + DownloadsPatch.launchExternalDownloader( + VideoInformation.getVideoId(), + view.getContext(), + true); + } +} + diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/PlaybackSpeedDialogButton.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/PlaybackSpeedDialogButton.java new file mode 100644 index 000000000..2954eac15 --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/PlaybackSpeedDialogButton.java @@ -0,0 +1,51 @@ +package app.revanced.extension.youtube.videoplayer; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import app.revanced.extension.youtube.patches.playback.speed.CustomPlaybackSpeedPatch; +import app.revanced.extension.youtube.settings.Settings; +import app.revanced.extension.shared.Logger; + +@SuppressWarnings("unused") +public class PlaybackSpeedDialogButton extends PlayerControlButton { + @Nullable + private static PlaybackSpeedDialogButton instance; + + public PlaybackSpeedDialogButton(ViewGroup viewGroup) { + super( + viewGroup, + "revanced_playback_speed_dialog_button", + Settings.PLAYBACK_SPEED_DIALOG_BUTTON, + view -> CustomPlaybackSpeedPatch.showOldPlaybackSpeedMenu(), + null + ); + } + + /** + * Injection point. + */ + public static void initializeButton(View view) { + try { + instance = new PlaybackSpeedDialogButton((ViewGroup) view); + } catch (Exception ex) { + Logger.printException(() -> "initializeButton failure", ex); + } + } + + /** + * injection point + */ + public static void changeVisibilityImmediate(boolean visible) { + if (instance != null) instance.setVisibilityImmediate(visible); + } + + /** + * injection point + */ + public static void changeVisibility(boolean visible, boolean animated) { + if (instance != null) instance.setVisibility(visible, animated); + } +} \ No newline at end of file diff --git a/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java b/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java new file mode 100644 index 000000000..71e176c1f --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/youtube/videoplayer/PlayerControlButton.java @@ -0,0 +1,117 @@ +package app.revanced.extension.youtube.videoplayer; + +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.Logger; +import app.revanced.extension.shared.Utils; +import app.revanced.extension.shared.settings.BooleanSetting; + +public abstract class PlayerControlButton { + private static final Animation fadeIn; + private static final Animation fadeOut; + private static final Animation fadeOutImmediate; + + private final WeakReference buttonRef; + protected final BooleanSetting setting; + protected boolean isVisible; + + static { + // TODO: check if these durations are correct. + fadeIn = Utils.getResourceAnimation("fade_in"); + fadeIn.setDuration(Utils.getResourceInteger("fade_duration_fast")); + + fadeOut = Utils.getResourceAnimation("fade_out"); + fadeOut.setDuration(Utils.getResourceInteger("fade_duration_scheduled")); + + fadeOutImmediate = Utils.getResourceAnimation("abc_fade_out"); + fadeOutImmediate.setDuration(Utils.getResourceInteger("fade_duration_fast")); + } + + @NonNull + public static Animation getButtonFadeIn() { + return fadeIn; + } + + @NonNull + public static Animation getButtonFadeOut() { + return fadeOut; + } + + @NonNull + public static Animation getButtonFadeOutImmediately() { + return fadeOutImmediate; + } + + public PlayerControlButton(@NonNull ViewGroup bottomControlsViewGroup, @NonNull String imageViewButtonId, + @NonNull BooleanSetting booleanSetting, @NonNull View.OnClickListener onClickListener, + @Nullable View.OnLongClickListener longClickListener) { + Logger.printDebug(() -> "Initializing button: " + imageViewButtonId); + + ImageView imageView = Objects.requireNonNull(bottomControlsViewGroup.findViewById( + Utils.getResourceIdentifier(imageViewButtonId, "id") + )); + imageView.setVisibility(View.GONE); + + imageView.setOnClickListener(onClickListener); + if (longClickListener != null) { + imageView.setOnLongClickListener(longClickListener); + } + + setting = booleanSetting; + buttonRef = new WeakReference<>(imageView); + } + + public void setVisibilityImmediate(boolean visible) { + if (visible) { + // Fix button flickering, by pushing this call to the back of + // the main thread and letting other layout code run first. + Utils.runOnMainThread(() -> private_setVisibility(true, false)); + } else { + private_setVisibility(false, false); + } + } + + public void setVisibility(boolean visible, boolean animated) { + // Ignore this call, otherwise with full screen thumbnails the buttons are visible while seeking. + if (visible && !animated) return; + + private_setVisibility(visible, animated); + } + + private void private_setVisibility(boolean visible, boolean animated) { + try { + if (isVisible == visible) return; + isVisible = visible; + + ImageView iView = buttonRef.get(); + if (iView == null) { + return; + } + + if (visible && setting.get()) { + iView.clearAnimation(); + if (animated) { + iView.startAnimation(PlayerControlButton.getButtonFadeIn()); + } + iView.setVisibility(View.VISIBLE); + } else if (iView.getVisibility() == View.VISIBLE) { + iView.clearAnimation(); + if (animated) { + iView.startAnimation(PlayerControlButton.getButtonFadeOut()); + } + iView.setVisibility(View.GONE); + } + } catch (Exception ex) { + Logger.printException(() -> "setVisibility failure", ex); + } + } +} diff --git a/extensions/shared/stub/build.gradle.kts b/extensions/shared/stub/build.gradle.kts new file mode 100644 index 000000000..c1cc5794c --- /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 = 33 + + defaultConfig { + minSdk = 24 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} 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/constraint/ConstraintLayout.java b/extensions/shared/stub/src/main/java/android/support/constraint/ConstraintLayout.java new file mode 100644 index 000000000..2861235a5 --- /dev/null +++ b/extensions/shared/stub/src/main/java/android/support/constraint/ConstraintLayout.java @@ -0,0 +1,20 @@ +package android.support.constraint; + +import android.content.Context; +import android.view.ViewGroup; + +/** + * "CompileOnly" class + * because android.support.android.support.constraint.ConstraintLayout is deprecated + * in favour of androidx.constraintlayout.widget.ConstraintLayout. + * + * This class will not be included and "replaced" by the real package's class. + */ +public class ConstraintLayout extends ViewGroup { + public ConstraintLayout(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/RecyclerView.java b/extensions/shared/stub/src/main/java/android/support/v7/widget/RecyclerView.java new file mode 100644 index 000000000..d902dbfc8 --- /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.View; + +public class RecyclerView extends View { + + public RecyclerView(Context context) { + super(context); + } + + public View getChildAt(@SuppressWarnings("unused") final int index) { + return null; + } + + public int getChildCount() { + return 0; + } +} diff --git a/extensions/shared/stub/src/main/java/com/bytedance/ies/ugc/aweme/commercialize/compliance/personalization/AdPersonalizationActivity.java b/extensions/shared/stub/src/main/java/com/bytedance/ies/ugc/aweme/commercialize/compliance/personalization/AdPersonalizationActivity.java new file mode 100644 index 000000000..ecf204d22 --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/bytedance/ies/ugc/aweme/commercialize/compliance/personalization/AdPersonalizationActivity.java @@ -0,0 +1,6 @@ +package com.bytedance.ies.ugc.aweme.commercialize.compliance.personalization; + +import android.app.Activity; + +//Dummy class +public class AdPersonalizationActivity extends Activity { } diff --git a/extensions/shared/stub/src/main/java/com/google/android/apps/youtube/app/ui/SlimMetadataScrollableButtonContainerLayout.java b/extensions/shared/stub/src/main/java/com/google/android/apps/youtube/app/ui/SlimMetadataScrollableButtonContainerLayout.java new file mode 100644 index 000000000..9b314f997 --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/google/android/apps/youtube/app/ui/SlimMetadataScrollableButtonContainerLayout.java @@ -0,0 +1,25 @@ +package com.google.android.apps.youtube.app.ui; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.ViewGroup; + +public class SlimMetadataScrollableButtonContainerLayout extends ViewGroup { + + public SlimMetadataScrollableButtonContainerLayout(Context context) { + super(context); + } + + public SlimMetadataScrollableButtonContainerLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SlimMetadataScrollableButtonContainerLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @Override + protected void onLayout(boolean b, int i, int i1, int i2, int i3) { + + } +} 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/protos/youtube/api/innertube/InnertubeContext$ClientInfo.java b/extensions/shared/stub/src/main/java/com/google/protos/youtube/api/innertube/InnertubeContext$ClientInfo.java new file mode 100644 index 000000000..f517608f2 --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/google/protos/youtube/api/innertube/InnertubeContext$ClientInfo.java @@ -0,0 +1,5 @@ +package com.google.protos.youtube.api.innertube; + +public class InnertubeContext$ClientInfo { + public int r; +} diff --git a/extensions/shared/stub/src/main/java/com/laurencedawson/reddit_sync/ui/activities/WebViewActivity.java b/extensions/shared/stub/src/main/java/com/laurencedawson/reddit_sync/ui/activities/WebViewActivity.java new file mode 100644 index 000000000..a33226a85 --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/laurencedawson/reddit_sync/ui/activities/WebViewActivity.java @@ -0,0 +1,6 @@ +package com.laurencedawson.reddit_sync.ui.activities; + +import android.app.Activity; + +public class WebViewActivity extends Activity { +} 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/com/rubenmayayo/reddit/ui/activities/WebViewActivity.java b/extensions/shared/stub/src/main/java/com/rubenmayayo/reddit/ui/activities/WebViewActivity.java new file mode 100644 index 000000000..d0c585072 --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/rubenmayayo/reddit/ui/activities/WebViewActivity.java @@ -0,0 +1,6 @@ +package com.rubenmayayo.reddit.ui.activities; + +import android.app.Activity; + +public class WebViewActivity extends Activity { +} diff --git a/extensions/shared/stub/src/main/java/com/ss/android/ugc/aweme/feed/model/Aweme.java b/extensions/shared/stub/src/main/java/com/ss/android/ugc/aweme/feed/model/Aweme.java new file mode 100644 index 000000000..e1ea9af6c --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/ss/android/ugc/aweme/feed/model/Aweme.java @@ -0,0 +1,36 @@ +package com.ss.android.ugc.aweme.feed.model; + +//Dummy class +public class Aweme { + public boolean isAd() { + throw new UnsupportedOperationException("Stub"); + } + + public boolean isLive() { + throw new UnsupportedOperationException("Stub"); + } + + public boolean isLiveReplay() { + throw new UnsupportedOperationException("Stub"); + } + + public boolean isWithPromotionalMusic() { + throw new UnsupportedOperationException("Stub"); + } + + public boolean getIsTikTokStory() { + throw new UnsupportedOperationException("Stub"); + } + + public boolean isImage() { + throw new UnsupportedOperationException("Stub"); + } + + public boolean isPhotoMode() { + throw new UnsupportedOperationException("Stub"); + } + + public AwemeStatistics getStatistics() { + throw new UnsupportedOperationException("Stub"); + } +} diff --git a/extensions/shared/stub/src/main/java/com/ss/android/ugc/aweme/feed/model/AwemeStatistics.java b/extensions/shared/stub/src/main/java/com/ss/android/ugc/aweme/feed/model/AwemeStatistics.java new file mode 100644 index 000000000..a9123e67c --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/ss/android/ugc/aweme/feed/model/AwemeStatistics.java @@ -0,0 +1,10 @@ +package com.ss.android.ugc.aweme.feed.model; + +public class AwemeStatistics { + public long getPlayCount() { + throw new UnsupportedOperationException("Stub"); + } + public long getDiggCount() { + throw new UnsupportedOperationException("Stub"); + } +} diff --git a/extensions/shared/stub/src/main/java/com/ss/android/ugc/aweme/feed/model/FeedItemList.java b/extensions/shared/stub/src/main/java/com/ss/android/ugc/aweme/feed/model/FeedItemList.java new file mode 100644 index 000000000..139159ea4 --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/ss/android/ugc/aweme/feed/model/FeedItemList.java @@ -0,0 +1,8 @@ +package com.ss.android.ugc.aweme.feed.model; + +import java.util.List; + +//Dummy class +public class FeedItemList { + public List items; +} diff --git a/extensions/shared/stub/src/main/java/com/tumblr/rumblr/model/TimelineObject.java b/extensions/shared/stub/src/main/java/com/tumblr/rumblr/model/TimelineObject.java new file mode 100644 index 000000000..8bb2c885d --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/tumblr/rumblr/model/TimelineObject.java @@ -0,0 +1,8 @@ +package com.tumblr.rumblr.model; + +public class TimelineObject { + public final T getData() { + throw new UnsupportedOperationException("Stub"); + } + +} diff --git a/extensions/shared/stub/src/main/java/com/tumblr/rumblr/model/TimelineObjectType.java b/extensions/shared/stub/src/main/java/com/tumblr/rumblr/model/TimelineObjectType.java new file mode 100644 index 000000000..f9b7d7abc --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/tumblr/rumblr/model/TimelineObjectType.java @@ -0,0 +1,4 @@ +package com.tumblr.rumblr.model; + +public enum TimelineObjectType { +} diff --git a/extensions/shared/stub/src/main/java/com/tumblr/rumblr/model/Timelineable.java b/extensions/shared/stub/src/main/java/com/tumblr/rumblr/model/Timelineable.java new file mode 100644 index 000000000..bf84887de --- /dev/null +++ b/extensions/shared/stub/src/main/java/com/tumblr/rumblr/model/Timelineable.java @@ -0,0 +1,5 @@ +package com.tumblr.rumblr.model; + +public interface Timelineable { + TimelineObjectType getTimelineObjectType(); +} 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..4c02f1a40 --- /dev/null +++ b/extensions/shared/stub/src/main/java/org/chromium/net/UrlRequest.java @@ -0,0 +1,8 @@ +package org.chromium.net; + +public abstract class UrlRequest { + public abstract class Builder { + public abstract Builder addHeader(String name, String value); + public abstract UrlRequest build(); + } +} 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/extensions/shared/stub/src/main/java/tv/twitch/android/feature/settings/menu/SettingsMenuGroup.java b/extensions/shared/stub/src/main/java/tv/twitch/android/feature/settings/menu/SettingsMenuGroup.java new file mode 100644 index 000000000..72495e4b5 --- /dev/null +++ b/extensions/shared/stub/src/main/java/tv/twitch/android/feature/settings/menu/SettingsMenuGroup.java @@ -0,0 +1,14 @@ +package tv.twitch.android.feature.settings.menu; + +import java.util.List; + +// Dummy +public final class SettingsMenuGroup { + public SettingsMenuGroup(List settingsMenuItems) { + throw new UnsupportedOperationException("Stub"); + } + + public List getSettingsMenuItems() { + throw new UnsupportedOperationException("Stub"); + } +} \ No newline at end of file diff --git a/extensions/shared/stub/src/main/java/tv/twitch/android/settings/SettingsActivity.java b/extensions/shared/stub/src/main/java/tv/twitch/android/settings/SettingsActivity.java new file mode 100644 index 000000000..dcf42ab30 --- /dev/null +++ b/extensions/shared/stub/src/main/java/tv/twitch/android/settings/SettingsActivity.java @@ -0,0 +1,5 @@ +package tv.twitch.android.settings; + +import android.app.Activity; + +public class SettingsActivity extends Activity {} diff --git a/extensions/shared/stub/src/main/java/tv/twitch/android/shared/chat/util/ClickableUsernameSpan.java b/extensions/shared/stub/src/main/java/tv/twitch/android/shared/chat/util/ClickableUsernameSpan.java new file mode 100644 index 000000000..0322bdba2 --- /dev/null +++ b/extensions/shared/stub/src/main/java/tv/twitch/android/shared/chat/util/ClickableUsernameSpan.java @@ -0,0 +1,9 @@ +package tv.twitch.android.shared.chat.util; + +import android.text.style.ClickableSpan; +import android.view.View; + +public final class ClickableUsernameSpan extends ClickableSpan { + @Override + public void onClick(View widget) {} +} \ No newline at end of file diff --git a/extensions/spoof-wifi/build.gradle.kts b/extensions/spoof-wifi/build.gradle.kts new file mode 100644 index 000000000..9a2728690 --- /dev/null +++ b/extensions/spoof-wifi/build.gradle.kts @@ -0,0 +1,11 @@ +extension { + name = "extensions/all/connectivity/wifi/spoof/spoof-wifi.rve" +} + +android { + namespace = "app.revanced.extension" +} + +dependencies { + compileOnly(libs.annotation) +} diff --git a/extensions/spoof-wifi/src/main/AndroidManifest.xml b/extensions/spoof-wifi/src/main/AndroidManifest.xml new file mode 100644 index 000000000..7425a54c5 --- /dev/null +++ b/extensions/spoof-wifi/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + diff --git a/extensions/spoof-wifi/src/main/java/app/revanced/extension/all/connectivity/wifi/spoof/SpoofWifiPatch.java b/extensions/spoof-wifi/src/main/java/app/revanced/extension/all/connectivity/wifi/spoof/SpoofWifiPatch.java new file mode 100644 index 000000000..5f00bd730 --- /dev/null +++ b/extensions/spoof-wifi/src/main/java/app/revanced/extension/all/connectivity/wifi/spoof/SpoofWifiPatch.java @@ -0,0 +1,424 @@ +package app.revanced.extension.all.connectivity.wifi.spoof; + +import android.app.PendingIntent; +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkInfo; +import android.net.NetworkRequest; +import android.os.Build; +import android.os.Handler; + +import androidx.annotation.RequiresApi; + +/** @noinspection deprecation, unused */ +public class SpoofWifiPatch { + + // Used to check what the (real or fake) active network is (take a look at `hasTransport`). + private static ConnectivityManager CONNECTIVITY_MANAGER; + + // If Wifi is not enabled, these are types that would pretend to be Wifi for android.net.Network (lower index = higher priority). + // This does not apply to android.net.NetworkInfo, because we can pretend that Wifi is always active there. + // + // VPN should be a fallback, because Reverse Tethering uses VPN. + private static final int[] FAKE_FALLBACK_NETWORKS = { NetworkCapabilities.TRANSPORT_ETHERNET, NetworkCapabilities.TRANSPORT_VPN }; + + // In order to initialize our own ConnectivityManager, if it isn't initialized yet. + public static Object getSystemService(Context context, String name) { + Object result = context.getSystemService(name); + if (CONNECTIVITY_MANAGER == null) { + if (Context.CONNECTIVITY_SERVICE.equals(name)) { + CONNECTIVITY_MANAGER = (ConnectivityManager) result; + } else { + CONNECTIVITY_MANAGER = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + } + } + return result; + } + + // In order to initialize our own ConnectivityManager, if it isn't initialized yet. + public static Object getSystemService(Context context, Class serviceClass) { + Object result = context.getSystemService(serviceClass); + if (CONNECTIVITY_MANAGER == null) { + if (serviceClass == ConnectivityManager.class) { + CONNECTIVITY_MANAGER = (ConnectivityManager) result; + } else { + CONNECTIVITY_MANAGER = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + } + } + return result; + } + + // Simply always return Wifi as active network. + public static NetworkInfo getActiveNetworkInfo(ConnectivityManager connectivityManager) { + for (NetworkInfo networkInfo : connectivityManager.getAllNetworkInfo()) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return networkInfo; + } + } + return connectivityManager.getActiveNetworkInfo(); + } + + // Pretend Wifi is always connected. + public static boolean isConnected(NetworkInfo networkInfo) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return true; + } + return networkInfo.isConnected(); + } + + // Pretend Wifi is always connected. + public static boolean isConnectedOrConnecting(NetworkInfo networkInfo) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return true; + } + return networkInfo.isConnectedOrConnecting(); + } + + // Pretend Wifi is always available. + public static boolean isAvailable(NetworkInfo networkInfo) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return true; + } + return networkInfo.isAvailable(); + } + + // Pretend Wifi is always connected. + public static NetworkInfo.State getState(NetworkInfo networkInfo) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return NetworkInfo.State.CONNECTED; + } + return networkInfo.getState(); + } + + // Pretend Wifi is always connected. + public static NetworkInfo.DetailedState getDetailedState(NetworkInfo networkInfo) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return NetworkInfo.DetailedState.CONNECTED; + } + return networkInfo.getDetailedState(); + } + + // Pretend Wifi is enabled, so connection isn't metered. + public static boolean isActiveNetworkMetered(ConnectivityManager connectivityManager) { + return false; + } + + // Returns the Wifi network, if Wifi is enabled. + // Otherwise if one of our fallbacks has a connection, return them. + // And as a last resort, return the default active network. + public static Network getActiveNetwork(ConnectivityManager connectivityManager) { + Network[] prioritizedNetworks = new Network[FAKE_FALLBACK_NETWORKS.length]; + for (Network network : connectivityManager.getAllNetworks()) { + NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network); + if (networkCapabilities == null) { + continue; + } + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + return network; + } + if (networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) { + for (int i = 0; i < FAKE_FALLBACK_NETWORKS.length; i++) { + int transportType = FAKE_FALLBACK_NETWORKS[i]; + if (networkCapabilities.hasTransport(transportType)) { + prioritizedNetworks[i] = network; + break; + } + } + } + } + for (Network network : prioritizedNetworks) { + if (network != null) { + return network; + } + } + return connectivityManager.getActiveNetwork(); + } + + // If the given network is a real or fake Wifi connection, return a Wifi network. + // Otherwise fallback to default implementation. + public static NetworkInfo getNetworkInfo(ConnectivityManager connectivityManager, Network network) { + NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network); + if (networkCapabilities != null && hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI)) { + for (NetworkInfo networkInfo : connectivityManager.getAllNetworkInfo()) { + if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { + return networkInfo; + } + } + } + return connectivityManager.getNetworkInfo(network); + } + + // If we are checking if the NetworkCapabilities use Wifi, return yes if + // - it is a real Wifi connection, + // - or the NetworkCapabilities are from a network pretending being a Wifi network. + // Otherwise fallback to default implementation. + public static boolean hasTransport(NetworkCapabilities networkCapabilities, int transportType) { + if (transportType == NetworkCapabilities.TRANSPORT_WIFI) { + if (networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) { + return true; + } + if (CONNECTIVITY_MANAGER != null) { + Network activeNetwork = getActiveNetwork(CONNECTIVITY_MANAGER); + NetworkCapabilities activeNetworkCapabilities = CONNECTIVITY_MANAGER.getNetworkCapabilities(activeNetwork); + if (activeNetworkCapabilities != null) { + for (int fallbackTransportType : FAKE_FALLBACK_NETWORKS) { + if (activeNetworkCapabilities.hasTransport(fallbackTransportType) && networkCapabilities.hasTransport(fallbackTransportType)) { + return true; + } + } + } + } + } + return networkCapabilities.hasTransport(transportType); + } + + // If the given network is a real or fake Wifi connection, pretend it has a connection (and some other things). + public static boolean hasCapability(NetworkCapabilities networkCapabilities, int capability) { + if (hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI) && ( + capability == NetworkCapabilities.NET_CAPABILITY_INTERNET + || capability == NetworkCapabilities.NET_CAPABILITY_FOREGROUND + || capability == NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED + || capability == NetworkCapabilities.NET_CAPABILITY_NOT_METERED + || capability == NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED + || capability == NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING + || capability == NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED + || capability == NetworkCapabilities.NET_CAPABILITY_NOT_VPN + || capability == NetworkCapabilities.NET_CAPABILITY_TRUSTED + || capability == NetworkCapabilities.NET_CAPABILITY_VALIDATED)) { + return true; + } + return networkCapabilities.hasCapability(capability); + } + + // If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + @RequiresApi(api = Build.VERSION_CODES.S) + public static void registerBestMatchingNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.of(handler), + () -> connectivityManager.registerBestMatchingNetworkCallback(request, networkCallback, handler) + ); + } + + // If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + @RequiresApi(api = Build.VERSION_CODES.N) + public static void registerDefaultNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback) { + Utils.networkCallback( + connectivityManager, + Utils.Option.empty(), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.empty(), + () -> connectivityManager.registerDefaultNetworkCallback(networkCallback) + ); + } + + // If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + @RequiresApi(api = Build.VERSION_CODES.O) + public static void registerDefaultNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback, Handler handler) { + Utils.networkCallback( + connectivityManager, + Utils.Option.empty(), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.of(handler), + () -> connectivityManager.registerDefaultNetworkCallback(networkCallback, handler) + ); + } + + // If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.empty(), + () -> connectivityManager.registerNetworkCallback(request, networkCallback) + ); + } + + // If it waits for Wifi connectivity, pretend it is fulfilled immediately. + public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, PendingIntent operation) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.empty(), + Utils.Option.of(operation), + Utils.Option.empty(), + () -> connectivityManager.registerNetworkCallback(request, operation) + ); + } + + // If it waits for Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + @RequiresApi(api = Build.VERSION_CODES.O) + public static void registerNetworkCallback(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.of(handler), + () -> connectivityManager.registerNetworkCallback(request, networkCallback, handler) + ); + } + + // If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.empty(), + () -> connectivityManager.requestNetwork(request, networkCallback) + ); + } + + // If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + @RequiresApi(api = Build.VERSION_CODES.O) + public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, int timeoutMs) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.empty(), + () -> connectivityManager.requestNetwork(request, networkCallback, timeoutMs) + ); + } + + // If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + @RequiresApi(api = Build.VERSION_CODES.O) + public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.of(handler), + () -> connectivityManager.requestNetwork(request, networkCallback, handler) + ); + } + + // If it requests Wifi connectivity, pretend it is fulfilled immediately. + public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, PendingIntent operation) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.empty(), + Utils.Option.of(operation), + Utils.Option.empty(), + () -> connectivityManager.requestNetwork(request, operation) + ); + } + + // If it requests Wifi connectivity, pretend it is fulfilled immediately if we have an active network. + @RequiresApi(api = Build.VERSION_CODES.O) + public static void requestNetwork(ConnectivityManager connectivityManager, NetworkRequest request, ConnectivityManager.NetworkCallback networkCallback, Handler handler, int timeoutMs) { + Utils.networkCallback( + connectivityManager, + Utils.Option.of(request), + Utils.Option.of(networkCallback), + Utils.Option.empty(), + Utils.Option.of(handler), + () -> connectivityManager.requestNetwork(request, networkCallback, handler, timeoutMs) + ); + } + + public static void unregisterNetworkCallback(ConnectivityManager connectivityManager, ConnectivityManager.NetworkCallback networkCallback) { + try { + connectivityManager.unregisterNetworkCallback(networkCallback); + } catch (IllegalArgumentException ignore) { + // ignore: NetworkCallback was not registered + } + } + + public static void unregisterNetworkCallback(ConnectivityManager connectivityManager, PendingIntent operation) { + try { + connectivityManager.unregisterNetworkCallback(operation); + } catch (IllegalArgumentException ignore) { + // ignore: PendingIntent was not registered + } + } + + private static class Utils { + private static class Option { + private final T value; + private final boolean isPresent; + + private Option(T value, boolean isPresent) { + this.value = value; + this.isPresent = isPresent; + } + + private static Option of(T value) { + return new Option<>(value, true); + } + + private static Option empty() { + return new Option<>(null, false); + } + } + + private static void networkCallback( + ConnectivityManager connectivityManager, + Option request, + Option networkCallback, + Option operation, + Option handler, + Runnable fallback + ) { + if(!request.isPresent || (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && request.value != null && requestsWifiNetwork(request.value))) { + Runnable runnable = null; + if (networkCallback.isPresent && networkCallback.value != null) { + Network network = activeWifiNetwork(connectivityManager); + if (network != null) { + runnable = () -> networkCallback.value.onAvailable(network); + } + } else if (operation.isPresent && operation.value != null) { + runnable = () -> { + try { + operation.value.send(); + } catch (PendingIntent.CanceledException ignore) {} + }; + } + if (runnable != null) { + if (handler.isPresent) { + if (handler.value != null) { + handler.value.post(runnable); + return; + } + } else { + runnable.run(); + return; + } + } + } + fallback.run(); + } + + // Returns an active (maybe fake) Wifi network if there is one, otherwise null. + private static Network activeWifiNetwork(ConnectivityManager connectivityManager) { + Network network = getActiveNetwork(connectivityManager); + NetworkCapabilities networkCapabilities = connectivityManager.getNetworkCapabilities(network); + if (networkCapabilities != null && hasTransport(networkCapabilities, NetworkCapabilities.TRANSPORT_WIFI)) { + return network; + } + return null; + } + + // Whether a Wifi network with connection is requested. + @RequiresApi(api = Build.VERSION_CODES.P) + private static boolean requestsWifiNetwork(NetworkRequest request) { + return request.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) + && (request.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + || request.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)); + } + } +} diff --git a/gradle.properties b/gradle.properties index aac016168..4ba4a5d4d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,6 @@ -org.gradle.parallel = true org.gradle.caching = true +org.gradle.jvmargs = -Xms512M -Xmx2048M +org.gradle.parallel = true +android.useAndroidX = true kotlin.code.style = official -version = 4.17.0 +version = 5.0.0-dev.4 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3a4060085..af87b2376 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,26 @@ [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" # 3.0.7 breaks binary compatibility. Tracking https://github.com/google/smali/issues/58. -guava = "33.2.1-jre" +smali = "3.0.5" gson = "2.11.0" -binary-compatibility-validator = "0.15.1" -kotlin = "2.0.0" +# 8.3.0 causes java verifier error: https://github.com/ReVanced/revanced-patches/issues/2818. +#noinspection GradleDependency +agp = "8.2.2" +annotation = "1.9.1" +appcompat = "1.7.0" +okhttp = "5.0.0-alpha.14" +retrofit = "2.11.0" +guava = "33.2.1-jre" [libraries] -revanced-patcher = { module = "app.revanced:revanced-patcher", version.ref = "revanced-patcher" } -smali = { module = "com.android.tools.smali:smali", version.ref = "smali" } -guava = { module = "com.google.guava:guava", version.ref = "guava" } gson = { module = "com.google.code.gson:gson", version.ref = "gson" } +annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } +appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } +okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +guava = { module = "com.google.guava:guava", version.ref = "guava" } + [plugins] -binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binary-compatibility-validator" } -kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +android-library = { id = "com.android.library", version.ref = "agp" } diff --git a/patches/api/patches.api b/patches/api/patches.api new file mode 100644 index 000000000..04e8b64e2 --- /dev/null +++ b/patches/api/patches.api @@ -0,0 +1,1519 @@ +public final class app/revanced/patches/all/misc/activity/exportall/ExportAllActivitiesPatchKt { + public static final fun getExportAllActivitiesPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/all/misc/build/BaseSpoofBuildInfoPatchKt { + public static final fun baseSpoofBuildInfoPatch (Lkotlin/jvm/functions/Function0;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/all/misc/build/BuildInfo { + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Long;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getBoard ()Ljava/lang/String; + public final fun getBootloader ()Ljava/lang/String; + public final fun getBrand ()Ljava/lang/String; + public final fun getCpuAbi ()Ljava/lang/String; + public final fun getCpuAbi2 ()Ljava/lang/String; + public final fun getDevice ()Ljava/lang/String; + public final fun getDisplay ()Ljava/lang/String; + public final fun getFingerprint ()Ljava/lang/String; + public final fun getHardware ()Ljava/lang/String; + public final fun getHost ()Ljava/lang/String; + public final fun getId ()Ljava/lang/String; + public final fun getManufacturer ()Ljava/lang/String; + public final fun getModel ()Ljava/lang/String; + public final fun getOdmSku ()Ljava/lang/String; + public final fun getProduct ()Ljava/lang/String; + public final fun getRadio ()Ljava/lang/String; + public final fun getSerial ()Ljava/lang/String; + public final fun getSku ()Ljava/lang/String; + public final fun getSocManufacturer ()Ljava/lang/String; + public final fun getSocModel ()Ljava/lang/String; + public final fun getTags ()Ljava/lang/String; + public final fun getTime ()Ljava/lang/Long; + public final fun getType ()Ljava/lang/String; + public final fun getUser ()Ljava/lang/String; +} + +public final class app/revanced/patches/all/misc/build/SpoofBuildInfoPatchKt { + public static final fun getSpoofBuildInfoPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/all/misc/connectivity/location/hide/HideMockLocationPatchKt { + public static final fun getHideMockLocationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/all/misc/connectivity/telephony/sim/spoof/SpoofSimCountryPatchKt { + public static final fun getSpoofSimCountryPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/all/misc/connectivity/wifi/spoof/SpoofWifiPatchKt { + public static final fun getSpoofWifiPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/all/misc/debugging/EnableAndroidDebuggingPatchKt { + public static final fun getEnableAndroidDebuggingPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatchKt { + public static final fun getChangeDataDirectoryLocationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/all/misc/hex/HexPatchKt { + public static final fun getHexPatch ()Lapp/revanced/patcher/patch/RawResourcePatch; +} + +public final class app/revanced/patches/all/misc/interaction/gestures/PredictiveBackGesturePatchKt { + public static final fun getPredictiveBackGesturePatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/all/misc/network/OverrideCertificatePinningPatchKt { + public static final fun getOverrideCertificatePinningPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/all/misc/packagename/ChangePackageNamePatchKt { + public static field packageNameOption Lapp/revanced/patcher/patch/Option; + public static final fun getChangePackageNamePatch ()Lapp/revanced/patcher/patch/ResourcePatch; + public static final fun getPackageNameOption ()Lapp/revanced/patcher/patch/Option; + public static final fun setOrGetFallbackPackageName (Ljava/lang/String;)Ljava/lang/String; + public static final fun setPackageNameOption (Lapp/revanced/patcher/patch/Option;)V +} + +public final class app/revanced/patches/all/misc/resources/AddResourcesPatchKt { + public static final fun addResource (Ljava/lang/String;Lapp/revanced/util/resource/BaseResource;)Z + public static final fun addResources (Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;)Z + public static final fun addResources (Ljava/lang/String;Ljava/lang/Iterable;)Z + public static final fun addResources (Ljava/lang/String;Ljava/lang/String;)V + public static final fun addResources (Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;)Z + public static final fun addResources (Ljava/lang/String;Ljava/util/List;)Z + public static synthetic fun addResources$default (Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Z + public static synthetic fun addResources$default (Ljava/lang/String;Ljava/lang/String;ZLjava/lang/String;ILjava/lang/Object;)Z + public static final fun getAddResourcesPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/all/misc/screencapture/RemoveScreenCaptureRestrictionPatchKt { + public static final fun getRemoveScreenCaptureRestrictionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/all/misc/screenshot/RemoveScreenshotRestrictionPatchKt { + public static final fun getRemoveScreenshotRestrictionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/all/misc/shortcut/sharetargets/RemoveShareTargetsPatchKt { + public static final fun getRemoveShareTargetsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public abstract interface class app/revanced/patches/all/misc/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/all/misc/transformation/IMethodCall$DefaultImpls { + public static fun replaceInvokeVirtualWithExtension (Lapp/revanced/patches/all/misc/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/all/misc/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/all/misc/versioncode/ChangeVersionCodePatchKt { + public static final fun getChangeVersionCodePatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/amazon/DeepLinkingPatchKt { + public static final fun getDeepLinkingPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/backdrops/misc/pro/ProUnlockPatchKt { + public static final fun getProUnlockPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/bandcamp/limitations/RemovePlayLimitsPatchKt { + public static final fun getRemovePlayLimitsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/cieid/restrictions/root/BypassRootChecksPatchKt { + public static final fun getBypassRootChecksPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/duolingo/ad/DisableAdsPatchKt { + public static final fun getDisableAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/duolingo/debug/EnableDebugMenuPatchKt { + public static final fun getEnableDebugMenuPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/facebook/ads/mainfeed/HideSponsoredStoriesPatchKt { + public static final fun getHideSponsoredStoriesPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/facebook/ads/story/HideStoryAdsPatchKt { + public static final fun getHideStoryAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/finanzonline/detection/bootloader/BootloaderDetectionPatchKt { + public static final fun getBootloaderDetectionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/finanzonline/detection/root/RootDetectionPatchKt { + public static final fun getRootDetectionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/googlenews/customtabs/EnableCustomTabsPatchKt { + public static final fun getEnableCustomTabsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/googlenews/misc/extension/ExtensionPatchKt { + public static final fun getExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/googlenews/misc/gms/GmsCoreSupportPatchKt { + public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/googlephotos/misc/extension/ExtensionPatchKt { + public static final fun getExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/googlephotos/misc/features/SpoofBuildInfoPatchKt { + public static final fun getSpoofBuildInfoPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/googlephotos/misc/features/SpoofFeaturesPatchKt { + public static final fun getSpoofFeaturesPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/googlephotos/misc/gms/GmsCoreSupportPatchKt { + public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/googlephotos/misc/preferences/RestoreHiddenBackUpWhileChargingTogglePatchKt { + public static final fun getRestoreHiddenBackUpWhileChargingTogglePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/googlerecorder/restrictions/RemoveDeviceRestrictionsKt { + public static final fun getRemoveDeviceRestrictionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/hexeditor/ad/DisableAdsPatchKt { + public static final fun getDisableAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/iconpackstudio/misc/pro/UnlockProPatchKt { + public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/idaustria/detection/root/RootDetectionPatchKt { + public static final fun getRootDetectionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/idaustria/detection/signature/SpoofSignaturePatchKt { + public static final fun getSpoofSignaturePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/inshorts/ad/InshortsAdsPatchKt { + public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/instagram/ads/HideAdsPatchKt { + public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/irplus/ad/RemoveAdsPatchKt { + public static final fun getRemoveAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatchKt { + public static final fun getDisableMandatoryLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/lightroom/misc/premium/UnlockPremiumPatchKt { + public static final fun getUnlockPremiumPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/memegenerator/detection/license/LicenseValidationPatchKt { + public static final fun getLicenseValidationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/memegenerator/detection/signature/SignatureVerificationPatchKt { + public static final fun getSignatureVerificationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/memegenerator/misc/pro/UnlockProVersionPatchKt { + public static final fun getUnlockProVersionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/messenger/inbox/HideInboxAdsPatchKt { + public static final fun getHideInboxAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/messenger/inbox/HideInboxSubtabsPatchKt { + public static final fun getHideInboxSubtabsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/messenger/inputfield/DisableSwitchingEmojiToStickerPatchKt { + public static final fun getDisableSwitchingEmojiToStickerPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/messenger/inputfield/DisableTypingIndicatorPatchKt { + public static final fun getDisableTypingIndicatorPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/mifitness/misc/locale/ForceEnglishLocalePatchKt { + public static final fun getForceEnglishLocalePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/mifitness/misc/login/FixLoginPatchKt { + public static final fun getFixLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/ad/video/HideVideoAdsKt { + public static final fun getHideVideoAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlaybackKt { + public static final fun getEnableExclusiveAudioPlaybackPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatchKt { + public static final fun getPermanentRepeatPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/interaction/permanentshuffle/PermanentShufflePatchKt { + public static final fun getPermanentShufflePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/layout/compactheader/HideCategoryBarKt { + public static final fun getHideCategoryBar ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/layout/premium/HideGetPremiumPatchKt { + public static final fun getHideGetPremiumPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/layout/upgradebutton/RemoveUpgradeButtonPatchKt { + public static final fun getRemoveUpgradeButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatchKt { + public static final fun getBypassCertificateChecksPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +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/extension/SharedExtensionPatchKt { + public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/music/misc/gms/Constants { + public static final field INSTANCE Lapp/revanced/patches/music/misc/gms/Constants; +} + +public final class app/revanced/patches/music/misc/gms/GmsCoreSupportPatchKt { + public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/myexpenses/misc/pro/UnlockProPatchKt { + public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/myfitnesspal/ads/HideAdsPatchKt { + public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/netguard/broadcasts/removerestriction/RemoveBroadcastsRestrictionPatchKt { + public static final fun getRemoveBroadcastsRestrictionPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/nfctoolsse/misc/pro/UnlockProPatchKt { + public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/nyx/misc/pro/UnlockProPatchKt { + public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/openinghours/misc/fix/crash/FixCrashPatchKt { + public static final fun getFixCrashPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/photomath/detection/deviceid/SpoofDeviceIdPatchKt { + public static final fun getGetDeviceIdPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/photomath/detection/signature/SignatureDetectionPatchKt { + public static final fun getSignatureDetectionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/photomath/misc/annoyances/HideUpdatePopupPatchKt { + public static final fun getHideUpdatePopupPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/photomath/misc/unlock/bookpoint/EnableBookpointPatchKt { + public static final fun getEnableBookpointPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/photomath/misc/unlock/plus/UnlockPlusPatchKt { + public static final fun getUnlockPlusPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/piccomafr/misc/SpoofAndroidDeviceIdPatchKt { + public static final fun getSpoofAndroidDeviceIdPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/piccomafr/tracking/DisableTrackingPatchKt { + public static final fun getDisableTrackingPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/pixiv/ads/HideAdsPatchKt { + public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/rar/misc/annoyances/purchasereminder/HidePurchaseReminderPatchKt { + public static final fun getHidePurchaseReminderPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/ad/banner/HideBannerPatchKt { + public static final fun getHideBannerPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/reddit/ad/comments/HideCommentAdsPatchKt { + public static final fun getHideCommentAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/ad/general/HideAdsPatchKt { + public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/FixSLinksPatchKt { + public static final field RESOLVE_S_LINK_METHOD Ljava/lang/String; + public static final field SET_ACCESS_TOKEN_METHOD Ljava/lang/String; + public static final fun fixSLinksPatch (Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch; + public static synthetic fun fixSLinksPatch$default (Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/SpoofClientPatchKt { + public static final fun spoofClientPatch (Ljava/lang/String;Lkotlin/jvm/functions/Function2;)Lapp/revanced/patcher/patch/BytecodePatch; + public static synthetic fun spoofClientPatch$default (Ljava/lang/String;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/baconreader/api/SpoofClientPatchKt { + public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/boostforreddit/ads/DisableAdsPatchKt { + public static final fun getDisableAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/boostforreddit/api/SpoofClientPatchKt { + public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/FixAudioMissingInDownloadsPatchKt { + public static final fun getFixAudioMissingInDownloadsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/boostforreddit/fix/slink/FixSLinksPatchKt { + public static final field EXTENSION_CLASS_DESCRIPTOR Ljava/lang/String; + public static final fun getFixSlinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/boostforreddit/misc/extension/SharedExtensionPatchKt { + public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/infinityforreddit/api/SpoofClientPatchKt { + public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/infinityforreddit/subscription/UnlockSubscriptionPatchKt { + public static final fun getUnlockSubscriptionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/joeyforreddit/ads/DisableAdsPatchKt { + public static final fun getDisableAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/joeyforreddit/api/SpoofClientPatchKt { + public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/joeyforreddit/detection/piracy/DisablePiracyDetectionPatchKt { + public static final fun getDisablePiracyDetectionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/redditisfun/api/FingerprintsKt { + public static final fun baseClientIdFingerprint (Ljava/lang/String;)Lapp/revanced/patcher/Fingerprint; +} + +public final class app/revanced/patches/reddit/customclients/redditisfun/api/SpoofClientPatchKt { + public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/relayforreddit/api/SpoofClientPatchKt { + public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/slide/api/SpoofClientPatchKt { + public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/sync/ads/DisableAdsPatchKt { + public static final fun disableAdsPatch (Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch; + public static synthetic fun disableAdsPatch$default (Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/sync/detection/piracy/DisablePiracyDetectionPatchKt { + public static final fun getDisablePiracyDetectionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/sync/syncforlemmy/ads/DisableAdsPatchKt { + public static final fun getDisableAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/ads/DisableAdsPatchKt { + public static final fun getDisableAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/annoyances/startup/DisableSyncForLemmyBottomSheetPatchKt { + public static final fun getDisableSyncForLemmyBottomSheetPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/api/SpoofClientPatchKt { + public static final fun getSpoofClientPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/extension/SharedExtensionPatchKt { + public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/slink/FixSLinksPatchKt { + public static final field EXTENSION_CLASS_DESCRIPTOR Ljava/lang/String; + public static final fun getFixSLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/user/UseUserEndpointPatchKt { + public static final fun getUseUserEndpointPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/customclients/syncforreddit/fix/video/FixVideoDownloadsPatchKt { + public static final fun getFixVideoDownloadsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/layout/disablescreenshotpopup/DisableScreenshotPopupPatchKt { + public static final fun getDisableScreenshotPopupPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/layout/premiumicon/UnlockPremiumIconPatchKt { + public static final fun getUnlockPremiumIconPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/reddit/misc/extension/ExtensionPatchKt { + public static final fun getSharedExtensionPatch ()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/serviceportalbund/detection/root/RootDetectionPatchKt { + public static final fun getRootDetectionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/misc/checks/BaseCheckEnvironmentPatchKt { + public static final fun checkEnvironmentPatch (Lapp/revanced/patcher/Fingerprint;Lapp/revanced/patcher/patch/Patch;[Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/misc/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/misc/extension/SharedExtensionPatchKt { + public static final fun extensionHook (Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patches/shared/misc/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/misc/extension/ExtensionHook; + public static final fun sharedExtensionPatch ([Lapp/revanced/patches/shared/misc/extension/ExtensionHook;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/misc/fix/verticalscroll/VerticalScrollPatchKt { + public static final fun getVerticalScrollPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/shared/misc/gms/FingerprintsKt { + public static final field GET_GMS_CORE_VENDOR_GROUP_ID_METHOD_NAME Ljava/lang/String; +} + +public final class app/revanced/patches/shared/misc/gms/GmsCoreSupportPatchKt { + public static final fun gmsCoreSupportPatch (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patcher/Fingerprint;Ljava/util/Set;Lapp/revanced/patcher/Fingerprint;Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;)Lapp/revanced/patcher/patch/BytecodePatch; + public static synthetic fun gmsCoreSupportPatch$default (Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patcher/Fingerprint;Ljava/util/Set;Lapp/revanced/patcher/Fingerprint;Lapp/revanced/patcher/patch/Patch;Lkotlin/jvm/functions/Function1;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;Ljava/lang/String;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;Ljava/lang/String;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/misc/hex/HexPatchKt { + public static final fun hexPatch (Lkotlin/jvm/functions/Function0;)Lapp/revanced/patcher/patch/RawResourcePatch; +} + +public final class app/revanced/patches/shared/misc/hex/Replacement { + public static final field Companion Lapp/revanced/patches/shared/misc/hex/Replacement$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public final fun replacePattern ([B)V +} + +public final class app/revanced/patches/shared/misc/hex/Replacement$Companion { +} + +public final class app/revanced/patches/shared/misc/mapping/ResourceElement { + 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/misc/mapping/ResourceElement; + public static synthetic fun copy$default (Lapp/revanced/patches/shared/misc/mapping/ResourceElement;Ljava/lang/String;Ljava/lang/String;JILjava/lang/Object;)Lapp/revanced/patches/shared/misc/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/misc/mapping/ResourceMappingPatchKt { + 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/misc/settings/SettingsPatchKt { + public static final fun settingsPatch (Lkotlin/Pair;Ljava/util/Set;)Lapp/revanced/patcher/patch/ResourcePatch; + public static synthetic fun settingsPatch$default (Lkotlin/Pair;Ljava/util/Set;ILjava/lang/Object;)Lapp/revanced/patcher/patch/ResourcePatch; +} + +public abstract class app/revanced/patches/shared/misc/settings/preference/BasePreference { + public static final field Companion Lapp/revanced/patches/shared/misc/settings/preference/BasePreference$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getKey ()Ljava/lang/String; + public final fun getSummaryKey ()Ljava/lang/String; + public final fun getTag ()Ljava/lang/String; + public final fun getTitleKey ()Ljava/lang/String; + public fun hashCode ()I + public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; +} + +public final class app/revanced/patches/shared/misc/settings/preference/BasePreference$Companion { + public final fun addSummary (Lorg/w3c/dom/Element;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/SummaryType;)V + public static synthetic fun addSummary$default (Lapp/revanced/patches/shared/misc/settings/preference/BasePreference$Companion;Lorg/w3c/dom/Element;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/SummaryType;ILjava/lang/Object;)V +} + +public abstract class app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen : java/io/Closeable { + public fun ()V + public fun (Ljava/util/Set;)V + public synthetic fun (Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun close ()V + public abstract fun commit (Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference;)V +} + +public abstract class app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$BasePreferenceCollection { + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getKey ()Ljava/lang/String; + public final fun getPreferences ()Ljava/util/Set; + public final fun getTitleKey ()Ljava/lang/String; + public abstract fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreference; +} + +public class app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen : app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$BasePreferenceCollection { + public fun (Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference$Sorting;)V + public synthetic fun (Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference$Sorting;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun addPreferences ([Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)V + public final fun getCategories ()Ljava/util/Set; + public synthetic fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreference; + public fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference; +} + +public class app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen$Category : app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$BasePreferenceCollection { + public fun (Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V + public synthetic fun (Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun addPreferences ([Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)V + public synthetic fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreference; + public fun transform ()Lapp/revanced/patches/shared/misc/settings/preference/PreferenceCategory; +} + +public final class app/revanced/patches/shared/misc/settings/preference/InputType : java/lang/Enum { + public static final field NUMBER Lapp/revanced/patches/shared/misc/settings/preference/InputType; + public static final field TEXT Lapp/revanced/patches/shared/misc/settings/preference/InputType; + public static final field TEXT_CAP_CHARACTERS Lapp/revanced/patches/shared/misc/settings/preference/InputType; + public static final field TEXT_MULTI_LINE Lapp/revanced/patches/shared/misc/settings/preference/InputType; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getType ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/shared/misc/settings/preference/InputType; + public static fun values ()[Lapp/revanced/patches/shared/misc/settings/preference/InputType; +} + +public final class app/revanced/patches/shared/misc/settings/preference/IntentPreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getIntent ()Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent; + public fun hashCode ()I + public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; +} + +public final class app/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent { + public fun (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)V + public final fun copy (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;)Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent; + public static synthetic fun copy$default (Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function0;ILjava/lang/Object;)Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class app/revanced/patches/shared/misc/settings/preference/ListPreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/util/resource/ArrayResource;Lapp/revanced/util/resource/ArrayResource;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/util/resource/ArrayResource;Lapp/revanced/util/resource/ArrayResource;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getEntries ()Lapp/revanced/util/resource/ArrayResource; + public final fun getEntriesKey ()Ljava/lang/String; + public final fun getEntryValues ()Lapp/revanced/util/resource/ArrayResource; + public final fun getEntryValuesKey ()Ljava/lang/String; + public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; +} + +public final class app/revanced/patches/shared/misc/settings/preference/NonInteractivePreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Z)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getSelectable ()Z + public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; +} + +public class app/revanced/patches/shared/misc/settings/preference/PreferenceCategory : app/revanced/patches/shared/misc/settings/preference/BasePreference { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getPreferences ()Ljava/util/Set; + public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; +} + +public class app/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference$Sorting;Ljava/lang/String;Ljava/util/Set;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference$Sorting;Ljava/lang/String;Ljava/util/Set;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getPreferences ()Ljava/util/Set; + public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; +} + +public final class app/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference$Sorting : java/lang/Enum { + public static final field BY_KEY Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference$Sorting; + public static final field BY_TITLE Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference$Sorting; + public static final field UNSORTED Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference$Sorting; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getKeySuffix ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference$Sorting; + public static fun values ()[Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference$Sorting; +} + +public final class app/revanced/patches/shared/misc/settings/preference/SummaryType : java/lang/Enum { + public static final field DEFAULT Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; + public static final field OFF Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; + public static final field ON Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public final fun getType ()Ljava/lang/String; + public static fun valueOf (Ljava/lang/String;)Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; + public static fun values ()[Lapp/revanced/patches/shared/misc/settings/preference/SummaryType; +} + +public final class app/revanced/patches/shared/misc/settings/preference/SwitchPreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getSummaryOffKey ()Ljava/lang/String; + public final fun getSummaryOnKey ()Ljava/lang/String; + public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; +} + +public final class app/revanced/patches/shared/misc/settings/preference/TextPreference : app/revanced/patches/shared/misc/settings/preference/BasePreference { + public fun ()V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/InputType;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lapp/revanced/patches/shared/misc/settings/preference/InputType;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getInputType ()Lapp/revanced/patches/shared/misc/settings/preference/InputType; + public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; +} + +public final class app/revanced/patches/solidexplorer2/functionality/filesize/RemoveFileSizeLimitPatchKt { + public static final fun getRemoveFileSizeLimitPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/songpal/badge/BadgeTabPatchKt { + public static final fun getBadgeTabPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/songpal/badge/RemoveNotificationBadgePatchKt { + public static final fun getRemoveNotificationBadgePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/soundcloud/ad/HideAdsPatchKt { + public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/soundcloud/analytics/DisableTelemetryPatchKt { + public static final fun getDisableTelemetryPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/soundcloud/offlinesync/EnableOfflineSyncPatchKt { + public static final fun getEnableOfflineSync ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/spotify/layout/theme/CustomThemePatchKt { + public static final fun getCustomThemePatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/spotify/lite/ondemand/OnDemandPatchKt { + public static final fun getOnDemandPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/spotify/navbar/PremiumNavbarTabPatchKt { + public static final fun getPremiumNavbarTabPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/stocard/layout/HideOffersTabPatchKt { + public static final fun getHideOffersTabPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/stocard/layout/HideStoryBubblesPatchKt { + public static final fun getHideStoryBubblesPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/strava/subscription/UnlockSubscriptionPatchKt { + public static final fun getUnlockSubscriptionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/strava/upselling/DisableSubscriptionSuggestionsPatchKt { + public static final fun getDisableSubscriptionSuggestionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/swissid/integritycheck/RemoveGooglePlayIntegrityCheckPatchKt { + public static final fun getRemoveGooglePlayIntegrityCheckPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/ticktick/misc/themeunlock/UnlockThemePatchKt { + public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tiktok/feedfilter/FeedFilterPatchKt { + public static final fun getFeedFilterPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tiktok/interaction/cleardisplay/RememberClearDisplayPatchKt { + public static final fun getRememberClearDisplayPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tiktok/interaction/downloads/DownloadsPatchKt { + public static final fun getDownloadsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tiktok/interaction/seekbar/ShowSeekbarPatchKt { + public static final fun getShowSeekbarPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tiktok/interaction/speed/PlaybackSpeedPatchKt { + public static final fun getPlaybackSpeedPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tiktok/misc/extension/ExtensionPatchKt { + public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tiktok/misc/login/disablerequirement/DisableLoginRequirementPatchKt { + public static final fun getDisableLoginRequirementPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tiktok/misc/login/fixgoogle/FixGoogleLoginPatchKt { + public static final fun getFixGoogleLoginPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tiktok/misc/settings/SettingsPatchKt { + public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tiktok/misc/spoof/sim/SpoofSimPatchKt { + public static final fun getSpoofSimPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/trakt/UnlockProPatchKt { + public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tudortmund/lockscreen/ShowOnLockscreenPatchKt { + public static final fun getShowOnLockscreenPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tudortmund/misc/extension/ExtensionPatchKt { + public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tumblr/ads/DisableDashboardAdsKt { + public static final fun getDisableDashboardAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tumblr/annoyances/adfree/DisableAdFreeBannerPatchKt { + public static final fun getDisableAdFreeBannerPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tumblr/annoyances/inappupdate/DisableInAppUpdatePatchKt { + public static final fun getDisableInAppUpdatePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tumblr/annoyances/notifications/DisableBlogNotificationReminderPatchKt { + public static final fun getDisableBlogNotificationReminderPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tumblr/annoyances/popups/DisableGiftMessagePopupPatchKt { + public static final fun getDisableGiftMessagePopupPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tumblr/featureflags/OverrideFeatureFlagsPatchKt { + public static final fun getOverrideFeatureFlagsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tumblr/fixes/FixOldVersionsPatchKt { + public static final fun getFixOldVersionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tumblr/misc/extension/ExtensionPatchKt { + public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/tumblr/timelinefilter/FilterTimelineObjectsPatchKt { + public static field addTimelineObjectTypeFilter Lkotlin/jvm/functions/Function1; + public static final fun getAddTimelineObjectTypeFilter ()Lkotlin/jvm/functions/Function1; + public static final fun getFilterTimelineObjectsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; + public static final fun setAddTimelineObjectTypeFilter (Lkotlin/jvm/functions/Function1;)V +} + +public final class app/revanced/patches/twitch/ad/audio/AudioAdsPatchKt { + public static final fun getAudioAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitch/ad/embedded/EmbeddedAdsPatchKt { + public static final fun getEmbeddedAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitch/ad/shared/util/AdPatchKt { + public static final fun adPatch (Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function3;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitch/ad/shared/util/ReturnMethod { + public static final field Companion Lapp/revanced/patches/twitch/ad/shared/util/ReturnMethod$Companion; + public fun (CLjava/lang/String;)V + public final fun getReturnType ()C + public final fun getValue ()Ljava/lang/String; +} + +public final class app/revanced/patches/twitch/ad/shared/util/ReturnMethod$Companion { + public final fun getDefault ()Lapp/revanced/patches/twitch/ad/shared/util/ReturnMethod; +} + +public final class app/revanced/patches/twitch/ad/video/VideoAdsPatchKt { + public static final fun getVideoAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitch/chat/antidelete/ShowDeletedMessagesPatchKt { + public static final fun getShowDeletedMessagesPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitch/chat/autoclaim/AutoClaimChannelPointsPatchKt { + public static final fun getAutoClaimChannelPointsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitch/debug/DebugModePatchKt { + public static final fun getDebugModePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitch/misc/extension/SharedExtensionPatchKt { + public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitch/misc/settings/SettingsPatchKt { + public static final fun addSettingPreference (Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)V + public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitter/interaction/downloads/UnlockDownloadsPatchKt { + public static final fun getUnlockDownloadsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitter/layout/viewcount/HideViewCountPatchKt { + public static final fun getHideViewCountPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitter/misc/dynamiccolor/DynamicColorPatchKt { + public static final fun getDynamicColorPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/twitter/misc/extension/ExtensionPatchKt { + public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitter/misc/hook/HideAdsHookPatchKt { + public static final fun getHideAdsHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitter/misc/hook/HideRecommendedUsersPatchKt { + public static final fun getHideRecommendedUsersPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitter/misc/hook/HookPatchKt { + public static final fun hookPatch (Ljava/lang/String;Ljava/lang/String;)Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitter/misc/hook/json/JsonHook { + public fun (Lapp/revanced/patcher/patch/BytecodePatchContext;Ljava/lang/String;)V +} + +public final class app/revanced/patches/twitter/misc/hook/json/JsonHookPatchKt { + public static final fun addJsonHook (Lapp/revanced/patcher/patch/BytecodePatchContext;Lapp/revanced/patches/twitter/misc/hook/json/JsonHook;)V + public static final fun getJsonHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitter/misc/links/ChangeLinkSharingDomainPatchKt { + public static final fun getChangeLinkSharingDomainPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitter/misc/links/OpenLinksWithAppChooserPatchKt { + public static final fun getOpenLinksWithAppChooserPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/twitter/misc/links/SanitizeSharingLinksPatchKt { + public static final fun getSanitizeSharingLinksPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/vsco/misc/pro/UnlockProPatchKt { + public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/warnwetter/misc/firebasegetcert/FirebaseGetCertPatchKt { + public static final fun getFirebaseGetCertPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/warnwetter/misc/promocode/PromoCodeUnlockPatchKt { + public static final fun getPromoCodeUnlockPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/windyapp/misc/unlockpro/UnlockProPatchKt { + public static final fun getUnlockProPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/ad/general/HideAdsPatchKt { + public static final fun getHideAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/ad/getpremium/HideGetPremiumPatchKt { + public static final fun getHideGetPremiumPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/ad/video/VideoAdsPatchKt { + public static final fun getVideoAdsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoUrlPatchKt { + public static final fun getCopyVideoUrlPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatchKt { + public static final fun getRemoveViewerDiscretionDialogPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/interaction/downloads/DownloadsPatchKt { + public static final fun getDownloadsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/interaction/seekbar/DisablePreciseSeekingGesturePatchKt { + public static final fun getDisablePreciseSeekingGesturePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/interaction/seekbar/EnableSeekbarTappingPatchKt { + public static final fun getEnableSeekbarTappingPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/interaction/seekbar/EnableSlideToSeekPatchKt { + public static final fun getEnableSlideToSeekPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/interaction/seekbar/SeekbarThumbnailsPatchKt { + public static final fun getSeekbarThumbnailsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatchKt { + public static final fun getSwipeControlsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatchKt { + public static final fun getAutoCaptionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/branding/CustomBrandingPatchKt { + public static final fun getCustomBrandingPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatchKt { + public static final fun getChangeHeaderPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/buttons/action/HideButtonsPatchKt { + public static final fun getHideButtonsPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/buttons/navigation/NavigationButtonsPatchKt { + public static final fun getNavigationButtonsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatchKt { + public static final fun getHidePlayerOverlayButtonsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/hide/endscreencards/HideEndscreenCardsPatchKt { + public static final fun getHideEndscreenCardsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatchKt { + public static final fun getDisableFullscreenAmbientModePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatchKt { + public static final fun getAlbumCardId ()J + public static final fun getBarContainerHeightId ()J + public static final fun getCrowdfundingBoxId ()J + public static final fun getExpandButtonDownId ()J + public static final fun getFabButtonId ()J + public static final fun getFilterBarHeightId ()J + public static final fun getHideLayoutComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; + public static final fun getRelatedChipCloudMarginId ()J + public static final fun getYouTubeLogo ()J +} + +public final class app/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatchKt { + public static final fun getHideInfoCardsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/hide/player/flyoutmenupanel/HidePlayerFlyoutMenuPatchKt { + public static final fun getHidePlayerFlyoutMenuPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatchKt { + public static final fun getDisableRollingNumberAnimationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/hide/seekbar/HideSeekbarPatchKt { + public static final fun getHideSeekbarPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatchKt { + public static final fun getHideShortsComponentsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/hide/suggestedvideoendscreen/DisableSuggestedVideoEndScreenPatchKt { + public static final fun getDisableSuggestedVideoEndScreenPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/hide/time/HideTimestampPatchKt { + public static final fun getHideTimestampPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatchKt { + public static final fun getFloatyBarButtonTopMargin ()J + public static final fun getMiniplayerMaxSize ()J + public static final fun getMiniplayerPatch ()Lapp/revanced/patcher/patch/BytecodePatch; + 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 getPlayerOverlays ()J + public static final fun getScrimOverlay ()J + public static final fun getYtOutlinePictureInPictureWhite24 ()J + public static final fun getYtOutlineXWhite24 ()J +} + +public final class app/revanced/patches/youtube/layout/panels/popup/PlayerPopupPanelsPatchKt { + public static final fun getPlayerPopupPanelsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/player/background/PlayerControlsBackgroundPatchKt { + public static final fun getPlayerControlsBackgroundPatch ()Lapp/revanced/patcher/patch/ResourcePatch; +} + +public final class app/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatchKt { + public static final fun getCustomPlayerOverlayOpacityPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatchKt { + public static final fun getReturnYouTubeDislikePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/returnyoutubedislike/Vote : java/lang/Enum { + public static final field DISLIKE Lapp/revanced/patches/youtube/layout/returnyoutubedislike/Vote; + public static final field LIKE Lapp/revanced/patches/youtube/layout/returnyoutubedislike/Vote; + public static final field REMOVE_LIKE Lapp/revanced/patches/youtube/layout/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/layout/returnyoutubedislike/Vote; + public static fun values ()[Lapp/revanced/patches/youtube/layout/returnyoutubedislike/Vote; +} + +public final class app/revanced/patches/youtube/layout/searchbar/WideSearchbarPatchKt { + public static final fun getWideSearchbarPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/seekbar/SeekbarColorPatchKt { + public static final fun getSeekbarColorPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatchKt { + public static final fun getShortsAutoplayPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatchKt { + public static final fun getSponsorBlockPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatchKt { + public static final fun getSpoofAppVersionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatchKt { + public static final fun getChangeStartPagePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatchKt { + public static final fun getDisableResumingShortsOnStartupPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/startupshortsreset/FingerprintsKt { + public static final fun indexOfOptionalInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;)I +} + +public final class app/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatchKt { + public static final field EXTENSION_CLASS_DESCRIPTOR Ljava/lang/String; + public static final fun getEnableTabletLayoutPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/theme/LithoColorHookPatchKt { + public static final fun getLithoColorHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; + public static final fun getLithoColorOverrideHook ()Lkotlin/jvm/functions/Function2; +} + +public final class app/revanced/patches/youtube/layout/theme/ThemePatchKt { + public static final fun getThemePatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatchKt { + public static final fun getAlternativeThumbnailsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/layout/thumbnails/BypassImageRegionRestrictionsPatchKt { + public static final fun getBypassImageRegionRestrictionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/announcements/AnnouncementsPatchKt { + public static final fun getAnnouncementsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/autorepeat/AutoRepeatPatchKt { + public static final fun getAutoRepeatPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +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/check/CheckEnvironmentPatchKt { + public static final fun getCheckEnvironmentPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatchKt { + public static final fun getEnableDebuggingPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatchKt { + public static final fun getSpoofDeviceDimensionsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatchKt { + public static final fun getCheckWatchHistoryDomainNameResolutionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/extension/SharedExtensionPatchKt { + public static final fun getSharedExtensionPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/fix/playback/SpoofVideoStreamsPatchKt { + public static final fun getSpoofVideoStreamsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/fix/playback/UserAgentClientSpoofPatchKt { + public static final fun getUserAgentClientSpoofPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatchKt { + public static final fun getGmsCoreSupportPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHookKt { + public static final fun addImageUrlErrorCallbackHook (Ljava/lang/String;)V + public static final fun addImageUrlHook (Ljava/lang/String;Z)V + public static synthetic fun addImageUrlHook$default (Ljava/lang/String;ZILjava/lang/Object;)V + public static final fun addImageUrlSuccessCallbackHook (Ljava/lang/String;)V + public static final fun getCronetImageUrlHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatchKt { + public static final fun getBypassURLRedirectsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/links/OpenLinksExternallyPatchKt { + public static final fun getOpenLinksExternallyPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatchKt { + public static final fun getAddLithoFilter ()Lkotlin/jvm/functions/Function1; + public static final fun getLithoFilterPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatchKt { + public static field hookNavigationButtonCreated Lkotlin/jvm/functions/Function1; + 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/misc/playercontrols/PlayerControlsPatchKt { + public static final fun getAddBottomControl ()Lkotlin/jvm/functions/Function1; + public static final fun getPlayerControlsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; + public static final fun getPlayerControlsResourcePatch ()Lapp/revanced/patcher/patch/ResourcePatch; + public static final fun initializeBottomControl (Ljava/lang/String;)V + public static final fun injectVisibilityCheckCall (Ljava/lang/String;)V +} + +public final class app/revanced/patches/youtube/misc/playertype/PlayerTypeHookPatchKt { + public static final fun getPlayerTypeHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/playservice/VersionCheckPatchKt { + public static final fun getVersionCheckPatch ()Lapp/revanced/patcher/patch/ResourcePatch; + public static final fun is_19_03_or_greater ()Z + public static final fun is_19_04_or_greater ()Z + public static final fun is_19_16_or_greater ()Z + public static final fun is_19_17_or_greater ()Z + public static final fun is_19_18_or_greater ()Z + public static final fun is_19_23_or_greater ()Z + public static final fun is_19_25_or_greater ()Z + public static final fun is_19_26_or_greater ()Z + public static final fun is_19_29_or_greater ()Z + public static final fun is_19_32_or_greater ()Z + public static final fun is_19_33_or_greater ()Z + public static final fun is_19_34_or_greater ()Z + public static final fun is_19_35_or_greater ()Z + public static final fun is_19_36_or_greater ()Z + public static final fun is_19_41_or_greater ()Z + public static final fun is_19_43_or_greater ()Z +} + +public final class app/revanced/patches/youtube/misc/privacy/RemoveTrackingQueryParameterPatchKt { + public static final fun getRemoveTrackingQueryParameterPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/recyclerviewtree/hook/RecyclerViewTreeHookPatchKt { + public static final fun getAddRecyclerViewTreeHook ()Lkotlin/jvm/functions/Function1; + public static final fun getRecyclerViewTreeHookPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/misc/settings/PreferenceScreen : app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen { + public static final field INSTANCE Lapp/revanced/patches/youtube/misc/settings/PreferenceScreen; + public fun commit (Lapp/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference;)V + public final fun getADS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; + public final fun getALTERNATIVE_THUMBNAILS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; + public final fun getFEED ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; + public final fun getGENERAL_LAYOUT ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; + public final fun getMISC ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; + public final fun getPLAYER ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; + public final fun getSEEKBAR ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; + public final fun getSHORTS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; + public final fun getSWIPE_CONTROLS ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; + public final fun getVIDEO ()Lapp/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen$Screen; +} + +public final class app/revanced/patches/youtube/misc/settings/SettingsPatchKt { + public static final fun addSettingPreference (Lapp/revanced/patches/shared/misc/settings/preference/BasePreference;)V + public static final fun getSettingsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; + public static final fun newIntent (Ljava/lang/String;)Lapp/revanced/patches/shared/misc/settings/preference/IntentPreference$Intent; +} + +public final class app/revanced/patches/youtube/misc/zoomhaptics/ZoomHapticsPatchKt { + public static final fun getZoomHapticsPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/shared/FingerprintsKt { + public static final fun getRollingNumberTextViewAnimationUpdateFingerprint ()Lapp/revanced/patcher/Fingerprint; +} + +public final class app/revanced/patches/youtube/video/information/VideoInformationPatchKt { + public static final fun getSetPlaybackSpeedClassFieldReference ()Ljava/lang/String; + public static final fun getSetPlaybackSpeedContainerClassFieldReference ()Ljava/lang/String; + public static final fun getSetPlaybackSpeedMethodReference ()Ljava/lang/String; + public static final fun getVideoInformationPatch ()Lapp/revanced/patcher/patch/BytecodePatch; + public static final fun userSelectedPlaybackSpeedHook (Ljava/lang/String;Ljava/lang/String;)V + public static final fun videoTimeHook (Ljava/lang/String;Ljava/lang/String;)V +} + +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$ProtoBufferParameter : app/revanced/patches/youtube/video/playerresponse/Hook { + public fun (Ljava/lang/String;)V +} + +public final class app/revanced/patches/youtube/video/playerresponse/Hook$ProtoBufferParameterBeforeVideoId : 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/quality/RememberVideoQualityPatchKt { + public static final fun getRememberVideoQualityPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/video/speed/PlaybackSpeedPatchKt { + public static final fun getPlaybackSpeedPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/video/speed/button/PlaybackSpeedButtonPatchKt { + public static final fun getPlaybackSpeedButtonPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatchKt { + public static final fun getSpeedUnavailableId ()J +} + +public final class app/revanced/patches/youtube/video/videoid/VideoIdPatchKt { + public static final fun getVideoIdPatch ()Lapp/revanced/patcher/patch/BytecodePatch; + public static final fun hookBackgroundPlayVideoId (Ljava/lang/String;)V + public static final fun hookPlayerResponseVideoId (Ljava/lang/String;)V + public static final fun hookVideoId (Ljava/lang/String;)V +} + +public final class app/revanced/patches/youtube/video/videoqualitymenu/RestoreOldVideoQualityMenuPatchKt { + public static final fun getRestoreOldVideoQualityMenuPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/patches/yuka/misc/unlockpremium/UnlockPremiumPatchKt { + public static final fun getUnlockPremiumPatch ()Lapp/revanced/patcher/patch/BytecodePatch; +} + +public final class app/revanced/util/BytecodeUtilsKt { + 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 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 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;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 injectHideViewCall (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;IILjava/lang/String;Ljava/lang/String;)V + public static final fun literal (Lapp/revanced/patcher/FingerprintBuilder;Lkotlin/jvm/functions/Function0;)V + public static final fun 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 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 asSequence (Lorg/w3c/dom/NodeList;)Lkotlin/sequences/Sequence; + public static final fun childElementsSequence (Lorg/w3c/dom/Node;)Lkotlin/sequences/Sequence; + public static final fun copyResources (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;[Lapp/revanced/util/ResourceGroup;)V + 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 insertFirst (Lorg/w3c/dom/Node;Lorg/w3c/dom/Node;)V + public static final fun iterateXmlNodeChildren (Lapp/revanced/patcher/patch/ResourcePatchContext;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V +} + +public final class app/revanced/util/resource/ArrayResource : app/revanced/util/resource/BaseResource { + public static final field Companion Lapp/revanced/util/resource/ArrayResource$Companion; + public fun (Ljava/lang/String;Ljava/util/List;)V + public final fun getItems ()Ljava/util/List; + public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; +} + +public final class app/revanced/util/resource/ArrayResource$Companion { + public final fun fromNode (Lorg/w3c/dom/Node;)Lapp/revanced/util/resource/ArrayResource; +} + +public abstract class app/revanced/util/resource/BaseResource { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getName ()Ljava/lang/String; + public final fun getTag ()Ljava/lang/String; + public fun hashCode ()I + public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; + public static synthetic fun serialize$default (Lapp/revanced/util/resource/BaseResource;Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/w3c/dom/Element; +} + +public final class app/revanced/util/resource/StringResource : app/revanced/util/resource/BaseResource { + public static final field Companion Lapp/revanced/util/resource/StringResource$Companion; + public fun (Ljava/lang/String;Ljava/lang/String;Z)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun getFormatted ()Z + public final fun getValue ()Ljava/lang/String; + public fun serialize (Lorg/w3c/dom/Document;Lkotlin/jvm/functions/Function1;)Lorg/w3c/dom/Element; +} + +public final class app/revanced/util/resource/StringResource$Companion { + public final fun fromNode (Lorg/w3c/dom/Node;)Lapp/revanced/util/resource/StringResource; +} + diff --git a/patches/build.gradle.kts b/patches/build.gradle.kts new file mode 100644 index 000000000..459d50a0c --- /dev/null +++ b/patches/build.gradle.kts @@ -0,0 +1,41 @@ +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) + // Required due to smali, or build fails. Can be removed once smali is bumped. + implementation(libs.guava) + // Android API stubs defined here. + compileOnly(project(":patches:stub")) +} + +kotlin { + compilerOptions { + freeCompilerArgs = listOf("-Xcontext-receivers") + } +} + +publishing { + repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/revanced/revanced-patches") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/all/activity/exportall/ExportAllActivitiesPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/activity/exportall/ExportAllActivitiesPatch.kt similarity index 63% rename from src/main/kotlin/app/revanced/patches/all/activity/exportall/ExportAllActivitiesPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/all/misc/activity/exportall/ExportAllActivitiesPatch.kt index ccb324694..30193b704 100644 --- a/src/main/kotlin/app/revanced/patches/all/activity/exportall/ExportAllActivitiesPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/activity/exportall/ExportAllActivitiesPatch.kt @@ -1,27 +1,22 @@ -package app.revanced.patches.all.activity.exportall +package app.revanced.patches.all.misc.activity.exportall -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patcher.patch.resourcePatch -@Patch( +@Suppress("unused") +val exportAllActivitiesPatch = resourcePatch( name = "Export all activities", description = "Makes all app activities exportable.", use = false, -) -@Suppress("unused") -object ExportAllActivitiesPatch : ResourcePatch() { - private const val EXPORTED_FLAG = "android:exported" - - override fun execute(context: ResourceContext) { - context.xmlEditor["AndroidManifest.xml"].use { editor -> - val document = editor.file +) { + execute { + val exportedFlag = "android:exported" + document("AndroidManifest.xml").use { document -> val activities = document.getElementsByTagName("activity") for (i in 0..activities.length) { activities.item(i)?.apply { - val exportedAttribute = attributes.getNamedItem(EXPORTED_FLAG) + val exportedAttribute = attributes.getNamedItem(exportedFlag) if (exportedAttribute != null) { if (exportedAttribute.nodeValue != "true") { @@ -31,7 +26,7 @@ object ExportAllActivitiesPatch : ResourcePatch() { // Reason why the attribute is added in the case it does not exist: // https://github.com/revanced/revanced-patches/pull/1751/files#r1141481604 else { - document.createAttribute(EXPORTED_FLAG) + document.createAttribute(exportedFlag) .apply { value = "true" } .let(attributes::setNamedItem) } diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/build/BaseSpoofBuildInfoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/build/BaseSpoofBuildInfoPatch.kt new file mode 100644 index 000000000..434b97e27 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/build/BaseSpoofBuildInfoPatch.kt @@ -0,0 +1,92 @@ +package app.revanced.patches.all.misc.build + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +private const val BUILD_CLASS_DESCRIPTOR = "Landroid/os/Build;" + +class BuildInfo( + // The build information supported32BitAbis, supported64BitAbis, and supportedAbis are not supported for now, + // because initializing an array in transform is a bit more complex. + val board: String? = null, + val bootloader: String? = null, + val brand: String? = null, + val cpuAbi: String? = null, + val cpuAbi2: String? = null, + val device: String? = null, + val display: String? = null, + val fingerprint: String? = null, + val hardware: String? = null, + val host: String? = null, + val id: String? = null, + val manufacturer: String? = null, + val model: String? = null, + val odmSku: String? = null, + val product: String? = null, + val radio: String? = null, + val serial: String? = null, + val sku: String? = null, + val socManufacturer: String? = null, + val socModel: String? = null, + val tags: String? = null, + val time: Long? = null, + val type: String? = null, + val user: String? = null, +) + +fun baseSpoofBuildInfoPatch(buildInfoSupplier: () -> BuildInfo) = bytecodePatch { + // Lazy, so that patch options above are initialized before they are accessed. + val replacements by lazy { + with(buildInfoSupplier()) { + buildMap { + if (board != null) put("BOARD", "const-string" to "\"$board\"") + if (bootloader != null) put("BOOTLOADER", "const-string" to "\"$bootloader\"") + if (brand != null) put("BRAND", "const-string" to "\"$brand\"") + if (cpuAbi != null) put("CPU_ABI", "const-string" to "\"$cpuAbi\"") + if (cpuAbi2 != null) put("CPU_ABI2", "const-string" to "\"$cpuAbi2\"") + if (device != null) put("DEVICE", "const-string" to "\"$device\"") + if (display != null) put("DISPLAY", "const-string" to "\"$display\"") + if (fingerprint != null) put("FINGERPRINT", "const-string" to "\"$fingerprint\"") + if (hardware != null) put("HARDWARE", "const-string" to "\"$hardware\"") + if (host != null) put("HOST", "const-string" to "\"$host\"") + if (id != null) put("ID", "const-string" to "\"$id\"") + if (manufacturer != null) put("MANUFACTURER", "const-string" to "\"$manufacturer\"") + if (model != null) put("MODEL", "const-string" to "\"$model\"") + if (odmSku != null) put("ODM_SKU", "const-string" to "\"$odmSku\"") + if (product != null) put("PRODUCT", "const-string" to "\"$product\"") + if (radio != null) put("RADIO", "const-string" to "\"$radio\"") + if (serial != null) put("SERIAL", "const-string" to "\"$serial\"") + if (sku != null) put("SKU", "const-string" to "\"$sku\"") + if (socManufacturer != null) put("SOC_MANUFACTURER", "const-string" to "\"$socManufacturer\"") + if (socModel != null) put("SOC_MODEL", "const-string" to "\"$socModel\"") + if (tags != null) put("TAGS", "const-string" to "\"$tags\"") + if (time != null) put("TIME", "const-wide" to "$time") + if (type != null) put("TYPE", "const-string" to "\"$type\"") + if (user != null) put("USER", "const-string" to "\"$user\"") + } + } + } + + dependsOn( + transformInstructionsPatch( + filterMap = filterMap@{ _, _, instruction, instructionIndex -> + val reference = instruction.getReference() ?: return@filterMap null + if (reference.definingClass != BUILD_CLASS_DESCRIPTOR) return@filterMap null + + return@filterMap replacements[reference.name]?.let { instructionIndex to it } + }, + transform = { mutableMethod, entry -> + val (index, replacement) = entry + val (opcode, operand) = replacement + val register = mutableMethod.getInstruction(index).registerA + + mutableMethod.replaceInstruction(index, "$opcode v$register, $operand") + }, + ), + ) +} diff --git a/src/main/kotlin/app/revanced/patches/all/misc/build/SpoofBuildInfoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/build/SpoofBuildInfoPatch.kt similarity index 57% rename from src/main/kotlin/app/revanced/patches/all/misc/build/SpoofBuildInfoPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/all/misc/build/SpoofBuildInfoPatch.kt index 994ddd455..7cfd385c6 100644 --- a/src/main/kotlin/app/revanced/patches/all/misc/build/SpoofBuildInfoPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/build/SpoofBuildInfoPatch.kt @@ -1,183 +1,214 @@ package app.revanced.patches.all.misc.build -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.longPatchOption -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.longOption +import app.revanced.patcher.patch.stringOption -@Patch( +@Suppress("unused") +val spoofBuildInfoPatch = bytecodePatch( name = "Spoof build info", description = "Spoof the information about the current build.", - use = false -) -@Suppress("unused") -class SpoofBuildInfoPatch : BaseSpoofBuildInfoPatch() { - override val board by stringPatchOption( + use = false, +) { + val board by stringOption( key = "board", default = null, title = "Board", - description = "The name of the underlying board, like \"goldfish\"." + description = "The name of the underlying board, like \"goldfish\".", ) - override val bootloader by stringPatchOption( + val bootloader by stringOption( key = "bootloader", default = null, title = "Bootloader", - description = "The system bootloader version number." + description = "The system bootloader version number.", ) - override val brand by stringPatchOption( + val brand by stringOption( key = "brand", default = null, title = "Brand", - description = "The consumer-visible brand with which the product/hardware will be associated, if any." + description = "The consumer-visible brand with which the product/hardware will be associated, if any.", ) - override val cpuAbi by stringPatchOption( + val cpuAbi by stringOption( key = "cpu-abi", default = null, title = "CPU ABI", - description = "This field was deprecated in API level 21. Use SUPPORTED_ABIS instead." + description = "This field was deprecated in API level 21. Use SUPPORTED_ABIS instead.", ) - override val cpuAbi2 by stringPatchOption( + val cpuAbi2 by stringOption( key = "cpu-abi-2", default = null, title = "CPU ABI 2", - description = "This field was deprecated in API level 21. Use SUPPORTED_ABIS instead." + description = "This field was deprecated in API level 21. Use SUPPORTED_ABIS instead.", ) - override val device by stringPatchOption( + val device by stringOption( key = "device", default = null, title = "Device", - description = "The name of the industrial design." + description = "The name of the industrial design.", ) - override val display by stringPatchOption( + val display by stringOption( key = "display", default = null, title = "Display", - description = "A build ID string meant for displaying to the user." + description = "A build ID string meant for displaying to the user.", ) - override val fingerprint by stringPatchOption( + val fingerprint by stringOption( key = "fingerprint", default = null, title = "Fingerprint", - description = "A string that uniquely identifies this build." + description = "A string that uniquely identifies this build.", ) - override val hardware by stringPatchOption( + val hardware by stringOption( key = "hardware", default = null, title = "Hardware", - description = "The name of the hardware (from the kernel command line or /proc)." + description = "The name of the hardware (from the kernel command line or /proc).", ) - override val host by stringPatchOption( + val host by stringOption( key = "host", default = null, title = "Host", - description = "The host." + description = "The host.", ) - override val id by stringPatchOption( + val id by stringOption( key = "id", default = null, title = "ID", - description = "Either a changelist number, or a label like \"M4-rc20\"." + description = "Either a changelist number, or a label like \"M4-rc20\".", ) - override val manufacturer by stringPatchOption( + val manufacturer by stringOption( key = "manufacturer", default = null, title = "Manufacturer", - description = "The manufacturer of the product/hardware." + description = "The manufacturer of the product/hardware.", ) - override val model by stringPatchOption( + val model by stringOption( key = "model", default = null, title = "Model", - description = "The end-user-visible name for the end product." + description = "The end-user-visible name for the end product.", ) - override val odmSku by stringPatchOption( + val odmSku by stringOption( key = "odm-sku", default = null, title = "ODM SKU", - description = "The SKU of the device as set by the original design manufacturer (ODM)." + description = "The SKU of the device as set by the original design manufacturer (ODM).", ) - override val product by stringPatchOption( + val product by stringOption( key = "product", default = null, title = "Product", - description = "The name of the overall product." + description = "The name of the overall product.", ) - override val radio by stringPatchOption( + val radio by stringOption( key = "radio", default = null, title = "Radio", description = "This field was deprecated in API level 15. " + - "The radio firmware version is frequently not available when this class is initialized, " + - "leading to a blank or \"unknown\" value for this string. Use getRadioVersion() instead." + "The radio firmware version is frequently not available when this class is initialized, " + + "leading to a blank or \"unknown\" value for this string. Use getRadioVersion() instead.", ) - override val serial by stringPatchOption( + val serial by stringOption( key = "serial", default = null, title = "Serial", - description = "This field was deprecated in API level 26. Use getSerial() instead." + description = "This field was deprecated in API level 26. Use getSerial() instead.", ) - override val sku by stringPatchOption( + val sku by stringOption( key = "sku", default = null, title = "SKU", - description = "The SKU of the hardware (from the kernel command line)." + description = "The SKU of the hardware (from the kernel command line).", ) - override val socManufacturer by stringPatchOption( + val socManufacturer by stringOption( key = "soc-manufacturer", default = null, title = "SOC Manufacturer", - description = "The manufacturer of the device's primary system-on-chip." + description = "The manufacturer of the device's primary system-on-chip.", ) - override val socModel by stringPatchOption( + val socModel by stringOption( key = "soc-model", default = null, title = "SOC Model", - description = "The model name of the device's primary system-on-chip." + description = "The model name of the device's primary system-on-chip.", ) - override val tags by stringPatchOption( + val tags by stringOption( key = "tags", default = null, title = "Tags", - description = "Comma-separated tags describing the build, like \"unsigned,debug\"." + description = "Comma-separated tags describing the build, like \"unsigned,debug\".", ) - override val time by longPatchOption( + val time by longOption( key = "time", default = null, title = "Time", - description = "The time at which the build was produced, given in milliseconds since the UNIX epoch." + description = "The time at which the build was produced, given in milliseconds since the UNIX epoch.", ) - override val type by stringPatchOption( + val type by stringOption( key = "type", default = null, title = "Type", - description = "The type of build, like \"user\" or \"eng\"." + description = "The type of build, like \"user\" or \"eng\".", ) - override val user by stringPatchOption( + val user by stringOption( key = "user", default = null, title = "User", - description = "The user." + description = "The user.", ) -} \ No newline at end of file + + dependsOn( + baseSpoofBuildInfoPatch { + BuildInfo( + board, + bootloader, + brand, + cpuAbi, + cpuAbi2, + device, + display, + fingerprint, + hardware, + host, + id, + manufacturer, + model, + odmSku, + product, + radio, + serial, + sku, + socManufacturer, + socModel, + tags, + time, + type, + user, + ) + }, + + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/location/hide/HideMockLocationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/location/hide/HideMockLocationPatch.kt new file mode 100644 index 000000000..b17eea94d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/location/hide/HideMockLocationPatch.kt @@ -0,0 +1,50 @@ +@file:Suppress("unused") + +package app.revanced.patches.all.misc.connectivity.location.hide + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.transformation.IMethodCall +import app.revanced.patches.all.misc.transformation.fromMethodReference +import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +@Suppress("unused") +val hideMockLocationPatch = bytecodePatch( + name = "Hide mock location", + description = "Prevents the app from knowing the device location is being mocked by a third party app.", + use = false, +) { + dependsOn( + transformInstructionsPatch( + filterMap = filter@{ _, _, instruction, instructionIndex -> + val reference = instruction.getReference() ?: return@filter null + if (fromMethodReference(reference) == null) return@filter null + + instruction to instructionIndex + }, + transform = { method, entry -> + val (instruction, index) = entry + instruction as FiveRegisterInstruction + + // Replace return value with a constant `false` boolean. + method.replaceInstruction( + index + 1, + "const/4 v${instruction.registerC}, 0x0", + ) + }, + ), + ) +} + +private enum class MethodCall( + override val definedClassName: String, + override val methodName: String, + override val methodParams: Array, + override val returnType: String, +) : IMethodCall { + IsMock("Landroid/location/Location;", "isMock", emptyArray(), "Z"), + IsFromMockProvider("Landroid/location/Location;", "isFromMockProvider", emptyArray(), "Z"), +} diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/telephony/sim/spoof/SpoofSimCountryPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/telephony/sim/spoof/SpoofSimCountryPatch.kt new file mode 100644 index 000000000..b50ccfc88 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/telephony/sim/spoof/SpoofSimCountryPatch.kt @@ -0,0 +1,105 @@ +package app.revanced.patches.all.misc.connectivity.telephony.sim.spoof + +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.stringOption +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +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.immutable.reference.ImmutableMethodReference +import com.android.tools.smali.dexlib2.util.MethodUtil +import java.util.* + +@Suppress("unused") +val spoofSimCountryPatch = bytecodePatch( + name = "Spoof SIM country", + description = "Spoofs country information returned by the SIM card provider.", + use = false, +) { + val countries = Locale.getISOCountries().associateBy { Locale("", it).displayCountry } + + fun isoCountryPatchOption( + key: String, + title: String, + ) = stringOption( + key, + null, + countries, + title, + "ISO-3166-1 alpha-2 country code equivalent for the SIM provider's country code.", + false, + validator = { it: String? -> it == null || it.uppercase() in countries.values }, + ) + + val networkCountryIso by isoCountryPatchOption( + "networkCountryIso", + "Network ISO Country Code", + ) + + val simCountryIso by isoCountryPatchOption( + "simCountryIso", + "Sim ISO Country Code", + ) + + dependsOn( + transformInstructionsPatch( + filterMap = { _, _, instruction, instructionIndex -> + if (instruction !is ReferenceInstruction) return@transformInstructionsPatch null + + val reference = instruction.reference as? MethodReference ?: return@transformInstructionsPatch null + + val match = MethodCall.entries.firstOrNull { search -> + MethodUtil.methodSignaturesMatch(reference, search.reference) + } ?: return@transformInstructionsPatch null + + val iso = when (match) { + MethodCall.NetworkCountryIso -> networkCountryIso + MethodCall.SimCountryIso -> simCountryIso + }?.lowercase() + + iso?.let { instructionIndex to it } + }, + transform = { mutableMethod, entry: Pair -> + transformMethodCall(entry, mutableMethod) + }, + ), + ) +} + +private fun transformMethodCall( + entry: Pair, + mutableMethod: MutableMethod, +) { + val (instructionIndex, methodCallValue) = entry + + val register = mutableMethod.getInstruction(instructionIndex + 1).registerA + + mutableMethod.replaceInstruction( + instructionIndex + 1, + "const-string v$register, \"$methodCallValue\"", + ) +} + +private enum class MethodCall( + val reference: MethodReference, +) { + NetworkCountryIso( + ImmutableMethodReference( + "Landroid/telephony/TelephonyManager;", + "getNetworkCountryIso", + emptyList(), + "Ljava/lang/String;", + ), + ), + SimCountryIso( + ImmutableMethodReference( + "Landroid/telephony/TelephonyManager;", + "getSimCountryIso", + emptyList(), + "Ljava/lang/String;", + ), + ), +} diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.kt new file mode 100644 index 000000000..3e086f95c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/connectivity/wifi/spoof/SpoofWifiPatch.kt @@ -0,0 +1,224 @@ +package app.revanced.patches.all.misc.connectivity.wifi.spoof + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.transformation.IMethodCall +import app.revanced.patches.all.misc.transformation.filterMapInstruction35c +import app.revanced.patches.all.misc.transformation.transformInstructionsPatch + +internal const val EXTENSION_CLASS_DESCRIPTOR_PREFIX = "Lapp/revanced/extension/all/connectivity/wifi/spoof/SpoofWifiPatch" + +internal const val EXTENSION_CLASS_DESCRIPTOR = "$EXTENSION_CLASS_DESCRIPTOR_PREFIX;" + +@Suppress("unused") +val spoofWifiPatch = bytecodePatch( + name = "Spoof Wi-Fi connection", + description = "Spoofs an existing Wi-Fi connection.", + use = false, +) { + extendWith("extensions/all/connectivity/wifi/spoof/spoof-wifi.rve") + + dependsOn( + transformInstructionsPatch( + filterMap = { classDef, _, instruction, instructionIndex -> + filterMapInstruction35c( + EXTENSION_CLASS_DESCRIPTOR_PREFIX, + classDef, + instruction, + instructionIndex, + ) + }, + transform = { method, entry -> + val (methodType, instruction, instructionIndex) = entry + methodType.replaceInvokeVirtualWithExtension( + EXTENSION_CLASS_DESCRIPTOR, + method, + instruction, + instructionIndex, + ) + }, + ), + ) +} + +// Information about method calls we want to replace +@Suppress("unused") +private enum class MethodCall( + override val definedClassName: String, + override val methodName: String, + override val methodParams: Array, + override val returnType: String, +) : IMethodCall { + GetSystemService1( + "Landroid/content/Context;", + "getSystemService", + arrayOf("Ljava/lang/String;"), + "Ljava/lang/Object;", + ), + GetSystemService2( + "Landroid/content/Context;", + "getSystemService", + arrayOf("Ljava/lang/Class;"), + "Ljava/lang/Object;", + ), + GetActiveNetworkInfo( + "Landroid/net/ConnectivityManager;", + "getActiveNetworkInfo", + arrayOf(), + "Landroid/net/NetworkInfo;", + ), + IsConnected( + "Landroid/net/NetworkInfo;", + "isConnected", + arrayOf(), + "Z", + ), + IsConnectedOrConnecting( + "Landroid/net/NetworkInfo;", + "isConnectedOrConnecting", + arrayOf(), + "Z", + ), + IsAvailable( + "Landroid/net/NetworkInfo;", + "isAvailable", + arrayOf(), + "Z", + ), + GetState( + "Landroid/net/NetworkInfo;", + "getState", + arrayOf(), + "Landroid/net/NetworkInfo\$State;", + ), + GetDetailedState( + "Landroid/net/NetworkInfo;", + "getDetailedState", + arrayOf(), + "Landroid/net/NetworkInfo\$DetailedState;", + ), + IsActiveNetworkMetered( + "Landroid/net/ConnectivityManager;", + "isActiveNetworkMetered", + arrayOf(), + "Z", + ), + GetActiveNetwork( + "Landroid/net/ConnectivityManager;", + "getActiveNetwork", + arrayOf(), + "Landroid/net/Network;", + ), + GetNetworkInfo( + "Landroid/net/ConnectivityManager;", + "getNetworkInfo", + arrayOf("Landroid/net/Network;"), + "Landroid/net/NetworkInfo;", + ), + HasTransport( + "Landroid/net/NetworkCapabilities;", + "hasTransport", + arrayOf("I"), + "Z", + ), + HasCapability( + "Landroid/net/NetworkCapabilities;", + "hasCapability", + arrayOf("I"), + "Z", + ), + RegisterBestMatchingNetworkCallback( + "Landroid/net/ConnectivityManager;", + "registerBestMatchingNetworkCallback", + arrayOf( + "Landroid/net/NetworkRequest;", + "Landroid/net/ConnectivityManager\$NetworkCallback;", + "Landroid/os/Handler;", + ), + "V", + ), + RegisterDefaultNetworkCallback1( + "Landroid/net/ConnectivityManager;", + "registerDefaultNetworkCallback", + arrayOf("Landroid/net/ConnectivityManager\$NetworkCallback;"), + "V", + ), + RegisterDefaultNetworkCallback2( + "Landroid/net/ConnectivityManager;", + "registerDefaultNetworkCallback", + arrayOf("Landroid/net/ConnectivityManager\$NetworkCallback;", "Landroid/os/Handler;"), + "V", + ), + RegisterNetworkCallback1( + "Landroid/net/ConnectivityManager;", + "registerNetworkCallback", + arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"), + "V", + ), + RegisterNetworkCallback2( + "Landroid/net/ConnectivityManager;", + "registerNetworkCallback", + arrayOf("Landroid/net/NetworkRequest;", "Landroid/app/PendingIntent;"), + "V", + ), + RegisterNetworkCallback3( + "Landroid/net/ConnectivityManager;", + "registerNetworkCallback", + arrayOf( + "Landroid/net/NetworkRequest;", + "Landroid/net/ConnectivityManager\$NetworkCallback;", + "Landroid/os/Handler;", + ), + "V", + ), + RequestNetwork1( + "Landroid/net/ConnectivityManager;", + "requestNetwork", + arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;"), + "V", + ), + RequestNetwork2( + "Landroid/net/ConnectivityManager;", + "requestNetwork", + arrayOf("Landroid/net/NetworkRequest;", "Landroid/net/ConnectivityManager\$NetworkCallback;", "I"), + "V", + ), + RequestNetwork3( + "Landroid/net/ConnectivityManager;", + "requestNetwork", + arrayOf( + "Landroid/net/NetworkRequest;", + "Landroid/net/ConnectivityManager\$NetworkCallback;", + "Landroid/os/Handler;", + ), + "V", + ), + RequestNetwork4( + "Landroid/net/ConnectivityManager;", + "requestNetwork", + arrayOf("Landroid/net/NetworkRequest;", "Landroid/app/PendingIntent;"), + "V", + ), + RequestNetwork5( + "Landroid/net/ConnectivityManager;", + "requestNetwork", + arrayOf( + "Landroid/net/NetworkRequest;", + "Landroid/net/ConnectivityManager\$NetworkCallback;", + "Landroid/os/Handler;", + "I", + ), + "V", + ), + UnregisterNetworkCallback1( + "Landroid/net/ConnectivityManager;", + "unregisterNetworkCallback", + arrayOf("Landroid/net/ConnectivityManager\$NetworkCallback;"), + "V", + ), + UnregisterNetworkCallback2( + "Landroid/net/ConnectivityManager;", + "unregisterNetworkCallback", + arrayOf("Landroid/app/PendingIntent;"), + "V", + ), +} diff --git a/src/main/kotlin/app/revanced/patches/all/misc/debugging/EnableAndroidDebuggingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/debugging/EnableAndroidDebuggingPatch.kt similarity index 56% rename from src/main/kotlin/app/revanced/patches/all/misc/debugging/EnableAndroidDebuggingPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/all/misc/debugging/EnableAndroidDebuggingPatch.kt index 4e3dedd32..fc7e33c5d 100644 --- a/src/main/kotlin/app/revanced/patches/all/misc/debugging/EnableAndroidDebuggingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/debugging/EnableAndroidDebuggingPatch.kt @@ -1,21 +1,15 @@ package app.revanced.patches.all.misc.debugging -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patcher.patch.resourcePatch import org.w3c.dom.Element -@Patch( +val enableAndroidDebuggingPatch = resourcePatch( name = "Enable Android debugging", description = "Enables Android debugging capabilities. This can slow down the app.", use = false, -) -@Suppress("unused") -object EnableAndroidDebuggingPatch : ResourcePatch() { - override fun execute(context: ResourceContext) { - context.xmlEditor["AndroidManifest.xml"].use { editor -> - val document = editor.file - +) { + execute { + document("AndroidManifest.xml").use { document -> val applicationNode = document .getElementsByTagName("application") diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatch.kt new file mode 100644 index 000000000..7256d1edb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/directory/ChangeDataDirectoryLocationPatch.kt @@ -0,0 +1,58 @@ +package app.revanced.patches.all.misc.directory + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference +import com.android.tools.smali.dexlib2.util.MethodUtil + +@Suppress("unused") +val changeDataDirectoryLocationPatch = bytecodePatch( + name = "Change data directory location", + description = "Changes the data directory in the application from " + + "the app internal storage directory to /sdcard/android/data accessible by root-less devices." + + "Using this patch can cause unexpected issues with some apps.", + use = false, +) { + dependsOn( + transformInstructionsPatch( + filterMap = filter@{ _, _, instruction, instructionIndex -> + val reference = instruction.getReference() ?: return@filter null + + if (!MethodUtil.methodSignaturesMatch(reference, MethodCall.GetDir.reference)) { + return@filter null + } + + return@filter instructionIndex + }, + transform = { method, index -> + val getDirInstruction = method.getInstruction(index) + val contextRegister = getDirInstruction.registerC + val dataRegister = getDirInstruction.registerD + + method.replaceInstruction( + index, + "invoke-virtual { v$contextRegister, v$dataRegister }, " + + "Landroid/content/Context;->getExternalFilesDir(Ljava/lang/String;)Ljava/io/File;", + ) + }, + ), + ) +} + +private enum class MethodCall( + val reference: MethodReference, +) { + GetDir( + ImmutableMethodReference( + "Landroid/content/Context;", + "getDir", + listOf("Ljava/lang/String;", "I"), + "Ljava/io/File;", + ), + ), +} diff --git a/src/main/kotlin/app/revanced/patches/all/misc/hex/HexPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/hex/HexPatch.kt similarity index 54% rename from src/main/kotlin/app/revanced/patches/all/misc/hex/HexPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/all/misc/hex/HexPatch.kt index 7aaee12b4..a8bb80517 100644 --- a/src/main/kotlin/app/revanced/patches/all/misc/hex/HexPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/hex/HexPatch.kt @@ -1,23 +1,22 @@ package app.revanced.patches.all.misc.hex import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.registerNewPatchOption -import app.revanced.patches.shared.misc.hex.BaseHexPatch +import app.revanced.patcher.patch.rawResourcePatch +import app.revanced.patcher.patch.stringsOption +import app.revanced.patches.shared.misc.hex.Replacement +import app.revanced.patches.shared.misc.hex.hexPatch import app.revanced.util.Utils.trimIndentMultiline -import app.revanced.patcher.patch.Patch as PatchClass -@Patch( +@Suppress("unused") +val hexPatch = rawResourcePatch( name = "Hex", description = "Replaces a hexadecimal patterns of bytes of files in an APK.", use = false, -) -@Suppress("unused") -class HexPatch : BaseHexPatch() { +) { // TODO: Instead of stringArrayOption, use a custom option type to work around // https://github.com/ReVanced/revanced-library/issues/48. // Replace the custom option type with a stringArrayOption once the issue is resolved. - private val replacementsOption by registerNewPatchOption, List>( + val replacements by stringsOption( key = "replacements", title = "Replacements", description = """ @@ -34,22 +33,24 @@ class HexPatch : BaseHexPatch() { 'aa 01 02 FF|00 00 00 00|path/to/file' """.trimIndentMultiline(), required = true, - valueType = "StringArray", ) - override val replacements - get() = replacementsOption!!.map { from -> - val (pattern, replacementPattern, targetFilePath) = try { - from.split("|", limit = 3) - } catch (e: Exception) { - throw PatchException( - "Invalid input: $from.\n" + - "Every pattern must be followed by a pipe ('|'), " + - "the replacement pattern, another pipe ('|'), " + - "and the path to the file to make the changes in relative to the APK root. ", - ) - } + dependsOn( + hexPatch { + replacements!!.map { from -> + val (pattern, replacementPattern, targetFilePath) = try { + from.split("|", limit = 3) + } catch (e: Exception) { + throw PatchException( + "Invalid input: $from.\n" + + "Every pattern must be followed by a pipe ('|'), " + + "the replacement pattern, another pipe ('|'), " + + "and the path to the file to make the changes in relative to the APK root. ", + ) + } - Replacement(pattern, replacementPattern, targetFilePath) - } + Replacement(pattern, replacementPattern, targetFilePath) + }.toSet() + }, + ) } diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/interaction/gestures/PredictiveBackGesturePatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/interaction/gestures/PredictiveBackGesturePatch.kt new file mode 100644 index 000000000..29f644435 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/interaction/gestures/PredictiveBackGesturePatch.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.all.misc.interaction.gestures + +import app.revanced.patcher.patch.resourcePatch + +@Suppress("unused") +val predictiveBackGesturePatch = resourcePatch( + name = "Predictive back gesture", + description = "Enables the predictive back gesture introduced on Android 13.", + use = false, +) { + execute { + val flag = "android:enableOnBackInvokedCallback" + + document("AndroidManifest.xml").use { document -> + with(document.getElementsByTagName("application").item(0)) { + if (attributes.getNamedItem(flag) != null) return@with + + document.createAttribute(flag) + .apply { value = "true" } + .let(attributes::setNamedItem) + } + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/all/misc/network/OverrideCertificatePinningPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/network/OverrideCertificatePinningPatch.kt similarity index 79% rename from src/main/kotlin/app/revanced/patches/all/misc/network/OverrideCertificatePinningPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/all/misc/network/OverrideCertificatePinningPatch.kt index dc300df4a..c85f4c3ba 100644 --- a/src/main/kotlin/app/revanced/patches/all/misc/network/OverrideCertificatePinningPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/network/OverrideCertificatePinningPatch.kt @@ -1,28 +1,24 @@ package app.revanced.patches.all.misc.network -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patches.all.misc.debugging.EnableAndroidDebuggingPatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.all.misc.debugging.enableAndroidDebuggingPatch import app.revanced.util.Utils.trimIndentMultiline import org.w3c.dom.Element import java.io.File -@Patch( +@Suppress("unused") +val overrideCertificatePinningPatch = resourcePatch( name = "Override certificate pinning", description = "Overrides certificate pinning, allowing to inspect traffic via a proxy.", - dependencies = [EnableAndroidDebuggingPatch::class], use = false, -) -@Suppress("unused") -object OverrideCertificatePinningPatch : ResourcePatch() { - override fun execute(context: ResourceContext) { - val resXmlDirectory = context.get("res/xml") +) { + dependsOn(enableAndroidDebuggingPatch) + + execute { + val resXmlDirectory = get("res/xml") // Add android:networkSecurityConfig="@xml/network_security_config" and the "networkSecurityConfig" attribute if it does not exist. - context.xmlEditor["AndroidManifest.xml"].use { editor -> - val document = editor.file - + document("AndroidManifest.xml").use { document -> val applicationNode = document.getElementsByTagName("application").item(0) as Element if (!applicationNode.hasAttribute("networkSecurityConfig")) { diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/packagename/ChangePackageNamePatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/packagename/ChangePackageNamePatch.kt new file mode 100644 index 000000000..48f951633 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/packagename/ChangePackageNamePatch.kt @@ -0,0 +1,60 @@ +package app.revanced.patches.all.misc.packagename + +import app.revanced.patcher.patch.Option +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import org.w3c.dom.Element + +lateinit var packageNameOption: Option + +/** + * Set the package name to use. + * If this is called multiple times, the first call will set the package name. + * + * @param fallbackPackageName The package name to use if the user has not already specified a package name. + * @return The package name that was set. + * @throws OptionException.ValueValidationException If the package name is invalid. + */ +fun setOrGetFallbackPackageName(fallbackPackageName: String): String { + val packageName = packageNameOption.value!! + + return if (packageName == packageNameOption.default) { + fallbackPackageName.also { packageNameOption.value = it } + } else { + packageName + } +} + +val changePackageNamePatch = resourcePatch( + name = "Change package name", + description = "Appends \".revanced\" to the package name by default. Changing the package name of the app can lead to unexpected issues.", + use = false, +) { + packageNameOption = stringOption( + key = "packageName", + default = "Default", + values = mapOf("Default" to "Default"), + title = "Package name", + description = "The name of the package to rename the app to.", + required = true, + ) { + it == "Default" || it!!.matches(Regex("^[a-z]\\w*(\\.[a-z]\\w*)+\$")) + } + + finalize { + document("AndroidManifest.xml").use { document -> + + val replacementPackageName = packageNameOption.value + + val manifest = document.getElementsByTagName("manifest").item(0) as Element + manifest.setAttribute( + "package", + if (replacementPackageName != packageNameOption.default) { + replacementPackageName + } else { + "${manifest.getAttribute("package")}.revanced" + }, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/resources/AddResourcesPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/resources/AddResourcesPatch.kt new file mode 100644 index 000000000..fed38b963 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/resources/AddResourcesPatch.kt @@ -0,0 +1,396 @@ +package app.revanced.patches.all.misc.resources + +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.util.Document +import app.revanced.util.* +import app.revanced.util.resource.ArrayResource +import app.revanced.util.resource.BaseResource +import app.revanced.util.resource.StringResource +import org.w3c.dom.Node + +/** + * An identifier of an app. For example, `youtube`. + */ +private typealias AppId = String + +/** + * An identifier of a patch. For example, `ad.general.HideAdsPatch`. + */ +private typealias PatchId = String + +/** + * A set of resources of a patch. + */ +private typealias PatchResources = MutableSet + +/** + * A map of resources belonging to a patch. + */ +private typealias AppResources = MutableMap + +/** + * A map of resources belonging to an app. + */ +private typealias Resources = MutableMap + +/** + * The value of a resource. + * For example, `values` or `values-de`. + */ +private typealias Value = String + +/** + * A set of resources mapped by their value. + */ +private typealias MutableResources = MutableMap> + +/** + * A map of all resources associated by their value staged by [addResourcesPatch]. + */ +private lateinit var stagedResources: Map + +/** + * A map of all resources added to the app by [addResourcesPatch]. + */ +private val resources: MutableResources = mutableMapOf() + +/** + * Map of Crowdin locales to Android resource locale names. + * + * Fixme: Instead this patch should detect what locale regions are present in both patches and the target app, + * and automatically merge into the appropriate existing target file. + * So if a target app has only 'es', then the Crowdin file of 'es-rES' should merge into that. + * But if a target app has specific regions (such as 'pt-rBR'), + * then the Crowdin region specific file should merged into that. + */ +private val locales = mapOf( + "af-rZA" to "af", + "am-rET" to "am", + "ar-rSA" to "ar", + "as-rIN" to "as", + "az-rAZ" to "az", + "be-rBY" to "be", + "bg-rBG" to "bg", + "bn-rBD" to "bn", + "bs-rBA" to "bs", + "ca-rES" to "ca", + "cs-rCZ" to "cs", + "da-rDK" to "da", + "de-rDE" to "de", + "el-rGR" to "el", + "es-rES" to "es", + "et-rEE" to "et", + "eu-rES" to "eu", + "fa-rIR" to "fa", + "fi-rFI" to "fi", + "fil-rPH" to "tl", + "fr-rFR" to "fr", + "ga-rIE" to "ga", + "gl-rES" to "gl", + "gu-rIN" to "gu", + "hi-rIN" to "hi", + "hr-rHR" to "hr", + "hu-rHU" to "hu", + "hy-rAM" to "hy", + "in-rID" to "in", + "is-rIS" to "is", + "it-rIT" to "it", + "iw-rIL" to "iw", + "ja-rJP" to "ja", + "ka-rGE" to "ka", + "kk-rKZ" to "kk", + "km-rKH" to "km", + "kn-rIN" to "kn", + "ko-rKR" to "ko", + "ky-rKG" to "ky", + "lo-rLA" to "lo", + "lt-rLT" to "lt", + "lv-rLV" to "lv", + "mk-rMK" to "mk", + "ml-rIN" to "ml", + "mn-rMN" to "mn", + "mr-rIN" to "mr", + "ms-rMY" to "ms", + "my-rMM" to "my", + "nb-rNO" to "nb", + "ne-rIN" to "ne", + "nl-rNL" to "nl", + "or-rIN" to "or", + "pa-rIN" to "pa", + "pl-rPL" to "pl", + "pt-rBR" to "pt-rBR", + "pt-rPT" to "pt-rPT", + "ro-rRO" to "ro", + "ru-rRU" to "ru", + "si-rLK" to "si", + "sk-rSK" to "sk", + "sl-rSI" to "sl", + "sq-rAL" to "sq", + "sr-rCS" to "b+sr+Latn", + "sr-rSP" to "sr", + "sv-rSE" to "sv", + "sw-rKE" to "sw", + "ta-rIN" to "ta", + "te-rIN" to "te", + "th-rTH" to "th", + "tl-rPH" to "tl", + "tr-rTR" to "tr", + "uk-rUA" to "uk", + "ur-rIN" to "ur", + "uz-rUZ" to "uz", + "vi-rVN" to "vi", + "zh-rCN" to "zh-rCN", + "zh-rTW" to "zh-rTW", + "zu-rZA" to "zu", +) + +/** + * Adds a [BaseResource] to the map using [MutableMap.getOrPut]. + * + * @param value The value of the resource. For example, `values` or `values-de`. + * @param resource The resource to add. + * + * @return True if the resource was added, false if it already existed. + */ +fun addResource( + value: Value, + resource: BaseResource, +) = resources.getOrPut(value, ::mutableSetOf).add(resource) + +/** + * Adds a list of [BaseResource]s to the map using [MutableMap.getOrPut]. + * + * @param value The value of the resource. For example, `values` or `values-de`. + * @param resources The resources to add. + * + * @return True if the resources were added, false if they already existed. + */ +fun addResources( + value: Value, + resources: Iterable, +) = app.revanced.patches.all.misc.resources.resources.getOrPut(value, ::mutableSetOf).addAll(resources) + +/** + * Adds a [StringResource]. + * + * @param name The name of the string resource. + * @param value The value of the string resource. + * @param formatted Whether the string resource is formatted. Defaults to `true`. + * @param resourceValue The value of the resource. For example, `values` or `values-de`. + * + * @return True if the resource was added, false if it already existed. + */ +fun addResources( + name: String, + value: String, + formatted: Boolean = true, + resourceValue: Value = "values", +) = addResource(resourceValue, StringResource(name, value, formatted)) + +/** + * Adds an [ArrayResource]. + * + * @param name The name of the array resource. + * @param items The items of the array resource. + * + * @return True if the resource was added, false if it already existed. + */ +fun addResources( + name: String, + items: List, +) = addResource("values", ArrayResource(name, items)) + +/** + * Puts all resources of any [Value] staged in [stagedResources] for the [Patch] to [addResources]. + * + * @param patch The [Patch] of the patch to stage resources for. + * @param parseIds A function that parses a set of [PatchId] each mapped to an [AppId] from the given [Patch]. + * This is used to access the resources in [addResources] to stage them in [stagedResources]. + * The default implementation assumes that the [Patch] has a name and declares packages it is compatible with. + * + * @return True if any resources were added, false if none were added. + * + * @see addResourcesPatch + */ +fun addResources( + patch: Patch<*>, + parseIds: (Patch<*>) -> Map> = { + val patchId = patch.name ?: throw PatchException("Patch has no name") + val packages = patch.compatiblePackages ?: throw PatchException("Patch has no compatible packages") + + buildMap> { + packages.forEach { (appId, _) -> + getOrPut(appId) { mutableSetOf() }.add(patchId) + } + } + }, +): Boolean { + var result = false + + // Stage resources for the given patch to addResourcesPatch associated with their value. + parseIds(patch).forEach { (appId, patchIds) -> + patchIds.forEach { patchId -> + stagedResources.forEach { (value, resources) -> + resources[appId]?.get(patchId)?.let { patchResources -> + if (addResources(value, patchResources)) result = true + } + } + } + } + + return result +} + +/** + * Puts all resources for the given [appId] and [patchId] staged in [addResources] to [addResourcesPatch]. + * + * + * @return True if any resources were added, false if none were added. + * + * @see addResourcesPatch + */ +fun addResources( + appId: AppId, + patchId: String, +) = stagedResources.forEach { (value, resources) -> + resources[appId]?.get(patchId)?.let { patchResources -> + addResources(value, patchResources) + } +} + +val addResourcesPatch = resourcePatch( + description = "Add resources such as strings or arrays to the app.", +) { + /* + The strategy of this patch is to stage resources present in `/resources/addresources`. + These resources are organized by their respective value and patch. + + On addResourcesPatch#execute, all resources are staged in a temporary map. + After that, other patches that depend on addResourcesPatch can call + addResourcesPatch#invoke(Patch) to stage resources belonging to that patch + from the temporary map to addResourcesPatch. + + After all patches that depend on addResourcesPatch have been executed, + addResourcesPatch#finalize is finally called to add all staged resources to the app. + */ + execute { + stagedResources = buildMap { + /** + * Puts resources under `/resources/addresources//.xml` into the map. + * + * @param sourceValue The source value of the resource. For example, `values` or `values-de-rDE`. + * @param destValue The destination value of the resource. For example, 'values' or 'values-de'. + * @param resourceKind The kind of the resource. For example, `strings` or `arrays`. + * @param transform A function that transforms the [Node]s from the XML files to a [BaseResource]. + */ + fun addResources( + sourceValue: Value, + destValue: Value = sourceValue, + resourceKind: String, + transform: (Node) -> BaseResource, + ) { + inputStreamFromBundledResource( + "addresources", + "$sourceValue/$resourceKind.xml", + )?.let { stream -> + // Add the resources associated with the given value to the map, + // instead of overwriting it. + // This covers the example case such as adding strings and arrays of the same value. + getOrPut(destValue, ::mutableMapOf).apply { + document(stream).use { document -> + document.getElementsByTagName("app").asSequence().forEach { app -> + val appId = app.attributes.getNamedItem("id").textContent + + getOrPut(appId, ::mutableMapOf).apply { + app.forEachChildElement { patch -> + val patchId = patch.attributes.getNamedItem("id").textContent + + getOrPut(patchId, ::mutableSetOf).apply { + patch.forEachChildElement { resourceNode -> + val resource = transform(resourceNode) + + add(resource) + } + } + } + } + } + } + } + } + } + + // Stage all resources to a temporary map. + // Staged resources consumed by addResourcesPatch#invoke(Patch) + // are later used in addResourcesPatch#finalize. + try { + val addStringResources = { source: Value, dest: Value -> + addResources(source, dest, "strings", StringResource::fromNode) + } + locales.forEach { (source, dest) -> addStringResources("values-$source", "values-$dest") } + addStringResources("values", "values") + + addResources("values", "values", "arrays", ArrayResource::fromNode) + } catch (e: Exception) { + throw PatchException("Failed to read resources", e) + } + } + } + + /** + * Adds all resources staged in [addResourcesPatch] to the app. + * This is called after all patches that depend on [addResourcesPatch] have been executed. + */ + finalize { + operator fun MutableMap>.invoke( + value: Value, + resource: BaseResource, + ) { + // TODO: Fix open-closed principle violation by modifying BaseResource#serialize so that it accepts + // a Value and the map of documents. It will then get or put the document suitable for its resource type + // to serialize itself to it. + val resourceFileName = + when (resource) { + is StringResource -> "strings" + is ArrayResource -> "arrays" + else -> throw NotImplementedError("Unsupported resource type") + } + + getOrPut(resourceFileName) { + val targetFile = this@finalize["res/$value/$resourceFileName.xml"].also { + it.parentFile?.mkdirs() + + if (it.createNewFile()) { + it.writeText("\n\n") + } + } + + document(targetFile.path).let { document -> + + // Save the target node here as well + // in order to avoid having to call document.getNode("resources") + // but also save the document so that it can be closed later. + document to document.getNode("resources") + } + }.let { (_, targetNode) -> + targetNode.addResource(resource) { invoke(value, it) } + } + } + + resources.forEach { (value, resources) -> + // A map of document associated by their kind (e.g. strings, arrays). + // Each document is accompanied by the target node to which resources are added. + // A map is used because Map#getOrPut allows opening a new document for the duration of a resource value. + // This is done to prevent having to open the files for every resource that is added. + // Instead, it is cached once and reused for resources of the same value. + // This map is later accessed to close all documents for the current resource value. + val documents = mutableMapOf>() + + resources.forEach { resource -> documents(value, resource) } + + documents.values.forEach { (document, _) -> document.close() } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/screencapture/RemoveScreenCaptureRestrictionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/screencapture/RemoveScreenCaptureRestrictionPatch.kt new file mode 100644 index 000000000..5eef83ac0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/screencapture/RemoveScreenCaptureRestrictionPatch.kt @@ -0,0 +1,83 @@ +package app.revanced.patches.all.misc.screencapture + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.all.misc.transformation.IMethodCall +import app.revanced.patches.all.misc.transformation.filterMapInstruction35c +import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +import org.w3c.dom.Element + +private val removeCaptureRestrictionResourcePatch = resourcePatch( + description = "Sets allowAudioPlaybackCapture in manifest to true.", +) { + execute { + document("AndroidManifest.xml").use { document -> + // Get the application node. + val applicationNode = + document + .getElementsByTagName("application") + .item(0) as Element + + // Set allowAudioPlaybackCapture attribute to true. + applicationNode.setAttribute("android:allowAudioPlaybackCapture", "true") + } + } +} + +private const val EXTENSION_CLASS_DESCRIPTOR_PREFIX = + "Lapp/revanced/extension/all/screencapture/removerestriction/RemoveScreencaptureRestrictionPatch" +private const val EXTENSION_CLASS_DESCRIPTOR = "$EXTENSION_CLASS_DESCRIPTOR_PREFIX;" + +@Suppress("unused") +val removeScreenCaptureRestrictionPatch = bytecodePatch( + name = "Remove screen capture restriction", + description = "Removes the restriction of capturing audio from apps that normally wouldn't allow it.", + use = false, +) { + extendWith("extensions/all/screencapture/remove-screen-capture-restriction.rve") + + dependsOn( + removeCaptureRestrictionResourcePatch, + transformInstructionsPatch( + filterMap = { classDef, _, instruction, instructionIndex -> + filterMapInstruction35c( + EXTENSION_CLASS_DESCRIPTOR_PREFIX, + classDef, + instruction, + instructionIndex, + ) + }, + transform = { mutableMethod, entry -> + val (methodType, instruction, instructionIndex) = entry + methodType.replaceInvokeVirtualWithExtension( + EXTENSION_CLASS_DESCRIPTOR, + mutableMethod, + instruction, + instructionIndex, + ) + }, + ), + ) +} + +// Information about method calls we want to replace +@Suppress("unused") +private enum class MethodCall( + override val definedClassName: String, + override val methodName: String, + override val methodParams: Array, + override val returnType: String, +) : IMethodCall { + SetAllowedCapturePolicySingle( + "Landroid/media/AudioAttributes\$Builder;", + "setAllowedCapturePolicy", + arrayOf("I"), + "Landroid/media/AudioAttributes\$Builder;", + ), + SetAllowedCapturePolicyGlobal( + "Landroid/media/AudioManager;", + "setAllowedCapturePolicy", + arrayOf("I"), + "V", + ), +} diff --git a/patches/src/main/kotlin/app/revanced/patches/all/misc/screenshot/RemoveScreenshotRestrictionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/screenshot/RemoveScreenshotRestrictionPatch.kt new file mode 100644 index 000000000..af689f5ed --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/screenshot/RemoveScreenshotRestrictionPatch.kt @@ -0,0 +1,97 @@ +package app.revanced.patches.all.misc.screenshot + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.transformation.IMethodCall +import app.revanced.patches.all.misc.transformation.filterMapInstruction35c +import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22c +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +private const val EXTENSION_CLASS_DESCRIPTOR_PREFIX = + "Lapp/revanced/extension/all/screenshot/removerestriction/RemoveScreenshotRestrictionPatch" +private const val EXTENSION_CLASS_DESCRIPTOR = "$EXTENSION_CLASS_DESCRIPTOR_PREFIX;" + +@Suppress("unused") +val removeScreenshotRestrictionPatch = bytecodePatch( + name = "Remove screenshot restriction", + description = "Removes the restriction of taking screenshots in apps that normally wouldn't allow it.", + use = false, +) { + extendWith("extensions/all/screenshot/remove-screenshot-restriction.rve") + + dependsOn( + // Remove the restriction of taking screenshots. + transformInstructionsPatch( + filterMap = { classDef, _, instruction, instructionIndex -> + filterMapInstruction35c( + EXTENSION_CLASS_DESCRIPTOR_PREFIX, + classDef, + instruction, + instructionIndex, + ) + }, + transform = { mutableMethod, entry -> + val (methodType, instruction, instructionIndex) = entry + methodType.replaceInvokeVirtualWithExtension( + EXTENSION_CLASS_DESCRIPTOR, + mutableMethod, + instruction, + instructionIndex, + ) + }, + ), + // Modify layout params. + transformInstructionsPatch( + filterMap = { _, _, instruction, instructionIndex -> + if (instruction.opcode != Opcode.IPUT) { + return@transformInstructionsPatch null + } + + val instruction22c = instruction as Instruction22c + val fieldReference = instruction22c.reference as FieldReference + + if (fieldReference.definingClass != "Landroid/view/WindowManager\$LayoutParams;" || + fieldReference.name != "flags" || + fieldReference.type != "I" + ) { + return@transformInstructionsPatch null + } + + Pair(instruction22c, instructionIndex) + }, + transform = { mutableMethod, entry -> + val (instruction, index) = entry + val register = instruction.registerA + + mutableMethod.addInstructions( + index, + "and-int/lit16 v$register, v$register, -0x2001", + ) + }, + ), + ) +} + +// Information about method calls we want to replace +@Suppress("unused") +private enum class MethodCall( + override val definedClassName: String, + override val methodName: String, + override val methodParams: Array, + override val returnType: String, +) : IMethodCall { + AddFlags( + "Landroid/view/Window;", + "addFlags", + arrayOf("I"), + "V", + ), + SetFlags( + "Landroid/view/Window;", + "setFlags", + arrayOf("I", "I"), + "V", + ), +} diff --git a/src/main/kotlin/app/revanced/patches/all/shortcut/sharetargets/RemoveShareTargetsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/shortcut/sharetargets/RemoveShareTargetsPatch.kt similarity index 58% rename from src/main/kotlin/app/revanced/patches/all/shortcut/sharetargets/RemoveShareTargetsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/all/misc/shortcut/sharetargets/RemoveShareTargetsPatch.kt index 6e7fbb936..6b07c7dc6 100644 --- a/src/main/kotlin/app/revanced/patches/all/shortcut/sharetargets/RemoveShareTargetsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/shortcut/sharetargets/RemoveShareTargetsPatch.kt @@ -1,26 +1,23 @@ -package app.revanced.patches.all.shortcut.sharetargets +package app.revanced.patches.all.misc.shortcut.sharetargets -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patcher.patch.resourcePatch import app.revanced.util.asSequence import app.revanced.util.getNode import org.w3c.dom.Element import java.io.FileNotFoundException import java.util.logging.Logger -@Patch( +@Suppress("unused") +val removeShareTargetsPatch = resourcePatch( name = "Remove share targets", description = "Removes share targets like directly sharing to a frequent contact.", use = false, -) -@Suppress("unused") -object RemoveShareTargetsPatch : ResourcePatch() { - override fun execute(context: ResourceContext) { +) { + execute { try { - context.document["res/xml/shortcuts.xml"] + document("res/xml/shortcuts.xml") } catch (_: FileNotFoundException) { - return Logger.getLogger(this::class.java.name).warning("The app has no shortcuts") + return@execute Logger.getLogger(this::class.java.name).warning("The app has no shortcuts") }.use { document -> val rootNode = document.getNode("shortcuts") as? Element ?: return@use diff --git a/src/main/kotlin/app/revanced/patches/all/misc/transformation/MethodCall.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/transformation/MethodCall.kt similarity index 67% rename from src/main/kotlin/app/revanced/patches/all/misc/transformation/MethodCall.kt rename to patches/src/main/kotlin/app/revanced/patches/all/misc/transformation/MethodCall.kt index 5736ff247..0c39436d1 100644 --- a/src/main/kotlin/app/revanced/patches/all/misc/transformation/MethodCall.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/transformation/MethodCall.kt @@ -18,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. * @@ -27,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/all/misc/transformation/BaseTransformInstructionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/all/misc/transformation/TransformInstructionsPatch.kt similarity index 65% rename from src/main/kotlin/app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/all/misc/transformation/TransformInstructionsPatch.kt index e651c4d63..6564f4f26 100644 --- a/src/main/kotlin/app/revanced/patches/all/misc/transformation/BaseTransformInstructionsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/all/misc/transformation/TransformInstructionsPatch.kt @@ -1,35 +1,26 @@ package app.revanced.patches.all.misc.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 { // Returns the patch indices as a Sequence, which will execute lazily. - fun findPatchIndices(classDef: ClassDef, method: Method): Sequence? { - return method.implementation?.instructions?.asSequence()?.withIndex()?.mapNotNull { (index, instruction) -> - filterMap(classDef, method, instruction, index) - } + 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, @@ -45,7 +36,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 = findPatchIndices(mutableClass, mutableMethod)?.toCollection(ArrayDeque()) 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 64% 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 5170332db..617f18a42 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,22 +1,19 @@ package app.revanced.patches.all.misc.versioncode -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.intPatchOption +import app.revanced.patcher.patch.intOption +import app.revanced.patcher.patch.resourcePatch import app.revanced.util.getNode import org.w3c.dom.Element -@Patch( +@Suppress("unused") +val changeVersionCodePatch = resourcePatch( name = "Change version code", description = "Changes the version code of the app. By default the highest version code is set. " + "This allows older versions of an app to be installed " + "if their version code is set to the same or a higher value and can stop app stores to update the app.", use = false, -) -@Suppress("unused") -object ChangeVersionCodePatch : ResourcePatch() { - private val versionCode by intPatchOption( +) { + val versionCode by intOption( key = "versionCode", default = Int.MAX_VALUE, values = mapOf( @@ -26,12 +23,10 @@ object ChangeVersionCodePatch : ResourcePatch() { title = "Version code", description = "The version code to use", required = true, - ) { - it!! >= 1 - } + ) { versionCode -> versionCode!! >= 1 } - override fun execute(context: ResourceContext) { - context.document["AndroidManifest.xml"].use { document -> + execute { + document("AndroidManifest.xml").use { document -> val manifestElement = document.getNode("manifest") as Element manifestElement.setAttribute("android:versionCode", "$versionCode") } diff --git a/patches/src/main/kotlin/app/revanced/patches/amazon/DeepLinkingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/amazon/DeepLinkingPatch.kt new file mode 100644 index 000000000..bf53d8622 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/amazon/DeepLinkingPatch.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.amazon + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val deepLinkingPatch = bytecodePatch( + name = "Always allow deep-linking", + description = "Open Amazon links, even if the app is not set to handle Amazon links.", +) { + compatibleWith("com.amazon.mShop.android.shopping") + + execute { + deepLinkingFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/amazon/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/amazon/Fingerprints.kt new file mode 100644 index 000000000..1339149f4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/amazon/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.amazon + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val deepLinkingFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE) + returns("Z") + parameters("L") + strings("https://www.", "android.intent.action.VIEW") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/backdrops/misc/pro/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/backdrops/misc/pro/Fingerprints.kt new file mode 100644 index 000000000..bbe71e8c7 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/backdrops/misc/pro/Fingerprints.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.backdrops.misc.pro + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val proUnlockFingerprint = fingerprint { + opcodes( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ, + ) + custom { method, _ -> + method.name == "lambda\$existPurchase\$0" && + method.definingClass == "Lcom/backdrops/wallpapers/data/local/DatabaseHandlerIAB;" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/backdrops/misc/pro/ProUnlockPatch.kt b/patches/src/main/kotlin/app/revanced/patches/backdrops/misc/pro/ProUnlockPatch.kt new file mode 100644 index 000000000..70bbcd9fd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/backdrops/misc/pro/ProUnlockPatch.kt @@ -0,0 +1,25 @@ +package app.revanced.patches.backdrops.misc.pro + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +@Suppress("unused") +val proUnlockPatch = bytecodePatch( + name = "Pro unlock", +) { + compatibleWith("com.backdrops.wallpapers") + + execute { + val registerIndex = proUnlockFingerprint.patternMatch!!.endIndex - 1 + + proUnlockFingerprint.method.apply { + val register = getInstruction(registerIndex).registerA + addInstruction( + proUnlockFingerprint.patternMatch!!.endIndex, + "const/4 v$register, 0x1", + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/bandcamp/limitations/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/bandcamp/limitations/Fingerprints.kt new file mode 100644 index 000000000..96bbab5cc --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/bandcamp/limitations/Fingerprints.kt @@ -0,0 +1,7 @@ +package app.revanced.patches.bandcamp.limitations + +import app.revanced.patcher.fingerprint + +internal val handlePlaybackLimitsFingerprint = fingerprint { + strings("play limits processing track", "found play_count") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/bandcamp/limitations/RemovePlayLimitsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/bandcamp/limitations/RemovePlayLimitsPatch.kt new file mode 100644 index 000000000..9ba60935f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/bandcamp/limitations/RemovePlayLimitsPatch.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.bandcamp.limitations + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val removePlayLimitsPatch = bytecodePatch( + name = "Remove play limits", + description = "Disables purchase nagging and playback limits of not purchased tracks.", +) { + compatibleWith("com.bandcamp.android") + + execute { + handlePlaybackLimitsFingerprint.method.addInstructions(0, "return-void") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/cieid/restrictions/root/BypassRootChecksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/cieid/restrictions/root/BypassRootChecksPatch.kt new file mode 100644 index 000000000..c2abcc5b6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/cieid/restrictions/root/BypassRootChecksPatch.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.cieid.restrictions.root + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val bypassRootChecksPatch = bytecodePatch( + name = "Bypass root checks", + description = "Removes the restriction to use the app with root permissions or on a custom ROM.", +) { + compatibleWith("it.ipzs.cieid") + + execute { + checkRootFingerprint.method.addInstruction(1, "return-void") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/cieid/restrictions/root/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/cieid/restrictions/root/Fingerprints.kt new file mode 100644 index 000000000..387b0a0be --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/cieid/restrictions/root/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.cieid.restrictions.root + +import app.revanced.patcher.fingerprint + +internal val checkRootFingerprint = fingerprint { + custom { method, _ -> + method.name == "onResume" && method.definingClass == "Lit/ipzs/cieid/BaseActivity;" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/duolingo/ad/DisableAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/duolingo/ad/DisableAdsPatch.kt new file mode 100644 index 000000000..a5c56ddb0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/duolingo/ad/DisableAdsPatch.kt @@ -0,0 +1,32 @@ +package app.revanced.patches.duolingo.ad + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +@Suppress("unused") +val disableAdsPatch = bytecodePatch( + "Disable ads", +) { + compatibleWith("com.duolingo") + + execute { + // Couple approaches to remove ads exist: + // + // MonetizationDebugSettings has a boolean value for "disableAds". + // OnboardingState has a getter to check if the user has any "adFreeSessions". + // SharedPreferences has a debug boolean value with key "disable_ads", which maps to "DebugCategory.DISABLE_ADS". + // + // MonetizationDebugSettings seems to be the most general setting to work fine. + initializeMonetizationDebugSettingsFingerprint.method.apply { + val insertIndex = initializeMonetizationDebugSettingsFingerprint.patternMatch!!.startIndex + val register = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, + "const/4 v$register, 0x1", + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/duolingo/ad/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/duolingo/ad/Fingerprints.kt new file mode 100644 index 000000000..59b0644d9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/duolingo/ad/Fingerprints.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.duolingo.ad + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val initializeMonetizationDebugSettingsFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + parameters( + "Z", // disableAds + "Z", // useDebugBilling + "Z", // showManageSubscriptions + "Z", // alwaysShowSuperAds + "Lcom/duolingo/debug/FamilyQuestOverride;", + ) + opcodes(Opcode.IPUT_BOOLEAN) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/duolingo/debug/EnableDebugMenuPatch.kt b/patches/src/main/kotlin/app/revanced/patches/duolingo/debug/EnableDebugMenuPatch.kt new file mode 100644 index 000000000..833f9dd7d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/duolingo/debug/EnableDebugMenuPatch.kt @@ -0,0 +1,26 @@ +package app.revanced.patches.duolingo.debug + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +@Suppress("unused") +val enableDebugMenuPatch = bytecodePatch( + name = "Enable debug menu", + use = false, +) { + compatibleWith("com.duolingo"("5.158.4")) + + execute { + initializeBuildConfigProviderFingerprint.method.apply { + val insertIndex = initializeBuildConfigProviderFingerprint.patternMatch!!.startIndex + val register = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, + "const/4 v$register, 0x1", + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/duolingo/debug/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/duolingo/debug/Fingerprints.kt new file mode 100644 index 000000000..543e40b43 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/duolingo/debug/Fingerprints.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.duolingo.debug + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +/** + * The `BuildConfigProvider` class has two booleans: + * + * - `isChina`: (usually) compares "play" with "china"...except for builds in China + * - `isDebug`: compares "release" with "debug" <-- we want to force this to `true` + */ + +internal val initializeBuildConfigProviderFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + opcodes(Opcode.IPUT_BOOLEAN) + strings("debug", "release", "china") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/facebook/ads/mainfeed/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/facebook/ads/mainfeed/Fingerprints.kt new file mode 100644 index 000000000..7c732378f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/facebook/ads/mainfeed/Fingerprints.kt @@ -0,0 +1,47 @@ +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val baseModelMapperFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Lcom/facebook/graphql/modelutil/BaseModelWithTree;") + parameters("Ljava/lang/Class", "I", "I") + opcodes( + Opcode.SGET_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CONST_4, + Opcode.IF_EQ, + ) +} + +internal val getSponsoredDataModelTemplateFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("L") + parameters() + opcodes( + Opcode.CONST, + Opcode.CONST, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.RETURN_OBJECT, + ) + custom { _, classDef -> + classDef.type == "Lcom/facebook/graphql/model/GraphQLFBMultiAdsFeedUnit;" + } +} + +internal val getStoryVisibilityFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Ljava/lang/String;") + opcodes( + Opcode.INSTANCE_OF, + Opcode.IF_NEZ, + Opcode.INSTANCE_OF, + Opcode.IF_NEZ, + Opcode.INSTANCE_OF, + Opcode.IF_NEZ, + Opcode.CONST, + ) + strings("This should not be called for base class object") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/facebook/ads/mainfeed/HideSponsoredStoriesPatch.kt b/patches/src/main/kotlin/app/revanced/patches/facebook/ads/mainfeed/HideSponsoredStoriesPatch.kt new file mode 100644 index 000000000..e8a406587 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/facebook/ads/mainfeed/HideSponsoredStoriesPatch.kt @@ -0,0 +1,87 @@ +package app.revanced.patches.facebook.ads.mainfeed + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import baseModelMapperFingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction31i +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter +import getSponsoredDataModelTemplateFingerprint +import getStoryVisibilityFingerprint + +@Suppress("unused") +val hideSponsoredStoriesPatch = bytecodePatch( + name = "Hide 'Sponsored Stories'", +) { + compatibleWith("com.facebook.katana") + + execute { + val sponsoredDataModelTemplateMethod = getSponsoredDataModelTemplateFingerprint.originalMethod + val baseModelMapperMethod = baseModelMapperFingerprint.originalMethod + val baseModelWithTreeType = baseModelMapperMethod.returnType + + val graphQlStoryClassDescriptor = "Lcom/facebook/graphql/model/GraphQLStory;" + + // The "SponsoredDataModelTemplate" methods has the ids in its body to extract sponsored data + // from GraphQL models, but targets the wrong derived type of "BaseModelWithTree". Since those ids + // could change in future version, we need to extract them and call the base implementation directly. + + val getSponsoredDataHelperMethod = ImmutableMethod( + getStoryVisibilityFingerprint.originalClassDef.type, + "getSponsoredData", + listOf(ImmutableMethodParameter(graphQlStoryClassDescriptor, null, null)), + baseModelWithTreeType, + AccessFlags.PRIVATE.value or AccessFlags.STATIC.value, + null, + null, + MutableMethodImplementation(4), + ).toMutable().apply { + // Extract the ids of the original method. These ids seem to correspond to model types for + // GraphQL data structure. They are then fed to a method of BaseModelWithTree that populate + // and cast the requested GraphQL subtype. The Ids are found in the two first "CONST" instructions. + val constInstructions = sponsoredDataModelTemplateMethod.implementation!!.instructions + .asSequence() + .filterIsInstance() + .take(2) + .toList() + + val storyTypeId = constInstructions[0].narrowLiteral + val sponsoredDataTypeId = constInstructions[1].narrowLiteral + + addInstructions( + """ + const-class v2, $baseModelWithTreeType + const v1, $storyTypeId + const v0, $sponsoredDataTypeId + invoke-virtual {p0, v2, v1, v0}, $baseModelMapperMethod + move-result-object v0 + check-cast v0, $baseModelWithTreeType + return-object v0 + """, + ) + } + + getStoryVisibilityFingerprint.classDef.methods.add(getSponsoredDataHelperMethod) + + // Check if the parameter type is GraphQLStory and if sponsoredDataModelGetter returns a non-null value. + // If so, hide the story by setting the visibility to StoryVisibility.GONE. + getStoryVisibilityFingerprint.method.addInstructionsWithLabels( + getStoryVisibilityFingerprint.patternMatch!!.startIndex, + """ + instance-of v0, p0, $graphQlStoryClassDescriptor + if-eqz v0, :resume_normal + invoke-static {p0}, $getSponsoredDataHelperMethod + move-result-object v0 + if-eqz v0, :resume_normal + const-string v0, "GONE" + return-object v0 + :resume_normal + nop + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/facebook/ads/story/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/facebook/ads/story/Fingerprints.kt new file mode 100644 index 000000000..293d7ee82 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/facebook/ads/story/Fingerprints.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.facebook.ads.story + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.iface.value.StringEncodedValue + +internal val adsInsertionFingerprint = fieldFingerprint( + fieldValue = "AdBucketDataSourceUtil\$attemptAdsInsertion\$1", +) + +internal val fetchMoreAdsFingerprint = fieldFingerprint( + fieldValue = "AdBucketDataSourceUtil\$attemptFetchMoreAds\$1", +) + +internal fun fieldFingerprint(fieldValue: String) = fingerprint { + returns("V") + parameters() + custom { method, classDef -> + method.name == "run" && + classDef.fields.any any@{ field -> + if (field.name != "__redex_internal_original_name") return@any false + (field.initialValue as? StringEncodedValue)?.value == fieldValue + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/facebook/ads/story/HideStoryAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/facebook/ads/story/HideStoryAdsPatch.kt new file mode 100644 index 000000000..ec811fc3c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/facebook/ads/story/HideStoryAdsPatch.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.facebook.ads.story + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val hideStoryAdsPatch = bytecodePatch( + name = "Hide story ads", + description = "Hides the ads in the Facebook app stories.", +) { + compatibleWith("com.facebook.katana") + + execute { + setOf( + fetchMoreAdsFingerprint, + adsInsertionFingerprint, + ).forEach { fingerprint -> + fingerprint.method.replaceInstruction(0, "return-void") + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/finanzonline/detection/bootloader/BootloaderDetectionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/finanzonline/detection/bootloader/BootloaderDetectionPatch.kt new file mode 100644 index 000000000..76dc2413f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/finanzonline/detection/bootloader/BootloaderDetectionPatch.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.finanzonline.detection.bootloader + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val bootloaderDetectionPatch = bytecodePatch( + name = "Remove bootloader detection", + description = "Removes the check for an unlocked bootloader.", +) { + compatibleWith("at.gv.bmf.bmf2go") + + execute { + setOf(createKeyFingerprint, bootStateFingerprint).forEach { fingerprint -> + fingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/finanzonline/detection/bootloader/fingerprints/BootStateFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/finanzonline/detection/bootloader/Fingerprints.kt similarity index 57% rename from src/main/kotlin/app/revanced/patches/finanzonline/detection/bootloader/fingerprints/BootStateFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/finanzonline/detection/bootloader/Fingerprints.kt index f76ab78aa..4c8ee1c54 100644 --- a/src/main/kotlin/app/revanced/patches/finanzonline/detection/bootloader/fingerprints/BootStateFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/finanzonline/detection/bootloader/Fingerprints.kt @@ -1,14 +1,14 @@ -package app.revanced.patches.finanzonline.detection.bootloader.fingerprints +package app.revanced.patches.finanzonline.detection.bootloader -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint // Located @ at.gv.bmf.bmf2go.taxequalization.tools.utils.AttestationHelper#isBootStateOk (3.0.1) -internal object BootStateFingerprint : MethodFingerprint( - "Z", - accessFlags = AccessFlags.PUBLIC.value, - opcodes = listOf( +internal val bootStateFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("Z") + opcodes( Opcode.INVOKE_DIRECT, Opcode.MOVE_RESULT_OBJECT, Opcode.CONST_4, @@ -27,4 +27,11 @@ internal object BootStateFingerprint : MethodFingerprint( Opcode.MOVE, Opcode.RETURN ) -) +} + +// Located @ at.gv.bmf.bmf2go.taxequalization.tools.utils.AttestationHelper#createKey (3.0.1) +internal val createKeyFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("Z") + strings("attestation", "SHA-256", "random", "EC", "AndroidKeyStore") +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/finanzonline/detection/root/fingerprints/RootDetectionFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/finanzonline/detection/root/Fingerprints.kt similarity index 53% rename from src/main/kotlin/app/revanced/patches/finanzonline/detection/root/fingerprints/RootDetectionFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/finanzonline/detection/root/Fingerprints.kt index 50c7f76a8..fec7f1249 100644 --- a/src/main/kotlin/app/revanced/patches/finanzonline/detection/root/fingerprints/RootDetectionFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/finanzonline/detection/root/Fingerprints.kt @@ -1,16 +1,15 @@ -package app.revanced.patches.finanzonline.detection.root.fingerprints +package app.revanced.patches.finanzonline.detection.root -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint // Located @ at.gv.bmf.bmf2go.taxequalization.tools.utils.RootDetection#isRooted (3.0.1) -internal object RootDetectionFingerprint : MethodFingerprint( - "L", - accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, - parameters = listOf("L"), - opcodes = listOf( +internal val rootDetectionFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("L") + parameters("L") + opcodes( Opcode.NEW_INSTANCE, Opcode.INVOKE_DIRECT, Opcode.INVOKE_VIRTUAL, @@ -19,4 +18,4 @@ internal object RootDetectionFingerprint : MethodFingerprint( Opcode.MOVE_RESULT_OBJECT, Opcode.RETURN_OBJECT ) -) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/finanzonline/detection/root/RootDetectionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/finanzonline/detection/root/RootDetectionPatch.kt new file mode 100644 index 000000000..e2b7b0616 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/finanzonline/detection/root/RootDetectionPatch.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.finanzonline.detection.root + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val rootDetectionPatch = bytecodePatch( + name = "Remove root detection", + description = "Removes the check for root permissions.", +) { + compatibleWith("at.gv.bmf.bmf2go") + + execute { + rootDetectionFingerprint.method.addInstructions( + 0, + """ + sget-object v0, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean; + return-object v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/googlenews/customtabs/EnableCustomTabsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/googlenews/customtabs/EnableCustomTabsPatch.kt new file mode 100644 index 000000000..0e84ec6df --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/googlenews/customtabs/EnableCustomTabsPatch.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.googlenews.customtabs + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.sun.org.apache.bcel.internal.generic.InstructionConst.getInstruction + +@Suppress("unused") +val enableCustomTabsPatch = bytecodePatch( + name = "Enable CustomTabs", + description = "Enables CustomTabs to open articles in your default browser.", +) { + compatibleWith("com.google.android.apps.magazines") + + execute { + launchCustomTabFingerprint.method.apply { + val checkIndex = launchCustomTabFingerprint.patternMatch!!.endIndex + 1 + val register = getInstruction(checkIndex).registerA + + replaceInstruction(checkIndex, "const/4 v$register, 0x1") + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/googlenews/customtabs/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/googlenews/customtabs/Fingerprints.kt new file mode 100644 index 000000000..8880c010e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/googlenews/customtabs/Fingerprints.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.googlenews.customtabs + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val launchCustomTabFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + opcodes( + Opcode.IPUT_OBJECT, + Opcode.CONST_4, + Opcode.IPUT, + Opcode.CONST_4, + Opcode.IPUT_BOOLEAN, + ) + custom { _, classDef -> classDef.endsWith("CustomTabsArticleLauncher;") } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/googlenews/misc/extension/ExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/googlenews/misc/extension/ExtensionPatch.kt new file mode 100644 index 000000000..281872662 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/googlenews/misc/extension/ExtensionPatch.kt @@ -0,0 +1,6 @@ +package app.revanced.patches.googlenews.misc.extension + +import app.revanced.patches.googlenews.misc.extension.hooks.startActivityInitHook +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch + +val extensionPatch = sharedExtensionPatch(startActivityInitHook) diff --git a/src/main/kotlin/app/revanced/patches/googlenews/misc/integrations/fingerprints/StartActivityInitFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/googlenews/misc/extension/hooks/StartActivityInitHook.kt similarity index 68% rename from src/main/kotlin/app/revanced/patches/googlenews/misc/integrations/fingerprints/StartActivityInitFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/googlenews/misc/extension/hooks/StartActivityInitHook.kt index b71636793..4e9e5c14c 100644 --- a/src/main/kotlin/app/revanced/patches/googlenews/misc/integrations/fingerprints/StartActivityInitFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/googlenews/misc/extension/hooks/StartActivityInitHook.kt @@ -1,26 +1,15 @@ -package app.revanced.patches.googlenews.misc.integrations.fingerprints +package app.revanced.patches.googlenews.misc.extension.hooks -import app.revanced.patches.googlenews.misc.integrations.fingerprints.StartActivityInitFingerprint.getApplicationContextIndex -import app.revanced.patches.shared.misc.integrations.BaseIntegrationsPatch.IntegrationsFingerprint +import app.revanced.patches.shared.misc.extension.extensionHook 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 -internal object StartActivityInitFingerprint : IntegrationsFingerprint( - opcodes = listOf( - Opcode.INVOKE_STATIC, - Opcode.MOVE_RESULT, - Opcode.CONST_4, - Opcode.IF_EQZ, - Opcode.CONST, - Opcode.INVOKE_VIRTUAL, - Opcode.IPUT_OBJECT, - Opcode.IPUT_BOOLEAN, - Opcode.INVOKE_VIRTUAL, // Calls startActivity.getApplicationContext(). - Opcode.MOVE_RESULT_OBJECT, - ), +private var getApplicationContextIndex = -1 + +internal val startActivityInitHook = extensionHook( insertIndexResolver = { method -> getApplicationContextIndex = method.indexOfFirstInstructionOrThrow { getReference()?.name == "getApplicationContext" @@ -31,11 +20,22 @@ internal object StartActivityInitFingerprint : IntegrationsFingerprint( contextRegisterResolver = { method -> val moveResultInstruction = method.implementation!!.instructions.elementAt(getApplicationContextIndex + 1) as OneRegisterInstruction - moveResultInstruction.registerA - }, - customFingerprint = { methodDef, classDef -> - methodDef.name == "onCreate" && classDef.endsWith("/StartActivity;") + "v${moveResultInstruction.registerA}" }, ) { - private var getApplicationContextIndex = -1 + opcodes( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.CONST_4, + Opcode.IF_EQZ, + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.IPUT_OBJECT, + Opcode.IPUT_BOOLEAN, + Opcode.INVOKE_VIRTUAL, // Calls startActivity.getApplicationContext(). + Opcode.MOVE_RESULT_OBJECT, + ) + custom { methodDef, classDef -> + methodDef.name == "onCreate" && classDef.endsWith("/StartActivity;") + } } diff --git a/src/main/kotlin/app/revanced/patches/googlenews/misc/gms/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/googlenews/misc/gms/Constants.kt similarity index 100% rename from src/main/kotlin/app/revanced/patches/googlenews/misc/gms/Constants.kt rename to patches/src/main/kotlin/app/revanced/patches/googlenews/misc/gms/Constants.kt diff --git a/patches/src/main/kotlin/app/revanced/patches/googlenews/misc/gms/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/googlenews/misc/gms/Fingerprints.kt new file mode 100644 index 000000000..6ddeb3e07 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/googlenews/misc/gms/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.googlenews.misc.gms + +import app.revanced.patcher.fingerprint + +internal val magazinesActivityOnCreateFingerprint = fingerprint { + custom { methodDef, classDef -> + methodDef.name == "onCreate" && classDef.endsWith("/StartActivity;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/googlenews/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/googlenews/misc/gms/GmsCoreSupportPatch.kt new file mode 100644 index 000000000..d03ce31e7 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/googlenews/misc/gms/GmsCoreSupportPatch.kt @@ -0,0 +1,30 @@ +package app.revanced.patches.googlenews.misc.gms + +import app.revanced.patcher.patch.Option +import app.revanced.patches.googlenews.misc.extension.extensionPatch +import app.revanced.patches.googlenews.misc.gms.Constants.MAGAZINES_PACKAGE_NAME +import app.revanced.patches.googlenews.misc.gms.Constants.REVANCED_MAGAZINES_PACKAGE_NAME +import app.revanced.patches.shared.misc.gms.gmsCoreSupportPatch +import app.revanced.patches.shared.misc.gms.gmsCoreSupportResourcePatch + +@Suppress("unused") +val gmsCoreSupportPatch = gmsCoreSupportPatch( + fromPackageName = MAGAZINES_PACKAGE_NAME, + toPackageName = REVANCED_MAGAZINES_PACKAGE_NAME, + mainActivityOnCreateFingerprint = magazinesActivityOnCreateFingerprint, + extensionPatch = extensionPatch, + gmsCoreSupportResourcePatchFactory = ::gmsCoreSupportResourcePatch, +) { + // Remove version constraint, + // once https://github.com/ReVanced/revanced-patches/pull/3111#issuecomment-2240877277 is resolved. + compatibleWith(MAGAZINES_PACKAGE_NAME("5.108.0.644447823")) +} + +private fun gmsCoreSupportResourcePatch( + gmsCoreVendorGroupIdOption: Option, +) = gmsCoreSupportResourcePatch( + fromPackageName = MAGAZINES_PACKAGE_NAME, + toPackageName = REVANCED_MAGAZINES_PACKAGE_NAME, + spoofedPackageSignature = "24bb24c05e47e0aefa68a58a766179d9b613a666", + gmsCoreVendorGroupIdOption = gmsCoreVendorGroupIdOption, +) diff --git a/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/extension/ExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/extension/ExtensionPatch.kt new file mode 100644 index 000000000..406d28fe4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/extension/ExtensionPatch.kt @@ -0,0 +1,5 @@ +package app.revanced.patches.googlephotos.misc.extension + +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch + +val extensionPatch = sharedExtensionPatch(homeActivityInitHook) diff --git a/src/main/kotlin/app/revanced/patches/googlephotos/misc/integrations/fingerprints/HomeActivityInitFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/extension/Fingerprints.kt similarity index 66% rename from src/main/kotlin/app/revanced/patches/googlephotos/misc/integrations/fingerprints/HomeActivityInitFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/extension/Fingerprints.kt index 1fb7bf440..ca1065faa 100644 --- a/src/main/kotlin/app/revanced/patches/googlephotos/misc/integrations/fingerprints/HomeActivityInitFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/extension/Fingerprints.kt @@ -1,22 +1,15 @@ -package app.revanced.patches.googlephotos.misc.integrations.fingerprints +package app.revanced.patches.googlephotos.misc.extension -import app.revanced.patches.googlephotos.misc.integrations.fingerprints.HomeActivityInitFingerprint.getApplicationContextIndex -import app.revanced.patches.shared.misc.integrations.BaseIntegrationsPatch.IntegrationsFingerprint +import app.revanced.patches.shared.misc.extension.extensionHook 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 -internal object HomeActivityInitFingerprint : IntegrationsFingerprint( - opcodes = listOf( - Opcode.CONST_STRING, - Opcode.INVOKE_STATIC, - Opcode.MOVE_RESULT_OBJECT, - Opcode.IF_NEZ, - Opcode.INVOKE_VIRTUAL, // Calls getApplicationContext(). - Opcode.MOVE_RESULT_OBJECT, - ), +private var getApplicationContextIndex = -1 + +internal val homeActivityInitHook = extensionHook( insertIndexResolver = { method -> getApplicationContextIndex = method.indexOfFirstInstructionOrThrow { getReference()?.name == "getApplicationContext" @@ -27,11 +20,18 @@ internal object HomeActivityInitFingerprint : IntegrationsFingerprint( contextRegisterResolver = { method -> val moveResultInstruction = method.implementation!!.instructions.elementAt(getApplicationContextIndex + 1) as OneRegisterInstruction - moveResultInstruction.registerA - }, - customFingerprint = { methodDef, classDef -> - methodDef.name == "onCreate" && classDef.endsWith("/HomeActivity;") + "v${moveResultInstruction.registerA}" }, ) { - private var getApplicationContextIndex = -1 + opcodes( + Opcode.CONST_STRING, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IF_NEZ, + Opcode.INVOKE_VIRTUAL, // Calls getApplicationContext(). + Opcode.MOVE_RESULT_OBJECT, + ) + custom { methodDef, classDef -> + methodDef.name == "onCreate" && classDef.endsWith("/HomeActivity;") + } } diff --git a/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/features/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/features/Fingerprints.kt new file mode 100644 index 000000000..95f2a3dba --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/features/Fingerprints.kt @@ -0,0 +1,7 @@ +package app.revanced.patches.googlephotos.misc.features + +import app.revanced.patcher.fingerprint + +internal val initializeFeaturesEnumFingerprint = fingerprint { + strings("com.google.android.apps.photos.NEXUS_PRELOAD") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/features/SpoofBuildInfoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/features/SpoofBuildInfoPatch.kt new file mode 100644 index 000000000..eeadc462a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/features/SpoofBuildInfoPatch.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.googlephotos.misc.features + +import app.revanced.patches.all.misc.build.BuildInfo +import app.revanced.patches.all.misc.build.baseSpoofBuildInfoPatch + +// Spoof build info to Google Pixel XL. +val spoofBuildInfoPatch = baseSpoofBuildInfoPatch { + BuildInfo( + brand = "google", + manufacturer = "Google", + device = "marlin", + product = "marlin", + model = "Pixel XL", + fingerprint = "google/marlin/marlin:10/QP1A.191005.007.A3/5972272:user/release-keys", + ) +} diff --git a/src/main/kotlin/app/revanced/patches/googlephotos/features/SpoofFeaturesPatch.kt b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/features/SpoofFeaturesPatch.kt similarity index 53% rename from src/main/kotlin/app/revanced/patches/googlephotos/features/SpoofFeaturesPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/features/SpoofFeaturesPatch.kt index 040012d45..4c0be2b77 100644 --- a/src/main/kotlin/app/revanced/patches/googlephotos/features/SpoofFeaturesPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/features/SpoofFeaturesPatch.kt @@ -1,30 +1,26 @@ -package app.revanced.patches.googlephotos.features +package app.revanced.patches.googlephotos.misc.features -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.InstructionExtensions.getInstructions +import app.revanced.patcher.extensions.InstructionExtensions.instructions import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -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.options.PatchOption.PatchExtensions.stringArrayPatchOption -import app.revanced.patches.googlephotos.features.fingerprints.InitializeFeaturesEnumFingerprint +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.stringsOption import app.revanced.util.getReference -import app.revanced.util.resultOrThrow import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.StringReference -@Patch( +@Suppress("unused") +val spoofFeaturesPatch = bytecodePatch( name = "Spoof features", description = "Spoofs the device to enable Google Pixel exclusive features, including unlimited storage.", - dependencies = [SpoofBuildInfoPatch::class], - compatiblePackages = [CompatiblePackage("com.google.android.apps.photos")], -) -@Suppress("unused") -object SpoofFeaturesPatch : BytecodePatch(setOf(InitializeFeaturesEnumFingerprint)) { - private val featuresToEnable by stringArrayPatchOption( - "featuresToEnable", - arrayOf( +) { + compatibleWith("com.google.android.apps.photos") + + dependsOn(spoofBuildInfoPatch) + + val featuresToEnable by stringsOption( + key = "featuresToEnable", + default = listOf( "com.google.android.apps.photos.NEXUS_PRELOAD", "com.google.android.apps.photos.nexus_preload", ), @@ -33,9 +29,9 @@ object SpoofFeaturesPatch : BytecodePatch(setOf(InitializeFeaturesEnumFingerprin required = true, ) - private val featuresToDisable by stringArrayPatchOption( - "featuresToDisable", - arrayOf( + val featuresToDisable by stringsOption( + key = "featuresToDisable", + default = listOf( "com.google.android.apps.photos.PIXEL_2017_PRELOAD", "com.google.android.apps.photos.PIXEL_2018_PRELOAD", "com.google.android.apps.photos.PIXEL_2019_MIDYEAR_PRELOAD", @@ -58,29 +54,30 @@ object SpoofFeaturesPatch : BytecodePatch(setOf(InitializeFeaturesEnumFingerprin required = true, ) - override fun execute(context: BytecodeContext) { + execute { + @Suppress("NAME_SHADOWING") val featuresToEnable = featuresToEnable!!.toSet() + + @Suppress("NAME_SHADOWING") val featuresToDisable = featuresToDisable!!.toSet() - InitializeFeaturesEnumFingerprint.resultOrThrow().let { result -> - result.mutableMethod.apply { - getInstructions().filter { it.opcode == Opcode.CONST_STRING }.forEach { - val feature = it.getReference()!!.string + initializeFeaturesEnumFingerprint.method.apply { + instructions.filter { it.opcode == Opcode.CONST_STRING }.forEach { + val feature = it.getReference()!!.string - val spoofedFeature = when (feature) { - in featuresToEnable -> "android.hardware.wifi" - in featuresToDisable -> "dummy" - else -> return@forEach - } - - val constStringIndex = it.location.index - val constStringRegister = (it as OneRegisterInstruction).registerA - - replaceInstruction( - constStringIndex, - "const-string v$constStringRegister, \"$spoofedFeature\"", - ) + val spoofedFeature = when (feature) { + in featuresToEnable -> "android.hardware.wifi" + in featuresToDisable -> "dummy" + else -> return@forEach } + + val constStringIndex = it.location.index + val constStringRegister = (it as OneRegisterInstruction).registerA + + replaceInstruction( + constStringIndex, + "const-string v$constStringRegister, \"$spoofedFeature\"", + ) } } } diff --git a/src/main/kotlin/app/revanced/patches/googlephotos/misc/gms/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/gms/Constants.kt similarity index 100% rename from src/main/kotlin/app/revanced/patches/googlephotos/misc/gms/Constants.kt rename to patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/gms/Constants.kt diff --git a/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/gms/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/gms/Fingerprints.kt new file mode 100644 index 000000000..f47c1a3d9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/gms/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.googlephotos.misc.gms + +import app.revanced.patcher.fingerprint + +internal val homeActivityOnCreateFingerprint = fingerprint { + custom { methodDef, classDef -> + methodDef.name == "onCreate" && classDef.endsWith("/HomeActivity;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/gms/GmsCoreSupportPatch.kt new file mode 100644 index 000000000..3ed14a29d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/gms/GmsCoreSupportPatch.kt @@ -0,0 +1,27 @@ +package app.revanced.patches.googlephotos.misc.gms + +import app.revanced.patcher.patch.Option +import app.revanced.patches.googlephotos.misc.extension.extensionPatch +import app.revanced.patches.googlephotos.misc.gms.Constants.PHOTOS_PACKAGE_NAME +import app.revanced.patches.googlephotos.misc.gms.Constants.REVANCED_PHOTOS_PACKAGE_NAME +import app.revanced.patches.shared.misc.gms.gmsCoreSupportPatch + +@Suppress("unused") +val gmsCoreSupportPatch = gmsCoreSupportPatch( + fromPackageName = PHOTOS_PACKAGE_NAME, + toPackageName = REVANCED_PHOTOS_PACKAGE_NAME, + mainActivityOnCreateFingerprint = homeActivityOnCreateFingerprint, + extensionPatch = extensionPatch, + gmsCoreSupportResourcePatchFactory = ::gmsCoreSupportResourcePatch, +) { + compatibleWith(PHOTOS_PACKAGE_NAME) +} + +private fun gmsCoreSupportResourcePatch( + gmsCoreVendorGroupIdOption: Option, +) = app.revanced.patches.shared.misc.gms.gmsCoreSupportResourcePatch( + fromPackageName = PHOTOS_PACKAGE_NAME, + toPackageName = REVANCED_PHOTOS_PACKAGE_NAME, + spoofedPackageSignature = "24bb24c05e47e0aefa68a58a766179d9b613a600", + gmsCoreVendorGroupIdOption = gmsCoreVendorGroupIdOption, +) diff --git a/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/preferences/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/preferences/Fingerprints.kt new file mode 100644 index 000000000..54c20a7f8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/preferences/Fingerprints.kt @@ -0,0 +1,8 @@ +package app.revanced.patches.googlephotos.misc.preferences + +import app.revanced.patcher.fingerprint + +internal val backupPreferencesFingerprint = fingerprint { + returns("Lcom/google/android/apps/photos/backup/data/BackupPreferences;") + strings("backup_prefs_had_backup_only_when_charging_enabled") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/preferences/RestoreHiddenBackUpWhileChargingTogglePatch.kt b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/preferences/RestoreHiddenBackUpWhileChargingTogglePatch.kt new file mode 100644 index 000000000..1d8717115 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/googlephotos/misc/preferences/RestoreHiddenBackUpWhileChargingTogglePatch.kt @@ -0,0 +1,26 @@ +package app.revanced.patches.googlephotos.misc.preferences + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.sun.org.apache.bcel.internal.generic.InstructionConst.getInstruction + +@Suppress("unused") +val restoreHiddenBackUpWhileChargingTogglePatch = bytecodePatch( + name = "Restore hidden 'Back up while charging' toggle", + description = "Restores a hidden toggle to only run backups when the device is charging.", +) { + compatibleWith("com.google.android.apps.photos") + + execute { + // Patches 'backup_prefs_had_backup_only_when_charging_enabled' to always be true. + val chargingPrefStringIndex = backupPreferencesFingerprint.stringMatches!!.first().index + backupPreferencesFingerprint.method.apply { + // Get the register of move-result. + val resultRegister = getInstruction(chargingPrefStringIndex + 2).registerA + // Insert const after move-result to override register as true. + addInstruction(chargingPrefStringIndex + 3, "const/4 v$resultRegister, 0x1") + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/googlerecorder/restrictions/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/googlerecorder/restrictions/Fingerprints.kt new file mode 100644 index 000000000..62e1e5f16 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/googlerecorder/restrictions/Fingerprints.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.googlerecorder.restrictions + +import app.revanced.patcher.fingerprint + +internal val onApplicationCreateFingerprint = fingerprint { + strings("com.google.android.feature.PIXEL_2017_EXPERIENCE") + custom { method, classDef -> + if (method.name != "onCreate") return@custom false + + classDef.endsWith("RecorderApplication;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/googlerecorder/restrictions/RemoveDeviceRestrictions.kt b/patches/src/main/kotlin/app/revanced/patches/googlerecorder/restrictions/RemoveDeviceRestrictions.kt new file mode 100644 index 000000000..2cf32adef --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/googlerecorder/restrictions/RemoveDeviceRestrictions.kt @@ -0,0 +1,30 @@ +package app.revanced.patches.googlerecorder.restrictions + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.sun.org.apache.bcel.internal.generic.InstructionConst.getInstruction + +@Suppress("unused") +val removeDeviceRestrictionsPatch = bytecodePatch( + name = "Remove device restrictions", + description = "Removes restrictions from using the app on any device. Requires mounting patched app over original.", +) { + compatibleWith("com.google.android.apps.recorder") + + execute { + val featureStringIndex = onApplicationCreateFingerprint.stringMatches!!.first().index + + onApplicationCreateFingerprint.method.apply { + // Remove check for device restrictions. + removeInstructions(featureStringIndex - 2, 5) + + val featureAvailableRegister = getInstruction(featureStringIndex).registerA + + // Override "isPixelDevice()" to return true. + addInstruction(featureStringIndex, "const/4 v$featureAvailableRegister, 0x1") + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/hexeditor/ad/DisableAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/hexeditor/ad/DisableAdsPatch.kt new file mode 100644 index 000000000..1132a5ad9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/hexeditor/ad/DisableAdsPatch.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.hexeditor.ad + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val disableAdsPatch = bytecodePatch( + name = "Disable ads", +) { + compatibleWith("com.myprog.hexedit") + + execute { + primaryAdsFingerprint.method.replaceInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/hexeditor/ad/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/hexeditor/ad/Fingerprints.kt new file mode 100644 index 000000000..2fa2c5b85 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/hexeditor/ad/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.hexeditor.ad + +import app.revanced.patcher.fingerprint + +internal val primaryAdsFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("PreferencesHelper;") && method.name == "isAdsDisabled" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/iconpackstudio/misc/pro/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/iconpackstudio/misc/pro/Fingerprints.kt new file mode 100644 index 000000000..84db55457 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/iconpackstudio/misc/pro/Fingerprints.kt @@ -0,0 +1,8 @@ +package app.revanced.patches.iconpackstudio.misc.pro + +import app.revanced.patcher.fingerprint + +internal val checkProFingerprint = fingerprint { + returns("Z") + custom { _, classDef -> classDef.endsWith("IPSPurchaseRepository;") } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/iconpackstudio/misc/pro/UnlockProPatch.kt b/patches/src/main/kotlin/app/revanced/patches/iconpackstudio/misc/pro/UnlockProPatch.kt new file mode 100644 index 000000000..c1e4719b7 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/iconpackstudio/misc/pro/UnlockProPatch.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.iconpackstudio.misc.pro + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val unlockProPatch = bytecodePatch( + name = "Unlock pro", +) { + compatibleWith("ginlemon.iconpackstudio"("2.2 build 016")) + + execute { + checkProFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/idaustria/detection/root/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/idaustria/detection/root/Fingerprints.kt new file mode 100644 index 000000000..38f814f6d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/idaustria/detection/root/Fingerprints.kt @@ -0,0 +1,31 @@ +package app.revanced.patches.idaustria.detection.root + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val attestationSupportedCheckFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("V") + custom { method, classDef -> + method.name == "attestationSupportCheck" && + classDef.endsWith("/DeviceIntegrityCheck;") + } +} + +internal val bootloaderCheckFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("Z") + custom { method, classDef -> + method.name == "bootloaderCheck" && + classDef.endsWith("/DeviceIntegrityCheck;") + } +} + +internal val rootCheckFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("V") + custom { method, classDef -> + method.name == "rootCheck" && + classDef.endsWith("/DeviceIntegrityCheck;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/idaustria/detection/root/RootDetectionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/idaustria/detection/root/RootDetectionPatch.kt new file mode 100644 index 000000000..6c79650d9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/idaustria/detection/root/RootDetectionPatch.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.idaustria.detection.root + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.returnEarly + +@Suppress("unused") +val rootDetectionPatch = bytecodePatch( + name = "Remove root detection", + description = "Removes the check for root permissions and unlocked bootloader.", +) { + compatibleWith("at.gv.oe.app") + + execute { + setOf( + attestationSupportedCheckFingerprint, + bootloaderCheckFingerprint, + rootCheckFingerprint, + ).forEach { it.method.returnEarly(true) } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/idaustria/detection/signature/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/idaustria/detection/signature/Fingerprints.kt new file mode 100644 index 000000000..61cd9605f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/idaustria/detection/signature/Fingerprints.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.idaustria.detection.signature + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val spoofSignatureFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE) + returns("L") + parameters("L") + custom { method, classDef -> + classDef.endsWith("/SL2Step1Task;") && method.name == "getPubKey" + } +} diff --git a/src/main/kotlin/app/revanced/patches/idaustria/detection/signature/SpoofSignaturePatch.kt b/patches/src/main/kotlin/app/revanced/patches/idaustria/detection/signature/SpoofSignaturePatch.kt similarity index 65% rename from src/main/kotlin/app/revanced/patches/idaustria/detection/signature/SpoofSignaturePatch.kt rename to patches/src/main/kotlin/app/revanced/patches/idaustria/detection/signature/SpoofSignaturePatch.kt index ae427b744..e6b312792 100644 --- a/src/main/kotlin/app/revanced/patches/idaustria/detection/signature/SpoofSignaturePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/idaustria/detection/signature/SpoofSignaturePatch.kt @@ -1,23 +1,18 @@ package app.revanced.patches.idaustria.detection.signature -import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.InstructionExtensions.addInstructions -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.annotation.CompatiblePackage -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patches.idaustria.detection.signature.fingerprints.SpoofSignatureFingerprint +import app.revanced.patcher.patch.bytecodePatch -@Patch( +@Suppress("unused") +val spoofSignaturePatch = bytecodePatch( name = "Spoof signature", description = "Spoofs the signature of the app.", - compatiblePackages = [CompatiblePackage("at.gv.oe.app")] -) -@Suppress("unused") -object SpoofSignaturePatch : BytecodePatch( - setOf(SpoofSignatureFingerprint) ) { - private const val EXPECTED_SIGNATURE = - "OpenSSLRSAPublicKey{modulus=ac3e6fd6050aa7e0d6010ae58190404cd89a56935b44f6fee" + + compatibleWith("at.gv.oe.app") + + execute { + val expectedSignature = + "OpenSSLRSAPublicKey{modulus=ac3e6fd6050aa7e0d6010ae58190404cd89a56935b44f6fee" + "067c149768320026e10b24799a1339e414605e448e3f264444a327b9ae292be2b62ad567dd1800dbed4a88f718a33dc6db6b" + "f5178aa41aa0efff8a3409f5ca95dbfccd92c7b4298966df806ea7a0204a00f0e745f6d9f13bdf24f3df715d7b62c1600906" + "15de1c8a956b9286764985a3b3c060963c435fb9481a5543aaf0671fc2dba6c5c2b17d1ef1d85137f14dc9bbdf3490288087" + @@ -29,13 +24,12 @@ object SpoofSignaturePatch : BytecodePatch( "77ef1be61b2c01ebdabddcbf53cc4b6fd9a3c445606ee77b3758162c80ad8f8137b3c6864e92db904807dcb2be9d7717dd21" + "bf42c121d620ddfb7914f7a95c713d9e1c1b7bdb4a03d618e40cf7e9e235c0b5687e03b7ab3,publicExponent=10001}" - override fun execute(context: BytecodeContext) { - SpoofSignatureFingerprint.result!!.mutableMethod.addInstructions( + spoofSignatureFingerprint.method.addInstructions( 0, """ - const-string v0, "$EXPECTED_SIGNATURE" + const-string v0, "$expectedSignature" return-object v0 - """ + """, ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/inshorts/ad/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/inshorts/ad/Fingerprints.kt new file mode 100644 index 000000000..573bd72e3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/inshorts/ad/Fingerprints.kt @@ -0,0 +1,8 @@ +package app.revanced.patches.inshorts.ad + +import app.revanced.patcher.fingerprint + +internal val inshortsAdsFingerprint = fingerprint { + returns("V") + strings("GoogleAdLoader", "exception in requestAd") +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/inshorts/ad/InshortsAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/inshorts/ad/InshortsAdsPatch.kt new file mode 100644 index 000000000..87fddcb78 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/inshorts/ad/InshortsAdsPatch.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.inshorts.ad + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val hideAdsPatch = bytecodePatch( + name = "Hide ads", +) { + compatibleWith("com.nis.app") + + execute { + inshortsAdsFingerprint.method.addInstruction( + 0, + """ + return-void + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/ads/Fingerprints.kt new file mode 100644 index 000000000..65d052729 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/instagram/ads/Fingerprints.kt @@ -0,0 +1,14 @@ +package app.revanced.patches.instagram.ads + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val adInjectorFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE) + returns("Z") + parameters("L", "L") + strings( + "SponsoredContentController.insertItem", + "SponsoredContentController::Delivery", + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/instagram/ads/HideAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/instagram/ads/HideAdsPatch.kt new file mode 100644 index 000000000..29aeccc1b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/instagram/ads/HideAdsPatch.kt @@ -0,0 +1,23 @@ +package app.revanced.patches.instagram.ads + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val hideAdsPatch = bytecodePatch( + name = "Hide ads", + description = "Hides ads in stories, discover, profile, etc. " + + "An ad can still appear once when refreshing the home feed.", +) { + compatibleWith("com.instagram.android") + + execute { + adInjectorFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x0 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/irplus/ad/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/irplus/ad/Fingerprints.kt new file mode 100644 index 000000000..30242b8d9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/irplus/ad/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.irplus.ad + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val irplusAdsFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + parameters("L", "Z") + strings("TAGGED") +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/irplus/ad/RemoveAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/irplus/ad/RemoveAdsPatch.kt new file mode 100644 index 000000000..44b76f5fc --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/irplus/ad/RemoveAdsPatch.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.irplus.ad + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val removeAdsPatch = bytecodePatch( + name = "Remove ads", +) { + compatibleWith("net.binarymode.android.irplus") + + execute { + // By overwriting the second parameter of the method, + // the view which holds the advertisement is removed. + irplusAdsFingerprint.method.addInstruction(0, "const/4 p2, 0x0") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch.kt b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch.kt new file mode 100644 index 000000000..a5a1cf8f3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/login/DisableMandatoryLoginPatch.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.lightroom.misc.login + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val disableMandatoryLoginPatch = bytecodePatch( + name = "Disable mandatory login", +) { + compatibleWith("com.adobe.lrmobile") + + execute { + isLoggedInFingerprint.method.apply { + val index = implementation!!.instructions.lastIndex - 1 + // Set isLoggedIn = true. + replaceInstruction(index, "const/4 v0, 0x1") + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/login/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/login/Fingerprints.kt new file mode 100644 index 000000000..6345541e1 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/login/Fingerprints.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.lightroom.misc.login + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val isLoggedInFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL) + returns("Z") + opcodes( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.SGET_OBJECT, + Opcode.IF_NE, + Opcode.CONST_4, + Opcode.GOTO + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/premium/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/premium/Fingerprints.kt new file mode 100644 index 000000000..5a00dc68c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/premium/Fingerprints.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.lightroom.misc.premium + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val hasPurchasedFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returns("Z") + opcodes( + Opcode.SGET_OBJECT, + Opcode.CONST_4, + Opcode.CONST_4, + Opcode.CONST_4, + ) + strings("isPurchaseDoneRecently = true, access platform profile present? = ") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch.kt new file mode 100644 index 000000000..b9187af27 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/lightroom/misc/premium/UnlockPremiumPatch.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.lightroom.misc.premium + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val unlockPremiumPatch = bytecodePatch( + name = "Unlock premium", +) { + compatibleWith("com.adobe.lrmobile") + + execute { + // Set hasPremium = true. + hasPurchasedFingerprint.method.replaceInstruction(2, "const/4 v2, 0x1") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/memegenerator/detection/license/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/memegenerator/detection/license/Fingerprints.kt new file mode 100644 index 000000000..0dfbf5cda --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/memegenerator/detection/license/Fingerprints.kt @@ -0,0 +1,23 @@ +package app.revanced.patches.memegenerator.detection.license + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val licenseValidationFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Z") + parameters("Landroid/content/Context;") + opcodes( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_WIDE, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_WIDE, + Opcode.CMP_LONG, + Opcode.IF_GEZ, + Opcode.CONST_4, + Opcode.RETURN, + Opcode.CONST_4, + Opcode.RETURN + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/memegenerator/detection/license/LicenseValidationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/memegenerator/detection/license/LicenseValidationPatch.kt new file mode 100644 index 000000000..8b4a41734 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/memegenerator/detection/license/LicenseValidationPatch.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.memegenerator.detection.license + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions +import app.revanced.patcher.patch.bytecodePatch + +val licenseValidationPatch = bytecodePatch( + description = "Disables Firebase license validation.", +) { + + execute { + licenseValidationFingerprint.method.replaceInstructions( + 0, + """ + const/4 p0, 0x1 + return p0 + """, + ) + } +} diff --git a/src/main/kotlin/app/revanced/patches/memegenerator/detection/signature/fingerprints/VerifySignatureFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/memegenerator/detection/signature/Fingerprints.kt similarity index 54% rename from src/main/kotlin/app/revanced/patches/memegenerator/detection/signature/fingerprints/VerifySignatureFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/memegenerator/detection/signature/Fingerprints.kt index b77fd051f..75912318b 100644 --- a/src/main/kotlin/app/revanced/patches/memegenerator/detection/signature/fingerprints/VerifySignatureFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/memegenerator/detection/signature/Fingerprints.kt @@ -1,17 +1,14 @@ -package app.revanced.patches.memegenerator.detection.signature.fingerprints +package app.revanced.patches.memegenerator.detection.signature -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.annotation.FuzzyPatternScanMethod -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint -@FuzzyPatternScanMethod(2) -internal object VerifySignatureFingerprint : MethodFingerprint( - returnType = "Z", - accessFlags = AccessFlags.PUBLIC or AccessFlags.STATIC, - parameters = listOf("Landroid/app/Activity;"), - opcodes = listOf( +internal val verifySignatureFingerprint = fingerprint(fuzzyPatternScanThreshold = 2) { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Z") + parameters("Landroid/app/Activity;") + opcodes( Opcode.SGET_OBJECT, Opcode.IF_NEZ, Opcode.INVOKE_STATIC, @@ -31,5 +28,5 @@ internal object VerifySignatureFingerprint : MethodFingerprint( Opcode.CONST_4, Opcode.RETURN, Opcode.ADD_INT_LIT8 - ), -) \ No newline at end of file + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/memegenerator/detection/signature/SignatureVerificationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/memegenerator/detection/signature/SignatureVerificationPatch.kt new file mode 100644 index 000000000..c6b05ae8d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/memegenerator/detection/signature/SignatureVerificationPatch.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.memegenerator.detection.signature + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions +import app.revanced.patcher.patch.bytecodePatch + +val signatureVerificationPatch = bytecodePatch( + description = "Disables detection of incorrect signature.", +) { + + execute { + verifySignatureFingerprint.method.replaceInstructions( + 0, + """ + const/4 p0, 0x1 + return p0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/memegenerator/misc/pro/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/memegenerator/misc/pro/Fingerprints.kt new file mode 100644 index 000000000..1f16bb10e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/memegenerator/misc/pro/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.memegenerator.misc.pro + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val isFreeVersionFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Ljava/lang/Boolean;") + parameters("Landroid/content/Context;") + opcodes( + Opcode.SGET, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CONST_STRING, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ + ) + strings("free") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/memegenerator/misc/pro/UnlockProVersionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/memegenerator/misc/pro/UnlockProVersionPatch.kt new file mode 100644 index 000000000..b74e2d3a8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/memegenerator/misc/pro/UnlockProVersionPatch.kt @@ -0,0 +1,25 @@ +package app.revanced.patches.memegenerator.misc.pro + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.memegenerator.detection.license.licenseValidationPatch +import app.revanced.patches.memegenerator.detection.signature.signatureVerificationPatch + +@Suppress("unused") +val unlockProVersionPatch = bytecodePatch( + name = "Unlock pro", +) { + dependsOn(signatureVerificationPatch, licenseValidationPatch) + + compatibleWith("com.zombodroid.MemeGenerator"("4.6364", "4.6370", "4.6375", "4.6377")) + + execute { + isFreeVersionFingerprint.method.replaceInstructions( + 0, + """ + sget-object p0, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean; + return-object p0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/messenger/inbox/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/messenger/inbox/Fingerprints.kt new file mode 100644 index 000000000..a2fa6329c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/messenger/inbox/Fingerprints.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.messenger.inbox + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.value.StringEncodedValue + +internal val createInboxSubTabsFingerprint = fingerprint { + returns("V") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + opcodes( + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID, + ) + custom { method, classDef -> + method.name == "run" && + classDef.fields.any any@{ field -> + if (field.name != "__redex_internal_original_name") return@any false + (field.initialValue as? StringEncodedValue)?.value == "InboxSubtabsItemSupplierImplementation\$onSubscribe\$1" + } + } +} + +internal val loadInboxAdsFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("V") + strings( + "ads_load_begin", + "inbox_ads_fetch_start", + ) + custom { method, _ -> + method.definingClass == "Lcom/facebook/messaging/business/inboxads/plugins/inboxads/itemsupplier/" + + "InboxAdsItemSupplierImplementation;" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/messenger/inbox/HideInboxAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/messenger/inbox/HideInboxAdsPatch.kt new file mode 100644 index 000000000..090ffd9f8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/messenger/inbox/HideInboxAdsPatch.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.messenger.inbox + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val hideInboxAdsPatch = bytecodePatch( + name = "Hide inbox ads", + description = "Hides ads in inbox.", +) { + compatibleWith("com.facebook.orca") + + execute { + loadInboxAdsFingerprint.method.replaceInstruction(0, "return-void") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/messenger/inbox/HideInboxSubtabsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/messenger/inbox/HideInboxSubtabsPatch.kt new file mode 100644 index 000000000..2d190615f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/messenger/inbox/HideInboxSubtabsPatch.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.messenger.inbox + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val hideInboxSubtabsPatch = bytecodePatch( + name = "Hide inbox subtabs", + description = "Hides Home and Channels tabs between active now tray and chats.", +) { + compatibleWith("com.facebook.orca") + + execute { + createInboxSubTabsFingerprint.method.replaceInstruction(2, "const/4 v0, 0x0") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/messenger/inputfield/DisableSwitchingEmojiToStickerPatch.kt b/patches/src/main/kotlin/app/revanced/patches/messenger/inputfield/DisableSwitchingEmojiToStickerPatch.kt new file mode 100644 index 000000000..716b40e1d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/messenger/inputfield/DisableSwitchingEmojiToStickerPatch.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.messenger.inputfield + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.sun.org.apache.bcel.internal.generic.InstructionConst.getInstruction + +@Suppress("unused") +val disableSwitchingEmojiToStickerPatch = bytecodePatch( + name = "Disable switching emoji to sticker", + description = "Disables switching from emoji to sticker search mode in message input field.", +) { + compatibleWith("com.facebook.orca"("439.0.0.29.119")) + + execute { + switchMessengeInputEmojiButtonFingerprint.method.apply { + val setStringIndex = switchMessengeInputEmojiButtonFingerprint.patternMatch!!.startIndex + 2 + val targetRegister = getInstruction(setStringIndex).registerA + + replaceInstruction(setStringIndex, "const-string v$targetRegister, \"expression\"") + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/messenger/inputfield/DisableTypingIndicatorPatch.kt b/patches/src/main/kotlin/app/revanced/patches/messenger/inputfield/DisableTypingIndicatorPatch.kt new file mode 100644 index 000000000..0d5bfd58c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/messenger/inputfield/DisableTypingIndicatorPatch.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.messenger.inputfield + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val disableTypingIndicatorPatch = bytecodePatch( + name = "Disable typing indicator", + description = "Disables the indicator while typing a message.", +) { + compatibleWith("com.facebook.orca") + + execute { + sendTypingIndicatorFingerprint.method.replaceInstruction(0, "return-void") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/messenger/inputfield/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/messenger/inputfield/Fingerprints.kt new file mode 100644 index 000000000..75fd54f7d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/messenger/inputfield/Fingerprints.kt @@ -0,0 +1,31 @@ +package app.revanced.patches.messenger.inputfield + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.dexbacked.value.DexBackedStringEncodedValue + +internal val sendTypingIndicatorFingerprint = fingerprint { + returns("V") + parameters() + custom { method, classDef -> + method.name == "run" && + classDef.fields.any { + it.name == "__redex_internal_original_name" && + (it.initialValue as? DexBackedStringEncodedValue)?.value == "ConversationTypingContext\$sendActiveStateRunnable\$1" + } + } +} + +internal val switchMessengeInputEmojiButtonFingerprint = fingerprint { + returns("V") + parameters("L", "Z") + opcodes( + Opcode.IGET_OBJECT, + Opcode.IF_EQZ, + Opcode.CONST_STRING, + Opcode.GOTO, + Opcode.CONST_STRING, + Opcode.GOTO, + ) + strings("afterTextChanged", "expression_search") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/mifitness/misc/locale/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/mifitness/misc/locale/Fingerprints.kt new file mode 100644 index 000000000..14105f762 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/mifitness/misc/locale/Fingerprints.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.mifitness.misc.locale + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val syncBluetoothLanguageFingerprint = fingerprint { + opcodes(Opcode.MOVE_RESULT_OBJECT) + custom { method, _ -> + method.name == "syncBluetoothLanguage" && + method.definingClass == "Lcom/xiaomi/fitness/devicesettings/DeviceSettingsSyncer;" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/mifitness/misc/locale/ForceEnglishLocalePatch.kt b/patches/src/main/kotlin/app/revanced/patches/mifitness/misc/locale/ForceEnglishLocalePatch.kt new file mode 100644 index 000000000..a2a53caba --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/mifitness/misc/locale/ForceEnglishLocalePatch.kt @@ -0,0 +1,29 @@ +package app.revanced.patches.mifitness.misc.locale + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.mifitness.misc.login.fixLoginPatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +@Suppress("unused") +val forceEnglishLocalePatch = bytecodePatch( + name = "Force English locale", + description = "Forces wearable devices to use the English locale.", +) { + compatibleWith("com.xiaomi.wearable") + + dependsOn(fixLoginPatch) + + execute { + syncBluetoothLanguageFingerprint.method.apply { + val resolvePhoneLocaleInstruction = syncBluetoothLanguageFingerprint.patternMatch!!.startIndex + val registerIndexToUpdate = getInstruction(resolvePhoneLocaleInstruction).registerA + + replaceInstruction( + resolvePhoneLocaleInstruction, + "const-string v$registerIndexToUpdate, \"en_gb\"", + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/mifitness/misc/login/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/mifitness/misc/login/Fingerprints.kt new file mode 100644 index 000000000..e3eee2499 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/mifitness/misc/login/Fingerprints.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.mifitness.misc.login + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val xiaomiAccountManagerConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR) + parameters("Landroid/content/Context;", "Z") + custom { method, _ -> + method.definingClass == "Lcom/xiaomi/passport/accountmanager/XiaomiAccountManager;" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/mifitness/misc/login/FixLoginPatch.kt b/patches/src/main/kotlin/app/revanced/patches/mifitness/misc/login/FixLoginPatch.kt new file mode 100644 index 000000000..093a5d4f1 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/mifitness/misc/login/FixLoginPatch.kt @@ -0,0 +1,15 @@ +package app.revanced.patches.mifitness.misc.login + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +val fixLoginPatch = bytecodePatch( + name = "Fix login", + description = "Fixes login for uncertified Mi Fitness app", +) { + compatibleWith("com.xiaomi.wearable") + + execute { + xiaomiAccountManagerConstructorFingerprint.method.addInstruction(0, "const/16 p2, 0x0") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/ad/video/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/ad/video/Fingerprints.kt new file mode 100644 index 000000000..6ce0519ad --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/ad/video/Fingerprints.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.music.ad.video + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val showVideoAdsParentFingerprint = fingerprint { + opcodes( + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.IGET_OBJECT, + ) + strings("maybeRegenerateCpnAndStatsClient called unexpectedly, but no error.") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt b/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt new file mode 100644 index 000000000..27c981256 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/ad/video/HideVideoAds.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.music.ad.video + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val hideVideoAdsPatch = bytecodePatch( + name = "Hide music video ads", + description = "Hides ads that appear while listening to or streaming music videos, podcasts, or songs.", +) { + compatibleWith("com.google.android.apps.youtube.music") + + execute { + navigate(showVideoAdsParentFingerprint.originalMethod) + .to(showVideoAdsParentFingerprint.patternMatch!!.startIndex + 1) + .stop() + .addInstruction(0, "const/4 p1, 0x0") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt b/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt new file mode 100644 index 000000000..466e81aeb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/EnableExclusiveAudioPlayback.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.music.audio.exclusiveaudio + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val enableExclusiveAudioPlaybackPatch = bytecodePatch( + name = "Enable exclusive audio playback", + description = "Enables the option to play audio without video.", +) { + compatibleWith("com.google.android.apps.youtube.music") + + execute { + allowExclusiveAudioPlaybackFingerprint.method.apply { + addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/fingerprints/AllowExclusiveAudioPlaybackFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/Fingerprints.kt similarity index 55% rename from src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/fingerprints/AllowExclusiveAudioPlaybackFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/Fingerprints.kt index c86f79427..02a978f0f 100644 --- a/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/fingerprints/AllowExclusiveAudioPlaybackFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/audio/exclusiveaudio/Fingerprints.kt @@ -1,15 +1,14 @@ -package app.revanced.patches.music.audio.exclusiveaudio.fingerprints +package app.revanced.patches.music.audio.exclusiveaudio -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint -internal object AllowExclusiveAudioPlaybackFingerprint: MethodFingerprint( - "Z", - AccessFlags.PUBLIC or AccessFlags.FINAL, - listOf(), - listOf( +internal val allowExclusiveAudioPlaybackFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters() + opcodes( Opcode.INVOKE_VIRTUAL, Opcode.MOVE_RESULT_OBJECT, Opcode.CHECK_CAST, @@ -22,4 +21,4 @@ internal object AllowExclusiveAudioPlaybackFingerprint: MethodFingerprint( Opcode.MOVE_RESULT, Opcode.RETURN ) -) \ No newline at end of file +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/Fingerprints.kt new file mode 100644 index 000000000..13820d29d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/Fingerprints.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.music.interaction.permanentrepeat + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val repeatTrackFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L", "L") + opcodes( + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.SGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_NEZ + ) + strings("w_st") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt new file mode 100644 index 000000000..c6617664f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentrepeat/PermanentRepeatPatch.kt @@ -0,0 +1,29 @@ +package app.revanced.patches.music.interaction.permanentrepeat + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel + +@Suppress("unused") +val permanentRepeatPatch = bytecodePatch( + name = "Permanent repeat", + description = "Permanently remember your repeating preference even if the playlist ends or another track is played.", + use = false, +) { + compatibleWith("com.google.android.apps.youtube.music") + + execute { + + val startIndex = repeatTrackFingerprint.patternMatch!!.endIndex + val repeatIndex = startIndex + 1 + + repeatTrackFingerprint.method.apply { + addInstructionsWithLabels( + startIndex, + "goto :repeat", + ExternalLabel("repeat", instructions[repeatIndex]), + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentshuffle/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentshuffle/Fingerprints.kt new file mode 100644 index 000000000..f2169744f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentshuffle/Fingerprints.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.music.interaction.permanentshuffle + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val disableShuffleFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters() + opcodes( + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.SGET_OBJECT, + Opcode.IPUT_OBJECT, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentshuffle/PermanentShufflePatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentshuffle/PermanentShufflePatch.kt new file mode 100644 index 000000000..8d044de2c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/interaction/permanentshuffle/PermanentShufflePatch.kt @@ -0,0 +1,26 @@ +package app.revanced.patches.music.interaction.permanentshuffle + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val permanentShufflePatch = bytecodePatch( + name = "Permanent shuffle", + description = "Permanently remember your shuffle preference " + + "even if the playlist ends or another track is played.", + use = false, +) { + compatibleWith( + "com.google.android.apps.youtube.music"( + "6.45.54", + "6.51.53", + "7.01.53", + "7.02.52", + "7.03.52", + ), + ) + + execute { + disableShuffleFingerprint.method.addInstruction(0, "return-void") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/Fingerprints.kt new file mode 100644 index 000000000..d7f0f03de --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/Fingerprints.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.music.layout.compactheader + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val constructCategoryBarFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + parameters("Landroid/content/Context;", "L", "L", "L") + opcodes( + Opcode.IPUT_OBJECT, + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IPUT_OBJECT, + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.NEW_INSTANCE, + Opcode.INVOKE_DIRECT, + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt new file mode 100644 index 000000000..b0021b966 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/compactheader/HideCategoryBar.kt @@ -0,0 +1,30 @@ +package app.revanced.patches.music.layout.compactheader + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +@Suppress("unused") +val hideCategoryBar = bytecodePatch( + name = "Hide category bar", + description = "Hides the category bar at the top of the homepage.", + use = false, +) { + compatibleWith("com.google.android.apps.youtube.music") + + execute { + constructCategoryBarFingerprint.method.apply { + val insertIndex = constructCategoryBarFingerprint.patternMatch!!.startIndex + val register = getInstruction(insertIndex - 1).registerA + + addInstructions( + insertIndex, + """ + const/16 v2, 0x8 + invoke-virtual {v$register, v2}, Landroid/view/View;->setVisibility(I)V + """, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/Fingerprints.kt new file mode 100644 index 000000000..29558ab4b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/Fingerprints.kt @@ -0,0 +1,31 @@ +package app.revanced.patches.music.layout.premium + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val hideGetPremiumFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters() + opcodes( + Opcode.IF_NEZ, + Opcode.CONST_16, + Opcode.INVOKE_VIRTUAL, + ) + strings("FEmusic_history", "FEmusic_offline") +} + +internal val membershipSettingsFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Ljava/lang/CharSequence;") + opcodes( + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt new file mode 100644 index 000000000..892dac76a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/premium/HideGetPremiumPatch.kt @@ -0,0 +1,45 @@ +package app.revanced.patches.music.layout.premium + +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 com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction + +@Suppress("unused") +val hideGetPremiumPatch = bytecodePatch( + name = "Hide 'Get Music Premium' label", + description = "Hides the \"Get Music Premium\" label from the account menu and settings.", +) { + compatibleWith("com.google.android.apps.youtube.music") + + execute { + hideGetPremiumFingerprint.method.apply { + val insertIndex = hideGetPremiumFingerprint.patternMatch!!.endIndex + + val setVisibilityInstruction = getInstruction(insertIndex) + val getPremiumViewRegister = setVisibilityInstruction.registerC + val visibilityRegister = setVisibilityInstruction.registerD + + replaceInstruction( + insertIndex, + "const/16 v$visibilityRegister, 0x8", + ) + + addInstruction( + insertIndex + 1, + "invoke-virtual {v$getPremiumViewRegister, v$visibilityRegister}, " + + "Landroid/view/View;->setVisibility(I)V", + ) + } + + membershipSettingsFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x0 + return-object v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/Fingerprints.kt new file mode 100644 index 000000000..f3c96dc44 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/Fingerprints.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.music.layout.upgradebutton + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val pivotBarConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + parameters("L", "Z") + opcodes( + Opcode.CHECK_CAST, + Opcode.INVOKE_INTERFACE, + Opcode.GOTO, + Opcode.IPUT_OBJECT, + Opcode.RETURN_VOID + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/RemoveUpgradeButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/RemoveUpgradeButtonPatch.kt new file mode 100644 index 000000000..22878b05f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/upgradebutton/RemoveUpgradeButtonPatch.kt @@ -0,0 +1,75 @@ +package app.revanced.patches.music.layout.upgradebutton + +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.extensions.newLabel +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.toInstructions +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction22t +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +@Suppress("unused") +val removeUpgradeButtonPatch = bytecodePatch( + name = "Remove upgrade button", + description = "Removes the upgrade tab from the pivot bar.", +) { + compatibleWith("com.google.android.apps.youtube.music") + + execute { + pivotBarConstructorFingerprint.method.apply { + val pivotBarElementFieldReference = + getInstruction(pivotBarConstructorFingerprint.patternMatch!!.endIndex - 1) + .getReference() + + val register = getInstruction(0).registerC + + // First compile all the needed instructions. + val instructionList = """ + invoke-interface { v0 }, Ljava/util/List;->size()I + move-result v1 + const/4 v2, 0x4 + invoke-interface {v0, v2}, Ljava/util/List;->remove(I)Ljava/lang/Object; + iput-object v0, v$register, $pivotBarElementFieldReference + """.toInstructions().toMutableList() + + val endIndex = pivotBarConstructorFingerprint.patternMatch!!.endIndex + + // Replace the instruction to retain the label at given index. + replaceInstruction( + endIndex - 1, + instructionList[0], // invoke-interface. + ) + // Do not forget to remove this instruction since we added it already. + instructionList.removeFirst() + + val exitInstruction = instructionList.last() // iput-object + addInstruction( + endIndex, + exitInstruction, + ) + // Do not forget to remove this instruction since we added it already. + instructionList.removeLast() + + // Add the necessary if statement to remove the upgrade tab button in case it exists. + instructionList.add( + 2, // if-le. + BuilderInstruction22t( + Opcode.IF_LE, + 1, + 2, + newLabel(endIndex), + ), + ) + + addInstructions( + endIndex, + instructionList, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch.kt new file mode 100644 index 000000000..d53691886 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/BypassCertificateChecksPatch.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.music.misc.androidauto + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val bypassCertificateChecksPatch = bytecodePatch( + name = "Bypass certificate checks", + description = "Bypasses certificate checks which prevent YouTube Music from working on Android Auto.", +) { + compatibleWith("com.google.android.apps.youtube.music") + + execute { + checkCertificateFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/Fingerprints.kt new file mode 100644 index 000000000..957f055b6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/androidauto/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.music.misc.androidauto + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val checkCertificateFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters("Ljava/lang/String;") + strings("X509", "Failed to get certificate.") +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt new file mode 100644 index 000000000..9561d9ff8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/BackgroundPlaybackPatch.kt @@ -0,0 +1,28 @@ +package app.revanced.patches.music.misc.backgroundplayback + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val backgroundPlaybackPatch = bytecodePatch( + name = "Remove background playback restrictions", + description = "Removes restrictions on background playback, including playing kids videos in the background.", +) { + compatibleWith("com.google.android.apps.youtube.music") + + execute { + kidsBackgroundPlaybackPolicyControllerFingerprint.method.addInstruction( + 0, + "return-void", + ) + + backgroundPlaybackDisableFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } +} 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..e1cf24e1a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/backgroundplayback/Fingerprints.kt @@ -0,0 +1,42 @@ +package app.revanced.patches.music.misc.backgroundplayback + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val backgroundPlaybackDisableFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Z") + parameters("L") + opcodes( + Opcode.CONST_4, + Opcode.IF_EQZ, + Opcode.IGET, + Opcode.AND_INT_LIT16, + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, + Opcode.IF_NEZ, + Opcode.SGET_OBJECT, + Opcode.IGET, + ) +} + +internal val kidsBackgroundPlaybackPolicyControllerFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("I", "L", "Z") + opcodes( + 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, + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/extension/SharedExtensionPatch.kt new file mode 100644 index 000000000..e6c1c69fe --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/extension/SharedExtensionPatch.kt @@ -0,0 +1,6 @@ +package app.revanced.patches.music.misc.extension + +import app.revanced.patches.music.misc.extension.hooks.applicationInitHook +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch + +val sharedExtensionPatch = sharedExtensionPatch(applicationInitHook) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/extension/hooks/ApplicationInitHook.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/extension/hooks/ApplicationInitHook.kt new file mode 100644 index 000000000..1e1a43f9a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/extension/hooks/ApplicationInitHook.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.music.misc.extension.hooks + +import app.revanced.patches.shared.misc.extension.extensionHook + +internal val applicationInitHook = extensionHook { + returns("V") + parameters() + strings("activity") + custom { method, _ -> method.name == "onCreate" } +} diff --git a/src/main/kotlin/app/revanced/patches/music/misc/gms/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/Constants.kt similarity index 100% rename from src/main/kotlin/app/revanced/patches/music/misc/gms/Constants.kt rename to patches/src/main/kotlin/app/revanced/patches/music/misc/gms/Constants.kt diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/Fingerprints.kt new file mode 100644 index 000000000..7131e143d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.music.misc.gms + +import app.revanced.patcher.fingerprint + +internal val musicActivityOnCreateFingerprint = fingerprint { + returns("V") + parameters("Landroid/os/Bundle;") + custom { method, classDef -> + method.name == "onCreate" && classDef.endsWith("/MusicActivity;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt new file mode 100644 index 000000000..e38ca6015 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/gms/GmsCoreSupportPatch.kt @@ -0,0 +1,33 @@ +package app.revanced.patches.music.misc.gms + +import app.revanced.patcher.patch.Option +import app.revanced.patches.music.misc.extension.sharedExtensionPatch +import app.revanced.patches.music.misc.gms.Constants.MUSIC_PACKAGE_NAME +import app.revanced.patches.music.misc.gms.Constants.REVANCED_MUSIC_PACKAGE_NAME +import app.revanced.patches.shared.castContextFetchFingerprint +import app.revanced.patches.shared.misc.gms.gmsCoreSupportPatch +import app.revanced.patches.shared.primeMethodFingerprint + +@Suppress("unused") +val gmsCoreSupportPatch = gmsCoreSupportPatch( + fromPackageName = MUSIC_PACKAGE_NAME, + toPackageName = REVANCED_MUSIC_PACKAGE_NAME, + primeMethodFingerprint = primeMethodFingerprint, + earlyReturnFingerprints = setOf( + castContextFetchFingerprint, + ), + mainActivityOnCreateFingerprint = musicActivityOnCreateFingerprint, + extensionPatch = sharedExtensionPatch, + gmsCoreSupportResourcePatchFactory = ::gmsCoreSupportResourcePatch, +) { + compatibleWith(MUSIC_PACKAGE_NAME) +} + +private fun gmsCoreSupportResourcePatch( + gmsCoreVendorGroupIdOption: Option, +) = app.revanced.patches.shared.misc.gms.gmsCoreSupportResourcePatch( + fromPackageName = MUSIC_PACKAGE_NAME, + toPackageName = REVANCED_MUSIC_PACKAGE_NAME, + gmsCoreVendorGroupIdOption = gmsCoreVendorGroupIdOption, + spoofedPackageSignature = "afb0fed5eeaebdd86f56a97742f4b6b33ef59875", +) diff --git a/patches/src/main/kotlin/app/revanced/patches/myexpenses/misc/pro/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/myexpenses/misc/pro/Fingerprints.kt new file mode 100644 index 000000000..6bc4c21e5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/myexpenses/misc/pro/Fingerprints.kt @@ -0,0 +1,8 @@ +package app.revanced.patches.myexpenses.misc.pro + +import app.revanced.patcher.fingerprint + +internal val isEnabledFingerprint = fingerprint { + returns("Z") + strings("feature", "feature.licenceStatus") +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/myexpenses/misc/pro/UnlockProPatch.kt b/patches/src/main/kotlin/app/revanced/patches/myexpenses/misc/pro/UnlockProPatch.kt new file mode 100644 index 000000000..4365c2c2c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/myexpenses/misc/pro/UnlockProPatch.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.myexpenses.misc.pro + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val unlockProPatch = bytecodePatch( + name = "Unlock pro", +) { + compatibleWith("org.totschnig.myexpenses") + + execute { + isEnabledFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/myfitnesspal/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/myfitnesspal/ads/Fingerprints.kt new file mode 100644 index 000000000..160e2db27 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/myfitnesspal/ads/Fingerprints.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.myfitnesspal.ads + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val isPremiumUseCaseImplFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + custom { method, classDef -> + classDef.endsWith("IsPremiumUseCaseImpl;") && method.name == "doWork" + } +} + +internal val mainActivityNavigateToNativePremiumUpsellFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returns("V") + custom { method, classDef -> + classDef.endsWith("MainActivity;") && method.name == "navigateToNativePremiumUpsell" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/myfitnesspal/ads/HideAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/myfitnesspal/ads/HideAdsPatch.kt new file mode 100644 index 000000000..a9428b29e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/myfitnesspal/ads/HideAdsPatch.kt @@ -0,0 +1,30 @@ +package app.revanced.patches.myfitnesspal.ads + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val hideAdsPatch = bytecodePatch( + name = "Hide ads", + description = "Hides most of the ads across the app.", +) { + compatibleWith("com.myfitnesspal.android"("24.14.2")) + + execute { + // Overwrite the premium status specifically for ads. + isPremiumUseCaseImplFingerprint.method.replaceInstructions( + 0, + """ + sget-object v0, Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean; + return-object v0 + """, + ) + + // Prevent the premium upsell dialog from showing when the main activity is launched. + // In other places that are premium-only the dialog will still show. + mainActivityNavigateToNativePremiumUpsellFingerprint.method.replaceInstructions( + 0, + "return-void", + ) + } +} diff --git a/src/main/kotlin/app/revanced/patches/netguard/broadcasts/removerestriction/RemoveBroadcastsRestrictionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/netguard/broadcasts/removerestriction/RemoveBroadcastsRestrictionPatch.kt similarity index 62% rename from src/main/kotlin/app/revanced/patches/netguard/broadcasts/removerestriction/RemoveBroadcastsRestrictionPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/netguard/broadcasts/removerestriction/RemoveBroadcastsRestrictionPatch.kt index 70346ec2c..b4f675ed7 100644 --- a/src/main/kotlin/app/revanced/patches/netguard/broadcasts/removerestriction/RemoveBroadcastsRestrictionPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/netguard/broadcasts/removerestriction/RemoveBroadcastsRestrictionPatch.kt @@ -1,23 +1,17 @@ package app.revanced.patches.netguard.broadcasts.removerestriction -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.annotation.CompatiblePackage -import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patcher.patch.resourcePatch import org.w3c.dom.Element -@Patch( +@Suppress("unused") +val removeBroadcastsRestrictionPatch = resourcePatch( name = "Remove broadcasts restriction", description = "Enables starting/stopping NetGuard via broadcasts.", - compatiblePackages = [CompatiblePackage("eu.faircode.netguard")], - use = false, -) -@Suppress("unused") -object RemoveBroadcastsRestrictionPatch : ResourcePatch() { - override fun execute(context: ResourceContext) { - context.xmlEditor["AndroidManifest.xml"].use { editor -> - val document = editor.file +) { + compatibleWith("eu.faircode.netguard") + execute { + document("AndroidManifest.xml").use { document -> val applicationNode = document .getElementsByTagName("application") diff --git a/patches/src/main/kotlin/app/revanced/patches/nfctoolsse/misc/pro/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/nfctoolsse/misc/pro/Fingerprints.kt new file mode 100644 index 000000000..f4f5d48c0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/nfctoolsse/misc/pro/Fingerprints.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.nfctoolsse.misc.pro + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val isLicenseRegisteredFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("Z") + strings("kLicenseCheck") +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/nfctoolsse/misc/pro/UnlockProPatch.kt b/patches/src/main/kotlin/app/revanced/patches/nfctoolsse/misc/pro/UnlockProPatch.kt new file mode 100644 index 000000000..55276126e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/nfctoolsse/misc/pro/UnlockProPatch.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.nfctoolsse.misc.pro + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val unlockProPatch = bytecodePatch( + name = "Unlock pro", +) { + compatibleWith("com.wakdev.apps.nfctools.se") + + execute { + isLicenseRegisteredFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/nyx/misc/pro/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/nyx/misc/pro/Fingerprints.kt new file mode 100644 index 000000000..e2bffa451 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/nyx/misc/pro/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.nyx.misc.pro + +import app.revanced.patcher.fingerprint + +internal val checkProFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("BillingManager;") && method.name == "isProVersion" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/nyx/misc/pro/UnlockProPatch.kt b/patches/src/main/kotlin/app/revanced/patches/nyx/misc/pro/UnlockProPatch.kt new file mode 100644 index 000000000..65040b32a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/nyx/misc/pro/UnlockProPatch.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.nyx.misc.pro + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val unlockProPatch = bytecodePatch( + name = "Unlock pro", +) { + compatibleWith("com.awedea.nyx") + + execute { + checkProFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/openinghours/misc/fix/crash/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/openinghours/misc/fix/crash/Fingerprints.kt new file mode 100644 index 000000000..69463c510 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/openinghours/misc/fix/crash/Fingerprints.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.openinghours.misc.fix.crash + +import app.revanced.patcher.fingerprint + +internal val setPlaceFingerprint = fingerprint { + returns("V") + parameters("Lde/simon/openinghours/models/Place;") + custom { method, _ -> + method.name == "setPlace" && + method.definingClass == "Lde/simon/openinghours/views/custom/PlaceCard;" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/openinghours/misc/fix/crash/FixCrashPatch.kt b/patches/src/main/kotlin/app/revanced/patches/openinghours/misc/fix/crash/FixCrashPatch.kt new file mode 100644 index 000000000..db38e6161 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/openinghours/misc/fix/crash/FixCrashPatch.kt @@ -0,0 +1,105 @@ +package app.revanced.patches.openinghours.misc.fix.crash + +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.extensions.newLabel +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t +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.reference.MethodReference + +@Suppress("unused") +val fixCrashPatch = bytecodePatch( + name = "Fix crash", +) { + compatibleWith("de.simon.openinghours"("1.0")) + + execute { + val indexedInstructions = setPlaceFingerprint.method.instructions.withIndex().toList() + + /** + * This function replaces all `checkNotNull` instructions in the integer interval + * from [startIndex] to [endIndex], both inclusive. In place of the `checkNotNull` + * instruction an if-null check is inserted. If the if-null check yields that + * the value is indeed null, we jump to a newly created label at `endIndex + 1`. + */ + fun avoidNullPointerException(startIndex: Int, endIndex: Int) { + val continueLabel = setPlaceFingerprint.method.newLabel(endIndex + 1) + + for (index in startIndex..endIndex) { + val instruction = indexedInstructions[index].value + + if (!instruction.isCheckNotNullInstruction) { + continue + } + + val checkNotNullInstruction = instruction as FiveRegisterInstruction + val originalRegister = checkNotNullInstruction.registerC + + setPlaceFingerprint.method.replaceInstruction( + index, + BuilderInstruction21t( + Opcode.IF_EQZ, + originalRegister, + continueLabel, + ), + ) + } + } + + val getOpeningHoursIndex = getIndicesOfInvoke( + indexedInstructions, + "Lde/simon/openinghours/models/Place;", + "getOpeningHours", + ) + + val setWeekDayTextIndex = getIndexOfInvoke( + indexedInstructions, + "Lde/simon/openinghours/views/custom/PlaceCard;", + "setWeekDayText", + ) + + val startCalculateStatusIndex = getIndexOfInvoke( + indexedInstructions, + "Lde/simon/openinghours/views/custom/PlaceCard;", + "startCalculateStatus", + ) + + // Replace the Intrinsics;->checkNotNull instructions with a null check + // and jump to our newly created label if it returns true. + // This avoids the NullPointerExceptions. + avoidNullPointerException(getOpeningHoursIndex[1], startCalculateStatusIndex) + avoidNullPointerException(getOpeningHoursIndex[0], setWeekDayTextIndex) + } +} + +private fun isInvokeInstruction(instruction: Instruction, className: String, methodName: String): Boolean { + val methodRef = instruction.getReference() ?: return false + return methodRef.definingClass == className && methodRef.name == methodName +} + +private fun getIndicesOfInvoke( + instructions: List>, + className: String, + methodName: String, +): List = instructions.mapNotNull { (index, instruction) -> + if (isInvokeInstruction(instruction, className, methodName)) { + index + } else { + null + } +} + +private fun getIndexOfInvoke( + instructions: List>, + className: String, + methodName: String, +): Int = instructions.first { (_, instruction) -> + isInvokeInstruction(instruction, className, methodName) +}.index + +private val Instruction.isCheckNotNullInstruction + get() = isInvokeInstruction(this, "Lkotlin/jvm/internal/Intrinsics;", "checkNotNull") diff --git a/src/main/kotlin/app/revanced/patches/photomath/detection/deviceid/fingerprints/GetDeviceIdFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/photomath/detection/deviceid/Fingerprints.kt similarity index 55% rename from src/main/kotlin/app/revanced/patches/photomath/detection/deviceid/fingerprints/GetDeviceIdFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/photomath/detection/deviceid/Fingerprints.kt index fe01d7b59..90c0bbb91 100644 --- a/src/main/kotlin/app/revanced/patches/photomath/detection/deviceid/fingerprints/GetDeviceIdFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/photomath/detection/deviceid/Fingerprints.kt @@ -1,11 +1,12 @@ -package app.revanced.patches.photomath.detection.deviceid.fingerprints +package app.revanced.patches.photomath.detection.deviceid -import app.revanced.patcher.fingerprint.MethodFingerprint import com.android.tools.smali.dexlib2.Opcode +import app.revanced.patcher.fingerprint -internal object GetDeviceIdFingerprint : MethodFingerprint( - returnType = "Ljava/lang/String;", - opcodes = listOf( +internal val getDeviceIdFingerprint = fingerprint { + returns("Ljava/lang/String;") + parameters() + opcodes( Opcode.SGET_OBJECT, Opcode.IGET_OBJECT, Opcode.INVOKE_STATIC, @@ -16,6 +17,5 @@ internal object GetDeviceIdFingerprint : MethodFingerprint( Opcode.INVOKE_VIRTUAL, Opcode.MOVE_RESULT_OBJECT, Opcode.INVOKE_VIRTUAL, - ), - parameters = listOf() -) \ No newline at end of file + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/photomath/detection/deviceid/SpoofDeviceIdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/photomath/detection/deviceid/SpoofDeviceIdPatch.kt new file mode 100644 index 000000000..16c84b056 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/photomath/detection/deviceid/SpoofDeviceIdPatch.kt @@ -0,0 +1,26 @@ +package app.revanced.patches.photomath.detection.deviceid + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.photomath.detection.signature.signatureDetectionPatch +import kotlin.random.Random + +@Suppress("unused") +val getDeviceIdPatch = bytecodePatch( + name = "Spoof device ID", + description = "Spoofs device ID to mitigate manual bans by developers.", +) { + dependsOn(signatureDetectionPatch) + + compatibleWith("com.microblink.photomath"("8.37.0")) + + execute { + getDeviceIdFingerprint.method.replaceInstructions( + 0, + """ + const-string v0, "${Random.nextLong().toString(16)}" + return-object v0 + """, + ) + } +} diff --git a/src/main/kotlin/app/revanced/patches/photomath/detection/signature/fingerprints/CheckSignatureFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/photomath/detection/signature/Fingerprints.kt similarity index 52% rename from src/main/kotlin/app/revanced/patches/photomath/detection/signature/fingerprints/CheckSignatureFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/photomath/detection/signature/Fingerprints.kt index cd784ab20..5d7a20783 100644 --- a/src/main/kotlin/app/revanced/patches/photomath/detection/signature/fingerprints/CheckSignatureFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/photomath/detection/signature/Fingerprints.kt @@ -1,13 +1,10 @@ -package app.revanced.patches.photomath.detection.signature.fingerprints +package app.revanced.patches.photomath.detection.signature -import app.revanced.patcher.fingerprint.MethodFingerprint import com.android.tools.smali.dexlib2.Opcode +import app.revanced.patcher.fingerprint -internal object CheckSignatureFingerprint : MethodFingerprint( - strings = listOf( - "signatures", - ), - opcodes = listOf( +internal val checkSignatureFingerprint = fingerprint { + opcodes( Opcode.CONST_STRING, Opcode.INVOKE_STATIC, Opcode.INVOKE_STATIC, @@ -17,4 +14,5 @@ internal object CheckSignatureFingerprint : MethodFingerprint( Opcode.INVOKE_STATIC, Opcode.MOVE_RESULT, ) -) + strings("signatures") +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/photomath/detection/signature/SignatureDetectionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/photomath/detection/signature/SignatureDetectionPatch.kt new file mode 100644 index 000000000..00b47e516 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/photomath/detection/signature/SignatureDetectionPatch.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.photomath.detection.signature + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +val signatureDetectionPatch = bytecodePatch( + description = "Disables detection of incorrect signature.", +) { + + execute { + val replacementIndex = checkSignatureFingerprint.patternMatch!!.endIndex + val checkRegister = + checkSignatureFingerprint.method.getInstruction(replacementIndex).registerA + checkSignatureFingerprint.method.replaceInstruction(replacementIndex, "const/4 v$checkRegister, 0x1") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/photomath/misc/annoyances/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/photomath/misc/annoyances/Fingerprints.kt new file mode 100644 index 000000000..301f2f9a5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/photomath/misc/annoyances/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.photomath.misc.annoyances + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val hideUpdatePopupFingerprint = fingerprint { + accessFlags(AccessFlags.FINAL, AccessFlags.PUBLIC) + returns("V") + opcodes( + Opcode.CONST_HIGH16, + Opcode.INVOKE_VIRTUAL, // ViewPropertyAnimator.alpha(1.0f) + Opcode.MOVE_RESULT_OBJECT, + Opcode.CONST_WIDE_16, + Opcode.INVOKE_VIRTUAL, // ViewPropertyAnimator.setDuration(1000L) + ) + custom { method, _ -> + // The popup is shown only in the main activity + method.definingClass == "Lcom/microblink/photomath/main/activity/MainActivity;" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/photomath/misc/annoyances/HideUpdatePopupPatch.kt b/patches/src/main/kotlin/app/revanced/patches/photomath/misc/annoyances/HideUpdatePopupPatch.kt new file mode 100644 index 000000000..a3586de1c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/photomath/misc/annoyances/HideUpdatePopupPatch.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.photomath.misc.annoyances + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.photomath.detection.signature.signatureDetectionPatch + +@Suppress("unused") +val hideUpdatePopupPatch = bytecodePatch( + name = "Hide update popup", + description = "Prevents the update popup from showing up.", +) { + dependsOn(signatureDetectionPatch) + + compatibleWith("com.microblink.photomath"("8.32.0")) + + execute { + hideUpdatePopupFingerprint.method.addInstructions( + 2, // Insert after the null check. + "return-void", + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/photomath/misc/unlock/bookpoint/EnableBookpointPatch.kt b/patches/src/main/kotlin/app/revanced/patches/photomath/misc/unlock/bookpoint/EnableBookpointPatch.kt new file mode 100644 index 000000000..6e3da9802 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/photomath/misc/unlock/bookpoint/EnableBookpointPatch.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.photomath.misc.unlock.bookpoint + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions +import app.revanced.patcher.patch.bytecodePatch + +val enableBookpointPatch = bytecodePatch( + description = "Enables textbook access", +) { + + execute { + isBookpointEnabledFingerprint.method.replaceInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/photomath/misc/unlock/bookpoint/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/photomath/misc/unlock/bookpoint/Fingerprints.kt new file mode 100644 index 000000000..6722f4223 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/photomath/misc/unlock/bookpoint/Fingerprints.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.photomath.misc.unlock.bookpoint + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val isBookpointEnabledFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters() + strings( + "NoGeoData", + "NoCountryInGeo", + "RemoteConfig", + "GeoRCMismatch" + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/photomath/misc/unlock/plus/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/photomath/misc/unlock/plus/Fingerprints.kt new file mode 100644 index 000000000..f6c282cbd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/photomath/misc/unlock/plus/Fingerprints.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.photomath.misc.unlock.plus + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val isPlusUnlockedFingerprint = fingerprint{ + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + strings("genius") + custom { _, classDef -> + classDef.endsWith("/User;") + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/photomath/misc/unlock/plus/UnlockPlusPatch.kt b/patches/src/main/kotlin/app/revanced/patches/photomath/misc/unlock/plus/UnlockPlusPatch.kt new file mode 100644 index 000000000..8cc80c72e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/photomath/misc/unlock/plus/UnlockPlusPatch.kt @@ -0,0 +1,25 @@ +package app.revanced.patches.photomath.misc.unlock.plus + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.photomath.detection.signature.signatureDetectionPatch +import app.revanced.patches.photomath.misc.unlock.bookpoint.enableBookpointPatch + +@Suppress("unused") +val unlockPlusPatch = bytecodePatch( + name = "Unlock plus", +) { + dependsOn(signatureDetectionPatch, enableBookpointPatch) + + compatibleWith("com.microblink.photomath"("8.37.0")) + + execute { + isPlusUnlockedFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/piccomafr/misc/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/piccomafr/misc/Fingerprints.kt new file mode 100644 index 000000000..8c2d579ef --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/piccomafr/misc/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.piccomafr.misc + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val getAndroidIdFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Ljava/lang/String;") + parameters("Landroid/content/Context;") + strings("context", "android_id") +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/piccomafr/misc/SpoofAndroidDeviceIdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/piccomafr/misc/SpoofAndroidDeviceIdPatch.kt new file mode 100644 index 000000000..a2b327135 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/piccomafr/misc/SpoofAndroidDeviceIdPatch.kt @@ -0,0 +1,50 @@ +package app.revanced.patches.piccomafr.misc + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.stringOption + +@Suppress("unused") +val spoofAndroidDeviceIdPatch = bytecodePatch( + name = "Spoof Android device ID", + description = "Spoofs the Android device ID used by the app for account authentication." + + "This can be used to copy the account to another device.", + use = false, +) { + compatibleWith( + "com.piccomaeurope.fr"( + "6.4.0", + "6.4.1", + "6.4.2", + "6.4.3", + "6.4.4", + "6.4.5", + "6.5.0", + "6.5.1", + "6.5.2", + "6.5.3", + "6.5.4", + "6.6.0", + "6.6.1", + "6.6.2", + ), + ) + + val androidDeviceId by stringOption( + key = "android-device-id", + default = "0011223344556677", + title = "Android device ID", + description = "The Android device ID to spoof to.", + required = true, + ) { it!!.matches("[A-Fa-f0-9]{16}".toRegex()) } + + execute { + getAndroidIdFingerprint.method.addInstructions( + 0, + """ + const-string v0, "$androidDeviceId" + return-object v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/piccomafr/tracking/DisableTrackingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/piccomafr/tracking/DisableTrackingPatch.kt new file mode 100644 index 000000000..32e02f37a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/piccomafr/tracking/DisableTrackingPatch.kt @@ -0,0 +1,67 @@ +package app.revanced.patches.piccomafr.tracking + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.getReference +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 + +@Suppress("unused") +val disableTrackingPatch = bytecodePatch( + name = "Disable tracking", + description = "Disables tracking by replacing tracking URLs with example.com.", +) { + compatibleWith( + "com.piccomaeurope.fr"( + "6.4.0", + "6.4.1", + "6.4.2", + "6.4.3", + "6.4.4", + "6.4.5", + "6.5.0", + "6.5.1", + "6.5.2", + "6.5.3", + "6.5.4", + "6.6.0", + "6.6.1", + "6.6.2", + ), + ) + + execute { + facebookSDKFingerprint.method.apply { + instructions.filter { instruction -> + instruction.opcode == Opcode.CONST_STRING + }.forEach { instruction -> + instruction as OneRegisterInstruction + + replaceInstruction( + instruction.location.index, + "const-string v${instruction.registerA}, \"example.com\"", + ) + } + } + + firebaseInstallFingerprint.method.apply { + instructions.filter { + it.opcode == Opcode.CONST_STRING + }.filter { + it.getReference()?.string == "firebaseinstallations.googleapis.com" + }.forEach { instruction -> + instruction as OneRegisterInstruction + + replaceInstruction( + instruction.location.index, + "const-string v${instruction.registerA}, \"example.com\"", + ) + } + } + + appMeasurementFingerprint.method.addInstruction(0, "return-void") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/piccomafr/tracking/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/piccomafr/tracking/Fingerprints.kt new file mode 100644 index 000000000..794f21bcb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/piccomafr/tracking/Fingerprints.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.piccomafr.tracking + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val appMeasurementFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returns("V") + strings("config/app/", "Fetching remote configuration") +} + +internal val facebookSDKFingerprint = fingerprint { + accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) + returns("V") + strings("instagram.com", "facebook.com") +} + +internal val firebaseInstallFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE) + strings( + "https://%s/%s/%s", + "firebaseinstallations.googleapis.com", + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/pixiv/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/pixiv/ads/Fingerprints.kt new file mode 100644 index 000000000..3e2addaa0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/pixiv/ads/Fingerprints.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.pixiv.ads + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val shouldShowAdsFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + custom { methodDef, classDef -> + classDef.type.endsWith("AdUtils;") && methodDef.name == "shouldShowAds" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/pixiv/ads/HideAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/pixiv/ads/HideAdsPatch.kt new file mode 100644 index 000000000..29f63e9cf --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/pixiv/ads/HideAdsPatch.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.pixiv.ads + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val hideAdsPatch = bytecodePatch( + name = "Hide ads", +) { + compatibleWith("jp.pxv.android") + + execute { + shouldShowAdsFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x0 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/rar/misc/annoyances/purchasereminder/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/rar/misc/annoyances/purchasereminder/Fingerprints.kt new file mode 100644 index 000000000..a4d2c9e22 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/rar/misc/annoyances/purchasereminder/Fingerprints.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.rar.misc.annoyances.purchasereminder + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val showReminderFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("V") + custom { method, _ -> + method.definingClass.endsWith("AdsNotify;") && method.name == "show" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/rar/misc/annoyances/purchasereminder/HidePurchaseReminderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/rar/misc/annoyances/purchasereminder/HidePurchaseReminderPatch.kt new file mode 100644 index 000000000..885c2400f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/rar/misc/annoyances/purchasereminder/HidePurchaseReminderPatch.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.rar.misc.annoyances.purchasereminder + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val hidePurchaseReminderPatch = bytecodePatch( + name = "Hide purchase reminder", + description = "Hides the popup that reminds you to purchase the app.", + +) { + compatibleWith("com.rarlab.rar") + + execute { + showReminderFingerprint.method.addInstruction(0, "return-void") + } +} diff --git a/src/main/kotlin/app/revanced/patches/reddit/ad/banner/HideBannerPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/banner/HideBannerPatch.kt similarity index 65% rename from src/main/kotlin/app/revanced/patches/reddit/ad/banner/HideBannerPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/reddit/ad/banner/HideBannerPatch.kt index 311803195..62e8d5d84 100644 --- a/src/main/kotlin/app/revanced/patches/reddit/ad/banner/HideBannerPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/banner/HideBannerPatch.kt @@ -1,20 +1,17 @@ package app.revanced.patches.reddit.ad.banner -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patcher.patch.resourcePatch // Note that for now, this patch and anything using it will only work on // Reddit 2024.17.0 or older. Newer versions will crash during patching. // See https://github.com/ReVanced/revanced-patches/issues/3099 -@Patch(description = "Hides banner ads from comments on subreddits.") -object HideBannerPatch : ResourcePatch() { - private const val RESOURCE_FILE_PATH = "res/layout/merge_listheader_link_detail.xml" - - override fun execute(context: ResourceContext) { - context.xmlEditor[RESOURCE_FILE_PATH].use { editor -> - val document = editor.file +val hideBannerPatch = resourcePatch( + description = "Hides banner ads from comments on subreddits.", +) { + execute { + val resourceFilePath = "res/layout/merge_listheader_link_detail.xml" + document(resourceFilePath).use { document -> document.getElementsByTagName("merge").item(0).childNodes.apply { val attributes = arrayOf("height", "width") diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/ad/comments/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/comments/Fingerprints.kt new file mode 100644 index 000000000..c99df5707 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/comments/Fingerprints.kt @@ -0,0 +1,14 @@ +package app.revanced.patches.reddit.ad.comments + +import app.revanced.patcher.fingerprint + +internal val hideCommentAdsFingerprint = fingerprint { + strings( + "link", + // CommentPageRepository is not returning a link object + "is not returning a link object" + ) + custom { _, classDef -> + classDef.sourceFile == "PostDetailPresenter.kt" + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/ad/comments/HideCommentAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/comments/HideCommentAdsPatch.kt new file mode 100644 index 000000000..e2c53d332 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/comments/HideCommentAdsPatch.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.reddit.ad.comments + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +val hideCommentAdsPatch = bytecodePatch( + description = "Removes ads in the comments.", +) { + + execute { + hideCommentAdsFingerprint.method.addInstructions( + 0, + """ + new-instance v0, Ljava/lang/Object; + invoke-direct {v0}, Ljava/lang/Object;->()V + return-object v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/ad/general/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/general/Fingerprints.kt new file mode 100644 index 000000000..e7dd78912 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/general/Fingerprints.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.reddit.ad.general + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val adPostFingerprint = fingerprint { + returns("V") + // "children" are present throughout multiple versions + strings("children") + custom { _, classDef -> classDef.endsWith("Listing;") } +} + +internal val newAdPostFingerprint = fingerprint { + opcodes(Opcode.INVOKE_VIRTUAL) + strings("chain", "feedElement") + custom { _, classDef -> classDef.sourceFile == "AdElementConverter.kt" } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/ad/general/HideAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/general/HideAdsPatch.kt new file mode 100644 index 000000000..c3e47f8f8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/ad/general/HideAdsPatch.kt @@ -0,0 +1,79 @@ +package app.revanced.patches.reddit.ad.general + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.reddit.ad.banner.hideBannerPatch +import app.revanced.patches.reddit.ad.comments.hideCommentAdsPatch +import app.revanced.patches.reddit.misc.extension.sharedExtensionPatch +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22c +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +@Suppress("unused") +val hideAdsPatch = bytecodePatch( + name = "Hide ads", +) { + dependsOn(hideBannerPatch, hideCommentAdsPatch, sharedExtensionPatch) + + // Note that for now, this patch and anything using it will only work on + // Reddit 2024.17.0 or older. Newer versions will crash during patching. + // See https://github.com/ReVanced/revanced-patches/issues/3099 + // and https://github.com/iBotPeaches/Apktool/issues/3534. + // This constraint is necessary due to dependency on hideBannerPatch. + compatibleWith("com.reddit.frontpage"("2024.17.0")) + + execute { + // region Filter promoted ads (does not work in popular or latest feed) + + val filterMethodDescriptor = + "Lapp/revanced/extension/reddit/patches/FilterPromotedLinksPatch;" + + "->filterChildren(Ljava/lang/Iterable;)Ljava/util/List;" + + adPostFingerprint.method.apply { + val setPostsListChildren = implementation!!.instructions.first { instruction -> + if (instruction.opcode != Opcode.IPUT_OBJECT) return@first false + + val reference = (instruction as ReferenceInstruction).reference as FieldReference + reference.name == "children" + } + + val castedInstruction = setPostsListChildren as Instruction22c + val itemsRegister = castedInstruction.registerA + val listInstanceRegister = castedInstruction.registerB + + // postsList.children = filterChildren(postListItems) + removeInstruction(setPostsListChildren.location.index) + addInstructions( + setPostsListChildren.location.index, + """ + invoke-static {v$itemsRegister}, $filterMethodDescriptor + move-result-object v0 + iput-object v0, v$listInstanceRegister, ${castedInstruction.reference} + """, + ) + } + + // endregion + + // region Remove ads from popular and latest feed + + // The new feeds work by inserting posts into lists. + // AdElementConverter is conveniently responsible for inserting all feed ads. + // By removing the appending instruction no ad posts gets appended to the feed. + + val index = newAdPostFingerprint.originalMethod.implementation!!.instructions.indexOfFirst { + if (it.opcode != Opcode.INVOKE_VIRTUAL) return@indexOfFirst false + + val reference = (it as ReferenceInstruction).reference as MethodReference + + reference.name == "add" && reference.definingClass == "Ljava/util/ArrayList;" + } + + newAdPostFingerprint.method.removeInstruction(index) + } + + // endregion +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/FixSLinksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/FixSLinksPatch.kt new file mode 100644 index 000000000..ccc0346a6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/FixSLinksPatch.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.reddit.customclients + +import app.revanced.patcher.patch.BytecodePatchBuilder +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.bytecodePatch + +const val RESOLVE_S_LINK_METHOD = "patchResolveSLink(Ljava/lang/String;)Z" +const val SET_ACCESS_TOKEN_METHOD = "patchSetAccessToken(Ljava/lang/String;)V" + +fun fixSLinksPatch( + extensionPatch: Patch<*>, + block: BytecodePatchBuilder.() -> Unit = {}, +) = bytecodePatch(name = "Fix /s/ links") { + dependsOn(extensionPatch) + + block() +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/SpoofClientPatch.kt new file mode 100644 index 000000000..b21b36a0d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/SpoofClientPatch.kt @@ -0,0 +1,34 @@ +package app.revanced.patches.reddit.customclients + +import app.revanced.patcher.patch.BytecodePatchBuilder +import app.revanced.patcher.patch.Option +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.stringOption + +/** + * Base class for patches that spoof the Reddit client. + * + * @param redirectUri The redirect URI of the Reddit OAuth client. + * @param block The patch block. It is called with the client ID option. + */ +fun spoofClientPatch( + redirectUri: String, + block: BytecodePatchBuilder.(Option) -> Unit = {}, +) = bytecodePatch( + name = "Spoof client", + description = "Restores functionality of the app by using custom client ID.", +) { + block( + stringOption( + "client-id", + null, + null, + "OAuth client ID", + "The Reddit OAuth client ID. " + + "You can get your client ID from https://www.reddit.com/prefs/apps. " + + "The application type has to be \"Installed app\" " + + "and the redirect URI has to be set to \"$redirectUri\".", + true, + ), + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/baconreader/api/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/baconreader/api/Fingerprints.kt new file mode 100644 index 000000000..bb87c2114 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/baconreader/api/Fingerprints.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.reddit.customclients.baconreader.api + +import app.revanced.patcher.fingerprint + +internal val getAuthorizationUrlFingerprint = fingerprint { + strings("client_id=zACVn0dSFGdWqQ") +} +internal val getClientIdFingerprint = fingerprint { + strings("client_id=zACVn0dSFGdWqQ") + custom { method, classDef -> + if (!classDef.endsWith("RedditOAuth;")) return@custom false + + method.name == "getAuthorizeUrl" + } +} + +internal val requestTokenFingerprint = fingerprint { + strings("zACVn0dSFGdWqQ", "kDm2tYpu9DqyWFFyPlNcXGEni4k") // App ID and secret. +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/baconreader/api/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/baconreader/api/SpoofClientPatch.kt new file mode 100644 index 000000000..d9cfa994f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/baconreader/api/SpoofClientPatch.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.reddit.customclients.baconreader.api + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patches.reddit.customclients.spoofClientPatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +val spoofClientPatch = spoofClientPatch(redirectUri = "http://baconreader.com/auth") { clientIdOption -> + compatibleWith( + "com.onelouder.baconreader", + "com.onelouder.baconreader.premium", + ) + + val clientId by clientIdOption + + execute { + fun Fingerprint.patch(replacementString: String) { + val clientIdIndex = stringMatches!!.first().index + + method.apply { + val clientIdRegister = getInstruction(clientIdIndex).registerA + replaceInstruction( + clientIdIndex, + "const-string v$clientIdRegister, \"$replacementString\"", + ) + } + } + + // Patch client id in authorization url. + getAuthorizationUrlFingerprint.patch("client_id=$clientId") + + // Patch client id for access token request. + requestTokenFingerprint.patch(clientId!!) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/ads/DisableAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/ads/DisableAdsPatch.kt new file mode 100644 index 000000000..fc5cabd21 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/ads/DisableAdsPatch.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.reddit.customclients.boostforreddit.ads + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val disableAdsPatch = bytecodePatch( + name = "Disable ads", +) { + compatibleWith("com.rubenmayayo.reddit") + + execute { + arrayOf(maxMediationFingerprint, admobMediationFingerprint).forEach { fingerprint -> + fingerprint.method.addInstructions(0, "return-void") + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/ads/Fingerprints.kt new file mode 100644 index 000000000..618e2f145 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/ads/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.reddit.customclients.boostforreddit.ads + +import app.revanced.patcher.fingerprint + +internal val maxMediationFingerprint = fingerprint { + strings("MaxMediation: Attempting to initialize SDK") +} + +internal val admobMediationFingerprint = fingerprint { + strings("AdmobMediation: Attempting to initialize SDK") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/api/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/api/Fingerprints.kt new file mode 100644 index 000000000..cc06fd396 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/api/Fingerprints.kt @@ -0,0 +1,15 @@ +package app.revanced.patches.reddit.customclients.boostforreddit.api + +import app.revanced.patcher.fingerprint + +internal val buildUserAgentFingerprint = fingerprint { + strings("%s:%s:%s (by /u/%s)") +} + +internal val getClientIdFingerprint = fingerprint { + custom { method, classDef -> + if (!classDef.endsWith("Credentials;")) return@custom false + + method.name == "getClientId" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/api/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/api/SpoofClientPatch.kt new file mode 100644 index 000000000..389facb95 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/api/SpoofClientPatch.kt @@ -0,0 +1,37 @@ +package app.revanced.patches.reddit.customclients.boostforreddit.api + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patches.reddit.customclients.spoofClientPatch + +val spoofClientPatch = spoofClientPatch(redirectUri = "http://rubenmayayo.com") { clientIdOption -> + compatibleWith("com.rubenmayayo.reddit") + + val clientId by clientIdOption + + execute { + // region Patch client id. + + getClientIdFingerprint.method.addInstructions( + 0, + """ + const-string v0, "$clientId" + return-object v0 + """, + ) + + // endregion + + // region Patch user agent. + + // Use a random number as the platform in the user agent string. + val platformName = (0..100000).random() + val platformParameter = 0 + + buildUserAgentFingerprint.method.addInstructions( + 0, + "const-string p$platformParameter, \"$platformName\"", + ) + + // endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/Fingerprints.kt new file mode 100644 index 000000000..a2b1530b8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/Fingerprints.kt @@ -0,0 +1,7 @@ +package app.revanced.patches.reddit.customclients.boostforreddit.fix.downloads + +import app.revanced.patcher.fingerprint + +internal val downloadAudioFingerprint = fingerprint { + strings("/DASH_audio.mp4", "/audio") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/FixAudioMissingInDownloadsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/FixAudioMissingInDownloadsPatch.kt new file mode 100644 index 000000000..8cb3f5518 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/downloads/FixAudioMissingInDownloadsPatch.kt @@ -0,0 +1,30 @@ +package app.revanced.patches.reddit.customclients.boostforreddit.fix.downloads + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +@Suppress("unused") +val fixAudioMissingInDownloadsPatch = bytecodePatch( + name = "Fix missing audio in video downloads", + description = "Fixes audio missing in videos downloaded from v.redd.it.", +) { + compatibleWith("com.rubenmayayo.reddit") + + execute { + val endpointReplacements = mapOf( + "/DASH_audio.mp4" to "/DASH_AUDIO_128.mp4", + "/audio" to "/DASH_AUDIO_64.mp4", + ) + + downloadAudioFingerprint.method.apply { + downloadAudioFingerprint.stringMatches!!.forEach { match -> + val replacement = endpointReplacements[match.string] + val register = getInstruction(match.index).registerA + + replaceInstruction(match.index, "const-string v$register, \"$replacement\"") + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/slink/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/slink/Fingerprints.kt new file mode 100644 index 000000000..665dba5a4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/slink/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.reddit.customclients.boostforreddit.fix.slink + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val getOAuthAccessTokenFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("Ljava/lang/String") + strings("access_token") + custom { method, _ -> method.definingClass == "Lnet/dean/jraw/http/oauth/OAuthData;" } +} + +internal val handleNavigationFingerprint = fingerprint { + strings( + "android.intent.action.SEARCH", + "subscription", + "sort", + "period", + "boostforreddit.com/themes", + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/slink/FixSLinksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/slink/FixSLinksPatch.kt new file mode 100644 index 000000000..076221e47 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/fix/slink/FixSLinksPatch.kt @@ -0,0 +1,50 @@ +package app.revanced.patches.reddit.customclients.boostforreddit.fix.slink + +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.util.smali.ExternalLabel +import app.revanced.patches.reddit.customclients.RESOLVE_S_LINK_METHOD +import app.revanced.patches.reddit.customclients.SET_ACCESS_TOKEN_METHOD +import app.revanced.patches.reddit.customclients.boostforreddit.misc.extension.sharedExtensionPatch +import app.revanced.patches.reddit.customclients.fixSLinksPatch + +const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/boostforreddit/FixSLinksPatch;" + +@Suppress("unused") +val fixSlinksPatch = fixSLinksPatch( + extensionPatch = sharedExtensionPatch, +) { + compatibleWith("com.rubenmayayo.reddit") + + execute { + // region Patch navigation handler. + + handleNavigationFingerprint.method.apply { + val urlRegister = "p1" + val tempRegister = "v1" + + addInstructionsWithLabels( + 0, + """ + invoke-static { $urlRegister }, $EXTENSION_CLASS_DESCRIPTOR->$RESOLVE_S_LINK_METHOD + move-result $tempRegister + if-eqz $tempRegister, :continue + return $tempRegister + """, + ExternalLabel("continue", getInstruction(0)), + ) + } + + // endregion + + // region Patch set access token. + + getOAuthAccessTokenFingerprint.method.addInstruction( + 3, + "invoke-static { v0 }, $EXTENSION_CLASS_DESCRIPTOR->$SET_ACCESS_TOKEN_METHOD", + ) + + // endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/misc/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/misc/extension/SharedExtensionPatch.kt new file mode 100644 index 000000000..3d92d142b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/misc/extension/SharedExtensionPatch.kt @@ -0,0 +1,6 @@ +package app.revanced.patches.reddit.customclients.boostforreddit.misc.extension + +import app.revanced.patches.reddit.customclients.boostforreddit.misc.extension.hooks.initHook +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch + +val sharedExtensionPatch = sharedExtensionPatch(initHook) diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/misc/extension/hooks/InitHook.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/misc/extension/hooks/InitHook.kt new file mode 100644 index 000000000..1e7e4e2e8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/boostforreddit/misc/extension/hooks/InitHook.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.reddit.customclients.boostforreddit.misc.extension.hooks + +import app.revanced.patches.shared.misc.extension.extensionHook + +internal val initHook = extensionHook( + insertIndexResolver = { 1 }, +) { + custom { method, _ -> + method.definingClass == "Lcom/rubenmayayo/reddit/MyApplication;" && method.name == "onCreate" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/api/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/api/Fingerprints.kt new file mode 100644 index 000000000..4bce1362c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/api/Fingerprints.kt @@ -0,0 +1,7 @@ +package app.revanced.patches.reddit.customclients.infinityforreddit.api + +import app.revanced.patcher.fingerprint + +internal val apiUtilsFingerprint = fingerprint { + strings("native-lib") +} \ No newline at end of file diff --git a/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/api/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/api/SpoofClientPatch.kt similarity index 59% rename from src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/api/SpoofClientPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/api/SpoofClientPatch.kt index 0565bfefa..d19ddbfed 100644 --- a/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/api/SpoofClientPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/api/SpoofClientPatch.kt @@ -1,24 +1,19 @@ package app.revanced.patches.reddit.customclients.infinityforreddit.api -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprintResult import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable import app.revanced.patcher.util.smali.toInstructions -import app.revanced.patches.reddit.customclients.BaseSpoofClientPatch -import app.revanced.patches.reddit.customclients.infinityforreddit.api.fingerprints.APIUtilsFingerprint +import app.revanced.patches.reddit.customclients.spoofClientPatch import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethodImplementation -@Suppress("unused") -object SpoofClientPatch : BaseSpoofClientPatch( - redirectUri = "infinity://localhost", - clientIdFingerprints = setOf(APIUtilsFingerprint), - compatiblePackages = setOf(CompatiblePackage("ml.docilealligator.infinityforreddit")) -) { - override fun Set.patchClientId(context: BytecodeContext) { - first().mutableClass.methods.apply { +val spoofClientPatch = spoofClientPatch(redirectUri = "infinity://localhost") { clientIdOption -> + compatibleWith("ml.docilealligator.infinityforreddit") + + val clientId by clientIdOption + + execute { + apiUtilsFingerprint.classDef.methods.apply { val getClientIdMethod = single { it.name == "getId" }.also(::remove) val newGetClientIdMethod = ImmutableMethod( @@ -26,7 +21,7 @@ object SpoofClientPatch : BaseSpoofClientPatch( getClientIdMethod.name, null, getClientIdMethod.returnType, - AccessFlags.PUBLIC or AccessFlags.STATIC, + AccessFlags.PUBLIC.value or AccessFlags.STATIC.value, null, null, ImmutableMethodImplementation( diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/subscription/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/subscription/Fingerprints.kt new file mode 100644 index 000000000..36fe06279 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/subscription/Fingerprints.kt @@ -0,0 +1,15 @@ +package app.revanced.patches.reddit.customclients.infinityforreddit.subscription + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal + +internal val billingClientOnServiceConnectedFingerprint = fingerprint { + strings("Billing service connected") +} + +internal val startSubscriptionActivityFingerprint = fingerprint { + literal { + // Intent start flag only used in the subscription activity + 0x10008000 + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/subscription/UnlockSubscriptionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/subscription/UnlockSubscriptionPatch.kt new file mode 100644 index 000000000..ab9b07c1b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/subscription/UnlockSubscriptionPatch.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.reddit.customclients.infinityforreddit.subscription + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.reddit.customclients.infinityforreddit.api.spoofClientPatch +import app.revanced.util.returnEarly + +@Suppress("unused") +val unlockSubscriptionPatch = bytecodePatch( + name = "Unlock subscription", + description = "Unlocks the subscription feature but requires a custom client ID.", +) { + dependsOn(spoofClientPatch) + + compatibleWith("ml.docilealligator.infinityforreddit") + + execute { + setOf( + startSubscriptionActivityFingerprint, + billingClientOnServiceConnectedFingerprint, + ).forEach { it.method.returnEarly() } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/ads/DisableAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/ads/DisableAdsPatch.kt new file mode 100644 index 000000000..0c7508758 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/ads/DisableAdsPatch.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.reddit.customclients.joeyforreddit.ads + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.reddit.customclients.joeyforreddit.detection.piracy.disablePiracyDetectionPatch + +@Suppress("unused") +val disableAdsPatch = bytecodePatch( + name = "Disable ads", +) { + dependsOn(disablePiracyDetectionPatch) + + compatibleWith("o.o.joey") + + execute { + isAdFreeUserFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/ads/Fingerprints.kt new file mode 100644 index 000000000..465faf120 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/ads/Fingerprints.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.reddit.customclients.joeyforreddit.ads + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val isAdFreeUserFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("Z") + strings("AD_FREE_USER") +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/api/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/api/Fingerprints.kt new file mode 100644 index 000000000..e6c591748 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/api/Fingerprints.kt @@ -0,0 +1,28 @@ +package app.revanced.patches.reddit.customclients.joeyforreddit.api + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val authUtilityUserAgentFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Ljava/lang/String;") + opcodes(Opcode.APUT_OBJECT) + custom { method, classDef -> + classDef.sourceFile == "AuthUtility.java" + } +} + +internal val getClientIdFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("L") + opcodes( + Opcode.CONST, // R.string.valuable_cid + Opcode.INVOKE_STATIC, // StringMaster.decrypt + Opcode.MOVE_RESULT_OBJECT, + Opcode.RETURN_OBJECT + ) + custom { _, classDef -> + classDef.sourceFile == "AuthUtility.java" + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/api/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/api/SpoofClientPatch.kt new file mode 100644 index 000000000..44d7f6e3e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/api/SpoofClientPatch.kt @@ -0,0 +1,48 @@ +package app.revanced.patches.reddit.customclients.joeyforreddit.api + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions +import app.revanced.patches.reddit.customclients.joeyforreddit.detection.piracy.disablePiracyDetectionPatch +import app.revanced.patches.reddit.customclients.spoofClientPatch + +val spoofClientPatch = spoofClientPatch(redirectUri = "https://127.0.0.1:65023/authorize_callback") { clientIdOption -> + dependsOn(disablePiracyDetectionPatch) + + compatibleWith( + "o.o.joey", + "o.o.joey.pro", + "o.o.joey.dev", + ) + + val clientId by clientIdOption + + execute { + // region Patch client id. + + getClientIdFingerprint.method.addInstructions( + 0, + """ + const-string v0, "$clientId" + return-object v0 + """, + ) + + // endregion + + // region Patch user agent. + + // Use a random user agent. + val randomName = (0..100000).random() + val userAgent = "$randomName:app.revanced.$randomName:v1.0.0 (by /u/revanced)" + + authUtilityUserAgentFingerprint.method.replaceInstructions( + 0, + """ + const-string v0, "$userAgent" + return-object v0 + """, + ) + + // endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/detection/piracy/DisablePiracyDetectionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/detection/piracy/DisablePiracyDetectionPatch.kt new file mode 100644 index 000000000..a6871dbc0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/detection/piracy/DisablePiracyDetectionPatch.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.reddit.customclients.joeyforreddit.detection.piracy + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +val disablePiracyDetectionPatch = bytecodePatch { + + execute { + piracyDetectionFingerprint.method.addInstruction(0, "return-void") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/detection/piracy/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/detection/piracy/Fingerprints.kt new file mode 100644 index 000000000..76343a530 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/joeyforreddit/detection/piracy/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.reddit.customclients.joeyforreddit.detection.piracy + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val piracyDetectionFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC) + returns("V") + opcodes( + Opcode.NEW_INSTANCE, + Opcode.CONST_16, + Opcode.CONST_WIDE_16, + Opcode.INVOKE_DIRECT, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID + ) + custom { _, classDef -> + classDef.endsWith("ProcessLifeCyleListener;") + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/redditisfun/api/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/redditisfun/api/Fingerprints.kt new file mode 100644 index 000000000..14d0f4766 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/redditisfun/api/Fingerprints.kt @@ -0,0 +1,31 @@ +package app.revanced.patches.reddit.customclients.redditisfun.api + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +fun baseClientIdFingerprint(string: String) = fingerprint { + strings("yyOCBp.RHJhDKd", string) +} + +internal val basicAuthorizationFingerprint = baseClientIdFingerprint( + string = "fJOxVwBUyo*=f:.patchClientId(context: BytecodeContext) { + +val spoofClientPatch = spoofClientPatch(redirectUri = "redditisfun://auth") { clientIdOption -> + compatibleWith( + "com.andrewshu.android.reddit", + "com.andrewshu.android.redditdonation", + ) + + val clientId by clientIdOption + + execute { + // region Patch client id. + /** * Replaces a one register instruction with a const-string instruction * at the index returned by [getReplacementIndex]. * * @param string The string to replace the instruction with. * @param getReplacementIndex A function that returns the index of the instruction to replace - * using the [StringMatch] list from the [MethodFingerprintResult]. + * using the [Match.StringMatch] list from the [Match]. */ - fun MethodFingerprintResult.replaceWith( + fun Fingerprint.replaceWith( string: String, - getReplacementIndex: List.() -> Int, - ) = mutableMethod.apply { - val replacementIndex = scanResult.stringsScanResult!!.matches.getReplacementIndex() + getReplacementIndex: List.() -> Int, + ) = method.apply { + val replacementIndex = stringMatches!!.getReplacementIndex() val clientIdRegister = getInstruction(replacementIndex).registerA replaceInstruction(replacementIndex, "const-string v$clientIdRegister, \"$string\"") } // Patch OAuth authorization. - first().replaceWith(clientId!!) { first().index + 4 } + buildAuthorizationStringFingerprint.replaceWith(clientId!!) { first().index + 4 } // Path basic authorization. - last().replaceWith("$clientId:") { last().index + 7 } - } + basicAuthorizationFingerprint.replaceWith("$clientId:") { last().index + 7 } + + // endregion + + // region Patch user agent. - override fun Set.patchUserAgent(context: BytecodeContext) { // Use a random user agent. val randomName = (0..100000).random() val userAgent = "$randomName:app.revanced.$randomName:v1.0.0 (by /u/revanced)" - first().mutableMethod.addInstructions( + getUserAgentFingerprint.method.addInstructions( 0, """ const-string v0, "$userAgent" return-object v0 """, ) - } - override fun Set.patchMiscellaneous(context: BytecodeContext) { + // endregion + + // region Patch miscellaneous. + // Reddit messed up and does not append a redirect uri to the authorization url to old.reddit.com/login. // Replace old.reddit.com with ssl.reddit.com to fix this. - BuildAuthorizationStringFingerprint.result!!.mutableMethod.apply { + buildAuthorizationStringFingerprint.method.apply { val index = indexOfFirstInstructionOrThrow { getReference()?.contains("old.reddit.com") == true } @@ -78,5 +79,7 @@ object SpoofClientPatch : BaseSpoofClientPatch( "const-string v$targetRegister, \"https://ssl.reddit.com/api/v1/authorize.compact\"", ) } + + // endregion } } diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/relayforreddit/api/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/relayforreddit/api/Fingerprints.kt new file mode 100644 index 000000000..263602f73 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/relayforreddit/api/Fingerprints.kt @@ -0,0 +1,26 @@ +package app.revanced.patches.reddit.customclients.relayforreddit.api + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal fun baseClientIdFingerprint(string: String) = fingerprint { + strings("dj-xCIZQYiLbEg", string) +} + +internal val getLoggedInBearerTokenFingerprint = baseClientIdFingerprint("authorization_code") + +internal val getLoggedOutBearerTokenFingerprint = baseClientIdFingerprint("https://oauth.reddit.com/grants/installed_client") + +internal val getRefreshTokenFingerprint = baseClientIdFingerprint("refresh_token") + +internal val loginActivityClientIdFingerprint = baseClientIdFingerprint("&duration=permanent") + +internal val redditCheckDisableAPIFingerprint = fingerprint { + opcodes(Opcode.IF_EQZ) + strings("Reddit Disabled") +} + +internal val setRemoteConfigFingerprint = fingerprint { + parameters("Lcom/google/firebase/remoteconfig/FirebaseRemoteConfig;") + strings("reddit_oauth_url") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/relayforreddit/api/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/relayforreddit/api/SpoofClientPatch.kt new file mode 100644 index 000000000..854b7cfa5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/relayforreddit/api/SpoofClientPatch.kt @@ -0,0 +1,57 @@ +package app.revanced.patches.reddit.customclients.relayforreddit.api + +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.reddit.customclients.spoofClientPatch +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction10t +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21t +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +val spoofClientPatch = spoofClientPatch(redirectUri = "dbrady://relay") { + compatibleWith( + "free.reddit.news", + "reddit.news", + ) + + val clientId by it + + execute { + // region Patch client id. + + setOf( + loginActivityClientIdFingerprint, + getLoggedInBearerTokenFingerprint, + getLoggedOutBearerTokenFingerprint, + getRefreshTokenFingerprint, + ).forEach { fingerprint -> + val clientIdIndex = fingerprint.stringMatches!!.first().index + fingerprint.method.apply { + val clientIdRegister = getInstruction(clientIdIndex).registerA + + fingerprint.method.replaceInstruction( + clientIdIndex, + "const-string v$clientIdRegister, \"$clientId\"", + ) + } + } + + // endregion + + // region Patch miscellaneous. + + // Do not load remote config which disables OAuth login remotely. + setRemoteConfigFingerprint.method.addInstructions(0, "return-void") + + // Prevent OAuth login being disabled remotely. + val checkIsOAuthRequestIndex = redditCheckDisableAPIFingerprint.patternMatch!!.startIndex + + redditCheckDisableAPIFingerprint.method.apply { + val returnNextChain = getInstruction(checkIsOAuthRequestIndex).target + replaceInstruction(checkIsOAuthRequestIndex, BuilderInstruction10t(Opcode.GOTO, returnNextChain)) + } + + // endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/slide/api/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/slide/api/Fingerprints.kt new file mode 100644 index 000000000..4ff8be461 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/slide/api/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.reddit.customclients.slide.api + +import app.revanced.patcher.fingerprint + +internal val getClientIdFingerprint = fingerprint { + custom { method, classDef -> + if (!classDef.endsWith("Credentials;")) return@custom false + + method.name == "getClientId" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/slide/api/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/slide/api/SpoofClientPatch.kt new file mode 100644 index 000000000..62f0ccf38 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/slide/api/SpoofClientPatch.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.reddit.customclients.slide.api + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patches.reddit.customclients.spoofClientPatch + +val spoofClientPatch = spoofClientPatch(redirectUri = "http://www.ccrama.me") { clientIdOption -> + compatibleWith("me.ccrama.redditslide") + + val clientId by clientIdOption + + execute { + getClientIdFingerprint.method.addInstructions( + 0, + """ + const-string v0, "$clientId" + return-object v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/ads/DisableAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/ads/DisableAdsPatch.kt new file mode 100644 index 000000000..f210a6adb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/ads/DisableAdsPatch.kt @@ -0,0 +1,15 @@ +package app.revanced.patches.reddit.customclients.sync.ads + +import app.revanced.patcher.patch.BytecodePatchBuilder +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.returnEarly + +fun disableAdsPatch(block: BytecodePatchBuilder.() -> Unit = {}) = bytecodePatch( + name = "Disable ads", +) { + execute { + isAdsEnabledFingerprint.method.returnEarly() + } + + block() +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/ads/Fingerprints.kt new file mode 100644 index 000000000..e055493bd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/ads/Fingerprints.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.reddit.customclients.sync.ads + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val isAdsEnabledFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Z") + strings("SyncIapHelper") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/detection/piracy/DisablePiracyDetectionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/detection/piracy/DisablePiracyDetectionPatch.kt new file mode 100644 index 000000000..fa797c693 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/detection/piracy/DisablePiracyDetectionPatch.kt @@ -0,0 +1,15 @@ +package app.revanced.patches.reddit.customclients.sync.detection.piracy + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +val disablePiracyDetectionPatch = bytecodePatch( + description = "Disables detection of modified versions.", +) { + + execute { + // Do not throw an error if the fingerprint is not resolved. + // This is fine because new versions of the target app do not need this patch. + piracyDetectionFingerprint.method.addInstruction(0, "return-void") + } +} diff --git a/src/main/kotlin/app/revanced/patches/reddit/customclients/syncforreddit/detection/piracy/fingerprints/PiracyDetectionFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/detection/piracy/Fingerprints.kt similarity index 55% rename from src/main/kotlin/app/revanced/patches/reddit/customclients/syncforreddit/detection/piracy/fingerprints/PiracyDetectionFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/detection/piracy/Fingerprints.kt index f7cbc8771..a01be33c9 100644 --- a/src/main/kotlin/app/revanced/patches/reddit/customclients/syncforreddit/detection/piracy/fingerprints/PiracyDetectionFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/detection/piracy/Fingerprints.kt @@ -1,28 +1,27 @@ -package app.revanced.patches.reddit.customclients.syncforreddit.detection.piracy.fingerprints +package app.revanced.patches.reddit.customclients.sync.detection.piracy -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags -import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint -internal object PiracyDetectionFingerprint : MethodFingerprint( - returnType = "V", - accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL, - opcodes = listOf( +internal val piracyDetectionFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returns("V") + opcodes( Opcode.NEW_INSTANCE, Opcode.INVOKE_DIRECT, Opcode.NEW_INSTANCE, Opcode.INVOKE_DIRECT, - Opcode.INVOKE_VIRTUAL - ), - customFingerprint = { method, _ -> + Opcode.INVOKE_VIRTUAL, + ) + custom { method, _ -> method.implementation?.instructions?.any { if (it.opcode != Opcode.NEW_INSTANCE) return@any false val reference = (it as ReferenceInstruction).reference reference.toString() == "Lcom/github/javiersantos/piracychecker/PiracyChecker;" - } ?: false + } == true } -) \ No newline at end of file +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforlemmy/ads/DisableAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforlemmy/ads/DisableAdsPatch.kt new file mode 100644 index 000000000..ca74997aa --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforlemmy/ads/DisableAdsPatch.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.reddit.customclients.sync.syncforlemmy.ads + +import app.revanced.patches.reddit.customclients.sync.ads.disableAdsPatch +import app.revanced.patches.reddit.customclients.sync.detection.piracy.disablePiracyDetectionPatch + +@Suppress("unused") +val disableAdsPatch = disableAdsPatch { + dependsOn(disablePiracyDetectionPatch) + + compatibleWith("com.laurencedawson.reddit_sync") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/ads/DisableAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/ads/DisableAdsPatch.kt new file mode 100644 index 000000000..e50158cdd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/ads/DisableAdsPatch.kt @@ -0,0 +1,8 @@ +package app.revanced.patches.reddit.customclients.sync.syncforreddit.ads + +import app.revanced.patches.reddit.customclients.sync.ads.disableAdsPatch + +@Suppress("unused") +val disableAdsPatch = disableAdsPatch { + compatibleWith("io.syncapps.lemmy_sync") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/annoyances/startup/DisableSyncForLemmyBottomSheetPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/annoyances/startup/DisableSyncForLemmyBottomSheetPatch.kt new file mode 100644 index 000000000..0bfbe74d0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/annoyances/startup/DisableSyncForLemmyBottomSheetPatch.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.reddit.customclients.sync.syncforreddit.annoyances.startup + +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val disableSyncForLemmyBottomSheetPatch = bytecodePatch( + name = "Disable Sync for Lemmy bottom sheet", + description = "Disables the bottom sheet at the startup that asks you to signup to \"Sync for Lemmy\".", +) { + compatibleWith( + "com.laurencedawson.reddit_sync"("v23.06.30-13:39"), + "com.laurencedawson.reddit_sync.pro"(), // Version unknown. + "com.laurencedawson.reddit_sync.dev"(), // Version unknown. + ) + + execute { + mainActivityOnCreateFingerprint.method.apply { + val showBottomSheetIndex = implementation!!.instructions.lastIndex - 1 + + removeInstruction(showBottomSheetIndex) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/annoyances/startup/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/annoyances/startup/Fingerprints.kt new file mode 100644 index 000000000..21c788a89 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/annoyances/startup/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.reddit.customclients.sync.syncforreddit.annoyances.startup + +import app.revanced.patcher.fingerprint + +internal val mainActivityOnCreateFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("MainActivity;") && method.name == "onCreate" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/api/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/api/Fingerprints.kt new file mode 100644 index 000000000..c7902b1f4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/api/Fingerprints.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.reddit.customclients.sync.syncforreddit.api + +import app.revanced.patcher.fingerprint + +internal val getAuthorizationStringFingerprint = fingerprint { + strings("authorize.compact?client_id") +} + +internal val getBearerTokenFingerprint = fingerprint { + strings("Basic") +} + +internal val getUserAgentFingerprint = fingerprint { + strings("android:com.laurencedawson.reddit_sync") +} + +internal val imgurImageAPIFingerprint = fingerprint { + strings("https://imgur-apiv3.p.rapidapi.com/3/image") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/api/SpoofClientPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/api/SpoofClientPatch.kt new file mode 100644 index 000000000..3f9cbf9e9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/api/SpoofClientPatch.kt @@ -0,0 +1,86 @@ +package app.revanced.patches.reddit.customclients.sync.syncforreddit.api + +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.reddit.customclients.spoofClientPatch +import app.revanced.patches.reddit.customclients.sync.detection.piracy.disablePiracyDetectionPatch +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 +import java.util.* + +val spoofClientPatch = spoofClientPatch( + redirectUri = "http://redditsync/auth", +) { clientIdOption -> + dependsOn(disablePiracyDetectionPatch) + + compatibleWith( + "com.laurencedawson.reddit_sync", + "com.laurencedawson.reddit_sync.pro", + "com.laurencedawson.reddit_sync.dev", + ) + + val clientId by clientIdOption + + execute { + // region Patch client id. + + getBearerTokenFingerprint.match(getAuthorizationStringFingerprint.originalClassDef).method.apply { + val auth = Base64.getEncoder().encodeToString("$clientId:".toByteArray(Charsets.UTF_8)) + addInstructions( + 0, + """ + const-string v0, "Basic $auth" + return-object v0 + """, + ) + val occurrenceIndex = + getAuthorizationStringFingerprint.stringMatches!!.first().index + + getAuthorizationStringFingerprint.method.apply { + val authorizationStringInstruction = getInstruction(occurrenceIndex) + val targetRegister = (authorizationStringInstruction as OneRegisterInstruction).registerA + val reference = authorizationStringInstruction.reference as StringReference + + val newAuthorizationUrl = reference.string.replace( + "client_id=.*?&".toRegex(), + "client_id=$clientId&", + ) + + replaceInstruction( + occurrenceIndex, + "const-string v$targetRegister, \"$newAuthorizationUrl\"", + ) + } + } + + // endregion + + // region Patch user agent. + + // Use a random user agent. + val randomName = (0..100000).random() + val userAgent = "$randomName:app.revanced.$randomName:v1.0.0 (by /u/revanced)" + + imgurImageAPIFingerprint.method.replaceInstruction( + 0, + """ + const-string v0, "$userAgent" + return-object v0 + """, + ) + + // endregion + + // region Patch Imgur API URL. + + val apiUrlIndex = getUserAgentFingerprint.stringMatches!!.first().index + getUserAgentFingerprint.method.replaceInstruction( + apiUrlIndex, + "const-string v1, \"https://api.imgur.com/3/image\"", + ) + + // endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/extension/SharedExtensionPatch.kt new file mode 100644 index 000000000..67f02676f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/extension/SharedExtensionPatch.kt @@ -0,0 +1,6 @@ +package app.revanced.patches.reddit.customclients.sync.syncforreddit.extension + +import app.revanced.patches.reddit.customclients.sync.syncforreddit.extension.hooks.initHook +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch + +val sharedExtensionPatch = sharedExtensionPatch(initHook) diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/extension/hooks/InitHook.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/extension/hooks/InitHook.kt new file mode 100644 index 000000000..00244b4df --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/extension/hooks/InitHook.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.reddit.customclients.sync.syncforreddit.extension.hooks + +import app.revanced.patches.shared.misc.extension.extensionHook + +internal val initHook = extensionHook( + insertIndexResolver = { 1 }, // Insert after call to super class. +) { + custom { method, classDef -> + method.name == "onCreate" && classDef.type == "Lcom/laurencedawson/reddit_sync/RedditApplication;" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/slink/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/slink/Fingerprints.kt new file mode 100644 index 000000000..f7287fcc3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/slink/Fingerprints.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.reddit.customclients.sync.syncforreddit.fix.slink + +import app.revanced.patcher.fingerprint + +internal val linkHelperOpenLinkFingerprint = fingerprint { + strings("Link title: ") +} + +internal val setAuthorizationHeaderFingerprint = fingerprint { + returns("Ljava/util/HashMap;") + strings("Authorization", "bearer ") + custom { method, _ -> method.definingClass == "Lcom/laurencedawson/reddit_sync/singleton/a;" } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/slink/FixSLinksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/slink/FixSLinksPatch.kt new file mode 100644 index 000000000..a640936a2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/slink/FixSLinksPatch.kt @@ -0,0 +1,56 @@ +package app.revanced.patches.reddit.customclients.sync.syncforreddit.fix.slink + +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.util.smali.ExternalLabel +import app.revanced.patches.reddit.customclients.RESOLVE_S_LINK_METHOD +import app.revanced.patches.reddit.customclients.SET_ACCESS_TOKEN_METHOD +import app.revanced.patches.reddit.customclients.boostforreddit.fix.slink.getOAuthAccessTokenFingerprint +import app.revanced.patches.reddit.customclients.boostforreddit.fix.slink.handleNavigationFingerprint +import app.revanced.patches.reddit.customclients.fixSLinksPatch +import app.revanced.patches.reddit.customclients.sync.syncforreddit.extension.sharedExtensionPatch + +const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/syncforreddit/FixSLinksPatch;" + +@Suppress("unused") +val fixSLinksPatch = fixSLinksPatch( + extensionPatch = sharedExtensionPatch, +) { + compatibleWith( + "com.laurencedawson.reddit_sync", + "com.laurencedawson.reddit_sync.pro", + "com.laurencedawson.reddit_sync.dev", + ) + + execute { + // region Patch navigation handler. + + handleNavigationFingerprint.method.apply { + val urlRegister = "p3" + val tempRegister = "v2" + + addInstructionsWithLabels( + 0, + """ + invoke-static { $urlRegister }, $EXTENSION_CLASS_DESCRIPTOR->$RESOLVE_S_LINK_METHOD + move-result $tempRegister + if-eqz $tempRegister, :continue + return $tempRegister + """, + ExternalLabel("continue", getInstruction(0)), + ) + } + + // endregion + + // region Patch set access token. + + getOAuthAccessTokenFingerprint.method.addInstruction( + 0, + "invoke-static { p0 }, $EXTENSION_CLASS_DESCRIPTOR->$SET_ACCESS_TOKEN_METHOD", + ) + + // endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/user/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/user/Fingerprints.kt new file mode 100644 index 000000000..4bac74de7 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/user/Fingerprints.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.reddit.customclients.sync.syncforreddit.fix.user + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal fun userEndpointFingerprint(source: String, accessFlags: Set? = null) = fingerprint { + strings("u/") + custom { _, classDef -> classDef.sourceFile == source } + accessFlags(*accessFlags?.toTypedArray() ?: return@fingerprint) +} + +internal val oAuthFriendRequestFingerprint = userEndpointFingerprint( + "OAuthFriendRequest.java", +) + +internal val oAuthUnfriendRequestFingerprint = userEndpointFingerprint( + "OAuthUnfriendRequest.java", +) + +internal val oAuthUserIdRequestFingerprint = userEndpointFingerprint( + "OAuthUserIdRequest.java", +) + +internal val oAuthUserInfoRequestFingerprint = userEndpointFingerprint( + "OAuthUserInfoRequest.java", +) + +internal val oAuthSubredditInfoRequestConstructorFingerprint = userEndpointFingerprint( + "OAuthSubredditInfoRequest.java", + setOf(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR), +) + +internal val oAuthSubredditInfoRequestHelperFingerprint = userEndpointFingerprint( + "OAuthSubredditInfoRequest.java", + setOf(AccessFlags.PRIVATE, AccessFlags.STATIC), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/user/UseUserEndpointPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/user/UseUserEndpointPatch.kt new file mode 100644 index 000000000..2d46c284c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/sync/syncforreddit/fix/user/UseUserEndpointPatch.kt @@ -0,0 +1,46 @@ +package app.revanced.patches.reddit.customclients.sync.syncforreddit.fix.user + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.StringReference + +@Suppress("unused") +val useUserEndpointPatch = bytecodePatch( + name = "Use /user/ endpoint", + description = "Replaces the deprecated endpoint for viewing user profiles /u with /user, that used to fix a bug.", + use = false, + +) { + compatibleWith( + "com.laurencedawson.reddit_sync", + "com.laurencedawson.reddit_sync.pro", + "com.laurencedawson.reddit_sync.dev", + ) + + execute { + arrayOf( + oAuthFriendRequestFingerprint, + oAuthSubredditInfoRequestConstructorFingerprint, + oAuthSubredditInfoRequestHelperFingerprint, + oAuthUnfriendRequestFingerprint, + oAuthUserIdRequestFingerprint, + oAuthUserInfoRequestFingerprint, + ).map { fingerprint -> + fingerprint.stringMatches!!.first().index to fingerprint.method + }.forEach { (userPathStringIndex, method) -> + val userPathStringInstruction = method.getInstruction(userPathStringIndex) + + val userPathStringRegister = userPathStringInstruction.registerA + val fixedUserPathString = userPathStringInstruction.getReference()!! + .string.replace("u/", "user/") + + method.replaceInstruction( + userPathStringIndex, + "const-string v$userPathStringRegister, \"${fixedUserPathString}\"", + ) + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/reddit/customclients/syncforreddit/fix/video/fingerprints/ParseRedditVideoNetworkResponseFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/syncforreddit/fix/video/Fingerprints.kt similarity index 53% rename from src/main/kotlin/app/revanced/patches/reddit/customclients/syncforreddit/fix/video/fingerprints/ParseRedditVideoNetworkResponseFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/reddit/customclients/syncforreddit/fix/video/Fingerprints.kt index 1a06cd54d..3123563ee 100644 --- a/src/main/kotlin/app/revanced/patches/reddit/customclients/syncforreddit/fix/video/fingerprints/ParseRedditVideoNetworkResponseFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/syncforreddit/fix/video/Fingerprints.kt @@ -1,16 +1,16 @@ -package app.revanced.patches.reddit.customclients.syncforreddit.fix.video.fingerprints +package app.revanced.patches.reddit.customclients.syncforreddit.fix.video -import app.revanced.patcher.fingerprint.MethodFingerprint +import app.revanced.patcher.fingerprint import com.android.tools.smali.dexlib2.Opcode -internal object ParseRedditVideoNetworkResponseFingerprint : MethodFingerprint( - opcodes = listOf( +internal val parseRedditVideoNetworkResponseFingerprint = fingerprint { + opcodes( Opcode.NEW_INSTANCE, Opcode.IGET_OBJECT, Opcode.INVOKE_DIRECT, - Opcode.CONST_WIDE_32 - ), - customFingerprint = { methodDef, classDef -> + Opcode.CONST_WIDE_32, + ) + custom { methodDef, classDef -> classDef.sourceFile == "RedditVideoRequest.java" && methodDef.name == "parseNetworkResponse" } -) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/syncforreddit/fix/video/FixVideoDownloadsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/syncforreddit/fix/video/FixVideoDownloadsPatch.kt new file mode 100644 index 000000000..9990af83f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/customclients/syncforreddit/fix/video/FixVideoDownloadsPatch.kt @@ -0,0 +1,56 @@ +package app.revanced.patches.reddit.customclients.syncforreddit.fix.video + +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.customclients.sync.syncforreddit.extension.sharedExtensionPatch +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/syncforreddit/FixRedditVideoDownloadPatch;" +private const val GET_LINKS_METHOD = "getLinks([B)[Ljava/lang/String;" + +@Suppress("unused") +val fixVideoDownloadsPatch = bytecodePatch( + name = "Fix video downloads", + description = "Fixes a bug in Sync's MPD parser resulting in only the audio-track being saved.", +) { + dependsOn(sharedExtensionPatch) + + compatibleWith( + "com.laurencedawson.reddit_sync", + "com.laurencedawson.reddit_sync.pro", + "com.laurencedawson.reddit_sync.dev", + ) + + execute { + val scanResult = parseRedditVideoNetworkResponseFingerprint.patternMatch!! + val newInstanceIndex = scanResult.startIndex + val invokeDirectIndex = scanResult.endIndex - 1 + + val buildResponseInstruction = + parseRedditVideoNetworkResponseFingerprint.method.getInstruction(invokeDirectIndex) + + parseRedditVideoNetworkResponseFingerprint.method.addInstructions( + newInstanceIndex + 1, + """ + # Get byte array from response. + iget-object v2, p1, Lcom/android/volley/NetworkResponse;->data:[B + + # Parse the videoUrl and audioUrl from the byte array. + invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->$GET_LINKS_METHOD + move-result-object v2 + + # Get videoUrl (Index 0). + const/4 v5, 0x0 + aget-object v${buildResponseInstruction.registerE}, v2, v5 + + # Get audioUrl (Index 1). + const/4 v6, 0x1 + aget-object v${buildResponseInstruction.registerF}, v2, v6 + + # Register E and F are used to build the response. + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/disablescreenshotpopup/DisableScreenshotPopupPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/disablescreenshotpopup/DisableScreenshotPopupPatch.kt new file mode 100644 index 000000000..84ee7e2fd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/disablescreenshotpopup/DisableScreenshotPopupPatch.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.reddit.layout.disablescreenshotpopup + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val disableScreenshotPopupPatch = bytecodePatch( + name = "Disable screenshot popup", + description = "Disables the popup that shows up when taking a screenshot.", +) { + compatibleWith("com.reddit.frontpage") + + execute { + disableScreenshotPopupFingerprint.method.addInstruction(0, "return-void") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/disablescreenshotpopup/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/disablescreenshotpopup/Fingerprints.kt new file mode 100644 index 000000000..09fe16247 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/disablescreenshotpopup/Fingerprints.kt @@ -0,0 +1,15 @@ +package app.revanced.patches.reddit.layout.disablescreenshotpopup + +import app.revanced.patcher.fingerprint + +internal val disableScreenshotPopupFingerprint = fingerprint { + returns("V") + parameters("Landroidx/compose/runtime/", "I") + custom { method, classDef -> + if (!classDef.endsWith("\$ScreenshotTakenBannerKt\$lambda-1\$1;")) { + return@custom false + } + + method.name == "invoke" + } +} 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..2eac1cbe2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/premiumicon/Fingerprints.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.reddit.layout.premiumicon + +import app.revanced.patcher.fingerprint + +internal val hasPremiumIconAccessFingerprint = fingerprint { + returns("Z") + custom { method, classDef -> + classDef.endsWith("MyAccount;") && method.name == "isPremiumSubscriber" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/layout/premiumicon/UnlockPremiumIconPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/premiumicon/UnlockPremiumIconPatch.kt new file mode 100644 index 000000000..bde2b52b9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/layout/premiumicon/UnlockPremiumIconPatch.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.reddit.layout.premiumicon + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val unlockPremiumIconPatch = bytecodePatch( + name = "Unlock premium Reddit icons", + description = "Unlocks the premium Reddit icons.", +) { + compatibleWith("com.reddit.frontpage") + + execute { + hasPremiumIconAccessFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/reddit/misc/extension/ExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/extension/ExtensionPatch.kt new file mode 100644 index 000000000..db449a93e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/extension/ExtensionPatch.kt @@ -0,0 +1,5 @@ +package app.revanced.patches.reddit.misc.extension + +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch + +val sharedExtensionPatch = sharedExtensionPatch() 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..3381fd2bb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/tracking/url/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.reddit.misc.tracking.url + +import app.revanced.patcher.fingerprint + +internal val shareLinkFormatterFingerprint = fingerprint { + custom { _, classDef -> + classDef.startsWith("Lcom/reddit/sharing/") && classDef.sourceFile == "UrlUtil.kt" + } +} \ No newline at end of file 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..26ed42660 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/reddit/misc/tracking/url/SanitizeUrlQueryPatch.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.reddit.misc.tracking.url + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val sanitizeUrlQueryPatch = bytecodePatch( + name = "Sanitize sharing links", + description = "Removes (tracking) query parameters from the URLs when sharing links.", +) { + compatibleWith("com.reddit.frontpage") + + execute { + shareLinkFormatterFingerprint.method.addInstructions( + 0, + "return-object p0", + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/serviceportalbund/detection/root/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/serviceportalbund/detection/root/Fingerprints.kt new file mode 100644 index 000000000..f7efe3103 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/serviceportalbund/detection/root/Fingerprints.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.serviceportalbund.detection.root + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val rootDetectionFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("V") + custom { _, classDef -> + classDef.endsWith("/DeviceIntegrityCheck;") + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/serviceportalbund/detection/root/RootDetectionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/serviceportalbund/detection/root/RootDetectionPatch.kt new file mode 100644 index 000000000..43ebaa19f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/serviceportalbund/detection/root/RootDetectionPatch.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.serviceportalbund.detection.root + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val rootDetectionPatch = bytecodePatch( + name = "Remove root detection", + description = "Removes the check for root permissions and unlocked bootloader.", +) { + compatibleWith("at.gv.bka.serviceportal") + + execute { + rootDetectionFingerprint.method.addInstruction(0, "return-void") + } +} 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..df927dd4a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.shared + +import app.revanced.patcher.fingerprint + +internal val castContextFetchFingerprint = fingerprint { + strings("Error fetching CastContext.") +} + +internal val primeMethodFingerprint = fingerprint { + strings("com.google.android.GoogleCamera", "com.android.vending") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/checks/BaseCheckEnvironmentPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/checks/BaseCheckEnvironmentPatch.kt new file mode 100644 index 000000000..3ede5d48a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/checks/BaseCheckEnvironmentPatch.kt @@ -0,0 +1,106 @@ +package app.revanced.patches.shared.misc.checks + +import android.os.Build.* +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.Patch +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableEncodedValue +import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableLongEncodedValue +import app.revanced.patcher.util.proxy.mutableTypes.encodedValue.MutableStringEncodedValue +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import com.android.tools.smali.dexlib2.immutable.value.ImmutableLongEncodedValue +import com.android.tools.smali.dexlib2.immutable.value.ImmutableStringEncodedValue +import java.nio.charset.StandardCharsets +import java.security.MessageDigest +import kotlin.io.encoding.Base64 +import kotlin.io.encoding.ExperimentalEncodingApi + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/shared/checks/CheckEnvironmentPatch;" + +fun checkEnvironmentPatch( + mainActivityOnCreateFingerprint: Fingerprint, + extensionPatch: Patch<*>, + vararg compatiblePackages: String, +) = bytecodePatch( + description = "Checks, if the application was patched by, otherwise warns the user.", +) { + compatibleWith(*compatiblePackages) + + dependsOn( + extensionPatch, + addResourcesPatch, + ) + + execute { + addResources("shared", "misc.checks.checkEnvironmentPatch") + + fun setPatchInfo() { + fun Fingerprint.setClassFields(vararg fieldNameValues: Pair) { + val fieldNameValueMap = mapOf(*fieldNameValues) + + classDef.fields.forEach { field -> + field.initialValue = fieldNameValueMap[field.name] ?: return@forEach + } + } + + patchInfoFingerprint.setClassFields( + "PATCH_TIME" to System.currentTimeMillis().encoded, + ) + + fun setBuildInfo() { + patchInfoBuildFingerprint.setClassFields( + "PATCH_BOARD" to BOARD.encodedAndHashed, + "PATCH_BOOTLOADER" to BOOTLOADER.encodedAndHashed, + "PATCH_BRAND" to BRAND.encodedAndHashed, + "PATCH_CPU_ABI" to CPU_ABI.encodedAndHashed, + "PATCH_CPU_ABI2" to CPU_ABI2.encodedAndHashed, + "PATCH_DEVICE" to DEVICE.encodedAndHashed, + "PATCH_DISPLAY" to DISPLAY.encodedAndHashed, + "PATCH_FINGERPRINT" to FINGERPRINT.encodedAndHashed, + "PATCH_HARDWARE" to HARDWARE.encodedAndHashed, + "PATCH_HOST" to HOST.encodedAndHashed, + "PATCH_ID" to ID.encodedAndHashed, + "PATCH_MANUFACTURER" to MANUFACTURER.encodedAndHashed, + "PATCH_MODEL" to MODEL.encodedAndHashed, + "PATCH_PRODUCT" to PRODUCT.encodedAndHashed, + "PATCH_RADIO" to RADIO.encodedAndHashed, + "PATCH_TAGS" to TAGS.encodedAndHashed, + "PATCH_TYPE" to TYPE.encodedAndHashed, + "PATCH_USER" to USER.encodedAndHashed, + ) + } + + try { + Class.forName("android.os.Build") + // This only works on Android, + // because it uses Android APIs. + setBuildInfo() + } catch (_: ClassNotFoundException) { + } + } + + fun invokeCheck() = mainActivityOnCreateFingerprint.method.addInstructions( + 0, + "invoke-static/range { p0 .. p0 },$EXTENSION_CLASS_DESCRIPTOR->check(Landroid/app/Activity;)V", + ) + + setPatchInfo() + invokeCheck() + } +} + +@OptIn(ExperimentalEncodingApi::class) +private val String.encodedAndHashed + get() = MutableStringEncodedValue( + ImmutableStringEncodedValue( + Base64.encode( + MessageDigest.getInstance("SHA-1") + .digest(this.toByteArray(StandardCharsets.UTF_8)), + ), + ), + ) + +private val Long.encoded get() = MutableLongEncodedValue(ImmutableLongEncodedValue(this)) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/checks/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/checks/Fingerprints.kt new file mode 100644 index 000000000..0eabd2f54 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/checks/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.shared.misc.checks + +import app.revanced.patcher.fingerprint + +internal val patchInfoFingerprint = fingerprint { + custom { _, classDef -> classDef.type == "Lapp/revanced/extension/shared/checks/PatchInfo;" } +} + +internal val patchInfoBuildFingerprint = fingerprint { + custom { _, classDef -> classDef.type == "Lapp/revanced/extension/shared/checks/PatchInfo\$Build;" } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/extension/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/extension/Fingerprints.kt new file mode 100644 index 000000000..58cc5082f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/extension/Fingerprints.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.shared.misc.extension + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val revancedUtilsPatchesVersionFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Ljava/lang/String;") + parameters() + custom { method, _ -> + method.name == "getPatchesReleaseVersion" && method.definingClass == EXTENSION_CLASS_DESCRIPTOR + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/extension/SharedExtensionPatch.kt new file mode 100644 index 000000000..33c3ddf15 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/extension/SharedExtensionPatch.kt @@ -0,0 +1,96 @@ +package app.revanced.patches.shared.misc.extension + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.FingerprintBuilder +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.fingerprint +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.iface.Method +import java.net.URLDecoder +import java.util.jar.JarFile + +internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/shared/Utils;" + +fun sharedExtensionPatch( + vararg hooks: ExtensionHook, +) = bytecodePatch { + extendWith("extensions/shared.rve") + + execute { + if (classBy { EXTENSION_CLASS_DESCRIPTOR in it.type } == null) { + throw PatchException( + "Shared extension has not been merged yet. This patch can not succeed without merging it.", + ) + } + + hooks.forEach { hook -> hook(EXTENSION_CLASS_DESCRIPTOR) } + + // Modify Utils method to include the patches release version. + revancedUtilsPatchesVersionFingerprint.method.apply { + /** + * @return The file path for the jar this classfile is contained inside. + */ + fun getCurrentJarFilePath(): String { + val className = object {}::class.java.enclosingClass.name.replace('.', '/') + ".class" + val classUrl = object {}::class.java.classLoader.getResource(className) + if (classUrl != null) { + val urlString = classUrl.toString() + + if (urlString.startsWith("jar:file:")) { + val end = urlString.lastIndexOf('!') + + return URLDecoder.decode(urlString.substring("jar:file:".length, end), "UTF-8") + } + } + throw IllegalStateException("Not running from inside a JAR file.") + } + + /** + * @return The value for the manifest entry, + * or "Unknown" if the entry does not exist or is blank. + */ + @Suppress("SameParameterValue") + fun getPatchesManifestEntry(attributeKey: String) = JarFile(getCurrentJarFilePath()).use { jarFile -> + jarFile.manifest.mainAttributes.entries.firstOrNull { it.key.toString() == attributeKey }?.value?.toString() + ?: "Unknown" + } + + val manifestValue = getPatchesManifestEntry("Version") + + addInstructions( + 0, + """ + const-string v0, "$manifestValue" + return-object v0 + """, + ) + } + } +} + +class ExtensionHook internal constructor( + val fingerprint: Fingerprint, + private val insertIndexResolver: ((Method) -> Int), + private val contextRegisterResolver: (Method) -> String, +) { + context(BytecodePatchContext) + operator fun invoke(extensionClassDescriptor: String) { + val insertIndex = insertIndexResolver(fingerprint.method) + val contextRegister = contextRegisterResolver(fingerprint.method) + + fingerprint.method.addInstruction( + insertIndex, + "invoke-static/range { $contextRegister .. $contextRegister }, " + + "$extensionClassDescriptor->setContext(Landroid/content/Context;)V", + ) + } +} + +fun extensionHook( + insertIndexResolver: ((Method) -> Int) = { 0 }, + contextRegisterResolver: (Method) -> String = { "p0" }, + fingerprintBuilderBlock: FingerprintBuilder.() -> Unit, +) = ExtensionHook(fingerprint(block = fingerprintBuilderBlock), insertIndexResolver, contextRegisterResolver) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/fix/verticalscroll/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/fix/verticalscroll/Fingerprints.kt new file mode 100644 index 000000000..80d041ada --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/fix/verticalscroll/Fingerprints.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.shared.misc.fix.verticalscroll + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val canScrollVerticallyFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters() + opcodes( + Opcode.MOVE_RESULT, + Opcode.RETURN, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT + ) + custom { _, classDef -> classDef.endsWith("SwipeRefreshLayout;") } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/fix/verticalscroll/VerticalScrollPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/fix/verticalscroll/VerticalScrollPatch.kt new file mode 100644 index 000000000..a01839c10 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/fix/verticalscroll/VerticalScrollPatch.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.shared.misc.fix.verticalscroll + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +val verticalScrollPatch = bytecodePatch( + description = "Fixes issues with refreshing the feed when the first component is of type EmptyComponent.", +) { + + execute { + canScrollVerticallyFingerprint.method.apply { + val moveResultIndex = canScrollVerticallyFingerprint.patternMatch!!.endIndex + val moveResultRegister = getInstruction(moveResultIndex).registerA + + val insertIndex = moveResultIndex + 1 + addInstruction( + insertIndex, + "const/4 v$moveResultRegister, 0x0", + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/gms/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/gms/Fingerprints.kt new file mode 100644 index 000000000..b5f613d54 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/gms/Fingerprints.kt @@ -0,0 +1,30 @@ +package app.revanced.patches.shared.misc.gms + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +const val GET_GMS_CORE_VENDOR_GROUP_ID_METHOD_NAME = "getGmsCoreVendorGroupId" + +internal val gmsCoreSupportFingerprint = fingerprint { + custom { _, classDef -> + classDef.endsWith("GmsCoreSupport;") + } +} + +internal val googlePlayUtilityFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("I") + parameters("L", "I") + strings( + "This should never happen.", + "MetadataValueReader", + "com.google.android.gms", + ) +} + +internal val serviceCheckFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("V") + parameters("L", "I") + strings("Google Play Services not available") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/gms/GmsCoreSupportPatch.kt new file mode 100644 index 000000000..0e54bb691 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/gms/GmsCoreSupportPatch.kt @@ -0,0 +1,604 @@ +package app.revanced.patches.shared.misc.gms + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.* +import app.revanced.patches.all.misc.packagename.changePackageNamePatch +import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.gms.Constants.ACTIONS +import app.revanced.patches.shared.misc.gms.Constants.AUTHORITIES +import app.revanced.patches.shared.misc.gms.Constants.PERMISSIONS +import app.revanced.util.* +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction21c +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.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 PACKAGE_NAME_REGEX_PATTERN = "^[a-z]\\w*(\\.[a-z]\\w*)+\$" + +/** + * 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 toPackageName The package name to fall back to if no custom package name is specified in patch options. + * @param primeMethodFingerprint The fingerprint of the "prime" method that needs to be patched. + * @param earlyReturnFingerprints The fingerprints of methods that need to be returned early. + * @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, + toPackageName: String, + primeMethodFingerprint: Fingerprint? = null, + earlyReturnFingerprints: Set = setOf(), + mainActivityOnCreateFingerprint: Fingerprint, + extensionPatch: Patch<*>, + gmsCoreSupportResourcePatchFactory: (gmsCoreVendorGroupIdOption: 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)) } + + dependsOn( + changePackageNamePatch, + gmsCoreSupportResourcePatchFactory(gmsCoreVendorGroupIdOption), + 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 { MethodUtil.methodSignaturesMatch(it, 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(packageName: String) { + primeMethodFingerprint!!.method.apply { + var register = 2 + + val index = instructions.indexOfFirst { + if (it.getReference()?.string != fromPackageName) return@indexOfFirst false + + register = (it as OneRegisterInstruction).registerA + return@indexOfFirst true + } + + replaceInstruction(index, "const-string v$register, \"$packageName\"") + } + } + + // endregion + + val packageName = setOrGetFallbackPackageName(toPackageName) + + // 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 + } + + // Specific method that needs to be patched. + primeMethodFingerprint?.let { transformPrimeMethod(packageName) } + + // Return these methods early to prevent the app from crashing. + earlyReturnFingerprints.forEach { it.method.returnEarly() } + serviceCheckFingerprint.method.returnEarly() + + // Google Play Utility is not present in all apps, so we need to check if it's present. + if (googlePlayUtilityFingerprint.methodOrNull != null) { + googlePlayUtilityFingerprint.method.returnEarly() + } + + // 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. + addInstructions( + if (setContextIndex < 0) 0 else setContextIndex + 1, + "invoke-static/range { p0 .. p0 }, Lapp/revanced/extension/shared/GmsCoreSupport;->" + + "checkGmsCore(Landroid/app/Activity;)V", + ) + } + + // Change the vendor of GmsCore in the extension. + gmsCoreSupportFingerprint.classDef.methods + .single { it.name == GET_GMS_CORE_VENDOR_GROUP_ID_METHOD_NAME } + .replaceInstruction(0, "const-string v0, \"$gmsCoreVendorGroupId\"") + + 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( + "com.google.android.c2dm.permission.RECEIVE", + "com.google.android.c2dm.permission.SEND", + "com.google.android.gms.auth.api.phone.permission.SEND", + "com.google.android.gms.permission.AD_ID", + "com.google.android.gms.permission.AD_ID_NOTIFICATION", + "com.google.android.gms.permission.CAR_FUEL", + "com.google.android.gms.permission.CAR_INFORMATION", + "com.google.android.gms.permission.CAR_MILEAGE", + "com.google.android.gms.permission.CAR_SPEED", + "com.google.android.gms.permission.CAR_VENDOR_EXTENSION", + "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", + "com.google.android.gtalkservice.permission.GTALK_SERVICE", + "com.google.android.providers.gsf.permission.READ_GSERVICES", + ) + + /** + * All intent actions. + */ + val ACTIONS = setOf( + "com.google.android.c2dm.intent.RECEIVE", + "com.google.android.c2dm.intent.REGISTER", + "com.google.android.c2dm.intent.REGISTRATION", + "com.google.android.c2dm.intent.UNREGISTER", + "com.google.android.contextmanager.service.ContextManagerService.START", + "com.google.android.gcm.intent.SEND", + "com.google.android.gms.accounts.ACCOUNT_SERVICE", + "com.google.android.gms.accountsettings.ACCOUNT_PREFERENCES_SETTINGS", + "com.google.android.gms.accountsettings.action.BROWSE_SETTINGS", + "com.google.android.gms.accountsettings.action.VIEW_SETTINGS", + "com.google.android.gms.accountsettings.MY_ACCOUNT", + "com.google.android.gms.accountsettings.PRIVACY_SETTINGS", + "com.google.android.gms.accountsettings.SECURITY_SETTINGS", + "com.google.android.gms.ads.gservice.START", + "com.google.android.gms.ads.identifier.service.EVENT_ATTESTATION", + "com.google.android.gms.ads.service.CACHE", + "com.google.android.gms.ads.service.CONSENT_LOOKUP", + "com.google.android.gms.ads.service.HTTP", + "com.google.android.gms.analytics.service.START", + "com.google.android.gms.app.settings.GoogleSettingsLink", + "com.google.android.gms.appstate.service.START", + "com.google.android.gms.appusage.service.START", + "com.google.android.gms.asterism.service.START", + "com.google.android.gms.audiomodem.service.AudioModemService.START", + "com.google.android.gms.audit.service.START", + "com.google.android.gms.auth.account.authapi.START", + "com.google.android.gms.auth.account.authenticator.auto.service.START", + "com.google.android.gms.auth.account.authenticator.chromeos.START", + "com.google.android.gms.auth.account.authenticator.tv.service.START", + "com.google.android.gms.auth.account.data.service.START", + "com.google.android.gms.auth.api.credentials.PICKER", + "com.google.android.gms.auth.api.credentials.service.START", + "com.google.android.gms.auth.api.identity.service.authorization.START", + "com.google.android.gms.auth.api.identity.service.credentialsaving.START", + "com.google.android.gms.auth.api.identity.service.signin.START", + "com.google.android.gms.auth.api.phone.service.InternalService.START", + "com.google.android.gms.auth.api.signin.service.START", + "com.google.android.gms.auth.be.appcert.AppCertService", + "com.google.android.gms.auth.blockstore.service.START", + "com.google.android.gms.auth.config.service.START", + "com.google.android.gms.auth.cryptauth.cryptauthservice.START", + "com.google.android.gms.auth.GOOGLE_SIGN_IN", + "com.google.android.gms.auth.login.LOGIN", + "com.google.android.gms.auth.proximity.devicesyncservice.START", + "com.google.android.gms.auth.proximity.securechannelservice.START", + "com.google.android.gms.auth.proximity.START", + "com.google.android.gms.auth.service.START", + "com.google.android.gms.backup.ACTION_BACKUP_SETTINGS", + "com.google.android.gms.backup.G1_BACKUP", + "com.google.android.gms.backup.G1_RESTORE", + "com.google.android.gms.backup.GMS_MODULE_RESTORE", + "com.google.android.gms.beacon.internal.IBleService.START", + "com.google.android.gms.car.service.START", + "com.google.android.gms.carrierauth.service.START", + "com.google.android.gms.cast.firstparty.START", + "com.google.android.gms.cast.remote_display.service.START", + "com.google.android.gms.cast.service.BIND_CAST_DEVICE_CONTROLLER_SERVICE", + "com.google.android.gms.cast_mirroring.service.START", + "com.google.android.gms.checkin.BIND_TO_SERVICE", + "com.google.android.gms.chromesync.service.START", + "com.google.android.gms.clearcut.service.START", + "com.google.android.gms.common.account.CHOOSE_ACCOUNT", + "com.google.android.gms.common.download.START", + "com.google.android.gms.common.service.START", + "com.google.android.gms.common.telemetry.service.START", + "com.google.android.gms.config.START", + "com.google.android.gms.constellation.service.START", + "com.google.android.gms.credential.manager.service.firstparty.START", + "com.google.android.gms.deviceconnection.service.START", + "com.google.android.gms.drive.ApiService.RESET_AFTER_BOOT", + "com.google.android.gms.drive.ApiService.START", + "com.google.android.gms.drive.ApiService.STOP", + "com.google.android.gms.droidguard.service.INIT", + "com.google.android.gms.droidguard.service.PING", + "com.google.android.gms.droidguard.service.START", + "com.google.android.gms.enterprise.loader.service.START", + "com.google.android.gms.facs.cache.service.START", + "com.google.android.gms.facs.internal.service.START", + "com.google.android.gms.feedback.internal.IFeedbackService", + "com.google.android.gms.fido.credentialstore.internal_service.START", + "com.google.android.gms.fido.fido2.privileged.START", + "com.google.android.gms.fido.fido2.regular.START", + "com.google.android.gms.fido.fido2.zeroparty.START", + "com.google.android.gms.fido.sourcedevice.service.START", + "com.google.android.gms.fido.targetdevice.internal_service.START", + "com.google.android.gms.fido.u2f.privileged.START", + "com.google.android.gms.fido.u2f.thirdparty.START", + "com.google.android.gms.fido.u2f.zeroparty.START", + "com.google.android.gms.fitness.BleApi", + "com.google.android.gms.fitness.ConfigApi", + "com.google.android.gms.fitness.GoalsApi", + "com.google.android.gms.fitness.GoogleFitnessService.START", + "com.google.android.gms.fitness.HistoryApi", + "com.google.android.gms.fitness.InternalApi", + "com.google.android.gms.fitness.RecordingApi", + "com.google.android.gms.fitness.SensorsApi", + "com.google.android.gms.fitness.SessionsApi", + "com.google.android.gms.fonts.service.START", + "com.google.android.gms.freighter.service.START", + "com.google.android.gms.games.internal.connect.service.START", + "com.google.android.gms.games.PLAY_GAMES_UPGRADE", + "com.google.android.gms.games.service.START", + "com.google.android.gms.gass.START", + "com.google.android.gms.gmscompliance.service.START", + "com.google.android.gms.googlehelp.HELP", + "com.google.android.gms.googlehelp.service.GoogleHelpService.START", + "com.google.android.gms.growth.service.START", + "com.google.android.gms.herrevad.services.LightweightNetworkQualityAndroidService.START", + "com.google.android.gms.icing.INDEX_SERVICE", + "com.google.android.gms.icing.LIGHTWEIGHT_INDEX_SERVICE", + "com.google.android.gms.identity.service.BIND", + "com.google.android.gms.inappreach.service.START", + "com.google.android.gms.instantapps.START", + "com.google.android.gms.kids.service.START", + "com.google.android.gms.languageprofile.service.START", + "com.google.android.gms.learning.internal.dynamitesupport.START", + "com.google.android.gms.learning.intservice.START", + "com.google.android.gms.learning.predictor.START", + "com.google.android.gms.learning.trainer.START", + "com.google.android.gms.learning.training.background.START", + "com.google.android.gms.location.places.GeoDataApi", + "com.google.android.gms.location.places.PlaceDetectionApi", + "com.google.android.gms.location.places.PlacesApi", + "com.google.android.gms.location.reporting.service.START", + "com.google.android.gms.location.settings.LOCATION_HISTORY", + "com.google.android.gms.location.settings.LOCATION_REPORTING_SETTINGS", + "com.google.android.gms.locationsharing.api.START", + "com.google.android.gms.locationsharingreporter.service.START", + "com.google.android.gms.lockbox.service.START", + "com.google.android.gms.matchstick.lighter.service.START", + "com.google.android.gms.mdm.services.DeviceManagerApiService.START", + "com.google.android.gms.mdm.services.START", + "com.google.android.gms.mdns.service.START", + "com.google.android.gms.measurement.START", + "com.google.android.gms.nearby.bootstrap.service.NearbyBootstrapService.START", + "com.google.android.gms.nearby.connection.service.START", + "com.google.android.gms.nearby.fastpair.START", + "com.google.android.gms.nearby.messages.service.NearbyMessagesService.START", + "com.google.android.gms.nearby.sharing.service.NearbySharingService.START", + "com.google.android.gms.nearby.sharing.START_SERVICE", + "com.google.android.gms.notifications.service.START", + "com.google.android.gms.ocr.service.internal.START", + "com.google.android.gms.ocr.service.START", + "com.google.android.gms.oss.licenses.service.START", + "com.google.android.gms.payse.service.BIND", + "com.google.android.gms.people.contactssync.service.START", + "com.google.android.gms.people.service.START", + "com.google.android.gms.phenotype.service.START", + "com.google.android.gms.photos.autobackup.service.START", + "com.google.android.gms.playlog.service.START", + "com.google.android.gms.plus.service.default.INTENT", + "com.google.android.gms.plus.service.image.INTENT", + "com.google.android.gms.plus.service.internal.START", + "com.google.android.gms.plus.service.START", + "com.google.android.gms.potokens.service.START", + "com.google.android.gms.pseudonymous.service.START", + "com.google.android.gms.rcs.START", + "com.google.android.gms.reminders.service.START", + "com.google.android.gms.romanesco.MODULE_BACKUP_AGENT", + "com.google.android.gms.romanesco.service.START", + "com.google.android.gms.safetynet.service.START", + "com.google.android.gms.scheduler.ACTION_PROXY_SCHEDULE", + "com.google.android.gms.search.service.SEARCH_AUTH_START", + "com.google.android.gms.semanticlocation.service.START_ODLH", + "com.google.android.gms.sesame.service.BIND", + "com.google.android.gms.settings.EXPOSURE_NOTIFICATION_SETTINGS", + "com.google.android.gms.setup.auth.SecondDeviceAuth.START", + "com.google.android.gms.signin.service.START", + "com.google.android.gms.smartdevice.d2d.SourceDeviceService.START", + "com.google.android.gms.smartdevice.d2d.TargetDeviceService.START", + "com.google.android.gms.smartdevice.directtransfer.SourceDirectTransferService.START", + "com.google.android.gms.smartdevice.directtransfer.TargetDirectTransferService.START", + "com.google.android.gms.smartdevice.postsetup.PostSetupService.START", + "com.google.android.gms.smartdevice.setup.accounts.AccountsService.START", + "com.google.android.gms.smartdevice.wifi.START_WIFI_HELPER_SERVICE", + "com.google.android.gms.social.location.activity.service.START", + "com.google.android.gms.speech.service.START", + "com.google.android.gms.statementservice.EXECUTE", + "com.google.android.gms.stats.ACTION_UPLOAD_DROPBOX_ENTRIES", + "com.google.android.gms.tapandpay.service.BIND", + "com.google.android.gms.telephonyspam.service.START", + "com.google.android.gms.testsupport.service.START", + "com.google.android.gms.thunderbird.service.START", + "com.google.android.gms.trustagent.BridgeApi.START", + "com.google.android.gms.trustagent.StateApi.START", + "com.google.android.gms.trustagent.trustlet.trustletmanagerservice.BIND", + "com.google.android.gms.trustlet.bluetooth.service.BIND", + "com.google.android.gms.trustlet.connectionlessble.service.BIND", + "com.google.android.gms.trustlet.face.service.BIND", + "com.google.android.gms.trustlet.nfc.service.BIND", + "com.google.android.gms.trustlet.onbody.service.BIND", + "com.google.android.gms.trustlet.place.service.BIND", + "com.google.android.gms.trustlet.voiceunlock.service.BIND", + "com.google.android.gms.udc.service.START", + "com.google.android.gms.update.START_API_SERVICE", + "com.google.android.gms.update.START_SERVICE", + "com.google.android.gms.update.START_SINGLE_USER_API_SERVICE", + "com.google.android.gms.update.START_TV_API_SERVICE", + "com.google.android.gms.usagereporting.service.START", + "com.google.android.gms.userlocation.service.START", + "com.google.android.gms.vehicle.cabin.service.START", + "com.google.android.gms.vehicle.climate.service.START", + "com.google.android.gms.vehicle.info.service.START", + "com.google.android.gms.wallet.service.BIND", + "com.google.android.gms.walletp2p.service.firstparty.BIND", + "com.google.android.gms.walletp2p.service.zeroparty.BIND", + "com.google.android.gms.wearable.BIND", + "com.google.android.gms.wearable.BIND_LISTENER", + "com.google.android.gms.wearable.DATA_CHANGED", + "com.google.android.gms.wearable.MESSAGE_RECEIVED", + "com.google.android.gms.wearable.NODE_CHANGED", + "com.google.android.gsf.action.GET_GLS", + "com.google.android.location.settings.LOCATION_REPORTING_SETTINGS", + "com.google.android.mdd.service.START", + "com.google.android.mdh.service.listener.START", + "com.google.android.mdh.service.START", + "com.google.android.mobstore.service.START", + "com.google.firebase.auth.api.gms.service.START", + "com.google.firebase.dynamiclinks.service.START", + "com.google.iid.TOKEN_REQUEST", + "com.google.android.gms.location.places.ui.PICK_PLACE", + ) + + /** + * All content provider authorities. + */ + val AUTHORITIES = setOf( + "com.google.android.gms.auth.accounts", + "com.google.android.gms.chimera", + "com.google.android.gms.fonts", + "com.google.android.gms.phenotype", + "com.google.android.gsf.gservices", + "com.google.settings", + ) +} + +/** + * 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 toPackageName The package name to fall back to if no custom package name is specified in patch options. + * @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, + toPackageName: String, + spoofedPackageSignature: String, + gmsCoreVendorGroupIdOption: Option, + executeBlock: ResourcePatchContext.() -> Unit = {}, + block: ResourcePatchBuilder.() -> Unit = {}, +) = resourcePatch { + dependsOn( + changePackageNamePatch, + addResourcesPatch, + ) + + val gmsCoreVendorGroupId by gmsCoreVendorGroupIdOption + + execute { + addResources("shared", "misc.gms.gmsCoreSupportResourcePatch") + + /** + * 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 = setOrGetFallbackPackageName(toPackageName) + + 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", + "" to "", + ) + + 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/misc/hex/HexPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/hex/HexPatch.kt new file mode 100644 index 000000000..d361a5571 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/hex/HexPatch.kt @@ -0,0 +1,123 @@ +package app.revanced.patches.shared.misc.hex + +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.rawResourcePatch +import kotlin.math.max + +// The replacements being passed using a function is intended. +// Previously the replacements were a property of the patch. Getter were being delegated to that property. +// This late evaluation was being leveraged in app.revanced.patches.all.misc.hex.HexPatch. +// Without the function, the replacements would be evaluated at the time of patch creation. +// This isn't possible because the delegated property is not accessible at that time. +fun hexPatch(replacementsSupplier: () -> Set) = rawResourcePatch { + execute { + replacementsSupplier().groupBy { it.targetFilePath }.forEach { (targetFilePath, replacements) -> + val targetFile = try { + get(targetFilePath, true) + } catch (e: Exception) { + throw PatchException("Could not find target file: $targetFilePath") + } + + // TODO: Use a file channel to read and write the file instead of reading the whole file into memory, + // in order to reduce memory usage. + val targetFileBytes = targetFile.readBytes() + + replacements.forEach { replacement -> + replacement.replacePattern(targetFileBytes) + } + + targetFile.writeBytes(targetFileBytes) + } + } +} + +/** + * Represents a pattern to search for and its replacement pattern. + * + * @property pattern The pattern to search for. + * @property replacementPattern The pattern to replace the [pattern] with. + * @property targetFilePath The path to the file to make the changes in relative to the APK root. + */ +class Replacement( + private val pattern: String, + replacementPattern: String, + internal val targetFilePath: String, +) { + private val patternBytes = pattern.toByteArrayPattern() + private val replacementPattern = replacementPattern.toByteArrayPattern() + + init { + if (this.patternBytes.size != this.replacementPattern.size) { + throw PatchException("Pattern and replacement pattern must have the same length: $pattern") + } + } + + /** + * Replaces the [patternBytes] with the [replacementPattern] in the [targetFileBytes]. + * + * @param targetFileBytes The bytes of the file to make the changes in. + */ + fun replacePattern(targetFileBytes: ByteArray) { + val startIndex = indexOfPatternIn(targetFileBytes) + + if (startIndex == -1) { + throw PatchException("Pattern not found in target file: $pattern") + } + + replacementPattern.copyInto(targetFileBytes, startIndex) + } + + // TODO: Allow searching in a file channel instead of a byte array to reduce memory usage. + /** + * Returns the index of the first occurrence of [patternBytes] in the haystack + * using the Boyer-Moore algorithm. + * + * @param haystack The array to search in. + * + * @return The index of the first occurrence of the [patternBytes] in the haystack or -1 + * if the [patternBytes] is not found. + */ + private fun indexOfPatternIn(haystack: ByteArray): Int { + val needle = patternBytes + + val haystackLength = haystack.size - 1 + val needleLength = needle.size - 1 + val right = IntArray(256) { -1 } + + for (i in 0 until needleLength) right[needle[i].toInt().and(0xFF)] = i + + var skip: Int + for (i in 0..haystackLength - needleLength) { + skip = 0 + + for (j in needleLength - 1 downTo 0) { + if (needle[j] != haystack[i + j]) { + skip = max(1, j - right[haystack[i + j].toInt().and(0xFF)]) + + break + } + } + + if (skip == 0) return i + } + return -1 + } + + companion object { + /** + * Convert a string representing a pattern of hexadecimal bytes to a byte array. + * + * @return The byte array representing the pattern. + * @throws PatchException If the pattern is invalid. + */ + private fun String.toByteArrayPattern() = try { + split(" ").map { it.toInt(16).toByte() }.toByteArray() + } catch (e: NumberFormatException) { + throw PatchException( + "Could not parse pattern: $this. A pattern is a sequence of case insensitive strings " + + "representing hexadecimal bytes separated by spaces", + e, + ) + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/mapping/ResourceMappingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/mapping/ResourceMappingPatch.kt similarity index 54% rename from src/main/kotlin/app/revanced/patches/shared/misc/mapping/ResourceMappingPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/misc/mapping/ResourceMappingPatch.kt index 2c165e1f1..4368bace0 100644 --- a/src/main/kotlin/app/revanced/patches/shared/misc/mapping/ResourceMappingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/mapping/ResourceMappingPatch.kt @@ -1,31 +1,33 @@ package app.revanced.patches.shared.misc.mapping -import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.patch.ResourcePatch +import app.revanced.patcher.patch.resourcePatch import org.w3c.dom.Element import java.util.* import java.util.concurrent.Executors import java.util.concurrent.TimeUnit -object ResourceMappingPatch : ResourcePatch() { - private val resourceMappings = Collections.synchronizedList(mutableListOf()) +// TODO: Probably renaming the patch/this is a good idea. +lateinit var resourceMappings: List + private set - private val THREAD_COUNT = Runtime.getRuntime().availableProcessors() - private val threadPoolExecutor = Executors.newFixedThreadPool(THREAD_COUNT) +val resourceMappingPatch = resourcePatch { + val threadCount = Runtime.getRuntime().availableProcessors() + val threadPoolExecutor = Executors.newFixedThreadPool(threadCount) - override fun execute(context: ResourceContext) { - // sSve the file in memory to concurrently read from it. - val resourceXmlFile = context.get("res/values/public.xml").readBytes() + val resourceMappings = Collections.synchronizedList(mutableListOf()) - for (threadIndex in 0 until THREAD_COUNT) { + 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@{ - context.xmlEditor[resourceXmlFile.inputStream()].use { editor -> - val document = editor.file + document(resourceXmlFile.inputStream()).use { document -> val resources = document.documentElement.childNodes val resourcesLength = resources.length - val jobSize = resourcesLength / THREAD_COUNT + val jobSize = resourcesLength / threadCount val batchStart = jobSize * threadIndex val batchEnd = jobSize * (threadIndex + 1) @@ -50,12 +52,13 @@ object ResourceMappingPatch : ResourcePatch() { } threadPoolExecutor.also { it.shutdown() }.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS) + + app.revanced.patches.shared.misc.mapping.resourceMappings = resourceMappings } - - operator fun get(type: String, name: String) = - resourceMappings.firstOrNull { - it.type == type && it.name == name - }?.id ?: throw PatchException("Could not find resource type: $type name: $name") - - data class ResourceElement(val type: String, val name: String, val id: Long) } + +operator fun List.get(type: String, name: String) = resourceMappings.firstOrNull { + it.type == type && it.name == name +}?.id ?: throw PatchException("Could not find resource type: $type name: $name") + +data class ResourceElement internal constructor(val type: String, val name: String, val id: Long) diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/settings/BaseSettingsResourcePatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/SettingsPatch.kt similarity index 56% rename from src/main/kotlin/app/revanced/patches/shared/misc/settings/BaseSettingsResourcePatch.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/SettingsPatch.kt index 1570cc3dc..7c343961c 100644 --- a/src/main/kotlin/app/revanced/patches/shared/misc/settings/BaseSettingsResourcePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/SettingsPatch.kt @@ -1,9 +1,9 @@ package app.revanced.patches.shared.misc.settings -import app.revanced.patcher.PatchClass -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patches.all.misc.resources.AddResourcesPatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.all.misc.resources.addResource +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.settings.preference.BasePreference import app.revanced.patches.shared.misc.settings.preference.IntentPreference import app.revanced.util.ResourceGroup @@ -11,42 +11,35 @@ import app.revanced.util.copyResources import app.revanced.util.getNode import app.revanced.util.insertFirst import org.w3c.dom.Node -import java.io.Closeable /** * A resource patch that adds settings to a settings fragment. * * @param rootPreference A pair of an intent preference and the name of the fragment file to add it to. * If null, no preference will be added. - * @param dependencies Additional dependencies of this patch. + * @param preferences A set of preferences to add to the ReVanced fragment. */ -abstract class BaseSettingsResourcePatch( - private val rootPreference: Pair? = null, - dependencies: Set = emptySet(), -) : ResourcePatch( - dependencies = setOf(AddResourcesPatch::class) + dependencies, -), - MutableSet by mutableSetOf(), - Closeable { - private lateinit var context: ResourceContext +fun settingsPatch( + rootPreference: Pair? = null, + preferences: Set, +) = resourcePatch { + dependsOn(addResourcesPatch) - override fun execute(context: ResourceContext) { - context.copyResources( + execute { + copyResources( "settings", ResourceGroup("xml", "revanced_prefs.xml"), ) - this.context = context - - AddResourcesPatch(BaseSettingsResourcePatch::class) + addResources("shared", "misc.settings.settingsResourcePatch") } - override fun close() { + finalize { fun Node.addPreference(preference: BasePreference, prepend: Boolean = false) { preference.serialize(ownerDocument) { resource -> // TODO: Currently, resources can only be added to "values", which may not be the correct place. // It may be necessary to ask for the desired resourceValue in the future. - AddResourcesPatch("values", resource) + addResource("values", resource) }.let { preferenceNode -> insertFirst(preferenceNode) } @@ -54,19 +47,15 @@ abstract class BaseSettingsResourcePatch( // Add the root preference to an existing fragment if needed. rootPreference?.let { (intentPreference, fragment) -> - context.xmlEditor["res/xml/$fragment.xml"].use { editor -> - val document = editor.file - + document("res/xml/$fragment.xml").use { document -> document.getNode("PreferenceScreen").addPreference(intentPreference, true) } } // Add all preferences to the ReVanced fragment. - context.xmlEditor["res/xml/revanced_prefs.xml"].use { editor -> - val document = editor.file - + document("res/xml/revanced_prefs.xml").use { document -> val revancedPreferenceScreenNode = document.getNode("PreferenceScreen") - forEach { revancedPreferenceScreenNode.addPreference(it) } + preferences.forEach { revancedPreferenceScreenNode.addPreference(it) } } } } diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/BasePreference.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/BasePreference.kt similarity index 100% rename from src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/BasePreference.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/BasePreference.kt diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen.kt similarity index 92% rename from src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen.kt index 381a2cf3d..648f5eefd 100644 --- a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/BasePreferenceScreen.kt @@ -1,6 +1,6 @@ package app.revanced.patches.shared.misc.settings.preference -import app.revanced.patches.shared.misc.settings.preference.PreferenceScreen.Sorting +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting import java.io.Closeable abstract class BasePreferenceScreen( @@ -18,7 +18,7 @@ abstract class BasePreferenceScreen( /** * Finalize and insert root preference into resource patch */ - abstract fun commit(screen: PreferenceScreen) + abstract fun commit(screen: PreferenceScreenPreference) open inner class Screen( key: String? = null, @@ -29,13 +29,13 @@ abstract class BasePreferenceScreen( private val sorting: Sorting = Sorting.BY_TITLE, ) : BasePreferenceCollection(key, titleKey, preferences) { - override fun transform(): PreferenceScreen { - return PreferenceScreen( + override fun transform(): PreferenceScreenPreference { + return PreferenceScreenPreference( key, titleKey, summaryKey, sorting, - // Screens and preferences are sorted at runtime by integrations code, + // Screens and preferences are sorted at runtime by extension code, // so title sorting uses the localized language in use. preferences = preferences + categories.map { it.transform() }, ) diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/InputType.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/InputType.kt similarity index 100% rename from src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/InputType.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/InputType.kt diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/IntentPreference.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/IntentPreference.kt similarity index 100% rename from src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/IntentPreference.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/IntentPreference.kt diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/ListPreference.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/ListPreference.kt similarity index 100% rename from src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/ListPreference.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/ListPreference.kt diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/NonInteractivePreference.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/NonInteractivePreference.kt similarity index 70% rename from src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/NonInteractivePreference.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/NonInteractivePreference.kt index 787115dba..d4ecaae7e 100644 --- a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/NonInteractivePreference.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/NonInteractivePreference.kt @@ -6,7 +6,7 @@ import org.w3c.dom.Document /** * A non-interactive preference. * - * Typically used to present static text, but also used for custom integration code that responds to taps. + * Typically used to present static text, but also used for custom extension code that responds to taps. * * @param key The preference key. * @param summaryKey The preference summary key. @@ -19,17 +19,8 @@ class NonInteractivePreference( titleKey: String = "${key}_title", summaryKey: String? = "${key}_summary", tag: String = "Preference", - val selectable: Boolean = false + val selectable: Boolean = false, ) : BasePreference(key, titleKey, summaryKey, tag) { - - @Deprecated("Here only for binary compatibility, and should be removed after the next major version update.") - constructor( - key: String, - summaryKey: String? = "${key}_summary", - tag: String = "Preference", - selectable: Boolean = false - ) : this(key, "${key}_title", summaryKey, tag, selectable) - override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) = super.serialize(ownerDocument, resourceCallback).apply { setAttribute("android:selectable", selectable.toString()) diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/PreferenceCategory.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/PreferenceCategory.kt similarity index 100% rename from src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/PreferenceCategory.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/PreferenceCategory.kt diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/PreferenceScreen.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference.kt similarity index 95% rename from src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/PreferenceScreen.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference.kt index 049966a2c..2b21a8413 100644 --- a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/PreferenceScreen.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/PreferenceScreenPreference.kt @@ -15,7 +15,7 @@ import org.w3c.dom.Document * @param preferences The preferences in this screen. */ @Suppress("MemberVisibilityCanBePrivate") -open class PreferenceScreen( +open class PreferenceScreenPreference( key: String? = null, titleKey: String = "${key}_title", summaryKey: String? = "${key}_summary", @@ -26,7 +26,7 @@ open class PreferenceScreen( // an extra bundle parameter can be added to the preferences XML declaration. // This would require bundling and referencing an additional XML file // or adding new attributes to the attrs.xml file. - // Since the key value is not currently used by integrations, + // Since the key value is not currently used by the extensions, // for now it's much simpler to modify the key to include the sort parameter. ) : BasePreference(if (sorting == Sorting.UNSORTED) key else (key + sorting.keySuffix), titleKey, summaryKey, tag) { override fun serialize(ownerDocument: Document, resourceCallback: (BaseResource) -> Unit) = diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/SummaryType.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/SummaryType.kt similarity index 100% rename from src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/SummaryType.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/SummaryType.kt diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/SwitchPreference.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/SwitchPreference.kt similarity index 100% rename from src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/SwitchPreference.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/SwitchPreference.kt diff --git a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/TextPreference.kt b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/TextPreference.kt similarity index 90% rename from src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/TextPreference.kt rename to patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/TextPreference.kt index d329a9177..81de61569 100644 --- a/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/TextPreference.kt +++ b/patches/src/main/kotlin/app/revanced/patches/shared/misc/settings/preference/TextPreference.kt @@ -17,7 +17,7 @@ class TextPreference( key: String? = null, titleKey: String = "${key}_title", summaryKey: String? = "${key}_summary", - tag: String = "app.revanced.integrations.shared.settings.preference.ResettableEditTextPreference", + tag: String = "app.revanced.extension.shared.settings.preference.ResettableEditTextPreference", val inputType: InputType = InputType.TEXT ) : BasePreference(key, titleKey, summaryKey, tag) { diff --git a/patches/src/main/kotlin/app/revanced/patches/solidexplorer2/functionality/filesize/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/solidexplorer2/functionality/filesize/Fingerprints.kt new file mode 100644 index 000000000..0e81d7e59 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/solidexplorer2/functionality/filesize/Fingerprints.kt @@ -0,0 +1,15 @@ +package app.revanced.patches.solidexplorer2.functionality.filesize + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val onReadyFingerprint = fingerprint { + opcodes( + Opcode.CONST_WIDE_32, // Constant storing the 2MB limit + Opcode.CMP_LONG, + Opcode.IF_LEZ, + ) + custom { method, _ -> + method.name == "onReady" && method.definingClass == "Lpl/solidexplorer/plugins/texteditor/TextEditor;" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/solidexplorer2/functionality/filesize/RemoveFileSizeLimitPatch.kt b/patches/src/main/kotlin/app/revanced/patches/solidexplorer2/functionality/filesize/RemoveFileSizeLimitPatch.kt new file mode 100644 index 000000000..7ef509301 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/solidexplorer2/functionality/filesize/RemoveFileSizeLimitPatch.kt @@ -0,0 +1,23 @@ +package app.revanced.patches.solidexplorer2.functionality.filesize + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import com.android.tools.smali.dexlib2.iface.instruction.ThreeRegisterInstruction + +@Suppress("unused") +val removeFileSizeLimitPatch = bytecodePatch( + name = "Remove file size limit", + description = "Allows opening files larger than 2 MB in the text editor.", +) { + compatibleWith("pl.solidexplorer2") + + execute { + onReadyFingerprint.method.apply { + val cmpIndex = onReadyFingerprint.patternMatch!!.startIndex + 1 + val cmpResultRegister = getInstruction(cmpIndex).registerA + + replaceInstruction(cmpIndex, "const/4 v$cmpResultRegister, 0x0") + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/songpal/badge/BadgeTabPatch.kt b/patches/src/main/kotlin/app/revanced/patches/songpal/badge/BadgeTabPatch.kt similarity index 60% rename from src/main/kotlin/app/revanced/patches/songpal/badge/BadgeTabPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/songpal/badge/BadgeTabPatch.kt index c32a7b097..f37ebe71e 100644 --- a/src/main/kotlin/app/revanced/patches/songpal/badge/BadgeTabPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/songpal/badge/BadgeTabPatch.kt @@ -1,25 +1,22 @@ package app.revanced.patches.songpal.badge -import app.revanced.util.exception -import app.revanced.patcher.data.BytecodeContext import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.annotation.CompatiblePackage -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patches.songpal.badge.fingerprints.CreateTabsFingerprint +import app.revanced.patcher.patch.bytecodePatch -@Patch( +internal const val ACTIVITY_TAB_DESCRIPTOR = "Ljp/co/sony/vim/framework/ui/yourheadphones/YhContract\$Tab;" + +@Suppress("unused") +val badgeTabPatch = bytecodePatch( name = "Remove badge tab", description = "Removes the badge tab from the activity tab.", - compatiblePackages = [CompatiblePackage("com.sony.songpal.mdr")] -) -object BadgeTabPatch : BytecodePatch(setOf(CreateTabsFingerprint)) { - const val ACTIVITY_TAB_DESCRIPTOR = "Ljp/co/sony/vim/framework/ui/yourheadphones/YhContract\$Tab;" - private val arrayTabs = listOf("Log", "HealthCare") +) { + compatibleWith("com.sony.songpal.mdr") - override fun execute(context: BytecodeContext) { - CreateTabsFingerprint.result?.mutableMethod?.apply { + val arrayTabs = listOf("Log", "HealthCare") + + execute { + createTabsFingerprint.method.apply { removeInstructions(0, 2) val arrayRegister = 0 @@ -35,7 +32,7 @@ object BadgeTabPatch : BytecodePatch(setOf(CreateTabsFingerprint)) { const/4 v$indexRegister, $index sget-object v$arrayItemRegister, $ACTIVITY_TAB_DESCRIPTOR->$tab:$ACTIVITY_TAB_DESCRIPTOR aput-object v$arrayItemRegister, v$arrayRegister, v$indexRegister - """ + """, ) } @@ -47,9 +44,8 @@ object BadgeTabPatch : BytecodePatch(setOf(CreateTabsFingerprint)) { """ const/4 v$arrayRegister, ${arrayTabs.size} new-array v$arrayRegister, v$arrayRegister, [$ACTIVITY_TAB_DESCRIPTOR - """ + """, ) - - } ?: throw CreateTabsFingerprint.exception + } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/songpal/badge/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/songpal/badge/Fingerprints.kt new file mode 100644 index 000000000..5d7498c5f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/songpal/badge/Fingerprints.kt @@ -0,0 +1,54 @@ +package app.revanced.patches.songpal.badge + +import app.revanced.patcher.fingerprint +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 +import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference + +// Located @ ub.i0.h#p (9.5.0) +internal val createTabsFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE) + returns("Ljava/util/List;") + custom { method, _ -> + method.implementation?.instructions?.any { instruction -> + if (instruction.opcode != Opcode.INVOKE_STATIC) return@any false + + val reference = (instruction as ReferenceInstruction).reference as MethodReference + + if (reference.parameterTypes.isNotEmpty()) return@any false + if (reference.definingClass != ACTIVITY_TAB_DESCRIPTOR) return@any false + if (reference.returnType != "[${ACTIVITY_TAB_DESCRIPTOR}") return@any false + true + } ?: false + } +} + +// Located @ com.sony.songpal.mdr.vim.activity.MdrRemoteBaseActivity.e#run (9.5.0) +internal val showNotificationFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("V") + custom { method, _ -> + method.implementation?.instructions?.any { instruction -> + if (instruction.opcode != Opcode.INVOKE_VIRTUAL) return@any false + + with(expectedReference) { + val currentReference = (instruction as ReferenceInstruction).reference as MethodReference + currentReference.let { + if (it.definingClass != definingClass) return@any false + if (it.parameterTypes != parameterTypes) return@any false + if (it.returnType != returnType) return@any false + } + } + true + } ?: false + } +} + +internal val expectedReference = ImmutableMethodReference( + "Lcom/google/android/material/bottomnavigation/BottomNavigationView;", + "getOrCreateBadge", // Non-obfuscated placeholder method name. + listOf("I"), + "Lcom/google/android/material/badge/BadgeDrawable;", +) diff --git a/patches/src/main/kotlin/app/revanced/patches/songpal/badge/RemoveNotificationBadgePatch.kt b/patches/src/main/kotlin/app/revanced/patches/songpal/badge/RemoveNotificationBadgePatch.kt new file mode 100644 index 000000000..eae07fc91 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/songpal/badge/RemoveNotificationBadgePatch.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.songpal.badge + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val removeNotificationBadgePatch = bytecodePatch( + name = "Remove notification badge", + description = "Removes the red notification badge from the activity tab.", +) { + compatibleWith("com.sony.songpal.mdr"("10.1.0")) + + execute { + showNotificationFingerprint.method.addInstructions(0, "return-void") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/soundcloud/ad/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/soundcloud/ad/Fingerprints.kt new file mode 100644 index 000000000..28780ea57 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/soundcloud/ad/Fingerprints.kt @@ -0,0 +1,30 @@ +package app.revanced.patches.soundcloud.ad + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val interceptFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("L") + parameters("L") + opcodes( + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT + ) + strings("SC-Mob-UserPlan", "Configuration") +} + +internal val userConsumerPlanConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + parameters( + "Ljava/lang/String;", + "Z", + "Ljava/lang/String;", + "Ljava/util/List;", + "Ljava/lang/String;", + "Ljava/lang/String;", + ) +} diff --git a/src/main/kotlin/app/revanced/patches/soundcloud/ad/HideAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/soundcloud/ad/HideAdsPatch.kt similarity index 66% rename from src/main/kotlin/app/revanced/patches/soundcloud/ad/HideAdsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/soundcloud/ad/HideAdsPatch.kt index ea94b3f2a..3e56ed727 100644 --- a/src/main/kotlin/app/revanced/patches/soundcloud/ad/HideAdsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/soundcloud/ad/HideAdsPatch.kt @@ -1,33 +1,25 @@ package app.revanced.patches.soundcloud.ad -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.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.soundcloud.ad.fingerprints.InterceptFingerprint -import app.revanced.patches.soundcloud.shared.fingerprints.FeatureConstructorFingerprint -import app.revanced.patches.soundcloud.ad.fingerprints.UserConsumerPlanConstructorFingerprint -import app.revanced.util.resultOrThrow +import app.revanced.patches.soundcloud.shared.featureConstructorFingerprint -@Patch( - name = "Hide ads", - compatiblePackages = [CompatiblePackage("com.soundcloud.android")], -) @Suppress("unused") -object HideAdsPatch : BytecodePatch( - setOf(FeatureConstructorFingerprint, UserConsumerPlanConstructorFingerprint, InterceptFingerprint), +val hideAdsPatch = bytecodePatch( + name = "Hide ads", ) { - override fun execute(context: BytecodeContext) { + compatibleWith("com.soundcloud.android") + + execute { // Enable a preset feature to disable audio ads by modifying the JSON server response. // This method is the constructor of a class representing a "Feature" object parsed from JSON data. // p1 is the name of the feature. // p2 is true if the feature is enabled, false otherwise. - FeatureConstructorFingerprint.resultOrThrow().mutableMethod.apply { + featureConstructorFingerprint.method.apply { val afterCheckNotNullIndex = 2 addInstructionsWithLabels( afterCheckNotNullIndex, @@ -49,7 +41,7 @@ object HideAdsPatch : BytecodePatch( // p4 is the "consumerPlanUpsells" value, a list of plans to try to sell to the user. // p5 is the "currentConsumerPlan" value, the type of plan currently subscribed to. // p6 is the "currentConsumerPlanTitle" value, the name of the plan currently subscribed to, shown to the user. - UserConsumerPlanConstructorFingerprint.resultOrThrow().mutableMethod.addInstructions( + userConsumerPlanConstructorFingerprint.method.addInstructions( 0, """ const-string p1, "high_tier" @@ -61,12 +53,11 @@ object HideAdsPatch : BytecodePatch( ) // Prevent verification of an HTTP header containing the user's current plan, which would contradict the previous patch. - InterceptFingerprint.resultOrThrow().let { result -> - val conditionIndex = result.scanResult.patternScanResult!!.endIndex + 1 - result.mutableMethod.addInstruction( - conditionIndex, - "return-object p1", - ) - } + + val conditionIndex = interceptFingerprint.patternMatch!!.endIndex + 1 + interceptFingerprint.method.addInstruction( + conditionIndex, + "return-object p1", + ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/soundcloud/analytics/DisableTelemetryPatch.kt b/patches/src/main/kotlin/app/revanced/patches/soundcloud/analytics/DisableTelemetryPatch.kt new file mode 100644 index 000000000..8614aca3b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/soundcloud/analytics/DisableTelemetryPatch.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.soundcloud.analytics + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val disableTelemetryPatch = bytecodePatch( + name = "Disable telemetry", + description = "Disables SoundCloud's telemetry system.", +) { + compatibleWith("com.soundcloud.android") + + execute { + // Empty the "backend" argument to abort the initializer. + createTrackingApiFingerprint.method.addInstruction(0, "const-string p1, \"\"") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/soundcloud/analytics/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/soundcloud/analytics/Fingerprints.kt new file mode 100644 index 000000000..2954b4d99 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/soundcloud/analytics/Fingerprints.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.soundcloud.analytics + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val createTrackingApiFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("L") + custom { methodDef, _ -> + methodDef.name == "create" + } + strings("backend", "boogaloo") +} diff --git a/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/EnableOfflineSyncPatch.kt b/patches/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/EnableOfflineSyncPatch.kt similarity index 69% rename from src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/EnableOfflineSyncPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/EnableOfflineSyncPatch.kt index 08446f986..ff54b06dc 100644 --- a/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/EnableOfflineSyncPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/EnableOfflineSyncPatch.kt @@ -1,41 +1,30 @@ package app.revanced.patches.soundcloud.offlinesync -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.getInstructions +import app.revanced.patcher.extensions.InstructionExtensions.instructions import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -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.soundcloud.offlinesync.fingerprints.DownloadOperationsHeaderVerificationFingerprint -import app.revanced.patches.soundcloud.offlinesync.fingerprints.DownloadOperationsURLBuilderFingerprint -import app.revanced.patches.soundcloud.shared.fingerprints.FeatureConstructorFingerprint +import app.revanced.patches.soundcloud.shared.featureConstructorFingerprint import app.revanced.util.getReference -import app.revanced.util.resultOrThrow import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference -@Patch( - name = "Enable offline sync", - compatiblePackages = [CompatiblePackage("com.soundcloud.android")], -) @Suppress("unused") -object EnableOfflineSyncPatch : BytecodePatch( - setOf( - FeatureConstructorFingerprint, DownloadOperationsURLBuilderFingerprint, - DownloadOperationsHeaderVerificationFingerprint - ), +val enableOfflineSync = bytecodePatch( + name = "Enable offline sync", ) { - override fun execute(context: BytecodeContext) { + compatibleWith("com.soundcloud.android") + + execute { // Enable the feature to allow offline track syncing by modifying the JSON server response. // This method is the constructor of a class representing a "Feature" object parsed from JSON data. // p1 is the name of the feature. // p2 is true if the feature is enabled, false otherwise. - FeatureConstructorFingerprint.resultOrThrow().mutableMethod.apply { + featureConstructorFingerprint.method.apply { val afterCheckNotNullIndex = 2 addInstructionsWithLabels( @@ -53,7 +42,7 @@ object EnableOfflineSyncPatch : BytecodePatch( // Patch the URL builder to use the HTTPS_STREAM endpoint // instead of the offline sync endpoint to downloading the track. - DownloadOperationsURLBuilderFingerprint.resultOrThrow().mutableMethod.apply { + downloadOperationsURLBuilderFingerprint.method.apply { val getEndpointsEnumFieldIndex = 1 val getEndpointsEnumFieldInstruction = getInstruction(getEndpointsEnumFieldIndex) @@ -62,16 +51,16 @@ object EnableOfflineSyncPatch : BytecodePatch( replaceInstruction( getEndpointsEnumFieldIndex, - "sget-object v$targetRegister, $endpointsType->HTTPS_STREAM:$endpointsType" + "sget-object v$targetRegister, $endpointsType->HTTPS_STREAM:$endpointsType", ) } // The HTTPS_STREAM endpoint does not return the necessary headers for offline sync. // Mock the headers to prevent the app from crashing by setting them to empty strings. // The headers are all cosmetic and do not affect the functionality of the app. - DownloadOperationsHeaderVerificationFingerprint.resultOrThrow().mutableMethod.apply { + downloadOperationsHeaderVerificationFingerprint.method.apply { // The first three null checks need to be patched. - getInstructions().asSequence().filter { + instructions.asSequence().filter { it.opcode == Opcode.IF_EQZ }.take(3).toList().map { it.location.index }.asReversed().forEach { nullCheckIndex -> val headerStringRegister = getInstruction(nullCheckIndex).registerA diff --git a/patches/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/Fingerprints.kt new file mode 100644 index 000000000..688fe3604 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/soundcloud/offlinesync/Fingerprints.kt @@ -0,0 +1,29 @@ +package app.revanced.patches.soundcloud.offlinesync + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val downloadOperationsURLBuilderFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Ljava/lang/String") + parameters("L", "L") + opcodes( + Opcode.IGET_OBJECT, + Opcode.SGET_OBJECT, + Opcode.FILLED_NEW_ARRAY, + ) +} + +internal val downloadOperationsHeaderVerificationFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L", "L") + opcodes( + Opcode.CONST_STRING, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CONST_STRING, + ) + strings("X-SC-Mime-Type", "X-SC-Preset", "X-SC-Quality") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/soundcloud/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/soundcloud/shared/Fingerprints.kt new file mode 100644 index 000000000..3a50ae407 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/soundcloud/shared/Fingerprints.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.soundcloud.shared + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val featureConstructorFingerprint = fingerprint { + returns("V") + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + parameters("Ljava/lang/String;", "Z", "Ljava/util/List;") + opcodes( + Opcode.SGET_OBJECT, + Opcode.CHECK_CAST, + Opcode.INVOKE_VIRTUAL + ) +} diff --git a/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt similarity index 75% rename from src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt rename to patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt index 9418c9f76..4c17ebce6 100644 --- a/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemePatch.kt @@ -1,20 +1,19 @@ +@file:Suppress("NAME_SHADOWING") + package app.revanced.patches.spotify.layout.theme -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.annotation.CompatiblePackage -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption import org.w3c.dom.Element -@Patch( +@Suppress("unused") +val customThemePatch = resourcePatch( name = "Custom theme", description = "Applies a custom theme.", - compatiblePackages = [CompatiblePackage("com.spotify.music")], -) -@Suppress("unused") -object CustomThemePatch : ResourcePatch() { - private var backgroundColor by stringPatchOption( +) { + compatibleWith("com.spotify.music") + + val backgroundColor by stringOption( key = "backgroundColor", default = "@android:color/black", title = "Primary background color", @@ -22,7 +21,7 @@ object CustomThemePatch : ResourcePatch() { required = true, ) - private var backgroundColorSecondary by stringPatchOption( + val backgroundColorSecondary by stringOption( key = "backgroundColorSecondary", default = "#ff282828", title = "Secondary background color", @@ -30,7 +29,7 @@ object CustomThemePatch : ResourcePatch() { required = true, ) - private var accentColor by stringPatchOption( + val accentColor by stringOption( key = "accentColor", default = "#ff1ed760", title = "Accent color", @@ -38,7 +37,7 @@ object CustomThemePatch : ResourcePatch() { required = true, ) - private var accentColorPressed by stringPatchOption( + val accentColorPressed by stringOption( key = "accentColorPressed", default = "#ff169c46", title = "Pressed dark theme accent color", @@ -48,15 +47,13 @@ object CustomThemePatch : ResourcePatch() { required = true, ) - override fun execute(context: ResourceContext) { + execute { val backgroundColor = backgroundColor!! val backgroundColorSecondary = backgroundColorSecondary!! val accentColor = accentColor!! val accentColorPressed = accentColorPressed!! - context.xmlEditor["res/values/colors.xml"].use { editor -> - val document = editor.file - + document("res/values/colors.xml").use { document -> val resourcesNode = document.getElementsByTagName("resources").item(0) as Element for (i in 0 until resourcesNode.childNodes.length) { diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/lite/ondemand/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/lite/ondemand/Fingerprints.kt new file mode 100644 index 000000000..365235bee --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/lite/ondemand/Fingerprints.kt @@ -0,0 +1,23 @@ +package app.revanced.patches.spotify.lite.ondemand + +import com.android.tools.smali.dexlib2.Opcode +import app.revanced.patcher.fingerprint + +internal val onDemandFingerprint = fingerprint(fuzzyPatternScanThreshold = 2) { + returns("L") + parameters() + opcodes( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IF_EQZ, + Opcode.SGET_OBJECT, + Opcode.GOTO, + Opcode.SGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IPUT, + Opcode.RETURN_OBJECT, + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/lite/ondemand/OnDemandPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/lite/ondemand/OnDemandPatch.kt new file mode 100644 index 000000000..6444ff7fb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/lite/ondemand/OnDemandPatch.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.spotify.lite.ondemand + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val onDemandPatch = bytecodePatch( + name = "Enable on demand", + description = "Enables listening to songs on-demand, allowing to play any song from playlists, albums or artists without limitations. This does not remove ads.", +) { + compatibleWith("com.spotify.lite") + + execute { + // Spoof a premium account + + onDemandFingerprint.method.addInstruction( + onDemandFingerprint.patternMatch!!.endIndex - 1, + "const/4 v0, 0x2", + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/Fingerprints.kt new file mode 100644 index 000000000..761e32206 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.spotify.navbar + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags + +internal val addNavBarItemFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + literal { showBottomNavigationItemsTextId } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/PremiumNavbarTabPatch.kt b/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/PremiumNavbarTabPatch.kt new file mode 100644 index 000000000..c191c676b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/spotify/navbar/PremiumNavbarTabPatch.kt @@ -0,0 +1,50 @@ +package app.revanced.patches.spotify.navbar + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings + +internal var showBottomNavigationItemsTextId = -1L + private set +internal var premiumTabId = -1L + private set + +private val premiumNavbarTabResourcePatch = resourcePatch { + dependsOn(resourceMappingPatch) + + execute { + premiumTabId = resourceMappings["id", "premium_tab"] + + showBottomNavigationItemsTextId = resourceMappings[ + "bool", + "show_bottom_navigation_items_text", + ] + } +} + +@Suppress("unused") +val premiumNavbarTabPatch = bytecodePatch( + name = "Premium navbar tab", + description = "Hides the premium tab from the navigation bar.", +) { + dependsOn(premiumNavbarTabResourcePatch) + + compatibleWith("com.spotify.music") + + // If the navigation bar item is the premium tab, do not add it. + execute { + addNavBarItemFingerprint.method.addInstructions( + 0, + """ + const v1, $premiumTabId + if-ne p5, v1, :continue + return-void + :continue + nop + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/stocard/layout/HideOffersTabPatch.kt b/patches/src/main/kotlin/app/revanced/patches/stocard/layout/HideOffersTabPatch.kt new file mode 100644 index 000000000..b7831e14a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/stocard/layout/HideOffersTabPatch.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.stocard.layout + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.util.childElementsSequence +import app.revanced.util.getNode + +@Suppress("unused") +val hideOffersTabPatch = resourcePatch( + name = "Hide offers tab", +) { + compatibleWith("de.stocard.stocard") + + execute { + document("res/menu/bottom_navigation_menu.xml").use { document -> + document.getNode("menu").apply { + removeChild( + childElementsSequence().first { + it.attributes.getNamedItem("android:id")?.nodeValue?.contains("offer") == true + }, + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/stocard/layout/HideStoryBubblesPatch.kt b/patches/src/main/kotlin/app/revanced/patches/stocard/layout/HideStoryBubblesPatch.kt new file mode 100644 index 000000000..aab14bae9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/stocard/layout/HideStoryBubblesPatch.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.stocard.layout + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.util.getNode + +@Suppress("unused") +val hideStoryBubblesPatch = resourcePatch( + name = "Hide story bubbles", +) { + compatibleWith("de.stocard.stocard") + + execute { + document("res/layout/rv_story_bubbles_list.xml").use { document -> + document.getNode("androidx.recyclerview.widget.RecyclerView").apply { + arrayOf( + "android:layout_width", + "android:layout_height", + ).forEach { + attributes.getNamedItem(it).nodeValue = "0dp" + } + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt new file mode 100644 index 000000000..0458f45d3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.strava.subscription + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val getSubscribedFingerprint = fingerprint { + opcodes(Opcode.IGET_BOOLEAN) + custom { method, classDef -> + classDef.endsWith("/SubscriptionDetailResponse;") && method.name == "getSubscribed" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt new file mode 100644 index 000000000..e59660472 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/subscription/UnlockSubscriptionPatch.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.strava.subscription + +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val unlockSubscriptionPatch = bytecodePatch( + name = "Unlock subscription features", + description = "Unlocks \"Routes\", \"Matched Runs\" and \"Segment Efforts\".", +) { + compatibleWith("com.strava") + + execute { + getSubscribedFingerprint.method.replaceInstruction( + getSubscribedFingerprint.patternMatch!!.startIndex, + "const/4 v0, 0x1", + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/upselling/DisableSubscriptionSuggestionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/strava/upselling/DisableSubscriptionSuggestionsPatch.kt new file mode 100644 index 000000000..73246fa21 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/upselling/DisableSubscriptionSuggestionsPatch.kt @@ -0,0 +1,67 @@ +package app.revanced.patches.strava.upselling + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod + +@Suppress("unused") +val disableSubscriptionSuggestionsPatch = bytecodePatch( + name = "Disable subscription suggestions", +) { + compatibleWith("com.strava"("320.12")) + + execute { + val helperMethodName = "getModulesIfNotUpselling" + val pageSuffix = "_upsell" + val label = "original" + + val className = getModulesFingerprint.originalClassDef.type + val originalMethod = getModulesFingerprint.method + val returnType = originalMethod.returnType + + getModulesFingerprint.classDef.methods.add( + ImmutableMethod( + className, + helperMethodName, + emptyList(), + returnType, + AccessFlags.PRIVATE.value, + null, + null, + MutableMethodImplementation(3), + ).toMutable().apply { + addInstructions( + """ + iget-object v0, p0, $className->page:Ljava/lang/String; + const-string v1, "$pageSuffix" + invoke-virtual {v0, v1}, Ljava/lang/String;->endsWith(Ljava/lang/String;)Z + move-result v0 + if-eqz v0, :$label + invoke-static {}, Ljava/util/Collections;->emptyList()Ljava/util/List; + move-result-object v0 + return-object v0 + :$label + iget-object v0, p0, $className->modules:Ljava/util/List; + return-object v0 + """, + ) + }, + ) + + val getModulesIndex = getModulesFingerprint.patternMatch!!.startIndex + with(originalMethod) { + removeInstruction(getModulesIndex) + addInstructions( + getModulesIndex, + """ + invoke-direct {p0}, $className->$helperMethodName()$returnType + move-result-object v0 + """, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/strava/upselling/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/strava/upselling/Fingerprints.kt new file mode 100644 index 000000000..1204a36f2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/strava/upselling/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.strava.upselling + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val getModulesFingerprint = fingerprint { + opcodes(Opcode.IGET_OBJECT) + custom { method, classDef -> + classDef.endsWith("/GenericLayoutEntry;") && method.name == "getModules" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/swissid/integritycheck/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/swissid/integritycheck/Fingerprints.kt new file mode 100644 index 000000000..0efa845fb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/swissid/integritycheck/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.swissid.integritycheck + +import app.revanced.patcher.fingerprint + +internal val checkIntegrityFingerprint = fingerprint { + returns("V") + parameters("Lcom/swisssign/deviceintegrity/model/DeviceIntegrityResult;") + strings("it", "result") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/swissid/integritycheck/RemoveGooglePlayIntegrityCheckPatch.kt b/patches/src/main/kotlin/app/revanced/patches/swissid/integritycheck/RemoveGooglePlayIntegrityCheckPatch.kt new file mode 100644 index 000000000..85ae4a2a8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/swissid/integritycheck/RemoveGooglePlayIntegrityCheckPatch.kt @@ -0,0 +1,31 @@ +package app.revanced.patches.swissid.integritycheck + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +private const val RESULT_METHOD_REFERENCE = + " Lcom/swisssign/deviceintegrity/DeviceintegrityPlugin\$onMethodCall\$1;->" + + "\$result:Lio/flutter/plugin/common/MethodChannel\$Result;" +private const val SUCCESS_METHOD_REFERENCE = + "Lio/flutter/plugin/common/MethodChannel\$Result;->success(Ljava/lang/Object;)V" + +@Suppress("unused") +val removeGooglePlayIntegrityCheckPatch = bytecodePatch( + name = "Remove Google Play Integrity check", + description = "Removes the Google Play Integrity check. With this it's possible to use SwissID on custom ROMS." + + "If the device is rooted, root permissions must be hidden from the app.", +) { + compatibleWith("com.swisssign.swissid.mobile") + + execute { + checkIntegrityFingerprint.method.addInstructions( + 0, + """ + iget-object p1, p0, $RESULT_METHOD_REFERENCE + const-string v0, "VALID" + invoke-interface {p1, v0}, $SUCCESS_METHOD_REFERENCE + return-void + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/ticktick/misc/themeunlock/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/ticktick/misc/themeunlock/Fingerprints.kt new file mode 100644 index 000000000..4bd688de4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/ticktick/misc/themeunlock/Fingerprints.kt @@ -0,0 +1,15 @@ +package app.revanced.patches.ticktick.misc.themeunlock + +import app.revanced.patcher.fingerprint + +internal val checkLockedThemesFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("Theme;") && method.name == "isLockedTheme" + } +} + +internal val setThemeFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("ThemePreviewActivity;") && method.name == "lambda\$updateUserBtn\$1" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/ticktick/misc/themeunlock/UnlockThemePatch.kt b/patches/src/main/kotlin/app/revanced/patches/ticktick/misc/themeunlock/UnlockThemePatch.kt new file mode 100644 index 000000000..c9d777904 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/ticktick/misc/themeunlock/UnlockThemePatch.kt @@ -0,0 +1,25 @@ +package app.revanced.patches.ticktick.misc.themeunlock + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val unlockProPatch = bytecodePatch( + name = "Unlock themes", + description = "Unlocks all themes that are inaccessible until a certain level is reached.", +) { + compatibleWith("com.ticktick.task") + + execute { + checkLockedThemesFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x0 + return v0 + """, + ) + + setThemeFingerprint.method.removeInstructions(0, 10) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/feedfilter/FeedFilterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/feedfilter/FeedFilterPatch.kt new file mode 100644 index 000000000..9ea783f95 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/feedfilter/FeedFilterPatch.kt @@ -0,0 +1,45 @@ +package app.revanced.patches.tiktok.feedfilter + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.tiktok.misc.extension.sharedExtensionPatch +import app.revanced.patches.tiktok.misc.settings.settingsPatch +import app.revanced.patches.tiktok.misc.settings.settingsStatusLoadFingerprint +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +@Suppress("unused") +val feedFilterPatch = bytecodePatch( + name = "Feed filter", + description = "Removes ads, livestreams, stories, image videos " + + "and videos with a specific amount of views or likes from the feed.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + ) + + compatibleWith( + "com.ss.android.ugc.trill"("36.5.4"), + "com.zhiliaoapp.musically"("36.5.4"), + ) + + execute { + feedApiServiceLIZFingerprint.method.apply { + val returnFeedItemInstruction = instructions.first { it.opcode == Opcode.RETURN_OBJECT } + val feedItemsRegister = (returnFeedItemInstruction as OneRegisterInstruction).registerA + + addInstruction( + returnFeedItemInstruction.location.index, + "invoke-static { v$feedItemsRegister }, " + + "Lapp/revanced/extension/tiktok/feedfilter/FeedItemsFilter;->filter(Lcom/ss/android/ugc/aweme/feed/model/FeedItemList;)V", + ) + } + + settingsStatusLoadFingerprint.method.addInstruction( + 0, + "invoke-static {}, Lapp/revanced/extension/tiktok/settings/SettingsStatus;->enableFeedFilter()V", + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/feedfilter/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/feedfilter/Fingerprints.kt new file mode 100644 index 000000000..4f899661e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/feedfilter/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.tiktok.feedfilter + +import app.revanced.patcher.fingerprint + +internal val feedApiServiceLIZFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("/FeedApiService;") && method.name == "fetchFeedList" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/cleardisplay/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/cleardisplay/Fingerprints.kt new file mode 100644 index 000000000..eb2868374 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/cleardisplay/Fingerprints.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.tiktok.interaction.cleardisplay + +import app.revanced.patcher.fingerprint + +internal val onClearDisplayEventFingerprint = fingerprint { + custom { method, classDef -> + // Internally the feature is called "Clear mode". + classDef.endsWith("/ClearModePanelComponent;") && method.name == "onClearModeEvent" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/cleardisplay/RememberClearDisplayPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/cleardisplay/RememberClearDisplayPatch.kt new file mode 100644 index 000000000..13c829e62 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/cleardisplay/RememberClearDisplayPatch.kt @@ -0,0 +1,70 @@ +package app.revanced.patches.tiktok.interaction.cleardisplay + +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.tiktok.shared.onRenderFirstFrameFingerprint +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +@Suppress("unused") +val rememberClearDisplayPatch = bytecodePatch( + name = "Remember clear display", + description = "Remembers the clear display configurations in between videos.", +) { + compatibleWith( + "com.ss.android.ugc.trill"("36.5.4"), + "com.zhiliaoapp.musically"("36.5.4"), + ) + + execute { + onClearDisplayEventFingerprint.method.let { + // region Hook the "Clear display" configuration save event to remember the state of clear display. + + val isEnabledIndex = it.indexOfFirstInstructionOrThrow(Opcode.IGET_BOOLEAN) + 1 + val isEnabledRegister = it.getInstruction(isEnabledIndex - 1).registerA + + it.addInstructions( + isEnabledIndex, + "invoke-static { v$isEnabledRegister }, " + + "Lapp/revanced/extension/tiktok/cleardisplay/RememberClearDisplayPatch;->rememberClearDisplayState(Z)V", + ) + + // endregion + + // region Override the "Clear display" configuration load event to load the state of clear display. + + val clearDisplayEventClass = it.parameters[0].type + onRenderFirstFrameFingerprint.method.addInstructionsWithLabels( + 0, + """ + # Create a new clearDisplayEvent and post it to the EventBus (https://github.com/greenrobot/EventBus) + + # Clear display type such as 0 = LONG_PRESS, 1 = SCREEN_RECORD etc. + const/4 v1, 0x0 + + # Enter method (Such as "pinch", "swipe_exit", or an empty string (unknown, what it means)). + const-string v2, "" + + # Name of the clear display type which is equivalent to the clear display type. + const-string v3, "long_press" + + # The state of clear display. + invoke-static { }, Lapp/revanced/extension/tiktok/cleardisplay/RememberClearDisplayPatch;->getClearDisplayState()Z + move-result v4 + if-eqz v4, :clear_display_disabled + + new-instance v0, $clearDisplayEventClass + invoke-direct { v0, v1, v2, v3, v4 }, $clearDisplayEventClass->(ILjava/lang/String;Ljava/lang/String;Z)V + invoke-virtual { v0 }, $clearDisplayEventClass->post()Lcom/ss/android/ugc/governance/eventbus/IEvent; + """, + ExternalLabel("clear_display_disabled", onRenderFirstFrameFingerprint.method.getInstruction(0)), + ) + + // endregion + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/downloads/DownloadsPatch.kt new file mode 100644 index 000000000..b80ceaed0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/downloads/DownloadsPatch.kt @@ -0,0 +1,92 @@ +package app.revanced.patches.tiktok.interaction.downloads + +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.replaceInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.tiktok.misc.extension.sharedExtensionPatch +import app.revanced.patches.tiktok.misc.settings.settingsPatch +import app.revanced.patches.tiktok.misc.settings.settingsStatusLoadFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +@Suppress("unused") +val downloadsPatch = bytecodePatch( + name = "Downloads", + description = "Removes download restrictions and changes the default path to download to.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + ) + + compatibleWith( + "com.ss.android.ugc.trill"("36.5.4"), + "com.zhiliaoapp.musically"("36.5.4"), + ) + + execute { + aclCommonShareFingerprint.method.replaceInstructions( + 0, + """ + const/4 v0, 0x0 + return v0 + """, + ) + + aclCommonShare2Fingerprint.method.replaceInstructions( + 0, + """ + const/4 v0, 0x2 + return v0 + """, + ) + + // Download videos without watermark. + aclCommonShare3Fingerprint.method.addInstructionsWithLabels( + 0, + """ + invoke-static {}, Lapp/revanced/extension/tiktok/download/DownloadsPatch;->shouldRemoveWatermark()Z + move-result v0 + if-eqz v0, :noremovewatermark + const/4 v0, 0x1 + return v0 + :noremovewatermark + nop + """, + ) + + // Change the download path patch. + downloadUriFingerprint.method.apply { + val firstIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "" + } + val secondIndex = indexOfFirstInstructionOrThrow { + getReference()?.returnType?.contains("Uri") == true + } + + addInstructions( + secondIndex, + """ + invoke-static {}, Lapp/revanced/extension/tiktok/download/DownloadsPatch;->getDownloadPath()Ljava/lang/String; + move-result-object v0 + """, + ) + + addInstructions( + firstIndex, + """ + invoke-static {}, Lapp/revanced/extension/tiktok/download/DownloadsPatch;->getDownloadPath()Ljava/lang/String; + move-result-object v0 + """, + ) + } + + settingsStatusLoadFingerprint.method.addInstruction( + 0, + "invoke-static {}, Lapp/revanced/extension/tiktok/settings/SettingsStatus;->enableDownload()V", + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/downloads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/downloads/Fingerprints.kt new file mode 100644 index 000000000..160b49c15 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/downloads/Fingerprints.kt @@ -0,0 +1,46 @@ +package app.revanced.patches.tiktok.interaction.downloads + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val aclCommonShareFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("I") + custom { method, classDef -> + classDef.endsWith("/ACLCommonShare;") && + method.name == "getCode" + } +} + +internal val aclCommonShare2Fingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("I") + custom { method, classDef -> + classDef.endsWith("/ACLCommonShare;") && + method.name == "getShowType" + } +} + +internal val aclCommonShare3Fingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("I") + custom { method, classDef -> + classDef.endsWith("/ACLCommonShare;") && + method.name == "getTranscode" + } +} + +internal val downloadUriFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Landroid/net/Uri;") + parameters( + "Landroid/content/Context;", + "Ljava/lang/String;" + ) + strings( + "/", + "/Camera", + "/Camera/", + "video/mp4" + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/seekbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/seekbar/Fingerprints.kt new file mode 100644 index 000000000..ce372ea42 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/seekbar/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.tiktok.interaction.seekbar + +import app.revanced.patcher.fingerprint + +internal val setSeekBarShowTypeFingerprint = fingerprint { + strings("seekbar show type change, change to:") +} + +internal val shouldShowSeekBarFingerprint = fingerprint { + strings("can not show seekbar, state: 1, not in resume") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/seekbar/ShowSeekbarPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/seekbar/ShowSeekbarPatch.kt new file mode 100644 index 000000000..417095b33 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/seekbar/ShowSeekbarPatch.kt @@ -0,0 +1,35 @@ +package app.revanced.patches.tiktok.interaction.seekbar + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val showSeekbarPatch = bytecodePatch( + name = "Show seekbar", + description = "Shows progress bar for all video.", +) { + compatibleWith( + "com.ss.android.ugc.trill", + "com.zhiliaoapp.musically", + ) + + execute { + shouldShowSeekBarFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + setSeekBarShowTypeFingerprint.method.apply { + val typeRegister = implementation!!.registerCount - 1 + + addInstructions( + 0, + """ + const/16 v$typeRegister, 0x64 + """, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/speed/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/speed/Fingerprints.kt new file mode 100644 index 000000000..221036bb9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/speed/Fingerprints.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.tiktok.interaction.speed + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val getSpeedFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("/BaseListFragmentPanel;") && method.name == "onFeedSpeedSelectedEvent" + } +} + +internal val setSpeedFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("V") + parameters("Ljava/lang/String;", "Lcom/ss/android/ugc/aweme/feed/model/Aweme;", "F") + strings("enterFrom") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/speed/PlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/speed/PlaybackSpeedPatch.kt new file mode 100644 index 000000000..d59626ee7 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/interaction/speed/PlaybackSpeedPatch.kt @@ -0,0 +1,70 @@ +package app.revanced.patches.tiktok.interaction.speed + +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.tiktok.shared.getEnterFromFingerprint +import app.revanced.patches.tiktok.shared.onRenderFirstFrameFingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction11x +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +@Suppress("unused") +val playbackSpeedPatch = bytecodePatch( + name = "Playback speed", + description = "Enables the playback speed option for all videos and " + + "retains the speed configurations in between videos.", +) { + compatibleWith( + "com.ss.android.ugc.trill"("36.5.4"), + "com.zhiliaoapp.musically"("36.5.4"), + ) + + execute { + setSpeedFingerprint.let { onVideoSwiped -> + getSpeedFingerprint.method.apply { + val injectIndex = + indexOfFirstInstructionOrThrow { getReference()?.returnType == "F" } + 2 + val register = getInstruction(injectIndex - 1).registerA + + addInstruction( + injectIndex, + "invoke-static { v$register }," + + " Lapp/revanced/extension/tiktok/speed/PlaybackSpeedPatch;->rememberPlaybackSpeed(F)V", + ) + } + + // By default, the playback speed will reset to 1.0 at the start of each video. + // Instead, override it with the desired playback speed. + onRenderFirstFrameFingerprint.method.addInstructions( + 0, + """ + # Video playback location (e.g. home page, following page or search result page) retrieved using getEnterFrom method. + const/4 v0, 0x1 + invoke-virtual { p0, v0 }, ${getEnterFromFingerprint.originalMethod} + move-result-object v0 + + # Model of current video retrieved using getCurrentAweme method. + invoke-virtual { p0 }, Lcom/ss/android/ugc/aweme/feed/panel/BaseListFragmentPanel;->getCurrentAweme()Lcom/ss/android/ugc/aweme/feed/model/Aweme; + move-result-object v1 + + # Desired playback speed retrieved using getPlaybackSpeed method. + invoke-static { }, Lapp/revanced/extension/tiktok/speed/PlaybackSpeedPatch;->getPlaybackSpeed()F + move-result v2 + invoke-static { v0, v1, v2 }, ${onVideoSwiped.originalMethod} + """, + ) + + // Force enable the playback speed option for all videos. + onVideoSwiped.classDef.methods.find { method -> method.returnType == "Z" }?.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/extension/ExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/extension/ExtensionPatch.kt new file mode 100644 index 000000000..b714b9d1c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/extension/ExtensionPatch.kt @@ -0,0 +1,5 @@ +package app.revanced.patches.tiktok.misc.extension + +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch + +val sharedExtensionPatch = sharedExtensionPatch(initHook) diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/extension/Hooks.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/extension/Hooks.kt new file mode 100644 index 000000000..24a49dd35 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/extension/Hooks.kt @@ -0,0 +1,14 @@ +package app.revanced.patches.tiktok.misc.extension + +import app.revanced.patches.shared.misc.extension.extensionHook +import com.android.tools.smali.dexlib2.AccessFlags + +internal val initHook = extensionHook( + insertIndexResolver = { 1 }, // Insert after call to super class. +) { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + custom { method, classDef -> + classDef.endsWith("/AwemeHostApplication;") && + method.name == "" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/login/disablerequirement/DisableLoginRequirementPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/login/disablerequirement/DisableLoginRequirementPatch.kt new file mode 100644 index 000000000..2976abe5e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/login/disablerequirement/DisableLoginRequirementPatch.kt @@ -0,0 +1,29 @@ +package app.revanced.patches.tiktok.misc.login.disablerequirement + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val disableLoginRequirementPatch = bytecodePatch( + name = "Disable login requirement", +) { + compatibleWith( + "com.ss.android.ugc.trill", + "com.zhiliaoapp.musically", + ) + + execute { + listOf( + mandatoryLoginServiceFingerprint, + mandatoryLoginService2Fingerprint, + ).forEach { fingerprint -> + fingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x0 + return v0 + """, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/login/disablerequirement/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/login/disablerequirement/Fingerprints.kt new file mode 100644 index 000000000..929ef8672 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/login/disablerequirement/Fingerprints.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.tiktok.misc.login.disablerequirement + +import app.revanced.patcher.fingerprint + +internal val mandatoryLoginServiceFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("/MandatoryLoginService;") && + method.name == "enableForcedLogin" + } +} + +internal val mandatoryLoginService2Fingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("/MandatoryLoginService;") && + method.name == "shouldShowForcedLogin" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/login/fixgoogle/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/login/fixgoogle/Fingerprints.kt new file mode 100644 index 000000000..a40f5251f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/login/fixgoogle/Fingerprints.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.tiktok.misc.login.fixgoogle + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val googleAuthAvailableFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters() + custom { method, _ -> + method.definingClass == "Lcom/bytedance/lobby/google/GoogleAuth;" + } +} + +internal val googleOneTapAuthAvailableFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters() + custom { method, _ -> + method.definingClass == "Lcom/bytedance/lobby/google/GoogleOneTapAuth;" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/login/fixgoogle/FixGoogleLoginPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/login/fixgoogle/FixGoogleLoginPatch.kt new file mode 100644 index 000000000..7ab636c09 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/login/fixgoogle/FixGoogleLoginPatch.kt @@ -0,0 +1,30 @@ +package app.revanced.patches.tiktok.misc.login.fixgoogle + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val fixGoogleLoginPatch = bytecodePatch( + name = "Fix Google login", + description = "Allows logging in with a Google account.", +) { + compatibleWith( + "com.ss.android.ugc.trill", + "com.zhiliaoapp.musically", + ) + + execute { + listOf( + googleOneTapAuthAvailableFingerprint.method, + googleAuthAvailableFingerprint.method, + ).forEach { method -> + method.addInstructions( + 0, + """ + const/4 v0, 0x0 + return v0 + """, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/settings/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/settings/Fingerprints.kt new file mode 100644 index 000000000..d1c4d6de6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/settings/Fingerprints.kt @@ -0,0 +1,35 @@ +package app.revanced.patches.tiktok.misc.settings + +import app.revanced.patcher.fingerprint + +internal val addSettingsEntryFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("/SettingNewVersionFragment;") && + method.name == "initUnitManger" + } +} + +internal val adPersonalizationActivityOnCreateFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("/AdPersonalizationActivity;") && + method.name == "onCreate" + } +} + +internal val settingsEntryFingerprint = fingerprint { + strings("pls pass item or extends the EventUnit") +} + +internal val settingsEntryInfoFingerprint = fingerprint { + strings( + "ExposeItem(title=", + ", icon=", + ) +} + +internal val settingsStatusLoadFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("Lapp/revanced/extension/tiktok/settings/SettingsStatus;") && + method.name == "load" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/settings/SettingsPatch.kt new file mode 100644 index 000000000..a48305177 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/settings/SettingsPatch.kt @@ -0,0 +1,96 @@ +package app.revanced.patches.tiktok.misc.settings + +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.tiktok.misc.extension.sharedExtensionPatch +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22c +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c +import com.android.tools.smali.dexlib2.iface.reference.FieldReference + +internal const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/tiktok/settings/AdPersonalizationActivityHook;" + +val settingsPatch = bytecodePatch( + name = "Settings", + description = "Adds ReVanced settings to TikTok.", +) { + dependsOn(sharedExtensionPatch) + + compatibleWith( + "com.ss.android.ugc.trill"("36.5.4"), + "com.zhiliaoapp.musically"("36.5.4"), + ) + + execute { + val initializeSettingsMethodDescriptor = + "$EXTENSION_CLASS_DESCRIPTOR->initialize(" + + "Lcom/bytedance/ies/ugc/aweme/commercialize/compliance/personalization/AdPersonalizationActivity;" + + ")Z" + + val createSettingsEntryMethodDescriptor = + "$EXTENSION_CLASS_DESCRIPTOR->createSettingsEntry(" + + "Ljava/lang/String;" + + "Ljava/lang/String;" + + ")Ljava/lang/Object;" + + fun String.toClassName(): String = substring(1, this.length - 1).replace("/", ".") + + // Find the class name of classes which construct a settings entry + val settingsButtonClass = settingsEntryFingerprint.originalClassDef.type.toClassName() + val settingsButtonInfoClass = settingsEntryInfoFingerprint.originalClassDef.type.toClassName() + + // Create a settings entry for 'revanced settings' and add it to settings fragment + addSettingsEntryFingerprint.method.apply { + val markIndex = implementation!!.instructions.indexOfFirst { + it.opcode == Opcode.IGET_OBJECT && ((it as Instruction22c).reference as FieldReference).name == "headerUnit" + } + + val getUnitManager = getInstruction(markIndex + 2) + val addEntry = getInstruction(markIndex + 1) + + addInstructions( + markIndex + 2, + listOf( + getUnitManager, + addEntry, + ), + ) + + addInstructions( + markIndex + 2, + """ + const-string v0, "$settingsButtonClass" + const-string v1, "$settingsButtonInfoClass" + invoke-static {v0, v1}, $createSettingsEntryMethodDescriptor + move-result-object v0 + check-cast v0, ${settingsEntryFingerprint.originalClassDef.type} + """, + ) + } + + // Initialize the settings menu once the replaced setting entry is clicked. + adPersonalizationActivityOnCreateFingerprint.method.apply { + val initializeSettingsIndex = implementation!!.instructions.indexOfFirst { + it.opcode == Opcode.INVOKE_SUPER + } + 1 + + val thisRegister = getInstruction(initializeSettingsIndex - 1).registerC + val usableRegister = implementation!!.registerCount - parameters.size - 2 + + addInstructionsWithLabels( + initializeSettingsIndex, + """ + invoke-static {v$thisRegister}, $initializeSettingsMethodDescriptor + move-result v$usableRegister + if-eqz v$usableRegister, :do_not_open + return-void + """, + ExternalLabel("do_not_open", getInstruction(initializeSettingsIndex)), + ) + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/tiktok/misc/spoof/sim/SpoofSimPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/spoof/sim/SpoofSimPatch.kt similarity index 56% rename from src/main/kotlin/app/revanced/patches/tiktok/misc/spoof/sim/SpoofSimPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/tiktok/misc/spoof/sim/SpoofSimPatch.kt index 1c40b5d9c..593241d15 100644 --- a/src/main/kotlin/app/revanced/patches/tiktok/misc/spoof/sim/SpoofSimPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/misc/spoof/sim/SpoofSimPatch.kt @@ -1,47 +1,47 @@ package app.revanced.patches.tiktok.misc.spoof.sim -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.BytecodePatch -import app.revanced.patcher.patch.annotation.CompatiblePackage -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod -import app.revanced.patches.tiktok.misc.integrations.IntegrationsPatch -import app.revanced.patches.tiktok.misc.settings.SettingsPatch -import app.revanced.patches.tiktok.misc.settings.fingerprints.SettingsStatusLoadFingerprint +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.tiktok.misc.extension.sharedExtensionPatch +import app.revanced.patches.tiktok.misc.settings.settingsStatusLoadFingerprint +import app.revanced.patches.twitch.misc.settings.settingsPatch import app.revanced.util.findMutableMethodOf import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c import com.android.tools.smali.dexlib2.iface.reference.MethodReference -@Patch( +@Suppress("unused") +val spoofSimPatch = bytecodePatch( name = "SIM spoof", description = "Spoofs the information which is retrieved from the SIM card.", - dependencies = [IntegrationsPatch::class, SettingsPatch::class], - compatiblePackages = [ - CompatiblePackage("com.ss.android.ugc.trill"), - CompatiblePackage("com.zhiliaoapp.musically"), - ], use = false, -) -@Suppress("unused") -object SpoofSimPatch : BytecodePatch(emptySet()) { - private val replacements = hashMapOf( - "getSimCountryIso" to "getCountryIso", - "getNetworkCountryIso" to "getCountryIso", - "getSimOperator" to "getOperator", - "getNetworkOperator" to "getOperator", - "getSimOperatorName" to "getOperatorName", - "getNetworkOperatorName" to "getOperatorName", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, ) - override fun execute(context: BytecodeContext) { + compatibleWith( + "com.ss.android.ugc.trill", + "com.zhiliaoapp.musically", + ) + + execute { + val replacements = hashMapOf( + "getSimCountryIso" to "getCountryIso", + "getNetworkCountryIso" to "getCountryIso", + "getSimOperator" to "getOperator", + "getNetworkOperator" to "getOperator", + "getSimOperatorName" to "getOperatorName", + "getNetworkOperatorName" to "getOperatorName", + ) + // Find all api call to check sim information. buildMap { - context.classes.forEach { classDef -> + classes.forEach { classDef -> classDef.methods.let { methods -> buildMap methodList@{ methods.forEach methods@{ method -> @@ -69,12 +69,22 @@ object SpoofSimPatch : BytecodePatch(emptySet()) { } } }.forEach { (classDef, methods) -> - with(context.proxy(classDef).mutableClass) { + with(proxy(classDef).mutableClass) { methods.forEach { (method, patches) -> with(findMutableMethodOf(method)) { while (!patches.isEmpty()) { val (index, replacement) = patches.removeLast() - replaceReference(index, replacement) + + val resultReg = getInstruction(index + 1).registerA + + // Patch Android API and return fake sim information. + addInstructions( + index + 2, + """ + invoke-static {v$resultReg}, Lapp/revanced/extension/tiktok/spoof/sim/SpoofSimPatch;->$replacement(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$resultReg + """, + ) } } } @@ -82,24 +92,9 @@ object SpoofSimPatch : BytecodePatch(emptySet()) { } // Enable patch in settings. - with(SettingsStatusLoadFingerprint.result!!.mutableMethod) { - addInstruction( - 0, - "invoke-static {}, Lapp/revanced/integrations/tiktok/settings/SettingsStatus;->enableSimSpoof()V", - ) - } - } - - // Patch Android API and return fake sim information. - private fun MutableMethod.replaceReference(index: Int, replacement: String) { - val resultReg = getInstruction(index + 1).registerA - - addInstructions( - index + 2, - """ - invoke-static {v$resultReg}, Lapp/revanced/integrations/tiktok/spoof/sim/SpoofSimPatch;->$replacement(Ljava/lang/String;)Ljava/lang/String; - move-result-object v$resultReg - """, + settingsStatusLoadFingerprint.method.addInstruction( + 0, + "invoke-static {}, Lapp/revanced/extension/tiktok/settings/SettingsStatus;->enableSimSpoof()V", ) } } diff --git a/patches/src/main/kotlin/app/revanced/patches/tiktok/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/tiktok/shared/Fingerprints.kt new file mode 100644 index 000000000..3e98d213e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tiktok/shared/Fingerprints.kt @@ -0,0 +1,30 @@ +package app.revanced.patches.tiktok.shared + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val getEnterFromFingerprint = fingerprint { + returns("Ljava/lang/String;") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + parameters("Z") + opcodes( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.RETURN_OBJECT, + ) + custom { methodDef, _ -> + methodDef.definingClass.endsWith("/BaseListFragmentPanel;") + } +} + +internal val onRenderFirstFrameFingerprint = fingerprint { + strings("method_enable_viewpager_preload_duration") + custom { _, classDef -> + classDef.endsWith("/BaseListFragmentPanel;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/trakt/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/trakt/Fingerprints.kt new file mode 100644 index 000000000..4a02c6221 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/trakt/Fingerprints.kt @@ -0,0 +1,25 @@ +package app.revanced.patches.trakt + +import app.revanced.patcher.fingerprint + +internal val isVIPEPFingerprint = fingerprint { + custom { method, classDef -> + if (!classDef.endsWith("RemoteUser;")) return@custom false + + method.name == "isVIPEP" + } +} + +internal val isVIPFingerprint = fingerprint { + custom { method, classDef -> + if (!classDef.endsWith("RemoteUser;")) return@custom false + + method.name == "isVIP" + } +} + +internal val remoteUserFingerprint = fingerprint { + custom { _, classDef -> + classDef.endsWith("RemoteUser;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/trakt/UnlockProPatch.kt b/patches/src/main/kotlin/app/revanced/patches/trakt/UnlockProPatch.kt new file mode 100644 index 000000000..bf10b9f40 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/trakt/UnlockProPatch.kt @@ -0,0 +1,29 @@ +package app.revanced.patches.trakt + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val unlockProPatch = bytecodePatch( + name = "Unlock pro", +) { + compatibleWith("tv.trakt.trakt"("1.1.1")) + + execute { + arrayOf(isVIPFingerprint, isVIPEPFingerprint).onEach { fingerprint -> + // Resolve both fingerprints on the same class. + fingerprint.match(remoteUserFingerprint.originalClassDef) + }.forEach { fingerprint -> + // Return true for both VIP check methods. + fingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x1 + invoke-static {v0}, Ljava/lang/Boolean;->valueOf(Z)Ljava/lang/Boolean; + move-result-object v1 + return-object v1 + """, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tudortmund/lockscreen/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/tudortmund/lockscreen/Fingerprints.kt new file mode 100644 index 000000000..12ffa15c1 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tudortmund/lockscreen/Fingerprints.kt @@ -0,0 +1,15 @@ +package app.revanced.patches.tudortmund.lockscreen + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val brightnessFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC) + returns("V") + parameters() + custom { method, classDef -> + method.name == "run" && + method.definingClass.contains("/ScreenPlugin\$") && + classDef.fields.any { it.type == "Ljava/lang/Float;" } + } +} diff --git a/src/main/kotlin/app/revanced/patches/tudortmund/lockscreen/patch/ShowOnLockscreenPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tudortmund/lockscreen/ShowOnLockscreenPatch.kt similarity index 70% rename from src/main/kotlin/app/revanced/patches/tudortmund/lockscreen/patch/ShowOnLockscreenPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/tudortmund/lockscreen/ShowOnLockscreenPatch.kt index 368879cfa..2d3325190 100644 --- a/src/main/kotlin/app/revanced/patches/tudortmund/lockscreen/patch/ShowOnLockscreenPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/tudortmund/lockscreen/ShowOnLockscreenPatch.kt @@ -1,36 +1,33 @@ -package app.revanced.patches.tudortmund.lockscreen.patch +package app.revanced.patches.tudortmund.lockscreen -import app.revanced.util.exception -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.instructions import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.annotation.CompatiblePackage -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patches.tudortmund.lockscreen.fingerprints.BrightnessFingerprint +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.tudortmund.misc.extension.sharedExtensionPatch import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction22c import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference -@Patch( +internal const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/tudortmund/lockscreen/ShowOnLockscreenPatch;" + +@Suppress("unused") +val showOnLockscreenPatch = bytecodePatch( name = "Show on lockscreen", description = "Shows student id and student ticket on lockscreen.", - compatiblePackages = [CompatiblePackage("de.tudortmund.app")], - requiresIntegrations = true -) -@Suppress("unused") -object ShowOnLockscreenPatch : BytecodePatch( - setOf(BrightnessFingerprint) ) { - private const val INTEGRATIONS_CLASS_DESCRIPTOR = "Lapp/revanced/integrations/tudortmund/lockscreen/ShowOnLockscreenPatch;" + dependsOn(sharedExtensionPatch) - override fun execute(context: BytecodeContext) { - BrightnessFingerprint.result?.mutableMethod?.apply { + compatibleWith("de.tudortmund.app") + + execute { + brightnessFingerprint.method.apply { // Find the instruction where the brightness value is loaded into a register - val brightnessInstruction = implementation!!.instructions.firstNotNullOf { instruction -> + val brightnessInstruction = instructions.firstNotNullOf { instruction -> if (instruction.opcode != Opcode.IGET_OBJECT) return@firstNotNullOf null val getInstruction = instruction as Instruction22c @@ -45,29 +42,30 @@ object ShowOnLockscreenPatch : BytecodePatch( // Gets the index of that instruction and the register of the Activity. val (windowIndex, activityRegister) = implementation!!.instructions.withIndex() .firstNotNullOf { (index, instruction) -> - if (instruction.opcode != Opcode.INVOKE_VIRTUAL) + if (instruction.opcode != Opcode.INVOKE_VIRTUAL) { return@firstNotNullOf null + } val invokeInstruction = instruction as Instruction35c val methodRef = invokeInstruction.reference as MethodReference - if (methodRef.name != "getWindow" || methodRef.returnType != "Landroid/view/Window;") + if (methodRef.name != "getWindow" || methodRef.returnType != "Landroid/view/Window;") { return@firstNotNullOf null + } Pair(index, invokeInstruction.registerC) } // The register in which the brightness value is loaded val brightnessRegister = brightnessInstruction.registerA - // Replaces the getWindow call with our custom one to run the lockscreen code replaceInstruction( windowIndex, "invoke-static { v$activityRegister, v$brightnessRegister }, " + - "$INTEGRATIONS_CLASS_DESCRIPTOR->" + - "getWindow" + - "(Landroidx/appcompat/app/AppCompatActivity;F)" + - "Landroid/view/Window;" + "$EXTENSION_CLASS_DESCRIPTOR->" + + "getWindow" + + "(Landroidx/appcompat/app/AppCompatActivity;F)" + + "Landroid/view/Window;", ) // Normally, the brightness is loaded into a register after the getWindow call. @@ -79,11 +77,10 @@ object ShowOnLockscreenPatch : BytecodePatch( """ invoke-virtual { v$brightnessRegister }, Ljava/lang/Float;->floatValue()F move-result v$brightnessRegister - """ + """, ) addInstruction(windowIndex, brightnessInstruction) - - } ?: throw BrightnessFingerprint.exception + } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/tudortmund/misc/extension/ExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tudortmund/misc/extension/ExtensionPatch.kt new file mode 100644 index 000000000..1c056b0c6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tudortmund/misc/extension/ExtensionPatch.kt @@ -0,0 +1,5 @@ +package app.revanced.patches.tudortmund.misc.extension + +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch + +val sharedExtensionPatch = sharedExtensionPatch() diff --git a/src/main/kotlin/app/revanced/patches/tumblr/ads/DisableDashboardAds.kt b/patches/src/main/kotlin/app/revanced/patches/tumblr/ads/DisableDashboardAds.kt similarity index 66% rename from src/main/kotlin/app/revanced/patches/tumblr/ads/DisableDashboardAds.kt rename to patches/src/main/kotlin/app/revanced/patches/tumblr/ads/DisableDashboardAds.kt index 77d31b9f3..4a9bbe297 100644 --- a/src/main/kotlin/app/revanced/patches/tumblr/ads/DisableDashboardAds.kt +++ b/patches/src/main/kotlin/app/revanced/patches/tumblr/ads/DisableDashboardAds.kt @@ -1,20 +1,19 @@ package app.revanced.patches.tumblr.ads -import app.revanced.patcher.data.BytecodeContext -import app.revanced.patcher.patch.BytecodePatch -import app.revanced.patcher.patch.annotation.CompatiblePackage -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patches.tumblr.timelinefilter.TimelineFilterPatch +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.tumblr.timelinefilter.addTimelineObjectTypeFilter +import app.revanced.patches.tumblr.timelinefilter.filterTimelineObjectsPatch -@Patch( +@Suppress("unused") +val disableDashboardAdsPatch = bytecodePatch( name = "Disable dashboard ads", description = "Disables ads in the dashboard.", - compatiblePackages = [CompatiblePackage("com.tumblr")], - dependencies = [TimelineFilterPatch::class], -) -@Suppress("unused") -object DisableDashboardAds : BytecodePatch(emptySet()) { - override fun execute(context: BytecodeContext) { +) { + dependsOn(filterTimelineObjectsPatch) + + compatibleWith("com.tumblr") + + execute { // The timeline object types are filtered by their name in the TimelineObjectType enum. // This is often different from the "object_type" returned in the api (noted in comments here) arrayOf( @@ -31,7 +30,7 @@ object DisableDashboardAds : BytecodePatch(emptySet()) { "FACEBOOK_BIDDAABLE", // "facebook_biddable_sdk_ad" "GOOGLE_NATIVE", // "google_native_ad" ).forEach { - TimelineFilterPatch.addObjectTypeFilter(it) + addTimelineObjectTypeFilter(it) } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/adfree/DisableAdFreeBannerPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/adfree/DisableAdFreeBannerPatch.kt new file mode 100644 index 000000000..c166cb83b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/adfree/DisableAdFreeBannerPatch.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.tumblr.annoyances.adfree + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.tumblr.featureflags.addFeatureFlagOverride +import app.revanced.patches.tumblr.featureflags.overrideFeatureFlagsPatch + +@Suppress("unused") +val disableAdFreeBannerPatch = bytecodePatch( + name = "Disable Ad-Free Banner", + description = "Disables the banner with a frog, prompting you to buy Tumblr Ad-Free.", +) { + dependsOn(overrideFeatureFlagsPatch) + + compatibleWith("com.tumblr") + + execute { + // Disable the "AD_FREE_CTA_BANNER" ("Whether or not to show ad free prompt") feature flag. + addFeatureFlagOverride("adFreeCtaBanner", "false") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/inappupdate/DisableInAppUpdatePatch.kt b/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/inappupdate/DisableInAppUpdatePatch.kt new file mode 100644 index 000000000..3a46c19b2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/inappupdate/DisableInAppUpdatePatch.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.tumblr.annoyances.inappupdate + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.tumblr.featureflags.addFeatureFlagOverride +import app.revanced.patches.tumblr.featureflags.overrideFeatureFlagsPatch + +@Suppress("unused") +val disableInAppUpdatePatch = bytecodePatch( + name = "Disable in-app update", + description = "Disables the in-app update check and update prompt.", +) { + dependsOn(overrideFeatureFlagsPatch) + + compatibleWith("com.tumblr") + + execute { + // Before checking for updates using Google Play core AppUpdateManager, the value of this feature flag is checked. + // If this flag is false or the last update check was today and no update check is performed. + addFeatureFlagOverride("inAppUpdate", "false") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/DisableBlogNotificationReminderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/DisableBlogNotificationReminderPatch.kt new file mode 100644 index 000000000..fc3d1fc8e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/DisableBlogNotificationReminderPatch.kt @@ -0,0 +1,23 @@ +package app.revanced.patches.tumblr.annoyances.notifications + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val disableBlogNotificationReminderPatch = bytecodePatch( + name = "Disable blog notification reminder", + description = "Disables the reminder to enable notifications for blogs you visit.", +) { + compatibleWith("com.tumblr") + + execute { + isBlogNotifyEnabledFingerprint.method.addInstructions( + 0, + """ + # Return false for BlogNotifyCtaDialog.isEnabled() method. + const/4 v0, 0x0 + return v0 + """, + ) + } +} diff --git a/src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/fingerprints/IsBlogNotifyEnabledFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/Fingerprints.kt similarity index 55% rename from src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/fingerprints/IsBlogNotifyEnabledFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/Fingerprints.kt index 55b67f2fa..67e051a7b 100644 --- a/src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/fingerprints/IsBlogNotifyEnabledFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/notifications/Fingerprints.kt @@ -1,9 +1,11 @@ -package app.revanced.patches.tumblr.annoyances.notifications.fingerprints +package app.revanced.patches.tumblr.annoyances.notifications -import app.revanced.patcher.fingerprint.MethodFingerprint +import app.revanced.patcher.fingerprint // The BlogNotifyCtaDialog asks you if you want to enable notifications for a blog. // It shows whenever you visit a certain blog for the second time and disables itself // if it was shown a total of 3 times (stored in app storage). // This targets the BlogNotifyCtaDialog.isEnabled() method to let it always return false. -internal object IsBlogNotifyEnabledFingerprint : MethodFingerprint(strings = listOf("isEnabled --> ", "blog_notify_enabled")) \ No newline at end of file +internal val isBlogNotifyEnabledFingerprint = fingerprint { + strings("isEnabled --> ", "blog_notify_enabled") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/popups/DisableGiftMessagePopupPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/popups/DisableGiftMessagePopupPatch.kt new file mode 100644 index 000000000..09faa1734 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/popups/DisableGiftMessagePopupPatch.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.tumblr.annoyances.popups + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val disableGiftMessagePopupPatch = bytecodePatch( + name = "Disable gift message popup", + description = "Disables the popup suggesting to buy TumblrMart items for other people.", +) { + compatibleWith("com.tumblr") + + execute { + showGiftMessagePopupFingerprint.method.addInstructions(0, "return-void") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/popups/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/popups/Fingerprints.kt new file mode 100644 index 000000000..7d00f2f1b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tumblr/annoyances/popups/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.tumblr.annoyances.popups + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +// This method is responsible for loading and displaying the visual Layout of the Gift Message Popup. +internal val showGiftMessagePopupFingerprint = fingerprint { + accessFlags(AccessFlags.FINAL, AccessFlags.PUBLIC) + returns("V") + strings("activity", "anchorView", "textMessage") +} diff --git a/src/main/kotlin/app/revanced/patches/tumblr/featureflags/fingerprints/GetFeatureValueFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/tumblr/featureflags/Fingerprints.kt similarity index 66% rename from src/main/kotlin/app/revanced/patches/tumblr/featureflags/fingerprints/GetFeatureValueFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/tumblr/featureflags/Fingerprints.kt index 0088c3aa6..e24ca2884 100644 --- a/src/main/kotlin/app/revanced/patches/tumblr/featureflags/fingerprints/GetFeatureValueFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/tumblr/featureflags/Fingerprints.kt @@ -1,9 +1,8 @@ -package app.revanced.patches.tumblr.featureflags.fingerprints +package app.revanced.patches.tumblr.featureflags -import app.revanced.patcher.extensions.or -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint // This fingerprint targets the method to get the value of a Feature in the class "com.tumblr.configuration.Feature". // Features seem to be Tumblr's A/B testing program. @@ -14,14 +13,14 @@ import com.android.tools.smali.dexlib2.Opcode // Some features seem to be very old and never removed, though, such as Google Login. // The startIndex of the opcode pattern is at the start of the function after the arg null check. // we want to insert our instructions there. -internal object GetFeatureValueFingerprint : MethodFingerprint( - strings = listOf("feature"), - opcodes = listOf( +internal val getFeatureValueFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Ljava/lang/String;") + parameters("L", "Z") + opcodes( Opcode.IF_EQZ, Opcode.INVOKE_STATIC, - Opcode.MOVE_RESULT - ), - returnType = "Ljava/lang/String;", - parameters = listOf("L", "Z"), - accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL -) \ No newline at end of file + Opcode.MOVE_RESULT, + ) + strings("feature") +} diff --git a/src/main/kotlin/app/revanced/patches/tumblr/featureflags/OverrideFeatureFlagsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tumblr/featureflags/OverrideFeatureFlagsPatch.kt similarity index 61% rename from src/main/kotlin/app/revanced/patches/tumblr/featureflags/OverrideFeatureFlagsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/tumblr/featureflags/OverrideFeatureFlagsPatch.kt index 94367009c..c2da658aa 100644 --- a/src/main/kotlin/app/revanced/patches/tumblr/featureflags/OverrideFeatureFlagsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/tumblr/featureflags/OverrideFeatureFlagsPatch.kt @@ -1,49 +1,45 @@ package app.revanced.patches.tumblr.featureflags -import app.revanced.util.exception -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.or -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.Companion.toMutable -import app.revanced.patches.tumblr.featureflags.fingerprints.GetFeatureValueFingerprint import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation import com.android.tools.smali.dexlib2.immutable.ImmutableMethod import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter -@Patch(description = "Forcibly set the value of A/B testing features of your choice.") -object OverrideFeatureFlagsPatch : BytecodePatch( - setOf(GetFeatureValueFingerprint) -) { - /** - * Override a feature flag with a value. - * - * @param name The name of the feature flag to override. - * @param value The value to override the feature flag with. - */ - @Suppress("KDocUnresolvedReference") - internal lateinit var addOverride: (name: String, value: String) -> Unit private set +/** + * Override a feature flag with a value. + * + * @param name The name of the feature flag to override. + * @param value The value to override the feature flag with. + */ +@Suppress("KDocUnresolvedReference") +internal lateinit var addFeatureFlagOverride: (name: String, value: String) -> Unit + private set - override fun execute(context: BytecodeContext) = GetFeatureValueFingerprint.result?.let { - val configurationClass = it.method.definingClass - val featureClass = it.method.parameterTypes[0].toString() +val overrideFeatureFlagsPatch = bytecodePatch( + description = "Forcibly set the value of A/B testing features of your choice.", +) { + + execute { + val configurationClass = getFeatureValueFingerprint.originalMethod.definingClass + val featureClass = getFeatureValueFingerprint.originalMethod.parameterTypes[0].toString() // The method we want to inject into does not have enough registers, so we inject a helper method // and inject more instructions into it later, see addOverride. - // This is not in an integration since the unused variable would get compiled away and the method would + // This is not in an extension since the unused variable would get compiled away and the method would // get compiled to only have one register, which is not enough for our later injected instructions. val helperMethod = ImmutableMethod( - it.method.definingClass, + getFeatureValueFingerprint.originalMethod.definingClass, "getValueOverride", listOf(ImmutableMethodParameter(featureClass, null, "feature")), "Ljava/lang/String;", - AccessFlags.PUBLIC or AccessFlags.FINAL, + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, null, null, - MutableMethodImplementation(4) + MutableMethodImplementation(4), ).toMutable().apply { // This is the equivalent of // String featureName = feature.toString() @@ -63,36 +59,36 @@ object OverrideFeatureFlagsPatch : BytecodePatch( # If none of the overrides returned a value, we should return null const/4 v0, 0x0 return-object v0 - """ + """, ) }.also { helperMethod -> - it.mutableClass.methods.add(helperMethod) + getFeatureValueFingerprint.classDef.methods.add(helperMethod) } // Here we actually insert the hook to call our helper method and return its value if it returns not null // This is equivalent to // String forcedValue = getValueOverride(feature) // if (forcedValue != null) return forcedValue - val getFeatureIndex = it.scanResult.patternScanResult!!.startIndex - it.mutableMethod.addInstructionsWithLabels( + val getFeatureIndex = getFeatureValueFingerprint.patternMatch!!.startIndex + getFeatureValueFingerprint.method.addInstructionsWithLabels( getFeatureIndex, """ - # Call the Helper Method with the Feature - invoke-virtual {p0, p1}, $configurationClass->getValueOverride($featureClass)Ljava/lang/String; - move-result-object v0 - # If it returned null, skip - if-eqz v0, :is_null - # If it didnt return null, return that string - return-object v0 - - # If our override helper returned null, we let the function continue normally - :is_null - nop - """ + # Call the Helper Method with the Feature + invoke-virtual {p0, p1}, $configurationClass->getValueOverride($featureClass)Ljava/lang/String; + move-result-object v0 + # If it returned null, skip + if-eqz v0, :is_null + # If it didnt return null, return that string + return-object v0 + + # If our override helper returned null, we let the function continue normally + :is_null + nop + """, ) val helperInsertIndex = 2 - addOverride = { name, value -> + addFeatureFlagOverride = { name, value -> // For every added override, we add a few instructions in the middle of the helper method // to check if the feature is the one we want and return the override value if it is. // This is equivalent to @@ -112,8 +108,8 @@ object OverrideFeatureFlagsPatch : BytecodePatch( # Else we just continue... :no_override nop - """ + """, ) } - } ?: throw GetFeatureValueFingerprint.exception -} \ No newline at end of file + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tumblr/fixes/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/tumblr/fixes/Fingerprints.kt new file mode 100644 index 000000000..11616fcc9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tumblr/fixes/Fingerprints.kt @@ -0,0 +1,23 @@ +package app.revanced.patches.tumblr.fixes + +import com.android.tools.smali.dexlib2.Opcode +import app.revanced.patcher.fingerprint + +// Fingerprint for the addQueryParam method from retrofit2 +// https://github.com/square/retrofit/blob/trunk/retrofit/src/main/java/retrofit2/RequestBuilder.java#L186 +// Injecting here allows modifying dynamically set query parameters +internal val addQueryParamFingerprint = fingerprint { + parameters("Ljava/lang/String;", "Ljava/lang/String;", "Z") + strings("Malformed URL. Base: ", ", Relative: ") +} + +// Fingerprint for the parseHttpMethodAndPath method from retrofit2 +// https://github.com/square/retrofit/blob/ebf87b10997e2136af4d335276fa950221852c64/retrofit/src/main/java/retrofit2/RequestFactory.java#L270-L302 +// Injecting here allows modifying the path/query params of API endpoints defined via annotations +internal val httpPathParserFingerprint = fingerprint { + opcodes( + Opcode.IPUT_OBJECT, + Opcode.IPUT_BOOLEAN, + ) + strings("Only one HTTP method is allowed. Found: %s and %s.") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tumblr/fixes/FixOldVersionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tumblr/fixes/FixOldVersionsPatch.kt new file mode 100644 index 000000000..dbe75d5f8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tumblr/fixes/FixOldVersionsPatch.kt @@ -0,0 +1,54 @@ +package app.revanced.patches.tumblr.fixes + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val fixOldVersionsPatch = bytecodePatch( + name = "Fix old versions", + description = "Fixes old versions of the app (v33.2 and earlier) breaking due to Tumblr removing remnants of Tumblr" + + " Live from the API, which causes many requests to fail. This patch has no effect on newer versions of the app.", + use = false, +) { + compatibleWith("com.tumblr") + + execute { + val liveQueryParameters = listOf( + ",?live_now", + ",?live_streaming_user_id", + ) + + // Remove the live query parameters from the path when it's specified via a @METHOD annotation. + for (liveQueryParameter in liveQueryParameters) { + httpPathParserFingerprint.method.addInstructions( + httpPathParserFingerprint.patternMatch!!.endIndex + 1, + """ + # urlPath = urlPath.replace(liveQueryParameter, "") + const-string p1, "$liveQueryParameter" + const-string p3, "" + invoke-virtual {p2, p1, p3}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String; + move-result-object p2 + """, + ) + } + + // Remove the live query parameters when passed via a parameter which has the @Query annotation. + // e.g. an API call could be defined like this: + // @GET("api/me/info") + // ApiResponse getCurrentUserInfo(@Query("fields[blog]") String value) + // which would result in the path "api/me/inf0?fields[blog]=${value}" + // Here we make sure that this value doesn't contain the broken query parameters. + for (liveQueryParameter in liveQueryParameters) { + addQueryParamFingerprint.method.addInstructions( + 0, + """ + # queryParameterValue = queryParameterValue.replace(liveQueryParameter, "") + const-string v0, "$liveQueryParameter" + const-string v1, "" + invoke-virtual {p2, v0, v1}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String; + move-result-object p2 + """, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tumblr/misc/extension/ExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tumblr/misc/extension/ExtensionPatch.kt new file mode 100644 index 000000000..3a69b74a5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tumblr/misc/extension/ExtensionPatch.kt @@ -0,0 +1,5 @@ +package app.revanced.patches.tumblr.misc.extension + +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch + +val sharedExtensionPatch = sharedExtensionPatch() diff --git a/patches/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/FilterTimelineObjectsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/FilterTimelineObjectsPatch.kt new file mode 100644 index 000000000..c4ab799e8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/FilterTimelineObjectsPatch.kt @@ -0,0 +1,62 @@ +package app.revanced.patches.tumblr.timelinefilter + +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.removeInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.tumblr.misc.extension.sharedExtensionPatch +import com.android.tools.smali.dexlib2.builder.instruction.BuilderInstruction35c + +/** + * Add a filter to hide the given timeline object type. + * The list of all Timeline object types is found in the TimelineObjectType class, + * where they are mapped from their api name (returned by tumblr via the HTTP API) to the enum value name. + * + * @param typeName The enum name of the timeline object type to hide. + */ +@Suppress("KDocUnresolvedReference") +lateinit var addTimelineObjectTypeFilter: (typeName: String) -> Unit + +val filterTimelineObjectsPatch = bytecodePatch( + description = "Filter timeline objects.", +) { + dependsOn(sharedExtensionPatch) + + execute { + val filterInsertIndex = timelineFilterExtensionFingerprint.patternMatch!!.startIndex + + timelineFilterExtensionFingerprint.method.apply { + val addInstruction = getInstruction(filterInsertIndex + 1) + + val filterListRegister = addInstruction.registerC + val stringRegister = addInstruction.registerD + + // Remove "BLOCKED_OBJECT_DUMMY" + removeInstructions(filterInsertIndex, 2) + + addTimelineObjectTypeFilter = { typeName -> + // blockedObjectTypes.add({typeName}) + addInstructionsWithLabels( + filterInsertIndex, + """ + const-string v$stringRegister, "$typeName" + invoke-virtual { v$filterListRegister, v$stringRegister }, Ljava/util/HashSet;->add(Ljava/lang/Object;)Z + """, + ) + } + } + + mapOf( + timelineConstructorFingerprint to 1, + postsResponseConstructorFingerprint to 2, + ).forEach { (fingerprint, timelineObjectsRegister) -> + fingerprint.method.addInstructions( + 0, + "invoke-static {p$timelineObjectsRegister}, " + + "Lapp/revanced/extension/tumblr/patches/TimelineFilterPatch;->" + + "filterTimeline(Ljava/util/List;)V", + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/Fingerprints.kt new file mode 100644 index 000000000..b8ed3bfc4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/tumblr/timelinefilter/Fingerprints.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.tumblr.timelinefilter + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +// This is the constructor of the PostsResponse class. +// The same applies here as with the TimelineConstructorFingerprint. +internal val postsResponseConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.CONSTRUCTOR, AccessFlags.PUBLIC) + custom { method, classDef -> classDef.endsWith("/PostsResponse;") && method.parameters.size == 4 } +} + +// This is the constructor of the Timeline class. +// It receives the List as an argument with a @Json annotation, so this should be the first time +// that the List is exposed in non-library code. +internal val timelineConstructorFingerprint = fingerprint { + strings("timelineObjectsList") + custom { method, classDef -> + classDef.endsWith("/Timeline;") && method.parameters[0].type == "Ljava/util/List;" + } +} + +// This fingerprints the extension TimelineFilterPatch.filterTimeline method. +// The opcode fingerprint is searching for +// if ("BLOCKED_OBJECT_DUMMY".equals(elementType)) iterator.remove(); +internal val timelineFilterExtensionFingerprint = fingerprint { + opcodes( + Opcode.CONST_STRING, // "BLOCKED_OBJECT_DUMMY" + Opcode.INVOKE_VIRTUAL, // HashSet.add(^) + ) + strings("BLOCKED_OBJECT_DUMMY") + custom { _, classDef -> + classDef.endsWith("/TimelineFilterPatch;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/ad/audio/AudioAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/ad/audio/AudioAdsPatch.kt new file mode 100644 index 000000000..4b57bddbd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/ad/audio/AudioAdsPatch.kt @@ -0,0 +1,46 @@ +package app.revanced.patches.twitch.ad.audio + +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.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.twitch.misc.extension.sharedExtensionPatch +import app.revanced.patches.twitch.misc.settings.PreferenceScreen +import app.revanced.patches.twitch.misc.settings.settingsPatch + +@Suppress("unused") +val audioAdsPatch = bytecodePatch( + name = "Block audio ads", + description = "Blocks audio ads in streams and VODs.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith("tv.twitch.android.app"("15.4.1", "16.1.0", "16.9.1")) + + execute { + addResources("twitch", "ad.audio.audioAdsPatch") + + PreferenceScreen.ADS.CLIENT_SIDE.addPreferences( + SwitchPreference("revanced_block_audio_ads"), + ) + + // Block playAds call + audioAdsPresenterPlayFingerprint.method.addInstructionsWithLabels( + 0, + """ + invoke-static { }, Lapp/revanced/extension/twitch/patches/AudioAdsPatch;->shouldBlockAudioAds()Z + move-result v0 + if-eqz v0, :show_audio_ads + return-void + """, + ExternalLabel("show_audio_ads", audioAdsPresenterPlayFingerprint.method.getInstruction(0)), + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/ad/audio/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/ad/audio/Fingerprints.kt new file mode 100644 index 000000000..21e9cb6d2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/ad/audio/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.twitch.ad.audio + +import app.revanced.patcher.fingerprint + +internal val audioAdsPresenterPlayFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("AudioAdsPlayerPresenter;") && method.name == "playAd" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/EmbeddedAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/EmbeddedAdsPatch.kt new file mode 100644 index 000000000..973ccefe0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/EmbeddedAdsPatch.kt @@ -0,0 +1,42 @@ +package app.revanced.patches.twitch.ad.embedded + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.shared.misc.settings.preference.ListPreference +import app.revanced.patches.twitch.ad.video.videoAdsPatch +import app.revanced.patches.twitch.misc.extension.sharedExtensionPatch +import app.revanced.patches.twitch.misc.settings.PreferenceScreen +import app.revanced.patches.twitch.misc.settings.settingsPatch + +@Suppress("unused") +val embeddedAdsPatch = bytecodePatch( + name = "Block embedded ads", + description = "Blocks embedded stream ads using services like Luminous or PurpleAdBlocker.", +) { + dependsOn( + videoAdsPatch, + sharedExtensionPatch, + settingsPatch, + ) + + compatibleWith("tv.twitch.android.app"("15.4.1", "16.1.0", "16.9.1")) + + execute { + addResources("twitch", "ad.embedded.embeddedAdsPatch") + + PreferenceScreen.ADS.SURESTREAM.addPreferences( + ListPreference("revanced_block_embedded_ads", summaryKey = null), + ) + + // Inject OkHttp3 application interceptor + createsUsherClientFingerprint.method.addInstructions( + 3, + """ + invoke-static {}, Lapp/revanced/extension/twitch/patches/EmbeddedAdsPatch;->createRequestInterceptor()Lapp/revanced/extension/twitch/api/RequestInterceptor; + move-result-object v2 + invoke-virtual {v0, v2}, Lokhttp3/OkHttpClient${"$"}Builder;->addInterceptor(Lokhttp3/Interceptor;)Lokhttp3/OkHttpClient${"$"}Builder; + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/Fingerprints.kt new file mode 100644 index 000000000..cbf817469 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/ad/embedded/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.twitch.ad.embedded + +import app.revanced.patcher.fingerprint + +internal val createsUsherClientFingerprint = fingerprint { + custom { method, _ -> + method.definingClass.endsWith("Ltv/twitch/android/network/OkHttpClientFactory;") && method.name == "buildOkHttpClient" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/ad/shared/util/AdPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/ad/shared/util/AdPatch.kt new file mode 100644 index 000000000..48ecefba8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/ad/shared/util/AdPatch.kt @@ -0,0 +1,67 @@ +package app.revanced.patches.twitch.ad.shared.util + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.BytecodePatchBuilder +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel + +fun adPatch( + conditionCall: String, + skipLabelName: String, + block: BytecodePatchBuilder.( + createConditionInstructions: (register: String) -> String, + blockMethods: BytecodePatchContext.( + clazz: String, + methodNames: Set, + returnMethod: ReturnMethod, + ) -> Boolean, + ) -> Unit, +) = bytecodePatch { + fun createConditionInstructions(register: String) = """ + invoke-static { }, $conditionCall + move-result $register + if-eqz $register, :$skipLabelName + """ + + fun BytecodePatchContext.blockMethods( + classDefType: String, + methodNames: Set, + returnMethod: ReturnMethod, + ) = with(classBy { classDefType == it.type }?.mutableClass) { + this ?: return false + + methods.filter { it.name in methodNames }.forEach { + val retInstruction = when (returnMethod.returnType) { + 'V' -> "return-void" + 'Z' -> + """ + const/4 v0, ${returnMethod.value} + return v0 + """ + + else -> throw NotImplementedError() + } + + it.addInstructionsWithLabels( + 0, + """ + ${createConditionInstructions("v0")} + $retInstruction + """, + ExternalLabel(skipLabelName, it.getInstruction(0)), + ) + } + + true + } + + block(::createConditionInstructions, BytecodePatchContext::blockMethods) +} + +class ReturnMethod(val returnType: Char, val value: String) { + companion object { + val default = ReturnMethod('V', "") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/ad/video/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/ad/video/Fingerprints.kt new file mode 100644 index 000000000..d03449733 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/ad/video/Fingerprints.kt @@ -0,0 +1,28 @@ +package app.revanced.patches.twitch.ad.video + +import app.revanced.patcher.fingerprint + +internal val checkAdEligibilityLambdaFingerprint = fingerprint { + returns("Lio/reactivex/Single;") + parameters("L") + custom { method, _ -> + method.definingClass.endsWith("/AdEligibilityFetcher;") && + method.name == "shouldRequestAd" + } +} + +internal val contentConfigShowAdsFingerprint = fingerprint { + returns("Z") + parameters() + custom { method, _ -> + method.definingClass.endsWith("/ContentConfigData;") && method.name == "getShowAds" + } +} + +internal val getReadyToShowAdFingerprint = fingerprint { + returns("Ltv/twitch/android/core/mvp/presenter/StateAndAction;") + parameters("L", "L") + custom { method, _ -> + method.definingClass.endsWith("/StreamDisplayAdsPresenter;") && method.name == "getReadyToShowAdOrAbort" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/ad/video/VideoAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/ad/video/VideoAdsPatch.kt new file mode 100644 index 000000000..49b715fce --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/ad/video/VideoAdsPatch.kt @@ -0,0 +1,164 @@ +package app.revanced.patches.twitch.ad.video + +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.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.twitch.ad.shared.util.ReturnMethod +import app.revanced.patches.twitch.ad.shared.util.adPatch +import app.revanced.patches.twitch.misc.extension.sharedExtensionPatch +import app.revanced.patches.twitch.misc.settings.PreferenceScreen +import app.revanced.patches.twitch.misc.settings.settingsPatch + +val videoAdsPatch = bytecodePatch( + name = "Block video ads", + description = "Blocks video ads in streams and VODs.", +) { + val conditionCall = "Lapp/revanced/extension/twitch/patches/VideoAdsPatch;->shouldBlockVideoAds()Z" + val skipLabelName = "show_video_ads" + + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + adPatch(conditionCall, skipLabelName) { createConditionInstructions, blockMethods -> + + execute { + addResources("twitch", "ad.video.videoAdsPatch") + + PreferenceScreen.ADS.CLIENT_SIDE.addPreferences( + SwitchPreference("revanced_block_video_ads"), + ) + + /* Amazon ads SDK */ + blockMethods( + "Lcom/amazon/ads/video/player/AdsManagerImpl;", + setOf("playAds"), + ReturnMethod.default, + ) + + /* Twitch ads manager */ + blockMethods( + "Ltv/twitch/android/shared/ads/VideoAdManager;", + setOf( + "checkAdEligibilityAndRequestAd", + "requestAd", + "requestAds", + ), + ReturnMethod.default, + ) + + /* Various ad presenters */ + blockMethods( + "Ltv/twitch/android/shared/ads/AdsPlayerPresenter;", + setOf( + "requestAd", + "requestFirstAd", + "requestFirstAdIfEligible", + "requestMidroll", + "requestAdFromMultiAdFormatEvent", + ), + ReturnMethod.default, + ) + + blockMethods( + "Ltv/twitch/android/shared/ads/AdsVodPlayerPresenter;", + setOf( + "requestAd", + "requestFirstAd", + ), + ReturnMethod.default, + ) + + blockMethods( + "Ltv/twitch/android/feature/theatre/ads/AdEdgeAllocationPresenter;", + setOf( + "parseAdAndCheckEligibility", + "requestAdsAfterEligibilityCheck", + "showAd", + "bindMultiAdFormatAllocation", + ), + ReturnMethod.default, + ) + + /* A/B ad testing experiments */ + blockMethods( + "Ltv/twitch/android/provider/experiments/helpers/DisplayAdsExperimentHelper;", + setOf("areDisplayAdsEnabled"), + ReturnMethod('Z', "0"), + ) + + blockMethods( + "Ltv/twitch/android/shared/ads/tracking/MultiFormatAdsTrackingExperiment;", + setOf( + "shouldUseMultiAdFormatTracker", + "shouldUseVideoAdTracker", + ), + ReturnMethod('Z', "0"), + ) + + blockMethods( + "Ltv/twitch/android/shared/ads/MultiformatAdsExperiment;", + setOf( + "shouldDisableClientSideLivePreroll", + "shouldDisableClientSideVodPreroll", + ), + ReturnMethod('Z', "1"), + ) + + // Pretend our player is ineligible for all ads. + checkAdEligibilityLambdaFingerprint.method.addInstructionsWithLabels( + 0, + """ + ${createConditionInstructions("v0")} + const/4 v0, 0 + invoke-static {v0}, Lio/reactivex/Single;->just(Ljava/lang/Object;)Lio/reactivex/Single; + move-result-object p0 + return-object p0 + """, + ExternalLabel( + skipLabelName, + checkAdEligibilityLambdaFingerprint.method.getInstruction(0), + ), + ) + + val adFormatDeclined = + "Ltv/twitch/android/shared/display/ads/theatre/StreamDisplayAdsPresenter\$Action\$AdFormatDeclined;" + getReadyToShowAdFingerprint.method.addInstructionsWithLabels( + 0, + """ + ${createConditionInstructions("v0")} + sget-object p2, $adFormatDeclined->INSTANCE:$adFormatDeclined + invoke-static {p1, p2}, Ltv/twitch/android/core/mvp/presenter/StateMachineKt;->plus(Ltv/twitch/android/core/mvp/presenter/PresenterState;Ltv/twitch/android/core/mvp/presenter/PresenterAction;)Ltv/twitch/android/core/mvp/presenter/StateAndAction; + move-result-object p1 + return-object p1 + """, + ExternalLabel(skipLabelName, getReadyToShowAdFingerprint.method.getInstruction(0)), + ) + + // Spoof showAds JSON field. + contentConfigShowAdsFingerprint.method.addInstructions( + 0, + """ + ${createConditionInstructions("v0")} + const/4 v0, 0 + :$skipLabelName + return v0 + """, + ) + } + }, + ) + + compatibleWith( + "tv.twitch.android.app"( + "15.4.1", + "16.1.0", + "16.9.1", + ), + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/chat/antidelete/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/chat/antidelete/Fingerprints.kt new file mode 100644 index 000000000..21da99a5b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/chat/antidelete/Fingerprints.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.twitch.chat.antidelete + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val chatUtilCreateDeletedSpanFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("ChatUtil\$Companion;") && method.name == "createDeletedSpanFromChatMessageSpan" + } +} + +internal val deletedMessageClickableSpanCtorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + custom { _, classDef -> + classDef.endsWith("DeletedMessageClickableSpan;") + } +} + +internal val setHasModAccessFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("DeletedMessageClickableSpan;") && method.name == "setHasModAccess" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/chat/antidelete/ShowDeletedMessagesPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/chat/antidelete/ShowDeletedMessagesPatch.kt new file mode 100644 index 000000000..f607d1b05 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/chat/antidelete/ShowDeletedMessagesPatch.kt @@ -0,0 +1,74 @@ +package app.revanced.patches.twitch.chat.antidelete + +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.smali.ExternalLabel +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.ListPreference +import app.revanced.patches.twitch.misc.extension.sharedExtensionPatch +import app.revanced.patches.twitch.misc.settings.PreferenceScreen +import app.revanced.patches.twitch.misc.settings.settingsPatch + +@Suppress("unused") +val showDeletedMessagesPatch = bytecodePatch( + name = "Show deleted messages", + description = "Shows deleted chat messages behind a clickable spoiler.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith("tv.twitch.android.app"("15.4.1", "16.1.0", "16.9.1")) + + fun createSpoilerConditionInstructions(register: String = "v0") = """ + invoke-static {}, Lapp/revanced/extension/twitch/patches/ShowDeletedMessagesPatch;->shouldUseSpoiler()Z + move-result $register + if-eqz $register, :no_spoiler + """ + + execute { + addResources("twitch", "chat.antidelete.showDeletedMessagesPatch") + + PreferenceScreen.CHAT.GENERAL.addPreferences( + ListPreference( + key = "revanced_show_deleted_messages", + summaryKey = null, + ), + ) + + // Spoiler mode: Force set hasModAccess member to true in constructor + deletedMessageClickableSpanCtorFingerprint.method.apply { + addInstructionsWithLabels( + implementation!!.instructions.lastIndex, /* place in front of return-void */ + """ + ${createSpoilerConditionInstructions()} + const/4 v0, 1 + iput-boolean v0, p0, $definingClass->hasModAccess:Z + """, + ExternalLabel("no_spoiler", getInstruction(implementation!!.instructions.lastIndex)), + ) + } + + // Spoiler mode: Disable setHasModAccess setter + setHasModAccessFingerprint.method.addInstruction(0, "return-void") + + // Cross-out mode: Reformat span of deleted message + chatUtilCreateDeletedSpanFingerprint.method.apply { + addInstructionsWithLabels( + 0, + """ + invoke-static {p2}, Lapp/revanced/extension/twitch/patches/ShowDeletedMessagesPatch;->reformatDeletedMessage(Landroid/text/Spanned;)Landroid/text/Spanned; + move-result-object v0 + if-eqz v0, :no_reformat + return-object v0 + """, + ExternalLabel("no_reformat", getInstruction(0)), + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/chat/autoclaim/AutoClaimChannelPointsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/chat/autoclaim/AutoClaimChannelPointsPatch.kt new file mode 100644 index 000000000..fd5b81011 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/chat/autoclaim/AutoClaimChannelPointsPatch.kt @@ -0,0 +1,51 @@ +package app.revanced.patches.twitch.chat.autoclaim + +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.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.twitch.misc.settings.PreferenceScreen +import app.revanced.patches.twitch.misc.settings.settingsPatch + +@Suppress("unused") +val autoClaimChannelPointsPatch = bytecodePatch( + name = "Auto claim channel points", + description = "Automatically claim Channel Points.", +) { + dependsOn( + settingsPatch, + addResourcesPatch, + ) + + compatibleWith("tv.twitch.android.app"("15.4.1", "16.1.0", "16.9.1")) + + execute { + addResources("twitch", "chat.autoclaim.autoClaimChannelPointsPatch") + + PreferenceScreen.CHAT.GENERAL.addPreferences( + SwitchPreference("revanced_auto_claim_channel_points"), + ) + + communityPointsButtonViewDelegateFingerprint.method.apply { + val lastIndex = instructions.lastIndex + addInstructionsWithLabels( + lastIndex, // place in front of return-void + """ + invoke-static {}, Lapp/revanced/extension/twitch/patches/AutoClaimChannelPointsPatch;->shouldAutoClaim()Z + move-result v0 + if-eqz v0, :auto_claim + + # Claim by calling the button's onClick method + + iget-object v0, p0, Ltv/twitch/android/shared/community/points/viewdelegate/CommunityPointsButtonViewDelegate;->buttonLayout:Landroid/view/ViewGroup; + invoke-virtual { v0 }, Landroid/view/View;->callOnClick()Z + """, + ExternalLabel("auto_claim", getInstruction(lastIndex)), + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/chat/autoclaim/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/chat/autoclaim/Fingerprints.kt new file mode 100644 index 000000000..80abc9ac4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/chat/autoclaim/Fingerprints.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.twitch.chat.autoclaim + +import app.revanced.patcher.fingerprint + +internal val communityPointsButtonViewDelegateFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("CommunityPointsButtonViewDelegate;") && + method.name == "showClaimAvailable" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/debug/DebugModePatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/debug/DebugModePatch.kt new file mode 100644 index 000000000..d5cbd81aa --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/debug/DebugModePatch.kt @@ -0,0 +1,48 @@ +package app.revanced.patches.twitch.debug + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.twitch.misc.extension.sharedExtensionPatch +import app.revanced.patches.twitch.misc.settings.PreferenceScreen +import app.revanced.patches.twitch.misc.settings.settingsPatch + +@Suppress("unused") +val debugModePatch = bytecodePatch( + name = "Debug mode", + description = "Enables Twitch's internal debugging mode.", + use = false, +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith("tv.twitch.android.app") + + execute { + addResources("twitch", "debug.debugModePatch") + + PreferenceScreen.MISC.OTHER.addPreferences( + SwitchPreference("revanced_twitch_debug_mode"), + ) + + listOf( + isDebugConfigEnabledFingerprint, + isOmVerificationEnabledFingerprint, + shouldShowDebugOptionsFingerprint, + ).forEach { fingerprint -> + fingerprint.method.addInstructions( + 0, + """ + invoke-static {}, Lapp/revanced/extension/twitch/patches/DebugModePatch;->isDebugModeEnabled()Z + move-result v0 + return v0 + """, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/debug/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/debug/Fingerprints.kt new file mode 100644 index 000000000..665180c19 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/debug/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.twitch.debug + +import app.revanced.patcher.fingerprint + +internal val isDebugConfigEnabledFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("/BuildConfigUtil;") && method.name == "isDebugConfigEnabled" + } +} + +internal val isOmVerificationEnabledFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("/BuildConfigUtil;") && method.name == "isOmVerificationEnabled" + } +} + +internal val shouldShowDebugOptionsFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("/BuildConfigUtil;") && method.name == "shouldShowDebugOptions" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/misc/extension/Hooks.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/misc/extension/Hooks.kt new file mode 100644 index 000000000..9a46867ab --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/misc/extension/Hooks.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.twitch.misc.extension + +import app.revanced.patches.shared.misc.extension.extensionHook + +internal val initHook = extensionHook { + custom { method, classDef -> + method.name == "onCreate" && classDef.endsWith("/TwitchApplication;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/misc/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/misc/extension/SharedExtensionPatch.kt new file mode 100644 index 000000000..2299d3bb5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/misc/extension/SharedExtensionPatch.kt @@ -0,0 +1,5 @@ +package app.revanced.patches.twitch.misc.extension + +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch + +val sharedExtensionPatch = sharedExtensionPatch(initHook) diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/misc/settings/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/misc/settings/Fingerprints.kt new file mode 100644 index 000000000..43d5bb39b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/misc/settings/Fingerprints.kt @@ -0,0 +1,34 @@ +package app.revanced.patches.twitch.misc.settings + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val menuGroupsOnClickFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC, AccessFlags.FINAL) + returns("V") + parameters("L", "L", "L") + custom { method, classDef -> + classDef.endsWith("/SettingsMenuViewDelegate;") && + method.name.contains("render") + } +} + +internal val menuGroupsUpdatedFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("/SettingsMenuPresenter\$Event\$MenuGroupsUpdated;") && + method.name == "" + } +} + +internal val settingsActivityOnCreateFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("/SettingsActivity;") && + method.name == "onCreate" + } +} + +internal val settingsMenuItemEnumFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("/SettingsMenuItem;") && method.name == "" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitch/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitch/misc/settings/SettingsPatch.kt new file mode 100644 index 000000000..71bc5f54a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitch/misc/settings/SettingsPatch.kt @@ -0,0 +1,202 @@ +package app.revanced.patches.twitch.misc.settings + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableField.Companion.toMutable +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.BasePreference +import app.revanced.patches.shared.misc.settings.preference.BasePreferenceScreen +import app.revanced.patches.shared.misc.settings.preference.PreferenceCategory +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.shared.misc.settings.settingsPatch +import app.revanced.patches.twitch.misc.extension.sharedExtensionPatch +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.immutable.ImmutableField + +private const val REVANCED_SETTINGS_MENU_ITEM_NAME = "RevancedSettings" +private const val REVANCED_SETTINGS_MENU_ITEM_ID = 0x7 +private const val REVANCED_SETTINGS_MENU_ITEM_TITLE_RES = "revanced_settings" +private const val REVANCED_SETTINGS_MENU_ITEM_ICON_RES = "ic_settings" + +private const val MENU_ITEM_ENUM_CLASS_DESCRIPTOR = "Ltv/twitch/android/feature/settings/menu/SettingsMenuItem;" +private const val MENU_DISMISS_EVENT_CLASS_DESCRIPTOR = + "Ltv/twitch/android/feature/settings/menu/SettingsMenuViewDelegate\$Event\$OnDismissClicked;" + +private const val EXTENSION_PACKAGE = "app/revanced/extension/twitch" +private const val ACTIVITY_HOOKS_CLASS_DESCRIPTOR = "L$EXTENSION_PACKAGE/settings/AppCompatActivityHook;" +private const val UTILS_CLASS_DESCRIPTOR = "L$EXTENSION_PACKAGE/Utils;" + +private val preferences = mutableSetOf() + +fun addSettingPreference(screen: BasePreference) { + preferences += screen +} + +val settingsPatch = bytecodePatch( + name = "Settings", + description = "Adds settings menu to Twitch.", +) { + dependsOn( + sharedExtensionPatch, + addResourcesPatch, + settingsPatch(preferences = preferences), + ) + + compatibleWith( + "tv.twitch.android.app"( + "15.4.1", + "16.1.0", + "16.9.1", + ), + ) + + execute { + addResources("twitch", "misc.settings.settingsPatch") + + PreferenceScreen.MISC.OTHER.addPreferences( + // The debug setting is shared across multiple apps and the key must be the same. + // But the title and summary must be different, otherwise when the strings file is flattened + // for Crowdin push, Crowdin gets confused by the duplicate keys. + // FIXME: Ideally the shared debug strings are extracted into a common app group + // and then both apps import that. But for now unique unique title and summary keys also works. + SwitchPreference( + key = "revanced_debug", + titleKey = "revanced_twitch_debug_title", + summaryOnKey = "revanced_twitch_debug_summary_on", + summaryOffKey = "revanced_twitch_debug_summary_off", + ), + ) + + // Hook onCreate to handle fragment creation. + val insertIndex = settingsActivityOnCreateFingerprint.method.implementation!!.instructions.size - 2 + settingsActivityOnCreateFingerprint.method.addInstructionsWithLabels( + insertIndex, + """ + invoke-static { p0 }, $ACTIVITY_HOOKS_CLASS_DESCRIPTOR->handleSettingsCreation(Landroidx/appcompat/app/AppCompatActivity;)Z + move-result v0 + if-eqz v0, :no_rv_settings_init + return-void + """, + ExternalLabel( + "no_rv_settings_init", + settingsActivityOnCreateFingerprint.method.getInstruction(insertIndex), + ), + ) + + // Create new menu item for settings menu. + fun Fingerprint.injectMenuItem( + name: String, + value: Int, + titleResourceName: String, + iconResourceName: String, + ) { + // Add new static enum member field + classDef.staticFields.add( + ImmutableField( + method.definingClass, + name, + MENU_ITEM_ENUM_CLASS_DESCRIPTOR, + AccessFlags.PUBLIC.value or + AccessFlags.FINAL.value or + AccessFlags.ENUM.value or + AccessFlags.STATIC.value, + null, + null, + null, + ).toMutable(), + ) + + // Add initializer for the new enum member + method.addInstructions( + method.implementation!!.instructions.size - 4, + """ + new-instance v0, $MENU_ITEM_ENUM_CLASS_DESCRIPTOR + const-string v1, "$titleResourceName" + invoke-static {v1}, $UTILS_CLASS_DESCRIPTOR->getStringId(Ljava/lang/String;)I + move-result v1 + const-string v3, "$iconResourceName" + invoke-static {v3}, $UTILS_CLASS_DESCRIPTOR->getDrawableId(Ljava/lang/String;)I + move-result v3 + const-string v4, "$name" + const/4 v5, $value + invoke-direct {v0, v4, v5, v1, v3}, $MENU_ITEM_ENUM_CLASS_DESCRIPTOR->(Ljava/lang/String;III)V + sput-object v0, $MENU_ITEM_ENUM_CLASS_DESCRIPTOR->$name:$MENU_ITEM_ENUM_CLASS_DESCRIPTOR + """, + ) + } + + settingsMenuItemEnumFingerprint.injectMenuItem( + REVANCED_SETTINGS_MENU_ITEM_NAME, + REVANCED_SETTINGS_MENU_ITEM_ID, + REVANCED_SETTINGS_MENU_ITEM_TITLE_RES, + REVANCED_SETTINGS_MENU_ITEM_ICON_RES, + ) + + // Intercept settings menu creation and add new menu item. + menuGroupsUpdatedFingerprint.method.addInstructions( + 0, + """ + sget-object v0, $MENU_ITEM_ENUM_CLASS_DESCRIPTOR->$REVANCED_SETTINGS_MENU_ITEM_NAME:$MENU_ITEM_ENUM_CLASS_DESCRIPTOR + invoke-static { p1, v0 }, $ACTIVITY_HOOKS_CLASS_DESCRIPTOR->handleSettingMenuCreation(Ljava/util/List;Ljava/lang/Object;)Ljava/util/List; + move-result-object p1 + """, + ) + + // Intercept onclick events for the settings menu + menuGroupsOnClickFingerprint.method.addInstructionsWithLabels( + 0, + """ + invoke-static {p1}, $ACTIVITY_HOOKS_CLASS_DESCRIPTOR->handleSettingMenuOnClick(Ljava/lang/Enum;)Z + move-result p2 + if-eqz p2, :no_rv_settings_onclick + sget-object p1, $MENU_DISMISS_EVENT_CLASS_DESCRIPTOR->INSTANCE:$MENU_DISMISS_EVENT_CLASS_DESCRIPTOR + invoke-virtual { p0, p1 }, Ltv/twitch/android/core/mvp/viewdelegate/RxViewDelegate;->pushEvent(Ltv/twitch/android/core/mvp/viewdelegate/ViewDelegateEvent;)V + return-void + """, + ExternalLabel( + "no_rv_settings_onclick", + menuGroupsOnClickFingerprint.method.getInstruction(0), + ), + ) + } + + finalize { + PreferenceScreen.close() + } +} + +/** + * Preference screens patches should add their settings to. + */ +@Suppress("ktlint:standard:property-naming") +internal object PreferenceScreen : BasePreferenceScreen() { + val ADS = CustomScreen("revanced_ads_screen") + val CHAT = CustomScreen("revanced_chat_screen") + val MISC = CustomScreen("revanced_misc_screen") + + internal class CustomScreen(key: String) : Screen(key) { + /* Categories */ + val GENERAL = CustomCategory("revanced_general_category") + val OTHER = CustomCategory("revanced_other_category") + val CLIENT_SIDE = CustomCategory("revanced_client_ads_category") + val SURESTREAM = CustomCategory("revanced_surestream_ads_category") + + internal inner class CustomCategory(key: String) : Category(key) { + /* For Twitch, we need to load our CustomPreferenceCategory class instead of the default one. */ + override fun transform(): PreferenceCategory = PreferenceCategory( + key, + preferences = preferences, + tag = "app.revanced.extension.twitch.settings.preference.CustomPreferenceCategory", + ) + } + } + + override fun commit(screen: app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference) { + addSettingPreference(screen) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitter/interaction/downloads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/interaction/downloads/Fingerprints.kt new file mode 100644 index 000000000..dc100acb1 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/interaction/downloads/Fingerprints.kt @@ -0,0 +1,27 @@ +package app.revanced.patches.twitter.interaction.downloads + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val buildMediaOptionsSheetFingerprint = fingerprint { + opcodes( + Opcode.IF_EQ, + Opcode.SGET_OBJECT, + Opcode.GOTO_16, + Opcode.NEW_INSTANCE, + ) + strings("mediaEntity", "media_options_sheet") +} + +internal val constructMediaOptionsSheetFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + strings("captionsState") +} + +internal val showDownloadVideoUpsellBottomSheetFingerprint = fingerprint { + returns("Z") + strings("mediaEntity", "url") + opcodes(Opcode.IF_EQZ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitter/interaction/downloads/UnlockDownloadsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/interaction/downloads/UnlockDownloadsPatch.kt new file mode 100644 index 000000000..6f2b9c12c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/interaction/downloads/UnlockDownloadsPatch.kt @@ -0,0 +1,66 @@ +package app.revanced.patches.twitter.interaction.downloads + +import app.revanced.patcher.Fingerprint +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.instructions +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +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 + +@Suppress("unused") +val unlockDownloadsPatch = bytecodePatch( + name = "Unlock downloads", + description = "Unlocks the ability to download any video. GIFs can be downloaded via the menu on long press.", +) { + compatibleWith("com.twitter.android") + + execute { + fun Fingerprint.patch(getRegisterAndIndex: Fingerprint.() -> Pair) { + val (index, register) = getRegisterAndIndex() + method.addInstruction(index, "const/4 v$register, 0x1") + } + + // Allow downloads for non-premium users. + showDownloadVideoUpsellBottomSheetFingerprint.patch { + val checkIndex = patternMatch!!.startIndex + val register = method.getInstruction(checkIndex).registerA + + checkIndex to register + } + + // Force show the download menu item. + constructMediaOptionsSheetFingerprint.patch { + val showDownloadButtonIndex = method.instructions.lastIndex - 1 + val register = method.getInstruction(showDownloadButtonIndex).registerA + + showDownloadButtonIndex to register + } + + // Make GIFs downloadable. + val patternMatch = buildMediaOptionsSheetFingerprint.patternMatch!! + buildMediaOptionsSheetFingerprint.method.apply { + val checkMediaTypeIndex = patternMatch.startIndex + val checkMediaTypeInstruction = getInstruction(checkMediaTypeIndex) + + // Treat GIFs as videos. + addInstructionsWithLabels( + checkMediaTypeIndex + 1, + """ + const/4 v${checkMediaTypeInstruction.registerB}, 0x2 # GIF + if-eq v${checkMediaTypeInstruction.registerA}, v${checkMediaTypeInstruction.registerB}, :video + """, + ExternalLabel("video", getInstruction(patternMatch.endIndex)), + ) + + // Remove media.isDownloadable check. + removeInstruction( + instructions.first { it.opcode == Opcode.IGET_BOOLEAN }.location.index + 1, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitter/layout/viewcount/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/layout/viewcount/Fingerprints.kt new file mode 100644 index 000000000..625b6f0bb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/layout/viewcount/Fingerprints.kt @@ -0,0 +1,8 @@ +package app.revanced.patches.twitter.layout.viewcount + +import app.revanced.patcher.fingerprint + +internal val viewCountsEnabledFingerprint = fingerprint { + returns("Z") + strings("view_counts_public_visibility_enabled") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitter/layout/viewcount/HideViewCountPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/layout/viewcount/HideViewCountPatch.kt new file mode 100644 index 000000000..deee9749f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/layout/viewcount/HideViewCountPatch.kt @@ -0,0 +1,23 @@ +package app.revanced.patches.twitter.layout.viewcount + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val hideViewCountPatch = bytecodePatch( + name = "Hide view count", + description = "Hides the view count of Posts.", + use = false, +) { + compatibleWith("com.twitter.android") + + execute { + viewCountsEnabledFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x0 + return v0 + """, + ) + } +} diff --git a/src/main/kotlin/app/revanced/patches/twitter/misc/dynamiccolor/DynamicColorPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/dynamiccolor/DynamicColorPatch.kt similarity index 81% rename from src/main/kotlin/app/revanced/patches/twitter/misc/dynamiccolor/DynamicColorPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/twitter/misc/dynamiccolor/DynamicColorPatch.kt index 51d62a998..286f51afc 100644 --- a/src/main/kotlin/app/revanced/patches/twitter/misc/dynamiccolor/DynamicColorPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/dynamiccolor/DynamicColorPatch.kt @@ -1,22 +1,19 @@ package app.revanced.patches.twitter.misc.dynamiccolor -import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.annotation.CompatiblePackage -import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patcher.patch.resourcePatch import java.io.FileWriter import java.nio.file.Files -@Patch( +@Suppress("unused") +val dynamicColorPatch = resourcePatch( name = "Dynamic color", description = "Replaces the default X (Formerly Twitter) Blue with the user's Material You palette.", - compatiblePackages = [CompatiblePackage("com.twitter.android")], -) -@Suppress("unused") -object DynamicColorPatch : ResourcePatch() { - override fun execute(context: ResourceContext) { - val resDirectory = context.get("res") +) { + compatibleWith("com.twitter.android") + + execute { + val resDirectory = get("res") if (!resDirectory.isDirectory) throw PatchException("The res folder can not be found.") val valuesV31Directory = resDirectory.resolve("values-v31") @@ -35,8 +32,7 @@ object DynamicColorPatch : ResourcePatch() { } } - context.xmlEditor["res/values-v31/colors.xml"].use { editor -> - val document = editor.file + document("res/values-v31/colors.xml").use { document -> mapOf( "ps__twitter_blue" to "@color/twitter_blue", @@ -57,9 +53,7 @@ object DynamicColorPatch : ResourcePatch() { } } - context.xmlEditor["res/values-night-v31/colors.xml"].use { editor -> - val document = editor.file - + document("res/values-night-v31/colors.xml").use { document -> mapOf( "twitter_blue" to "@android:color/system_accent1_200", "twitter_blue_fill_pressed" to "@android:color/system_accent1_300", diff --git a/patches/src/main/kotlin/app/revanced/patches/twitter/misc/extension/ExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/extension/ExtensionPatch.kt new file mode 100644 index 000000000..f1e6a879b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/extension/ExtensionPatch.kt @@ -0,0 +1,5 @@ +package app.revanced.patches.twitter.misc.extension + +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch + +val sharedExtensionPatch = sharedExtensionPatch() diff --git a/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/HideAdsHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/HideAdsHookPatch.kt new file mode 100644 index 000000000..4f9b38ff6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/HideAdsHookPatch.kt @@ -0,0 +1,7 @@ +package app.revanced.patches.twitter.misc.hook + +@Suppress("unused") +val hideAdsHookPatch = hookPatch( + name = "Hide ads", + hookClassDescriptor = "Lapp/revanced/extension/twitter/patches/hook/patch/ads/HideAdsHook;", +) diff --git a/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/HideRecommendedUsersPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/HideRecommendedUsersPatch.kt new file mode 100644 index 000000000..253379615 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/HideRecommendedUsersPatch.kt @@ -0,0 +1,7 @@ +package app.revanced.patches.twitter.misc.hook + +@Suppress("unused") +val hideRecommendedUsersPatch = hookPatch( + name = "Hide recommended users", + hookClassDescriptor = "Lapp/revanced/extension/twitter/patches/hook/patch/recommendation/RecommendedUsersHook;", +) diff --git a/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/HookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/HookPatch.kt new file mode 100644 index 000000000..66d1a3f16 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/HookPatch.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.twitter.misc.hook + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.twitter.misc.hook.json.JsonHook +import app.revanced.patches.twitter.misc.hook.json.addJsonHook +import app.revanced.patches.twitter.misc.hook.json.jsonHookPatch + +fun hookPatch( + name: String, + hookClassDescriptor: String, +) = bytecodePatch(name) { + dependsOn(jsonHookPatch) + + compatibleWith("com.twitter.android") + + execute { + addJsonHook(JsonHook(hookClassDescriptor)) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/Fingerprints.kt new file mode 100644 index 000000000..337aeb567 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/Fingerprints.kt @@ -0,0 +1,27 @@ +package app.revanced.patches.twitter.misc.hook.json + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val jsonHookPatchFingerprint = fingerprint { + opcodes( + Opcode.INVOKE_INTERFACE, // Add dummy hook to hooks list. + // Add hooks to the hooks list. + Opcode.INVOKE_STATIC, // Call buildList. + ) + custom { method, _ -> method.name == "" } +} + +internal val jsonInputStreamFingerprint = fingerprint { + custom { method, _ -> + if (method.parameterTypes.isEmpty()) { + false + } else { + method.parameterTypes.first() == "Ljava/io/InputStream;" + } + } +} + +internal val loganSquareFingerprint = fingerprint { + custom { _, classDef -> classDef.endsWith("LoganSquare;") } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/JsonHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/JsonHookPatch.kt new file mode 100644 index 000000000..093305379 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/hook/json/JsonHookPatch.kt @@ -0,0 +1,109 @@ +package app.revanced.patches.twitter.misc.hook.json + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.removeInstructions +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.reddit.misc.extension.sharedExtensionPatch +import java.io.InvalidClassException + +/** + * Add a hook to the [jsonHookPatch]. + * Will not add the hook if it's already added. + * + * @param jsonHook The [JsonHook] to add. + */ +context(BytecodePatchContext) +fun addJsonHook( + jsonHook: JsonHook, +) { + if (jsonHook.added) return + + jsonHookPatchFingerprint.method.apply { + // Insert hooks right before calling buildList. + val insertIndex = jsonHookPatchFingerprint.patternMatch!!.endIndex + + addInstructions( + insertIndex, + """ + sget-object v1, ${jsonHook.descriptor}->INSTANCE:${jsonHook.descriptor} + invoke-interface {v0, v1}, Ljava/util/List;->add(Ljava/lang/Object;)Z + """, + ) + } + + jsonHook.added = true +} + +private const val JSON_HOOK_CLASS_NAMESPACE = "app/revanced/extension/twitter/patches/hook/json" +private const val JSON_HOOK_PATCH_CLASS_DESCRIPTOR = "L$JSON_HOOK_CLASS_NAMESPACE/JsonHookPatch;" +private const val BASE_PATCH_CLASS_NAME = "BaseJsonHook" +private const val JSON_HOOK_CLASS_DESCRIPTOR = "L$JSON_HOOK_CLASS_NAMESPACE/$BASE_PATCH_CLASS_NAME;" + +val jsonHookPatch = bytecodePatch( + description = "Hooks the stream which reads JSON responses.", +) { + dependsOn(sharedExtensionPatch) + + execute { + val jsonFactoryClassDef = jsonHookPatchFingerprint.apply { + // Make sure the extension is present. + val jsonHookPatch = classBy { classDef -> classDef.type == JSON_HOOK_PATCH_CLASS_DESCRIPTOR } + ?: throw PatchException("Could not find the extension.") + + matchOrNull(jsonHookPatch.immutableClass) + ?: throw PatchException("Unexpected extension.") + }.originalClassDef // Conveniently find the type to hook a method in, via a named field. + .fields + .firstOrNull { it.name == "JSON_FACTORY" } + ?.type + .let { type -> classes.find { it.type == type } } ?: throw PatchException("Could not find required class.") + + // Hook the methods first parameter. + jsonInputStreamFingerprint.match(jsonFactoryClassDef).method.addInstructions( + 0, + """ + invoke-static { p1 }, $JSON_HOOK_PATCH_CLASS_DESCRIPTOR->parseJsonHook(Ljava/io/InputStream;)Ljava/io/InputStream; + move-result-object p1 + """, + ) + } + + finalize { + // Remove hooks.add(dummyHook). + jsonHookPatchFingerprint.method.apply { + val addDummyHookIndex = jsonHookPatchFingerprint.patternMatch!!.endIndex - 2 + + removeInstructions(addDummyHookIndex, 2) + } + } +} + +/** + * Create a hook class. + * The class has to extend on **JsonHook**. + * The class has to be a Kotlin object class, or at least have an INSTANCE field of itself. + * + * @param descriptor The class descriptor of the hook. + * @throws ClassNotFoundException If the class could not be found. + */ +context(BytecodePatchContext) +class JsonHook( + internal val descriptor: String, +) { + internal var added = false + + init { + classBy { it.type == descriptor }?.let { + it.mutableClass.also { classDef -> + if ( + classDef.superclass != JSON_HOOK_CLASS_DESCRIPTOR || + !classDef.fields.any { field -> field.name == "INSTANCE" } + ) { + throw InvalidClassException(classDef.type, "Not a hook class") + } + } + } ?: throw ClassNotFoundException("Failed to find hook class $descriptor") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitter/misc/links/ChangeLinkSharingDomainPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/links/ChangeLinkSharingDomainPatch.kt new file mode 100644 index 000000000..35131aa03 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/links/ChangeLinkSharingDomainPatch.kt @@ -0,0 +1,93 @@ +package app.revanced.patches.twitter.misc.links + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c + +internal var tweetShareLinkTemplateId = -1L + private set + +internal val changeLinkSharingDomainResourcePatch = resourcePatch { + dependsOn(resourceMappingPatch) + + execute { + tweetShareLinkTemplateId = resourceMappings["string", "tweet_share_link"] + } +} + +// This method is used to build the link that is shared when the "Share via..." button is pressed. +private const val FORMAT_METHOD_RESOURCE_REFERENCE = + "Lapp/revanced/extension/twitter/patches/links/ChangeLinkSharingDomainPatch;->" + + "formatResourceLink([Ljava/lang/Object;)Ljava/lang/String;" + +// This method is used to build the link that is shared when the "Copy link" button is pressed. +private const val FORMAT_METHOD_REFERENCE = + "Lapp/revanced/extension/twitter/patches/links/ChangeLinkSharingDomainPatch;->" + + "formatLink(JLjava/lang/String;)Ljava/lang/String;" + +@Suppress("unused") +val changeLinkSharingDomainPatch = bytecodePatch( + name = "Change link sharing domain", + description = "Replaces the domain name of Twitter links when sharing them.", +) { + dependsOn(changeLinkSharingDomainResourcePatch) + + compatibleWith("com.twitter.android") + + val domainName by stringOption( + key = "domainName", + default = "fxtwitter.com", + title = "Domain name", + description = "The domain name to use when sharing links.", + required = true, + ) + + execute { + + val replacementIndex = + linkSharingDomainFingerprint.stringMatches!!.first().index + val domainRegister = + linkSharingDomainFingerprint.method.getInstruction(replacementIndex).registerA + + linkSharingDomainFingerprint.method.replaceInstruction( + replacementIndex, + "const-string v$domainRegister, \"https://$domainName\"", + ) + + // Replace the domain name when copying a link with "Copy link" button. + linkBuilderFingerprint.method.apply { + addInstructions( + 0, + """ + invoke-static { p0, p1, p2 }, $FORMAT_METHOD_REFERENCE + move-result-object p0 + return-object p0 + """, + ) + } + + // Used in the Share via... dialog. + linkResourceGetterFingerprint.method.apply { + val templateIdConstIndex = indexOfFirstLiteralInstructionOrThrow(tweetShareLinkTemplateId) + + // Format the link with the new domain name register (1 instruction below the const). + val formatLinkCallIndex = templateIdConstIndex + 1 + val formatLinkCall = getInstruction(formatLinkCallIndex) + + // Replace the original method call with the new method call. + replaceInstruction( + formatLinkCallIndex, + "invoke-static { v${formatLinkCall.registerE} }, $FORMAT_METHOD_RESOURCE_REFERENCE", + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitter/misc/links/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/links/Fingerprints.kt new file mode 100644 index 000000000..0d5d0e6f8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/links/Fingerprints.kt @@ -0,0 +1,31 @@ +package app.revanced.patches.twitter.misc.links + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags + +internal val openLinkFingerprint = fingerprint { + returns("V") + parameters("Landroid/content/Context;", "Landroid/content/Intent;", "Landroid/os/Bundle;") +} + +internal val sanitizeSharingLinksFingerprint = fingerprint { + returns("Ljava/lang/String;") + strings("", "shareParam", "sessionToken") +} + +// Returns a shareable link string based on a tweet ID and a username. +internal val linkBuilderFingerprint = fingerprint { + strings("/%1\$s/status/%2\$d") +} + +// Gets Resource string for share link view available by pressing "Share via" button. +internal val linkResourceGetterFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + parameters("Landroid/content/res/Resources;") + literal { tweetShareLinkTemplateId } +} + +internal val linkSharingDomainFingerprint = fingerprint { + strings("https://fxtwitter.com") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitter/misc/links/OpenLinksWithAppChooserPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/links/OpenLinksWithAppChooserPatch.kt new file mode 100644 index 000000000..03033e7d5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/links/OpenLinksWithAppChooserPatch.kt @@ -0,0 +1,28 @@ +package app.revanced.patches.twitter.misc.links + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val openLinksWithAppChooserPatch = bytecodePatch( + name = "Open links with app chooser", + description = "Instead of opening links directly, open them with an app chooser. " + + "As a result you can select a browser to open the link with.", + use = false, +) { + compatibleWith("com.twitter.android"("10.48.0-release.0")) + + execute { + val methodReference = + "Lapp/revanced/extension/twitter/patches/links/OpenLinksWithAppChooserPatch;->" + + "openWithChooser(Landroid/content/Context;Landroid/content/Intent;)V" + + openLinkFingerprint.method.addInstructions( + 0, + """ + invoke-static { p0, p1 }, $methodReference + return-void + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/twitter/misc/links/SanitizeSharingLinksPatch.kt b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/links/SanitizeSharingLinksPatch.kt new file mode 100644 index 000000000..1cde6fc82 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/twitter/misc/links/SanitizeSharingLinksPatch.kt @@ -0,0 +1,23 @@ +package app.revanced.patches.twitter.misc.links + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val sanitizeSharingLinksPatch = bytecodePatch( + name = "Sanitize sharing links", + description = "Removes the tracking query parameters from links before they are shared.", +) { + compatibleWith("com.twitter.android") + + execute { + sanitizeSharingLinksFingerprint.method.addInstructions( + 0, + """ + # Method takes in a link (string, param 0) and then appends the tracking query params, + # so all we need to do is return back the passed-in string + return-object p0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/vsco/misc/pro/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/vsco/misc/pro/Fingerprints.kt new file mode 100644 index 000000000..0809324c7 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/vsco/misc/pro/Fingerprints.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.vsco.misc.pro + +import app.revanced.patcher.fingerprint + +internal val revCatSubscriptionFingerprint = fingerprint { + returns("V") + strings("use_debug_subscription_settings") + custom { _, classDef -> + classDef.endsWith("/RevCatSubscriptionSettingsRepository;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/vsco/misc/pro/UnlockProPatch.kt b/patches/src/main/kotlin/app/revanced/patches/vsco/misc/pro/UnlockProPatch.kt new file mode 100644 index 000000000..2135b1ec4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/vsco/misc/pro/UnlockProPatch.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.vsco.misc.pro + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val unlockProPatch = bytecodePatch( + name = "Unlock pro", + description = "Unlocks pro features.", +) { + compatibleWith("com.vsco.cam"("345")) + + execute { + // Set isSubscribed to true. + revCatSubscriptionFingerprint.method.addInstruction(0, "const p1, 0x1") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/warnwetter/misc/firebasegetcert/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/warnwetter/misc/firebasegetcert/Fingerprints.kt new file mode 100644 index 000000000..6eb7bd176 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/warnwetter/misc/firebasegetcert/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.warnwetter.misc.firebasegetcert + +import app.revanced.patcher.fingerprint + +internal val getMessagingCertFingerprint = fingerprint { + returns("Ljava/lang/String;") + strings( + "ContentValues", + "Could not get fingerprint hash for package: ", + "No such package: ", + ) +} + +internal val getRegistrationCertFingerprint = fingerprint { + returns("Ljava/lang/String;") + strings( + "FirebaseRemoteConfig", + "Could not get fingerprint hash for package: ", + "No such package: ", + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/warnwetter/misc/firebasegetcert/FirebaseGetCertPatch.kt b/patches/src/main/kotlin/app/revanced/patches/warnwetter/misc/firebasegetcert/FirebaseGetCertPatch.kt new file mode 100644 index 000000000..82a9008a4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/warnwetter/misc/firebasegetcert/FirebaseGetCertPatch.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.warnwetter.misc.firebasegetcert + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +val firebaseGetCertPatch = bytecodePatch( + description = "Spoofs the X-Android-Cert header.", +) { + compatibleWith("de.dwd.warnapp") + + execute { + listOf(getRegistrationCertFingerprint, getMessagingCertFingerprint).forEach { match -> + match.method.addInstructions( + 0, + """ + const-string v0, "0799DDF0414D3B3475E88743C91C0676793ED450" + return-object v0 + """, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/warnwetter/misc/promocode/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/warnwetter/misc/promocode/Fingerprints.kt new file mode 100644 index 000000000..d33880de7 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/warnwetter/misc/promocode/Fingerprints.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.warnwetter.misc.promocode + +import app.revanced.patcher.fingerprint + +internal val promoCodeUnlockFingerprint = fingerprint { + custom { method, classDef -> + classDef.endsWith("PromoTokenVerification;") && method.name == "isValid" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/warnwetter/misc/promocode/PromoCodeUnlockPatch.kt b/patches/src/main/kotlin/app/revanced/patches/warnwetter/misc/promocode/PromoCodeUnlockPatch.kt new file mode 100644 index 000000000..8acb8a650 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/warnwetter/misc/promocode/PromoCodeUnlockPatch.kt @@ -0,0 +1,25 @@ +package app.revanced.patches.warnwetter.misc.promocode + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.warnwetter.misc.firebasegetcert.firebaseGetCertPatch + +@Suppress("unused") +val promoCodeUnlockPatch = bytecodePatch( + name = "Promo code unlock", + description = "Disables the validation of promo code. Any code will work to unlock all features.", +) { + dependsOn(firebaseGetCertPatch) + + compatibleWith("de.dwd.warnapp"("4.2.2")) + + execute { + promoCodeUnlockFingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/willhaben/ads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/willhaben/ads/Fingerprints.kt new file mode 100644 index 000000000..e326c2682 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/willhaben/ads/Fingerprints.kt @@ -0,0 +1,26 @@ +package app.revanced.patches.willhaben.ads + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val adResolverFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("L") + parameters("L", "L") + strings( + "Google Ad is invalid ", + "Google Native Ad is invalid ", + "Criteo Ad is invalid ", + "Amazon Ad is invalid ", + ) +} + +internal val whAdViewInjectorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L", "L", "L", "Z") + strings("successfulAdView") + custom { _, classDef -> + classDef.type == "Lat/willhaben/advertising/WHAdView;" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/willhaben/ads/HideAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/willhaben/ads/HideAdsPatch.kt new file mode 100644 index 000000000..b3dd1a3fc --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/willhaben/ads/HideAdsPatch.kt @@ -0,0 +1,17 @@ +package app.revanced.patches.willhaben.ads + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.util.returnEarly + +@Suppress("unused") +internal val hideAdsPatch = bytecodePatch( + name = "Hide ads", + description = "Hides all in-app ads.", +) { + compatibleWith("at.willhaben") + + execute { + adResolverFingerprint.method.returnEarly() + whAdViewInjectorFingerprint.method.returnEarly() + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/windyapp/misc/unlockpro/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/windyapp/misc/unlockpro/Fingerprints.kt new file mode 100644 index 000000000..f199f127c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/windyapp/misc/unlockpro/Fingerprints.kt @@ -0,0 +1,10 @@ +package app.revanced.patches.windyapp.misc.unlockpro + +import app.revanced.patcher.fingerprint + +internal val checkProFingerprint = fingerprint { + returns("I") + custom { method, classDef -> + classDef.endsWith("RawUserData;") && method.name == "isPro" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/windyapp/misc/unlockpro/UnlockProPatch.kt b/patches/src/main/kotlin/app/revanced/patches/windyapp/misc/unlockpro/UnlockProPatch.kt new file mode 100644 index 000000000..2478b235d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/windyapp/misc/unlockpro/UnlockProPatch.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.windyapp.misc.unlockpro + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val unlockProPatch = bytecodePatch( + name = "Unlock pro", + description = "Unlocks all pro features.", +) { + compatibleWith("co.windyapp.android") + + execute { + checkProFingerprint.method.addInstructions( + 0, + """ + const/16 v0, 0x1 + return v0 + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt new file mode 100644 index 000000000..3bd82fb26 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/general/HideAdsPatch.kt @@ -0,0 +1,119 @@ +package app.revanced.patches.youtube.ad.general + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.fix.verticalscroll.verticalScrollPatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.ad.getpremium.hideGetPremiumPatch +import app.revanced.patches.youtube.misc.fix.backtoexitgesture.fixBackToExitGesturePatch +import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter +import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.findMutableMethodOf +import app.revanced.util.injectHideViewCall +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction31i +import com.android.tools.smali.dexlib2.iface.instruction.formats.Instruction35c + +internal var adAttributionId = -1L + private set + +private val hideAdsResourcePatch = resourcePatch { + dependsOn( + lithoFilterPatch, + settingsPatch, + resourceMappingPatch, + addResourcesPatch, + ) + + execute { + addResources("youtube", "ad.general.hideAdsResourcePatch") + + PreferenceScreen.ADS.addPreferences( + SwitchPreference("revanced_hide_general_ads"), + SwitchPreference("revanced_hide_fullscreen_ads"), + SwitchPreference("revanced_hide_buttoned_ads"), + SwitchPreference("revanced_hide_paid_promotion_label"), + SwitchPreference("revanced_hide_player_store_shelf"), + SwitchPreference("revanced_hide_self_sponsor_ads"), + SwitchPreference("revanced_hide_products_banner"), + SwitchPreference("revanced_hide_shopping_links"), + SwitchPreference("revanced_hide_visit_store_button"), + SwitchPreference("revanced_hide_web_search_results"), + SwitchPreference("revanced_hide_merchandise_banners"), + ) + + addLithoFilter("Lapp/revanced/extension/youtube/patches/components/AdsFilter;") + + adAttributionId = resourceMappings["id", "ad_attribution"] + } +} + +@Suppress("unused") +val hideAdsPatch = bytecodePatch( + name = "Hide ads", + description = "Adds options to remove general ads.", +) { + dependsOn( + hideGetPremiumPatch, + hideAdsResourcePatch, + verticalScrollPatch, + fixBackToExitGesturePatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + classes.forEach { classDef -> + classDef.methods.forEach { method -> + with(method.implementation) { + 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 != adAttributionId) { + return@forEachIndexed + } + + val insertIndex = index + 1 + + // Call to get the view with the id adAttribution + with(instructions.elementAt(insertIndex)) { + if (opcode != Opcode.INVOKE_VIRTUAL) { + return@forEachIndexed + } + + // Hide the view + val viewRegister = (this as Instruction35c).registerC + proxy(classDef) + .mutableClass + .findMutableMethodOf(method) + .injectHideViewCall( + insertIndex, + viewRegister, + "Lapp/revanced/extension/youtube/patches/components/AdsFilter;", + "hideAdAttributionView", + ) + } + } + } + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/getpremium/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/getpremium/Fingerprints.kt new file mode 100644 index 000000000..7629d1760 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/getpremium/Fingerprints.kt @@ -0,0 +1,21 @@ +package app.revanced.patches.youtube.ad.getpremium + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val getPremiumViewFingerprint = fingerprint { + accessFlags(AccessFlags.PROTECTED, AccessFlags.FINAL) + returns("V") + parameters("I", "I") + opcodes( + Opcode.ADD_INT_2ADDR, + Opcode.ADD_INT_2ADDR, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID, + ) + custom { method, _ -> + method.name == "onMeasure" && + method.definingClass == "Lcom/google/android/apps/youtube/app/red/presenter/CompactYpcOfferModuleView;" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/getpremium/HideGetPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/getpremium/HideGetPremiumPatch.kt new file mode 100644 index 000000000..b4f044228 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/getpremium/HideGetPremiumPatch.kt @@ -0,0 +1,68 @@ +package app.revanced.patches.youtube.ad.getpremium + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +internal const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/HideGetPremiumPatch;" + +val hideGetPremiumPatch = bytecodePatch( + description = "Hides YouTube Premium signup promotions under the video player.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "ad.getpremium.hideGetPremiumPatch") + + PreferenceScreen.ADS.addPreferences( + SwitchPreference("revanced_hide_get_premium"), + ) + + getPremiumViewFingerprint.method.apply { + val startIndex = getPremiumViewFingerprint.patternMatch!!.startIndex + val measuredWidthRegister = getInstruction(startIndex).registerA + val measuredHeightInstruction = getInstruction(startIndex + 1) + + val measuredHeightRegister = measuredHeightInstruction.registerA + val tempRegister = measuredHeightInstruction.registerB + + addInstructionsWithLabels( + startIndex + 2, + """ + # Override the internal measurement of the layout with zero values. + invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->hideGetPremiumView()Z + move-result v$tempRegister + if-eqz v$tempRegister, :allow + const/4 v$measuredWidthRegister, 0x0 + const/4 v$measuredHeightRegister, 0x0 + :allow + nop + # Layout width/height is then passed to a protected class method. + """, + ) + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/ad/video/fingerprints/LoadVideoAdsFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/Fingerprints.kt similarity index 56% rename from src/main/kotlin/app/revanced/patches/youtube/ad/video/fingerprints/LoadVideoAdsFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/Fingerprints.kt index 240886f01..91cc0e8df 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/ad/video/fingerprints/LoadVideoAdsFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/Fingerprints.kt @@ -1,12 +1,11 @@ -package app.revanced.patches.youtube.ad.video.fingerprints +package app.revanced.patches.youtube.ad.video +import app.revanced.patcher.fingerprint -import app.revanced.patcher.fingerprint.MethodFingerprint - -internal object LoadVideoAdsFingerprint : MethodFingerprint( - strings = listOf( +internal val loadVideoAdsFingerprint = fingerprint { + strings( "TriggerBundle doesn't have the required metadata specified by the trigger ", "Tried to enter slot with no assigned slotAdapter", "Trying to enter a slot when a slot of same type and physical position is already active. Its status: ", ) -) \ No newline at end of file +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/VideoAdsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/VideoAdsPatch.kt new file mode 100644 index 000000000..b073814bf --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/ad/video/VideoAdsPatch.kt @@ -0,0 +1,54 @@ +package app.revanced.patches.youtube.ad.video + +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.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch + +@Suppress("unused") +val videoAdsPatch = bytecodePatch( + name = "Video ads", + description = "Adds an option to remove ads in the video player.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "ad.video.videoAdsPatch") + + PreferenceScreen.ADS.addPreferences( + SwitchPreference("revanced_hide_video_ads"), + ) + + loadVideoAdsFingerprint.method.addInstructionsWithLabels( + 0, + """ + invoke-static { }, Lapp/revanced/extension/youtube/patches/VideoAdsPatch;->shouldShowAds()Z + move-result v0 + if-nez v0, :show_video_ads + return-void + """, + ExternalLabel("show_video_ads", loadVideoAdsFingerprint.method.getInstruction(0)), + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoUrlPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoUrlPatch.kt new file mode 100644 index 000000000..f18264e93 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/copyvideourl/CopyVideoUrlPatch.kt @@ -0,0 +1,77 @@ +package app.revanced.patches.youtube.interaction.copyvideourl + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.playercontrols.* +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.video.information.videoInformationPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.copyResources + +private val copyVideoUrlResourcePatch = resourcePatch { + dependsOn( + settingsPatch, + playerControlsResourcePatch, + addResourcesPatch, + ) + + execute { + addResources("youtube", "interaction.copyvideourl.copyVideoUrlResourcePatch") + + PreferenceScreen.PLAYER.addPreferences( + SwitchPreference("revanced_copy_video_url"), + SwitchPreference("revanced_copy_video_url_timestamp"), + ) + + copyResources( + "copyvideourl", + ResourceGroup( + resourceDirectoryName = "drawable", + "revanced_yt_copy.xml", + "revanced_yt_copy_timestamp.xml", + ), + ) + + addBottomControl("copyvideourl") + } +} + +@Suppress("unused") +val copyVideoUrlPatch = bytecodePatch( + name = "Copy video URL", + description = "Adds options to display buttons in the video player to copy video URLs.", +) { + dependsOn( + copyVideoUrlResourcePatch, + playerControlsPatch, + videoInformationPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + val extensionPlayerPackage = "Lapp/revanced/extension/youtube/videoplayer" + val buttonsDescriptors = listOf( + "$extensionPlayerPackage/CopyVideoUrlButton;", + "$extensionPlayerPackage/CopyVideoUrlTimestampButton;", + ) + + buttonsDescriptors.forEach { descriptor -> + initializeBottomControl(descriptor) + injectVisibilityCheckCall(descriptor) + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/fingerprints/CreateDialogFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/Fingerprints.kt similarity index 51% rename from src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/fingerprints/CreateDialogFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/Fingerprints.kt index 232e65e6a..b25287590 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/fingerprints/CreateDialogFingerprint.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/Fingerprints.kt @@ -1,14 +1,14 @@ -package app.revanced.patches.youtube.interaction.dialog.fingerprints +package app.revanced.patches.youtube.interaction.dialog -import app.revanced.patcher.fingerprint.MethodFingerprint -import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint -internal object CreateDialogFingerprint : MethodFingerprint( - "V", - AccessFlags.PROTECTED.value, - listOf("L", "L", "Ljava/lang/String;"), - listOf( +internal val createDialogFingerprint = fingerprint { + accessFlags(AccessFlags.PROTECTED) + returns("V") + parameters("L", "L", "Ljava/lang/String;") + opcodes( Opcode.INVOKE_VIRTUAL, Opcode.MOVE_RESULT_OBJECT, Opcode.INVOKE_VIRTUAL, @@ -17,6 +17,6 @@ internal object CreateDialogFingerprint : MethodFingerprint( Opcode.MOVE_RESULT_OBJECT, Opcode.IPUT_OBJECT, Opcode.IGET_OBJECT, - Opcode.INVOKE_VIRTUAL // dialog.show() + Opcode.INVOKE_VIRTUAL, // dialog.show() ) -) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch.kt new file mode 100644 index 000000000..cf000474f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/dialog/RemoveViewerDiscretionDialogPatch.kt @@ -0,0 +1,58 @@ +package app.revanced.patches.youtube.interaction.dialog + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction + +@Suppress("unused") +val removeViewerDiscretionDialogPatch = bytecodePatch( + 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.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + val extensionMethodDescriptor = + "Lapp/revanced/extension/youtube/patches/RemoveViewerDiscretionDialogPatch;->" + + "confirmDialog(Landroid/app/AlertDialog;)V" + + execute { + addResources("youtube", "interaction.dialog.removeViewerDiscretionDialogPatch") + + PreferenceScreen.GENERAL_LAYOUT.addPreferences( + SwitchPreference("revanced_remove_viewer_discretion_dialog"), + ) + + createDialogFingerprint.method.apply { + val showDialogIndex = implementation!!.instructions.lastIndex - 2 + val dialogRegister = getInstruction(showDialogIndex).registerC + + replaceInstructions( + showDialogIndex, + "invoke-static { v$dialogRegister }, $extensionMethodDescriptor", + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt new file mode 100644 index 000000000..702270683 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/DownloadsPatch.kt @@ -0,0 +1,106 @@ +package app.revanced.patches.youtube.interaction.downloads + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.InputType +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.shared.misc.settings.preference.TextPreference +import app.revanced.patches.youtube.misc.playercontrols.* +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.mainActivityFingerprint +import app.revanced.patches.youtube.video.information.videoInformationPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.copyResources + +private val downloadsResourcePatch = resourcePatch { + dependsOn( + playerControlsResourcePatch, + settingsPatch, + addResourcesPatch, + ) + + execute { + addResources("youtube", "interaction.downloads.downloadsResourcePatch") + + PreferenceScreen.PLAYER.addPreferences( + PreferenceScreenPreference( + key = "revanced_external_downloader_screen", + sorting = Sorting.UNSORTED, + preferences = setOf( + SwitchPreference("revanced_external_downloader"), + SwitchPreference("revanced_external_downloader_action_button"), + TextPreference("revanced_external_downloader_name", inputType = InputType.TEXT), + ), + ), + ) + + copyResources( + "downloads", + ResourceGroup("drawable", "revanced_yt_download_button.xml"), + ) + + addBottomControl("downloads") + } +} + +internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/DownloadsPatch;" + +internal const val BUTTON_DESCRIPTOR = "Lapp/revanced/extension/youtube/videoplayer/ExternalDownloadButton;" + +@Suppress("unused") +val downloadsPatch = bytecodePatch( + name = "Downloads", + description = "Adds support to download videos with an external downloader app " + + "using the in-app download button or a video player action button.", +) { + dependsOn( + downloadsResourcePatch, + playerControlsPatch, + videoInformationPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + initializeBottomControl(BUTTON_DESCRIPTOR) + injectVisibilityCheckCall(BUTTON_DESCRIPTOR) + + // Main activity is used to launch downloader intent. + mainActivityFingerprint.method.apply { + addInstruction( + implementation!!.instructions.lastIndex, + "invoke-static { p0 }, $EXTENSION_CLASS_DESCRIPTOR->activityCreated(Landroid/app/Activity;)V", + ) + } + + offlineVideoEndpointFingerprint.method.apply { + addInstructionsWithLabels( + 0, + """ + invoke-static/range {p3 .. p3}, $EXTENSION_CLASS_DESCRIPTOR->inAppDownloadButtonOnClick(Ljava/lang/String;)Z + move-result v0 + if-eqz v0, :show_native_downloader + return-void + :show_native_downloader + nop + """, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/Fingerprints.kt new file mode 100644 index 000000000..f10fc8e83 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/Fingerprints.kt @@ -0,0 +1,16 @@ +package app.revanced.patches.youtube.interaction.downloads + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val offlineVideoEndpointFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters( + "Ljava/util/Map;", + "L", + "Ljava/lang/String", // VideoId + "L", + ) + strings("Object is not an offlineable video: ") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/DisablePreciseSeekingGesturePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/DisablePreciseSeekingGesturePatch.kt new file mode 100644 index 000000000..a8ec77ebf --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/DisablePreciseSeekingGesturePatch.kt @@ -0,0 +1,76 @@ +package app.revanced.patches.youtube.interaction.seekbar + +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.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch + +@Suppress("unused") +val disablePreciseSeekingGesturePatch = bytecodePatch( + name = "Disable precise seeking gesture", + description = "Adds an option to disable precise seeking when swiping up on the seekbar.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "interaction.seekbar.disablePreciseSeekingGesturePatch") + + PreferenceScreen.SEEKBAR.addPreferences( + SwitchPreference("revanced_disable_precise_seeking_gesture"), + ) + val extensionMethodDescriptor = + "Lapp/revanced/extension/youtube/patches/DisablePreciseSeekingGesturePatch;" + + allowSwipingUpGestureFingerprint.match( + swipingUpGestureParentFingerprint.originalClassDef, + ).method.apply { + addInstructionsWithLabels( + 0, + """ + invoke-static { }, $extensionMethodDescriptor->isGestureDisabled()Z + move-result v0 + if-eqz v0, :disabled + return-void + """, + ExternalLabel("disabled", getInstruction(0)), + ) + } + + showSwipingUpGuideFingerprint.match( + swipingUpGestureParentFingerprint.originalClassDef, + ).method.apply { + addInstructionsWithLabels( + 0, + """ + invoke-static { }, $extensionMethodDescriptor->isGestureDisabled()Z + move-result v0 + if-eqz v0, :disabled + const/4 v0, 0x0 + return v0 + """, + ExternalLabel("disabled", getInstruction(0)), + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableSeekbarTappingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableSeekbarTappingPatch.kt new file mode 100644 index 000000000..afc2eddae --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableSeekbarTappingPatch.kt @@ -0,0 +1,84 @@ +package app.revanced.patches.youtube.interaction.seekbar + +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.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +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") +val enableSeekbarTappingPatch = bytecodePatch( + name = "Seekbar tapping", + description = "Adds an option to enable tap-to-seek on the seekbar of the video player.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + // 18.38.44 patches but crashes on startup. + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "interaction.seekbar.enableSeekbarTappingPatch") + + PreferenceScreen.SEEKBAR.addPreferences( + SwitchPreference("revanced_seekbar_tapping"), + ) + + // Find the required methods to tap the seekbar. + val patternMatch = onTouchEventHandlerFingerprint.patternMatch!! + + fun getReference(index: Int) = onTouchEventHandlerFingerprint.method.getInstruction(index) + .reference as MethodReference + + val seekbarTappingMethods = buildMap { + put("N", getReference(patternMatch.startIndex)) + put("O", getReference(patternMatch.endIndex)) + } + + val insertIndex = seekbarTappingFingerprint.patternMatch!!.endIndex - 1 + + seekbarTappingFingerprint.method.apply { + val thisInstanceRegister = getInstruction(insertIndex - 1).registerC + + val freeRegister = 0 + val xAxisRegister = 2 + + val oMethod = seekbarTappingMethods["O"]!! + val nMethod = seekbarTappingMethods["N"]!! + + fun MethodReference.toInvokeInstructionString() = + "invoke-virtual { v$thisInstanceRegister, v$xAxisRegister }, $this" + + addInstructionsWithLabels( + insertIndex, + """ + invoke-static { }, Lapp/revanced/extension/youtube/patches/SeekbarTappingPatch;->seekbarTappingEnabled()Z + move-result v$freeRegister + if-eqz v$freeRegister, :disabled + ${oMethod.toInvokeInstructionString()} + ${nMethod.toInvokeInstructionString()} + """, + ExternalLabel("disabled", getInstruction(insertIndex)), + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch.kt new file mode 100644 index 000000000..12d7451d6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/EnableSlideToSeekPatch.kt @@ -0,0 +1,122 @@ +package app.revanced.patches.youtube.interaction.seekbar + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.playservice.is_19_17_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.findInstructionIndicesReversed +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.sun.org.apache.bcel.internal.generic.InstructionConst.getInstruction + +internal const val EXTENSION_METHOD_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/SlideToSeekPatch;->isSlideToSeekDisabled(Z)Z" + +@Suppress("unused") +val enableSlideToSeekPatch = bytecodePatch( + name = "Enable slide to seek", + description = "Adds an option to enable slide to seek " + + "instead of playing at 2x speed when pressing and holding in the video player. " + + "Including this patch may cause issues with tapping or double tapping the video player overlay.", + use = false, +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + versionCheckPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "interaction.seekbar.enableSlideToSeekPatch") + + PreferenceScreen.SEEKBAR.addPreferences( + SwitchPreference("revanced_slide_to_seek"), + ) + + var modifiedMethods = false + + // Restore the behaviour to slide to seek. + + val checkIndex = slideToSeekFingerprint.patternMatch!!.startIndex + val checkReference = slideToSeekFingerprint.method.getInstruction(checkIndex) + .getReference()!! + + // A/B check method was only called on this class. + slideToSeekFingerprint.classDef.methods.forEach { method -> + method.findInstructionIndicesReversed { + opcode == Opcode.INVOKE_VIRTUAL && getReference() == checkReference + }.forEach { index -> + method.apply { + val register = getInstruction(index + 1).registerA + + addInstructions( + index + 2, + """ + invoke-static { v$register }, $EXTENSION_METHOD_DESCRIPTOR + move-result v$register + """, + ) + } + + modifiedMethods = true + } + } + + if (!modifiedMethods) throw PatchException("Could not find methods to modify") + + // Disable the double speed seek gesture. + if (is_19_17_or_greater) { + arrayOf( + disableFastForwardGestureFingerprint, + disableFastForwardNoticeFingerprint, + ).forEach { fingerprint -> + fingerprint.method.apply { + val targetIndex = fingerprint.patternMatch!!.endIndex + val targetRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, + """ + invoke-static { v$targetRegister }, $EXTENSION_METHOD_DESCRIPTOR + move-result v$targetRegister + """, + ) + } + } + } else { + disableFastForwardLegacyFingerprint.method.apply { + val insertIndex = disableFastForwardLegacyFingerprint.patternMatch!!.endIndex + 1 + val targetRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, + """ + invoke-static { v$targetRegister }, $EXTENSION_METHOD_DESCRIPTOR + move-result v$targetRegister + """, + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/Fingerprints.kt new file mode 100644 index 000000000..968ba89db --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/Fingerprints.kt @@ -0,0 +1,132 @@ +package app.revanced.patches.youtube.interaction.seekbar + +import app.revanced.patcher.fingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.reference.StringReference + +internal val swipingUpGestureParentFingerprint = fingerprint { + returns("Z") + parameters() + literal { 45379021 } +} + +/** + * Resolves using the class found in [swipingUpGestureParentFingerprint]. + */ +internal val showSwipingUpGuideFingerprint = fingerprint { + accessFlags(AccessFlags.FINAL) + returns("Z") + parameters() + literal { 1 } +} + +/** + * Resolves using the class found in [swipingUpGestureParentFingerprint]. + */ +internal val allowSwipingUpGestureFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L") +} + +internal val disableFastForwardLegacyFingerprint = fingerprint { + returns("Z") + parameters() + opcodes(Opcode.MOVE_RESULT) + literal { 45411330 } +} + +internal val disableFastForwardGestureFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters() + opcodes( + Opcode.IF_EQZ, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + ) + custom { methodDef, classDef -> + methodDef.implementation!!.instructions.count() > 30 && + classDef.type.endsWith("/NextGenWatchLayout;") + } +} + +internal val disableFastForwardNoticeFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters() + opcodes( + Opcode.CHECK_CAST, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + ) + custom { method, _ -> + method.name == "run" && method.indexOfFirstInstruction { + // In later targets the code is found in different methods with different strings. + val string = getReference()?.string + string == "Failed to easy seek haptics vibrate." || string == "search_landing_cache_key" + } >= 0 + } +} + +internal val onTouchEventHandlerFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.PUBLIC) + returns("Z") + parameters("L") + opcodes( + Opcode.INVOKE_VIRTUAL, // nMethodReference + Opcode.RETURN, + Opcode.IGET_OBJECT, + Opcode.IGET_BOOLEAN, + Opcode.IF_EQZ, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN, + Opcode.INT_TO_FLOAT, + Opcode.INT_TO_FLOAT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ, + Opcode.INVOKE_VIRTUAL, + Opcode.INVOKE_VIRTUAL, // oMethodReference + ) + custom { method, _ -> method.name == "onTouchEvent" } +} + +internal val seekbarTappingFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters("L") + opcodes( + Opcode.IPUT_OBJECT, + Opcode.INVOKE_VIRTUAL, + // Insert seekbar tapping instructions here. + Opcode.RETURN, + Opcode.INVOKE_VIRTUAL, + ) + literal { Integer.MAX_VALUE.toLong() } +} + +internal val slideToSeekFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returns("V") + parameters("Landroid/view/View;", "F") + opcodes( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ, + Opcode.GOTO_16, + ) + literal { 67108864 } +} + +internal val fullscreenSeekbarThumbnailsQualityFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters() + literal { 45399684L } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarThumbnailsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarThumbnailsPatch.kt new file mode 100644 index 000000000..a9ad7563b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/seekbar/SeekbarThumbnailsPatch.kt @@ -0,0 +1,78 @@ +package app.revanced.patches.youtube.interaction.seekbar + +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.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.layout.seekbar.fullscreenSeekbarThumbnailsFingerprint +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.playservice.is_19_17_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/SeekbarThumbnailsPatch;" + +@Suppress("unused") +val seekbarThumbnailsPatch = bytecodePatch( + name = "Seekbar thumbnails", + description = "Adds an option to use high quality fullscreen seekbar thumbnails. " + + "Patching 19.16.39 or lower adds an option to restore old seekbar thumbnails.", +) { + dependsOn( + sharedExtensionPatch, + addResourcesPatch, + versionCheckPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ) + ) + + execute { + addResources("youtube", "layout.seekbar.seekbarThumbnailsPatch") + + if (is_19_17_or_greater) { + PreferenceScreen.SEEKBAR.addPreferences( + SwitchPreference("revanced_seekbar_thumbnails_high_quality") + ) + } else { + PreferenceScreen.SEEKBAR.addPreferences( + SwitchPreference("revanced_restore_old_seekbar_thumbnails"), + SwitchPreference( + key = "revanced_seekbar_thumbnails_high_quality", + summaryOnKey = "revanced_seekbar_thumbnails_high_quality_legacy_summary_on", + summaryOffKey = "revanced_seekbar_thumbnails_high_quality_legacy_summary_on" + ) + ) + + fullscreenSeekbarThumbnailsFingerprint.method.apply { + val moveResultIndex = instructions.lastIndex - 1 + + addInstruction( + moveResultIndex, + "invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->useFullscreenSeekbarThumbnails()Z", + ) + } + } + + fullscreenSeekbarThumbnailsQualityFingerprint.method.addInstructions( + 0, + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->useHighQualityFullscreenThumbnails()Z + move-result v0 + return v0 + """ + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/Fingerprints.kt new file mode 100644 index 000000000..0a31f1c30 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/Fingerprints.kt @@ -0,0 +1,12 @@ +package app.revanced.patches.youtube.interaction.swipecontrols + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val swipeControlsHostActivityFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + parameters() + custom { method, _ -> + method.definingClass == "Lapp/revanced/extension/youtube/swipecontrols/SwipeControlsHostActivity;" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt new file mode 100644 index 000000000..6b6668420 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/SwipeControlsPatch.kt @@ -0,0 +1,102 @@ +package app.revanced.patches.youtube.interaction.swipecontrols + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.InputType +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.shared.misc.settings.preference.TextPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.mainActivityFingerprint +import app.revanced.util.* +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod + +private val swipeControlsResourcePatch = resourcePatch { + dependsOn( + settingsPatch, + addResourcesPatch, + ) + + execute { + addResources("youtube", "interaction.swipecontrols.swipeControlsResourcePatch") + + PreferenceScreen.SWIPE_CONTROLS.addPreferences( + SwitchPreference("revanced_swipe_brightness"), + SwitchPreference("revanced_swipe_volume"), + SwitchPreference("revanced_swipe_press_to_engage"), + SwitchPreference("revanced_swipe_haptic_feedback"), + SwitchPreference("revanced_swipe_save_and_restore_brightness"), + SwitchPreference("revanced_swipe_lowest_value_enable_auto_brightness"), + TextPreference("revanced_swipe_overlay_timeout", inputType = InputType.NUMBER), + TextPreference("revanced_swipe_text_overlay_size", inputType = InputType.NUMBER), + TextPreference("revanced_swipe_overlay_background_alpha", inputType = InputType.NUMBER), + TextPreference("revanced_swipe_threshold", inputType = InputType.NUMBER), + ) + + copyResources( + "swipecontrols", + ResourceGroup( + "drawable", + "revanced_ic_sc_brightness_auto.xml", + "revanced_ic_sc_brightness_manual.xml", + "revanced_ic_sc_volume_mute.xml", + "revanced_ic_sc_volume_normal.xml", + ), + ) + } +} + +@Suppress("unused") +val swipeControlsPatch = bytecodePatch( + name = "Swipe controls", + description = "Adds options to enable and configure volume and brightness swipe controls.", +) { + dependsOn( + sharedExtensionPatch, + playerTypeHookPatch, + swipeControlsResourcePatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + val wrapperClass = swipeControlsHostActivityFingerprint.classDef + val targetClass = mainActivityFingerprint.classDef + + // Inject the wrapper class from the extension into the class hierarchy of MainActivity. + wrapperClass.setSuperClass(targetClass.superclass) + targetClass.setSuperClass(wrapperClass.type) + + // Ensure all classes and methods in the hierarchy are non-final, so we can override them in the extension. + traverseClassHierarchy(targetClass) { + accessFlags = accessFlags and AccessFlags.FINAL.value.inv() + transformMethods { + ImmutableMethod( + definingClass, + name, + parameters, + returnType, + accessFlags and AccessFlags.FINAL.value.inv(), + annotations, + hiddenApiRestrictions, + implementation, + ).toMutable() + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch.kt new file mode 100644 index 000000000..4dfa092f0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/AutoCaptionsPatch.kt @@ -0,0 +1,70 @@ +package app.revanced.patches.youtube.layout.autocaptions + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.subtitleButtonControllerFingerprint + +@Suppress("unused") +val autoCaptionsPatch = bytecodePatch( + name = "Disable auto captions", + description = "Adds an option to disable captions from being automatically enabled.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.autocaptions.autoCaptionsPatch") + + PreferenceScreen.PLAYER.addPreferences( + SwitchPreference("revanced_auto_captions"), + ) + + mapOf( + startVideoInformerFingerprint to 0, + subtitleButtonControllerFingerprint to 1, + ).forEach { (fingerprint, enabled) -> + fingerprint.method.addInstructions( + 0, + """ + const/4 v0, 0x$enabled + sput-boolean v0, Lapp/revanced/extension/youtube/patches/DisableAutoCaptionsPatch;->captionsButtonDisabled:Z + """, + ) + } + + subtitleTrackFingerprint.method.addInstructions( + 0, + """ + invoke-static {}, Lapp/revanced/extension/youtube/patches/DisableAutoCaptionsPatch;->autoCaptionsEnabled()Z + move-result v0 + if-eqz v0, :auto_captions_enabled + sget-boolean v0, Lapp/revanced/extension/youtube/patches/DisableAutoCaptionsPatch;->captionsButtonDisabled:Z + if-nez v0, :auto_captions_enabled + const/4 v0, 0x1 + return v0 + :auto_captions_enabled + nop + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/Fingerprints.kt new file mode 100644 index 000000000..3e1c15629 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/autocaptions/Fingerprints.kt @@ -0,0 +1,33 @@ +package app.revanced.patches.youtube.layout.autocaptions + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val startVideoInformerFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + opcodes( + Opcode.INVOKE_INTERFACE, + Opcode.RETURN_VOID, + ) + strings("pc") +} + +internal val subtitleTrackFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters() + opcodes( + Opcode.CONST_STRING, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.RETURN, + ) + strings("DISABLE_CAPTIONS_OPTION") + custom { _, classDef -> + classDef.endsWith("SubtitleTrack;") + } +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/layout/branding/CustomBrandingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/CustomBrandingPatch.kt similarity index 57% rename from src/main/kotlin/app/revanced/patches/youtube/layout/branding/CustomBrandingPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/CustomBrandingPatch.kt index ec720ec97..db37550c8 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/layout/branding/CustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/CustomBrandingPatch.kt @@ -1,45 +1,49 @@ package app.revanced.patches.youtube.layout.branding -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.annotation.CompatiblePackage -import app.revanced.patcher.patch.annotation.Patch -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.misc.playservice.is_19_34_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.util.ResourceGroup import app.revanced.util.Utils.trimIndentMultiline import app.revanced.util.copyResources import java.io.File import java.nio.file.Files -@Patch( +private const val REVANCED_ICON = "ReVanced*Logo" // Can never be a valid path. +private const val APP_NAME = "YouTube ReVanced" + +private val iconResourceFileNames = arrayOf( + "adaptiveproduct_youtube_background_color_108", + "adaptiveproduct_youtube_foreground_color_108", + "ic_launcher", + "ic_launcher_round", +).map { "$it.png" }.toTypedArray() + +private val iconResourceFileNamesNew = mapOf( + "adaptiveproduct_youtube_foreground_color_108" to "adaptiveproduct_youtube_2024_q4_foreground_color_108", + "adaptiveproduct_youtube_background_color_108" to "adaptiveproduct_youtube_2024_q4_background_color_108", +) + +private val mipmapDirectories = arrayOf( + "xxxhdpi", + "xxhdpi", + "xhdpi", + "hdpi", + "mdpi", +).map { "mipmap-$it" } + +@Suppress("unused") +val customBrandingPatch = resourcePatch( name = "Custom branding", description = "Applies a custom app name and icon. Defaults to \"YouTube ReVanced\" and the ReVanced logo.", - compatiblePackages = [ - CompatiblePackage("com.google.android.youtube"), - ], use = false, -) -@Suppress("unused") -object CustomBrandingPatch : ResourcePatch() { - private const val REVANCED_ICON = "ReVanced*Logo" // Can never be a valid path. - private const val APP_NAME = "YouTube ReVanced" +) { + dependsOn(versionCheckPatch) - private val iconResourceFileNames = arrayOf( - "adaptiveproduct_youtube_background_color_108", - "adaptiveproduct_youtube_foreground_color_108", - "ic_launcher", - "ic_launcher_round", - ).map { "$it.png" }.toTypedArray() + compatibleWith("com.google.android.youtube") - private val mipmapDirectories = arrayOf( - "xxxhdpi", - "xxhdpi", - "xhdpi", - "hdpi", - "mdpi", - ).map { "mipmap-$it" } - - private var appName by stringPatchOption( + val appName by stringOption( key = "appName", default = APP_NAME, values = mapOf( @@ -52,7 +56,7 @@ object CustomBrandingPatch : ResourcePatch() { description = "The name of the app.", ) - private var icon by stringPatchOption( + val icon by stringOption( key = "iconPath", default = REVANCED_ICON, values = mapOf("ReVanced Logo" to REVANCED_ICON), @@ -70,7 +74,7 @@ object CustomBrandingPatch : ResourcePatch() { """.trimIndentMultiline(), ) - override fun execute(context: ResourceContext) { + execute { icon?.let { icon -> // Change the app icon. mipmapDirectories.map { directory -> @@ -81,7 +85,7 @@ object CustomBrandingPatch : ResourcePatch() { }.let { resourceGroups -> if (icon != REVANCED_ICON) { val path = File(icon) - val resourceDirectory = context.get("res") + val resourceDirectory = get("res") resourceGroups.forEach { group -> val fromDirectory = path.resolve(group.resourceDirectoryName) @@ -95,14 +99,29 @@ object CustomBrandingPatch : ResourcePatch() { } } } else { - resourceGroups.forEach { context.copyResources("custom-branding", it) } + resourceGroups.forEach { copyResources("custom-branding", it) } + } + } + + if (is_19_34_or_greater) { + val resourceDirectory = get("res") + + mipmapDirectories.forEach { directory -> + val targetDirectory = resourceDirectory.resolve(directory) + + iconResourceFileNamesNew.forEach { (old, new) -> + val oldFile = targetDirectory.resolve("$old.png") + val newFile = targetDirectory.resolve("$new.png") + + Files.write(newFile.toPath(), oldFile.readBytes()) + } } } } appName?.let { name -> // Change the app name. - val manifest = context.get("AndroidManifest.xml") + val manifest = get("AndroidManifest.xml") manifest.writeText( manifest.readText() .replace( diff --git a/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt similarity index 73% rename from src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt index d0614297a..e45680789 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt @@ -1,47 +1,42 @@ package app.revanced.patches.youtube.layout.branding.header -import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.annotation.CompatiblePackage -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patcher.patch.options.PatchOption.PatchExtensions.stringPatchOption +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.patch.stringOption import app.revanced.util.ResourceGroup import app.revanced.util.Utils.trimIndentMultiline import app.revanced.util.copyResources import java.io.File -@Patch( +private const val HEADER_FILE_NAME = "yt_wordmark_header" +private const val PREMIUM_HEADER_FILE_NAME = "yt_premium_wordmark_header" + +private const val HEADER_OPTION = "header*" +private const val PREMIUM_HEADER_OPTION = "premium*header" +private const val REVANCED_HEADER_OPTION = "revanced*" +private const val REVANCED_BORDERLESS_HEADER_OPTION = "revanced*borderless" + +private val targetResourceDirectoryNames = mapOf( + "xxxhdpi" to "512px 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") + +@Suppress("unused") +val changeHeaderPatch = resourcePatch( name = "Change header", description = "Applies a custom header in the top left corner within the app. Defaults to the ReVanced header.", - compatiblePackages = [ - CompatiblePackage("com.google.android.youtube"), - ], use = false, -) -@Suppress("unused") -object ChangeHeaderPatch : ResourcePatch() { - private const val HEADER_FILE_NAME = "yt_wordmark_header" - private const val PREMIUM_HEADER_FILE_NAME = "yt_premium_wordmark_header" +) { + compatibleWith("com.google.android.youtube") - private const val HEADER_OPTION = "header*" - private const val PREMIUM_HEADER_OPTION = "premium*header" - private const val REVANCED_HEADER_OPTION = "revanced*" - private const val REVANCED_BORDERLESS_HEADER_OPTION = "revanced*borderless" - - private val targetResourceDirectoryNames = mapOf( - "xxxhdpi" to "512px 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 header by stringPatchOption( + val header by stringOption( key = "header", default = REVANCED_BORDERLESS_HEADER_OPTION, values = mapOf( @@ -68,10 +63,10 @@ object ChangeHeaderPatch : ResourcePatch() { required = true, ) - override fun execute(context: ResourceContext) { + execute { // The directories to copy the header to. val targetResourceDirectories = targetResourceDirectoryNames.keys.mapNotNull { - context.get("res").resolve(it).takeIf(File::exists) + get("res").resolve(it).takeIf(File::exists) } // The files to replace in the target directories. val targetResourceFiles = targetResourceDirectoryNames.keys.map { directoryName -> @@ -100,14 +95,14 @@ object ChangeHeaderPatch : ResourcePatch() { val toHeader = { overwriteFromTo(HEADER_FILE_NAME, PREMIUM_HEADER_FILE_NAME) } val toReVanced = { // Copy the ReVanced header to the resource directories. - targetResourceFiles.forEach { context.copyResources("change-header/revanced", it) } + targetResourceFiles.forEach { copyResources("change-header/revanced", it) } // Overwrite the premium with the custom header as well. toHeader() } val toReVancedBorderless = { // Copy the ReVanced borderless header to the resource directories. - targetResourceFiles.forEach { context.copyResources("change-header/revanced-borderless", it) } + targetResourceFiles.forEach { copyResources("change-header/revanced-borderless", it) } // Overwrite the premium with the custom header as well. toHeader() @@ -120,7 +115,7 @@ object ChangeHeaderPatch : ResourcePatch() { // For each source folder, copy the files to the target resource directories. sourceFolders.forEach { dpiSourceFolder -> - val targetDpiFolder = context.get("res").resolve(dpiSourceFolder.name) + val targetDpiFolder = get("res").resolve(dpiSourceFolder.name) if (!targetDpiFolder.exists()) return@forEach val imgSourceFiles = dpiSourceFolder.listFiles { file -> file.isFile }!! diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/action/HideButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/action/HideButtonsPatch.kt new file mode 100644 index 000000000..5508e9e23 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/action/HideButtonsPatch.kt @@ -0,0 +1,56 @@ +package app.revanced.patches.youtube.layout.buttons.action + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter +import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen + +@Suppress("unused") +val hideButtonsPatch = resourcePatch( + name = "Hide video action buttons", + description = "Adds options to hide action buttons (such as the Download button) under videos.", +) { + dependsOn( + resourceMappingPatch, + lithoFilterPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.buttons.action.hideButtonsPatch") + + PreferenceScreen.PLAYER.addPreferences( + PreferenceScreenPreference( + "revanced_hide_buttons_screen", + preferences = setOf( + SwitchPreference("revanced_hide_like_dislike_button"), + SwitchPreference("revanced_hide_share_button"), + SwitchPreference("revanced_hide_report_button"), + SwitchPreference("revanced_hide_remix_button"), + SwitchPreference("revanced_hide_download_button"), + SwitchPreference("revanced_hide_thanks_button"), + SwitchPreference("revanced_hide_clip_button"), + SwitchPreference("revanced_hide_playlist_button"), + ), + ), + ) + + addLithoFilter("Lapp/revanced/extension/youtube/patches/components/ButtonsFilter;") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/Fingerprints.kt new file mode 100644 index 000000000..eebd0befb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/Fingerprints.kt @@ -0,0 +1,25 @@ +package app.revanced.patches.youtube.layout.buttons.navigation + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal const val ANDROID_AUTOMOTIVE_STRING = "Android Automotive" + +internal val addCreateButtonViewFingerprint = fingerprint { + strings("Android Wear", ANDROID_AUTOMOTIVE_STRING) +} + +internal val createPivotBarFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + parameters( + "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;", + "Landroid/widget/TextView;", + "Ljava/lang/CharSequence;", + ) + opcodes( + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID, + ) +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationButtonsPatch.kt new file mode 100644 index 000000000..e25eb0f55 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/navigation/NavigationButtonsPatch.kt @@ -0,0 +1,104 @@ +package app.revanced.patches.youtube.layout.buttons.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.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.navigation.hookNavigationButtonCreated +import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +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 +import com.sun.org.apache.bcel.internal.generic.InstructionConst.getInstruction + +internal const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/NavigationButtonsPatch;" + +val navigationButtonsPatch = bytecodePatch( + name = "Navigation buttons", + description = "Adds options to hide and change navigation buttons (such as the Shorts button).", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + navigationBarHookPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.buttons.navigation.navigationButtonsPatch") + + PreferenceScreen.GENERAL_LAYOUT.addPreferences( + PreferenceScreenPreference( + key = "revanced_navigation_buttons_screen", + sorting = Sorting.UNSORTED, + preferences = setOf( + SwitchPreference("revanced_hide_home_button"), + SwitchPreference("revanced_hide_shorts_button"), + SwitchPreference("revanced_hide_create_button"), + SwitchPreference("revanced_hide_subscriptions_button"), + SwitchPreference("revanced_switch_create_with_notifications_button"), + SwitchPreference("revanced_hide_navigation_button_labels"), + ), + ), + ) + + // Switch create with notifications button. + addCreateButtonViewFingerprint.method.apply { + val stringIndex = addCreateButtonViewFingerprint.stringMatches!!.find { match -> + match.string == ANDROID_AUTOMOTIVE_STRING + }!!.index + + val conditionalCheckIndex = stringIndex - 1 + val conditionRegister = + getInstruction(conditionalCheckIndex).registerA + + addInstructions( + conditionalCheckIndex, + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->switchCreateWithNotificationButton()Z + move-result v$conditionRegister + """, + ) + } + + // Hide navigation button labels. + createPivotBarFingerprint.method.apply { + val setTextIndex = indexOfFirstInstructionOrThrow { + getReference()?.name == "setText" + } + + val targetRegister = getInstruction(setTextIndex).registerC + + addInstruction( + setTextIndex, + "invoke-static { v$targetRegister }, " + + "$EXTENSION_CLASS_DESCRIPTOR->hideNavigationButtonLabels(Landroid/widget/TextView;)V", + ) + } + + // Hook navigation button created, in order to hide them. + hookNavigationButtonCreated(EXTENSION_CLASS_DESCRIPTOR) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/Fingerprints.kt new file mode 100644 index 000000000..323603b50 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/Fingerprints.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.youtube.layout.buttons.overlay + +import app.revanced.patcher.fingerprint +import app.revanced.util.containsLiteralInstruction +import com.android.tools.smali.dexlib2.AccessFlags + +internal val playerControlsPreviousNextOverlayTouchFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + strings("1.0x") + custom { methodDef, _ -> + methodDef.containsLiteralInstruction(playerControlPreviousButtonTouchArea) && + methodDef.containsLiteralInstruction(playerControlNextButtonTouchArea) + } +} + +internal val mediaRouteButtonFingerprint = fingerprint { + parameters("I") + custom { methodDef, _ -> + methodDef.definingClass.endsWith("/MediaRouteButton;") && methodDef.name == "setVisibility" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt new file mode 100644 index 000000000..63c4af749 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/buttons/overlay/HidePlayerOverlayButtonsPatch.kt @@ -0,0 +1,150 @@ +package app.revanced.patches.youtube.layout.buttons.overlay + +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.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.layoutConstructorFingerprint +import app.revanced.patches.youtube.shared.subtitleButtonControllerFingerprint +import app.revanced.util.* +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 + +internal var playerControlPreviousButtonTouchArea = -1L + private set +internal var playerControlNextButtonTouchArea = -1L + private set + +private val hidePlayerOverlayButtonsResourcePatch = resourcePatch { + dependsOn(resourceMappingPatch) + + execute { + playerControlPreviousButtonTouchArea = resourceMappings["id", "player_control_previous_button_touch_area"] + playerControlNextButtonTouchArea = resourceMappings["id", "player_control_next_button_touch_area"] + } +} + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/HidePlayerOverlayButtonsPatch;" + +val hidePlayerOverlayButtonsPatch = bytecodePatch( + name = "Hide player overlay buttons", + description = "Adds options to hide the player cast, autoplay, caption button and next/ previous buttons.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + hidePlayerOverlayButtonsResourcePatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.buttons.overlay.hidePlayerOverlayButtonsPatch") + + PreferenceScreen.PLAYER.addPreferences( + SwitchPreference("revanced_hide_player_previous_next_buttons"), + SwitchPreference("revanced_hide_cast_button"), + SwitchPreference("revanced_hide_captions_button"), + SwitchPreference("revanced_hide_autoplay_button"), + ) + + // region Hide player next/previous button. + + playerControlsPreviousNextOverlayTouchFingerprint.method.apply { + val resourceIndex = indexOfFirstLiteralInstructionOrThrow(playerControlPreviousButtonTouchArea) + + val insertIndex = indexOfFirstInstructionOrThrow(resourceIndex) { + opcode == Opcode.INVOKE_STATIC && + getReference()?.parameterTypes?.firstOrNull() == "Landroid/view/View;" + } + + val viewRegister = getInstruction(insertIndex).registerC + + addInstruction( + insertIndex, + "invoke-static { v$viewRegister }, $EXTENSION_CLASS_DESCRIPTOR" + + "->hidePreviousNextButtons(Landroid/view/View;)V", + ) + } + + // endregion + + // region Hide cast button. + + mediaRouteButtonFingerprint.method.addInstructions( + 0, + """ + invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->getCastButtonOverrideV2(I)I + move-result p1 + """, + ) + + // endregion + + // region Hide captions button. + + subtitleButtonControllerFingerprint.method.apply { + // Due to previously applied patches, scanResult index cannot be used in this context + val insertIndex = indexOfFirstInstructionOrThrow(Opcode.IGET_BOOLEAN) + 1 + + addInstruction( + insertIndex, + "invoke-static {v0}, $EXTENSION_CLASS_DESCRIPTOR->hideCaptionsButton(Landroid/widget/ImageView;)V", + ) + } + + // endregion + + // region Hide autoplay button. + + layoutConstructorFingerprint.method.apply { + val constIndex = indexOfFirstResourceIdOrThrow("autonav_toggle") + val constRegister = getInstruction(constIndex).registerA + + // Add a conditional branch around the code that inflates and adds the auto-repeat button. + val gotoIndex = indexOfFirstInstructionOrThrow(constIndex) { + val parameterTypes = getReference()?.parameterTypes + opcode == Opcode.INVOKE_VIRTUAL && + parameterTypes?.size == 2 && + parameterTypes.first() == "Landroid/view/ViewStub;" + } + 1 + + addInstructionsWithLabels( + constIndex, + """ + invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->hideAutoPlayButton()Z + move-result v$constRegister + if-nez v$constRegister, :hidden + """, + ExternalLabel("hidden", getInstruction(gotoIndex)), + ) + } + + // endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/Fingerprints.kt new file mode 100644 index 000000000..59d859e80 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/Fingerprints.kt @@ -0,0 +1,40 @@ +package app.revanced.patches.youtube.layout.hide.endscreencards + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.Opcode + +internal val layoutCircleFingerprint = fingerprint { + returns("Landroid/view/View;") + opcodes( + Opcode.CONST, + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + ) + literal { layoutCircle } +} + +internal val layoutIconFingerprint = fingerprint { + returns("Landroid/view/View;") + opcodes( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + + ) + literal { layoutIcon } +} + +internal val layoutVideoFingerprint = fingerprint { + returns("Landroid/view/View;") + opcodes( + Opcode.CONST, + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + ) + literal { layoutVideo } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndscreenCardsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndscreenCardsPatch.kt new file mode 100644 index 000000000..56a97f241 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/endscreencards/HideEndscreenCardsPatch.kt @@ -0,0 +1,87 @@ +package app.revanced.patches.youtube.layout.hide.endscreencards + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +internal var layoutCircle = -1L + private set +internal var layoutIcon = -1L + private set +internal var layoutVideo = -1L + private set + +private val hideEndscreenCardsResourcePatch = resourcePatch { + dependsOn( + settingsPatch, + resourceMappingPatch, + addResourcesPatch, + ) + + execute { + addResources("youtube", "layout.hide.endscreencards.hideEndscreenCardsResourcePatch") + + PreferenceScreen.PLAYER.addPreferences( + SwitchPreference("revanced_hide_endscreen_cards"), + ) + + fun idOf(name: String) = resourceMappings["layout", "endscreen_element_layout_$name"] + + layoutCircle = idOf("circle") + layoutIcon = idOf("icon") + layoutVideo = idOf("video") + } +} + +@Suppress("unused") +val hideEndscreenCardsPatch = bytecodePatch( + name = "Hide endscreen cards", + description = "Adds an option to hide suggested video cards at the end of videos.", +) { + dependsOn( + sharedExtensionPatch, + hideEndscreenCardsResourcePatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + listOf( + layoutCircleFingerprint, + layoutIconFingerprint, + layoutVideoFingerprint, + ).forEach { fingerprint -> + fingerprint.method.apply { + val insertIndex = fingerprint.patternMatch!!.endIndex + 1 + val viewRegister = getInstruction(insertIndex - 1).registerA + + addInstruction( + insertIndex, + "invoke-static { v$viewRegister }, " + + "Lapp/revanced/extension/youtube/patches/HideEndscreenCardsPatch;->" + + "hideEndscreen(Landroid/view/View;)V", + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch.kt new file mode 100644 index 000000000..7c7de97a2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/fullscreenambientmode/DisableFullscreenAmbientModePatch.kt @@ -0,0 +1,64 @@ +package app.revanced.patches.youtube.layout.hide.fullscreenambientmode + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/DisableFullscreenAmbientModePatch;" + +@Suppress("unused") +val disableFullscreenAmbientModePatch = bytecodePatch( + name = "Disable fullscreen ambient mode", + description = "Adds an option to disable the ambient mode when in fullscreen.", +) { + dependsOn( + settingsPatch, + sharedExtensionPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.hide.fullscreenambientmode.disableFullscreenAmbientModePatch") + + PreferenceScreen.PLAYER.addPreferences( + SwitchPreference("revanced_disable_fullscreen_ambient_mode"), + ) + + setFullScreenBackgroundColorFingerprint.method.apply { + val insertIndex = indexOfFirstInstructionReversedOrThrow { + getReference()?.name == "setBackgroundColor" + } + val register = getInstruction(insertIndex).registerD + + addInstructions( + insertIndex, + """ + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getFullScreenBackgroundColor(I)I + move-result v$register + """, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/fullscreenambientmode/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/fullscreenambientmode/Fingerprints.kt new file mode 100644 index 000000000..b61902087 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/fullscreenambientmode/Fingerprints.kt @@ -0,0 +1,14 @@ +package app.revanced.patches.youtube.layout.hide.fullscreenambientmode + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val setFullScreenBackgroundColorFingerprint = fingerprint { + returns("V") + accessFlags(AccessFlags.PROTECTED, AccessFlags.FINAL) + parameters("Z", "I", "I", "I", "I") + custom { method, classDef -> + classDef.type.endsWith("/YouTubePlayerViewNotForReflection;") + && method.name == "onLayout" + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/Fingerprints.kt new file mode 100644 index 000000000..463e4cdc6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/Fingerprints.kt @@ -0,0 +1,116 @@ +package app.revanced.patches.youtube.layout.hide.general + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val hideShowMoreButtonFingerprint = fingerprint { + opcodes( + Opcode.CONST, + Opcode.CONST_4, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + ) + literal { expandButtonDownId } +} + +internal val parseElementFromBufferFingerprint = fingerprint { + parameters("L", "L", "[B", "L", "L") + opcodes( + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + ) + strings("Failed to parse Element") // String is a partial match. +} + +internal val playerOverlayFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("L") + strings("player_overlay_in_video_programming") +} + +internal val showWatermarkFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L", "L") +} + +internal val yoodlesImageViewFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Landroid/view/View;") + parameters("L", "L") + literal { youTubeLogo } +} + +internal val crowdfundingBoxFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + opcodes( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IPUT_OBJECT, + ) + literal { crowdfundingBoxId } +} + +internal val albumCardsFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + opcodes( + Opcode.MOVE_RESULT_OBJECT, + Opcode.CONST, + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + ) + literal { albumCardId } +} + +internal val filterBarHeightFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + opcodes( + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IPUT, + ) + literal { filterBarHeightId } +} + +internal val relatedChipCloudFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + opcodes( + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + ) + literal { relatedChipCloudMarginId } +} + +internal val searchResultsChipBarFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + opcodes( + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + ) + literal { barContainerHeightId } +} + +internal val showFloatingMicrophoneButtonFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters() + opcodes( + Opcode.IGET_BOOLEAN, + Opcode.IF_EQZ, + Opcode.RETURN_VOID, + ) + literal { fabButtonId } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt new file mode 100644 index 000000000..39cbc4808 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/general/HideLayoutComponentsPatch.kt @@ -0,0 +1,420 @@ +package app.revanced.patches.youtube.layout.hide.general + +import app.revanced.patcher.Fingerprint +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.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.shared.misc.settings.preference.* +import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter +import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch +import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.findInstructionIndicesReversedOrThrow +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +var expandButtonDownId = -1L + private set +var albumCardId = -1L + private set +var crowdfundingBoxId = -1L + private set +var youTubeLogo = -1L + private set + +var filterBarHeightId = -1L + private set +var relatedChipCloudMarginId = -1L + private set +var barContainerHeightId = -1L + private set + +var fabButtonId = -1L + private set + +private val hideLayoutComponentsResourcePatch = resourcePatch { + dependsOn(resourceMappingPatch) + + execute { + expandButtonDownId = resourceMappings[ + "layout", + "expand_button_down", + ] + + albumCardId = resourceMappings[ + "layout", + "album_card", + ] + + crowdfundingBoxId = resourceMappings[ + "layout", + "donation_companion", + ] + + youTubeLogo = resourceMappings[ + "id", + "youtube_logo", + ] + + relatedChipCloudMarginId = resourceMappings[ + "layout", + "related_chip_cloud_reduced_margins", + ] + + filterBarHeightId = resourceMappings[ + "dimen", + "filter_bar_height", + ] + + barContainerHeightId = resourceMappings[ + "dimen", + "bar_container_height", + ] + + fabButtonId = resourceMappings[ + "id", + "fab", + ] + } +} + +private const val LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/components/LayoutComponentsFilter;" +private const val DESCRIPTION_COMPONENTS_FILTER_CLASS_NAME = + "Lapp/revanced/extension/youtube/patches/components/DescriptionComponentsFilter;" +private const val COMMENTS_FILTER_CLASS_NAME = + "Lapp/revanced/extension/youtube/patches/components/CommentsFilter;" +private const val CUSTOM_FILTER_CLASS_NAME = + "Lapp/revanced/extension/youtube/patches/components/CustomFilter;" +private const val KEYWORD_FILTER_CLASS_NAME = + "Lapp/revanced/extension/youtube/patches/components/KeywordContentFilter;" + +@Suppress("unused") +val hideLayoutComponentsPatch = bytecodePatch( + name = "Hide layout components", + description = "Adds options to hide general layout components.", + +) { + dependsOn( + lithoFilterPatch, + settingsPatch, + addResourcesPatch, + hideLayoutComponentsResourcePatch, + navigationBarHookPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.hide.general.hideLayoutComponentsPatch") + + PreferenceScreen.PLAYER.addPreferences( + PreferenceScreenPreference( + key = "revanced_hide_description_components_screen", + preferences = setOf( + SwitchPreference("revanced_hide_attributes_section"), + SwitchPreference("revanced_hide_chapters_section"), + SwitchPreference("revanced_hide_info_cards_section"), + SwitchPreference("revanced_hide_key_concepts_section"), + SwitchPreference("revanced_hide_podcast_section"), + SwitchPreference("revanced_hide_transcript_section"), + ), + ), + PreferenceScreenPreference( + "revanced_comments_screen", + preferences = setOf( + SwitchPreference("revanced_hide_comments_by_members_header"), + SwitchPreference("revanced_hide_comments_section"), + SwitchPreference("revanced_hide_comments_create_a_short_button"), + SwitchPreference("revanced_hide_comments_preview_comment"), + SwitchPreference("revanced_hide_comments_thanks_button"), + SwitchPreference("revanced_hide_comments_timestamp_and_emoji_buttons"), + ), + sorting = PreferenceScreenPreference.Sorting.UNSORTED, + ), + SwitchPreference("revanced_hide_channel_bar"), + SwitchPreference("revanced_hide_channel_guidelines"), + SwitchPreference("revanced_hide_channel_member_shelf"), + SwitchPreference("revanced_hide_channel_watermark"), + SwitchPreference("revanced_hide_community_guidelines"), + SwitchPreference("revanced_hide_emergency_box"), + SwitchPreference("revanced_hide_info_panels"), + SwitchPreference("revanced_hide_join_membership_button"), + SwitchPreference("revanced_disable_like_subscribe_glow"), + SwitchPreference("revanced_hide_medical_panels"), + SwitchPreference("revanced_hide_quick_actions"), + SwitchPreference("revanced_hide_related_videos"), + SwitchPreference("revanced_hide_subscribers_community_guidelines"), + SwitchPreference("revanced_hide_timed_reactions"), + ) + + PreferenceScreen.FEED.addPreferences( + PreferenceScreenPreference( + key = "revanced_hide_keyword_content_screen", + sorting = PreferenceScreenPreference.Sorting.UNSORTED, + preferences = setOf( + SwitchPreference("revanced_hide_keyword_content_home"), + SwitchPreference("revanced_hide_keyword_content_subscriptions"), + SwitchPreference("revanced_hide_keyword_content_search"), + TextPreference("revanced_hide_keyword_content_phrases", inputType = InputType.TEXT_MULTI_LINE), + NonInteractivePreference("revanced_hide_keyword_content_about"), + NonInteractivePreference( + key = "revanced_hide_keyword_content_about_whole_words", + tag = "app.revanced.extension.youtube.settings.preference.HtmlPreference", + ), + ), + ), + PreferenceScreenPreference( + key = "revanced_hide_filter_bar_screen", + preferences = setOf( + SwitchPreference("revanced_hide_filter_bar_feed_in_feed"), + SwitchPreference("revanced_hide_filter_bar_feed_in_search"), + SwitchPreference("revanced_hide_filter_bar_feed_in_related_videos"), + ), + ), + SwitchPreference("revanced_hide_album_cards"), + SwitchPreference("revanced_hide_artist_cards"), + SwitchPreference("revanced_hide_community_posts"), + SwitchPreference("revanced_hide_compact_banner"), + SwitchPreference("revanced_hide_crowdfunding_box"), + SwitchPreference("revanced_hide_chips_shelf"), + SwitchPreference("revanced_hide_expandable_chip"), + SwitchPreference("revanced_hide_feed_survey"), + SwitchPreference("revanced_hide_floating_microphone_button"), + SwitchPreference("revanced_hide_for_you_shelf"), + SwitchPreference("revanced_hide_horizontal_shelves"), + SwitchPreference("revanced_hide_image_shelf"), + SwitchPreference("revanced_hide_latest_posts_ads"), + SwitchPreference("revanced_hide_mix_playlists"), + SwitchPreference("revanced_hide_movies_section"), + SwitchPreference("revanced_hide_notify_me_button"), + SwitchPreference("revanced_hide_playables"), + SwitchPreference("revanced_hide_search_result_recommendations"), + SwitchPreference("revanced_hide_search_result_shelf_header"), + SwitchPreference("revanced_hide_show_more_button"), + SwitchPreference("revanced_hide_doodles"), + ) + + PreferenceScreen.GENERAL_LAYOUT.addPreferences( + PreferenceScreenPreference( + key = "revanced_custom_filter_screen", + sorting = PreferenceScreenPreference.Sorting.UNSORTED, + preferences = setOf( + SwitchPreference("revanced_custom_filter"), + // TODO: This should be a dynamic ListPreference, which does not exist yet + TextPreference("revanced_custom_filter_strings", inputType = InputType.TEXT_MULTI_LINE), + ), + ), + ) + + addLithoFilter(LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR) + addLithoFilter(DESCRIPTION_COMPONENTS_FILTER_CLASS_NAME) + addLithoFilter(COMMENTS_FILTER_CLASS_NAME) + addLithoFilter(KEYWORD_FILTER_CLASS_NAME) + addLithoFilter(CUSTOM_FILTER_CLASS_NAME) + + // region Mix playlists + + val startIndex = parseElementFromBufferFingerprint.patternMatch!!.startIndex + + parseElementFromBufferFingerprint.method.apply { + val freeRegister = "v0" + val byteArrayParameter = "p3" + val conversionContextRegister = getInstruction(startIndex).registerA + val returnEmptyComponentInstruction = instructions.last { it.opcode == Opcode.INVOKE_STATIC } + + addInstructionsWithLabels( + startIndex + 1, + """ + invoke-static { v$conversionContextRegister, $byteArrayParameter }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->filterMixPlaylists(Ljava/lang/Object;[B)Z + move-result $freeRegister + if-nez $freeRegister, :return_empty_component + const/4 $freeRegister, 0x0 # Restore register, required for 19.16 + """, + ExternalLabel("return_empty_component", returnEmptyComponentInstruction), + ) + } + + // endregion + + // region Watermark (legacy code for old versions of YouTube) + + showWatermarkFingerprint.match( + playerOverlayFingerprint.originalClassDef, + ).method.apply { + val index = implementation!!.instructions.size - 5 + + removeInstruction(index) + addInstructions( + index, + """ + invoke-static {}, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->showWatermark()Z + move-result p2 + """, + ) + } + + // endregion + + // region Show more button + + hideShowMoreButtonFingerprint.method.apply { + val moveRegisterIndex = hideShowMoreButtonFingerprint.patternMatch!!.endIndex + val viewRegister = getInstruction(moveRegisterIndex).registerA + + val insertIndex = moveRegisterIndex + 1 + addInstruction( + insertIndex, + "invoke-static { v$viewRegister }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" + + "->hideShowMoreButton(Landroid/view/View;)V", + ) + } + + // endregion + + // region crowdfunding box + crowdfundingBoxFingerprint.let { + it.method.apply { + val insertIndex = it.patternMatch!!.endIndex + val objectRegister = getInstruction(insertIndex).registerA + + addInstruction( + insertIndex, + "invoke-static {v$objectRegister}, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" + + "->hideCrowdfundingBox(Landroid/view/View;)V", + ) + } + } + + // endregion + + // region hide album cards + + albumCardsFingerprint.let { + it.method.apply { + val checkCastAnchorIndex = it.patternMatch!!.endIndex + val insertIndex = checkCastAnchorIndex + 1 + val register = getInstruction(checkCastAnchorIndex).registerA + + addInstruction( + insertIndex, + "invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR" + + "->hideAlbumCard(Landroid/view/View;)V", + ) + } + } + + // endregion + + // region hide floating microphone + + showFloatingMicrophoneButtonFingerprint.let { + it.method.apply { + val startIndex = it.patternMatch!!.startIndex + val register = getInstruction(startIndex).registerA + + addInstructions( + startIndex + 1, + """ + invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideFloatingMicrophoneButton(Z)Z + move-result v$register + """, + ) + } + } + + // endregion + + // region 'Yoodles' + + yoodlesImageViewFingerprint.method.apply { + findInstructionIndicesReversedOrThrow { + getReference()?.name == "setImageDrawable" + }.forEach { insertIndex -> + val register = getInstruction(insertIndex).registerD + + addInstructionsWithLabels( + insertIndex, + """ + invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideYoodles(Landroid/graphics/drawable/Drawable;)Landroid/graphics/drawable/Drawable; + move-result-object v$register + if-eqz v$register, :hide + """, + ExternalLabel("hide", getInstruction(insertIndex + 1)), + ) + } + } + + // endregion + + // region hide filter bar + + /** + * Patch a [Method] with a given [instructions]. + * + * @param RegisterInstruction The type of instruction to get the register from. + * @param insertIndexOffset The offset to add to the end index of the [Match.patternMatch]. + * @param hookRegisterOffset The offset to add to the register of the hook. + * @param instructions The instructions to add with the register as a parameter. + */ + fun Fingerprint.patch( + insertIndexOffset: Int = 0, + hookRegisterOffset: Int = 0, + instructions: (Int) -> String, + ) = method.apply { + val endIndex = patternMatch!!.endIndex + + val insertIndex = endIndex + insertIndexOffset + val register = + getInstruction(endIndex + hookRegisterOffset).registerA + + addInstructions(insertIndex, instructions(register)) + } + + filterBarHeightFingerprint.patch { register -> + """ + invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInFeed(I)I + move-result v$register + """ + } + + searchResultsChipBarFingerprint.patch(-1, -2) { register -> + """ + invoke-static { v$register }, $LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInSearch(I)I + move-result v$register + """ + } + + relatedChipCloudFingerprint.patch(1) { register -> + "invoke-static { v$register }, " + + "$LAYOUT_COMPONENTS_FILTER_CLASS_DESCRIPTOR->hideInRelatedVideos(Landroid/view/View;)V" + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/infocards/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/infocards/Fingerprints.kt new file mode 100644 index 000000000..5088472a1 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/infocards/Fingerprints.kt @@ -0,0 +1,29 @@ +package app.revanced.patches.youtube.layout.hide.infocards + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val infocardsIncognitoFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Ljava/lang/Boolean;") + parameters("L", "J") + strings("vibrator") +} + +internal val infocardsIncognitoParentFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Ljava/lang/String;") + strings("player_overlay_info_card_teaser") +} + +internal val infocardsMethodCallFingerprint = fingerprint { + opcodes( + Opcode.INVOKE_VIRTUAL, + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + ) + strings("Missing ControlsOverlayPresenter for InfoCards to work.") + literal { drawerResourceId } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt new file mode 100644 index 000000000..29eb8de6b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/infocards/HideInfoCardsPatch.kt @@ -0,0 +1,105 @@ +package app.revanced.patches.youtube.layout.hide.infocards + +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.patch.resourcePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter +import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +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 + +internal var drawerResourceId = -1L + private set + +private val hideInfocardsResourcePatch = resourcePatch { + dependsOn( + settingsPatch, + resourceMappingPatch, + addResourcesPatch, + ) + execute { + addResources("youtube", "layout.hide.infocards.hideInfocardsResourcePatch") + + PreferenceScreen.PLAYER.addPreferences( + SwitchPreference("revanced_hide_info_cards"), + ) + + drawerResourceId = resourceMappings[ + "id", + "info_cards_drawer_header", + ] + } +} + +@Suppress("unused") +val hideInfoCardsPatch = bytecodePatch( + name = "Hide info cards", + description = "Adds an option to hide info cards that creators add in the video player.", +) { + dependsOn( + sharedExtensionPatch, + lithoFilterPatch, + hideInfocardsResourcePatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + infocardsIncognitoFingerprint.match(infocardsIncognitoParentFingerprint.originalClassDef).method.apply { + val invokeInstructionIndex = implementation!!.instructions.indexOfFirst { + it.opcode.ordinal == Opcode.INVOKE_VIRTUAL.ordinal && + ((it as ReferenceInstruction).reference.toString() == "Landroid/view/View;->setVisibility(I)V") + } + + addInstruction( + invokeInstructionIndex, + "invoke-static {v${getInstruction(invokeInstructionIndex).registerC}}," + + " Lapp/revanced/extension/youtube/patches/HideInfoCardsPatch;->hideInfoCardsIncognito(Landroid/view/View;)V", + ) + } + + val hideInfoCardsCallMethod = infocardsMethodCallFingerprint.method + + val invokeInterfaceIndex = infocardsMethodCallFingerprint.patternMatch!!.endIndex + val toggleRegister = infocardsMethodCallFingerprint.method.implementation!!.registerCount - 1 + + hideInfoCardsCallMethod.addInstructionsWithLabels( + invokeInterfaceIndex, + """ + invoke-static {}, Lapp/revanced/extension/youtube/patches/HideInfoCardsPatch;->hideInfoCardsMethodCall()Z + move-result v$toggleRegister + if-nez v$toggleRegister, :hide_info_cards + """, + ExternalLabel( + "hide_info_cards", + hideInfoCardsCallMethod.getInstruction(invokeInterfaceIndex + 1), + ), + ) + + // Info cards can also appear as Litho components. + val filterClassDescriptor = "Lapp/revanced/extension/youtube/patches/components/HideInfoCardsFilterPatch;" + addLithoFilter(filterClassDescriptor) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/flyoutmenupanel/HidePlayerFlyoutMenuPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/flyoutmenupanel/HidePlayerFlyoutMenuPatch.kt new file mode 100644 index 000000000..dc9662d1f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/player/flyoutmenupanel/HidePlayerFlyoutMenuPatch.kt @@ -0,0 +1,65 @@ +package app.revanced.patches.youtube.layout.hide.player.flyoutmenupanel + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter +import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch +import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch + +@Suppress("unused") +val hidePlayerFlyoutMenuPatch = bytecodePatch( + name = "Hide player flyout menu items", + description = "Adds options to hide menu items that appear when pressing the gear icon in the video player.", +) { + dependsOn( + lithoFilterPatch, + playerTypeHookPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + val filterClassDescriptor = "Lapp/revanced/extension/youtube/patches/components/PlayerFlyoutMenuItemsFilter;" + + addResources("youtube", "layout.hide.player.flyoutmenupanel.hidePlayerFlyoutMenuPatch") + + PreferenceScreen.PLAYER.addPreferences( + PreferenceScreenPreference( + key = "revanced_hide_player_flyout", + preferences = setOf( + SwitchPreference("revanced_hide_player_flyout_captions"), + SwitchPreference("revanced_hide_player_flyout_additional_settings"), + SwitchPreference("revanced_hide_player_flyout_loop_video"), + SwitchPreference("revanced_hide_player_flyout_ambient_mode"), + SwitchPreference("revanced_hide_player_flyout_stable_volume"), + SwitchPreference("revanced_hide_player_flyout_help"), + SwitchPreference("revanced_hide_player_flyout_speed"), + SwitchPreference("revanced_hide_player_flyout_lock_screen"), + SwitchPreference("revanced_hide_player_flyout_more_info"), + SwitchPreference("revanced_hide_player_flyout_audio_track"), + SwitchPreference("revanced_hide_player_flyout_watch_in_vr"), + SwitchPreference("revanced_hide_player_flyout_sleep_timer"), + SwitchPreference("revanced_hide_player_flyout_video_quality_footer"), + ), + ), + ) + + addLithoFilter(filterClassDescriptor) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch.kt new file mode 100644 index 000000000..bf1aba4fd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/rollingnumber/DisableRollingNumberAnimationPatch.kt @@ -0,0 +1,74 @@ +package app.revanced.patches.youtube.layout.hide.rollingnumber + +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.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.rollingNumberTextViewAnimationUpdateFingerprint +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/DisableRollingNumberAnimationsPatch;" + +@Suppress("unused") +val disableRollingNumberAnimationPatch = bytecodePatch( + name = "Disable rolling number animations", + description = "Adds an option to disable rolling number animations of video view count, user likes, and upload time.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + // 18.43 is the earliest target this patch works. + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.hide.rollingnumber.disableRollingNumberAnimationPatch") + + PreferenceScreen.PLAYER.addPreferences( + SwitchPreference("revanced_disable_rolling_number_animations"), + ) + + // Animations are disabled by preventing an Image from being applied to the text span, + // which prevents the animations from appearing. + + val patternMatch = rollingNumberTextViewAnimationUpdateFingerprint.patternMatch!! + val blockStartIndex = patternMatch.startIndex + val blockEndIndex = patternMatch.endIndex + 1 + rollingNumberTextViewAnimationUpdateFingerprint.method.apply { + val freeRegister = getInstruction(blockStartIndex).registerA + + // ReturnYouTubeDislike also makes changes to this same method, + // and must add control flow label to a noop instruction to + // ensure RYD patch adds its changes after the control flow label. + addInstructions(blockEndIndex, "nop") + + addInstructionsWithLabels( + blockStartIndex, + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->disableRollingNumberAnimations()Z + move-result v$freeRegister + if-nez v$freeRegister, :disable_animations + """, + ExternalLabel("disable_animations", getInstruction(blockEndIndex)), + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/seekbar/HideSeekbarPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/seekbar/HideSeekbarPatch.kt new file mode 100644 index 000000000..34c231020 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/seekbar/HideSeekbarPatch.kt @@ -0,0 +1,59 @@ +package app.revanced.patches.youtube.layout.hide.seekbar + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.layout.seekbar.seekbarColorPatch +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.seekbarFingerprint +import app.revanced.patches.youtube.shared.seekbarOnDrawFingerprint + +@Suppress("unused") +val hideSeekbarPatch = bytecodePatch( + name = "Hide seekbar", + description = "Adds an option to hide the seekbar.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + seekbarColorPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.hide.seekbar.hideSeekbarPatch") + + PreferenceScreen.SEEKBAR.addPreferences( + SwitchPreference("revanced_hide_seekbar"), + SwitchPreference("revanced_hide_seekbar_thumbnail"), + ) + + seekbarOnDrawFingerprint.match(seekbarFingerprint.originalClassDef).method.addInstructionsWithLabels( + 0, + """ + const/4 v0, 0x0 + invoke-static { }, Lapp/revanced/extension/youtube/patches/HideSeekbarPatch;->hideSeekbar()Z + move-result v0 + if-eqz v0, :hide_seekbar + return-void + :hide_seekbar + nop + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/Fingerprints.kt new file mode 100644 index 000000000..5a2a2ac64 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/Fingerprints.kt @@ -0,0 +1,87 @@ +package app.revanced.patches.youtube.layout.hide.shorts + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val legacyRenderBottomNavigationBarParentFingerprint = fingerprint { + parameters( + "I", + "I", + "L", + "L", + "J", + "L", + ) + strings("aa") +} + +internal val shortsBottomBarContainerFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("Landroid/view/View;", "Landroid/os/Bundle;") + strings("r_pfvc") + literal { bottomBarContainer } +} + +internal val createShortsButtonsFingerprint = fingerprint { + returns("V") + literal { reelPlayerRightCellButtonHeight } +} + +internal val reelConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + opcodes(Opcode.INVOKE_VIRTUAL) + literal { reelMultipleItemShelfId } +} + +internal val renderBottomNavigationBarFingerprint = fingerprint { + returns("V") + parameters("Ljava/lang/String;") + opcodes( + Opcode.IGET_OBJECT, + Opcode.MONITOR_ENTER, + Opcode.IGET_OBJECT, + Opcode.IF_EQZ, + Opcode.INVOKE_INTERFACE, + Opcode.MONITOR_EXIT, + Opcode.RETURN_VOID, + Opcode.MOVE_EXCEPTION, + Opcode.MONITOR_EXIT, + Opcode.THROW, + ) +} + +/** + * Identical to [legacyRenderBottomNavigationBarParentFingerprint] + * except this has an extra parameter. + */ +internal val renderBottomNavigationBarParentFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + parameters( + "I", + "I", + "L", // ReelWatchEndpointOuterClass + "L", + "J", + "Ljava/lang/String;", + "L", + ) + strings("aa") +} + +internal val setPivotBarVisibilityFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returns("V") + parameters("Z") + opcodes( + Opcode.CHECK_CAST, + Opcode.IF_EQZ, + ) +} + +internal val setPivotBarVisibilityParentFingerprint = fingerprint { + parameters("Z") + strings("FEnotifications_inbox") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt new file mode 100644 index 000000000..816e65d3b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/shorts/HideShortsComponentsPatch.kt @@ -0,0 +1,317 @@ +package app.revanced.patches.youtube.layout.hide.shorts + +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.booleanOption +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter +import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch +import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch +import app.revanced.patches.youtube.misc.playservice.is_19_03_or_greater +import app.revanced.patches.youtube.misc.playservice.is_19_41_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.* +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.MethodReference +import com.sun.org.apache.bcel.internal.generic.InstructionConst.getInstruction + +internal var reelMultipleItemShelfId = -1L + private set +internal var reelPlayerRightCellButtonHeight = -1L + private set +internal var bottomBarContainer = -1L + private set +internal var reelPlayerRightPivotV2Size = -1L + private set + +internal val hideShortsAppShortcutOption = booleanOption( + key = "hideShortsAppShortcut", + default = false, + title = "Hide Shorts app shortcut", + description = "Permanently hides the shortcut to open Shorts when long pressing the app icon in your launcher.", +) + +internal val hideShortsWidgetOption = booleanOption( + key = "hideShortsWidget", + default = false, + title = "Hide Shorts widget", + description = "Permanently hides the launcher widget Shorts button.", +) + +private val hideShortsComponentsResourcePatch = resourcePatch { + dependsOn( + settingsPatch, + resourceMappingPatch, + addResourcesPatch, + versionCheckPatch, + ) + + execute { + val hideShortsAppShortcut by hideShortsAppShortcutOption + val hideShortsWidget by hideShortsWidgetOption + + addResources("youtube", "layout.hide.shorts.hideShortsComponentsResourcePatch") + + PreferenceScreen.SHORTS.addPreferences( + SwitchPreference("revanced_hide_shorts_home"), + SwitchPreference("revanced_hide_shorts_subscriptions"), + SwitchPreference("revanced_hide_shorts_search"), + + PreferenceScreenPreference( + key = "revanced_shorts_player_screen", + sorting = PreferenceScreenPreference.Sorting.UNSORTED, + preferences = setOf( + // Shorts player components. + // Ideally each group should be ordered similar to how they appear in the UI + + // Vertical row of buttons on right side of the screen. + SwitchPreference("revanced_hide_shorts_like_fountain"), + SwitchPreference("revanced_hide_shorts_like_button"), + SwitchPreference("revanced_hide_shorts_dislike_button"), + SwitchPreference("revanced_hide_shorts_comments_button"), + SwitchPreference("revanced_hide_shorts_share_button"), + SwitchPreference("revanced_hide_shorts_remix_button"), + SwitchPreference("revanced_hide_shorts_sound_button"), + + // Upper and middle area of the player. + SwitchPreference("revanced_hide_shorts_join_button"), + SwitchPreference("revanced_hide_shorts_subscribe_button"), + SwitchPreference("revanced_hide_shorts_paused_overlay_buttons"), + + // Suggested actions. + SwitchPreference("revanced_hide_shorts_save_sound_button"), + SwitchPreference("revanced_hide_shorts_use_template_button"), + SwitchPreference("revanced_hide_shorts_upcoming_button"), + SwitchPreference("revanced_hide_shorts_green_screen_button"), + SwitchPreference("revanced_hide_shorts_hashtag_button"), + SwitchPreference("revanced_hide_shorts_shop_button"), + SwitchPreference("revanced_hide_shorts_tagged_products"), + SwitchPreference("revanced_hide_shorts_search_suggestions"), + SwitchPreference("revanced_hide_shorts_super_thanks_button"), + SwitchPreference("revanced_hide_shorts_stickers"), + + // Bottom of the screen. + SwitchPreference("revanced_hide_shorts_location_label"), + SwitchPreference("revanced_hide_shorts_channel_bar"), + SwitchPreference("revanced_hide_shorts_info_panel"), + SwitchPreference("revanced_hide_shorts_full_video_link_label"), + SwitchPreference("revanced_hide_shorts_video_title"), + SwitchPreference("revanced_hide_shorts_sound_metadata_label"), + SwitchPreference("revanced_hide_shorts_navigation_bar"), + ), + ), + ) + + // Verify the file has the expected node, even if the patch option is off. + document("res/xml/main_shortcuts.xml").use { document -> + val shortsItem = document.childNodes.findElementByAttributeValueOrThrow( + "android:shortcutId", + "shorts-shortcut", + ) + + if (hideShortsAppShortcut == true) { + shortsItem.parentNode.removeChild(shortsItem) + } + } + + document("res/layout/appwidget_two_rows.xml").use { document -> + val shortsItem = document.childNodes.findElementByAttributeValueOrThrow( + "android:id", + "@id/button_shorts_container", + ) + + if (hideShortsWidget == true) { + shortsItem.parentNode.removeChild(shortsItem) + } + } + + reelPlayerRightCellButtonHeight = resourceMappings[ + "dimen", + "reel_player_right_cell_button_height", + ] + + bottomBarContainer = resourceMappings[ + "id", + "bottom_bar_container", + ] + + reelPlayerRightPivotV2Size = resourceMappings[ + "dimen", + "reel_player_right_pivot_v2_size", + ] + + if (!is_19_03_or_greater) { + reelMultipleItemShelfId = resourceMappings[ + "dimen", + "reel_player_right_cell_button_height", + ] + } + } +} + +private const val FILTER_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/components/ShortsFilter;" + +@Suppress("unused") +val hideShortsComponentsPatch = bytecodePatch( + name = "Hide Shorts components", + description = "Adds options to hide components related to YouTube Shorts.", +) { + dependsOn( + sharedExtensionPatch, + lithoFilterPatch, + hideShortsComponentsResourcePatch, + resourceMappingPatch, + navigationBarHookPatch, + versionCheckPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + // region Hide the Shorts shelf. + + // This patch point is not present in 19.03.x and greater. + if (!is_19_03_or_greater && reelConstructorFingerprint.methodOrNull != null) { + reelConstructorFingerprint.method.apply { + val insertIndex = reelConstructorFingerprint.patternMatch!!.startIndex + 2 + val viewRegister = getInstruction(insertIndex).registerA + + injectHideViewCall( + insertIndex, + viewRegister, + FILTER_CLASS_DESCRIPTOR, + "hideShortsShelf", + ) + } + } + + // endregion + + // region Hide the Shorts buttons in older versions of YouTube. + + // Some Shorts buttons are views, hide them by setting their visibility to GONE. + ShortsButtons.entries.forEach { button -> button.injectHideCall(createShortsButtonsFingerprint.method) } + + // endregion + + // region Hide the Shorts buttons in newer versions of YouTube. + + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + + forEachLiteralValueInstruction( + reelPlayerRightPivotV2Size, + ) { literalInstructionIndex -> + val targetIndex = indexOfFirstInstructionOrThrow(literalInstructionIndex) { + getReference()?.name == "getDimensionPixelSize" + } + 1 + + val sizeRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, + """ + invoke-static { v$sizeRegister }, $FILTER_CLASS_DESCRIPTOR->getSoundButtonSize(I)I + move-result v$sizeRegister + """, + ) + } + + // endregion + + // region Hide the navigation bar. + + // Hook to get the pivotBar view. + setPivotBarVisibilityFingerprint.match( + setPivotBarVisibilityParentFingerprint.originalClassDef, + ).let { result -> + result.method.apply { + val insertIndex = result.patternMatch!!.endIndex + val viewRegister = getInstruction(insertIndex - 1).registerA + addInstruction( + insertIndex, + "invoke-static {v$viewRegister}," + + " $FILTER_CLASS_DESCRIPTOR->setNavigationBar(Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;)V", + ) + } + } + + // Hook to hide the shared navigation bar when the Shorts player is opened. + renderBottomNavigationBarFingerprint.match( + if (is_19_41_or_greater) { + renderBottomNavigationBarParentFingerprint + } else { + legacyRenderBottomNavigationBarParentFingerprint + }.originalClassDef, + ).method.addInstruction( + 0, + "invoke-static { p1 }, $FILTER_CLASS_DESCRIPTOR->hideNavigationBar(Ljava/lang/String;)V", + ) + + // Hide the bottom bar container of the Shorts player. + shortsBottomBarContainerFingerprint.method.apply { + val resourceIndex = indexOfFirstLiteralInstruction(bottomBarContainer) + + val targetIndex = indexOfFirstInstructionOrThrow(resourceIndex) { + getReference()?.name == "getHeight" + } + 1 + + val heightRegister = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, + """ + invoke-static { v$heightRegister }, $FILTER_CLASS_DESCRIPTOR->getNavigationBarHeight(I)I + move-result v$heightRegister + """, + ) + } + + // endregion + } +} + +private enum class ShortsButtons(private val resourceName: String, private val methodName: String) { + LIKE("reel_dyn_like", "hideLikeButton"), + DISLIKE("reel_dyn_dislike", "hideDislikeButton"), + COMMENTS("reel_dyn_comment", "hideShortsCommentsButton"), + REMIX("reel_dyn_remix", "hideShortsRemixButton"), + SHARE("reel_dyn_share", "hideShortsShareButton"), + ; + + fun injectHideCall(method: MutableMethod) { + val referencedIndex = method.indexOfFirstResourceIdOrThrow(resourceName) + + val setIdIndex = method.indexOfFirstInstructionOrThrow(referencedIndex) { + opcode == Opcode.INVOKE_VIRTUAL && getReference()?.name == "setId" + } + + val viewRegister = method.getInstruction(setIdIndex).registerC + + method.injectHideViewCall(setIdIndex + 1, viewRegister, FILTER_CLASS_DESCRIPTOR, methodName) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/suggestedvideoendscreen/DisableSuggestedVideoEndScreenPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/suggestedvideoendscreen/DisableSuggestedVideoEndScreenPatch.kt new file mode 100644 index 000000000..17a83f98a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/suggestedvideoendscreen/DisableSuggestedVideoEndScreenPatch.kt @@ -0,0 +1,78 @@ +package app.revanced.patches.youtube.layout.hide.suggestedvideoendscreen + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction + +internal var sizeAdjustableLiteAutoNavOverlay = -1L + private set + +internal val disableSuggestedVideoEndScreenResourcePatch = resourcePatch { + dependsOn( + settingsPatch, + resourceMappingPatch, + addResourcesPatch, + ) + + execute { + addResources("youtube", "layout.hide.suggestedvideoendscreen.disableSuggestedVideoEndScreenResourcePatch") + + PreferenceScreen.PLAYER.addPreferences( + SwitchPreference("revanced_disable_suggested_video_end_screen"), + ) + + sizeAdjustableLiteAutoNavOverlay = resourceMappings[ + "layout", + "size_adjustable_lite_autonav_overlay", + ] + } +} + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/DisableSuggestedVideoEndScreenPatch;" + +@Suppress("unused") +val disableSuggestedVideoEndScreenPatch = bytecodePatch( + name = "Disable suggested video end screen", + description = "Adds an option to disable the suggested video end screen at the end of videos.", +) { + dependsOn( + sharedExtensionPatch, + disableSuggestedVideoEndScreenResourcePatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + createEndScreenViewFingerprint.method.apply { + val addOnClickEventListenerIndex = createEndScreenViewFingerprint.patternMatch!!.endIndex - 1 + val viewRegister = getInstruction(addOnClickEventListenerIndex).registerC + + addInstruction( + addOnClickEventListenerIndex + 1, + "invoke-static {v$viewRegister}, " + + "$EXTENSION_CLASS_DESCRIPTOR->closeEndScreen(Landroid/widget/ImageView;)V", + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/suggestedvideoendscreen/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/suggestedvideoendscreen/Fingerprints.kt new file mode 100644 index 000000000..78efc0680 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/suggestedvideoendscreen/Fingerprints.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.youtube.layout.hide.suggestedvideoendscreen + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val createEndScreenViewFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Landroid/view/View;") + parameters("Landroid/content/Context;") + opcodes( + Opcode.INVOKE_DIRECT, + Opcode.INVOKE_VIRTUAL, + Opcode.CONST, + ) + literal { sizeAdjustableLiteAutoNavOverlay } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/time/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/time/Fingerprints.kt new file mode 100644 index 000000000..d109c6455 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/time/Fingerprints.kt @@ -0,0 +1,24 @@ +package app.revanced.patches.youtube.layout.hide.time + +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import app.revanced.patcher.fingerprint + +internal val timeCounterFingerprint = fingerprint( + fuzzyPatternScanThreshold = 1, +) { + returns("V") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + parameters() + opcodes( + Opcode.SUB_LONG_2ADDR, + Opcode.IGET_WIDE, + Opcode.SUB_LONG_2ADDR, + Opcode.IGET_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IGET_WIDE, + Opcode.IGET_WIDE, + Opcode.SUB_LONG_2ADDR + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/time/HideTimestampPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/time/HideTimestampPatch.kt new file mode 100644 index 000000000..acb5f9f19 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/hide/time/HideTimestampPatch.kt @@ -0,0 +1,53 @@ +package app.revanced.patches.youtube.layout.hide.time + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch + +@Suppress("unused") +val hideTimestampPatch = bytecodePatch( + name = "Hide timestamp", + description = "Adds an option to hide the timestamp in the bottom left of the video player.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.hide.time.hideTimestampPatch") + + PreferenceScreen.SEEKBAR.addPreferences( + SwitchPreference("revanced_hide_timestamp"), + ) + + timeCounterFingerprint.method.addInstructionsWithLabels( + 0, + """ + invoke-static { }, Lapp/revanced/extension/youtube/patches/HideTimestampPatch;->hideTimestamp()Z + move-result v0 + if-eqz v0, :hide_time + return-void + :hide_time + nop + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/Fingerprints.kt new file mode 100644 index 000000000..125f043a3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/Fingerprints.kt @@ -0,0 +1,152 @@ +@file:Suppress("SpellCheckingInspection") + +package app.revanced.patches.youtube.layout.miniplayer + +import app.revanced.patcher.fingerprint +import app.revanced.util.containsLiteralInstruction +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val miniplayerDimensionsCalculatorParentFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L") + literal { floatyBarButtonTopMargin } +} + +/** + * Matches using the class found in [miniplayerModernViewParentFingerprint]. + */ +internal val miniplayerModernAddViewListenerFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("Landroid/view/View;") +} + +/** + * Matches using the class found in [miniplayerModernViewParentFingerprint]. + */ + +internal val miniplayerModernCloseButtonFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Landroid/widget/ImageView;") + parameters() + literal { modernMiniplayerClose } +} + +internal const val MINIPLAYER_MODERN_FEATURE_KEY = 45622882L +// In later targets this feature flag does nothing and is dead code. +internal const val MINIPLAYER_MODERN_FEATURE_LEGACY_KEY = 45630429L +internal const val MINIPLAYER_DOUBLE_TAP_FEATURE_KEY = 45628823L +internal const val MINIPLAYER_DRAG_DROP_FEATURE_KEY = 45628752L +internal const val MINIPLAYER_HORIZONTAL_DRAG_FEATURE_KEY = 45658112L +internal const val MINIPLAYER_ROUNDED_CORNERS_FEATURE_KEY = 45652224L +internal const val MINIPLAYER_INITIAL_SIZE_FEATURE_KEY = 45640023L + +internal val miniplayerModernConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + parameters("L") + literal { 45623000L } +} + +/** + * Matches using the class found in [miniplayerModernViewParentFingerprint]. + */ +internal val miniplayerModernExpandButtonFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Landroid/widget/ImageView;") + parameters() + literal { modernMiniplayerExpand } +} + +/** + * Matches using the class found in [miniplayerModernViewParentFingerprint]. + */ +internal val miniplayerModernExpandCloseDrawablesFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L") + literal { ytOutlinePictureInPictureWhite24 } +} + +/** + * Matches using the class found in [miniplayerModernViewParentFingerprint]. + */ +internal val miniplayerModernForwardButtonFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Landroid/widget/ImageView;") + parameters() + literal { modernMiniplayerForwardButton } +} + +/** + * Matches using the class found in [miniplayerModernViewParentFingerprint]. + */ +internal val miniplayerModernOverlayViewFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters() + literal { scrimOverlay } +} + +/** + * Matches using the class found in [miniplayerModernViewParentFingerprint]. + */ +internal val miniplayerModernRewindButtonFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Landroid/widget/ImageView;") + parameters() + literal { modernMiniplayerRewindButton } +} + +internal val miniplayerModernViewParentFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Ljava/lang/String;") + parameters() + strings("player_overlay_modern_mini_player_controls") +} + +internal val miniplayerMinimumSizeFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + custom { method, _ -> + method.containsLiteralInstruction(192) && + method.containsLiteralInstruction(128) && + method.containsLiteralInstruction(miniplayerMaxSize) + } +} + +internal val miniplayerOverrideFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("L") + strings("appName") +} + +internal val miniplayerOverrideNoContextFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returns("Z") + opcodes(Opcode.IGET_BOOLEAN) // Anchor to insert the instruction. +} + +internal val miniplayerResponseModelSizeCheckFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("L") + parameters("Ljava/lang/Object;", "Ljava/lang/Object;") + opcodes( + Opcode.RETURN_OBJECT, + Opcode.CHECK_CAST, + Opcode.CHECK_CAST, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.IF_NEZ, + ) +} + +internal const val YOUTUBE_PLAYER_OVERLAYS_LAYOUT_CLASS_NAME = + "Lcom/google/android/apps/youtube/app/common/player/overlay/YouTubePlayerOverlaysLayout;" + +internal val playerOverlaysLayoutFingerprint = fingerprint { + custom { method, _ -> + method.definingClass == YOUTUBE_PLAYER_OVERLAYS_LAYOUT_CLASS_NAME + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt new file mode 100644 index 000000000..cc2b9de91 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/miniplayer/MiniplayerPatch.kt @@ -0,0 +1,548 @@ +@file:Suppress("SpellCheckingInspection") + +package app.revanced.patches.youtube.layout.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.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.proxy.mutableTypes.MutableMethod +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.shared.misc.settings.preference.* +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.playservice.* +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.* +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.NarrowLiteralInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter + +var floatyBarButtonTopMargin = -1L + private set + +// Only available in 19.15 and upwards. +var ytOutlineXWhite24 = -1L + private set +var ytOutlinePictureInPictureWhite24 = -1L + private set +var scrimOverlay = -1L + private set +var modernMiniplayerClose = -1L + private set +var modernMiniplayerExpand = -1L + private set +var modernMiniplayerRewindButton = -1L + private set +var modernMiniplayerForwardButton = -1L + private set +var playerOverlays = -1L + private set +var miniplayerMaxSize = -1L + private set + +private val miniplayerResourcePatch = resourcePatch { + dependsOn( + resourceMappingPatch, + versionCheckPatch, + ) + + execute { + floatyBarButtonTopMargin = resourceMappings[ + "dimen", + "floaty_bar_button_top_margin", + ] + + scrimOverlay = resourceMappings[ + "id", + "scrim_overlay", + ] + + playerOverlays = resourceMappings[ + "layout", + "player_overlays", + ] + + if (is_19_16_or_greater) { + modernMiniplayerClose = resourceMappings[ + "id", + "modern_miniplayer_close", + ] + + modernMiniplayerExpand = resourceMappings[ + "id", + "modern_miniplayer_expand", + ] + + modernMiniplayerRewindButton = resourceMappings[ + "id", + "modern_miniplayer_rewind_button", + ] + + modernMiniplayerForwardButton = resourceMappings[ + "id", + "modern_miniplayer_forward_button", + ] + + // Resource id is not used during patching, but is used by extension. + // Verify the resource is present while patching. + resourceMappings[ + "id", + "modern_miniplayer_subtitle_text", + ] + + // Only required for exactly 19.16 + if (!is_19_17_or_greater) { + ytOutlinePictureInPictureWhite24 = resourceMappings[ + "drawable", + "yt_outline_picture_in_picture_white_24", + ] + + ytOutlineXWhite24 = resourceMappings[ + "drawable", + "yt_outline_x_white_24", + ] + } + + if (is_19_26_or_greater) { + miniplayerMaxSize = resourceMappings[ + "dimen", + "miniplayer_max_size", + ] + } + } + } +} + +private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/MiniplayerPatch;" + +@Suppress("unused") +val miniplayerPatch = bytecodePatch( + name = "Miniplayer", + description = "Adds options to change the in app minimized player. " + + "Patching target 19.16+ adds modern miniplayers.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + miniplayerResourcePatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + // 19.14.43 // Incomplete code for modern miniplayers. + // 19.15.36 // Different code for handling subtitle texts and not worth supporting. + "19.16.39", // First with modern miniplayers. + // 19.17.41 // Works without issues, but no reason to recommend over 19.16. + // 19.18.41 // Works without issues, but no reason to recommend over 19.16. + // 19.19.39 // Last bug free version with smaller Modern 1 miniplayer, but no reason to recommend over 19.16. + // 19.20.35 // Cannot swipe to expand. + // 19.21.40 // Cannot swipe to expand. + // 19.22.43 // Cannot swipe to expand. + // 19.23.40 // First with Modern 1 drag and drop, Cannot swipe to expand. + // 19.24.45 // First with larger Modern 1, Cannot swipe to expand. + "19.25.37", // First with double tap, last with skip forward/back buttons, last with swipe to expand/close, and last before double tap to expand seems to be required. + // 19.26.42 // Modern 1 Pause/play button are always hidden. Unusable. + // 19.28.42 // First with custom miniplayer size, screen flickers when swiping to maximize Modern 1. Swipe to close miniplayer is broken. + // 19.29.42 // All modern players are broken and ignore tapping the miniplayer video. + // 19.30.39 // Modern 3 is less broken when double tap expand is enabled, but cannot swipe to expand when double tap is off. + // 19.31.36 // All Modern 1 buttons are missing. Unusable. + // 19.32.36 // 19.32+ and beyond all work without issues. + // 19.33.35 + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.miniplayer.miniplayerPatch") + + val preferences = mutableSetOf() + + if (!is_19_16_or_greater) { + preferences += ListPreference( + "revanced_miniplayer_type", + summaryKey = null, + entriesKey = "revanced_miniplayer_type_legacy_entries", + entryValuesKey = "revanced_miniplayer_type_legacy_entry_values", + ) + } else { + preferences += ListPreference( + "revanced_miniplayer_type", + summaryKey = null, + ) + + if (is_19_25_or_greater) { + if (!is_19_29_or_greater) { + preferences += SwitchPreference("revanced_miniplayer_double_tap_action") + } + preferences += SwitchPreference("revanced_miniplayer_drag_and_drop") + } + + if (is_19_43_or_greater) { + preferences += SwitchPreference("revanced_miniplayer_horizontal_drag") + } + + if (is_19_36_or_greater) { + preferences += SwitchPreference("revanced_miniplayer_rounded_corners") + } + + preferences += SwitchPreference("revanced_miniplayer_hide_subtext") + + preferences += if (is_19_26_or_greater) { + SwitchPreference("revanced_miniplayer_hide_expand_close") + } else { + SwitchPreference( + key = "revanced_miniplayer_hide_expand_close", + titleKey = "revanced_miniplayer_hide_expand_close_legacy_title", + summaryOnKey = "revanced_miniplayer_hide_expand_close_legacy_summary_on", + summaryOffKey = "revanced_miniplayer_hide_expand_close_legacy_summary_off", + ) + } + + if (!is_19_26_or_greater) { + preferences += SwitchPreference("revanced_miniplayer_hide_rewind_forward") + } + + if (is_19_26_or_greater) { + preferences += TextPreference("revanced_miniplayer_width_dip", inputType = InputType.NUMBER) + } + + preferences += TextPreference("revanced_miniplayer_opacity", inputType = InputType.NUMBER) + } + + PreferenceScreen.PLAYER.addPreferences( + PreferenceScreenPreference( + key = "revanced_miniplayer_screen", + sorting = PreferenceScreenPreference.Sorting.UNSORTED, + preferences = preferences, + ), + ) + + 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 + """, + ) + } + + fun Method.findReturnIndicesReversed() = findInstructionIndicesReversedOrThrow(Opcode.RETURN) + + /** + * 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") + } + + fun Fingerprint.insertLiteralValueBooleanOverride( + literal: Long, + extensionMethod: String, + ) { + method.apply { + val literalIndex = indexOfFirstLiteralInstructionOrThrow(literal) + val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT) + + insertBooleanOverride(targetIndex + 1, extensionMethod) + } + } + + fun Fingerprint.insertLiteralValueFloatOverride( + literal: Long, + extensionMethod: String, + ) { + method.apply { + val literalIndex = indexOfFirstLiteralInstructionOrThrow(literal) + val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.DOUBLE_TO_FLOAT) + val register = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, + """ + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->$extensionMethod(F)F + move-result v$register + """, + ) + } + } + + /** + * Adds an override to specify which modern miniplayer is used. + */ + fun MutableMethod.insertModernMiniplayerTypeOverride(iPutIndex: Int) { + val register = getInstruction(iPutIndex).registerA + + addInstructionsAtControlFlowLabel( + iPutIndex, + """ + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getModernMiniplayerOverrideType(I)I + move-result v$register + """, + ) + } + + fun MutableMethod.hookInflatedView( + literalValue: Long, + hookedClassType: String, + extensionMethodName: String, + ) { + val imageViewIndex = indexOfFirstInstructionOrThrow( + indexOfFirstLiteralInstructionOrThrow(literalValue), + ) { + opcode == Opcode.CHECK_CAST && getReference()?.type == hookedClassType + } + + val register = getInstruction(imageViewIndex).registerA + addInstruction( + imageViewIndex + 1, + "invoke-static { v$register }, $extensionMethodName", + ) + } + + // region Enable tablet miniplayer. + + miniplayerOverrideNoContextFingerprint.match( + miniplayerDimensionsCalculatorParentFingerprint.originalClassDef, + ).method.apply { + findReturnIndicesReversed().forEach { index -> insertLegacyTabletMiniplayerOverride(index) } + } + + // endregion + + // region Legacy tablet miniplayer hooks. + val appNameStringIndex = miniplayerOverrideFingerprint.stringMatches!!.first().index + 2 + navigate(miniplayerOverrideFingerprint.originalMethod).to(appNameStringIndex).stop().apply { + findReturnIndicesReversed().forEach { index -> insertLegacyTabletMiniplayerOverride(index) } + } + + miniplayerResponseModelSizeCheckFingerprint.let { + it.method.insertLegacyTabletMiniplayerOverride(it.patternMatch!!.endIndex) + } + + if (!is_19_16_or_greater) { + // Return here, as patch below is only for the current versions of the app. + return@execute + } + + // endregion + + // region Enable modern miniplayer. + + miniplayerModernConstructorFingerprint.classDef.methods.forEach { + it.apply { + if (AccessFlags.CONSTRUCTOR.isSet(accessFlags)) { + val iPutIndex = indexOfFirstInstructionOrThrow { + this.opcode == Opcode.IPUT && this.getReference()?.type == "I" + } + + insertModernMiniplayerTypeOverride(iPutIndex) + } else { + findReturnIndicesReversed().forEach { index -> insertModernMiniplayerOverride(index) } + } + } + } + + if (is_19_23_or_greater) { + miniplayerModernConstructorFingerprint.insertLiteralValueBooleanOverride( + MINIPLAYER_DRAG_DROP_FEATURE_KEY, + "enableMiniplayerDragAndDrop", + ) + } + + if (is_19_43_or_greater) { + miniplayerModernConstructorFingerprint.insertLiteralValueBooleanOverride( + MINIPLAYER_HORIZONTAL_DRAG_FEATURE_KEY, + "setHorizontalDrag", + ) + } + + if (is_19_25_or_greater) { + miniplayerModernConstructorFingerprint.insertLiteralValueBooleanOverride( + MINIPLAYER_MODERN_FEATURE_LEGACY_KEY, + "getModernMiniplayerOverride", + ) + + miniplayerModernConstructorFingerprint.insertLiteralValueBooleanOverride( + MINIPLAYER_MODERN_FEATURE_KEY, + "getModernFeatureFlagsActiveOverride", + ) + + miniplayerModernConstructorFingerprint.insertLiteralValueBooleanOverride( + MINIPLAYER_DOUBLE_TAP_FEATURE_KEY, + "enableMiniplayerDoubleTapAction", + ) + } + + if (is_19_26_or_greater) { + miniplayerModernConstructorFingerprint.method.apply { + val literalIndex = indexOfFirstLiteralInstructionOrThrow( + MINIPLAYER_INITIAL_SIZE_FEATURE_KEY, + ) + val targetIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.LONG_TO_INT) + + val register = getInstruction(targetIndex).registerA + + addInstructions( + targetIndex + 1, + """ + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->setMiniplayerDefaultSize(I)I + move-result v$register + """, + ) + } + + // Override a minimum size constant. + miniplayerMinimumSizeFingerprint.method.apply { + val index = indexOfFirstInstructionOrThrow { + opcode == Opcode.CONST_16 && (this as NarrowLiteralInstruction).narrowLiteral == 192 + } + val register = getInstruction(index).registerA + + // Smaller sizes can be used, but the miniplayer will always start in size 170 if set any smaller. + // The 170 initial limit probably could be patched to allow even smaller initial sizes, + // but 170 is already half the horizontal space and smaller does not seem useful. + replaceInstruction(index, "const/16 v$register, 170") + } + } + + if (is_19_36_or_greater) { + miniplayerModernConstructorFingerprint.insertLiteralValueBooleanOverride( + MINIPLAYER_ROUNDED_CORNERS_FEATURE_KEY, + "setRoundedCorners", + ) + } + + // 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 (ytOutlinePictureInPictureWhite24 >= 0) { + miniplayerModernExpandCloseDrawablesFingerprint.match( + miniplayerModernViewParentFingerprint.originalClassDef, + ).method.apply { + listOf( + ytOutlinePictureInPictureWhite24 to ytOutlineXWhite24, + ytOutlineXWhite24 to ytOutlinePictureInPictureWhite24, + ).forEach { (originalResource, replacementResource) -> + val imageResourceIndex = indexOfFirstLiteralInstructionOrThrow(originalResource) + val register = getInstruction(imageResourceIndex).registerA + + replaceInstruction(imageResourceIndex, "const v$register, $replacementResource") + } + } + } + + // endregion + + // region Add hooks to hide 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.match( + miniplayerModernViewParentFingerprint.classDef, + ).method.hookInflatedView( + literalValue, + "Landroid/widget/ImageView;", + "$EXTENSION_CLASS_DESCRIPTOR->$methodName(Landroid/widget/ImageView;)V", + ) + } + + miniplayerModernAddViewListenerFingerprint.match( + miniplayerModernViewParentFingerprint.classDef, + ).method.addInstruction( + 0, + "invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->" + + "hideMiniplayerSubTexts(Landroid/view/View;)V", + ) + + // Modern 2 has a broken overlay subtitle view that is always present. + // Modern 2 uses the same overlay controls as the regular video player, + // and the overlay views are added at runtime. + // Add a hook to the overlay class, and pass the added views to extension. + // + // NOTE: Modern 2 uses the same video UI as the regular player except resized to smaller. + // This patch code could be used to hide other player overlays that do not use Litho. + playerOverlaysLayoutFingerprint.classDef.methods.add( + ImmutableMethod( + YOUTUBE_PLAYER_OVERLAYS_LAYOUT_CLASS_NAME, + "addView", + listOf( + ImmutableMethodParameter("Landroid/view/View;", null, null), + ImmutableMethodParameter("I", null, null), + ImmutableMethodParameter("Landroid/view/ViewGroup\$LayoutParams;", null, null), + ), + "V", + AccessFlags.PUBLIC.value, + null, + 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 + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/panels/popup/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/panels/popup/Fingerprints.kt new file mode 100644 index 000000000..0c31cc83b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/panels/popup/Fingerprints.kt @@ -0,0 +1,13 @@ +package app.revanced.patches.youtube.layout.panels.popup + +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val engagementPanelControllerFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returns("L") + strings( + "EngagementPanelController: cannot show EngagementPanel before EngagementPanelController.init() has been called.", + "[EngagementPanel] Cannot show EngagementPanel before EngagementPanelController.init() has been called.", + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/panels/popup/PlayerPopupPanelsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/panels/popup/PlayerPopupPanelsPatch.kt new file mode 100644 index 000000000..a245dc8a9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/panels/popup/PlayerPopupPanelsPatch.kt @@ -0,0 +1,55 @@ +package app.revanced.patches.youtube.layout.panels.popup + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch + +@Suppress("unused") +val playerPopupPanelsPatch = bytecodePatch( + name = "Disable player popup panels", + description = "Adds an option to disable panels (such as live chat) from opening automatically.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.panels.popup.playerPopupPanelsPatch") + + PreferenceScreen.PLAYER.addPreferences( + SwitchPreference("revanced_hide_player_popup_panels"), + ) + + engagementPanelControllerFingerprint.method.addInstructionsWithLabels( + 0, + """ + invoke-static { }, Lapp/revanced/extension/youtube/patches/DisablePlayerPopupPanelsPatch;->disablePlayerPopupPanels()Z + move-result v0 + if-eqz v0, :player_popup_panels + if-eqz p4, :player_popup_panels + const/4 v0, 0x0 + return-object v0 + :player_popup_panels + nop + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/background/PlayerControlsBackgroundPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/background/PlayerControlsBackgroundPatch.kt new file mode 100644 index 000000000..d039837f0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/background/PlayerControlsBackgroundPatch.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.youtube.layout.player.background + +import app.revanced.patcher.patch.resourcePatch +import app.revanced.util.doRecursively +import org.w3c.dom.Element + +@Suppress("unused") +val playerControlsBackgroundPatch = resourcePatch( + name = "Remove player controls background", + description = "Removes the dark background surrounding the video player controls.", + use = false, +) { + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + 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" + } + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch.kt new file mode 100644 index 000000000..91d39cd1f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/overlay/CustomPlayerOverlayOpacityPatch.kt @@ -0,0 +1,79 @@ +package app.revanced.patches.youtube.layout.player.overlay + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.shared.misc.settings.preference.InputType +import app.revanced.patches.shared.misc.settings.preference.TextPreference +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.indexOfFirstLiteralInstructionOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +internal var scrimOverlayId = -1L + private set + +private val customPlayerOverlayOpacityResourcePatch = resourcePatch { + dependsOn( + settingsPatch, + resourceMappingPatch, + addResourcesPatch, + ) + + execute { + addResources("youtube", "layout.player.overlay.customPlayerOverlayOpacityResourcePatch") + + PreferenceScreen.PLAYER.addPreferences( + TextPreference("revanced_player_overlay_opacity", inputType = InputType.NUMBER), + ) + + scrimOverlayId = resourceMappings[ + "id", + "scrim_overlay", + ] + } +} + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/CustomPlayerOverlayOpacityPatch;" + +@Suppress("unused") +val customPlayerOverlayOpacityPatch = bytecodePatch( + name = "Custom player overlay opacity", + description = "Adds an option to change the opacity of the video player background when player controls are visible.", +) { + dependsOn(customPlayerOverlayOpacityResourcePatch) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + createPlayerOverviewFingerprint.method.apply { + val viewRegisterIndex = + indexOfFirstLiteralInstructionOrThrow(scrimOverlayId) + 3 + val viewRegister = + getInstruction(viewRegisterIndex).registerA + + val insertIndex = viewRegisterIndex + 1 + addInstruction( + insertIndex, + "invoke-static { v$viewRegister }, " + + "$EXTENSION_CLASS_DESCRIPTOR->changeOpacity(Landroid/widget/ImageView;)V", + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/overlay/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/overlay/Fingerprints.kt new file mode 100644 index 000000000..726986840 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/player/overlay/Fingerprints.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.youtube.layout.player.overlay + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val createPlayerOverviewFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returns("V") + opcodes( + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + ) + literal { scrimOverlayId } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt new file mode 100644 index 000000000..2dd73ecc4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/Fingerprints.kt @@ -0,0 +1,137 @@ +package app.revanced.patches.youtube.layout.returnyoutubedislike + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val conversionContextFingerprint = fingerprint { + returns("Ljava/lang/String;") + parameters() + strings( + ", widthConstraint=", + ", heightConstraint=", + ", templateLoggerFactory=", + ", rootDisposableContainer=", + "ConversionContext{containerInternal=", + ) +} + +internal val dislikeFingerprint = fingerprint { + returns("V") + strings("like/dislike") +} + +internal val likeFingerprint = fingerprint { + returns("V") + strings("like/like") +} + +internal val removeLikeFingerprint = fingerprint { + returns("V") + strings("like/removelike") +} + +internal val rollingNumberMeasureAnimatedTextFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Lj\$/util/Optional;") + parameters("L", "Ljava/lang/String;", "L") + opcodes( + Opcode.IGET, // First instruction of method + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.CONST_HIGH16, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.CONST_4, + Opcode.AGET, + Opcode.CONST_4, + Opcode.CONST_4, // Measured text width + ) +} + +/** + * Matches to class found in [rollingNumberMeasureStaticLabelParentFingerprint]. + */ +internal val rollingNumberMeasureStaticLabelFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("F") + parameters("Ljava/lang/String;") + opcodes( + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.RETURN, + ) +} + +internal val rollingNumberMeasureStaticLabelParentFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Ljava/lang/String;") + parameters() + strings("RollingNumberFontProperties{paint=") +} + +internal val rollingNumberSetterFingerprint = fingerprint { + opcodes( + Opcode.INVOKE_DIRECT, + Opcode.IGET_OBJECT, + ) + // Partial string match. + strings("RollingNumberType required properties missing! Need") +} + +internal val rollingNumberTextViewFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L", "F", "F") + opcodes( + Opcode.IPUT, + null, // invoke-direct or invoke-virtual + Opcode.IPUT_OBJECT, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID, + ) + custom { _, classDef -> + classDef.superclass == "Landroid/support/v7/widget/AppCompatTextView;" || classDef.superclass == + "Lcom/google/android/libraries/youtube/rendering/ui/spec/typography/YouTubeAppCompatTextView;" + } +} + +internal val shortsTextViewFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L", "L") + opcodes( + Opcode.INVOKE_SUPER, // first instruction of method + Opcode.IF_NEZ, + null, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + ) +} + +internal val textComponentConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.CONSTRUCTOR, AccessFlags.PRIVATE) + strings("TextComponent") +} + +internal val textComponentDataFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + parameters("L", "L") + strings("text") + custom { _, classDef -> + classDef.fields.find { it.type == "Ljava/util/BitSet;" } != null + } +} + +/** + * Matches against the same class found in [textComponentConstructorFingerprint]. + */ +internal val textComponentLookupFingerprint = fingerprint { + accessFlags(AccessFlags.PROTECTED, AccessFlags.FINAL) + returns("L") + parameters("L") + strings("…") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt new file mode 100644 index 000000000..51c8ccc24 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/ReturnYouTubeDislikePatch.kt @@ -0,0 +1,333 @@ +package app.revanced.patches.youtube.layout.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.instructions +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.IntentPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter +import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch +import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.misc.playservice.is_19_33_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch +import app.revanced.patches.youtube.misc.settings.addSettingPreference +import app.revanced.patches.youtube.misc.settings.newIntent +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.rollingNumberTextViewAnimationUpdateFingerprint +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.* +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.iface.reference.TypeReference +import com.sun.org.apache.bcel.internal.generic.InstructionConst.getInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/ReturnYouTubeDislikePatch;" + +private const val FILTER_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/components/ReturnYouTubeDislikeFilterPatch;" + +@Suppress("unused") +val returnYouTubeDislikePatch = bytecodePatch( + name = "Return YouTube Dislike", + description = "Adds an option to show the dislike count of videos with Return YouTube Dislike.", +) { + dependsOn( + settingsPatch, + sharedExtensionPatch, + addResourcesPatch, + lithoFilterPatch, + videoIdPatch, + playerTypeHookPatch, + versionCheckPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.returnyoutubedislike.returnYouTubeDislikePatch") + + addSettingPreference( + IntentPreference( + key = "revanced_settings_screen_09", + titleKey = "revanced_ryd_settings_title", + summaryKey = null, + intent = newIntent("revanced_ryd_settings_intent"), + ), + ) + + // region Inject newVideoLoaded event handler to update dislikes when a new video is loaded. + + hookVideoId("$EXTENSION_CLASS_DESCRIPTOR->newVideoLoaded(Ljava/lang/String;)V") + + // Hook the player response video id, to start loading RYD sooner in the background. + hookPlayerResponseVideoId("$EXTENSION_CLASS_DESCRIPTOR->preloadVideoId(Ljava/lang/String;Z)V") + + // endregion + + // region Hook like/dislike/remove like button clicks to send votes to the API. + + mapOf( + likeFingerprint to Vote.LIKE, + dislikeFingerprint to Vote.DISLIKE, + removeLikeFingerprint to Vote.REMOVE_LIKE, + ).forEach { (fingerprint, vote) -> + fingerprint.method.addInstructions( + 0, + """ + const/4 v0, ${vote.value} + invoke-static {v0}, $EXTENSION_CLASS_DESCRIPTOR->sendVote(I)V + """, + ) + } + + // endregion + + // region Hook code for creation and cached lookup of text Spans. + + // Alternatively the hook can be made at tht it fails to update the Span when the user dislikes, + // // since the underlying (likes only) tee creation of Spans in TextComponentSpec, + // And it works in all situations excepxt did not change. + // This hook handles all situations, as it's where the created Spans are stored and later reused. + // Find the field name of the conversion context. + val conversionContextField = textComponentConstructorFingerprint.originalClassDef.fields.find { + it.type == conversionContextFingerprint.originalClassDef.type + } ?: throw PatchException("Could not find conversion context field") + + textComponentLookupFingerprint.match(textComponentConstructorFingerprint.originalClassDef) + textComponentLookupFingerprint.method.apply { + // Find the instruction for creating the text data object. + val textDataClassType = textComponentDataFingerprint.originalClassDef.type + + val insertIndex: Int + val tempRegister: Int + val charSequenceRegister: Int + + if (is_19_33_or_greater) { + insertIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_STATIC_RANGE && + getReference()?.returnType == textDataClassType + } + + tempRegister = getInstruction(insertIndex + 1).registerA + + // Find the instruction that sets the span to an instance field. + // The instruction is only a few lines after the creation of the instance. + charSequenceRegister = getInstruction( + indexOfFirstInstructionOrThrow(insertIndex) { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.parameterTypes?.firstOrNull() == "Ljava/lang/CharSequence;" + }, + ).registerD + } else { + insertIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.NEW_INSTANCE && + getReference()?.type == textDataClassType + } + + tempRegister = getInstruction(insertIndex).registerA + + charSequenceRegister = getInstruction( + indexOfFirstInstructionOrThrow(insertIndex) { + opcode == Opcode.IPUT_OBJECT && + getReference()?.type == "Ljava/lang/CharSequence;" + }, + ).registerA + } + + addInstructionsAtControlFlowLabel( + insertIndex, + """ + # Copy conversion context + move-object/from16 v$tempRegister, p0 + iget-object v$tempRegister, v$tempRegister, $conversionContextField + invoke-static { v$tempRegister, v$charSequenceRegister }, $EXTENSION_CLASS_DESCRIPTOR->onLithoTextLoaded(Ljava/lang/Object;Ljava/lang/CharSequence;)Ljava/lang/CharSequence; + move-result-object v$charSequenceRegister + """, + ) + } + + // endregion + + // region Hook for non-litho Short videos. + shortsTextViewFingerprint.method.apply { + val insertIndex = shortsTextViewFingerprint.patternMatch!!.endIndex + 1 + + // If the field is true, the TextView is for a dislike button. + val isDisLikesBooleanInstruction = instructions.first { instruction -> + instruction.opcode == Opcode.IGET_BOOLEAN + } as ReferenceInstruction + + val isDisLikesBooleanReference = isDisLikesBooleanInstruction.reference + + // Like/Dislike button TextView field. + val textViewFieldInstruction = instructions.first { instruction -> + instruction.opcode == Opcode.IGET_OBJECT + } as ReferenceInstruction + + val textViewFieldReference = textViewFieldInstruction.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. + addInstructionsWithLabels( + insertIndex, + """ + # Check, if the TextView is for a dislike button + iget-boolean v0, p0, $isDisLikesBooleanReference + if-eqz v0, :is_like + + # Hook the TextView, if it is for the dislike button + iget-object v0, p0, $textViewFieldReference + invoke-static {v0}, $EXTENSION_CLASS_DESCRIPTOR->setShortsDislikes(Landroid/view/View;)Z + move-result v0 + if-eqz v0, :ryd_disabled + return-void + + :is_like + :ryd_disabled + nop + """, + ) + } + + // endregion + + // region Hook for litho Shorts + + // Filter that parses the video id from the UI + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + + // Player response video id is needed to search for the video ids in Shorts litho components. + hookPlayerResponseVideoId("$FILTER_CLASS_DESCRIPTOR->newPlayerResponseVideoId(Ljava/lang/String;Z)V") + + // endregion + + // region Hook rolling numbers. + + // Do this last to allow patching old unsupported versions (if the user really wants), + // On older unsupported version this will fail to match and throw an exception, + // but everything will still work correctly anyway. + val dislikesIndex = rollingNumberSetterFingerprint.patternMatch!!.endIndex + + rollingNumberSetterFingerprint.method.apply { + val insertIndex = 1 + + val charSequenceInstanceRegister = + getInstruction(0).registerA + val charSequenceFieldReference = + getInstruction(dislikesIndex).reference + + 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_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. + val patternMatch = rollingNumberMeasureAnimatedTextFingerprint.patternMatch!! + // Additional check to verify the opcodes are at the start of the method + if (patternMatch.startIndex != 0) throw PatchException("Unexpected opcode location") + val endIndex = patternMatch.endIndex + rollingNumberMeasureAnimatedTextFingerprint.method.apply { + val measuredTextWidthRegister = getInstruction(endIndex).registerA + + addInstructions( + endIndex + 1, + """ + invoke-static {p1, v$measuredTextWidthRegister}, $EXTENSION_CLASS_DESCRIPTOR->onRollingNumberMeasured(Ljava/lang/String;F)F + move-result v$measuredTextWidthRegister + """, + ) + } + + // Additional text measurement method. Used if YouTube decides not to animate the likes count + // and sometimes used for initial video load. + rollingNumberMeasureStaticLabelFingerprint.match( + rollingNumberMeasureStaticLabelParentFingerprint.originalClassDef, + ).let { + val measureTextIndex = it.patternMatch!!.startIndex + 1 + it.method.apply { + val freeRegister = getInstruction(0).registerA + + addInstructions( + measureTextIndex + 1, + """ + move-result v$freeRegister + invoke-static {p1, v$freeRegister}, $EXTENSION_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. + // Initial TextView is set in this method. + val initiallyCreatedTextViewMethod = rollingNumberTextViewFingerprint.method + + // Videos 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. + arrayOf( + initiallyCreatedTextViewMethod, + rollingNumberTextViewAnimationUpdateFingerprint.method, + ).forEach { insertMethod -> + insertMethod.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_CLASS_DESCRIPTOR->updateRollingNumber(Landroid/widget/TextView;Ljava/lang/CharSequence;)Ljava/lang/CharSequence; + move-result-object v$textSpanRegister + """, + ) + } + } + + // endregion + } +} + +enum class Vote(val value: Int) { + LIKE(1), + DISLIKE(-1), + REMOVE_LIKE(0), +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/searchbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/searchbar/Fingerprints.kt new file mode 100644 index 000000000..69fc26dae --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/searchbar/Fingerprints.kt @@ -0,0 +1,31 @@ +package app.revanced.patches.youtube.layout.searchbar + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val createSearchSuggestionsFingerprint = fingerprint { + opcodes( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.CONST_4, + ) + strings("ss_rds") +} + +internal val setWordmarkHeaderFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("Landroid/widget/ImageView;") + opcodes( + Opcode.IGET_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.IF_NEZ, + Opcode.IGET_BOOLEAN, + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, + Opcode.CONST, + null, // invoke-static or invoke-virtual. + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/searchbar/WideSearchbarPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/searchbar/WideSearchbarPatch.kt new file mode 100644 index 000000000..233d611cf --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/searchbar/WideSearchbarPatch.kt @@ -0,0 +1,82 @@ +package app.revanced.patches.youtube.layout.searchbar + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.BytecodePatchContext +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/WideSearchbarPatch;" + +@Suppress("unused") +val wideSearchbarPatch = bytecodePatch( + name = "Wide searchbar", + description = "Adds an option to replace the search icon with a wide search bar. This will hide the YouTube logo when active.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.searchbar.wideSearchbarPatch") + + PreferenceScreen.FEED.addPreferences( + SwitchPreference("revanced_wide_searchbar"), + ) + + /** + * Navigate a fingerprints method at a given index mutably. + * + * @param index The index to navigate to. + * @param from The fingerprint to navigate the method on. + * @return The [MutableMethod] which was navigated on. + */ + fun BytecodePatchContext.walkMutable(index: Int, from: Fingerprint) = + navigate(from.originalMethod).to(index).stop() + + /** + * Injects instructions required for certain methods. + */ + fun MutableMethod.injectSearchBarHook() { + val insertIndex = implementation!!.instructions.size - 1 + val insertRegister = getInstruction(insertIndex).registerA + + addInstructions( + insertIndex, + """ + invoke-static {v$insertRegister}, $EXTENSION_CLASS_DESCRIPTOR->enableWideSearchbar(Z)Z + move-result v$insertRegister + """, + ) + } + + mapOf( + setWordmarkHeaderFingerprint to 1, + createSearchSuggestionsFingerprint to createSearchSuggestionsFingerprint.patternMatch!!.startIndex, + ).forEach { (fingerprint, callIndex) -> + walkMutable(callIndex, fingerprint).injectSearchBarHook() + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt new file mode 100644 index 000000000..67a6b017d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/Fingerprints.kt @@ -0,0 +1,50 @@ +package app.revanced.patches.youtube.layout.seekbar + +import app.revanced.patcher.fingerprint +import app.revanced.util.containsLiteralInstruction +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val fullscreenSeekbarThumbnailsFingerprint = fingerprint { + returns("Z") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + parameters() + literal { 45398577 } +} + +internal val playerSeekbarColorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + custom { method, _ -> + method.containsLiteralInstruction(inlineTimeBarColorizedBarPlayedColorDarkId) && + method.containsLiteralInstruction(inlineTimeBarPlayedNotHighlightedColorId) + } +} + +internal val setSeekbarClickedColorFingerprint = fingerprint { + opcodes(Opcode.CONST_HIGH16) + strings("YOUTUBE", "PREROLL", "POSTROLL") + custom { _, classDef -> + classDef.endsWith("ControlsOverlayStyle;") + } +} + +internal val shortsSeekbarColorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + literal { reelTimeBarPlayedColorId } +} + +internal const val PLAYER_SEEKBAR_GRADIENT_FEATURE_FLAG = 45617850L + +internal val playerSeekbarGradientConfigFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters() + literal { PLAYER_SEEKBAR_GRADIENT_FEATURE_FLAG } +} + +internal val lithoLinearGradientFingerprint = fingerprint { + accessFlags(AccessFlags.STATIC) + returns("Landroid/graphics/LinearGradient;") + parameters("F", "F", "F", "F", "[I", "[F") +} diff --git a/src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/api/fingerprints/AbstractClientIdFingerprint.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/RestoreOldSeekbarThumbnailsPatch.kt similarity index 100% rename from src/main/kotlin/app/revanced/patches/reddit/customclients/infinityforreddit/api/fingerprints/AbstractClientIdFingerprint.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/RestoreOldSeekbarThumbnailsPatch.kt diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/SeekbarColorPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/SeekbarColorPatch.kt new file mode 100644 index 000000000..49a5444ab --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/seekbar/SeekbarColorPatch.kt @@ -0,0 +1,143 @@ +package app.revanced.patches.youtube.layout.seekbar + +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.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.youtube.layout.theme.lithoColorHookPatch +import app.revanced.patches.youtube.layout.theme.lithoColorOverrideHook +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.playservice.is_19_23_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch +import app.revanced.patches.youtube.misc.settings.settingsPatch +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 +import org.w3c.dom.Element + +internal var reelTimeBarPlayedColorId = -1L + private set +internal var inlineTimeBarColorizedBarPlayedColorDarkId = -1L + private set +internal var inlineTimeBarPlayedNotHighlightedColorId = -1L + private set + +private val seekbarColorResourcePatch = resourcePatch { + dependsOn( + settingsPatch, + resourceMappingPatch, + versionCheckPatch, + ) + + execute { + reelTimeBarPlayedColorId = resourceMappings[ + "color", + "reel_time_bar_played_color", + ] + inlineTimeBarColorizedBarPlayedColorDarkId = resourceMappings[ + "color", + "inline_time_bar_colorized_bar_played_color_dark", + ] + inlineTimeBarPlayedNotHighlightedColorId = resourceMappings[ + "color", + "inline_time_bar_played_not_highlighted_color", + ] + + // Edit the resume playback drawable and replace the progress bar with a custom drawable + 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.theme.ProgressBarDrawable", + ) + scaleNode.replaceChild(replacementNode, shapeNode) + } + } +} + +private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/theme/SeekbarColorPatch;" + +val seekbarColorPatch = bytecodePatch( + description = "Hide or set a custom seekbar color", +) { + dependsOn( + sharedExtensionPatch, + lithoColorHookPatch, + seekbarColorResourcePatch, + ) + + execute { + fun MutableMethod.addColorChangeInstructions(resourceId: Long) { + val registerIndex = indexOfFirstLiteralInstructionOrThrow(resourceId) + 2 + val colorRegister = getInstruction(registerIndex).registerA + addInstructions( + registerIndex + 1, + """ + invoke-static { v$colorRegister }, $EXTENSION_CLASS_DESCRIPTOR->getVideoPlayerSeekbarColor(I)I + move-result v$colorRegister + """, + ) + } + + playerSeekbarColorFingerprint.method.apply { + addColorChangeInstructions(inlineTimeBarColorizedBarPlayedColorDarkId) + addColorChangeInstructions(inlineTimeBarPlayedNotHighlightedColorId) + } + + shortsSeekbarColorFingerprint.method.apply { + addColorChangeInstructions(reelTimeBarPlayedColorId) + } + + setSeekbarClickedColorFingerprint.originalMethod.let { + val setColorMethodIndex = setSeekbarClickedColorFingerprint.patternMatch!!.startIndex + 1 + + navigate(it).to(setColorMethodIndex).stop().apply { + val colorRegister = getInstruction(0).registerA + addInstructions( + 0, + """ + invoke-static { v$colorRegister }, $EXTENSION_CLASS_DESCRIPTOR->getVideoPlayerSeekbarClickedColor(I)I + move-result v$colorRegister + """, + ) + } + } + + if (is_19_23_or_greater) { + playerSeekbarGradientConfigFingerprint.method.apply { + val literalIndex = indexOfFirstLiteralInstructionOrThrow(PLAYER_SEEKBAR_GRADIENT_FEATURE_FLAG) + val resultIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT) + val register = getInstruction(resultIndex).registerA + + addInstructions( + resultIndex + 1, + """ + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->playerSeekbarGradientEnabled(Z)Z + move-result v$register + """, + ) + } + + lithoLinearGradientFingerprint.method.addInstruction( + 0, + "invoke-static/range { p4 .. p5 }, $EXTENSION_CLASS_DESCRIPTOR->setLinearGradient([I[F)V", + ) + } + + lithoColorOverrideHook(EXTENSION_CLASS_DESCRIPTOR, "getLithoColor") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/Fingerprints.kt new file mode 100644 index 000000000..cd48868f5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/Fingerprints.kt @@ -0,0 +1,22 @@ +package app.revanced.patches.youtube.layout.shortsautoplay + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val reelEnumConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) + opcodes(Opcode.RETURN_VOID) + strings( + "REEL_LOOP_BEHAVIOR_UNKNOWN", + "REEL_LOOP_BEHAVIOR_SINGLE_PLAY", + "REEL_LOOP_BEHAVIOR_REPEAT", + "REEL_LOOP_BEHAVIOR_END_SCREEN", + ) +} + +internal val reelPlaybackRepeatFingerprint = fingerprint { + returns("V") + parameters("L") + strings("YoutubePlayerState is in throwing an Error.") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt new file mode 100644 index 000000000..04088be7e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/shortsautoplay/ShortsAutoplayPatch.kt @@ -0,0 +1,100 @@ +package app.revanced.patches.youtube.layout.shortsautoplay + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.playservice.is_19_34_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.mainActivityOnCreateFingerprint +import app.revanced.util.findInstructionIndicesReversedOrThrow +import app.revanced.util.getReference +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/ShortsAutoplayPatch;" + +@Suppress("unused") +val shortsAutoplayPatch = bytecodePatch( + name = "Shorts autoplay", + description = "Adds options to automatically play the next Short.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + resourceMappingPatch, + versionCheckPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.shortsautoplay.shortsAutoplayPatch") + + PreferenceScreen.SHORTS.addPreferences( + SwitchPreference("revanced_shorts_autoplay"), + ) + + if (is_19_34_or_greater) { + PreferenceScreen.SHORTS.addPreferences( + SwitchPreference("revanced_shorts_autoplay_background"), + ) + } + + // Main activity is used to check if app is in pip mode. + mainActivityOnCreateFingerprint.method.addInstructions( + 0, + "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->" + + "setMainActivity(Landroid/app/Activity;)V", + ) + + val reelEnumClass = reelEnumConstructorFingerprint.originalClassDef.type + + reelEnumConstructorFingerprint.method.apply { + val insertIndex = reelEnumConstructorFingerprint.patternMatch!!.startIndex + + addInstructions( + insertIndex, + """ + # Pass the first enum value to extension. + # Any enum value of this type will work. + sget-object v0, $reelEnumClass->a:$reelEnumClass + invoke-static { v0 }, $EXTENSION_CLASS_DESCRIPTOR->setYTShortsRepeatEnum(Ljava/lang/Enum;)V + """, + ) + } + + reelPlaybackRepeatFingerprint.method.apply { + // The behavior enums are looked up from an ordinal value to an enum type. + findInstructionIndicesReversedOrThrow { + val reference = getReference() + reference?.definingClass == reelEnumClass && + reference.parameterTypes.firstOrNull() == "I" && + reference.returnType == reelEnumClass + }.forEach { index -> + val register = getInstruction(index + 1).registerA + + addInstructions( + index + 2, + """ + invoke-static {v$register}, $EXTENSION_CLASS_DESCRIPTOR->changeShortsRepeatBehavior(Ljava/lang/Enum;)Ljava/lang/Enum; + move-result-object v$register + """, + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/Fingerprints.kt new file mode 100644 index 000000000..612ceb6c8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/Fingerprints.kt @@ -0,0 +1,64 @@ +package app.revanced.patches.youtube.layout.sponsorblock + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +internal val appendTimeFingerprint = fingerprint { + returns("V") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + parameters("Ljava/lang/CharSequence;", "Ljava/lang/CharSequence;", "Ljava/lang/CharSequence;") + opcodes( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.CHECK_CAST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.IF_NEZ, + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.CHECK_CAST, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID, + ) +} + +internal val controlsOverlayFingerprint = fingerprint { + returns("V") + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + parameters() + opcodes( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, // R.id.inset_overlay_view_layout + Opcode.IPUT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + Opcode.NEW_INSTANCE, + ) +} + +internal val rectangleFieldInvalidatorFingerprint = fingerprint { + returns("V") + custom { method, _ -> + val instructions = method.implementation?.instructions!! + val instructionCount = instructions.count() + + // the method has definitely more than 5 instructions + if (instructionCount < 5) return@custom false + + val referenceInstruction = instructions.elementAt(instructionCount - 2) // the second to last instruction + val reference = ((referenceInstruction as? ReferenceInstruction)?.reference as? MethodReference) + + reference?.parameterTypes?.size == 1 && reference.name == "invalidate" // the reference is the invalidate(..) method + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt new file mode 100644 index 000000000..18c440d4e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/SponsorBlockPatch.kt @@ -0,0 +1,251 @@ +package app.revanced.patches.youtube.layout.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.instructions +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.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.settings.preference.IntentPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.playercontrols.* +import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.misc.settings.addSettingPreference +import app.revanced.patches.youtube.misc.settings.newIntent +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.* +import app.revanced.patches.youtube.video.information.onCreateHook +import app.revanced.patches.youtube.video.information.videoInformationPatch +import app.revanced.patches.youtube.video.information.videoTimeHook +import app.revanced.patches.youtube.video.videoid.hookBackgroundPlayVideoId +import app.revanced.patches.youtube.video.videoid.videoIdPatch +import app.revanced.util.* +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.* +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.StringReference + +private val sponsorBlockResourcePatch = resourcePatch { + dependsOn( + settingsPatch, + resourceMappingPatch, + addResourcesPatch, + playerControlsPatch, + ) + + execute { + addResources("youtube", "layout.sponsorblock.sponsorBlockResourcePatch") + + addSettingPreference( + IntentPreference( + key = "revanced_settings_screen_10", + titleKey = "revanced_sb_settings_title", + summaryKey = null, + intent = newIntent("revanced_sb_settings_intent"), + ), + ) + + arrayOf( + ResourceGroup( + "layout", + "revanced_sb_inline_sponsor_overlay.xml", + "revanced_sb_new_segment.xml", + "revanced_sb_skip_sponsor_button.xml", + ), + ResourceGroup( + // required resource for back button, because when the base APK is used, this resource will not exist + "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", + ), + ResourceGroup( + // required resource for back button, because when the base APK is used, this resource will not exist + "drawable-xxxhdpi", + "quantum_ic_skip_next_white_24.png", + ), + ).forEach { resourceGroup -> + copyResources("sponsorblock", resourceGroup) + } + + addTopControl("sponsorblock") + } +} + +private const val EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/sponsorblock/SegmentPlaybackController;" +private const val EXTENSION_CREATE_SEGMENT_BUTTON_CONTROLLER_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/sponsorblock/ui/CreateSegmentButtonController;" +private const val EXTENSION_VOTING_BUTTON_CONTROLLER_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/sponsorblock/ui/VotingButtonController;" +private const val EXTENSION_SPONSORBLOCK_VIEW_CONTROLLER_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/sponsorblock/ui/SponsorBlockViewController;" + +@Suppress("unused") +val sponsorBlockPatch = bytecodePatch( + name = "SponsorBlock", + description = "Adds options to enable and configure SponsorBlock, which can skip undesired video segments such as sponsored content.", +) { + dependsOn( + sharedExtensionPatch, + videoIdPatch, + // Required to skip segments on time. + videoInformationPatch, + // Used to prevent SponsorBlock from running on Shorts because SponsorBlock does not yet support Shorts. + playerTypeHookPatch, + playerControlsPatch, + sponsorBlockResourcePatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + // Hook the video time methods. + videoTimeHook( + EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR, + "setVideoTime", + ) + + hookBackgroundPlayVideoId( + EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR + + "->setCurrentVideoId(Ljava/lang/String;)V", + ) + + // Seekbar drawing + seekbarOnDrawFingerprint.match(seekbarFingerprint.originalClassDef).method.apply { + // Get left and right of seekbar rectangle. + val moveRectangleToRegisterIndex = indexOfFirstInstructionOrThrow(Opcode.MOVE_OBJECT_FROM16) + + addInstruction( + moveRectangleToRegisterIndex + 1, + "invoke-static/range { p0 .. p0 }, " + + "$EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->setSponsorBarRect(Ljava/lang/Object;)V", + ) + + // Set the thickness of the segment. + val thicknessIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_STATIC && getReference()?.name == "round" + } + val thicknessRegister = getInstruction(thicknessIndex).registerC + addInstruction( + thicknessIndex + 2, + "invoke-static { v$thicknessRegister }, " + + "$EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->setSponsorBarThickness(I)V", + ) + + // Find the drawCircle call and draw the segment before it. + val drawCircleIndex = indexOfFirstInstructionReversedOrThrow { + getReference()?.name == "drawCircle" + } + val drawCircleInstruction = getInstruction(drawCircleIndex) + val canvasInstanceRegister = drawCircleInstruction.registerC + val centerYRegister = drawCircleInstruction.registerE + + addInstruction( + drawCircleIndex, + "invoke-static { v$canvasInstanceRegister, v$centerYRegister }, " + + "$EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->" + + "drawSponsorTimeBars(Landroid/graphics/Canvas;F)V", + ) + } + + // Change visibility of the buttons. + initializeTopControl(EXTENSION_CREATE_SEGMENT_BUTTON_CONTROLLER_CLASS_DESCRIPTOR) + injectVisibilityCheckCall(EXTENSION_CREATE_SEGMENT_BUTTON_CONTROLLER_CLASS_DESCRIPTOR) + + initializeTopControl(EXTENSION_VOTING_BUTTON_CONTROLLER_CLASS_DESCRIPTOR) + injectVisibilityCheckCall(EXTENSION_VOTING_BUTTON_CONTROLLER_CLASS_DESCRIPTOR) + + // Append the new time to the player layout. + val appendTimePatternScanStartIndex = appendTimeFingerprint.patternMatch!!.startIndex + appendTimeFingerprint.method.apply { + val register = getInstruction(appendTimePatternScanStartIndex + 1).registerA + + addInstructions( + appendTimePatternScanStartIndex + 2, + """ + invoke-static { v$register }, $EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR->appendTimeWithoutSegments(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$register + """, + ) + } + + // Initialize the player controller. + onCreateHook(EXTENSION_SEGMENT_PLAYBACK_CONTROLLER_CLASS_DESCRIPTOR, "initialize") + + // Initialize the SponsorBlock view. + controlsOverlayFingerprint.match(layoutConstructorFingerprint.originalClassDef).let { + val startIndex = it.patternMatch!!.startIndex + it.method.apply { + val frameLayoutRegister = (getInstruction(startIndex + 2) as OneRegisterInstruction).registerA + addInstruction( + startIndex + 3, + "invoke-static {v$frameLayoutRegister}, $EXTENSION_SPONSORBLOCK_VIEW_CONTROLLER_CLASS_DESCRIPTOR->initialize(Landroid/view/ViewGroup;)V", + ) + } + } + + // Set seekbar draw rectangle. + rectangleFieldInvalidatorFingerprint.match(seekbarOnDrawFingerprint.originalClassDef).method.apply { + val fieldIndex = instructions.count() - 3 + val fieldReference = getInstruction(fieldIndex).reference as FieldReference + + // replace the "replaceMeWith*" strings + proxy(classes.first { it.type.endsWith("SegmentPlaybackController;") }) + .mutableClass + .methods + .find { it.name == "setSponsorBarRect" } + ?.let { method -> + fun MutableMethod.replaceStringInstruction(index: Int, instruction: Instruction, with: String) { + val register = (instruction as OneRegisterInstruction).registerA + this.replaceInstruction( + index, + "const-string v$register, \"$with\"", + ) + } + for ((index, it) in method.instructions.withIndex()) { + if (it.opcode.ordinal != Opcode.CONST_STRING.ordinal) continue + + when (((it as ReferenceInstruction).reference as StringReference).string) { + "replaceMeWithsetSponsorBarRect" -> method.replaceStringInstruction( + index, + it, + fieldReference.name, + ) + } + } + } ?: throw PatchException("Could not find the method which contains the replaceMeWith* strings") + } + + // 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. + autoRepeatFingerprint.match(autoRepeatParentFingerprint.originalClassDef).method.addInstruction( + 0, + "invoke-static {}, $EXTENSION_SPONSORBLOCK_VIEW_CONTROLLER_CLASS_DESCRIPTOR->endOfVideoReached()V", + ) + + // TODO: Channel whitelisting. + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/Fingerprints.kt new file mode 100644 index 000000000..969d90e02 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/Fingerprints.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.youtube.layout.spoofappversion + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val spoofAppVersionFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("L") + parameters("L") + opcodes( + Opcode.IGET_OBJECT, + Opcode.GOTO, + Opcode.CONST_STRING, + ) + // Instead of applying a bytecode patch, it might be possible to only rely on code from the extension and + // manually set the desired version string as this keyed value in the SharedPreferences. + // But, this bytecode patch is simple and it works. + strings("pref_override_build_version_name") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch.kt new file mode 100644 index 000000000..60f84dd72 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/spoofappversion/SpoofAppVersionPatch.kt @@ -0,0 +1,64 @@ +package app.revanced.patches.youtube.layout.spoofappversion + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.ListPreference +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/spoof/SpoofAppVersionPatch;" + +@Suppress("unused") +val spoofAppVersionPatch = bytecodePatch( + name = "Spoof app version", + description = "Adds an option to trick YouTube into thinking you are running an older version of the app. " + + "This can be used to restore old UI elements and features.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.spoofappversion.spoofAppVersionPatch") + + PreferenceScreen.GENERAL_LAYOUT.addPreferences( + SwitchPreference("revanced_spoof_app_version"), + ListPreference( + key = "revanced_spoof_app_version_target", + summaryKey = null, + ), + ) + + val insertIndex = spoofAppVersionFingerprint.patternMatch!!.startIndex + 1 + val buildOverrideNameRegister = + spoofAppVersionFingerprint.method.getInstruction(insertIndex - 1).registerA + + spoofAppVersionFingerprint.method.addInstructions( + insertIndex, + """ + invoke-static {v$buildOverrideNameRegister}, $EXTENSION_CLASS_DESCRIPTOR->getYouTubeVersionOverride(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$buildOverrideNameRegister + """, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt new file mode 100644 index 000000000..26c2633ae --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/ChangeStartPagePatch.kt @@ -0,0 +1,75 @@ +package app.revanced.patches.youtube.layout.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.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.ListPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.StringReference + +private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/ChangeStartPagePatch;" + +@Suppress("unused") +val changeStartPagePatch = bytecodePatch( + name = "Change start page", + description = "Adds an option to set which page the app opens in instead of the homepage.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.startpage.changeStartPagePatch") + + PreferenceScreen.GENERAL_LAYOUT.addPreferences( + ListPreference( + key = "revanced_change_start_page", + summaryKey = null, + ), + ) + + // Hook browseId. + browseIdFingerprint.method.apply { + val browseIdIndex = indexOfFirstInstructionOrThrow { + 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 browserId assigned to Shorts and Search. + // Just hook the Intent action. + intentActionFingerprint.method.addInstruction( + 0, + "invoke-static { p1 }, $EXTENSION_CLASS_DESCRIPTOR->overrideIntentAction(Landroid/content/Intent;)V", + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/Fingerprints.kt new file mode 100644 index 000000000..022084020 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startpage/Fingerprints.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.youtube.layout.startpage + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.Opcode + +internal val intentActionFingerprint = fingerprint { + parameters("Landroid/content/Intent;") + strings("has_handled_intent") +} + +internal val browseIdFingerprint = fingerprint { + returns("Lcom/google/android/apps/youtube/app/common/ui/navigation/PaneDescriptor;") + parameters() + opcodes( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.RETURN_OBJECT, + ) + strings("FEwhat_to_watch") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatch.kt new file mode 100644 index 000000000..2f48ee67e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/DisableResumingShortsOnStartupPatch.kt @@ -0,0 +1,97 @@ +package app.revanced.patches.youtube.layout.startupshortsreset + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.addInstructionsAtControlFlowLabel +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 const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/DisableResumingStartupShortsPlayerPatch;" + +@Suppress("unused") +val disableResumingShortsOnStartupPatch = bytecodePatch( + 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.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.startupshortsreset.disableResumingShortsOnStartupPatch") + + PreferenceScreen.SHORTS.addPreferences( + SwitchPreference("revanced_disable_resuming_shorts_player"), + ) + + userWasInShortsConfigFingerprint.originalMethod.apply { + val startIndex = indexOfOptionalInstruction(this) + val walkerIndex = indexOfFirstInstructionOrThrow(startIndex) { + val reference = getReference() + opcode == Opcode.INVOKE_VIRTUAL && + reference?.returnType == "Z" && + reference.definingClass != "Lj${'$'}/util/Optional;" && + reference.parameterTypes.isEmpty() + } + + // Presumably a method that processes the ProtoDataStore value (boolean) for the 'user_was_in_shorts' key. + navigate(this).to(walkerIndex).stop().addInstructionsWithLabels( + 0, + """ + invoke-static {}, $EXTENSION_CLASS_DESCRIPTOR->disableResumingStartupShortsPlayer()Z + move-result v0 + if-eqz v0, :show + const/4 v0, 0x0 + return v0 + :show + nop + """, + ) + } + + userWasInShortsFingerprint.method.apply { + val listenableInstructionIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_INTERFACE && + getReference()?.definingClass == "Lcom/google/common/util/concurrent/ListenableFuture;" && + getReference()?.name == "isDone" + } + val freeRegister = getInstruction(listenableInstructionIndex + 1).registerA + + addInstructionsAtControlFlowLabel( + listenableInstructionIndex, + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->disableResumingStartupShortsPlayer()Z + move-result v$freeRegister + if-eqz v$freeRegister, :show_startup_shorts_player + return-void + :show_startup_shorts_player + nop + """, + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/Fingerprints.kt new file mode 100644 index 000000000..326ebbe14 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/startupshortsreset/Fingerprints.kt @@ -0,0 +1,41 @@ +package app.revanced.patches.youtube.layout.startupshortsreset + +import app.revanced.patcher.fingerprint +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.reference.ImmutableMethodReference +import com.android.tools.smali.dexlib2.util.MethodUtil + +internal val userWasInShortsFingerprint = fingerprint { + returns("V") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + parameters("Ljava/lang/Object;") + strings("Failed to read user_was_in_shorts proto after successful warmup") +} + +/** + * 18.15.40+ + */ +internal val userWasInShortsConfigFingerprint = fingerprint { + returns("V") + strings("Failed to get offline response: ") + custom { method, _ -> + indexOfOptionalInstruction(method) >= 0 + } +} + +private val optionalOfMethodReference = ImmutableMethodReference( + "Lj${'$'}/util/Optional;", + "of", + listOf("Ljava/lang/Object;"), + "Lj${'$'}/util/Optional;", +) + +fun indexOfOptionalInstruction(method: Method) = method.indexOfFirstInstruction { + val reference = getReference() ?: return@indexOfFirstInstruction false + + MethodUtil.methodSignaturesMatch(reference, optionalOfMethodReference) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatch.kt new file mode 100644 index 000000000..181f5b920 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/tablet/EnableTabletLayoutPatch.kt @@ -0,0 +1,64 @@ +package app.revanced.patches.youtube.layout.tablet + +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.bytecodePatch +import app.revanced.patcher.util.smali.ExternalLabel +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch + +const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/TabletLayoutPatch;" + +@Suppress("unused") +val enableTabletLayoutPatch = bytecodePatch( + name = "Enable tablet layout", + description = "Adds an option to enable tablet layout.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.tablet.enableTabletLayoutPatch") + + PreferenceScreen.GENERAL_LAYOUT.addPreferences( + SwitchPreference("revanced_tablet_layout"), + ) + + getFormFactorFingerprint.method.apply { + val returnIsLargeFormFactorIndex = instructions.lastIndex - 4 + val returnIsLargeFormFactorLabel = getInstruction(returnIsLargeFormFactorIndex) + + addInstructionsWithLabels( + 0, + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->getTabletLayoutEnabled()Z + move-result v0 + if-nez v0, :is_large_form_factor + """, + ExternalLabel( + "is_large_form_factor", + returnIsLargeFormFactorLabel, + ), + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/tablet/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/tablet/Fingerprints.kt new file mode 100644 index 000000000..30667a85b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/tablet/Fingerprints.kt @@ -0,0 +1,25 @@ +package app.revanced.patches.youtube.layout.tablet + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val getFormFactorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("L") + parameters("Landroid/content/Context;", "Ljava/util/List;") + opcodes( + Opcode.SGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ, + Opcode.SGET_OBJECT, + Opcode.RETURN_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.RETURN_OBJECT, + ) + strings("") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/Fingerprints.kt new file mode 100644 index 000000000..9ed098a1b --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/Fingerprints.kt @@ -0,0 +1,56 @@ +package app.revanced.patches.youtube.layout.theme + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val lithoThemeFingerprint = fingerprint { + accessFlags(AccessFlags.PROTECTED, AccessFlags.FINAL) + returns("V") + parameters("Landroid/graphics/Rect;") + opcodes( + Opcode.APUT, + Opcode.NEW_INSTANCE, + Opcode.INVOKE_DIRECT, + Opcode.IGET_OBJECT, + Opcode.SGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.IPUT_OBJECT, + Opcode.IGET, + Opcode.IF_EQZ, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_NEZ, + Opcode.IGET_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.RETURN_VOID, + ) + custom { method, _ -> + method.name == "onBoundsChange" + } +} + +internal val themeHelperDarkColorFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC) + returns("Ljava/lang/String;") + parameters() + custom { method, _ -> + method.name == "darkThemeResourceName" && + method.definingClass == "Lapp/revanced/extension/youtube/ThemeHelper;" + } +} + +internal val themeHelperLightColorFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC) + returns("Ljava/lang/String;") + parameters() + custom { method, _ -> + method.name == "lightThemeResourceName" && + method.definingClass == "Lapp/revanced/extension/youtube/ThemeHelper;" + } +} + +internal val useGradientLoadingScreenFingerprint = fingerprint { + literal { GRADIENT_LOADING_SCREEN_AB_CONSTANT } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/LithoColorHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/LithoColorHookPatch.kt new file mode 100644 index 000000000..fdab6c4b8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/LithoColorHookPatch.kt @@ -0,0 +1,28 @@ +package app.revanced.patches.youtube.layout.theme + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +lateinit var lithoColorOverrideHook: (targetMethodClass: String, targetMethodName: String) -> Unit + private set + +val lithoColorHookPatch = bytecodePatch( + description = "Adds a hook to set color of Litho components.", +) { + + execute { + + var insertionIndex = lithoThemeFingerprint.patternMatch!!.endIndex - 1 + + lithoColorOverrideHook = { targetMethodClass, targetMethodName -> + lithoThemeFingerprint.method.addInstructions( + insertionIndex, + """ + invoke-static { p1 }, $targetMethodClass->$targetMethodName(I)I + move-result p1 + """, + ) + insertionIndex += 2 + } + } +} 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..194c4dbba --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/theme/ThemePatch.kt @@ -0,0 +1,245 @@ +package app.revanced.patches.youtube.layout.theme + +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.patch.stringOption +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.settings.preference.InputType +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.shared.misc.settings.preference.TextPreference +import app.revanced.patches.youtube.layout.seekbar.seekbarColorPatch +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.forEachChildElement +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 org.w3c.dom.Element + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/theme/ThemePatch;" + +internal const val GRADIENT_LOADING_SCREEN_AB_CONSTANT = 45412406L + +@Suppress("unused") +val themePatch = bytecodePatch( + name = "Theme", + description = "Adds options for theming and applies a custom background theme (dark background theme defaults to amoled black).", +) { + val amoledBlackColor = "@android:color/black" + val whiteColor = "@android:color/white" + + val darkThemeBackgroundColor by stringOption( + key = "darkThemeBackgroundColor", + default = amoledBlackColor, + values = mapOf( + "Amoled black" to amoledBlackColor, + "Material You" to "@android:color/system_neutral1_900", + "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", + ), + title = "Dark theme background color", + description = "Can be a hex color (#AARRGGBB) or a color resource reference.", + ) + + val lightThemeBackgroundColor by stringOption( + key = "lightThemeBackgroundColor", + default = whiteColor, + values = mapOf( + "White" to whiteColor, + "Material You" to "@android:color/system_neutral1_50", + "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", + ), + title = "Light theme background color", + description = "Can be a hex color (#AARRGGBB) or a color resource reference.", + ) + + dependsOn( + lithoColorHookPatch, + seekbarColorPatch, + resourcePatch { + dependsOn( + settingsPatch, + resourceMappingPatch, + addResourcesPatch, + ) + + execute { + addResources("youtube", "layout.theme.themeResourcePatch") + + PreferenceScreen.SEEKBAR.addPreferences( + SwitchPreference("revanced_seekbar_custom_color"), + TextPreference("revanced_seekbar_custom_color_value", inputType = InputType.TEXT_CAP_CHARACTERS), + ) + + // Edit theme colors via resources. + 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_black0", "yt_black1", "yt_black1_opacity95", "yt_black1_opacity98", "yt_black2", "yt_black3", + "yt_black4", "yt_status_bar_background_dark", "material_grey_850", + -> darkThemeBackgroundColor ?: continue + + "yt_white1", "yt_white1_opacity95", "yt_white1_opacity98", + "yt_white2", "yt_white3", "yt_white4", + -> lightThemeBackgroundColor ?: continue + + else -> continue + } + } + } + + fun addColorResource( + resourceFile: String, + colorName: String, + colorValue: String, + ) { + document(resourceFile).use { document -> + + val resourcesNode = document.getElementsByTagName("resources").item(0) as Element + + resourcesNode.appendChild( + document.createElement("color").apply { + setAttribute("name", colorName) + setAttribute("category", "color") + textContent = colorValue + }, + ) + } + } + + val splashBackgroundColor = "revanced_splash_background_color" + + // Add a dynamic background color to the colors.xml file. + lightThemeBackgroundColor?.let { + addColorResource("res/values/colors.xml", splashBackgroundColor, it) + } + + darkThemeBackgroundColor?.let { + addColorResource("res/values-night/colors.xml", splashBackgroundColor, it) + } + + // Edit splash screen files and change the background color, + // if the background colors are set. + if (darkThemeBackgroundColor != null && lightThemeBackgroundColor != null) { + val splashScreenResourceFiles = + listOf( + "res/drawable/quantum_launchscreen_youtube.xml", + "res/drawable-sw600dp/quantum_launchscreen_youtube.xml", + ) + + splashScreenResourceFiles.forEach editSplashScreen@{ resourceFile -> + document(resourceFile).use { document -> + document.getElementsByTagName("layer-list").item(0).forEachChildElement { node -> + if (node.hasAttribute("android:drawable")) { + node.setAttribute("android:drawable", "@color/$splashBackgroundColor") + return@editSplashScreen + } + } + + throw PatchException("Failed to modify launch screen") + } + } + + // 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 -> + // Create a night mode specific override for the splash screen background. + val style = document.createElement("style") + style.setAttribute("name", "Theme.YouTube.Home") + style.setAttribute("parent", "@style/Base.V23.Theme.YouTube.Home") + + val windowItem = document.createElement("item") + windowItem.setAttribute("name", "android:windowBackground") + windowItem.textContent = "@color/$splashBackgroundColor" + style.appendChild(windowItem) + + val resourcesNode = document.getElementsByTagName("resources").item(0) as Element + resourcesNode.appendChild(style) + } + } + } + }, + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.theme.themePatch") + + PreferenceScreen.GENERAL_LAYOUT.addPreferences( + SwitchPreference("revanced_gradient_loading_screen"), + ) + + useGradientLoadingScreenFingerprint.method.apply { + val literalIndex = indexOfFirstLiteralInstructionOrThrow(GRADIENT_LOADING_SCREEN_AB_CONSTANT) + val isEnabledIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT) + val isEnabledRegister = getInstruction(isEnabledIndex).registerA + + addInstructions( + isEnabledIndex + 1, + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->gradientLoadingScreenEnabled()Z + move-result v$isEnabledRegister + """, + ) + } + + mapOf( + themeHelperLightColorFingerprint to lightThemeBackgroundColor, + themeHelperDarkColorFingerprint to darkThemeBackgroundColor, + ).forEach { (fingerprint, color) -> + fingerprint.method.apply { + addInstructions( + 0, + """ + const-string v0, "$color" + return-object v0 + """, + ) + } + } + + lithoColorOverrideHook(EXTENSION_CLASS_DESCRIPTOR, "getValue") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt new file mode 100644 index 000000000..39df4a630 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/AlternativeThumbnailsPatch.kt @@ -0,0 +1,99 @@ +package app.revanced.patches.youtube.layout.thumbnails + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.ListPreference +import app.revanced.patches.shared.misc.settings.preference.NonInteractivePreference +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.shared.misc.settings.preference.TextPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.imageurlhook.addImageUrlErrorCallbackHook +import app.revanced.patches.youtube.misc.imageurlhook.addImageUrlHook +import app.revanced.patches.youtube.misc.imageurlhook.addImageUrlSuccessCallbackHook +import app.revanced.patches.youtube.misc.imageurlhook.cronetImageUrlHookPatch +import app.revanced.patches.youtube.misc.navigation.navigationBarHookPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/AlternativeThumbnailsPatch;" + +@Suppress("unused") +val alternativeThumbnailsPatch = bytecodePatch( + name = "Alternative thumbnails", + description = "Adds options to replace video thumbnails using the DeArrow API or image captures from the video.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + navigationBarHookPatch, + cronetImageUrlHookPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.thumbnails.alternativeThumbnailsPatch") + + val entries = "revanced_alt_thumbnail_options_entries" + val values = "revanced_alt_thumbnail_options_entry_values" + PreferenceScreen.ALTERNATIVE_THUMBNAILS.addPreferences( + ListPreference( + "revanced_alt_thumbnail_home", + summaryKey = null, + entriesKey = entries, + entryValuesKey = values, + ), + ListPreference( + "revanced_alt_thumbnail_subscription", + summaryKey = null, + entriesKey = entries, + entryValuesKey = values, + ), + ListPreference( + "revanced_alt_thumbnail_library", + summaryKey = null, + entriesKey = entries, + entryValuesKey = values, + ), + ListPreference( + "revanced_alt_thumbnail_player", + summaryKey = null, + entriesKey = entries, + entryValuesKey = values, + ), + ListPreference( + "revanced_alt_thumbnail_search", + summaryKey = null, + entriesKey = entries, + entryValuesKey = values, + ), + NonInteractivePreference( + "revanced_alt_thumbnail_dearrow_about", + // Custom about preference with link to the DeArrow website. + tag = "app.revanced.extension.youtube.settings.preference.AlternativeThumbnailsAboutDeArrowPreference", + selectable = true, + ), + SwitchPreference("revanced_alt_thumbnail_dearrow_connection_toast"), + TextPreference("revanced_alt_thumbnail_dearrow_api_url"), + NonInteractivePreference("revanced_alt_thumbnail_stills_about"), + SwitchPreference("revanced_alt_thumbnail_stills_fast"), + ListPreference("revanced_alt_thumbnail_stills_time", summaryKey = null), + ) + + addImageUrlHook(EXTENSION_CLASS_DESCRIPTOR) + addImageUrlSuccessCallbackHook(EXTENSION_CLASS_DESCRIPTOR) + addImageUrlErrorCallbackHook(EXTENSION_CLASS_DESCRIPTOR) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/BypassImageRegionRestrictionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/BypassImageRegionRestrictionsPatch.kt new file mode 100644 index 000000000..f8400ed25 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/thumbnails/BypassImageRegionRestrictionsPatch.kt @@ -0,0 +1,51 @@ +package app.revanced.patches.youtube.layout.thumbnails + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.imageurlhook.addImageUrlHook +import app.revanced.patches.youtube.misc.imageurlhook.cronetImageUrlHookPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/BypassImageRegionRestrictionsPatch;" + +@Suppress("unused") +val bypassImageRegionRestrictionsPatch = bytecodePatch( + name = "Bypass image region restrictions", + description = "Adds an option to use a different host for user avatar and channel images " + + "and can fix missing images that are blocked in some countries.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + cronetImageUrlHookPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "layout.thumbnails.bypassImageRegionRestrictionsPatch") + + PreferenceScreen.MISC.addPreferences( + SwitchPreference("revanced_bypass_image_region_restrictions"), + ) + + // A priority hook is not needed, as the image urls of interest are not modified + // by AlternativeThumbnails or any other patch in this repo. + addImageUrlHook(EXTENSION_CLASS_DESCRIPTOR) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt new file mode 100644 index 000000000..5afa220b0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/announcements/AnnouncementsPatch.kt @@ -0,0 +1,50 @@ +package app.revanced.patches.youtube.misc.announcements + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.mainActivityOnCreateFingerprint + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/announcements/AnnouncementsPatch;" + +@Suppress("unused") +val announcementsPatch = bytecodePatch( + name = "Announcements", + description = "Adds an option to show announcements from ReVanced on app startup.", +) { + dependsOn( + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "misc.announcements.announcementsPatch") + + PreferenceScreen.MISC.addPreferences( + SwitchPreference("revanced_announcements"), + ) + + mainActivityOnCreateFingerprint.method.addInstructions( + // Insert index must be greater than the insert index used by GmsCoreSupport, + // as both patch the same method and GmsCore check should be first. + 1, + "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->showAnnouncement(Landroid/app/Activity;)V", + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/autorepeat/AutoRepeatPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/autorepeat/AutoRepeatPatch.kt new file mode 100644 index 000000000..e2c24ed5c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/autorepeat/AutoRepeatPatch.kt @@ -0,0 +1,65 @@ +package app.revanced.patches.youtube.misc.autorepeat + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructionsWithLabels +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.extensions.InstructionExtensions.removeInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.shared.autoRepeatFingerprint +import app.revanced.patches.youtube.shared.autoRepeatParentFingerprint +import org.stringtemplate.v4.compiler.Bytecode.instructions + +// TODO: Rename this patch to AlwaysRepeatPatch (as well as strings and references in the extension). +@Suppress("unused") +val autoRepeatPatch = bytecodePatch( + name = "Always repeat", + description = "Adds an option to always repeat videos when they end.", +) { + dependsOn( + sharedExtensionPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "misc.autorepeat.autoRepeatPatch") + + PreferenceScreen.MISC.addPreferences( + SwitchPreference("revanced_auto_repeat"), + ) + + autoRepeatFingerprint.match(autoRepeatParentFingerprint.originalClassDef).method.apply { + val playMethod = autoRepeatParentFingerprint.method + val index = instructions.lastIndex + + // Remove return-void. + removeInstruction(index) + // Add own instructions there. + addInstructionsWithLabels( + index, + """ + invoke-static {}, Lapp/revanced/extension/youtube/patches/AutoRepeatPatch;->shouldAutoRepeat()Z + move-result v0 + if-eqz v0, :noautorepeat + invoke-virtual { p0 }, $playMethod + :noautorepeat + return-void + """, + ) + } + } +} 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..36f7cfddd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/BackgroundPlaybackPatch.kt @@ -0,0 +1,104 @@ +package app.revanced.patches.youtube.misc.backgroundplayback + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.instructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.video.information.videoInformationPatch +import app.revanced.util.* +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 + +internal var prefBackgroundAndOfflineCategoryId = -1L + private set + +private val backgroundPlaybackResourcePatch = resourcePatch { + dependsOn(resourceMappingPatch, addResourcesPatch) + + execute { + prefBackgroundAndOfflineCategoryId = resourceMappings["string", "pref_background_and_offline_category"] + } +} + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/BackgroundPlaybackPatch;" + +val backgroundPlaybackPatch = bytecodePatch( + name = "Remove background playback restrictions", + description = "Removes restrictions on background playback, including playing kids videos in the background.", +) { + dependsOn( + backgroundPlaybackResourcePatch, + sharedExtensionPatch, + playerTypeHookPatch, + videoInformationPatch, + settingsPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "misc.backgroundplayback.backgroundPlaybackPatch") + + PreferenceScreen.SHORTS.addPreferences( + SwitchPreference("revanced_shorts_disable_background_playback"), + ) + + arrayOf( + backgroundPlaybackManagerFingerprint to "isBackgroundPlaybackAllowed", + backgroundPlaybackManagerShortsFingerprint to "isBackgroundShortsPlaybackAllowed", + ).forEach { (fingerprint, integrationsMethod) -> + fingerprint.method.apply { + findInstructionIndicesReversedOrThrow(Opcode.RETURN).forEach { index -> + val register = getInstruction(index).registerA + + addInstructionsAtControlFlowLabel( + index, + """ + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->$integrationsMethod(Z)Z + move-result v$register + """, + ) + } + } + } + + // Enable background playback option in YouTube settings + backgroundPlaybackSettingsFingerprint.originalMethod.apply { + val booleanCalls = instructions.withIndex().filter { + it.value.getReference()?.returnType == "Z" + } + + val settingsBooleanIndex = booleanCalls.elementAt(1).index + val settingsBooleanMethod by navigate(this).to(settingsBooleanIndex) + + settingsBooleanMethod.returnEarly(true) + } + + // Force allowing background play for Shorts. + shortsBackgroundPlaybackFeatureFlagFingerprint.method.returnEarly(true) + + // Force allowing background play for videos labeled for kids. + kidsBackgroundPlaybackPolicyControllerFingerprint.method.returnEarly() + } +} 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..b12c8157c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/backgroundplayback/Fingerprints.kt @@ -0,0 +1,86 @@ +package app.revanced.patches.youtube.misc.backgroundplayback + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val backgroundPlaybackManagerFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Z") + parameters("L") + opcodes( + Opcode.CONST_4, + Opcode.IF_EQZ, + Opcode.IGET, + Opcode.AND_INT_LIT16, + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, + Opcode.IF_NEZ, + Opcode.SGET_OBJECT, + Opcode.IGET, + Opcode.CONST, + Opcode.IF_NE, + Opcode.IGET_OBJECT, + Opcode.IF_NEZ, + Opcode.SGET_OBJECT, + Opcode.IGET, + Opcode.IF_NE, + Opcode.IGET_OBJECT, + Opcode.CHECK_CAST, + Opcode.GOTO, + Opcode.SGET_OBJECT, + Opcode.GOTO, + Opcode.CONST_4, + Opcode.IF_EQZ, + Opcode.IGET_BOOLEAN, + Opcode.IF_EQZ, + ) +} + +internal val backgroundPlaybackSettingsFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Ljava/lang/String;") + parameters() + opcodes( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ, + Opcode.IF_NEZ, + Opcode.GOTO, + ) + literal { prefBackgroundAndOfflineCategoryId } +} + +internal val kidsBackgroundPlaybackPolicyControllerFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("I", "L", "L") + opcodes( + Opcode.CONST_4, + Opcode.IF_NE, + Opcode.SGET_OBJECT, + Opcode.IF_NE, + Opcode.IGET, + Opcode.CONST_4, + Opcode.IF_NE, + Opcode.IGET_OBJECT, + ) + literal { 5 } +} + +internal val backgroundPlaybackManagerShortsFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Z") + parameters("L") + literal { 151635310 } +} + +internal val shortsBackgroundPlaybackFeatureFlagFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters() + literal { 45415425 } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/check/CheckEnvironmentPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/check/CheckEnvironmentPatch.kt new file mode 100644 index 000000000..6b389625c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/check/CheckEnvironmentPatch.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.youtube.misc.check + +import app.revanced.patches.shared.misc.checks.checkEnvironmentPatch +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.shared.mainActivityOnCreateFingerprint + +val checkEnvironmentPatch = checkEnvironmentPatch( + mainActivityOnCreateFingerprint = mainActivityOnCreateFingerprint, + extensionPatch = sharedExtensionPatch, + "com.google.android.youtube", +) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt new file mode 100644 index 000000000..9696418eb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/EnableDebuggingPatch.kt @@ -0,0 +1,111 @@ +package app.revanced.patches.youtube.misc.debugging + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.Opcode + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/EnableDebuggingPatch;" + +@Suppress("unused") +val enableDebuggingPatch = bytecodePatch( + name = "Enable debugging", + description = "Adds options for debugging.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "misc.debugging.enableDebuggingPatch") + + PreferenceScreen.MISC.addPreferences( + PreferenceScreenPreference( + key = "revanced_debug_screen", + sorting = Sorting.UNSORTED, + preferences = setOf( + SwitchPreference("revanced_debug"), + SwitchPreference("revanced_debug_protobuffer"), + SwitchPreference("revanced_debug_stacktrace"), + SwitchPreference("revanced_debug_toast_on_error"), + ), + ), + ) + + // Hook the methods that look up if a feature flag is active. + experimentalBooleanFeatureFlagFingerprint.match( + experimentalFeatureFlagParentFingerprint.originalClassDef + ).method.apply { + val insertIndex = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT) + + // It appears that all usage of this method has a default of 'false', + // so there's no need to pass in the default. + addInstructions( + insertIndex, + """ + move-result v0 + invoke-static { v0, p1, p2 }, $EXTENSION_CLASS_DESCRIPTOR->isBooleanFeatureFlagEnabled(ZJ)Z + move-result v0 + return v0 + """ + ) + } + + experimentalDoubleFeatureFlagFingerprint.match( + experimentalFeatureFlagParentFingerprint.originalClassDef + ).method.apply { + val insertIndex = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT_WIDE) + + addInstructions( + insertIndex, + """ + move-result-wide v0 # Also clobbers v1 (p0) since result is wide. + invoke-static/range { v0 .. v5 }, $EXTENSION_CLASS_DESCRIPTOR->isDoubleFeatureFlagEnabled(DJD)D + move-result-wide v0 + return-wide v0 + """ + ) + } + + experimentalLongFeatureFlagFingerprint.match( + experimentalFeatureFlagParentFingerprint.originalClassDef + ).method.apply { + val insertIndex = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT_WIDE) + + addInstructions( + insertIndex, + """ + move-result-wide v0 + invoke-static/range { v0 .. v5 }, $EXTENSION_CLASS_DESCRIPTOR->isLongFeatureFlagEnabled(JJJ)J + move-result-wide v0 + return-wide v0 + """ + ) + } + + // There exists other experimental accessor methods for String, byte[], and wrappers for obfuscated classes, + // but currently none of those are hooked. + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/Fingerprints.kt new file mode 100644 index 000000000..549994eb5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/debugging/Fingerprints.kt @@ -0,0 +1,30 @@ +package app.revanced.patches.youtube.misc.debugging + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val experimentalFeatureFlagParentFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("L") + parameters("L", "J", "[B") + strings("Unable to parse proto typed experiment flag: ") +} + +internal val experimentalBooleanFeatureFlagFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters("J", "Z") +} + +internal val experimentalDoubleFeatureFlagFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("D") + parameters("J", "D") +} + +internal val experimentalLongFeatureFlagFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("J") + parameters("J", "J") +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dimensions/spoof/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dimensions/spoof/Fingerprints.kt new file mode 100644 index 000000000..4f99a4cf5 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dimensions/spoof/Fingerprints.kt @@ -0,0 +1,8 @@ +package app.revanced.patches.youtube.misc.dimensions.spoof + +import app.revanced.patcher.fingerprint + +internal val deviceDimensionsModelToStringFingerprint = fingerprint { + returns("L") + strings("minh.", ";maxh.") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch.kt new file mode 100644 index 000000000..66718480c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dimensions/spoof/SpoofDeviceDimensionsPatch.kt @@ -0,0 +1,62 @@ +package app.revanced.patches.youtube.misc.dimensions.spoof + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/spoof/SpoofDeviceDimensionsPatch;" + +@Suppress("unused") +val spoofDeviceDimensionsPatch = bytecodePatch( + name = "Spoof device dimensions", + description = "Adds an option to spoof the device dimensions which can unlock higher video qualities.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "misc.dimensions.spoof.spoofDeviceDimensionsPatch") + + PreferenceScreen.MISC.addPreferences( + SwitchPreference("revanced_spoof_device_dimensions"), + ) + + deviceDimensionsModelToStringFingerprint + .classDef.methods.first { method -> method.name == "" } + // Override the parameters containing the dimensions. + .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_CLASS_DESCRIPTOR->get$method(I)I + move-result p$parameter + """ + }.joinToString("\n") { it }, + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt new file mode 100644 index 000000000..e84893ebb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/dns/CheckWatchHistoryDomainNameResolutionPatch.kt @@ -0,0 +1,43 @@ +package app.revanced.patches.youtube.misc.dns + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.youtube.shared.mainActivityOnCreateFingerprint + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/CheckWatchHistoryDomainNameResolutionPatch;" + +@Suppress("unused") +val checkWatchHistoryDomainNameResolutionPatch = bytecodePatch( + name = "Check watch history domain name resolution", + description = "Checks if the device DNS server is preventing user watch history from being saved.", +) { + dependsOn(addResourcesPatch) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "misc.dns.checkWatchHistoryDomainNameResolutionPatch") + + mainActivityOnCreateFingerprint.method.addInstructions( + // FIXME: Insert index must be greater than the insert index used by GmsCoreSupport, + // as both patch the same method and GmsCoreSupport check should be first, + // but the patch does not depend on GmsCoreSupport, so it should not be possible to enforce this + // unless a third patch is added that this patch and GmsCoreSupport depend on to manage + // the order of the patches. + 1, + "invoke-static/range { p0 .. p0 }, $EXTENSION_CLASS_DESCRIPTOR->checkDnsResolver(Landroid/app/Activity;)V", + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/extension/SharedExtensionPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/extension/SharedExtensionPatch.kt new file mode 100644 index 000000000..369e3ea74 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/extension/SharedExtensionPatch.kt @@ -0,0 +1,9 @@ +package app.revanced.patches.youtube.misc.extension + +import app.revanced.patches.shared.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.extension.hooks.* + +// 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/misc/extension/hooks/ApplicationInitHook.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/extension/hooks/ApplicationInitHook.kt new file mode 100644 index 000000000..6a0e7d1f4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/extension/hooks/ApplicationInitHook.kt @@ -0,0 +1,11 @@ +package app.revanced.patches.youtube.misc.extension.hooks + +import app.revanced.patches.shared.misc.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/misc/fix/backtoexitgesture/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/backtoexitgesture/Fingerprints.kt new file mode 100644 index 000000000..fed0199aa --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/backtoexitgesture/Fingerprints.kt @@ -0,0 +1,73 @@ +package app.revanced.patches.youtube.misc.fix.backtoexitgesture + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val onBackPressedFingerprint = fingerprint { + returns("V") + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + opcodes(Opcode.RETURN_VOID) + custom { method, classDef -> + method.name == "onBackPressed" && + // Old versions of YouTube called this class "WatchWhileActivity" instead. + (classDef.endsWith("MainActivity;") || classDef.endsWith("WatchWhileActivity;")) + } +} + +internal val recyclerViewScrollingFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returns("V") + parameters() + opcodes( + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, + Opcode.CHECK_CAST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.IF_LEZ, + Opcode.IGET_OBJECT, + Opcode.CONST_4, + ) +} + +internal val recyclerViewTopScrollingFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters() + opcodes( + Opcode.IGET_OBJECT, + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL, + Opcode.GOTO, + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + ) +} + +internal val recyclerViewTopScrollingParentFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + parameters("L", "L", "Landroid/view/ViewGroup;", "Landroid/view/ViewGroup;") + opcodes( + Opcode.INVOKE_DIRECT, + Opcode.IPUT_OBJECT, + Opcode.IPUT_OBJECT, + Opcode.IPUT_OBJECT, + Opcode.IPUT_OBJECT, + Opcode.CONST_16, + Opcode.INVOKE_VIRTUAL, + Opcode.NEW_INSTANCE, + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/backtoexitgesture/FixBackToExitGesturePatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/backtoexitgesture/FixBackToExitGesturePatch.kt new file mode 100644 index 000000000..0778e814f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/backtoexitgesture/FixBackToExitGesturePatch.kt @@ -0,0 +1,54 @@ +package app.revanced.patches.youtube.misc.fix.backtoexitgesture + +import app.revanced.patcher.Fingerprint +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch + +internal val fixBackToExitGesturePatch = bytecodePatch( + description = "Fixes the swipe back to exit gesture.", +) { + + execute { + /** + * Inject a call to a method from the extension. + * + * @param targetMethod The target method to call. + */ + fun Fingerprint.injectCall(targetMethod: ExtensionMethod) = method.addInstruction( + patternMatch!!.endIndex, + targetMethod.toString(), + ) + + mapOf( + recyclerViewTopScrollingFingerprint.also { + it.match(recyclerViewTopScrollingParentFingerprint.originalClassDef) + } to ExtensionMethod( + methodName = "onTopView", + ), + recyclerViewScrollingFingerprint to ExtensionMethod( + methodName = "onScrollingViews", + ), + onBackPressedFingerprint to ExtensionMethod( + "p0", + "onBackPressed", + "Landroid/app/Activity;", + ), + ).forEach { (fingerprint, target) -> fingerprint.injectCall(target) } + } +} + +/** + * A reference to a method from the extension for [fixBackToExitGesturePatch]. + * + * @param register The method registers. + * @param methodName The method name. + * @param parameterTypes The parameters of the method. + */ +private class ExtensionMethod( + val register: String = "", + val methodName: String, + val parameterTypes: String = "", +) { + override fun toString() = + "invoke-static {$register}, Lapp/revanced/extension/youtube/patches/FixBackToExitGesturePatch;->$methodName($parameterTypes)V" +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/fix/cairo/DisableCairoSettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/cairo/DisableCairoSettingsPatch.kt similarity index 53% rename from src/main/kotlin/app/revanced/patches/youtube/misc/fix/cairo/DisableCairoSettingsPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/cairo/DisableCairoSettingsPatch.kt index b936d7bf9..d6fafe539 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/misc/fix/cairo/DisableCairoSettingsPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/cairo/DisableCairoSettingsPatch.kt @@ -1,31 +1,24 @@ package app.revanced.patches.youtube.misc.fix.cairo -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.patcher.patch.annotation.Patch -import app.revanced.patches.youtube.misc.backgroundplayback.BackgroundPlaybackPatch -import app.revanced.patches.youtube.misc.fix.cairo.fingerprints.CarioFragmentConfigFingerprint -import app.revanced.patches.youtube.misc.playservice.VersionCheckPatch +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.misc.backgroundplayback.backgroundPlaybackPatch +import app.revanced.patches.youtube.misc.playservice.is_19_04_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch import app.revanced.util.indexOfFirstInstructionOrThrow -import app.revanced.util.indexOfFirstWideLiteralInstructionValueOrThrow -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.OneRegisterInstruction -@Patch( +internal val disableCairoSettingsPatch = bytecodePatch( description = "Disables Cairo Fragment from being used.", - dependencies = [ - VersionCheckPatch::class - ] -) -internal object DisableCairoSettingsPatch : BytecodePatch( - setOf(CarioFragmentConfigFingerprint) ) { - override fun execute(context: BytecodeContext) { - if (!VersionCheckPatch.is_19_04_or_greater) { - return + dependsOn(versionCheckPatch) + + execute { + if (!is_19_04_or_greater) { + return@execute } /** @@ -33,24 +26,25 @@ internal object DisableCairoSettingsPatch : BytecodePatch( * 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. + * 1. [backgroundPlaybackPatch] does not activate the Minimized playback setting of Cairo Fragment. * 2. Some patches do not yet support Cairo Fragments (ie: custom Seekbar color). * 3. Settings preferences added by ReVanced are missing. * * Screenshots of the Cairo Fragment: * uYouPlus#1468. */ - CarioFragmentConfigFingerprint.resultOrThrow().mutableMethod.apply { - val literalIndex = indexOfFirstWideLiteralInstructionValueOrThrow( - CarioFragmentConfigFingerprint.CAIRO_CONFIG_LITERAL_VALUE + cairoFragmentConfigFingerprint.method.apply { + val literalIndex = indexOfFirstLiteralInstructionOrThrow( + CAIRO_CONFIG_LITERAL_VALUE, ) + val resultIndex = indexOfFirstInstructionOrThrow(literalIndex, Opcode.MOVE_RESULT) val register = getInstruction(resultIndex).registerA addInstruction( resultIndex + 1, - "const/16 v$register, 0x0" + "const/16 v$register, 0x0", ) } } -} \ No newline at end of file +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/cairo/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/cairo/Fingerprints.kt new file mode 100644 index 000000000..5272adc07 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/cairo/Fingerprints.kt @@ -0,0 +1,19 @@ +package app.revanced.patches.youtube.misc.fix.cairo + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags + +/** + * Added in YouTube v19.04.38. + * + * When this value is true, Cairo Fragment is used. + * In this case, some of the patches may be broken, so set this value to FALSE. + */ +internal const val CAIRO_CONFIG_LITERAL_VALUE = 45532100L + +internal val cairoFragmentConfigFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + literal { CAIRO_CONFIG_LITERAL_VALUE } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/playback/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/playback/Fingerprints.kt new file mode 100644 index 000000000..1862a79aa --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/playback/Fingerprints.kt @@ -0,0 +1,113 @@ +package app.revanced.patches.youtube.misc.fix.playback + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val buildInitPlaybackRequestFingerprint = fingerprint { + returns("Lorg/chromium/net/UrlRequest\$Builder;") + opcodes( + Opcode.MOVE_RESULT_OBJECT, + Opcode.IGET_OBJECT, // Moves the request URI string to a register to build the request with. + ) + strings( + "Content-Type", + "Range", + ) +} + +internal val buildPlayerRequestURIFingerprint = fingerprint { + returns("Ljava/lang/String;") + opcodes( + Opcode.INVOKE_VIRTUAL, // Register holds player request URI. + Opcode.MOVE_RESULT_OBJECT, + Opcode.IPUT_OBJECT, + Opcode.IGET_OBJECT, + Opcode.MONITOR_EXIT, + Opcode.RETURN_OBJECT, + ) + strings( + "youtubei/v1", + "key", + "asig", + ) +} + +internal val buildRequestFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Lorg/chromium/net/UrlRequest;") + custom { methodDef, _ -> + // Different targets have slightly different parameters + + // Earlier targets have parameters: + // L + // Ljava/util/Map; + // [B + // L + // L + // L + // Lorg/chromium/net/UrlRequest$Callback; + + // Later targets have parameters: + // L + // Ljava/util/Map; + // [B + // L + // L + // L + // Lorg/chromium/net/UrlRequest\$Callback; + // L + + val parameterTypes = methodDef.parameterTypes + (parameterTypes.size == 7 || parameterTypes.size == 8) && + parameterTypes[1] == "Ljava/util/Map;" // URL headers. + } +} + +internal val protobufClassParseByteBufferFingerprint = fingerprint { + accessFlags(AccessFlags.PROTECTED, AccessFlags.STATIC) + returns("L") + parameters("L", "Ljava/nio/ByteBuffer;") + opcodes( + Opcode.SGET_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.RETURN_OBJECT, + ) + custom { method, _ -> method.name == "parseFrom" } +} + +internal val createStreamingDataFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + parameters("L") + opcodes( + Opcode.IPUT_OBJECT, + Opcode.IGET_OBJECT, + Opcode.IF_NEZ, + Opcode.SGET_OBJECT, + Opcode.IPUT_OBJECT, + ) + custom { method, classDef -> + classDef.fields.any { field -> + field.name == "a" && field.type.endsWith("/StreamingDataOuterClass\$StreamingData;") + } + } +} + +internal val buildMediaDataSourceFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + parameters( + "Landroid/net/Uri;", + "J", + "I", + "[B", + "Ljava/util/Map;", + "J", + "J", + "Ljava/lang/String;", + "I", + "Ljava/lang/Object;", + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/playback/SpoofVideoStreamsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/playback/SpoofVideoStreamsPatch.kt new file mode 100644 index 000000000..25b35c447 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/playback/SpoofVideoStreamsPatch.kt @@ -0,0 +1,241 @@ +package app.revanced.patches.youtube.misc.fix.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.extensions.InstructionExtensions.instructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod.Companion.toMutable +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.ListPreference +import app.revanced.patches.shared.misc.settings.preference.NonInteractivePreference +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/spoof/SpoofVideoStreamsPatch;" + +val spoofVideoStreamsPatch = bytecodePatch( + name = "Spoof video streams", + description = "Spoofs the client video streams to allow video playback.", +) { + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + dependsOn( + settingsPatch, + addResourcesPatch, + userAgentClientSpoofPatch, + ) + + execute { + addResources("youtube", "misc.fix.playback.spoofVideoStreamsPatch") + + PreferenceScreen.MISC.addPreferences( + PreferenceScreenPreference( + key = "revanced_spoof_video_streams_screen", + sorting = PreferenceScreenPreference.Sorting.UNSORTED, + preferences = setOf( + SwitchPreference("revanced_spoof_video_streams"), + ListPreference( + "revanced_spoof_video_streams_client", + summaryKey = null, + ), + SwitchPreference( + "revanced_spoof_video_streams_ios_force_avc", + tag = "app.revanced.extension.youtube.settings.preference.ForceAVCSpoofingPreference", + ), + NonInteractivePreference("revanced_spoof_video_streams_about_android_vr"), + NonInteractivePreference("revanced_spoof_video_streams_about_ios"), + ), + ), + ) + + // region Block /initplayback requests to fall back to /get_watch requests. + + val moveUriStringIndex = buildInitPlaybackRequestFingerprint.patternMatch!!.startIndex + + buildInitPlaybackRequestFingerprint.method.apply { + val targetRegister = getInstruction(moveUriStringIndex).registerA + + addInstructions( + moveUriStringIndex + 1, + """ + invoke-static { v$targetRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockInitPlaybackRequest(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$targetRegister + """, + ) + } + + // endregion + + // region Block /get_watch requests to fall back to /player requests. + + val invokeToStringIndex = buildPlayerRequestURIFingerprint.patternMatch!!.startIndex + + buildPlayerRequestURIFingerprint.method.apply { + val uriRegister = getInstruction(invokeToStringIndex).registerC + + addInstructions( + invokeToStringIndex, + """ + invoke-static { v$uriRegister }, $EXTENSION_CLASS_DESCRIPTOR->blockGetWatchRequest(Landroid/net/Uri;)Landroid/net/Uri; + move-result-object v$uriRegister + """, + ) + } + + // endregion + + // region Get replacement streams at player requests. + + buildRequestFingerprint.method.apply { + val newRequestBuilderIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_VIRTUAL && + getReference()?.name == "newUrlRequestBuilder" + } + val urlRegister = getInstruction(newRequestBuilderIndex).registerD + val freeRegister = getInstruction(newRequestBuilderIndex + 1).registerA + + addInstructions( + newRequestBuilderIndex, + """ + move-object v$freeRegister, p1 + invoke-static { v$urlRegister, v$freeRegister }, $EXTENSION_CLASS_DESCRIPTOR->fetchStreams(Ljava/lang/String;Ljava/util/Map;)V + """, + ) + } + + // endregion + + // region Replace the streaming data with the replacement streams. + + createStreamingDataFingerprint.method.apply { + val setStreamDataMethodName = "patch_setStreamingData" + val resultMethodType = createStreamingDataFingerprint.classDef.type + val videoDetailsIndex = createStreamingDataFingerprint.patternMatch!!.endIndex + val videoDetailsRegister = getInstruction(videoDetailsIndex).registerA + val videoDetailsClass = getInstruction(videoDetailsIndex).getReference()!!.type + + addInstruction( + videoDetailsIndex + 1, + "invoke-direct { p0, v$videoDetailsRegister }, " + + "$resultMethodType->$setStreamDataMethodName($videoDetailsClass)V", + ) + + val protobufClass = protobufClassParseByteBufferFingerprint.method.definingClass + val setStreamingDataIndex = createStreamingDataFingerprint.patternMatch!!.startIndex + + val playerProtoClass = getInstruction(setStreamingDataIndex + 1) + .getReference()!!.definingClass + + val setStreamingDataField = getInstruction(setStreamingDataIndex).getReference() + + val getStreamingDataField = getInstruction( + indexOfFirstInstructionOrThrow { + opcode == Opcode.IGET_OBJECT && getReference()?.definingClass == playerProtoClass + }, + ).getReference() + + // Use a helper method to avoid the need of picking out multiple free registers from the hooked code. + createStreamingDataFingerprint.classDef.methods.add( + ImmutableMethod( + resultMethodType, + setStreamDataMethodName, + listOf(ImmutableMethodParameter(videoDetailsClass, null, "videoDetails")), + "V", + AccessFlags.PRIVATE.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(9), + ).toMutable().apply { + addInstructionsWithLabels( + 0, + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->isSpoofingEnabled()Z + move-result v0 + if-eqz v0, :disabled + + # Get video id. + iget-object v2, p1, $videoDetailsClass->c:Ljava/lang/String; + if-eqz v2, :disabled + + # Get streaming data. + invoke-static { v2 }, $EXTENSION_CLASS_DESCRIPTOR->getStreamingData(Ljava/lang/String;)Ljava/nio/ByteBuffer; + move-result-object v3 + if-eqz v3, :disabled + + # Parse streaming data. + sget-object v4, $playerProtoClass->a:$playerProtoClass + invoke-static { v4, v3 }, $protobufClass->parseFrom(${protobufClass}Ljava/nio/ByteBuffer;)$protobufClass + move-result-object v5 + check-cast v5, $playerProtoClass + + # Set streaming data. + iget-object v6, v5, $getStreamingDataField + if-eqz v6, :disabled + iput-object v6, p0, $setStreamingDataField + + :disabled + return-void + """, + ) + }, + ) + } + + // endregion + + // region Remove /videoplayback request body to fix playback. + // It is assumed, YouTube makes a request with a body tuned for Android. + // Requesting streams intended for other platforms with a body tuned for Android could be the cause of 400 errors. + // A proper fix may include modifying the request body to match the platforms expected body. + + buildMediaDataSourceFingerprint.method.apply { + val targetIndex = instructions.lastIndex + + // Instructions are added just before the method returns, + // so there's no concern of clobbering in-use registers. + addInstructions( + targetIndex, + """ + # Field a: Stream uri. + # Field c: Http method. + # Field d: Post data. + move-object v0, p0 # method has over 15 registers and must copy p0 to a lower register. + 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 + } +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/fix/playback/UserAgentClientSpoofPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/playback/UserAgentClientSpoofPatch.kt similarity index 54% rename from src/main/kotlin/app/revanced/patches/youtube/misc/fix/playback/UserAgentClientSpoofPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/playback/UserAgentClientSpoofPatch.kt index 2437dfda6..8bbd0ec77 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/misc/fix/playback/UserAgentClientSpoofPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/fix/playback/UserAgentClientSpoofPatch.kt @@ -2,39 +2,30 @@ package app.revanced.patches.youtube.misc.fix.playback 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.all.misc.transformation.BaseTransformInstructionsPatch import app.revanced.patches.all.misc.transformation.IMethodCall -import app.revanced.patches.all.misc.transformation.Instruction35cInfo import app.revanced.patches.all.misc.transformation.filterMapInstruction35c +import app.revanced.patches.all.misc.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 -object UserAgentClientSpoofPatch : BaseTransformInstructionsPatch() { - private const val ORIGINAL_PACKAGE_NAME = "com.google.android.youtube" - private const val USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE = - "Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;" +private const val ORIGINAL_PACKAGE_NAME = "com.google.android.youtube" +private const val USER_AGENT_STRING_BUILDER_APPEND_METHOD_REFERENCE = + "Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;" - override fun filterMap( - classDef: ClassDef, - method: Method, - instruction: Instruction, - instructionIndex: Int, - ) = filterMapInstruction35c( - "Lapp/revanced/integrations", - classDef, - instruction, - instructionIndex, - ) - - override fun transform(mutableMethod: MutableMethod, entry: Instruction35cInfo) { +val userAgentClientSpoofPatch = 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. @@ -42,16 +33,16 @@ object UserAgentClientSpoofPatch : BaseTransformInstructionsPatch()?.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. @@ -60,10 +51,10 @@ object UserAgentClientSpoofPatch : BaseTransformInstructionsPatch() opcode == Opcode.CONST_STRING && - (reference?.string == "android.resource://" || reference?.string == "gcore_") + (reference?.string == "android.resource://" || reference?.string == "gcore_") } if (resourceOrGmsStringInstructionIndex >= 0) { - return + return@transform } // Overwrite the result of context.getPackageName() with the original package name. @@ -72,20 +63,20 @@ object UserAgentClientSpoofPatch : BaseTransformInstructionsPatch, - override val returnType: String, - ) : IMethodCall { - GetPackageName( - "Landroid/content/Context;", - "getPackageName", - emptyArray(), - "Ljava/lang/String;", - ), - } +@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;", + ), } diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/gms/Constants.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/Constants.kt similarity index 100% rename from src/main/kotlin/app/revanced/patches/youtube/misc/gms/Constants.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/Constants.kt diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt new file mode 100644 index 000000000..3e808b1d2 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/gms/GmsCoreSupportPatch.kt @@ -0,0 +1,71 @@ +package app.revanced.patches.youtube.misc.gms + +import app.revanced.patcher.patch.Option +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.castContextFetchFingerprint +import app.revanced.patches.shared.misc.gms.gmsCoreSupportPatch +import app.revanced.patches.shared.misc.settings.preference.IntentPreference +import app.revanced.patches.shared.primeMethodFingerprint +import app.revanced.patches.youtube.layout.buttons.overlay.hidePlayerOverlayButtonsPatch +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.fix.playback.spoofVideoStreamsPatch +import app.revanced.patches.youtube.misc.gms.Constants.REVANCED_YOUTUBE_PACKAGE_NAME +import app.revanced.patches.youtube.misc.gms.Constants.YOUTUBE_PACKAGE_NAME +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.mainActivityOnCreateFingerprint + +@Suppress("unused") +val gmsCoreSupportPatch = gmsCoreSupportPatch( + fromPackageName = YOUTUBE_PACKAGE_NAME, + toPackageName = REVANCED_YOUTUBE_PACKAGE_NAME, + primeMethodFingerprint = primeMethodFingerprint, + earlyReturnFingerprints = setOf( + castContextFetchFingerprint, + ), + mainActivityOnCreateFingerprint = mainActivityOnCreateFingerprint, + extensionPatch = sharedExtensionPatch, + gmsCoreSupportResourcePatchFactory = ::gmsCoreSupportResourcePatch, +) { + dependsOn( + hidePlayerOverlayButtonsPatch, // Hide non-functional cast button. + spoofVideoStreamsPatch, + ) + + compatibleWith( + YOUTUBE_PACKAGE_NAME( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) +} + +private fun gmsCoreSupportResourcePatch( + gmsCoreVendorGroupIdOption: Option, +) = app.revanced.patches.shared.misc.gms.gmsCoreSupportResourcePatch( + fromPackageName = YOUTUBE_PACKAGE_NAME, + toPackageName = REVANCED_YOUTUBE_PACKAGE_NAME, + gmsCoreVendorGroupIdOption = gmsCoreVendorGroupIdOption, + spoofedPackageSignature = "24bb24c05e47e0aefa68a58a766179d9b613a600", + executeBlock = { + addResources("youtube", "misc.gms.gmsCoreSupportResourcePatch") + + val gmsCoreVendorGroupId by gmsCoreVendorGroupIdOption + + PreferenceScreen.MISC.addPreferences( + IntentPreference( + "microg_settings", + intent = IntentPreference.Intent("", "org.microg.gms.ui.SettingsActivity") { + "$gmsCoreVendorGroupId.android.gms" + }, + ), + ) + }, +) { + dependsOn(settingsPatch, addResourcesPatch) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHook.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHook.kt new file mode 100644 index 000000000..b9292ef84 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/imageurlhook/CronetImageUrlHook.kt @@ -0,0 +1,109 @@ +package app.revanced.patches.youtube.misc.imageurlhook + +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.youtube.misc.extension.sharedExtensionPatch +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 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 + +val cronetImageUrlHookPatch = bytecodePatch( + description = "Hooks Cronet image urls.", +) { + dependsOn(sharedExtensionPatch) + + execute { + loadImageUrlMethod = messageDigestImageUrlFingerprint + .match(messageDigestImageUrlParentFingerprint.originalClassDef).method + + loadImageSuccessCallbackMethod = onSucceededFingerprint + .match(onResponseStartedFingerprint.originalClassDef).method + + loadImageErrorCallbackMethod = onFailureFingerprint + .match(onResponseStartedFingerprint.originalClassDef).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. + val urlFieldInstruction = requestFingerprint.method.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.classDef.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. + */ +fun addImageUrlHook(targetMethodClass: String, highPriority: Boolean = false) { + 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. + */ +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. + */ +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/youtube/misc/imageurlhook/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/imageurlhook/Fingerprints.kt new file mode 100644 index 000000000..fcd5298ac --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/imageurlhook/Fingerprints.kt @@ -0,0 +1,64 @@ +package app.revanced.patches.youtube.misc.imageurlhook + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +internal val onFailureFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters( + "Lorg/chromium/net/UrlRequest;", + "Lorg/chromium/net/UrlResponseInfo;", + "Lorg/chromium/net/CronetException;" + ) + custom { method, _ -> + method.name == "onFailed" + } +} + +// Acts as a parent fingerprint. +internal val onResponseStartedFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("Lorg/chromium/net/UrlRequest;", "Lorg/chromium/net/UrlResponseInfo;") + strings( + "Content-Length", + "Content-Type", + "identity", + "application/x-protobuf", + ) + custom { method, _ -> + method.name == "onResponseStarted" + } +} + +internal val onSucceededFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("Lorg/chromium/net/UrlRequest;", "Lorg/chromium/net/UrlResponseInfo;") + custom { method, _ -> + method.name == "onSucceeded" + } +} + +internal const val CRONET_URL_REQUEST_CLASS_DESCRIPTOR = "Lorg/chromium/net/impl/CronetUrlRequest;" + +internal val requestFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + custom { _, classDef -> + classDef.type == CRONET_URL_REQUEST_CLASS_DESCRIPTOR + } +} + +internal val messageDigestImageUrlFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + parameters("Ljava/lang/String;", "L") +} + +internal val messageDigestImageUrlParentFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Ljava/lang/String;") + parameters() + strings("@#&=*+-_.,:!?()/~'%;\$") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch.kt new file mode 100644 index 000000000..d2c06292c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/BypassURLRedirectsPatch.kt @@ -0,0 +1,82 @@ +package app.revanced.patches.youtube.misc.links + +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.playservice.is_19_33_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +@Suppress("unused") +val bypassURLRedirectsPatch = bytecodePatch( + name = "Bypass URL redirects", + description = "Adds an option to bypass URL redirects and open the original URL directly.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + versionCheckPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "misc.links.bypassURLRedirectsPatch") + + PreferenceScreen.MISC.addPreferences( + SwitchPreference("revanced_bypass_url_redirects"), + ) + + val fingerprints = if (is_19_33_or_greater) { + arrayOf( + abUriParserFingerprint, + httpUriParserFingerprint, + ) + } else { + arrayOf( + abUriParserLegacyFingerprint, + httpUriParserLegacyFingerprint, + ) + } + + fingerprints.forEach { + it.method.apply { + val insertIndex = findUriParseIndex() + val uriStringRegister = getInstruction(insertIndex).registerC + + replaceInstruction( + insertIndex, + "invoke-static {v$uriStringRegister}," + + "Lapp/revanced/extension/youtube/patches/BypassURLRedirectsPatch;" + + "->" + + "parseRedirectUri(Ljava/lang/String;)Landroid/net/Uri;", + ) + } + } + } +} + +internal fun Method.findUriParseIndex() = indexOfFirstInstruction { + val reference = getReference() + reference?.returnType == "Landroid/net/Uri;" && reference.name == "parse" +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/Fingerprints.kt new file mode 100644 index 000000000..73f6fae75 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/Fingerprints.kt @@ -0,0 +1,72 @@ +package app.revanced.patches.youtube.misc.links + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +/** + * Target 19.33+ + */ +internal val abUriParserFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Ljava/lang/Object") + parameters("Ljava/lang/Object") + strings( + "Found entityKey=`", + "` that does not contain a PlaylistVideoEntityId message as it's identifier.", + ) + custom { method, _ -> + method.findUriParseIndex() >= 0 + } +} + +internal val abUriParserLegacyFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Ljava/lang/Object") + parameters("Ljava/lang/Object") + opcodes( + Opcode.RETURN_OBJECT, + Opcode.CHECK_CAST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CHECK_CAST, + Opcode.RETURN_OBJECT, + Opcode.CHECK_CAST, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.RETURN_OBJECT, + Opcode.CHECK_CAST, + ) + custom { methodDef, classDef -> + // This method is always called "a" because this kind of class always has a single (non-synthetic) method. + + if (methodDef.name != "a") return@custom false + + val count = classDef.methods.count() + count == 2 || count == 3 + } +} + +/** + * Target 19.33+ + */ +internal val httpUriParserFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Landroid/net/Uri") + parameters("Ljava/lang/String") + strings("https", "https:", "://") + custom { methodDef, _ -> + methodDef.findUriParseIndex() >= 0 + } +} + +internal val httpUriParserLegacyFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("Landroid/net/Uri") + parameters("Ljava/lang/String") + opcodes( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + ) + strings("://") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch.kt new file mode 100644 index 000000000..c2d24915f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/links/OpenLinksExternallyPatch.kt @@ -0,0 +1,61 @@ +package app.revanced.patches.youtube.misc.links + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.transformation.transformInstructionsPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +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( + name = "Open links externally", + description = "Adds an option to always open links in your browser instead of in the in-app-browser.", +) { + 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}, Lapp/revanced/extension/youtube/patches/OpenLinksExternallyPatch;->getIntent(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$register + """, + ) + }, + ), + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "misc.links.openLinksExternallyPatch") + + PreferenceScreen.MISC.addPreferences( + SwitchPreference("revanced_external_browser"), + ) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt new file mode 100644 index 000000000..ac158ee12 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/Fingerprints.kt @@ -0,0 +1,65 @@ +package app.revanced.patches.youtube.misc.litho.filter + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +/** + * In 19.17 and earlier, this resolves to the same method as [readComponentIdentifierFingerprint]. + * In 19.18+ this resolves to a different method. + */ +internal val componentContextParserFingerprint = fingerprint { + strings("Component was not found %s because it was removed due to duplicate converter bindings.") +} + +internal val lithoFilterFingerprint = fingerprint { + accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) + returns("V") + custom { _, classDef -> + classDef.endsWith("LithoFilterPatch;") + } +} + +internal val protobufBufferReferenceFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("I", "Ljava/nio/ByteBuffer;") + opcodes( + Opcode.IPUT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.SUB_INT_2ADDR, + ) +} + +/** +* In 19.17 and earlier, this resolves to the same method as [componentContextParserFingerprint]. +* In 19.18+ this resolves to a different method. +*/ +internal val readComponentIdentifierFingerprint = fingerprint { + strings("Number of bits must be positive") +} + +internal val emptyComponentFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.CONSTRUCTOR) + parameters() + strings("EmptyComponent") + custom { _, classDef -> + classDef.methods.filter { AccessFlags.STATIC.isSet(it.accessFlags) }.size == 1 + } +} + +internal val lithoComponentNameUpbFeatureFlagFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters() + literal { 45631264L } +} + +internal val lithoConverterBufferUpbFeatureFlagFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("L") + parameters("L") + literal { 45419603L } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt new file mode 100644 index 000000000..16a834083 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/litho/filter/LithoFilterPatch.kt @@ -0,0 +1,264 @@ +@file:Suppress("SpellCheckingInspection") + +package app.revanced.patches.youtube.misc.litho.filter + +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.removeInstructions +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.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.playservice.is_19_18_or_greater +import app.revanced.patches.youtube.misc.playservice.is_19_25_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import app.revanced.util.indexOfFirstInstructionReversedOrThrow +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.* +import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +lateinit var addLithoFilter: (String) -> Unit + private set + +internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/components/LithoFilterPatch;" + +val lithoFilterPatch = bytecodePatch( + description = "Hooks the method which parses the bytes into a ComponentContext to filter components.", +) { + dependsOn( + sharedExtensionPatch, + versionCheckPatch, + ) + + var filterCount = 0 + + /** + * The following patch inserts a hook into the method that parses the bytes into a ComponentContext. + * This method contains a StringBuilder object that represents the pathBuilder of the component. + * The pathBuilder is used to filter components by their path. + * + * Additionally, the method contains a reference to the component's identifier. + * The identifier is used to filter components by their identifier. + * + * The protobuf buffer is passed along from a different injection point before the filtering occurs. + * The buffer is a large byte array that represents the component tree. + * This byte array is searched for strings that indicate the current component. + * + * The following pseudocode shows how the patch works: + * + * class SomeOtherClass { + * // Called before ComponentContextParser.parseBytesToComponentContext method. + * public void someOtherMethod(ByteBuffer byteBuffer) { + * ExtensionClass.setProtoBuffer(byteBuffer); // Inserted by this patch. + * ... + * } + * } + * + * When patching 19.17 and earlier: + * + * class ComponentContextParser { + * public ComponentContext ReadComponentIdentifierFingerprint(...) { + * ... + * if (extensionClass.filter(identifier, pathBuilder)); // Inserted by this patch. + * return emptyComponent; + * ... + * } + * } + * + * When patching 19.18 and later: + * + * class ComponentContextParser { + * public ComponentContext parseBytesToComponentContext(...) { + * ... + * if (ReadComponentIdentifierFingerprint() == null); // Inserted by this patch. + * return emptyComponent; + * ... + * } + * + * public ComponentIdentifierObj readComponentIdentifier(...) { + * ... + * if (extensionClass.filter(identifier, pathBuilder)); // Inserted by this patch. + * return null; + * ... + * } + * } + */ + execute { + // Remove dummy filter from extenion static field + // and add the filters included during patching. + lithoFilterFingerprint.method.apply { + removeInstructions(2, 4) // Remove dummy filter. + + addLithoFilter = { classDescriptor -> + addInstructions( + 2, + """ + new-instance v1, $classDescriptor + invoke-direct {v1}, $classDescriptor->()V + const/16 v2, ${filterCount++} + aput-object v1, v0, v2 + """, + ) + } + } + + // region Pass the buffer into extension. + + protobufBufferReferenceFingerprint.method.addInstruction( + 0, + " invoke-static { p2 }, $EXTENSION_CLASS_DESCRIPTOR->setProtoBuffer(Ljava/nio/ByteBuffer;)V", + ) + + // endregion + + // region Hook the method that parses bytes into a ComponentContext. + + val readComponentMethod = readComponentIdentifierFingerprint.originalMethod + // Get the only static method in the class. + val builderMethodDescriptor = emptyComponentFingerprint.classDef.methods.first { method -> + AccessFlags.STATIC.isSet(method.accessFlags) + } + // Only one field. + val emptyComponentField = classBy { classDef -> + builderMethodDescriptor.returnType == classDef.type + }!!.immutableClass.fields.single() + + // Returns an empty component instead of the original component. + fun createReturnEmptyComponentInstructions(register: Int): String = + """ + move-object/from16 v$register, p1 + invoke-static { v$register }, $builderMethodDescriptor + move-result-object v$register + iget-object v$register, v$register, $emptyComponentField + return-object v$register + """ + + componentContextParserFingerprint.method.apply { + // 19.18 and later require patching 2 methods instead of one. + // Otherwise the modifications done here are the same for all targets. + if (is_19_18_or_greater) { + // Get the method name of the ReadComponentIdentifierFingerprint call. + val readComponentMethodCallIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + reference?.definingClass == readComponentMethod.definingClass && + reference.name == readComponentMethod.name + } + + // Result of read component, and also a free register. + val register = getInstruction(readComponentMethodCallIndex + 1).registerA + + // Insert after 'move-result-object' + val insertHookIndex = readComponentMethodCallIndex + 2 + + // Return an EmptyComponent instead of the original component if the filterState method returns true. + addInstructionsWithLabels( + insertHookIndex, + """ + if-nez v$register, :unfiltered + + # Component was filtered in ReadComponentIdentifierFingerprint hook + ${createReturnEmptyComponentInstructions(register)} + """, + ExternalLabel("unfiltered", getInstruction(insertHookIndex)), + ) + } + } + + // endregion + + // region Read component then store the result. + + readComponentIdentifierFingerprint.method.apply { + val insertHookIndex = indexOfFirstInstructionOrThrow { + opcode == Opcode.IPUT_OBJECT && + getReference()?.type == "Ljava/lang/StringBuilder;" + } + val stringBuilderRegister = getInstruction(insertHookIndex).registerA + + // Identifier is saved to a field just before the string builder. + val identifierRegister = getInstruction( + indexOfFirstInstructionReversedOrThrow(insertHookIndex) { + opcode == Opcode.IPUT_OBJECT && + getReference()?.type == "Ljava/lang/String;" + }, + ).registerA + + // Find a free temporary register. + val freeRegister = getInstruction( + // Immediately before is a StringBuilder append constant character. + indexOfFirstInstructionReversedOrThrow(insertHookIndex, Opcode.CONST_16), + ).registerA + + // Verify the temp register will not clobber the method result register. + if (stringBuilderRegister == freeRegister) { + throw PatchException("Free register will clobber StringBuilder register") + } + + val invokeFilterInstructions = """ + invoke-static { v$identifierRegister, v$stringBuilderRegister }, $EXTENSION_CLASS_DESCRIPTOR->filter(Ljava/lang/String;Ljava/lang/StringBuilder;)Z + move-result v$freeRegister + if-eqz v$freeRegister, :unfiltered + """ + + addInstructionsWithLabels( + insertHookIndex, + if (is_19_18_or_greater) { + """ + $invokeFilterInstructions + + # Return null, and the ComponentContextParserFingerprint hook + # handles returning an empty component. + const/4 v$freeRegister, 0x0 + return-object v$freeRegister + """ + } else { + """ + $invokeFilterInstructions + + ${createReturnEmptyComponentInstructions(freeRegister)} + """ + }, + ExternalLabel("unfiltered", getInstruction(insertHookIndex)), + ) + } + + // endregion + + // region A/B test of new Litho native code. + + // Turn off native code that handles litho component names. If this feature is on then nearly + // all litho components have a null name and identifier/path filtering is completely broken. + if (is_19_25_or_greater) { + lithoComponentNameUpbFeatureFlagFingerprint.method.apply { + // Don't use return early, so the debug patch logs if this was originally on. + val insertIndex = indexOfFirstInstructionOrThrow(Opcode.RETURN) + val register = getInstruction(insertIndex).registerA + + addInstruction(insertIndex, "const/4 v$register, 0x0") + } + } + + // Turn off a feature flag that enables native code of protobuf parsing (Upb protobuf). + // If this is enabled, then the litho protobuffer hook will always show an empty buffer + // since it's no longer handled by the hooked Java code. + lithoConverterBufferUpbFeatureFlagFingerprint.method.apply { + val index = indexOfFirstInstructionOrThrow(Opcode.MOVE_RESULT) + val register = getInstruction(index).registerA + + addInstruction(index + 1, "const/4 v$register, 0x0") + } + + // endregion + } + + finalize { + lithoFilterFingerprint.method.replaceInstruction(0, "const/16 v0, $filterCount") + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/Fingerprints.kt new file mode 100644 index 000000000..0f5ade30a --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/Fingerprints.kt @@ -0,0 +1,107 @@ +package app.revanced.patches.youtube.misc.navigation + +import app.revanced.patcher.fingerprint +import app.revanced.patches.youtube.layout.buttons.navigation.navigationButtonsPatch +import app.revanced.util.getReference +import app.revanced.util.indexOfFirstInstruction +import app.revanced.util.literal +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 actionBarSearchResultsFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Landroid/view/View;") + literal { actionBarSearchResultsViewMicId } +} + +/** + * Matches to the class found in [pivotBarConstructorFingerprint]. + */ +internal val initializeButtonsFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + literal { imageOnlyTabResourceId } +} + +internal val mainActivityOnBackPressedFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters() + custom { method, classDef -> + val matchesClass = classDef.endsWith("MainActivity;") || + // Old versions of YouTube called this class "WatchWhileActivity" instead. + classDef.endsWith("WatchWhileActivity;") + + matchesClass && method.name == "onBackPressed" + } +} + +/** + * Extension method, used for callback into to other patches. + * Specifically, [navigationButtonsPatch]. + */ +internal val navigationBarHookCallbackFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.STATIC) + returns("V") + parameters(EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR, "Landroid/view/View;") + custom { method, _ -> + method.name == "navigationTabCreatedCallback" && + method.definingClass == EXTENSION_CLASS_DESCRIPTOR + } +} + +/** + * Matches to the Enum class that looks up ordinal -> instance. + */ +internal val navigationEnumFingerprint = fingerprint { + accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) + strings( + "PIVOT_HOME", + "TAB_SHORTS", + "CREATION_TAB_LARGE", + "PIVOT_SUBSCRIPTIONS", + "TAB_ACTIVITY", + "VIDEO_LIBRARY_WHITE", + "INCOGNITO_CIRCLE", + ) +} + +internal val pivotBarButtonsCreateDrawableViewFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Landroid/view/View;") + custom { method, _ -> + method.definingClass == "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 = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Landroid/view/View;") + parameters("L", "Z", "I", "L") + custom { method, _ -> + method.definingClass == "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 = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("I", "Z") + custom { method, _ -> + indexOfSetViewSelectedInstruction(method) >= 0 && + method.definingClass == "Lcom/google/android/libraries/youtube/rendering/ui/pivotbar/PivotBar;" + } +} + +internal val pivotBarConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + strings("com.google.android.apps.youtube.app.endpoint.flags") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt new file mode 100644 index 000000000..503eb91d4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/navigation/NavigationBarHookPatch.kt @@ -0,0 +1,154 @@ +package app.revanced.patches.youtube.misc.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.patch.resourcePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.playertype.playerTypeHookPatch +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.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 var imageOnlyTabResourceId = -1L + private set +internal var actionBarSearchResultsViewMicId = -1L + private set + +private val navigationBarHookResourcePatch = resourcePatch { + dependsOn(resourceMappingPatch) + + execute { + imageOnlyTabResourceId = resourceMappings["layout", "image_only_tab"] + actionBarSearchResultsViewMicId = resourceMappings["layout", "action_bar_search_results_view_mic"] + } +} + +internal const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/shared/NavigationBar;" +internal const val EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR = + "Lapp/revanced/extension/youtube/shared/NavigationBar\$NavigationButton;" + +lateinit var hookNavigationButtonCreated: (String) -> Unit + +val navigationBarHookPatch = bytecodePatch(description = "Hooks the active navigation or search bar.") { + dependsOn( + sharedExtensionPatch, + navigationBarHookResourcePatch, + playerTypeHookPatch, // Required to detect the search bar in all situations. + ) + + 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.match(pivotBarConstructorFingerprint.originalClassDef).method.apply { + // Hook the current navigation bar enum value. Note, the 'You' tab does not have an enum value. + val navigationEnumClassName = navigationEnumFingerprint.classDef.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.method + addHook(Hook.NAVIGATION_TAB_LOADED) predicate@{ + MethodUtil.methodSignaturesMatch( + getReference() ?: return@predicate false, + drawableTabMethod, + ) + } + + val imageResourceTabMethod = pivotBarButtonsCreateResourceViewFingerprint.originalMethod + addHook(Hook.NAVIGATION_IMAGE_RESOURCE_TAB_LOADED) predicate@{ + MethodUtil.methodSignaturesMatch( + getReference() ?: return@predicate false, + imageResourceTabMethod, + ) + } + } + + pivotBarButtonsViewSetSelectedFingerprint.method.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", + ) + } + + // Hook onto back button pressed. Needed to fix race problem with + // Litho filtering based on navigation tab before the tab is updated. + mainActivityOnBackPressedFingerprint.method.addInstruction( + 0, + "invoke-static { p0 }, " + + "$EXTENSION_CLASS_DESCRIPTOR->onBackPressed(Landroid/app/Activity;)V", + ) + + // Hook the search bar. + + // Two different layouts are used at the hooked code. + // Insert before the first ViewGroup method call after inflating, + // so this works regardless which layout is used. + actionBarSearchResultsFingerprint.method.apply { + val searchBarResourceId = indexOfFirstLiteralInstructionOrThrow( + actionBarSearchResultsViewMicId, + ) + + val instructionIndex = indexOfFirstInstructionOrThrow(searchBarResourceId) { + opcode == Opcode.INVOKE_VIRTUAL && getReference()?.name == "setLayoutDirection" + } + + val viewRegister = getInstruction(instructionIndex).registerC + + addInstruction( + instructionIndex, + "invoke-static { v$viewRegister }, " + + "$EXTENSION_CLASS_DESCRIPTOR->searchBarResultsViewLoaded(Landroid/view/View;)V", + ) + } + + hookNavigationButtonCreated = { extensionClassDescriptor -> + navigationBarHookCallbackFingerprint.method.addInstruction( + 0, + "invoke-static { p0, p1 }, " + + "$extensionClassDescriptor->navigationTabCreated" + + "(${EXTENSION_NAVIGATION_BUTTON_DESCRIPTOR}Landroid/view/View;)V", + ) + } + } +} + +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/misc/playercontrols/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/Fingerprints.kt new file mode 100644 index 000000000..9fca4d89c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/Fingerprints.kt @@ -0,0 +1,56 @@ +package app.revanced.patches.youtube.misc.playercontrols + +import app.revanced.patcher.fingerprint +import app.revanced.util.containsLiteralInstruction +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags + +internal val playerTopControlsInflateFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters() + literal { controlsLayoutStub } +} + +internal val playerControlsExtensionHookFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("V") + parameters("Z") + custom { methodDef, classDef -> + methodDef.name == "fullscreenButtonVisibilityChanged" && + classDef.type == "Lapp/revanced/extension/youtube/patches/PlayerControlsPatch;" + } +} + +internal val playerBottomControlsInflateFingerprint = fingerprint { + returns("Ljava/lang/Object;") + parameters() + literal { bottomUiContainerResourceId } +} + +internal val overlayViewInflateFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("Landroid/view/View;") + custom { methodDef, _ -> + methodDef.containsLiteralInstruction(fullscreenButton) && + methodDef.containsLiteralInstruction(heatseekerViewstub) + } +} + +/** + * Resolves to the class found in [playerTopControlsInflateFingerprint]. + */ +internal val controlsOverlayVisibilityFingerprint = fingerprint { + accessFlags(AccessFlags.PRIVATE, AccessFlags.FINAL) + returns("V") + parameters("Z", "Z") +} + +internal val playerControlsExploderFeatureFlagFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters() + literal { 45643739L } +} + diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt new file mode 100644 index 000000000..d2a20deb0 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/PlayerControlsPatch.kt @@ -0,0 +1,274 @@ +package app.revanced.patches.youtube.misc.playercontrols + +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.patch.resourcePatch +import app.revanced.patcher.util.Document +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.youtube.misc.playservice.is_19_35_or_greater +import app.revanced.util.* +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.iface.reference.TypeReference +import org.w3c.dom.Node + +/** + * 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 + +/** + * Add a new bottom to the bottom of the YouTube player. + * + * @param resourceDirectoryName The name of the directory containing the hosting resource. + */ +@Suppress("KDocUnresolvedReference") +lateinit var addBottomControl: (String) -> Unit + private set + +internal var bottomUiContainerResourceId = -1L + private set +internal var controlsLayoutStub = -1L + private set +internal var heatseekerViewstub = -1L + private set +internal var fullscreenButton = -1L + private set + +val playerControlsResourcePatch = resourcePatch { + dependsOn(resourceMappingPatch) + + /** + * The element to the left of the element being added. + */ + /** + * The element to the left of the element being added. + */ + var bottomLastLeftOf = "@id/fullscreen_button" + + lateinit var bottomTargetDocument: Document + + execute { + val targetResourceName = "youtube_controls_bottom_ui_container.xml" + + bottomUiContainerResourceId = resourceMappings["id", "bottom_ui_container_stub"] + controlsLayoutStub = resourceMappings["id", "controls_layout_stub"] + heatseekerViewstub = resourceMappings["id", "heatseeker_viewstub"] + fullscreenButton = resourceMappings["id", "fullscreen_button"] + + bottomTargetDocument = document("res/layout/$targetResourceName") + + val bottomTargetElement: Node = bottomTargetDocument.getElementsByTagName( + "android.support.constraint.ConstraintLayout", + ).item(0) + + var bottomInsertBeforeNode: Node = bottomTargetDocument.childNodes.findElementByAttributeValue( + "android:inflatedId", + bottomLastLeftOf, + ) ?: bottomTargetDocument.childNodes.findElementByAttributeValueOrThrow( + "android:id", // Older targets use non-inflated id. + bottomLastLeftOf, + ) + + addTopControl = { resourceDirectoryName -> + val resourceFileName = "host/layout/youtube_controls_layout.xml" + val hostingResourceStream = inputStreamFromBundledResource( + resourceDirectoryName, + resourceFileName, + ) ?: throw PatchException("Could not find $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 + } + } + + addBottomControl = { resourceDirectoryName -> + val resourceFileName = "host/layout/youtube_controls_bottom_ui_container.xml" + val sourceDocument = document( + inputStreamFromBundledResource(resourceDirectoryName, resourceFileName) + ?: throw PatchException("Could not find $resourceFileName"), + ) + + val sourceElements = sourceDocument.getElementsByTagName( + "android.support.constraint.ConstraintLayout", + ).item(0).childNodes + + // Copy the patch layout xml into the target layout file. + for (index in 1 until sourceElements.length) { + val element = sourceElements.item(index).cloneNode(true) + + // If the element has no attributes there's no point adding it to the destination. + if (!element.hasAttributes()) continue + + element.attributes.getNamedItem("yt:layout_constraintRight_toLeftOf").nodeValue = bottomLastLeftOf + bottomLastLeftOf = element.attributes.getNamedItem("android:id").nodeValue + + bottomTargetDocument.adoptNode(element) + // Elements do not need to be added in the layout order since a layout constraint is used, + // but in order is easier to make sense of while debugging. + bottomTargetElement.insertBefore(element, bottomInsertBeforeNode) + bottomInsertBeforeNode = element + } + + sourceDocument.close() + } + } + + finalize { + arrayOf( + "@id/bottom_end_container", + "@id/multiview_button", + ).forEach { + bottomTargetDocument.childNodes.findElementByAttributeValue( + "android:id", + it, + )?.setAttribute("yt:layout_constraintRight_toLeftOf", bottomLastLeftOf) + } + + bottomTargetDocument.close() + } +} + +/** + * Injects the code to initialize the controls. + * @param descriptor The descriptor of the method which should be called. + */ +internal fun initializeTopControl(descriptor: String) { + inflateTopControlMethod.addInstruction( + inflateTopControlInsertIndex++, + "invoke-static { v$inflateTopControlRegister }, $descriptor->initialize(Landroid/view/View;)V", + ) +} + +/** + * Injects the code to initialize the controls. + * @param descriptor The descriptor of the method which should be called. + */ +fun initializeBottomControl(descriptor: String) { + inflateBottomControlMethod.addInstruction( + inflateBottomControlInsertIndex++, + "invoke-static { v$inflateBottomControlRegister }, $descriptor->initializeButton(Landroid/view/View;)V", + ) +} + +/** + * Injects the code to change the visibility of controls. + * @param descriptor The descriptor of the method which should be called. + */ +fun injectVisibilityCheckCall(descriptor: String) { + visibilityMethod.addInstruction( + visibilityInsertIndex++, + "invoke-static { p1 , p2 }, $descriptor->changeVisibility(ZZ)V", + ) + + visibilityImmediateMethod.addInstruction( + visibilityImmediateInsertIndex++, + "invoke-static { p0 }, $descriptor->changeVisibilityImmediate(Z)V", + ) +} + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/PlayerControlsPatch;" + +private lateinit var inflateTopControlMethod: MutableMethod +private var inflateTopControlInsertIndex: Int = -1 +private var inflateTopControlRegister: Int = -1 + +private lateinit var inflateBottomControlMethod: MutableMethod +private var inflateBottomControlInsertIndex: Int = -1 +private var inflateBottomControlRegister: Int = -1 + +private lateinit var visibilityMethod: MutableMethod +private var visibilityInsertIndex: Int = 0 + +private lateinit var visibilityImmediateMethod: MutableMethod +private var visibilityImmediateInsertIndex: Int = 0 + +val playerControlsPatch = bytecodePatch( + description = "Manages the code for the player controls of the YouTube player.", +) { + dependsOn(playerControlsResourcePatch) + + execute { + fun MutableMethod.indexOfFirstViewInflateOrThrow() = + indexOfFirstInstructionOrThrow { + val reference = getReference() + reference?.definingClass == "Landroid/view/ViewStub;" && + reference.name == "inflate" + } + + playerBottomControlsInflateFingerprint.method.apply { + inflateBottomControlMethod = this + + val inflateReturnObjectIndex = indexOfFirstViewInflateOrThrow() + 1 + inflateBottomControlRegister = getInstruction(inflateReturnObjectIndex).registerA + inflateBottomControlInsertIndex = inflateReturnObjectIndex + 1 + } + + playerTopControlsInflateFingerprint.method.apply { + inflateTopControlMethod = this + + val inflateReturnObjectIndex = indexOfFirstViewInflateOrThrow() + 1 + inflateTopControlRegister = getInstruction(inflateReturnObjectIndex).registerA + inflateTopControlInsertIndex = inflateReturnObjectIndex + 1 + } + + visibilityMethod = controlsOverlayVisibilityFingerprint.match( + playerTopControlsInflateFingerprint.originalClassDef, + ).method + + // Hook the fullscreen close button. Used to fix visibility + // when seeking and other situations. + overlayViewInflateFingerprint.method.apply { + val resourceIndex = indexOfFirstLiteralInstructionReversedOrThrow(fullscreenButton) + + val index = indexOfFirstInstructionOrThrow(resourceIndex) { + opcode == Opcode.CHECK_CAST && + getReference()?.type == + "Landroid/widget/ImageView;" + } + val register = getInstruction(index).registerA + + addInstruction( + index + 1, + "invoke-static { v$register }, " + + "$EXTENSION_CLASS_DESCRIPTOR->setFullscreenCloseButton(Landroid/widget/ImageView;)V", + ) + } + + visibilityImmediateMethod = playerControlsExtensionHookFingerprint.method + + // A/B test for a slightly different overlay controls, + // that uses layout file youtube_video_exploder_controls_bottom_ui_container.xml + // The change to support this is simple and only requires adding buttons to both layout files, + // but for now force this different layout off since it's still an experimental test. + if (is_19_35_or_greater) { + playerControlsExploderFeatureFlagFingerprint.method.returnEarly() + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playertype/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playertype/Fingerprints.kt new file mode 100644 index 000000000..2faae3acc --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playertype/Fingerprints.kt @@ -0,0 +1,28 @@ +package app.revanced.patches.youtube.misc.playertype + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val playerTypeFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L") + opcodes( + Opcode.IF_NE, + Opcode.RETURN_VOID, + ) + custom { _, classDef -> classDef.endsWith("/YouTubePlayerOverlaysLayout;") } +} + +internal val videoStateFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("Lcom/google/android/libraries/youtube/player/features/overlay/controls/ControlsState;") + opcodes( + Opcode.CONST_4, + Opcode.IF_EQZ, + Opcode.IF_EQZ, + Opcode.IGET_OBJECT, // obfuscated parameter field name + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playertype/PlayerTypeHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playertype/PlayerTypeHookPatch.kt new file mode 100644 index 000000000..e021928a8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playertype/PlayerTypeHookPatch.kt @@ -0,0 +1,36 @@ +package app.revanced.patches.youtube.misc.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.bytecodePatch +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction + +internal const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/PlayerTypeHookPatch;" + +val playerTypeHookPatch = bytecodePatch( + description = "Hook to get the current player type and video playback state.", +) { + dependsOn(sharedExtensionPatch) + + execute { + playerTypeFingerprint.method.addInstruction( + 0, + "invoke-static {p1}, $EXTENSION_CLASS_DESCRIPTOR->setPlayerType(Ljava/lang/Enum;)V", + ) + + videoStateFingerprint.method.apply { + val endIndex = videoStateFingerprint.patternMatch!!.endIndex + val videoStateFieldName = getInstruction(endIndex).reference + + addInstructions( + 0, + """ + iget-object v0, p1, $videoStateFieldName # copy VideoState parameter field + invoke-static {v0}, $EXTENSION_CLASS_DESCRIPTOR->setVideoState(Ljava/lang/Enum;)V + """, + ) + } + } +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt similarity index 50% rename from src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt index 5ef04463c..0598a77bd 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/playservice/VersionCheckPatch.kt @@ -1,36 +1,53 @@ +@file:Suppress("ktlint:standard:property-naming") + package app.revanced.patches.youtube.misc.playservice -import app.revanced.patcher.data.ResourceContext -import app.revanced.patcher.patch.ResourcePatch -import app.revanced.patcher.patch.annotation.Patch +import app.revanced.patcher.patch.resourcePatch import app.revanced.util.findElementByAttributeValueOrThrow -import kotlin.properties.Delegates -@Patch(description = "Uses the Play Store service version to find the major/minor version of the YouTube target app.") -internal object VersionCheckPatch : ResourcePatch() { - - var is_19_03_or_greater by Delegates.notNull() - var is_19_04_or_greater by Delegates.notNull() - var is_19_16_or_greater by Delegates.notNull() - var is_19_17_or_greater by Delegates.notNull() - var is_19_18_or_greater by Delegates.notNull() - var is_19_23_or_greater by Delegates.notNull() - var is_19_25_or_greater by Delegates.notNull() - var is_19_26_or_greater by Delegates.notNull() - var is_19_29_or_greater by Delegates.notNull() - var is_19_32_or_greater by Delegates.notNull() - var is_19_33_or_greater by Delegates.notNull() - var is_19_36_or_greater by Delegates.notNull() - var is_19_41_or_greater by Delegates.notNull() - - override fun execute(context: ResourceContext) { +var is_19_03_or_greater = false + private set +var is_19_04_or_greater = false + private set +var is_19_16_or_greater = false + private set +var is_19_17_or_greater = false + private set +var is_19_18_or_greater = false + private set +var is_19_23_or_greater = false + private set +var is_19_25_or_greater = false + private set +var is_19_26_or_greater = false + private set +var is_19_29_or_greater = false + private set +var is_19_32_or_greater = false + private set +var is_19_33_or_greater = false + private set +var is_19_34_or_greater = false + private set +var is_19_35_or_greater = false + private set +var is_19_36_or_greater = false + private set +var is_19_41_or_greater = false + private set +var is_19_43_or_greater = false + private set +val versionCheckPatch = resourcePatch( + description = "Uses the Play Store service version to find the major/minor version of the YouTube target app.", +) { + 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 = context.document["res/values/integers.xml"].use { document -> + val playStoreServicesVersion = document("res/values/integers.xml").use { document -> document.documentElement.childNodes.findElementByAttributeValueOrThrow( "name", - "google_play_services_version" + "google_play_services_version", ).textContent.toInt() } @@ -46,7 +63,10 @@ internal object VersionCheckPatch : ResourcePatch() { is_19_29_or_greater = 243005000 <= playStoreServicesVersion is_19_32_or_greater = 243199000 <= playStoreServicesVersion is_19_33_or_greater = 243405000 <= playStoreServicesVersion + is_19_34_or_greater = 243499000 <= playStoreServicesVersion + is_19_35_or_greater = 243605000 <= playStoreServicesVersion is_19_36_or_greater = 243705000 <= playStoreServicesVersion is_19_41_or_greater = 244305000 <= playStoreServicesVersion + is_19_43_or_greater = 244405000 <= playStoreServicesVersion } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/privacy/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/privacy/Fingerprints.kt new file mode 100644 index 000000000..72734bba7 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/privacy/Fingerprints.kt @@ -0,0 +1,44 @@ +package app.revanced.patches.youtube.misc.privacy + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val copyTextFingerprint = fingerprint { + returns("V") + parameters("L", "Ljava/util/Map;") + opcodes( + Opcode.IGET_OBJECT, // Contains the text to copy to be sanitized. + Opcode.CONST_STRING, + Opcode.INVOKE_STATIC, // ClipData.newPlainText + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.IGET_OBJECT, + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.RETURN_VOID, + ) + strings("text/plain") +} + +internal val systemShareSheetFingerprint = fingerprint { + returns("V") + parameters("L", "Ljava/util/Map;") + opcodes( + Opcode.CHECK_CAST, + Opcode.GOTO, + ) + strings("YTShare_Logging_Share_Intent_Endpoint_Byte_Array") +} + +internal val youtubeShareSheetFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L", "Ljava/util/Map;") + opcodes( + Opcode.CHECK_CAST, + Opcode.GOTO, + Opcode.MOVE_OBJECT, + Opcode.INVOKE_VIRTUAL, + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/privacy/RemoveTrackingQueryParameterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/privacy/RemoveTrackingQueryParameterPatch.kt new file mode 100644 index 000000000..7a6d519e4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/privacy/RemoveTrackingQueryParameterPatch.kt @@ -0,0 +1,80 @@ +package app.revanced.patches.youtube.misc.privacy + +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.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/RemoveTrackingQueryParameterPatch;" + +@Suppress("unused") +val removeTrackingQueryParameterPatch = bytecodePatch( + name = "Remove tracking query parameter", + description = "Adds an option to remove the tracking info from links you share.", +) { + dependsOn( + sharedExtensionPatch, + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "misc.privacy.removeTrackingQueryParameterPatch") + + PreferenceScreen.MISC.addPreferences( + SwitchPreference("revanced_remove_tracking_query_parameter"), + ) + + fun Fingerprint.hook( + getInsertIndex: Match.PatternMatch.() -> Int, + getUrlRegister: MutableMethod.(insertIndex: Int) -> Int, + ) { + val insertIndex = patternMatch!!.getInsertIndex() + val urlRegister = method.getUrlRegister(insertIndex) + + method.addInstructions( + insertIndex, + """ + invoke-static {v$urlRegister}, $EXTENSION_CLASS_DESCRIPTOR->sanitize(Ljava/lang/String;)Ljava/lang/String; + move-result-object v$urlRegister + """, + ) + } + + // YouTube share sheet.\ + youtubeShareSheetFingerprint.hook(getInsertIndex = { startIndex + 1 }) { insertIndex -> + getInstruction(insertIndex - 1).registerA + } + + // Native system share sheet. + systemShareSheetFingerprint.hook(getInsertIndex = { endIndex }) { insertIndex -> + getInstruction(insertIndex - 1).registerA + } + + copyTextFingerprint.hook(getInsertIndex = { startIndex + 2 }) { insertIndex -> + getInstruction(insertIndex - 2).registerA + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/recyclerviewtree/hook/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/recyclerviewtree/hook/Fingerprints.kt new file mode 100644 index 000000000..09aa7bf4c --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/recyclerviewtree/hook/Fingerprints.kt @@ -0,0 +1,18 @@ +package app.revanced.patches.youtube.misc.recyclerviewtree.hook + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val recyclerViewTreeObserverFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + opcodes( + Opcode.CHECK_CAST, + Opcode.NEW_INSTANCE, + Opcode.INVOKE_DIRECT, + Opcode.INVOKE_VIRTUAL, + Opcode.NEW_INSTANCE, + ) + strings("LithoRVSLCBinder") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/recyclerviewtree/hook/RecyclerViewTreeHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/recyclerviewtree/hook/RecyclerViewTreeHookPatch.kt new file mode 100644 index 000000000..86588df09 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/recyclerviewtree/hook/RecyclerViewTreeHookPatch.kt @@ -0,0 +1,28 @@ +package app.revanced.patches.youtube.misc.recyclerviewtree.hook + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch + +lateinit var addRecyclerViewTreeHook: (String) -> Unit + private set + +val recyclerViewTreeHookPatch = bytecodePatch { + dependsOn(sharedExtensionPatch) + + execute { + + recyclerViewTreeObserverFingerprint.method.apply { + val insertIndex = recyclerViewTreeObserverFingerprint.patternMatch!!.startIndex + 1 + val recyclerViewParameter = 2 + + addRecyclerViewTreeHook = { classDescriptor -> + addInstruction( + insertIndex, + "invoke-static/range { p$recyclerViewParameter .. p$recyclerViewParameter }, " + + "$classDescriptor->onFlyoutMenuCreate(Landroid/support/v7/widget/RecyclerView;)V", + ) + } + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/Fingerprints.kt new file mode 100644 index 000000000..42298d82f --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/Fingerprints.kt @@ -0,0 +1,23 @@ +package app.revanced.patches.youtube.misc.settings + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val licenseActivityOnCreateFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L") + custom { method, classDef -> + classDef.endsWith("LicenseActivity;") && method.name == "onCreate" + } +} + +internal val setThemeFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("L") + parameters() + opcodes(Opcode.RETURN_OBJECT) + literal { appearanceStringId } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt new file mode 100644 index 000000000..627a70f91 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/settings/SettingsPatch.kt @@ -0,0 +1,268 @@ +package app.revanced.patches.youtube.misc.settings + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.extensions.InstructionExtensions.replaceInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.all.misc.packagename.setOrGetFallbackPackageName +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.shared.misc.settings.preference.* +import app.revanced.patches.shared.misc.settings.preference.PreferenceScreenPreference.Sorting +import app.revanced.patches.shared.misc.settings.settingsPatch +import app.revanced.patches.youtube.misc.check.checkEnvironmentPatch +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.fix.cairo.disableCairoSettingsPatch +import app.revanced.util.* +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.util.MethodUtil + +// Used by a fingerprint() from SettingsPatch. +internal var appearanceStringId = -1L + private set + +private val preferences = mutableSetOf() + +fun addSettingPreference(screen: BasePreference) { + preferences += screen +} + +private val settingsResourcePatch = resourcePatch { + dependsOn( + resourceMappingPatch, + settingsPatch( + rootPreference = IntentPreference( + titleKey = "revanced_settings_title", + summaryKey = null, + intent = newIntent("revanced_settings_intent"), + ) to "settings_fragment", + preferences, + ), + ) + + execute { + // Used for a fingerprint from SettingsPatch. + appearanceStringId = resourceMappings["string", "app_theme_appearance_dark"] + + arrayOf( + ResourceGroup("layout", "revanced_settings_with_toolbar.xml"), + ).forEach { resourceGroup -> + copyResources("settings", resourceGroup) + } + + // Copy style properties used to fix over-sized copy menu that appear in EditTextPreference. + // For a full explanation of how this fixes the issue, see the comments in this style file + // and the comments in the extension code. + val targetResource = "values/styles.xml" + inputStreamFromBundledResource( + "settings/host", + targetResource, + )!!.let { inputStream -> + "resources".copyXmlNode( + document(inputStream), + document("res/$targetResource"), + ).close() + } + + // Remove horizontal divider from the settings Preferences + // To better match the appearance of the stock YouTube settings. + document("res/values/styles.xml").use { document -> + + arrayOf( + "Theme.YouTube.Settings", + "Theme.YouTube.Settings.Dark", + ).forEach { value -> + val listDividerNode = document.createElement("item") + listDividerNode.setAttribute("name", "android:listDivider") + listDividerNode.appendChild(document.createTextNode("@null")) + + document.childNodes.findElementByAttributeValueOrThrow( + "name", + value, + ).appendChild(listDividerNode) + } + } + + // Modify the manifest and add a data intent filter to the LicenseActivity. + // Some devices freak out if undeclared data is passed to an intent, + // and this change appears to fix the issue. + document("AndroidManifest.xml").use { document -> + + val licenseElement = document.childNodes.findElementByAttributeValueOrThrow( + "android:name", + "com.google.android.libraries.social.licenses.LicenseActivity", + ) + + val mimeType = document.createElement("data") + mimeType.setAttribute("android:mimeType", "text/plain") + + val intentFilter = document.createElement("intent-filter") + intentFilter.appendChild(mimeType) + + licenseElement.appendChild(intentFilter) + } + } +} + +val settingsPatch = bytecodePatch( + description = "Adds settings for ReVanced to YouTube.", +) { + dependsOn( + sharedExtensionPatch, + settingsResourcePatch, + addResourcesPatch, + disableCairoSettingsPatch, + // Currently there is no easy way to make a mandatory patch, + // so for now this is a dependent of this patch. + checkEnvironmentPatch, + ) + + val extensionPackage = "app/revanced/extension/youtube" + val activityHookClassDescriptor = "L$extensionPackage/settings/LicenseActivityHook;" + + val themeHelperDescriptor = "L$extensionPackage/ThemeHelper;" + val setThemeMethodName = "setTheme" + + execute { + addResources("youtube", "misc.settings.settingsPatch") + + // Add an "about" preference to the top. + preferences += NonInteractivePreference( + key = "revanced_settings_screen_00_about", + summaryKey = null, + tag = "app.revanced.extension.youtube.settings.preference.ReVancedYouTubeAboutPreference", + selectable = true, + ) + + PreferenceScreen.MISC.addPreferences( + TextPreference( + key = null, + titleKey = "revanced_pref_import_export_title", + summaryKey = "revanced_pref_import_export_summary", + inputType = InputType.TEXT_MULTI_LINE, + tag = "app.revanced.extension.shared.settings.preference.ImportExportPreference", + ), + ) + + setThemeFingerprint.method.let { setThemeMethod -> + setThemeMethod.implementation!!.instructions.mapIndexedNotNull { i, instruction -> + if (instruction.opcode == Opcode.RETURN_OBJECT) i else null + }.asReversed().forEach { returnIndex -> + // The following strategy is to replace the return instruction with the setTheme instruction, + // then add a return instruction after the setTheme instruction. + // This is done because the return instruction is a target of another instruction. + + setThemeMethod.apply { + // This register is returned by the setTheme method. + val register = getInstruction(returnIndex).registerA + replaceInstruction( + returnIndex, + "invoke-static { v$register }, " + + "$themeHelperDescriptor->$setThemeMethodName(Ljava/lang/Enum;)V", + ) + addInstruction(returnIndex + 1, "return-object v$register") + } + } + } + + // Modify the license activity and remove all existing layout code. + // Must modify an existing activity and cannot add a new activity to the manifest, + // as that fails for root installations. + + licenseActivityOnCreateFingerprint.method.addInstructions( + 1, + """ + invoke-static { p0 }, $activityHookClassDescriptor->initialize(Landroid/app/Activity;)V + return-void + """, + ) + + // Remove other methods as they will break as the onCreate method is modified above. + licenseActivityOnCreateFingerprint.classDef.apply { + methods.removeIf { it.name != "onCreate" && !MethodUtil.isConstructor(it) } + } + } + + finalize { + PreferenceScreen.close() + } +} + +/** + * Creates an intent to open ReVanced settings. + */ +fun newIntent(settingsName: String) = IntentPreference.Intent( + data = settingsName, + targetClass = "com.google.android.libraries.social.licenses.LicenseActivity", +) { + // The package name change has to be reflected in the intent. + setOrGetFallbackPackageName("com.google.android.youtube") +} + +object PreferenceScreen : BasePreferenceScreen() { + // Sort screens in the root menu by key, to not scatter related items apart + // (sorting key is set in revanced_prefs.xml). + // If no preferences are added to a screen, the screen will not be added to the settings. + val ADS = Screen( + key = "revanced_settings_screen_01_ads", + summaryKey = null, + ) + val ALTERNATIVE_THUMBNAILS = Screen( + key = "revanced_settings_screen_02_alt_thumbnails", + summaryKey = null, + sorting = Sorting.UNSORTED, + ) + val FEED = Screen( + key = "revanced_settings_screen_03_feed", + summaryKey = null, + ) + val PLAYER = Screen( + key = "revanced_settings_screen_04_player", + summaryKey = null, + ) + val GENERAL_LAYOUT = Screen( + key = "revanced_settings_screen_05_general", + summaryKey = null, + ) + + // Don't sort, as related preferences are scattered apart. + // Can use title sorting after PreferenceCategory support is added. + val SHORTS = Screen( + key = "revanced_settings_screen_06_shorts", + summaryKey = null, + ) + + // Don't sort, because title sorting scatters the custom color preferences. + val SEEKBAR = Screen( + key = "revanced_settings_screen_07_seekbar", + summaryKey = null, + sorting = Sorting.UNSORTED, + ) + val SWIPE_CONTROLS = Screen( + key = "revanced_settings_screen_08_swipe_controls", + summaryKey = null, + sorting = Sorting.UNSORTED, + ) + + // RYD and SB are items 9 and 10. + // Menus are added in their own patch because they use an Intent and not a Screen. + + val MISC = Screen( + key = "revanced_settings_screen_11_misc", + summaryKey = null, + ) + val VIDEO = Screen( + key = "revanced_settings_screen_12_video", + summaryKey = null, + ) + + override fun commit(screen: PreferenceScreenPreference) { + preferences += screen + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/zoomhaptics/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/zoomhaptics/Fingerprints.kt new file mode 100644 index 000000000..65cb70769 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/zoomhaptics/Fingerprints.kt @@ -0,0 +1,7 @@ +package app.revanced.patches.youtube.misc.zoomhaptics + +import app.revanced.patcher.fingerprint + +internal val zoomHapticsFingerprint = fingerprint { + strings("Failed to haptics vibrate for video zoom") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/misc/zoomhaptics/ZoomHapticsPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/zoomhaptics/ZoomHapticsPatch.kt new file mode 100644 index 000000000..77ec07f13 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/misc/zoomhaptics/ZoomHapticsPatch.kt @@ -0,0 +1,54 @@ +package app.revanced.patches.youtube.misc.zoomhaptics + +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.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch + +@Suppress("unused") +val zoomHapticsPatch = bytecodePatch( + name = "Disable zoom haptics", + description = "Adds an option to disable haptics when zooming.", +) { + dependsOn( + settingsPatch, + addResourcesPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + addResources("youtube", "misc.zoomhaptics.zoomHapticsPatch") + + PreferenceScreen.MISC.addPreferences( + SwitchPreference("revanced_disable_zoom_haptics"), + ) + + zoomHapticsFingerprint.method.apply { + addInstructionsWithLabels( + 0, + """ + invoke-static { }, Lapp/revanced/extension/youtube/patches/ZoomHapticsPatch;->shouldVibrate()Z + move-result v0 + if-nez v0, :vibrate + return-void + """, + ExternalLabel("vibrate", getInstruction(0)), + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt new file mode 100644 index 000000000..a7c72504e --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/shared/Fingerprints.kt @@ -0,0 +1,130 @@ +package app.revanced.patches.youtube.shared + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val autoRepeatFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters() + custom { method, _ -> + method.implementation!!.instructions.count() == 3 && method.annotations.isEmpty() + } +} + +internal val autoRepeatParentFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + strings( + "play() called when the player wasn't loaded.", + "play() blocked because Background Playability failed", + ) +} + +internal val layoutConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters() + strings("1.0x") +} + +internal val mainActivityFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + parameters() + custom { _, classDef -> + // Old versions of YouTube called this class "WatchWhileActivity" instead. + classDef.endsWith("MainActivity;") || classDef.endsWith("WatchWhileActivity;") + } +} + +internal val mainActivityOnCreateFingerprint = fingerprint { + returns("V") + parameters("Landroid/os/Bundle;") + custom { method, classDef -> + method.name == "onCreate" && + ( + classDef.endsWith("MainActivity;") || + // Old versions of YouTube called this class "WatchWhileActivity" instead. + classDef.endsWith("WatchWhileActivity;") + ) + } +} + +val rollingNumberTextViewAnimationUpdateFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("Landroid/graphics/Bitmap;") + opcodes( + Opcode.NEW_INSTANCE, // bitmap ImageSpan + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.CONST_4, + Opcode.INVOKE_DIRECT, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.CONST_16, + Opcode.INVOKE_VIRTUAL, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.INT_TO_FLOAT, + Opcode.INVOKE_VIRTUAL, // set textview padding using bitmap width + ) + custom { _, classDef -> + classDef.superclass == "Landroid/support/v7/widget/AppCompatTextView;" || + classDef.superclass == + "Lcom/google/android/libraries/youtube/rendering/ui/spec/typography/YouTubeAppCompatTextView;" + } +} + +internal val seekbarFingerprint = fingerprint { + returns("V") + strings("timed_markers_width") +} + +internal val seekbarOnDrawFingerprint = fingerprint { + custom { method, _ -> method.name == "onDraw" } +} + +internal val subtitleButtonControllerFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("Lcom/google/android/libraries/youtube/player/subtitles/model/SubtitleTrack;") + opcodes( + Opcode.IGET_OBJECT, + Opcode.IF_NEZ, + Opcode.RETURN_VOID, + Opcode.IGET_BOOLEAN, + Opcode.CONST_4, + Opcode.IF_NEZ, + Opcode.CONST, + Opcode.INVOKE_VIRTUAL, + Opcode.IGET_OBJECT, + ) +} + +internal val newVideoQualityChangedFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("L") + parameters("L") + opcodes( + Opcode.IGET, // Video resolution (human readable). + Opcode.IGET_OBJECT, + Opcode.IGET_BOOLEAN, + Opcode.IGET_OBJECT, + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_DIRECT, + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.GOTO, + Opcode.CONST_4, + Opcode.IF_NE, + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IGET, + ) +} diff --git a/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..d0c961c9d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/Fingerprints.kt @@ -0,0 +1,132 @@ +package app.revanced.patches.youtube.video.information + +import app.revanced.patcher.fingerprint +import app.revanced.patches.youtube.shared.newVideoQualityChangedFingerprint +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.iface.reference.FieldReference + +internal val createVideoPlayerSeekbarFingerprint = fingerprint { + returns("V") + strings("timed_markers_width") +} + +internal val onPlaybackSpeedItemClickFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L", "L", "I", "J") + custom { method, _ -> + method.name == "onItemClick" && + method.implementation?.instructions?.find { + it.opcode == Opcode.IGET_OBJECT && + it.getReference()!!.type == "Lcom/google/android/libraries/youtube/innertube/model/player/PlayerResponseModel;" + } != null + } +} + +internal val playerControllerSetTimeReferenceFingerprint = fingerprint { + opcodes(Opcode.INVOKE_DIRECT_RANGE, Opcode.IGET_OBJECT) + strings("Media progress reported outside media playback: ") +} + +internal val playerInitFingerprint = fingerprint { + strings("playVideo called on player response with no videoStreamingData.") +} + +/** + * Matched using class found in [playerInitFingerprint]. + */ +internal val seekFingerprint = fingerprint { + strings("Attempting to seek during an ad") +} + +internal val videoLengthFingerprint = fingerprint { + opcodes( + Opcode.MOVE_RESULT_WIDE, + Opcode.CMP_LONG, + Opcode.IF_LEZ, + Opcode.IGET_OBJECT, + Opcode.CHECK_CAST, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_WIDE, + Opcode.GOTO, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_WIDE, + Opcode.CONST_4, + Opcode.INVOKE_VIRTUAL, + ) +} + +/** + * Matches using class found in [mdxPlayerDirectorSetVideoStageFingerprint]. + */ +internal val mdxSeekFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + parameters("J", "L") + opcodes( + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT, + Opcode.RETURN, + ) + custom { methodDef, _ -> + // The instruction count is necessary here to avoid matching the relative version + // of the seek method we're after, which has the same function signature as the + // regular one, is in the same class, and even has the exact same 3 opcodes pattern. + methodDef.implementation!!.instructions.count() == 3 + } +} + +internal val mdxPlayerDirectorSetVideoStageFingerprint = fingerprint { + strings("MdxDirector setVideoStage ad should be null when videoStage is not an Ad state ") +} + +/** + * Matches using class found in [mdxPlayerDirectorSetVideoStageFingerprint]. + */ +internal val mdxSeekRelativeFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + // Return type is boolean up to 19.39, and void with 19.39+. + parameters("J", "L") + opcodes( + + Opcode.IGET_OBJECT, + Opcode.INVOKE_INTERFACE, + ) +} + +/** + * Matches using class found in [playerInitFingerprint]. + */ +internal val seekRelativeFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + // Return type is boolean up to 19.39, and void with 19.39+. + parameters("J", "L") + opcodes( + Opcode.ADD_LONG_2ADDR, + Opcode.INVOKE_VIRTUAL, + ) +} + +/** + * Resolves with the class found in [newVideoQualityChangedFingerprint]. + */ +internal val playbackSpeedMenuSpeedChangedFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("L") + parameters("L") + opcodes( + 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, + Opcode.SGET_OBJECT, + Opcode.RETURN_OBJECT, + ) +} 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..4b1755141 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/information/VideoInformationPatch.kt @@ -0,0 +1,309 @@ +package app.revanced.patches.youtube.video.information + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.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.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.shared.newVideoQualityChangedFingerprint +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.hookBackgroundPlayVideoId +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.getReference +import app.revanced.util.indexOfFirstInstructionOrThrow +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.builder.BuilderInstruction +import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction +import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference +import com.android.tools.smali.dexlib2.immutable.ImmutableMethod +import com.android.tools.smali.dexlib2.immutable.ImmutableMethodParameter +import com.android.tools.smali.dexlib2.util.MethodUtil + +private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/youtube/patches/VideoInformation;" +private const val EXTENSION_PLAYER_INTERFACE = + "Lapp/revanced/extension/youtube/patches/VideoInformation${'$'}PlaybackController;" + +private lateinit var playerInitMethod: MutableMethod +private var playerInitInsertIndex = -1 +private var playerInitInsertRegister = -1 + +private lateinit var mdxInitMethod: MutableMethod +private var mdxInitInsertIndex = -1 +private var mdxInitInsertRegister = -1 + +private lateinit var timeMethod: MutableMethod +private var timeInitInsertIndex = 2 + +// Old speed menu, where speeds are entries in a list. Method is also used by the player speed button. +private lateinit var legacySpeedSelectionInsertMethod: MutableMethod +private var legacySpeedSelectionInsertIndex = -1 +private var legacySpeedSelectionValueRegister = -1 + +// New speed menu, with preset buttons and 0.05x fine adjustments buttons. +private lateinit var speedSelectionInsertMethod: MutableMethod +private var speedSelectionInsertIndex = -1 +private var speedSelectionValueRegister = -1 + +// Used by other patches. +lateinit var setPlaybackSpeedContainerClassFieldReference: String + private set +lateinit var setPlaybackSpeedClassFieldReference: String + private set +lateinit var setPlaybackSpeedMethodReference: String + private set + +val videoInformationPatch = bytecodePatch( + description = "Hooks YouTube to get information about the current playing video.", +) { + dependsOn( + sharedExtensionPatch, + videoIdPatch, + playerResponseMethodHookPatch, + ) + + execute { + + playerInitMethod = playerInitFingerprint.classDef.methods.first { MethodUtil.isConstructor(it) } + + // Find the location of the first invoke-direct call and extract the register storing the 'this' object reference. + val initThisIndex = playerInitMethod.indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_DIRECT && getReference()?.name == "" + } + playerInitInsertRegister = playerInitMethod.getInstruction(initThisIndex).registerC + playerInitInsertIndex = initThisIndex + 1 + + // Hook the player controller for use through the extension. + onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "initialize") + + val seekFingerprintResultMethod = seekFingerprint.match(playerInitFingerprint.originalClassDef).method + val seekRelativeFingerprintResultMethod = + seekRelativeFingerprint.match(playerInitFingerprint.originalClassDef).method + + // Create extension interface methods. + addSeekInterfaceMethods( + playerInitFingerprint.classDef, + seekFingerprintResultMethod, + seekRelativeFingerprintResultMethod, + ) + + with(mdxPlayerDirectorSetVideoStageFingerprint) { + mdxInitMethod = classDef.methods.first { MethodUtil.isConstructor(it) } + + val initThisIndex = mdxInitMethod.indexOfFirstInstructionOrThrow { + opcode == Opcode.INVOKE_DIRECT && getReference()?.name == "" + } + mdxInitInsertRegister = mdxInitMethod.getInstruction(initThisIndex).registerC + mdxInitInsertIndex = initThisIndex + 1 + + // Hook the MDX director for use through the extension. + onCreateHookMdx(EXTENSION_CLASS_DESCRIPTOR, "initializeMdx") + + val mdxSeekFingerprintResultMethod = mdxSeekFingerprint.match(classDef).method + val mdxSeekRelativeFingerprintResultMethod = mdxSeekRelativeFingerprint.match(classDef).method + + addSeekInterfaceMethods(classDef, mdxSeekFingerprintResultMethod, mdxSeekRelativeFingerprintResultMethod) + } + + with(createVideoPlayerSeekbarFingerprint) { + val videoLengthMethodMatch = videoLengthFingerprint.match(originalClassDef) + + videoLengthMethodMatch.method.apply { + val videoLengthRegisterIndex = videoLengthMethodMatch.patternMatch!!.endIndex - 2 + val videoLengthRegister = getInstruction(videoLengthRegisterIndex).registerA + val dummyRegisterForLong = videoLengthRegister + 1 // required for long values since they are wide + + addInstruction( + videoLengthMethodMatch.patternMatch!!.endIndex, + "invoke-static {v$videoLengthRegister, v$dummyRegisterForLong}, " + + "$EXTENSION_CLASS_DESCRIPTOR->setVideoLength(J)V", + ) + } + } + + /* + * Inject call for video ids + */ + val videoIdMethodDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->setVideoId(Ljava/lang/String;)V" + hookVideoId(videoIdMethodDescriptor) + hookBackgroundPlayVideoId(videoIdMethodDescriptor) + 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.ProtoBufferParameterBeforeVideoId( + "$EXTENSION_CLASS_DESCRIPTOR->" + + "newPlayerResponseSignature(Ljava/lang/String;Ljava/lang/String;Z)Ljava/lang/String;", + ), + ) + + /* + * Set the video time method + */ + timeMethod = navigate(playerControllerSetTimeReferenceFingerprint.originalMethod) + .to(playerControllerSetTimeReferenceFingerprint.patternMatch!!.startIndex) + .stop() + + /* + * Hook the methods which set the time + */ + videoTimeHook(EXTENSION_CLASS_DESCRIPTOR, "setVideoTime") + + /* + * Hook the user playback speed selection + */ + onPlaybackSpeedItemClickFingerprint.method.apply { + val speedSelectionMethodInstructions = implementation!!.instructions + val speedSelectionValueInstructionIndex = speedSelectionMethodInstructions.indexOfFirst { + it.opcode == Opcode.IGET + } + legacySpeedSelectionInsertMethod = this + legacySpeedSelectionInsertIndex = speedSelectionValueInstructionIndex + 1 + legacySpeedSelectionValueRegister = + getInstruction(speedSelectionValueInstructionIndex).registerA + + setPlaybackSpeedClassFieldReference = + getInstruction(speedSelectionValueInstructionIndex + 1).reference.toString() + setPlaybackSpeedMethodReference = + getInstruction(speedSelectionValueInstructionIndex + 2).reference.toString() + setPlaybackSpeedContainerClassFieldReference = + getReference(speedSelectionMethodInstructions, -1, Opcode.IF_EQZ) + } + + // Handle new playback speed menu. + playbackSpeedMenuSpeedChangedFingerprint.match( + newVideoQualityChangedFingerprint.originalClassDef, + ).method.apply { + val index = indexOfFirstInstructionOrThrow(Opcode.IGET) + + speedSelectionInsertMethod = this + speedSelectionInsertIndex = index + 1 + speedSelectionValueRegister = getInstruction(index).registerA + } + + userSelectedPlaybackSpeedHook(EXTENSION_CLASS_DESCRIPTOR, "userSelectedPlaybackSpeed") + } +} + +private fun addSeekInterfaceMethods(targetClass: MutableClass, seekToMethod: Method, seekToRelativeMethod: Method) { + // Add the interface and methods that extension calls. + targetClass.interfaces.add(EXTENSION_PLAYER_INTERFACE) + + arrayOf( + Triple(seekToMethod, "seekTo", true), + Triple(seekToRelativeMethod, "seekToRelative", false), + ).forEach { (method, name, returnsBoolean) -> + // Add interface method. + // Get enum type for the seek helper method. + val seekSourceEnumType = method.parameterTypes[1].toString() + + val interfaceImplementation = ImmutableMethod( + targetClass.type, + name, + listOf(ImmutableMethodParameter("J", null, "time")), + if (returnsBoolean) "Z" else "V", + AccessFlags.PUBLIC.value or AccessFlags.FINAL.value, + null, + null, + MutableMethodImplementation(4), + ).toMutable() + + var instructions = """ + # First enum (field a) is SEEK_SOURCE_UNKNOWN. + sget-object v0, $seekSourceEnumType->a:$seekSourceEnumType + invoke-virtual { p0, p1, p2, v0 }, $method + """ + + instructions += if (returnsBoolean) { + """ + move-result p1 + return p1 + """ + } else { + "return-void" + } + + // Insert helper method instructions. + interfaceImplementation.addInstructions( + 0, + instructions, + ) + + targetClass.methods.add(interfaceImplementation) + } +} + +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) = + playerInitMethod.insert( + playerInitInsertIndex++, + "v$playerInitInsertRegister", + "$targetMethodClass->$targetMethodName($EXTENSION_PLAYER_INTERFACE)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) = + mdxInitMethod.insert( + mdxInitInsertIndex++, + "v$mdxInitInsertRegister", + "$targetMethodClass->$targetMethodName($EXTENSION_PLAYER_INTERFACE)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. + */ +fun videoTimeHook(targetMethodClass: String, targetMethodName: String) = + timeMethod.insertTimeHook( + timeInitInsertIndex++, + "$targetMethodClass->$targetMethodName(J)V", + ) + +private fun getReference(instructions: List, offset: Int, opcode: Opcode) = + (instructions[instructions.indexOfFirst { it.opcode == opcode } + offset] as ReferenceInstruction) + .reference.toString() + +/** + * Hook the video speed selected by the user. + */ +fun userSelectedPlaybackSpeedHook(targetMethodClass: String, targetMethodName: String) = + speedSelectionInsertMethod.addInstruction( + speedSelectionInsertIndex++, + "invoke-static {v$speedSelectionValueRegister}, $targetMethodClass->$targetMethodName(F)V", + ) diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt new file mode 100644 index 000000000..603329de7 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/Fingerprints.kt @@ -0,0 +1,52 @@ +package app.revanced.patches.youtube.video.playerresponse + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags + +/** + * For targets 19.25 and later. + */ +internal val playerParameterBuilderFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("L") + parameters( + "Ljava/lang/String;", // VideoId. + "[B", + "Ljava/lang/String;", // Player parameters proto buffer. + "Ljava/lang/String;", + "I", + "I", + "L", // 19.25+ parameter + "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", + ) + strings("psps") +} + +/** + * For targets 19.24 and earlier. + */ +internal val playerParameterBuilderLegacyFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("L") + parameters( + "Ljava/lang/String;", // VideoId. + "[B", + "Ljava/lang/String;", // Player parameters proto buffer. + "Ljava/lang/String;", + "I", + "I", + "Ljava/util/Set;", + "Ljava/lang/String;", + "Ljava/lang/String;", + "L", + "Z", // Appears to indicate if the video id is being opened or is currently playing. + "Z", + "Z", + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt new file mode 100644 index 000000000..2a9a91245 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/playerresponse/PlayerResponseMethodHookPatch.kt @@ -0,0 +1,121 @@ +package app.revanced.patches.youtube.video.playerresponse + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.playservice.is_19_23_or_greater +import app.revanced.patches.youtube.misc.playservice.versionCheckPatch + +private val hooks = mutableSetOf() + +fun addPlayerResponseMethodHook(hook: Hook) { + hooks += hook +} + +// Parameter numbers of the patched method. +private const val PARAMETER_VIDEO_ID = 1 +private const val PARAMETER_PROTO_BUFFER = 3 +private var parameterIsShortAndOpeningOrPlaying = -1 + +// Registers used to pass the parameters to the extension. +private var playerResponseMethodCopyRegisters = false +private lateinit var registerVideoId: String +private lateinit var registerProtoBuffer: String +private lateinit var registerIsShortAndOpeningOrPlaying: String + +private lateinit var playerResponseMethod: MutableMethod +private var numberOfInstructionsAdded = 0 + +val playerResponseMethodHookPatch = bytecodePatch { + dependsOn( + sharedExtensionPatch, + versionCheckPatch, + ) + + execute { + playerResponseMethod = if (is_19_23_or_greater) { + parameterIsShortAndOpeningOrPlaying = 12 + + playerParameterBuilderFingerprint + } else { + parameterIsShortAndOpeningOrPlaying = 11 + + playerParameterBuilderLegacyFingerprint + }.method + + // 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 the extension. + playerResponseMethodCopyRegisters = playerResponseMethod.implementation!!.registerCount - + playerResponseMethod.parameterTypes.size + parameterIsShortAndOpeningOrPlaying > 15 + + if (playerResponseMethodCopyRegisters) { + registerVideoId = "v0" + registerProtoBuffer = "v1" + registerIsShortAndOpeningOrPlaying = "v2" + } else { + registerVideoId = "p$PARAMETER_VIDEO_ID" + registerProtoBuffer = "p$PARAMETER_PROTO_BUFFER" + registerIsShortAndOpeningOrPlaying = "p$parameterIsShortAndOpeningOrPlaying" + } + } + + finalize { + fun hookVideoId(hook: Hook) { + playerResponseMethod.addInstruction( + 0, + "invoke-static {$registerVideoId, $registerIsShortAndOpeningOrPlaying}, $hook", + ) + numberOfInstructionsAdded++ + } + + fun hookProtoBufferParameter(hook: Hook) { + playerResponseMethod.addInstructions( + 0, + """ + invoke-static {$registerProtoBuffer, $registerVideoId, $registerIsShortAndOpeningOrPlaying}, $hook + move-result-object $registerProtoBuffer + """, + ) + 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(::hookProtoBufferParameter) + videoIdHooks.forEach(::hookVideoId) + beforeVideoIdHooks.forEach(::hookProtoBufferParameter) + + if (playerResponseMethodCopyRegisters) { + playerResponseMethod.addInstructions( + 0, + """ + move-object/from16 $registerVideoId, p$PARAMETER_VIDEO_ID + move-object/from16 $registerProtoBuffer, p$PARAMETER_PROTO_BUFFER + move/from16 $registerIsShortAndOpeningOrPlaying, p$parameterIsShortAndOpeningOrPlaying + """, + ) + numberOfInstructionsAdded += 3 + + // Move the modified register back. + playerResponseMethod.addInstruction( + numberOfInstructionsAdded, + "move-object/from16 p$PARAMETER_PROTO_BUFFER, $registerProtoBuffer", + ) + } + } +} + +sealed class Hook private constructor(private val methodDescriptor: String) { + class VideoId(methodDescriptor: String) : Hook(methodDescriptor) + + class ProtoBufferParameter(methodDescriptor: String) : Hook(methodDescriptor) + class ProtoBufferParameterBeforeVideoId(methodDescriptor: String) : Hook(methodDescriptor) + + override fun toString() = methodDescriptor +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt new file mode 100644 index 000000000..e33674a38 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/Fingerprints.kt @@ -0,0 +1,37 @@ +package app.revanced.patches.youtube.video.quality + +import app.revanced.patcher.fingerprint +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +/** + * Matches with the class found in [videoQualitySetterFingerprint]. + */ +internal val setQualityByIndexMethodClassFieldReferenceFingerprint = fingerprint { + returns("V") + parameters("L") + opcodes( + Opcode.IGET_OBJECT, + Opcode.IPUT_OBJECT, + Opcode.IGET_OBJECT, + ) +} + +internal val videoQualityItemOnClickParentFingerprint = fingerprint { + returns("V") + strings("VIDEO_QUALITIES_MENU_BOTTOM_SHEET_FRAGMENT") +} + +internal val videoQualitySetterFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("[L", "I", "Z") + opcodes( + Opcode.IF_EQZ, + Opcode.INVOKE_VIRTUAL, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_VIRTUAL, + Opcode.IPUT_BOOLEAN, + ) + strings("menu_item_video_quality") +} diff --git a/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt similarity index 53% rename from src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt rename to patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt index 71849406a..8a2dfe4cc 100644 --- a/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/quality/RememberVideoQualityPatch.kt @@ -1,77 +1,68 @@ package app.revanced.patches.youtube.video.quality -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.BytecodePatch import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.patch.annotation.CompatiblePackage -import app.revanced.patcher.patch.annotation.Patch -import app.revanced.patches.all.misc.resources.AddResourcesPatch +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch import app.revanced.patches.shared.misc.settings.preference.ListPreference import app.revanced.patches.shared.misc.settings.preference.SwitchPreference -import app.revanced.patches.youtube.misc.integrations.IntegrationsPatch -import app.revanced.patches.youtube.misc.settings.SettingsPatch -import app.revanced.patches.youtube.video.information.VideoInformationPatch -import app.revanced.patches.youtube.video.quality.fingerprints.NewVideoQualityChangedFingerprint -import app.revanced.patches.youtube.video.quality.fingerprints.SetQualityByIndexMethodClassFieldReferenceFingerprint -import app.revanced.patches.youtube.video.quality.fingerprints.VideoQualityItemOnClickParentFingerprint -import app.revanced.patches.youtube.video.quality.fingerprints.VideoQualitySetterFingerprint -import app.revanced.util.exception +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.shared.newVideoQualityChangedFingerprint +import app.revanced.patches.youtube.video.information.onCreateHook +import app.revanced.patches.youtube.video.information.videoInformationPatch import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction import com.android.tools.smali.dexlib2.iface.instruction.TwoRegisterInstruction import com.android.tools.smali.dexlib2.iface.reference.FieldReference +import com.sun.org.apache.bcel.internal.generic.InstructionConst.getInstruction -@Patch( +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/playback/quality/RememberVideoQualityPatch;" + +@Suppress("unused") +val rememberVideoQualityPatch = bytecodePatch( name = "Remember video quality", description = "Adds an option to remember the last video quality selected.", - dependencies = [ - IntegrationsPatch::class, - VideoInformationPatch::class, - SettingsPatch::class, - AddResourcesPatch::class - ], - compatiblePackages = [ - CompatiblePackage( - "com.google.android.youtube", [ - "18.38.44", - "18.49.37", - "19.16.39", - "19.25.37", - "19.34.42", - ] - ) - ] -) -@Suppress("unused") -object RememberVideoQualityPatch : BytecodePatch( - setOf( - VideoQualitySetterFingerprint, - VideoQualityItemOnClickParentFingerprint, - NewVideoQualityChangedFingerprint - ) ) { - private const val INTEGRATIONS_CLASS_DESCRIPTOR = - "Lapp/revanced/integrations/youtube/patches/playback/quality/RememberVideoQualityPatch;" + dependsOn( + sharedExtensionPatch, + videoInformationPatch, + settingsPatch, + addResourcesPatch, + ) - override fun execute(context: BytecodeContext) { - AddResourcesPatch(this::class) + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) - SettingsPatch.PreferenceScreen.VIDEO.addPreferences( + execute { + addResources("youtube", "video.quality.rememberVideoQualityPatch") + + PreferenceScreen.VIDEO.addPreferences( SwitchPreference("revanced_remember_video_quality_last_selected"), ListPreference( key = "revanced_video_quality_default_wifi", summaryKey = null, entriesKey = "revanced_video_quality_default_entries", - entryValuesKey = "revanced_video_quality_default_entry_values" + entryValuesKey = "revanced_video_quality_default_entry_values", ), ListPreference( key = "revanced_video_quality_default_mobile", summaryKey = null, entriesKey = "revanced_video_quality_default_entries", - entryValuesKey = "revanced_video_quality_default_entry_values" - ) + entryValuesKey = "revanced_video_quality_default_entry_values", + ), ) /* @@ -81,17 +72,14 @@ object RememberVideoQualityPatch : BytecodePatch( * It also hooks the method which is called when the video quality to set is determined. * Conveniently, at this point the video quality is overridden to the remembered playback speed. */ - VideoInformationPatch.onCreateHook(INTEGRATIONS_CLASS_DESCRIPTOR, "newVideoStarted") - + onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "newVideoStarted") // Inject a call to set the remembered quality once a video loads. - VideoQualitySetterFingerprint.result?.also { - if (!SetQualityByIndexMethodClassFieldReferenceFingerprint.resolve(context, it.classDef)) - throw PatchException("Could not resolve fingerprint to find setQualityByIndex method") - }?.let { + setQualityByIndexMethodClassFieldReferenceFingerprint.match( + videoQualitySetterFingerprint.originalClassDef, + ).let { match -> // This instruction refers to the field with the type that contains the setQualityByIndex method. - val instructions = SetQualityByIndexMethodClassFieldReferenceFingerprint.result!! - .method.implementation!!.instructions + val instructions = match.method.implementation!!.instructions val getOnItemClickListenerClassReference = (instructions.elementAt(0) as ReferenceInstruction).reference @@ -101,7 +89,7 @@ object RememberVideoQualityPatch : BytecodePatch( val setQualityByIndexMethodClassFieldReference = getSetQualityByIndexMethodClassFieldReference as FieldReference - val setQualityByIndexMethodClass = context.classes + val setQualityByIndexMethodClass = classes .find { classDef -> classDef.type == setQualityByIndexMethodClassFieldReference.type }!! // Get the name of the setQualityByIndex method. @@ -109,7 +97,7 @@ object RememberVideoQualityPatch : BytecodePatch( .find { method -> method.parameterTypes.first() == "I" } ?: throw PatchException("Could not find setQualityByIndex method") - it.mutableMethod.addInstructions( + videoQualitySetterFingerprint.method.addInstructions( 0, """ # Get the object instance to invoke the setQualityByIndex method on. @@ -124,39 +112,34 @@ object RememberVideoQualityPatch : BytecodePatch( # The second parameter is the index of the selected quality. # The register v0 stores the object instance to invoke the setQualityByIndex method on. # The register v1 stores the name of the setQualityByIndex method. - invoke-static {p1, p2, v0, v1}, $INTEGRATIONS_CLASS_DESCRIPTOR->setVideoQuality([Ljava/lang/Object;ILjava/lang/Object;Ljava/lang/String;)I + invoke-static { p1, p2, v0, v1 }, $EXTENSION_CLASS_DESCRIPTOR->setVideoQuality([Ljava/lang/Object;ILjava/lang/Object;Ljava/lang/String;)I move-result p2 """, ) - } ?: throw VideoQualitySetterFingerprint.exception - + } // Inject a call to remember the selected quality. - VideoQualityItemOnClickParentFingerprint.result?.let { - val onItemClickMethod = it.mutableClass.methods.find { method -> method.name == "onItemClick" } - - onItemClickMethod?.apply { + videoQualityItemOnClickParentFingerprint.classDef.methods.find { it.name == "onItemClick" } + ?.apply { val listItemIndexParameter = 3 addInstruction( 0, - "invoke-static {p$listItemIndexParameter}, $INTEGRATIONS_CLASS_DESCRIPTOR->userChangedQuality(I)V" + "invoke-static { p$listItemIndexParameter }, " + + "$EXTENSION_CLASS_DESCRIPTOR->userChangedQuality(I)V", ) } ?: throw PatchException("Failed to find onItemClick method") - } ?: throw VideoQualityItemOnClickParentFingerprint.exception - // Remember video quality if not using old layout menu. - NewVideoQualityChangedFingerprint.result?.apply { - mutableMethod.apply { - val index = scanResult.patternScanResult!!.startIndex - val qualityRegister = getInstruction(index).registerA + newVideoQualityChangedFingerprint.method.apply { + val index = newVideoQualityChangedFingerprint.patternMatch!!.startIndex + val qualityRegister = getInstruction(index).registerA - addInstruction( - index + 1, - "invoke-static {v$qualityRegister}, $INTEGRATIONS_CLASS_DESCRIPTOR->userChangedQualityInNewFlyout(I)V" - ) - } - } ?: throw NewVideoQualityChangedFingerprint.exception + addInstruction( + index + 1, + "invoke-static { v$qualityRegister }, " + + "$EXTENSION_CLASS_DESCRIPTOR->userChangedQualityInNewFlyout(I)V", + ) + } } } diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt new file mode 100644 index 000000000..42caa99ad --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/PlaybackSpeedPatch.kt @@ -0,0 +1,30 @@ +package app.revanced.patches.youtube.video.speed + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.youtube.video.speed.button.playbackSpeedButtonPatch +import app.revanced.patches.youtube.video.speed.custom.customPlaybackSpeedPatch +import app.revanced.patches.youtube.video.speed.remember.rememberPlaybackSpeedPatch + +@Suppress("unused") +val playbackSpeedPatch = bytecodePatch( + name = "Playback speed", + description = "Adds options to customize available playback speeds, remember the last playback speed selected " + + "and show a speed dialog button to the video player.", +) { + dependsOn( + playbackSpeedButtonPatch, + customPlaybackSpeedPatch, + rememberPlaybackSpeedPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/button/PlaybackSpeedButtonPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/button/PlaybackSpeedButtonPatch.kt new file mode 100644 index 000000000..1acc3ca74 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/button/PlaybackSpeedButtonPatch.kt @@ -0,0 +1,55 @@ +package app.revanced.patches.youtube.video.speed.button + +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.playercontrols.* +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.video.speed.custom.customPlaybackSpeedPatch +import app.revanced.util.ResourceGroup +import app.revanced.util.copyResources + +private val playbackSpeedButtonResourcePatch = resourcePatch { + dependsOn(playerControlsResourcePatch) + + execute { + copyResources( + "speedbutton", + ResourceGroup( + "drawable", + "revanced_playback_speed_dialog_button.xml", + ), + ) + + addBottomControl("speedbutton") + } +} + +private const val SPEED_BUTTON_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/videoplayer/PlaybackSpeedDialogButton;" + +val playbackSpeedButtonPatch = bytecodePatch( + description = "Adds the option to display playback speed dialog button in the video player.", +) { + dependsOn( + playbackSpeedButtonResourcePatch, + customPlaybackSpeedPatch, + playerControlsPatch, + settingsPatch, + addResourcesPatch, + ) + + execute { + addResources("youtube", "video.speed.button.playbackSpeedButtonPatch") + + PreferenceScreen.PLAYER.addPreferences( + SwitchPreference("revanced_playback_speed_dialog_button"), + ) + + initializeBottomControl(SPEED_BUTTON_CLASS_DESCRIPTOR) + injectVisibilityCheckCall(SPEED_BUTTON_CLASS_DESCRIPTOR) + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt new file mode 100644 index 000000000..bd6668993 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/CustomPlaybackSpeedPatch.kt @@ -0,0 +1,170 @@ +package app.revanced.patches.youtube.video.speed.custom + +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.replaceInstruction +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.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.shared.misc.settings.preference.InputType +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.shared.misc.settings.preference.TextPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter +import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch +import app.revanced.patches.youtube.misc.recyclerviewtree.hook.addRecyclerViewTreeHook +import app.revanced.patches.youtube.misc.recyclerviewtree.hook.recyclerViewTreeHookPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.util.* +import com.android.tools.smali.dexlib2.AccessFlags +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 +import com.android.tools.smali.dexlib2.immutable.ImmutableField + +var speedUnavailableId = -1L + internal set + +private val customPlaybackSpeedResourcePatch = resourcePatch { + dependsOn(resourceMappingPatch) + + execute { + speedUnavailableId = resourceMappings[ + "string", + "varispeed_unavailable_message", + ] + } +} + +private const val FILTER_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/components/PlaybackSpeedMenuFilterPatch;" + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/playback/speed/CustomPlaybackSpeedPatch;" + +internal val customPlaybackSpeedPatch = bytecodePatch( + description = "Adds custom playback speed options.", +) { + dependsOn( + sharedExtensionPatch, + lithoFilterPatch, + settingsPatch, + recyclerViewTreeHookPatch, + customPlaybackSpeedResourcePatch, + addResourcesPatch, + ) + + execute { + addResources("youtube", "video.speed.custom.customPlaybackSpeedPatch") + + PreferenceScreen.VIDEO.addPreferences( + SwitchPreference("revanced_custom_speed_menu"), + TextPreference("revanced_custom_playback_speeds", inputType = InputType.TEXT_MULTI_LINE), + ) + + // Replace the speeds float array with custom speeds. + speedArrayGeneratorFingerprint.method.apply { + val sizeCallIndex = indexOfFirstInstructionOrThrow { getReference()?.name == "size" } + val sizeCallResultRegister = getInstruction(sizeCallIndex + 1).registerA + + replaceInstruction(sizeCallIndex + 1, "const/4 v$sizeCallResultRegister, 0x0") + + val arrayLengthConstIndex = indexOfFirstLiteralInstructionOrThrow(7) + val arrayLengthConstDestination = getInstruction(arrayLengthConstIndex).registerA + val playbackSpeedsArrayType = "$EXTENSION_CLASS_DESCRIPTOR->customPlaybackSpeeds:[F" + + addInstructions( + arrayLengthConstIndex + 1, + """ + sget-object v$arrayLengthConstDestination, $playbackSpeedsArrayType + array-length v$arrayLengthConstDestination, v$arrayLengthConstDestination + """, + ) + + val originalArrayFetchIndex = indexOfFirstInstructionOrThrow { + val reference = getReference() + reference?.type == "[F" && reference.definingClass.endsWith("/PlayerConfigModel;") + } + val originalArrayFetchDestination = + getInstruction(originalArrayFetchIndex).registerA + + replaceInstruction( + originalArrayFetchIndex, + "sget-object v$originalArrayFetchDestination, $playbackSpeedsArrayType", + ) + } + + // Override the min/max speeds that can be used. + speedLimiterFingerprint.method.apply { + val limitMinIndex = indexOfFirstLiteralInstructionOrThrow(0.25f.toRawBits().toLong()) + var limitMaxIndex = indexOfFirstLiteralInstruction(2.0f.toRawBits().toLong()) + // Newer targets have 4x max speed. + if (limitMaxIndex < 0) { + limitMaxIndex = indexOfFirstLiteralInstructionOrThrow(4.0f.toRawBits().toLong()) + } + + val limitMinRegister = getInstruction(limitMinIndex).registerA + val limitMaxRegister = getInstruction(limitMaxIndex).registerA + + replaceInstruction(limitMinIndex, "const/high16 v$limitMinRegister, 0.0f") + replaceInstruction(limitMaxIndex, "const/high16 v$limitMaxRegister, 8.0f") + } + + // Add a static INSTANCE field to the class. + // This is later used to call "showOldPlaybackSpeedMenu" on the instance. + + val instanceField = ImmutableField( + getOldPlaybackSpeedsFingerprint.originalClassDef.type, + "INSTANCE", + getOldPlaybackSpeedsFingerprint.originalClassDef.type, + AccessFlags.PUBLIC.value or AccessFlags.STATIC.value, + null, + null, + null, + ).toMutable() + + getOldPlaybackSpeedsFingerprint.classDef.staticFields.add(instanceField) + // Set the INSTANCE field to the instance of the class. + // In order to prevent a conflict with another patch, add the instruction at index 1. + getOldPlaybackSpeedsFingerprint.method.addInstruction(1, "sput-object p0, $instanceField") + + // Get the "showOldPlaybackSpeedMenu" method. + // This is later called on the field INSTANCE. + val showOldPlaybackSpeedMenuMethod = showOldPlaybackSpeedMenuFingerprint.match( + getOldPlaybackSpeedsFingerprint.classDef, + ).method.toString() + + // Insert the call to the "showOldPlaybackSpeedMenu" method on the field INSTANCE. + showOldPlaybackSpeedMenuExtensionFingerprint.method.apply { + addInstructionsWithLabels( + instructions.lastIndex, + """ + sget-object v0, $instanceField + if-nez v0, :not_null + return-void + :not_null + invoke-virtual { v0 }, $showOldPlaybackSpeedMenuMethod + """, + ) + } + + // region Force old video quality menu. + // This is necessary, because there is no known way of adding custom playback speeds to the new menu. + + addRecyclerViewTreeHook(EXTENSION_CLASS_DESCRIPTOR) + + // Required to check if the playback speed menu is currently shown. + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + + // endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt new file mode 100644 index 000000000..d3aa805d8 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/custom/Fingerprints.kt @@ -0,0 +1,50 @@ +package app.revanced.patches.youtube.video.speed.custom + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val getOldPlaybackSpeedsFingerprint = fingerprint { + parameters("[L", "I") + strings("menu_item_playback_speed") +} + +internal val showOldPlaybackSpeedMenuFingerprint = fingerprint { + literal { speedUnavailableId } +} + +internal val showOldPlaybackSpeedMenuExtensionFingerprint = fingerprint { + custom { method, _ -> method.name == "showOldPlaybackSpeedMenu" } +} + +internal val speedArrayGeneratorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC) + returns("[L") + parameters("Lcom/google/android/libraries/youtube/innertube/model/player/PlayerResponseModel;") + opcodes( + Opcode.IF_NEZ, + Opcode.SGET_OBJECT, + Opcode.GOTO_16, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.IGET_OBJECT, + ) + strings("0.0#") +} + +internal val speedLimiterFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("F") + opcodes( + Opcode.INVOKE_STATIC, + Opcode.MOVE_RESULT, + Opcode.IF_EQZ, + Opcode.CONST_HIGH16, + Opcode.GOTO, + Opcode.CONST_HIGH16, + Opcode.CONST_HIGH16, + Opcode.INVOKE_STATIC, + ) +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/Fingerprints.kt new file mode 100644 index 000000000..3924588b4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/Fingerprints.kt @@ -0,0 +1,8 @@ +package app.revanced.patches.youtube.video.speed.remember + +import app.revanced.patcher.fingerprint + +internal val initializePlaybackSpeedValuesFingerprint = fingerprint { + parameters("[L", "I") + strings("menu_item_playback_speed") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt new file mode 100644 index 000000000..3c51559ea --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/speed/remember/RememberPlaybackSpeedPatch.kt @@ -0,0 +1,85 @@ +package app.revanced.patches.youtube.video.speed.remember + +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.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.settings.preference.ListPreference +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import app.revanced.patches.youtube.video.information.* +import app.revanced.patches.youtube.video.speed.custom.customPlaybackSpeedPatch +import com.android.tools.smali.dexlib2.iface.instruction.ReferenceInstruction + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/playback/speed/RememberPlaybackSpeedPatch;" + +internal val rememberPlaybackSpeedPatch = bytecodePatch { + dependsOn( + sharedExtensionPatch, + settingsPatch, + videoInformationPatch, + customPlaybackSpeedPatch, + addResourcesPatch, + ) + + execute { + addResources("youtube", "video.speed.remember.rememberPlaybackSpeedPatch") + + PreferenceScreen.VIDEO.addPreferences( + SwitchPreference("revanced_remember_playback_speed_last_selected"), + ListPreference( + key = "revanced_playback_speed_default", + summaryKey = null, + // Entries and values are set by the extension code based on the actual speeds available. + entriesKey = null, + entryValuesKey = null, + ), + ) + + onCreateHook(EXTENSION_CLASS_DESCRIPTOR, "newVideoStarted") + userSelectedPlaybackSpeedHook( + EXTENSION_CLASS_DESCRIPTOR, + "userSelectedPlaybackSpeed", + ) + + /* + * Hook the code that is called when the playback speeds are initialized, and sets the playback speed + */ + initializePlaybackSpeedValuesFingerprint.method.apply { + // Infer everything necessary for calling the method setPlaybackSpeed(). + val onItemClickListenerClassFieldReference = getInstruction(0).reference + + // Registers are not used at index 0, so they can be freely used. + addInstructionsWithLabels( + 0, + """ + invoke-static { }, $EXTENSION_CLASS_DESCRIPTOR->getPlaybackSpeedOverride()F + move-result v0 + + # Check if the playback speed is not 1.0x. + const/high16 v1, 1.0f + cmpg-float v1, v0, v1 + if-eqz v1, :do_not_override + + # Get the instance of the class which has the container class field below. + iget-object v1, p0, $onItemClickListenerClassFieldReference + + # Get the container class field. + iget-object v1, v1, $setPlaybackSpeedContainerClassFieldReference + + # Get the field from its class. + iget-object v2, v1, $setPlaybackSpeedClassFieldReference + + # Invoke setPlaybackSpeed on that class. + invoke-virtual {v2, v0}, $setPlaybackSpeedMethodReference + """, + ExternalLabel("do_not_override", getInstruction(0)), + ) + } + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/Fingerprints.kt new file mode 100644 index 000000000..6d4a3c6cb --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/Fingerprints.kt @@ -0,0 +1,55 @@ +package app.revanced.patches.youtube.video.videoid + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val videoIdFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("V") + parameters("L") + opcodes( + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + Opcode.INVOKE_INTERFACE, + Opcode.MOVE_RESULT_OBJECT, + ) + custom { method, _ -> + method.indexOfPlayerResponseModelString() >= 0 + } +} + +internal val videoIdBackgroundPlayFingerprint = fingerprint { + accessFlags(AccessFlags.DECLARED_SYNCHRONIZED, AccessFlags.FINAL, AccessFlags.PUBLIC) + returns("V") + parameters("L") + opcodes( + 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 + ) + // The target snippet of code is buried in a huge switch block and the target method + // has been changed many times by YT which makes identifying it more difficult than usual. + custom { method, classDef -> + // Access flags changed in 19.36 + AccessFlags.FINAL.isSet(method.accessFlags) && + AccessFlags.DECLARED_SYNCHRONIZED.isSet(method.accessFlags) && + classDef.methods.count() == 17 && + method.implementation != null && + method.indexOfPlayerResponseModelString() >= 0 + } + +} + +internal val videoIdParentFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("[L") + parameters("L") + literal { 524288L } +} 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..6d69381cd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoid/VideoIdPatch.kt @@ -0,0 +1,119 @@ +package app.revanced.patches.youtube.video.videoid + +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.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.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.getReference +import app.revanced.util.indexOfFirstInstruction +import com.android.tools.smali.dexlib2.iface.Method +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction +import com.android.tools.smali.dexlib2.iface.reference.MethodReference + +/** + * Hooks the new video id when the video changes. + * + * Supports all videos (regular videos and Shorts). + * + * _Does not function if playing in the background with no video visible_. + * + * Be aware, this can be called multiple times for the same video id. + * + * @param methodDescriptor which method to call. Params have to be `Ljava/lang/String;` + */ +fun hookVideoId( + methodDescriptor: String, +) = videoIdMethod.addInstruction( + videoIdInsertIndex++, + "invoke-static {v$videoIdRegister}, $methodDescriptor", +) + +/** + * Alternate hook that supports only regular videos, but hook supports changing to new video + * during background play when no video is visible. + * + * _Does not support Shorts_. + * + * Be aware, the hook can be called multiple times for the same video id. + * + * @param methodDescriptor which method to call. Params have to be `Ljava/lang/String;` + */ +fun hookBackgroundPlayVideoId( + methodDescriptor: String, +) = backgroundPlaybackMethod.addInstruction( + backgroundPlaybackInsertIndex++, // move-result-object offset + "invoke-static {v$backgroundPlaybackVideoIdRegister}, $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] or [hookBackgroundPlayVideoId] instead. + * + * Be aware, this can be called multiple times for the same video id. + * + * @param methodDescriptor which method to call. Params must be `Ljava/lang/String;Z` + */ +fun hookPlayerResponseVideoId(methodDescriptor: String) = addPlayerResponseMethodHook( + Hook.VideoId( + methodDescriptor, + ), +) + +private var videoIdRegister = 0 +private var videoIdInsertIndex = 0 +private lateinit var videoIdMethod: MutableMethod + +private var backgroundPlaybackVideoIdRegister = 0 +private var backgroundPlaybackInsertIndex = 0 +private lateinit var backgroundPlaybackMethod: MutableMethod + +val videoIdPatch = bytecodePatch( + description = "Hooks to detect when the video id changes.", +) { + dependsOn( + sharedExtensionPatch, + playerResponseMethodHookPatch, + ) + + execute { + videoIdFingerprint.match(videoIdParentFingerprint.originalClassDef).method.apply { + videoIdMethod = this + val index = indexOfPlayerResponseModelString() + videoIdRegister = getInstruction(index + 1).registerA + videoIdInsertIndex = index + 2 + } + + videoIdBackgroundPlayFingerprint.method.apply { + backgroundPlaybackMethod = this + val index = indexOfPlayerResponseModelString() + backgroundPlaybackVideoIdRegister = getInstruction(index + 1).registerA + backgroundPlaybackInsertIndex = index + 2 + } + } +} + +internal fun Method.indexOfPlayerResponseModelString() = indexOfFirstInstruction { + val reference = getReference() + reference?.definingClass == "Lcom/google/android/libraries/youtube/innertube/model/player/PlayerResponseModel;" && + reference.returnType == "Ljava/lang/String;" +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoqualitymenu/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoqualitymenu/Fingerprints.kt new file mode 100644 index 000000000..7f5469da3 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoqualitymenu/Fingerprints.kt @@ -0,0 +1,43 @@ +package app.revanced.patches.youtube.video.videoqualitymenu + +import app.revanced.patcher.fingerprint +import app.revanced.util.literal +import com.android.tools.smali.dexlib2.AccessFlags +import com.android.tools.smali.dexlib2.Opcode + +internal val videoQualityMenuOptionsFingerprint = fingerprint { + accessFlags(AccessFlags.STATIC) + returns("[L") + parameters("Landroid/content/Context", "L", "L") + opcodes( + Opcode.CONST_4, // First instruction of method. + Opcode.CONST_4, + Opcode.IF_EQZ, + Opcode.IGET_BOOLEAN, // Use the quality menu, that contains the advanced menu. + Opcode.IF_NEZ, + ) + literal { videoQualityQuickMenuAdvancedMenuDescription } +} + +internal val videoQualityMenuViewInflateFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("L") + parameters("L", "L", "L") + opcodes( + 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, + ) + literal { videoQualityBottomSheetListFragmentTitle } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoqualitymenu/RestoreOldVideoQualityMenuPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoqualitymenu/RestoreOldVideoQualityMenuPatch.kt new file mode 100644 index 000000000..886db27df --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/video/videoqualitymenu/RestoreOldVideoQualityMenuPatch.kt @@ -0,0 +1,134 @@ +package app.revanced.patches.youtube.video.videoqualitymenu + +import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.extensions.InstructionExtensions.getInstruction +import app.revanced.patcher.patch.PatchException +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patcher.patch.resourcePatch +import app.revanced.patches.all.misc.resources.addResources +import app.revanced.patches.all.misc.resources.addResourcesPatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings +import app.revanced.patches.shared.misc.settings.preference.SwitchPreference +import app.revanced.patches.youtube.misc.extension.sharedExtensionPatch +import app.revanced.patches.youtube.misc.litho.filter.addLithoFilter +import app.revanced.patches.youtube.misc.litho.filter.lithoFilterPatch +import app.revanced.patches.youtube.misc.recyclerviewtree.hook.addRecyclerViewTreeHook +import app.revanced.patches.youtube.misc.recyclerviewtree.hook.recyclerViewTreeHookPatch +import app.revanced.patches.youtube.misc.settings.PreferenceScreen +import app.revanced.patches.youtube.misc.settings.settingsPatch +import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction + +internal var videoQualityBottomSheetListFragmentTitle = -1L + private set +internal var videoQualityQuickMenuAdvancedMenuDescription = -1L + private set + +private val restoreOldVideoQualityMenuResourcePatch = resourcePatch { + dependsOn( + settingsPatch, + resourceMappingPatch, + addResourcesPatch, + ) + + execute { + addResources("youtube", "video.videoqualitymenu.restoreOldVideoQualityMenuResourcePatch") + + PreferenceScreen.VIDEO.addPreferences( + SwitchPreference("revanced_restore_old_video_quality_menu"), + ) + + // Used for the old type of the video quality menu. + videoQualityBottomSheetListFragmentTitle = resourceMappings[ + "layout", + "video_quality_bottom_sheet_list_fragment_title", + ] + + videoQualityQuickMenuAdvancedMenuDescription = resourceMappings[ + "string", + "video_quality_quick_menu_advanced_menu_description", + ] + } +} + +private const val FILTER_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/components/VideoQualityMenuFilterPatch;" + +private const val EXTENSION_CLASS_DESCRIPTOR = + "Lapp/revanced/extension/youtube/patches/playback/quality/RestoreOldVideoQualityMenuPatch;" + +@Suppress("unused") +val restoreOldVideoQualityMenuPatch = bytecodePatch( + name = "Restore old video quality menu", + description = "Adds an option to restore the old video quality menu with specific video resolution options.", + +) { + dependsOn( + sharedExtensionPatch, + restoreOldVideoQualityMenuResourcePatch, + lithoFilterPatch, + recyclerViewTreeHookPatch, + ) + + compatibleWith( + "com.google.android.youtube"( + "18.38.44", + "18.49.37", + "19.16.39", + "19.25.37", + "19.34.42", + "19.43.41", + ), + ) + + execute { + // region Patch for the old type of the video quality menu. + // Used for regular videos when spoofing to old app version, + // and for the Shorts quality flyout on newer app versions. + + videoQualityMenuViewInflateFingerprint.method.apply { + val checkCastIndex = videoQualityMenuViewInflateFingerprint.patternMatch!!.endIndex + val listViewRegister = getInstruction(checkCastIndex).registerA + + addInstruction( + checkCastIndex + 1, + "invoke-static { v$listViewRegister }, " + + "$EXTENSION_CLASS_DESCRIPTOR->" + + "showOldVideoQualityMenu(Landroid/widget/ListView;)V", + ) + } + + // Force YT to add the 'advanced' quality menu for Shorts. + val patternMatch = videoQualityMenuOptionsFingerprint.patternMatch!! + val startIndex = patternMatch.startIndex + if (startIndex != 0) throw PatchException("Unexpected opcode start index: $startIndex") + val insertIndex = patternMatch.endIndex + + videoQualityMenuOptionsFingerprint.method.apply { + val register = getInstruction(insertIndex).registerA + + // A condition controls whether to show the three or four items quality menu. + // Force the four items quality menu to make the "Advanced" item visible, necessary for the patch. + addInstructions( + insertIndex, + """ + invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->forceAdvancedVideoQualityMenuCreation(Z)Z + move-result v$register + """, + ) + } + + // endregion + + // region Patch for the new type of the video quality menu. + + addRecyclerViewTreeHook(EXTENSION_CLASS_DESCRIPTOR) + + // Required to check if the video quality menu is currently shown in order to click on the "Advanced" item. + addLithoFilter(FILTER_CLASS_DESCRIPTOR) + + // endregion + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/yuka/misc/unlockpremium/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/yuka/misc/unlockpremium/Fingerprints.kt new file mode 100644 index 000000000..af38f28f4 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/yuka/misc/unlockpremium/Fingerprints.kt @@ -0,0 +1,20 @@ +package app.revanced.patches.yuka.misc.unlockpremium + +import com.android.tools.smali.dexlib2.Opcode +import com.android.tools.smali.dexlib2.AccessFlags +import app.revanced.patcher.fingerprint + +internal val isPremiumFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.FINAL) + returns("Z") + opcodes( + Opcode.IGET_BOOLEAN, + Opcode.RETURN, + ) +} + +internal val yukaUserConstructorFingerprint = fingerprint { + accessFlags(AccessFlags.PUBLIC, AccessFlags.CONSTRUCTOR) + returns("V") + strings("premiumProvider") +} diff --git a/patches/src/main/kotlin/app/revanced/patches/yuka/misc/unlockpremium/UnlockPremiumPatch.kt b/patches/src/main/kotlin/app/revanced/patches/yuka/misc/unlockpremium/UnlockPremiumPatch.kt new file mode 100644 index 000000000..c6689540d --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/yuka/misc/unlockpremium/UnlockPremiumPatch.kt @@ -0,0 +1,23 @@ +package app.revanced.patches.yuka.misc.unlockpremium + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch + +@Suppress("unused") +val unlockPremiumPatch = bytecodePatch( + name = "Unlock premium", +) { + compatibleWith("io.yuka.android"("4.29")) + + execute { + isPremiumFingerprint.match( + yukaUserConstructorFingerprint.originalClassDef, + ).method.addInstructions( + 0, + """ + const/4 v0, 0x1 + return v0 + """, + ) + } +} diff --git a/src/main/kotlin/app/revanced/util/BytecodeUtils.kt b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt similarity index 65% rename from src/main/kotlin/app/revanced/util/BytecodeUtils.kt rename to patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt index c9b146645..db7d2664b 100644 --- a/src/main/kotlin/app/revanced/util/BytecodeUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/BytecodeUtils.kt @@ -1,16 +1,19 @@ package app.revanced.util -import app.revanced.patcher.data.BytecodeContext +import app.revanced.patcher.FingerprintBuilder 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.fingerprint.MethodFingerprint +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.patches.shared.misc.mapping.ResourceMappingPatch +import app.revanced.patches.shared.misc.mapping.get +import app.revanced.patches.shared.misc.mapping.resourceMappingPatch +import app.revanced.patches.shared.misc.mapping.resourceMappings import com.android.tools.smali.dexlib2.Opcode import com.android.tools.smali.dexlib2.iface.Method import com.android.tools.smali.dexlib2.iface.instruction.Instruction @@ -19,16 +22,6 @@ import com.android.tools.smali.dexlib2.iface.instruction.WideLiteralInstruction import com.android.tools.smali.dexlib2.iface.reference.Reference import com.android.tools.smali.dexlib2.util.MethodUtil -fun MethodFingerprint.resultOrThrow() = result ?: throw exception - -/** - * The [PatchException] of failing to resolve a [MethodFingerprint]. - * - * @return The [PatchException]. - */ -val MethodFingerprint.exception - get() = PatchException("Failed to resolve ${this.javaClass.simpleName}") - /** * Find the [MutableMethod] from a given [Method] in a [MutableClass]. * @@ -100,30 +93,30 @@ internal fun MutableMethod.addInstructionsAtControlFlowLabel( } /** - * Get the index of the first instruction with the id of the given resource name. + * Get the index of the first instruction with the id of the given resource id name. * - * Requires [ResourceMappingPatch] as a dependency. + * Requires [resourceMappingPatch] as a dependency. * * @param resourceName the name of the resource to find the id for. * @return the index of the first instruction with the id of the given resource name, or -1 if not found. * @throws PatchException if the resource cannot be found. - * @see [indexOfIdResourceOrThrow], [indexOfFirstWideLiteralInstructionValueReversed] + * @see [indexOfFirstResourceIdOrThrow], [indexOfFirstLiteralInstructionReversed] */ -fun Method.indexOfIdResource(resourceName: String): Int { - val resourceId = ResourceMappingPatch["id", resourceName] - return indexOfFirstWideLiteralInstructionValue(resourceId) +fun Method.indexOfFirstResourceId(resourceName: String): Int { + val resourceId = resourceMappings["id", resourceName] + return indexOfFirstLiteralInstruction(resourceId) } /** * Get the index of the first instruction with the id of the given resource name or throw a [PatchException]. * - * Requires [ResourceMappingPatch] as a dependency. + * Requires [resourceMappingPatch] as a dependency. * * @throws [PatchException] if the resource is not found, or the method does not contain the resource id literal value. - * @see [indexOfIdResource], [indexOfFirstWideLiteralInstructionValueReversedOrThrow] + * @see [indexOfFirstResourceId], [indexOfFirstLiteralInstructionReversedOrThrow] */ -fun Method.indexOfIdResourceOrThrow(resourceName: String): Int { - val index = indexOfIdResource(resourceName) +fun Method.indexOfFirstResourceIdOrThrow(resourceName: String): Int { + val index = indexOfFirstResourceId(resourceName) if (index < 0) { throw PatchException("Found resource id for: '$resourceName' but method does not contain the id: $this") } @@ -131,40 +124,37 @@ fun Method.indexOfIdResourceOrThrow(resourceName: String): Int { return index } -// TODO Rename these from 'FirstWideLiteralInstruction' to 'FirstLiteralInstruction', -// since NarrowLiteralInstruction is a subclass of WideLiteralInstruction. - /** - * 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) +fun Method.indexOfFirstLiteralInstructionOrThrow(literal: Long): Int { + val index = indexOfFirstLiteralInstruction(literal) if (index < 0) throw PatchException("Could not find literal value: $literal") return index } /** - * Find the index of the last wide literal instruction with the given value. + * 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 indexOfFirstWideLiteralInstructionValueOrThrow + * @see indexOfFirstLiteralInstructionOrThrow */ -fun Method.indexOfFirstWideLiteralInstructionValueReversed(literal: Long) = implementation?.let { +fun Method.indexOfFirstLiteralInstructionReversed(literal: Long) = implementation?.let { it.instructions.indexOfLast { instruction -> (instruction as? WideLiteralInstruction)?.wideLiteral == literal } @@ -176,8 +166,8 @@ fun Method.indexOfFirstWideLiteralInstructionValueReversed(literal: Long) = impl * * @return the last literal instruction with the value, or throws [PatchException] if not found. */ -fun Method.indexOfFirstWideLiteralInstructionValueReversedOrThrow(literal: Long): Int { - val index = indexOfFirstWideLiteralInstructionValueReversed(literal) +fun Method.indexOfFirstLiteralInstructionReversedOrThrow(literal: Long): Int { + val index = indexOfFirstLiteralInstructionReversed(literal) if (index < 0) throw PatchException("Could not find literal value: $literal") return index } @@ -187,8 +177,8 @@ fun Method.indexOfFirstWideLiteralInstructionValueReversedOrThrow(literal: Long) * * @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. @@ -196,9 +186,12 @@ 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(targetClass: MutableClass, callback: MutableClass.() -> Unit) { +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) } } @@ -211,19 +204,8 @@ fun BytecodeContext.traverseClassHierarchy(targetClass: MutableClass, callback: * if the [Instruction] is not a [ReferenceInstruction] or the [Reference] is not of type [T]. * @see ReferenceInstruction */ -inline fun Instruction.getReference() = (this as? ReferenceInstruction)?.reference as? T - -/** - * Get the index of the first [Instruction] that matches the predicate. - * - * @param predicate The predicate to match. - * @return The index of the first [Instruction] that matches the predicate. - */ -// TODO: delete this on next major release, the overloaded method with an optional start index serves the same purposes. -// Method is deprecated, but annotation is commented out otherwise during compilation usage of the replacement is -// incorrectly flagged as deprecated. -// @Deprecated("Use the overloaded method with an optional start index.", ReplaceWith("indexOfFirstInstruction(predicate)")) -fun Method.indexOfFirstInstruction(predicate: Instruction.() -> Boolean) = indexOfFirstInstruction(0, predicate) +inline fun Instruction.getReference() = + (this as? ReferenceInstruction)?.reference as? T /** * @return The index of the first opcode specified, or -1 if not found. @@ -249,12 +231,12 @@ fun Method.indexOfFirstInstruction(startIndex: Int = 0, targetOpcode: Opcode): I * @return -1 if the instruction is not found. * @see indexOfFirstInstructionOrThrow */ -fun Method.indexOfFirstInstruction(startIndex: Int = 0, predicate: Instruction.() -> Boolean): Int { +fun Method.indexOfFirstInstruction(startIndex: Int = 0, filter: Instruction.() -> Boolean): Int { var instructions = this.implementation!!.instructions if (startIndex != 0) { instructions = instructions.drop(startIndex) } - val index = instructions.indexOfFirst(predicate) + val index = instructions.indexOfFirst(filter) return if (index >= 0) { startIndex + index @@ -288,8 +270,8 @@ fun Method.indexOfFirstInstructionOrThrow(startIndex: Int = 0, targetOpcode: Opc * @throws PatchException * @see indexOfFirstInstruction */ -fun Method.indexOfFirstInstructionOrThrow(startIndex: Int = 0, predicate: Instruction.() -> Boolean): Int { - val index = indexOfFirstInstruction(startIndex, predicate) +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") } @@ -318,13 +300,13 @@ fun Method.indexOfFirstInstructionReversed(startIndex: Int? = null, targetOpcode * @return -1 if the instruction is not found. * @see indexOfFirstInstructionReversedOrThrow */ -fun Method.indexOfFirstInstructionReversed(startIndex: Int? = null, predicate: Instruction.() -> Boolean): Int { +fun Method.indexOfFirstInstructionReversed(startIndex: Int? = null, filter: Instruction.() -> Boolean): Int { var instructions = this.implementation!!.instructions if (startIndex != null) { instructions = instructions.take(startIndex + 1) } - return instructions.indexOfLast(predicate) + return instructions.indexOfLast(filter) } /** @@ -348,8 +330,8 @@ fun Method.indexOfFirstInstructionReversedOrThrow(startIndex: Int? = null, targe * @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) +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") @@ -359,30 +341,50 @@ fun Method.indexOfFirstInstructionReversedOrThrow(startIndex: Int? = null, predi } /** - * @return The list of indices of the opcode in reverse order. + * @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.findOpcodeIndicesReversed(opcode: Opcode): List = - findOpcodeIndicesReversed { this.opcode == opcode } +fun Method.findInstructionIndicesReversed(filter: Instruction.() -> Boolean): List = instructions + .withIndex() + .filter { (_, instruction) -> filter(instruction) } + .map { (index, _) -> index } + .asReversed() /** - * @return The list of indices of the opcode in reverse order. + * @return An immutable list of indices of the instructions in reverse order. + * @throws PatchException if no matching indices are found. */ -fun Method.findOpcodeIndicesReversed(filter: Instruction.() -> Boolean): List { - val indexes = implementation!!.instructions - .withIndex() - .filter { (_, instruction) -> filter(instruction) } - .map { (index, _) -> index } - .reversed() - +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 BytecodeContext.forEachLiteralValueInstruction( +fun BytecodePatchContext.forEachLiteralValueInstruction( literal: Long, block: MutableMethod.(literalInstructionIndex: Int) -> Unit, ) { @@ -401,47 +403,39 @@ fun BytecodeContext.forEachLiteralValueInstruction( } /** - * Return the resolved method early. + * Return the method early. */ -fun MethodFingerprint.returnEarly(bool: Boolean = false) { +fun MutableMethod.returnEarly(bool: Boolean = false) { val const = if (bool) "0x1" else "0x0" - result?.let { result -> - val stringInstructions = when (result.method.returnType.first()) { - 'L' -> - """ - const/4 v0, $const - return-object v0 - """ - 'V' -> "return-void" - 'I', 'Z' -> - """ - const/4 v0, $const - return v0 - """ - else -> throw Exception("This case should never happen.") - } - result.mutableMethod.addInstructions(0, stringInstructions) - } ?: throw exception + val stringInstructions = when (returnType.first()) { + 'L' -> + """ + const/4 v0, $const + return-object v0 + """ + + 'V' -> "return-void" + 'I', 'Z' -> + """ + const/4 v0, $const + return v0 + """ + + else -> throw Exception("This case should never happen.") + } + + addInstructions(0, stringInstructions) } /** - * Return the resolved methods early. + * Set the custom condition for this fingerprint to check for a literal value. + * + * @param literalSupplier The supplier for the literal value to check for. */ -fun Iterable.returnEarly(bool: Boolean = false) = forEach { fingerprint -> - fingerprint.returnEarly(bool) +// TODO: add a way for subclasses to also use their own custom fingerprint. +fun FingerprintBuilder.literal(literalSupplier: () -> Long) { + custom { method, _ -> + method.containsLiteralInstruction(literalSupplier()) + } } - -/** - * Return the resolved methods early. - */ -@Deprecated("Use the Iterable version") -fun List.returnEarly(bool: Boolean = false) = forEach { fingerprint -> - fingerprint.returnEarly(bool) -} - -/** - * Resolves this fingerprint using the classDef of a parent fingerprint. - */ -fun MethodFingerprint.alsoResolve(context: BytecodeContext, parentFingerprint: MethodFingerprint) = - also { resolve(context, parentFingerprint.resultOrThrow().classDef) }.resultOrThrow() diff --git a/src/main/kotlin/app/revanced/util/ResourceUtils.kt b/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt similarity index 66% rename from src/main/kotlin/app/revanced/util/ResourceUtils.kt rename to patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt index 2f0d93f84..3faf55a8f 100644 --- a/src/main/kotlin/app/revanced/util/ResourceUtils.kt +++ b/patches/src/main/kotlin/app/revanced/util/ResourceUtils.kt @@ -1,8 +1,8 @@ package app.revanced.util -import app.revanced.patcher.data.ResourceContext import app.revanced.patcher.patch.PatchException -import app.revanced.patcher.util.DomFileEditor +import app.revanced.patcher.patch.ResourcePatchContext +import app.revanced.patcher.util.Document import app.revanced.util.resource.BaseResource import org.w3c.dom.Attr import org.w3c.dom.Element @@ -22,12 +22,14 @@ fun NodeList.asSequence() = (0 until this.length).asSequence().map { this.item(i /** * Returns a sequence for all child nodes. */ -fun Node.childElementsSequence() = this.childNodes.asSequence().filter { it.nodeType == Node.ELEMENT_NODE } +@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. */ -fun Node.forEachChildElement(action: (Node) -> Unit) = +inline fun Node.forEachChildElement(action: (Element) -> Unit) = childElementsSequence().forEach { action(it) } @@ -56,7 +58,7 @@ fun Node.insertFirst(node: Node) { * @param sourceResourceDirectory The source resource directory name. * @param resources The resources to copy. */ -fun ResourceContext.copyResources( +fun ResourcePatchContext.copyResources( sourceResourceDirectory: String, vararg resources: ResourceGroup, ) { @@ -92,27 +94,32 @@ class ResourceGroup(val resourceDirectoryName: String, vararg val resources: Str * @param targetTag The target xml node. * @param callback The callback to call when iterating over the nodes. */ -fun ResourceContext.iterateXmlNodeChildren( +fun ResourcePatchContext.iterateXmlNodeChildren( resource: String, targetTag: String, callback: (node: Node) -> Unit, -) = xmlEditor[classLoader.getResourceAsStream(resource)!!].use { editor -> - val document = editor.file - +) = 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)) } -// TODO: After the migration to the new patcher, remove the following code and replace it with the commented code below. -fun String.copyXmlNode(source: DomFileEditor, target: DomFileEditor): AutoCloseable { - val hostNodes = source.file.getElementsByTagName(this).item(0).childNodes +/** + * Copies the specified node of the source [Document] to the target [Document]. + * @param source the source [Document]. + * @param target the target [Document]- + * @return AutoCloseable that closes the [Document]s. + */ +fun String.copyXmlNode( + source: Document, + target: Document, +): AutoCloseable { + val hostNodes = source.getElementsByTagName(this).item(0).childNodes - val 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) } @@ -122,45 +129,6 @@ fun String.copyXmlNode(source: DomFileEditor, target: DomFileEditor): AutoClosea } } -// /** -// * Copies the specified node of the source [Document] to the target [Document]. -// * @param source the source [Document]. -// * @param target the target [Document]- -// * @return AutoCloseable that closes the [Document]s. -// */ -// fun String.copyXmlNode( -// source: Document, -// target: Document, -// ): AutoCloseable { -// val hostNodes = source.getElementsByTagName(this).item(0).childNodes -// -// val destinationNode = target.getElementsByTagName(this).item(0) -// -// for (index in 0 until hostNodes.length) { -// val node = hostNodes.item(index).cloneNode(true) -// target.adoptNode(node) -// destinationNode.appendChild(node) -// } -// -// return AutoCloseable { -// source.close() -// target.close() -// } -// } - -// @Deprecated( -// "Use copyXmlNode(Document, Document) instead.", -// ReplaceWith( -// "this.copyXmlNode(source.file as Document, target.file as Document)", -// "app.revanced.patcher.util.Document", -// "app.revanced.patcher.util.Document", -// ), -// ) -// fun String.copyXmlNode( -// source: DomFileEditor, -// target: DomFileEditor, -// ) = this.copyXmlNode(source.file as Document, target.file as Document) - /** * Add a resource node child. * @@ -197,9 +165,8 @@ internal fun NodeList.findElementByAttributeValue(attributeName: String, value: return null } -internal fun NodeList.findElementByAttributeValueOrThrow(attributeName: String, value: String): Element { - return findElementByAttributeValue(attributeName, value) ?: throw PatchException("Could not find: $attributeName $value") -} +internal fun NodeList.findElementByAttributeValueOrThrow(attributeName: String, value: String) = + findElementByAttributeValue(attributeName, value) ?: throw PatchException("Could not find: $attributeName $value") internal fun Element.copyAttributesFrom(oldContainer: Element) { // Copy attributes from the old element to the new element 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/src/main/kotlin/app/revanced/util/resource/ArrayResource.kt b/patches/src/main/kotlin/app/revanced/util/resource/ArrayResource.kt similarity index 100% rename from src/main/kotlin/app/revanced/util/resource/ArrayResource.kt rename to patches/src/main/kotlin/app/revanced/util/resource/ArrayResource.kt diff --git a/src/main/kotlin/app/revanced/util/resource/BaseResource.kt b/patches/src/main/kotlin/app/revanced/util/resource/BaseResource.kt similarity index 100% rename from src/main/kotlin/app/revanced/util/resource/BaseResource.kt rename to patches/src/main/kotlin/app/revanced/util/resource/BaseResource.kt diff --git a/src/main/kotlin/app/revanced/util/resource/StringResource.kt b/patches/src/main/kotlin/app/revanced/util/resource/StringResource.kt similarity index 100% rename from src/main/kotlin/app/revanced/util/resource/StringResource.kt rename to patches/src/main/kotlin/app/revanced/util/resource/StringResource.kt diff --git a/src/main/resources/addresources/values-af-rZA/strings.xml b/patches/src/main/resources/addresources/values-af-rZA/strings.xml similarity index 69% rename from src/main/resources/addresources/values-af-rZA/strings.xml rename to patches/src/main/resources/addresources/values-af-rZA/strings.xml index 4281ce7a4..6892649b0 100644 --- a/src/main/resources/addresources/values-af-rZA/strings.xml +++ b/patches/src/main/resources/addresources/values-af-rZA/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,105 +139,104 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/src/main/resources/addresources/values-am-rET/strings.xml b/patches/src/main/resources/addresources/values-am-rET/strings.xml similarity index 69% rename from src/main/resources/addresources/values-am-rET/strings.xml rename to patches/src/main/resources/addresources/values-am-rET/strings.xml index 4281ce7a4..6892649b0 100644 --- a/src/main/resources/addresources/values-am-rET/strings.xml +++ b/patches/src/main/resources/addresources/values-am-rET/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,105 +139,104 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/src/main/resources/addresources/values-ar-rSA/strings.xml b/patches/src/main/resources/addresources/values-ar-rSA/strings.xml similarity index 93% rename from src/main/resources/addresources/values-ar-rSA/strings.xml rename to patches/src/main/resources/addresources/values-ar-rSA/strings.xml index a396e2bb4..535c59f76 100644 --- a/src/main/resources/addresources/values-ar-rSA/strings.xml +++ b/patches/src/main/resources/addresources/values-ar-rSA/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + فشلت عمليات التحقق فتح الموقع الرسمي تجاهل @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t تم التعديل منذ %s يوم تاريخ إنشاء APK تالف - + هل ترغب في المتابعة؟ إعادة التعيين تحديث وإعادة تشغيل @@ -62,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t الروابط الرسمية تبرع - + لم يتم تثبيت MicroG GmsCore . قم بتثبيته. الإجراء مطلوب @@ -73,7 +73,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + لمحة الإعلانات مُصغَّرات فيديو بديلة @@ -85,7 +85,12 @@ This is because Crowdin requires temporarily flattening this file and removing t إعدادات متنوعة الفيديو - + + تعطيل تشغيل فيديوهات Shorts في الخلفية + تم تعطيل تشغيل Shorts بالخلفية + تم تمكين تشغيل Shorts بالخلفية + + تصحيح الأخطاء تمكين أو تعطيل خيارات تصحيح الأخطاء تسجيل تصحيح الأخطاء @@ -102,13 +107,19 @@ This is because Crowdin requires temporarily flattening this file and removing t لا يتم عرض ملاحظة في حالة حدوث خطأ يؤدي إيقاف تشغيل ملاحظات الأخطاء إلى إخفاء كافة إشعارات خطأ ReVanced.\n\nلن يتم إعلامك بأي أخطاء غير متوقعة. - + تعطيل توهج زر أعجبني / اشتراك لن يتوهج زر أعجبني واشتراك عند ذكره أعجبني واشتراك سوف يتوهج عند الإشارة - إخفاء الفاصل الرمادي - تم إخفاء الفواصل الرمادية - يتم عرض الفواصل الرمادية + إخفاء بطاقات الألبوم + تم إخفاء بطاقات الألبوم + يتم عرض بطاقات الألبوم + إخفاء مربع التمويل الجماعي + تم إخفاء مربع التمويل الجماعي + يتم عرض مربع التمويل الجماعي + إخفاء زر الميكروفون العائم + تم إخفاء زر الميكروفون + يتم عرض زر الميكروفون إخفاء العلامة المائية للقناة تم إخفاء علامة الفيديو المائية يتم عرض علامة الفيديو المائية @@ -153,9 +164,6 @@ This is because Crowdin requires temporarily flattening this file and removing t إخفاء الشريحة القابلة للتوسيع تحت مقاطع الفيديو تم إخفاء الرقائق القابلة للتوسيع يتم عرض الرقائق القابلة للتوسيع - إخفاء تذييل قائمة جودة الفيديو - تم إخفاء تذييل قائمة جودة الفيديو - يتم عرض تذييل قائمة جودة الفيديو إخفاء مشاركات المجتمع تم إخفاء مشاركات المجتمع يتم عرض مشاركات المجتمع @@ -230,6 +238,37 @@ This is because Crowdin requires temporarily flattening this file and removing t يتم عرض قسم النص وصف الفيديو إخفاء أو عرض مكونات وصف الفيديو + شريط التصفية + إخفاء شريط التصفية أو عرضه في الموجز والبحث ومقاطع الفيديو ذات الصلة + إخفاء في الموجز + مخفي في الموجز + يعرض في الموجز + إخفاء في البحث + مخفي في البحث + يعرض في البحث + إخفاء في الفيديوهات ذات الصلة + مخفي في الفيديوهات ذات الصلة + يعرض في الفيديوهات ذات الصلة + التعليقات + إخفاء أو عرض مكونات قسم التعليقات + إخفاء رأس \'تعليقات الأعضاء\' + تم إخفاء رأس \'تعليقات الأعضاء\' + يتم عرض رأس \'تعليقات الأعضاء\' + إخفاء قسم التعليقات + تم إخفاء قسم التعليقات + يتم عرض قسم التعليقات + إخفاء زر \'إنشاء مقطع Short\' + تم إخفاء زر \'إنشاء Short\' + يتم عرض زر \'إنشاء Short\' + إخفاء تعليق المعاينة + تم إخفاء تعليق المعاينة + يتم عرض تعليق المعاينة + إخفاء زر شكرًا + تم إخفاء زر شكرًا + يتم عرض زر شكرًا + إخفاء أزرار الطابع الزمني والرموز التعبيرية + تم إخفاء أزرار الطابع الزمني والرموز التعبيرية + يتم عرض أزرار الطابع الزمني والرموز التعبيرية إخفاء رسومات YouTube تم إخفاء رسومات شريط البحث @@ -271,7 +310,7 @@ This is because Crowdin requires temporarily flattening this file and removing t الكلمة الرئيسية قصيرة جدًا وتتطلب اقتباسات: %s الكلمة الرئيسية سوف تخفي جميع الفيديوهات: %s - + إخفاء الإعلانات العامة تم إخفاء الإعلانات بشكل عام يتم عرض الإعلانات العامة @@ -290,6 +329,9 @@ This is because Crowdin requires temporarily flattening this file and removing t إخفاء لافتة لعرض المنتجات تم إخفاء البانر يتم عرض البانر + إخفاء رف مشغل التسوق + تم إخفاء رفوف التسوق + يتم عرض رفوف التسوق إخفاء روابط التسوق في وصف الفيديو تم إخفاء روابط التسوق يتم عرض روابط التسوق @@ -306,17 +348,17 @@ This is because Crowdin requires temporarily flattening this file and removing t إخفاء إعلانات ملء الشاشة يعمل فقط مع الأجهزة القديمة - + إخفاء ترقية YouTube Premium تم إخفاء عروض YouTube Premium الترويجية تحت مشغل الفيديو يتم عرض عروض YouTube Premium الترويجية تحت مشغل الفيديو - + إخفاء إعلانات الفيديو تم إخفاء إعلانات الفيديو يتم عرض إعلانات الفيديو - + تم نسخ URL إلى الحافظة تم نسخ عنوان URL مع الطابع الزمني عرض زر نسخ عنوان URL للفيديو @@ -326,13 +368,13 @@ This is because Crowdin requires temporarily flattening this file and removing t يتم عرض الزر. انقر لنسخ عنوان URL للفيديو مع الطابع الزمني. انقر مع الاستمرار لنسخ الفيديو بدون الطابع الزمني لا يتم عرض الزر - + إزالة مربع حوار تقدير المشاهد سيتم إزالة مربع الحوار سيتم عرض مربع الحوار وهذا لا يتجاوز قيود السن. بل يقبلها تلقائيًا. - + التنزيلات الخارجية إعدادات لاستخدام أداة التنزيل الخارجية عرض زر التنزيل الخارجي @@ -346,17 +388,17 @@ This is because Crowdin requires temporarily flattening this file and removing t اسم الحزمة لتطبيق التنزيل الخارجي المثبت لديك، مثل NewPipe أو Seal لم يتم تثبيت %s . الرجاء تثبيته. - + تعطيل إيماءة التمرير الدقيقة تم تعطيل الإيماءة تم تمكين الإيماءة - + تمكين النقر على شريط الوقت تم تمكين النقر على شريط الوقت (شريط تقدم الفيديو) تم تعطيل النقر على شريط الوقت (شريط تقدم الفيديو) - + التحكم بالسطوع عن طريق ايماءة التمرير تم تمكين التحكم بمستوى السطوع عن طريق الإيماءة تم تعطيل التحكم بمستوى السطوع عن طريق الإيماءة @@ -385,12 +427,12 @@ This is because Crowdin requires temporarily flattening this file and removing t مقدار حد التمرير الحد الأدنى من التمرير قبل اكتشاف الإيماءة - + تعطيل التَّرْجَمَة التلقائية تم تعطيل التَّرْجَمَة التلقائية تم تمكين التَّرْجَمَة التلقائية - + أزرار الإجراء إخفاء أو عرض الأزرار تحت مقاطع الفيديو إخفاء أعجبني ولم يعجبني @@ -426,23 +468,7 @@ This is because Crowdin requires temporarily flattening this file and removing t تم إخفاء زر الحفظ في قائمة التشغيل يتم عرض زر الحفظ في قائمة التشغيل - - إخفاء زر التشغيل التلقائي - تم إخفاء زر التشغيل التلقائي - يتم عرض زر التشغيل التلقائي - - - - إخفاء زر التَرْجَمَة - تم إخفاء زر التَرْجَمَة - يتم عرض زر التَرْجَمَة - - - إخفاء زر البث - تم إخفاء زر البث - يتم عرض زر البث - - + أزرار التنقل إخفاء أو تغيير الأزرار في شريط التنقل @@ -469,7 +495,7 @@ This is because Crowdin requires temporarily flattening this file and removing t تم إخفاء التسميات يتم عرض التسميات - + القائمة المنبثقة إخفاء أو عرض عناصر قائمة المشغل المنبثقة @@ -480,6 +506,10 @@ This is because Crowdin requires temporarily flattening this file and removing t إخفاء الإعدادات الإضافية تم إخفاء قائمة الإعدادات الإضافية يتم عرض قائمة الإعدادات الإضافية + + إخفاء مؤقت النوم + تم إخفاء قائمة مؤقت النوم + يتم عرض قائمة مؤقت النوم إخفاء تكرار الفيديو تم إخفاء قائمة تكرار الفيديو @@ -488,6 +518,9 @@ This is because Crowdin requires temporarily flattening this file and removing t إخفاء وضع الإضاءة السينمائية تم إخفاء قائمة الإضاءة السينمائية يتم عرض قائمة الإضاءة السينمائية + إخفاء مستوى الصوت الثابت + يتم عرض قائمة مستوى الصوت الثابت + تم إخفاء قائمة مستوى الصوت الثابت إخفاء المساعدة & الملاحظات تم إخفاء قائمة المساعدة & الملاحظات @@ -513,83 +546,46 @@ This is because Crowdin requires temporarily flattening this file and removing t إخفاء المشاهدة في VR تم إخفاء قائمة المشاهدة في الوضع الافتراضي يتم عرض قائمة المشاهدة في الوضع الافتراضي + إخفاء تذييل قائمة جودة الفيديو + تم إخفاء تذييل قائمة جودة الفيديو + يتم عرض تذييل قائمة جودة الفيديو - - إخفاء أزرار الفيديو السابق & التالي - تم إخفاء الأزرار - يتم عرض الأزرار + + إخفاء أزرار الفيديو السابق & التالي + تم إخفاء الأزرار + يتم عرض الأزرار + إخفاء زر البث + تم إخفاء زر البث + يتم عرض زر البث + + إخفاء زر التَرْجَمَة + تم إخفاء زر التَرْجَمَة + يتم عرض زر التَرْجَمَة + إخفاء زر التشغيل التلقائي + تم إخفاء زر التشغيل التلقائي + يتم عرض زر التشغيل التلقائي - - إخفاء بطاقات الألبوم - تم إخفاء بطاقات الألبوم - يتم عرض بطاقات الألبوم - - - التعليقات - إخفاء أو عرض مكونات قسم التعليقات - إخفاء رأس \'تعليقات الأعضاء\' - تم إخفاء رأس \'تعليقات الأعضاء\' - يتم عرض رأس \'تعليقات الأعضاء\' - إخفاء قسم التعليقات - تم إخفاء قسم التعليقات - يتم عرض قسم التعليقات - إخفاء زر \'إنشاء مقطع Short\' - تم إخفاء زر \'إنشاء Short\' - يتم عرض زر \'إنشاء Short\' - إخفاء تعليق المعاينة - تم إخفاء تعليق المعاينة - يتم عرض تعليق المعاينة - إخفاء زر شكرًا - تم إخفاء زر شكرًا - يتم عرض زر شكرًا - إخفاء أزرار الطابع الزمني والرموز التعبيرية - تم إخفاء أزرار الطابع الزمني والرموز التعبيرية - يتم عرض أزرار الطابع الزمني والرموز التعبيرية - - - إخفاء مربع التمويل الجماعي - تم إخفاء مربع التمويل الجماعي - يتم عرض مربع التمويل الجماعي - - + إخفاء بطاقات شاشة النهاية تم إخفاء بطاقات شاشة النهاية يتم عرض بطاقات شاشة النهاية - - شريط التصفية - إخفاء شريط التصفية أو عرضه في الموجز والبحث ومقاطع الفيديو ذات الصلة - إخفاء في الموجز - مخفي في الموجز - يعرض في الموجز - إخفاء في البحث - مخفي في البحث - يعرض في البحث - إخفاء في الفيديوهات ذات الصلة - مخفي في الفيديوهات ذات الصلة - يعرض في الفيديوهات ذات الصلة - - - إخفاء زر الميكروفون العائم - تم إخفاء زر الميكروفون - يتم عرض زر الميكروفون - - + تعطيل وضع الإضاءة السينمائية في ملء الشاشة تم تعطيل وضع الإضاءة السينمائية تم تمكين وضع الإضاءة السينمائية - + إخفاء بطاقات المعلومات تم إخفاء بطاقات المعلومات يتم عرض بطاقات المعلومات - + تعطيل عدد مرات المشاهدة والإعجابات في الوقت الفعلي عدد مرات المشاهدة والإعجابات غير متحركة عدد مرات المشاهدة والإعجابات متحركة - + إخفاء شريط التقدم في مشغل الفيديو تم إخفاء شريط تقدم الفيديو يتم عرض شريط تقدم الفيديو @@ -597,7 +593,9 @@ This is because Crowdin requires temporarily flattening this file and removing t تم إخفاء مصغرة شريط التقدم يتم عرض مصغرة شريط التقدم - + + مشغل Shorts + إخفاء أو عرض المكونات في مشغل Shorts إخفاء Shorts في موجز الصفحة الرئيسية تم إخفاء Shorts في موجز الصفحة الرئيسية @@ -695,27 +693,27 @@ This is because Crowdin requires temporarily flattening this file and removing t تم إخفاء شريط التنقل يتم عرض شريط التنقل - + تعطيل شاشة نهاية الفيديو المقترح الفيديوهات المقترحة سيتم تعطيلها الفيديوهات المقترحة سيتم عرضها - + إخفاء الطابع الزمني للفيديو تم إخفاء الطابع الزمني يتم عرض الطابع الزمني - + إخفاء لوحات المشغل المنبثقة تم إخفاء لوحات المشغل المنبثقة يتم عرض لوحات المشغل المنبثقة - + شفافية تراكب المشغل قيمة الشفافية بين 0-100، حيث يكون 0 شفاف شفافية واجهة المشغل يجب أن تكون بين 0-100 - + لم يعجبني غير متاح مؤقتًا (انتهت مهلة API) لم يعجبني غير متاح (الحالة %d) @@ -759,17 +757,23 @@ This is because Crowdin requires temporarily flattening this file and removing t تم مواجهة حد معدل العميل %d مرة %d جزء الثانية - + تمكين شريط البحث العريض تم تمكين شريط البحث العريض تم تعطيل شريط البحث العريض - + + تمكين المصغرات عالية الجودة + مصغرات شريط التقدم عالية الجودة + مصغرات شريط التقدم متوسطة الجودة + مصغرات شريط التقدم بملء الشاشة عالية الجودة + مصغرات شريط التقدم بملء الشاشة متوسطة الجودة + سيؤدي هذا أيضا إلى استعادة المصغرات على البث المباشر الذي لا يحتوي على مصغرات شريط التقدم.\n\nمصغرات شريط التقدم سوف تستخدم نفس جودة الفيديو الحالي.\n\nتعمل هذه الميزة بشكل أفضل مع جودة فيديو 720p أو أقل وعند استخدام اتصال إنترنت سريع جداً. استعادة مصغرات شريط التقدم القديم مصغرات شريط التقدم ستظهر فوق شريط تقدم الفيديو مصغرات شريط التقدم ستظهر في ملء الشاشة - + تمكين مانِع الرُعَاة SponsorBlock مانِع الرُعَاة هو نظام جماعي لتخطي الأجزاء المُمِلَّة في مقاطع YouTube المظهر @@ -950,7 +954,7 @@ This is because Crowdin requires temporarily flattening this file and removing t لمحة يتم توفير البيانات بواسطة SponsorBlock API. انقر هنا لمعرفة المزيد ومشاهدة التنزيلات لمنصات أخرى - + خِداع إصدار التطبيق تم تغيير اصدار التطبيق لم يتم تغيير اصدار التطبيق @@ -963,9 +967,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - استعادة سرعة الفيديو الواسعة & قائمة الجودة 18.09.39 - استعادة علامة تبويب المكتبة 17.41.37 - استعادة رف قائمة التشغيل القديم - 17.33.42 - استعادة تصميم واجهة المستخدم القديم - + تعيين صفحة البداية الافتراضي تصفح القنوات @@ -983,18 +986,26 @@ This is because Crowdin requires temporarily flattening this file and removing t المحتوى الرائج شاهد لاحقًا - + تعطيل استئناف مشغل Shorts لن يتم استئناف تشغيل مشغل Shorts عند بدء تشغيل التطبيق سيتم استئناف تشغيل مشغل Shorts عند بدء تشغيل التطبيق - + + التشغيل التلقائي لفيديوهات Shorts + سيتم تشغيل فيديوهات Shorts تلقائيًا + سيتم تكرار فيديوهات Shorts + تشغيل فيديوهات Shorts تلقائيًا في الخلفية + سيتم تشغيل فيديوهات Shorts تلقائيًا في الخلفية + سيتم تكرار فيديوهات Shorts في الخلفية + + تمكين تصميم الجهاز اللوحي تم تمكين تصميم الجهاز اللوحي تم تعطيل تصميم الجهاز اللوحي لا تظهر منشورات المجتمع على تخطيطات الجهاز اللوحي - + المشغل المصغر تغيير نمط المشغل المصغر داخل التطبيق نوع المشغل المصغر @@ -1013,6 +1024,9 @@ This is because Crowdin requires temporarily flattening this file and removing t تمكين السحب والإفلات السحب والإفلات مفعلان\n\nيمكن سحب المشغل المصغر إلى أي زاوية من الشاشة تم تعطيل السحب والإفلات + تمكين إيماءة السحب الأفقية + تم تمكين إيماءة السحب الأفقية\n\nيمكن سحب المشغل المصغر خارج الشاشة إلى اليسار أو اليمين + تم تعطيل إيماءة السحب الأفقية إخفاء زر الإغلاق تم إخفاء زر الإغلاق يتم عرض زر الإغلاق @@ -1032,12 +1046,12 @@ This is because Crowdin requires temporarily flattening this file and removing t قيمة الشفافية بين 0-100، حيث يكون 0 شفاف شفافية واجهة المشغل المصغر يجب أن تكون بين 0-100 - + تمكين شاشة التحميل المتدرجة ستحتوي شاشة التحميل على خلفية متدرجة ستحتوي شاشة التحميل على خلفية ثابتة - + تمكين لون شريط تقدم الفيديو المخصص يتم عرض لون شريط تقدم الفيديو المخصص يتم عرض لون شريط تقدم الفيديو الاصلي @@ -1045,12 +1059,12 @@ This is because Crowdin requires temporarily flattening this file and removing t لون شريط التقدم لون شريط التقدم غير صالح - + تجاوز قيود منطقة الصورة استخدام مضيف الصورة yt4.ggpht.com استخدام مضيف الصور الأصلي\n\nتمكين هذا يمكن إصلاح الصور المفقودة التي يتم حظرها في بعض المناطق - + علامة تبويب الصفحة الرئيسية @@ -1082,7 +1096,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow غير متوفر مؤقتًا (رمز الحالة: %s) DeArrow غير متوفر مؤقتًا - + عرض إعلانات ReVanced يتم عرض الإعلانات عند بدء التشغيل لا يتم عرض الإعلانات عند بدء التشغيل @@ -1090,47 +1104,47 @@ This is because Crowdin requires temporarily flattening this file and removing t فشل الاتصال بموفر الإعلانات تجاهل - + تحذير لم يتم حفظ سجل المشاهدة الخاص بك.<br><br>من المرجح أن يكون السبب في ذلك هو مانع إعلانات DNS أو وكيل الشبكة.<br><br>لإصلاح هذه المشكلة، قم بإضافة <b>s.youtube.com</b> إلى القائمة البيضاء أو قم بإيقاف تشغيل جميع أدوات حظر DNS ووكلاء البروكسي. لا تعرض مرة أخرى - + تمكين التكرار التلقائي تم تمكين التكرار التلقائي تم تعطيل التكرار التلقائي - + محاكاة أبعاد الجهاز تم محاكاة أبعاد الجهاز\n\nقد يتم فتح قفل جودة الفيديو العالية ولكن قد تواجه تقطعًا في تشغيل الفيديو وعمر بطارية أسوأ وتأثيرات جانبية غير معروفة أبعاد الجهاز غير محاكاة\n\nيمكن أن يؤدي تفعيل هذا إلى فتح جودة أعلى للفيديو قد يؤدي تمكين هذا إلى تباطؤ تشغيل الفيديو وتدهور عمر البطارية وآثار جانبية غير معروفة. - + إعدادات GmsCore إعدادات لـ GmsCore - + تجاوز إعادة توجيه URL تم تجاوز إعادة توجيه عنوان URL لم يتم تجاوز إعادة توجيه عنوان URL - + فتح الروابط في المتصفح فتح الروابط خارجيًا فتح الروابط في التطبيق - + إزالة معلمة تتبع الاستعلام يتم إزالة معلمة استعلام التتبع من الروابط لا يتم إزالة معلمة استعلام التتبع من الروابط - + تعطيل الاهتزاز عند التكبير تم تعطيل الاهتزاز تم تمكين الاهتزاز - + جودة تلقائية تذكر تغييرات جودة الفيديو تنطبق تغييرات الجودة على جميع مقاطع الفيديو @@ -1141,35 +1155,38 @@ This is because Crowdin requires temporarily flattening this file and removing t Wi-Fi تم تغيير جودة %1$s الافتراضية إلى: %2$s - + عرض زر مربع حوار السرعة يتم عرض الزر لا يتم عرض الزر - + + قائمة سرعة التشغيل المخصصة + يتم عرض قائمة سرعة التشغيل المخصصة + لا يتم عرض قائمة سرعة التشغيل المخصصة سرعة التشغيل المخصصة - إضافة أو تغيير سرعات التشغيل المتاحة + إضافة أو تغيير سرعة التشغيل المخصصة يجب أن تكون السرعة المخصصة أقل من %s. باستخدام القيم الافتراضية. سرعة تشغيل مخصصة غير صالحة. استخدام القيم الافتراضية. - + تذكر التغيرات في سرعة التشغيل تطبيق تغييرات سرعة التشغيل على جميع مقاطع الفيديو تطبيق تغييرات سرعة التشغيل فقط على الفيديو الحالي سرعة التشغيل الافتراضية تغيير السرعة الافتراضية إلى: %s - + استعادة قائمة جودة الفيديو القديمة يتم عرض قائمة جودة الفيديو القديمة لا يتم عرض قائمة جودة الفيديو القديمة - + تمكين Slide to Seek تم تمكين Slide to Seek تم تعطيل Slide to Seek - + Spoof Video Streams تزييف تدفقات الفيديو الخاصة بالعميل لمنع حدوث مشكلات أثناء التشغيل Spoof Video Streams @@ -1187,17 +1204,14 @@ This is because Crowdin requires temporarily flattening this file and removing t التأثيرات الجانبية لمحاكاة Android VR • قائمة المقطع الصوتي مفقودة\n• مستوى الصوت الثابت غير متوفر - - - - + منع الإعلانات الصوتية تم منع الإعلانات الصوتية تم إلغاء منع الإعلانات الصوتية - + %s غير متوفر. قد تظهر الإعلانات. حاول التبديل إلى خدمة منع إعلانات أخرى في الإعدادات. قام خادم %s بإرجاع خطأ. قد تظهر الإعلانات. حاول التبديل إلى خدمة منع إعلانات أخرى في الإعدادات. منع إعلانات الفيديو المضمنة @@ -1205,30 +1219,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Luminous Proxy PurpleAdBlock Proxy - + منع إعلانات الفيديو تم منع إعلانات الفيديو تم إلغاء منع إعلانات الفيديو - + تم حذف الرسالة عرض الرسائل المحذوفة لا تعرض الرسائل المحذوفة إخفاء الرسائل المحذوفة داخل Spoiler عرض الرسائل المحذوفة كنص مشطوب - + الحصول على نقاط القناة تلقائيًا يتم الحصول على نقاط القناة تلقائيًا لا تتم المطالبة بنقاط القناة تلقائيًا - + تمكين وضع تصحيح أخطاء Twitch تم تمكين وضع تصحيح أخطاء Twitch (غير مستحسن) تم تعطيل وضع تصحيح أخطاء Twitch - + إعدادات ReVanced الإعلانات إعدادات حجب الإعلانات diff --git a/src/main/resources/addresources/values-as-rIN/strings.xml b/patches/src/main/resources/addresources/values-as-rIN/strings.xml similarity index 69% rename from src/main/resources/addresources/values-as-rIN/strings.xml rename to patches/src/main/resources/addresources/values-as-rIN/strings.xml index 4281ce7a4..6892649b0 100644 --- a/src/main/resources/addresources/values-as-rIN/strings.xml +++ b/patches/src/main/resources/addresources/values-as-rIN/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,105 +139,104 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/src/main/resources/addresources/values-az-rAZ/strings.xml b/patches/src/main/resources/addresources/values-az-rAZ/strings.xml similarity index 91% rename from src/main/resources/addresources/values-az-rAZ/strings.xml rename to patches/src/main/resources/addresources/values-az-rAZ/strings.xml index e76bb708d..5748acbea 100644 --- a/src/main/resources/addresources/values-az-rAZ/strings.xml +++ b/patches/src/main/resources/addresources/values-az-rAZ/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Yoxlamalar uğursuz oldu Xidməti veb saytı aç Yan keç @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t %s gün əvvəl yamaqlanıb APK quruluş tarixi pozulub - + Davam etmək istəyirsiniz? Sıfırla Yenilə və yenidən başlat @@ -62,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Rəsmi bağlantılar İanə ver - + MicroG GmsCore quraşdırılmayıb. Bunu quraşdır. Fəaliyyət lazımdır @@ -73,7 +73,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Haqqında Reklamlar Seçmə miniatürlər @@ -85,7 +85,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Müxtəlif Video - + + Shorts arxa plan oynatmasın bağla + Shorts arxa plan oynatma qapalıdır + Shorts arxa plan oynatma aktivdir + + Sazlama Sazlama seçimlərini aktiv/qeyri-aktiv et Sazlama jurnalı @@ -102,13 +107,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Xəta baş verərsə bildiriş göstərmə Ani bildirişləri söndürəndə, bütün ReVanced xəta bildirişləri gizlənir.\n\nGözlənilməz hallardan xəbərdar olmayacaqsınız. - + Bəyən/abunə ol düymə parıltısın söndür \"Bəyən və abunə ol\" düyməsin klikləyəndə parıldamayacaq \"Bəyən və abunə ol\" düyməsinə klikləyəndə parlayacaq - Boz ayırıcını gizlət - Boz ayırıcılar gizlidir - Boz ayırıcılar göstərilir + Albom kartlarını gizlət + Albom kartları gizlidir + Albom kartları göstərilir + İanə qutusunu gizlət + İanə qutusu gizlidir + İanə qutusu göstərilir + Üzən mikrofon düyməsini gizlət + Mikrofon düyməsi gizlidir + Mikrofon düyməsi göstərilir Kanal filiqranını gizlət Su nişanı gizlidir Su nişanı göstərilir @@ -153,9 +164,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Videoların altında genişlənən çipi gizlət Genişlənən çiplər gizlidir Genişlənən çiplər göstərilir - Video keyfiyyət menyu alt yazısın gizlə - Video keyfiyyəti menyusu alt yazısı gizlidir - Video keyfiyyət menyusu alt yazısı göstərilir İcma elanların gizlət İcma elanları gizlədilib İcma elanları göstərilir @@ -230,6 +238,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Transkripsiya bölməsi göstərilir Video açıqlaması Video açıqlaması elementlərini gizlət və ya göstər + Filtr çubuğu + Axında, axtarışda və əlaqəli videolardakı filtr çubuğunu gizlət və ya göstər + Axında gizlət + Axında gizlidir + Axında göstərilir + Axtarışda gizlət + Axtarışda gizlidir + Axtarışda görünür + Əlaqəli videolarda gizlət + Əlaqəli videolarda gizlidir + Əlaqəli videolarda görünür + Şərhlər + Şərhlər bölməsi elementlərin gizlət və ya göstər + \'Üzvlərin şərhləri\' başlığını gizlət + \"Üzvlərin şərhləri\" başlığı gizlədilib + \"Üzvlərin şərhləri\" başlığı göstərilir + Şərhlər bölməsini gizlət + Şərhlər bölməsi gizlidir + Şərhlər bölməsi göstərilir + \"Shorts Yarat\" düyməsini gizlət + \"Shorts yarat\" düyməsi gizlidir + \"Shorts yarat\" düyməsi göstərilir + Önbaxış şərhin gizlət + Önbaxış şərhi gizlədilib + Önbaxış şərhi göstərilir + Təşəkkürlər düyməsini gizlət + Təşəkkür düyməsi gizlidir + Təşəkkür düyməsi göstərilir + Vaxt möhürü və emoji düymələrin gizlə + Vaxt möhürü və emoji düymələri gizlədilib + Vaxt möhürü və emoji düymələri göstərilir YouTube Doodle-ları gizlət Axtarış çubuğu Doodle-ları gizlidir @@ -271,7 +310,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Açar söz çox qısadır və istinad tələb edir: %s Açar söz, bütün videoları gizlədəcək: %s - + Ümumi reklamları gizlət Ümumi reklamlar gizlidir Ümumi reklamlar göstərilir @@ -290,6 +329,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Məhsullara baxma etiketin gizlət Etiket gizlədilib Etiket göstərilir + Oynadıcı alış-veriş rəfini gizlət + Alış-veriş rəfi gizlidir + Alış-veriş rəfi göstərilir Video açıqlama alış-veriş linklər gizlə Alış-veriş bağlantıları gizlədilir Alış-veriş bağlantıları göstərilir @@ -306,17 +348,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Tam ekran reklamları gizlətmə yalnız köhnə cihazlarda işləyir - + YouTube Premium reklamlarını gizlət Video oynadıcı altında YouTube Premium elanları gizlidir Video oynadıcı altındakı YouTube Premium elanları göstərilir - + Video reklamlarını gizlət Video reklamlar gizlədilir Video reklamlar göstərilir - + URL buferə köçürüldü Vaxt möhürlü URL köçürüldü Video URL-i köçürmə düyməsin göstər @@ -326,13 +368,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Düymə göstərilir. Vaxt möhürlü video URL-sini köçürtmək üçün toxunun. Vaxt möhürü olmadan köçürtmək üçün basılı tutun Düymə göstərilmir - + İzləyici mülahizə dialoqun sil Dialoq silindi Dialoq göstərilir Bu, yaş məhdudiyyətini ötürmür. Sadəcə avtomatik qəbul edir. - + Xarici yükləmələr Xarici yükləyici istifadəsi üçün tənzimləmələr Xarici yükləmə düyməsini göstər @@ -346,17 +388,17 @@ This is because Crowdin requires temporarily flattening this file and removing t NewPipe və ya Seal kimi quraşdırılmış xarici yükləmə tətbiqinizin paket adı %s quraşdırılmayıb. Lütfən, bunu quraşdır. - + Dəqiq axtarış jestini qeyri-aktiv edin Jest qeyri-aktiv edilib Jest aktivləşdirilib - + Axtarış çubuğuna toxunmanı aktivləşdir Axtarış çubuğuna toxunma aktivdir Axtarış çubuğuna toxunma qeyri-aktiv edilib - + Parlaqlıq jestini aktivləşdir Parlaqlıq sürüşdürməsi aktivləşdirilir Parlaqlıq sürüşdürmə qeyri-aktivdir @@ -385,12 +427,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Sürüşdürmə böyüklük həddi Sürüşdürmənin icra edilməsi üçün son dəyər - + Avtomatik titrləri qeyri-aktiv et Avtomatik titrlər qeyri-aktivdir Avtomatik titrlər aktivləşdirilir - + Fəaliyyət düymələri Videonun altındakı düymələri gizlət və ya göstər \"Bəyənmə\" və \"Bəyənməmə\"ni gizlət @@ -426,23 +468,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Pleylistdə saxla düyməsi gizlidir Pleylistdə saxla düyməsi göstərilir - - Avto-oynatma düyməsini gizlət - Avtomatik oynatma düyməsi gizlidir - Avtomatik oynatma düyməsi göstərilir - - - - Titrlər düyməsini gizlət - Titrlər düyməsi gizlidir - Titrlər düyməsi göstərilir - - - Yayımla düyməsini gizlət - Yayım düyməsi gizlidir - Yayım düyməsi göstərilir - - + Fəaliyyət düymələri Fəaliyyət çubuğundakı düymələri gizlət və ya dəyiş @@ -469,7 +495,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Etiketlər gizlidir Etiketlər göstərilir - + Açılan menyu Oynadıcı açılan menyu elementlərini gizlət və ya göstər @@ -480,6 +506,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Əlavə ayarları gizlət Əlavə ayarlar menyusu gizlidir Əlavə ayarlar menyusu göstərilir + + Yuxu taymerini gizlət + Yuxu taymeri menyusu gizlidir + Yuxu taymeri menyusu göstərilir Videonu təkrarlanı gizlət Təkrarlama video menyusu gizlidir @@ -488,6 +518,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Ambient rejimini gizlət Ambient rejimi menyusu gizlidir Ambient rejimi menyusu göstərilir + Stabil səs səviyyəsin gizlət + Sabit səs menyusu göstərilir + Stabil səs menyusu gizlidir Kömək və əks əlaqəni gizlət Kömək & rəy menyusu gizlidir @@ -513,83 +546,46 @@ This is because Crowdin requires temporarily flattening this file and removing t \"VR-da İzləni\" gizlət VR menyusunda izləmə gizlidir VR menyusunda izləmə göstərilir + Video keyfiyyət menyusu alt məlumatını gizlət + Video keyfiyyət menyusu alt məlumatı gizlidir + Video keyfiyyət menyusu alt məlumatı göstərilir - - Əvvəlki/növbəti video düymələrin gizlə - Düymələr gizlidir - Düymələr göstərilir + + Əvvəlki/növbəti video düymələrin gizlət + Düymələr gizlidir + Düymələr göstərilir + Yayımla düyməsini gizlət + Yayım düyməsi gizlidir + Yayım düyməsi göstərilir + + Titrlər düyməsini gizlət + Titrlər düyməsi gizlidir + Titrlər düyməsi göstərilir + Avto-oynatma düyməsini gizlət + Avtomatik oynatma düyməsi gizlidir + Avtomatik oynatma düyməsi göstərilir - - Albom kartlarını gizlət - Albom kartları gizlidir - Albom kartları göstərilir - - - Şərhlər - Şərhlər bölməsi elementlərin gizlət və ya göstər - \'Üzvlərin şərhləri\' başlığını gizlət - \"Üzvlərin şərhləri\" başlığı gizlədilib - \"Üzvlərin şərhləri\" başlığı göstərilir - Şərhlər bölməsini gizlət - Şərhlər bölməsi gizlidir - Şərhlər bölməsi göstərilir - \"Shorts Yarat\" düyməsini gizlət - \"Shorts yarat\" düyməsi gizlidir - \"Shorts yarat\" düyməsi göstərilir - Önbaxış şərhin gizlət - Önbaxış şərhi gizlədilib - Önbaxış şərhi göstərilir - Təşəkkürlər düyməsini gizlət - Təşəkkür düyməsi gizlidir - Təşəkkür düyməsi göstərilir - Vaxt möhürü və emoji düymələrin gizlə - Vaxt möhürü və emoji düymələri gizlədilib - Vaxt möhürü və emoji düymələri göstərilir - - - İanə qutusunu gizlət - İanə qutusu gizlidir - İanə qutusu göstərilir - - + Son ekran kartlarını gizlət Son ekran kartları gizlidir Son ekran kartları göstərilir - - Filtr çubuğu - Axında, axtarışda və əlaqəli videolardakı filtr çubuğunu gizlət və ya göstər - Axında gizlət - Axında gizlidir - Axında göstərilir - Axtarışda gizlət - Axtarışda gizlidir - Axtarışda görünür - Əlaqəli videolarda gizlət - Əlaqəli videolarda gizlidir - Əlaqəli videolarda görünür - - - Üzən mikrofon düyməsini gizlət - Mikrofon düyməsi gizlidir - Mikrofon düyməsi göstərilir - - + Tam ekranda ambient rejimin bağla Ambient rejimi qeyri-aktiv edilib Ambient rejimi aktivləşdirildi - + Məlumat kartlarını gizlət Məlumat kartları gizlidir Məlumat kartları göstərilir - + Sürüşən rəqəm animasiyaların söndür Sürüşən say animasiyası bağlıdır Sürüşən say animasiyası açıqdır - + Video oynadıcıda axtarış çubuğun gizlə Video oynadıcı axtarış çubuğu gizlidir Video oynadıcı axtarış çubuğu göstərilir @@ -597,7 +593,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Miniatür axtarış çubuğu gizlədilib Miniatür axtarış çubuğu göstərilir - + + Shorts oynadıcı + Shorts oynadıcıda hissəcikləri gizlət və ya göstər Ev axınında \"Shorts\"u gizlət Ev axınındakı Shorts gizlidir @@ -644,9 +642,9 @@ This is because Crowdin requires temporarily flattening this file and removing t \"Yaşıl ekran\" düyməsini gizlət \"Yaşıl ekran\" düyməsi gizlidir \"Yaşıl ekran\" düyməsi göstərilir - Hashtag düyməsini gizlət - Hashtag düyməsi gizlidir - Hashtag düyməsi göstərilir + Mövzu etiketi düyməsini gizlət + Mövzu etiketi düyməsi gizlidir + Mövzu etiketi düyməsi göstərilir Axtarış təkliflərini gizlət Axtarış təklifləri gizlədilib Axtarış təklifləri göstərilir @@ -695,27 +693,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Fəaliyyət çubuğu gizlidir Fəaliyyət çubuğu göstərilir - + Təklif edilən video bitiş ekranın ləğv et Təklif olunan videolar qeyri-aktiv ediləcək Təklif olunan videolar göstəriləcək - + Video vaxt möhürünü gizlət Vaxt möhürü gizlidir Vaxt möhürü göstərilir - + Oynadıcı açılan pəncərə panellərin gizlə Oynadıcı açılan pəncərə panelləri gizlidir Oynadıcı açılan pəncərə panelləri göstərilir - + Oynadıcı örtüyünün qeyri-şəffaflığı 0-100 arasında qeyri-şəffaflıq dəyəri, burada 0 şəffafdır Oynadıcı örtüyünün qeyri-şəffaflığı 0-100 arası olmalıdır - + \"Bəyənməmə\" müvəqqəti əlçatmazdır(API vaxtı bitdi) Bəyənməmə əlçatmazdır (status %d) @@ -759,17 +757,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Qəbuledici sürət limiti %d dəfə baş verdi %d millisaniyə - + Geniş axtarış çubuğunu aktivləşdir Geniş axtarış çubuğu aktivləşdirilir Geniş axtarış çubuğu qeyri-aktivdir - + + Yüksək keyfiyyətli miniatürləri aktivləşdir + Axtarış çubuğu miniatürləri yüksək keyfiyyətlidir + Axtarış çubuğu miniatürləri orta keyfiyyətlidir + Tam ekran axtarış çubuğu miniatürləri yüksək keyfiyyətlidir + Tam ekran axtarış çubuğu miniatürləri orta keyfiyyətlidir + Bu, həm də axtarış cizgisi üzrə miniatürləri olmayan canlı yayımlarda miniatürləri qaytaracaq.\n\nAxtarış cizgisi miniatürləri, cari video kimi eyni keyfiyyəti işlədəcək.\n\nBu xüsusiyyət, 720p və ya daha aşağı video keyfiyyəti və çox sürətli internet bağlantısı istifadə edərkən daha yaxşı işləyir. Köhnə axtarış çubuğu miniatürlərin al Axtarış çubuğu miniatürləri axtarış çubuğu üstündə görünəcək Axtarış çubuğu miniatürləri tam ekranda görünəcək - + \"SponsorBlock\"u aktivləşdir SponsorBlock, YouTube videolarının lazımsız hissələrini ötürmək üçün kütlə mənbəli sistemdir Görünüş @@ -950,7 +954,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Haqqında Məlumat SponsorBlock API tərəfindən təqdim edilir. Daha ətraflı öyrənmək və digər platformalar üzrə yükləmələrə baxmaq üçün bura toxunun - + Tətbiq versiyasını saxtalaşdır Versiya saxtalaşdırıldı Versiya saxtalaşdırılmadı @@ -963,12 +967,11 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Geniş video sürəti & keyfiyyət menyusunu bərpa et 18.09.39 - Kitabxana panelini bərpa et 17.41.37 - Köhnə pleylist bölməsin bərpa et - 17.33.42 - Köhnə UI tərtibatını bərpa et - + Başlanğıc səhifəsini tənzimlə İlkin - Kəşf edilən kanallar + Kanallara nəzər yetir Kəşf et Oyun Tarixçə @@ -977,24 +980,32 @@ This is because Crowdin requires temporarily flattening this file and removing t Canlı Filmlər Musiqi - Axtarış + Axtar İdman Abunəliklər - Trenddə olan + Trendlər Sonra izlə - + Shorts oynadıcı başladıcını bağla Tətbiq açılanda Shorts oynadıcı davam etməyəcək Tətbiq açılanda Shorts oynadıcı davam edəcək - + + Shorts-ları avto-oynatma + Shorts-lar avto-oynadılacaq + Shorts-lar təkrarlanacaq + Shorts-ları arxa planda avto-oynat + Shorts-lar arxa planda avto-oynadılacaq + Shorts-lar arxa planda təkrarlanacaq + + Planşet tərtibatını aktiv et Planşet tərtibatı aktiv edilib Planşet tərtibatı qeyri-aktivdir İcma elanları planşet tərtibatında göstərilmir - + Kiçik oynadıcı Tətbiqdə kiçildilən oynadıcı üslubunu dəyişdir Kiçik oynadıcı növü @@ -1005,20 +1016,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Müasir 2 Müasir 3 Dairəvi küncləri aktivləşdir - Künclər dövrələnir + Künclər dairəvidir Künclər kvadratdır - Ölçüsünü dəyişmək üçün iki dəfə toxun və çimdiklə - İki dəfə kliklə fəaliyyəti və ölçüsünü dəyişmək üçün çimdiklə, aktivdir\n\n• Kiçik oynadıcı ölçüsün artırmaq üçün iki dəfə toxunun\n• Əsl ölçüsün bərpa etmək üçün təkrar iki dəfə toxun - Ölçüsün dəyişmək üçün cüt kliklə və çimdiklə bağlıdır - Çəkmə və buraxma funksiyasın aktivləşdir - Çəkmə və buraxma aktivdir\n\nMinipleyeri ekranın istənilən küncünə çəkmək olar - Çəkmə və buraxma bağlıdır - Bağlama düyməsin gizlət - Bağlama düyməsi gizlidir - Bağlama düyməsi göstərilir + Ölçüsünü dəyişmək üçün cüt toxunmanı və çimdikləməni aktivləşdir + Ölçüsünü dəyişdirmək üçün cüt toxunma fəaliyyəti və çimdikləmə aktivləşdirildi\n\n• Mini oynadıcı ölçüsün artırmaq üçün cüt toxunun\n• Orijinal ölçünü bərpa etmək üçün təkrar cüt toxun + Ölçüsünü dəyişdirmək üçün cüt toxunma fəaliyyəti və çimdikləmə yoxdur + \"Sürüklə və burax\"ı aktivləşdir + \"Sürüklə və burax\" aktivdir\n\nMini oynadıcı, ekranın istənilən küncünə sürüklənə bilər + \"Sürüklə və burax\" aktiv deyil + Üfüqi sürükləmə jestini aktivləşdir + Üfüqi sürükləmə jesti aktivdir\n\nKiçik Oynadıcı ekranın soluna və ya sağına sürüklənə bilər + Üfüqi sürükləmə jesti qapatıldı + \"Bağla\" düyməsini gizlət + \"Bağla\" düyməsi gizlidir + \"Bağla\" düyməsi göstərilir Genişləndir və bağla düymələrini gizlət Düymələr gizlədilib\n\nGenişləndirmək və ya bağlamaq üçün sürüşdür - Genişləndirmə və bağlama düymələri göstərilir + Genişləndir və bağla düymələri göstərilir Alt mətnləri gizlət Alt mətnlər gizlədilir Alt mətnlər göstərilir @@ -1026,18 +1040,18 @@ This is because Crowdin requires temporarily flattening this file and removing t İrəli və geri ötürücülər gizlidir İrəli və geri ötürücülər göstərilir İlkin ölçü - Ekran ölçüsündə, piksellərdə ilkin ölçü - Piksel ölçüsü %1$s və %2$s arası olmalıdır + Piksel olaraq ekranda ilkin ölçü + Piksel ölçüsü, %1$s - %2$s arası olmalıdır Örtük qeyri-şəffaflığı 0-100 arasında qeyri-şəffaflıq dəyəri, burada 0 şəffafdır Kiçik Oynadıcı örtük qeyri-şəffaflığı 0-100 arası olmalıdır - + Rəngbərəng yükləmə ekranını aktivləşdir Yükləmə ekranı, rəngbərəng arxa plana malik olacaq Yükləmə ekranı, vahid arxa plana malik olacaq - + Fərdi axtarma çubuğu rəngini aktivləşdir Fərdi axtarma çubuğu rəngi göstərilir Orijinal axtarma çubuğu rəngi göstərilir @@ -1045,12 +1059,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Axtarma çubuğu rəngi Etibarsız axtarış çubuğu rəng dəyəri - + Təsvir bölgə məhdudiyyətlərini ötür yt4.ggpht.com təsvir host-u istifadə edilir Orijinal təsvir host-u istifadə edilir\n\nBunu aktivləşdirmə, bəzi bölgələrdə əngəllənən, ağ təsvirləri düzəldə bilər - + Ev paneli @@ -1082,7 +1096,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow müvəqqəti əlçatan deyil (status kodu: %s) DeArrow müvəqqəti olaraq əlçatan deyil - + ReVanced elanlarını göstər Elanlar açılışda göstərilir Elanlar açılışda göstərilmir @@ -1090,47 +1104,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Elan provayderinə bağlanmaq olmadı Ləğv et - + Xәbәrdarlıq Baxış tarixçəniz saxlanmır.<br><br>Bu çox güman ki, DNS reklam bloklayıcı və ya şəbəkə proksisinə görədir.<br><br>.Bunu düzəltmək üçün s.youtube.com-u</b> <b>ağ siyahıya salın və ya bütün DNS bloklayıcıları və proksiləri bağlayın. Təkrar göstərmə - + Avto-təkrarlamanı aktivləşdir Avtomatik təkrar aktivləşdirilib Avtomatik təkrarlama qeyri-aktiv edilib - + Cihaz ölçülərini saxtalaşdır Cihaz ölçüləri saxtalaşdı\n\nDaha yüksək video keyfiyyətləri göstərilə bilər, ancaq video oynatma donmaları, daha pis batareya istismarı və bilinməyən yan təsirləri görə bilərsiniz Cihaz ölçüləri saxtalaşmır\n\nBunu aktivləşdirmə, daha yüksək video keyfiyyətlərinin olmasın təmin edə bilir Bunu aktivləşdirmə, video oynatma donmalarına, daha pis batareya istismarına və bilinməyən yan təsirlərə səbəb ola bilər. - + GmsCore Tənzimləmələri GmsCore üçün Tənzimləmələr - + URL yönləndirmələrini ötür URL yönləndirmələri ötürülür URL yönləndirmələri ötürülmür - + Bağlantıları brauzerdə aç Bağlantılar xarici brauzerdə açılır Bağlantılar tətbiqdə açılır - + İzləmə sorğusu faktorun sil İzləmə sorğusu faktoru bağlantılardan silinir İzləmə sorğusu faktoru bağlantılardan silinmir - + Yaxınlaşdırma əks-əlaqəsini bağla Toxunuş əks-əlaqəsi bağlandı Toxunuş əks-əlaqəsi aktivdir - + Avtomatik keyfiyyət Video keyfiyyəti dəyişikliklərini xatırla Keyfiyyət dəyişiklikləri bütün videolara tətbiq edilir @@ -1141,35 +1155,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wi-fi İlkin %1$s keyfiyyəti %2$s kimi dəyişdi - + Sürət dialoq düyməsini göstər Düymə göstərilir Düymə göstərilmir - + + Fərdi oynatma sürəti menyusu + Fərdi sürət menyusu göstərilir + Fərdi sürət menyusu göstərilmir Fərdi oynatma sürəti - Mövcud oynatma sürəti əlavə et və ya dəyişdir + Fərdi oynatma sürətlərini əlavə et və ya dəyiş Fərdi sürətlər %s-dən az olmalıdır. Standart dəyərlər istifadəsi. Etibarsız oynatma sürətləri. Standartlar istifadədədir. - + Oynatma sürəti dəyişikliklərin xatırla Oynatma sürəti dəyişiklikləri bütün videolara aiddir Oynatma sürəti dəyişiklikləri yalnız cari videoya aiddir İlkin oynatma sürəti İlkin sürət %s kimi dəyişdirildi - + Köhnə video keyfiyyət menusun qaytar Köhnə video keyfiyyət menyusu göstərilir Köhnə video keyfiyyət menyusu göstərmir - + Axtarmaq üçün sürüşdürməni aktiv et Axtarmaq üçün sürüşdürmə aktivdir Axtarmaq üçün sürüşdürmə aktiv deyil - + Video yayımları saxtalaşdır Oynatma problemlərin önləmək üçün qəbuledici video yayımların saxtalaşdır Video yayımları saxtalaşdır @@ -1187,17 +1204,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Android VR saxtakarlığı yan təsirləri • Səs axını menyusu əskikdir\n• Stabil səs səviyyəsi əlçatan deyil - - - - + Səsli reklamları əngəllə Səsli reklamlar bloklanıb Səsli reklamlar bloklanmayıb - + %s əlçatmazdır. Reklamlar görünə bilər. Seçimlərdə başqa reklam bloku xidmətinə keçirməyə cəhd et. %s serveri xəta sorğusu verdi. Reklam görünə bilər. Seçimlərdə başqa reklam bloku xidmətinə keçir. Yerləşdirilən video reklamlarını əngəllə @@ -1205,30 +1219,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Dəqiq proksi PurpleAdBlock proksi - + Video reklamları əngəllə Video reklamlar bloklanıb Video reklamlar bloklanmır - + mesaj silindi Silinən mesajları göstər Silinən mesajlar göstərilməsin Silinmiş mesajları boz panel arxasında gizlət Silinən mesajları qaralanmış mətn kimi göstər - + Kanal Xallarını avtomatik təsdiqlə Kanal Xalları avtomatik olaraq təsdiqlənir Kanal Xalları avtomatik olaraq təsdiqlənmir - + Twitch sazlama rejimini aktivləşdir Twitch sazlama rejimi aktivdir (tövsiyə edilmir) Twitch sazlama rejimi qeyri-aktiv edilib - + ReVanced Tənzimləmələri Reklamlar Reklam əngəlləmə tənzimləmələri diff --git a/src/main/resources/addresources/values-be-rBY/strings.xml b/patches/src/main/resources/addresources/values-be-rBY/strings.xml similarity index 94% rename from src/main/resources/addresources/values-be-rBY/strings.xml rename to patches/src/main/resources/addresources/values-be-rBY/strings.xml index df8a93411..325ededb0 100644 --- a/src/main/resources/addresources/values-be-rBY/strings.xml +++ b/patches/src/main/resources/addresources/values-be-rBY/strings.xml @@ -32,9 +32,9 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + Вы хочаце працягнуць? Скінуць Абнавіце і перазагрузіце @@ -52,7 +52,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Гэтая версія з\"яўляецца папярэдняй версіяй, і вы можаце сутыкнуцца з непрадбачанымі праблемамі Афіцыйныя спасылкі - + MicroG GmsCore не ўсталяваны. Усталюйце яго. Патрабуецца дзеянне @@ -63,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Пра праграму Аб\"явы Альтэрнатыўныя мініяцюры @@ -75,7 +75,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Рознае Відэа - + + + Адладка Уключыць або выключыць параметры адладкі Запіс адладкі @@ -92,13 +94,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Тост не паказваецца, калі ўзнікае памылка Адключэнне апавяшчэнняў пра памылкі схавае ўсе апавяшчэнні аб памылках ReVanced.\n\nВы не будзеце атрымліваць апавяшчэнні аб непрадбачаных падзеях. - + Адключыць свячэнне кнопкі \"Падабаецца\" / \"Падпісацца\". Кнопка \"Падабаецца\" і \"Падпісацца\" не будуць свяціцца пры згадванні Кнопка \"Падабаецца\" і \"Падпісацца\" будуць свяціцца пры згадванні - Схаваць шэры раздзяляльнік - Шэрыя падзельнікі схаваныя - Паказаны шэрыя раздзяляльнікі + Схаваць карты альбома + Карткі альбомаў схаваныя + Паказваюцца альбомныя карткі + Схаваць скрыню краўдфандынгу + Краўдфандынгавая скрыня схавана + Паказана скрыня краўдфандынгу + Схаваць плаваючую кнопку мікрафона + Кнопка мікрафона схавана + Паказана кнопка мікрафона Схаваць вадзяны знак канала Вадзяны знак схаваны Паказаны вадзяны знак @@ -143,9 +151,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Схаваць пашыраемы чып пад відэа Чыпы, якія пашыраюцца, схаваныя Паказаны чыпы, якія пашыраюцца - Схаваць калонтытул меню якасці відэа - Ніжні калонтытул меню якасці відэа схаваны - Паказваецца ніжні калонтытул меню якасці відэа Схаваць паведамленні ў супольнасці Паведамленні ў супольнасці схаваны Паказваюцца паведамленні ў супольнасці @@ -220,6 +225,34 @@ This is because Crowdin requires temporarily flattening this file and removing t Паказваецца раздзел стэнаграмы Апісанне відэа Схаваць або паказаць кампаненты апісання відэа + Панэль фільтраў + Схаваць або паказаць панэль фільтраў у стужцы, пошуку і звязаных відэа + Схаваць у карме + Схаваны ў стужцы + Паказваецца ў стужцы + Схавацца ў пошуку + Схаваны ў пошуку + Паказваецца ў пошуку + Схаваць у звязаных відэа + Схавана ў звязаных відэа + Паказана ў звязаных відэа + Каментарыі + Схаваць або паказаць кампаненты раздзела каментарыяў + Схаваць загаловак \"Каментарыі ўдзельнікаў\" + Загаловак \"Каментарыі ўдзельнікаў\" схаваны + Паказаны загаловак \"Каментарыі ўдзельнікаў\" + Схаваць раздзел каментарыяў + Раздзел каментарыяў схаваны + Паказваецца раздзел каментарыяў + Схаваць каментарый для папярэдняга прагляду + Каментарый перад праглядам схаваны + Паказваецца папярэдні прагляд каментарыя + Схаваць кнопку падзякі + Кнопка падзякі схавана + Паказана кнопка падзякі + Схаваць метку часу і кнопкі эмодзі + Кнопкі меткі часу і эмодзі схаваны + Паказваюцца кнопкі меткі часу і эмодзі Карыстальніцкі фільтр Схавайце кампаненты з дапамогай карыстацкіх фільтраў @@ -248,7 +281,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Схаваць агульную рэкламу Агульныя аб\"явы схаваныя Паказваюцца агульныя аб\"явы @@ -283,17 +316,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Схаваць поўнаэкранную рэкламу працуе толькі са старымі прыладамі - + Схаваць акцыі YouTube Premium Акцыі YouTube Premium пад відэаплэерам схаваны Пад відэаплэерам паказваюцца акцыі YouTube Premium - + Схаваць відэарэкламу Відэарэклама схаваная Паказваецца відэарэклама - + URL скапіраваны ў буфер абмену URL-адрас з пазнакай часу скапіраваны Паказаць кнопку скапіравання URL відэа @@ -303,13 +336,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Паказана кнопка. Націсніце, каб скапіяваць URL відэа з пазнакай часу. Націсніце і ўтрымлівайце, каб скапіяваць відэа без пазнакі часу Кнопка не паказваецца - + Выдаліць дыялогавае акно права прагляду Дыялог будзе выдалены Будзе паказана дыялогавае акно Гэта не абыходзіць узроставае абмежаванне. Ён проста прымае гэта аўтаматычна. - + Знешнія загрузкі Налады для выкарыстання вонкавага загрузніка Паказаць знешнюю кнопку загрузкі @@ -323,17 +356,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Імя пакета ўсталяванай знешняй праграмы загрузкі, напрыклад NewPipe або Seal %s не ўсталяваны. Калі ласка, усталюйце яго. - + Адключыць жэст дакладнага пошуку Жэст адключаны Жэст уключаны - + Уключыць націск на панэлі пошуку Націск панэлі пошуку ўключаны Націск панэлі пошуку адключаны - + Уключыць жэст яркасці Правядзенне па яркасці ўключана Правядзенне пальцам па яркасці адключана @@ -362,12 +395,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Парог велічыні пальцам Велічыня парогавага значэння для правядзення пальцам - + Адключыць аўтаматычныя цітры Аўтаматычныя цітры адключаны Аўтаматычныя цітры ўключаны - + Кнопкі дзеянняў Схаваць або паказаць кнопкі пад відэа Схаваць \"Падабаецца\" і \"Не падабаецца\". @@ -403,23 +436,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Кнопка \"Захаваць у спіс прайгравання\" схавана Паказана кнопка \"Захаваць у спіс прайгравання\". - - Схаваць кнопку аўтазапуску - Кнопка аўтазапуску схавана - Паказана кнопка аўтазапуску - - - - Кнопка «Схаваць цітры». - Кнопка субцітраў схавана - Паказана кнопка субцітраў - - - Схаваць кнопку трансляцыі - Кнопка Cast схавана - Паказана кнопка Cast - - + Кнопкі навігацыі Схаваць або змяніць кнопкі на панэлі навігацыі @@ -427,9 +444,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Кнопка \"Дадому\" схавана Паказана кнопка \"Дадому\". - Схаваць кнопку \"Shorts\" - Кнопка Shorts схавана - Паказана кнопка Shorts Схаваць Стварыць Кнопка \"Стварыць\" схавана @@ -446,7 +460,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Цэтлікі схаваныя Этыкеткі паказаны - + Выпадаючае меню Схаваць або паказаць элементы ўсплываючага меню гульца @@ -457,6 +471,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Схаваць дадатковыя налады Меню дадатковых налад схавана Адлюструецца меню дадатковых налад + Схаваць цыкл відэа Меню цыклічнага відэа схавана @@ -490,83 +505,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Схаваць гадзіннік у VR Меню прагляду ў VR схавана Паказана меню \"Глядзець у VR\". + Схаваць калонтытул меню якасці відэа + Ніжні калонтытул меню якасці відэа схаваны + Паказваецца ніжні калонтытул меню якасці відэа - - Схаваць папярэдні & кнопкі наступнага відэа - Кнопкі схаваныя - Паказваюцца кнопкі + + Схаваць папярэдні & кнопкі наступнага відэа + Кнопкі схаваныя + Паказваюцца кнопкі + Схаваць кнопку трансляцыі + Кнопка Cast схавана + Паказана кнопка Cast + + Кнопка «Схаваць цітры». + Кнопка субцітраў схавана + Паказана кнопка субцітраў + Схаваць кнопку аўтазапуску + Кнопка аўтазапуску схавана + Паказана кнопка аўтазапуску - - Схаваць карты альбома - Карткі альбомаў схаваныя - Паказваюцца альбомныя карткі - - - Каментарыі - Схаваць або паказаць кампаненты раздзела каментарыяў - Схаваць загаловак \"Каментарыі ўдзельнікаў\" - Загаловак \"Каментарыі ўдзельнікаў\" схаваны - Паказаны загаловак \"Каментарыі ўдзельнікаў\" - Схаваць раздзел каментарыяў - Раздзел каментарыяў схаваны - Паказваецца раздзел каментарыяў - Схаваць кнопку \"Стварыць кароткае відэа\" - Кнопка \"Стварыць кароткае відэа\" схавана - Паказана кнопка \"Стварыць кароткае відэа\" - Схаваць каментарый для папярэдняга прагляду - Каментарый перад праглядам схаваны - Паказваецца папярэдні прагляд каментарыя - Схаваць кнопку падзякі - Кнопка падзякі схавана - Паказана кнопка падзякі - Схаваць метку часу і кнопкі эмодзі - Кнопкі меткі часу і эмодзі схаваны - Паказваюцца кнопкі меткі часу і эмодзі - - - Схаваць скрыню краўдфандынгу - Краўдфандынгавая скрыня схавана - Паказана скрыня краўдфандынгу - - + Схаваць карткі канцавога экрана Карткі канцавога экрана схаваны Паказваюцца карткі канцавога экрана - - Панэль фільтраў - Схаваць або паказаць панэль фільтраў у стужцы, пошуку і звязаных відэа - Схаваць у карме - Схаваны ў стужцы - Паказваецца ў стужцы - Схавацца ў пошуку - Схаваны ў пошуку - Паказваецца ў пошуку - Схаваць у звязаных відэа - Схавана ў звязаных відэа - Паказана ў звязаных відэа - - - Схаваць плаваючую кнопку мікрафона - Кнопка мікрафона схавана - Паказана кнопка мікрафона - - + Адключыць актыўны рэжым у поўнаэкранным рэжыме Навакольны рэжым адключаны Неактыўны рэжым уключаны - + Схаваць інфармацыйныя карткі Інфармацыйныя карткі схаваны Паказваюцца інфармацыйныя карткі - + Адключыць анімацыю рухомых лікаў Лічбы без анімацыі Пракатныя лічбы аніміраваныя - + Схаваць панэль пошуку ў відэаплэеры Панэль пошуку відэаплэера схавана Адлюстроўваецца панэль пошуку відэаплэера @@ -574,18 +552,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Панэль пошуку эскізаў схавана Адлюстроўваецца панэль пошуку эскізаў - + - Схаваць шорты ў хатняй стужцы - Шорты ў хатняй карме схаваныя - Шорты ў хатняй стужцы паказаны - Схаваць Shorts у стужцы падпісак - Шорты ў стужцы падпіскі схаваныя - Шорты ў стужцы падпіскі паказваюцца - Схаваць Shorts у выніках пошуку - Шорты ў выніках пошуку схаваныя - Шорты паказваюцца ў выніках пошуку Схаваць кнопку далучыцца Кнопка «Далучыцца» схавана @@ -651,27 +620,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Панэль навігацыі схавана Паказана панэль навігацыі - + Адключыць канчатковы экран прапанаванага відэа Прапанаваныя відэа будуць адключаны Будуць паказаны прапанаваныя відэа - + Схаваць метку часу відэа Метка часу схавана Адлюстроўваецца метка часу - + Схаваць усплывальныя панэлі прайгравальніка Усплывальныя панэлі прайгравальніка схаваныя Паказваюцца ўсплывальныя панэлі прайгравальніка - + Непразрыстасць накладання прайгравальніка Значэнне непразрыстасці паміж 0-100, дзе 0 - празрысты Непразрыстасць накладання прайгравальніка павінна быць паміж 0-100 - + Адзнакі \"Не падабаецца\" часова недаступныя (час чакання API скончыўся) Дызлайкі недаступныя (статус %d) @@ -715,17 +684,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Ліміт кліенцкай хуткасці сустракаецца %d разоў %d мілісекунд - + Уключыць шырокую панэль пошуку Уключана шырокая панэль пошуку Шырокая панэль пошуку адключана - + Аднавіць старыя мініяцюры панэлі пошуку Эскізы панэлі пошуку з\"явяцца над панэллю пошуку Мініяцюры панэлі пошуку з\"явяцца ў поўнаэкранным рэжыме - + Уключыць SponsorBlock SponsorBlock - гэта краўдсорсінгавая сістэма для пропуску раздражняльных частак відэа YouTube Паглядзіце @@ -904,7 +873,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Пра праграму Дадзеныя прадастаўляюцца API SponsorBlock. Націсніце тут, каб даведацца больш і паглядзець спампоўкі для іншых платформаў - + Версія праграмы Spoof Версія падробленая Версія не падробленая @@ -917,9 +886,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Аднавіць хуткасць шырокага відэа & якаснае меню 18.09.39 - Аднаўленне ўкладкі бібліятэкі 17.41.37 - Аднаўленне старой паліцы плэйлістоў - 17.33.42 - Аднавіць стары макет інтэрфейсу - + Усталяваць стартавую старонку Па змаўчанні Дасьледуйце @@ -929,18 +897,20 @@ This is because Crowdin requires temporarily flattening this file and removing t Падпіскі У трэндзе - + Адключыць аднаўленне прайгравання Shorts Прайгравальнік Shorts не аднаўляецца пры запуску праграмы Прайгравальнік Shorts адновіцца пры запуску праграмы - + + + Уключыць макет планшэта Макет планшэта ўключаны Макет планшэта адключаны Паведамленні ў супольнасці не адлюстроўваюцца на планшэце - + Міні-плэер Змяніце стыль мінімізаванага плэера ў праграме Тып мініплэера @@ -962,24 +932,24 @@ This is because Crowdin requires temporarily flattening this file and removing t Значэнне непразрыстасці паміж 0-100, дзе 0 - празрысты Непразрыстасць накладання міні-плэера павінна быць ад 0 да 100 - + Уключыць градыентны экран загрузкі Экран загрузкі будзе мець градыентны фон Экран загрузкі будзе мець суцэльны фон - + Уключыць уласны колер панэлі пошуку Паказваецца карыстальніцкі колер панэлі пошуку Паказаны зыходны колер панэлі пошуку Карыстальніцкі колер панэлі пошуку Колер панэлі пошуку - + Абыход абмежаванняў рэгіёну Выкарыстанне хаста відарысаў yt4.ggpht.com Выкарыстанне арыгінальнага хаста відарысаў\n\nУключэнне гэтай опцыі можа выправіць адсутнічаючыя відарысы, якія заблакіраваныя ў некаторых рэгіёнах - + Галоўная ўкладка @@ -1011,7 +981,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow часова недаступны (код стану: %s) DeArrow часова недаступны - + Паказаць аб\"явы ReVanced Аб\"явы паказваюцца пры запуску Аб\"явы не паказваюцца пры запуску @@ -1019,46 +989,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Не ўдалося падключыцца да пастаўшчыка аб\"яў расслабіцца - + Увага Больш не паказваць - + Уключыць аўтаматычны паўтор Аўтаматычны паўтор уключаны Аўтаматычны паўтор адключаны - + Памеры падманнага прылады Памеры прылады падробленыя\n\nМожа быць разблакіравана больш высокая якасць відэа, але вы можаце сутыкнуцца з затрымкамі пры прайграванні, пагаршэннем часу аўтаномнай працы і невядомымі пабочнымі эфектамі Памеры прылады не падробленыя\n\nУключэнне гэтага можа разблакіраваць больш высокую якасць відэа Уключэнне гэтага можа прывесці да прыпынкаў прайгравання відэа, пагаршэння тэрміну службы батарэі і невядомых пабочных эфектаў. - + Налады GmsCore Налады для GmsCore - + Абыход URL-перанакіраванняў Перанакіраванне URL абыходзіць URL-перанакіраванні не абыходзяць - + Адкрываць спасылкі ў браўзеры Адкрыццё спасылак звонку Адкрыццё спасылак у праграме - + Выдаліць параметр запыту адсочвання Параметр запыту адсочвання выдалены са спасылак Параметр адсочвання запыту не выдаляецца са спасылак - + Адключыць тактыльны эфект маштабавання Тактыльныя функцыі адключаны Тактыльныя сігналы ўключаны - + Аўтаматычнае якасць Запомніце змены якасці відэа Змены якасці распаўсюджваюцца на ўсе відэа @@ -1069,48 +1039,44 @@ This is because Crowdin requires temporarily flattening this file and removing t Wi-Fi Стандартная якасць %1$s зменена на: %2$s - + Паказаць дыялогавую кнопку хуткасці Паказана кнопка Кнопка не паказваецца - + Карыстальніцкія хуткасці прайгравання - Дадайце або зменіце даступныя хуткасці прайгравання Карыстальніцкія хуткасці павінны быць менш за %s. Выкарыстанне значэнняў па змаўчанні. Няправільныя карыстальніцкія хуткасці прайгравання. Выкарыстанне значэнняў па змаўчанні. - + Запомніце змены хуткасці прайгравання Змяненні хуткасці прайгравання прымяняюцца да ўсіх відэа Змены хуткасці прайгравання прымяняюцца толькі да бягучага відэа Стандартная хуткасць прайгравання Хуткасць па змаўчанні зменена на: %s - + Аднавіць старое меню якасці відэа Паказана старое меню якасці відэа Старое меню якасці відэа не паказваецца - + Уключыць слайд для пошуку Слайд для пошуку ўключаны Слайд для пошуку не ўключаны - + Адключэнне гэтай налады можа выклікаць праблемы з прайграваннем відэа. - - - - + Блакіраваць аўдыярэкламу Аўдыярэклама заблакіравана Аўдыёрэклама разблакіравана - + %s недаступны. Рэклама можа паказвацца. Паспрабуйце пераключыцца на іншую службу блакіроўкі рэкламы ў наладах. Сервер %s вярнуў памылку. Рэклама можа паказвацца. Паспрабуйце пераключыцца на іншую службу блакіроўкі рэкламы ў наладах. Блакіраваць убудаваную відэарэкламу @@ -1118,30 +1084,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Светлавы проксі Проксі PurpleAdBlock - + Блакіраваць відэарэкламу Відэарэклама заблакіравана Відэарэклама разблакіравана - + паведамленне выдалена Паказаць выдаленыя паведамленні Не паказваць выдаленыя паведамленні Схаваць выдаленыя паведамленні за спойлерам Паказаць выдаленыя паведамленні ў выглядзе закрэсленага тэксту - + Аўтаматычна патрабаваць балы канала Канальныя балы зарабляюцца аўтаматычна Ачкі канала не запытваюцца аўтаматычна - + Уключыце рэжым адладкі Twitch Рэжым адладкі Twitch уключаны (не рэкамендуецца) Рэжым адладкі Twitch адключаны - + Налады ReVanced Аб\"явы Налады блакіроўкі рэкламы diff --git a/src/main/resources/addresources/values-bg-rBG/strings.xml b/patches/src/main/resources/addresources/values-bg-rBG/strings.xml similarity index 95% rename from src/main/resources/addresources/values-bg-rBG/strings.xml rename to patches/src/main/resources/addresources/values-bg-rBG/strings.xml index ec3dd0725..2be1d543c 100644 --- a/src/main/resources/addresources/values-bg-rBG/strings.xml +++ b/patches/src/main/resources/addresources/values-bg-rBG/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Проверката е неуспешна Отворете официалния уебсайт Пропусни @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Коригирано преди %s дни Датата на компилация на APK е повредена - + Искате ли да продължите? Нулиране Рестартирай и опресни @@ -62,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Официални линкове Дарение - + GmsCore не е инсталиран. Инсталирайте го. Нужно е действие @@ -73,7 +73,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Относно Реклами Алтернативни миниатюри @@ -85,7 +85,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Разни Видео - + + + Отстраняване на грешки Активиране или деактивиране на отстраняването на грешки Дневник на отстраняването на грешки @@ -102,13 +104,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Системно съобщение няма да бъде показано, ако се появи грешка Ако изключите системните съобщения, ще скриете всички уведомления за ReVanced грешки. \n\nНяма да бъдете уведомени, ако настъпят неочаквани събития. - + Деактивирайте подсветката на бутона Харесвам /Абонамент Бутоните „Харесвам“ и „Абониране“ няма да светят, когато бъдат натиснати Бутоните „Харесвам“ и „Абониране“ ще светят, когато бъдат натиснати - Скриване на сивия разделител - Сивите разделители са скрити - Сивите разделители са показани + \"Карти на албумите\" + Албумните карти са скрити + Албумните карти се показват + Дарителска кутия + Кутията за дарения е скрита + Кутията за дарения се показва + Плаващ бутон за микрофона + Бутонът на микрофона е скрит + Показан е бутон на микрофона Скриване на водния знак на канала Водният знак е скрит Водният знак е показан @@ -153,9 +161,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Скриване на разширяемия чип под видеоклиповете Разширяващите се чипове са скрити Разширяващите се чипове са показани - Скриване на футъра на менюто за качество на видеото - Футърът на менюто за качество на видеото е скрит - Футърът на менюто за качество на видеото е показан Скриване на публикациите от общността Публикациите от общността са скрити Публикациите от общността са показани @@ -230,6 +235,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Разделът за транскрипция е показан Описание на видеото Скриване или показване на компонентите за описание на видеоклиповете + Лента с филтри + Скриване или показване на лентата с категории в емисията, резултатите от търсенето и свързаните видеоклипове + Скриване на горната лента с категории в емисията + Скрита + Показва се + Филтъри на търсене + Панелът с филтъри на търсене е скрит + Панелът с филтъри на търсене се показва + Скриване в сродни видеоклипове + Скриване в сродни видеоклипове + Показано в сродни видеоклипове + Коментари + Скриване или показване на секцията за коментари + Скриване на „Коментари, направени от членове“ + „Коментари от членове“ са скрити + „Коментари от членове“ се показват + Скриване на секцията с коментари + Секцията с коментари е скрита + Секцията с коментари се показва + Бутон за създаване на Shorts + Бутон за създаване на Shorts е скрит + Бутон за създаване на Shorts се показва + Преглед на коментари + Прегледа на коментари е скрит + Прегледа на коментари се показва + Бутон за благодарност + Бутона за благодарност е скрит + Бутона за благодарност се показва + Бутони в лентата на прогреса и емотикони + Бутоните са скрити + Бутоните се показват YouTube Doodles Doodles в лентата за търсене са скрити @@ -271,7 +307,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Ключовата дума е твърде кратка и изисква кавички: %s Всички видеа с ключовата дума ще бъдат скрити: %s - + Скриване на общите реклами Общите реклами са скрити Общите реклами са показани @@ -306,17 +342,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Скр. на реклами на цял екран, за по-стари устройства - + Скриване на YouTube Premium промоции YouTube Premium промоциите под видео плейъра са скрити YouTube Premium промоциите под видео плейъра са показани - + Скриване на видео рекламите Видео рекламите са скрити Видео рекламите са показани - + URL адресът е копиран в клипборда URL адресът с времеви отпечатък е копиран Показване на бутона за копиране на URL адреса на видеоклипа @@ -326,13 +362,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Показан е бутон. Докоснете, за да копирате URL адреса на видеоклипа с клеймо за време. Докоснете и задръжте, за да копирате видеоклип без клеймо за време Бутонът не е показан - + Скриване на прозореца за възрастово ограничение Диалоговият прозорец ще бъде премахнат Диалоговият прозорец ще бъде показан Тази функция не заобикаля възрастовото ограничение. Тя просто приема възрастовата граница автоматично. - + Външни изтегляния Настройки за използване на външно приложение за изтегляне Показване на бутона за изтегляне чрез външно приложение @@ -346,17 +382,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Име на пакета на приложението за изтегляне, като NewPipe или Seal %s не е инсталиран. Инсталирайте го. - + Деактивиране на жеста за точно търсене Жестът е деактивиран Жестът е активиран - + Активиране на докосването на лентата за време Докосването на лентата за време е включено Докосването на лентата за време е изключено - + Задаване на яркост чрез плъзгане Задаването на яркост чрез плъзгане е включено Задаването на яркост чрез плъзгане е изключено @@ -385,12 +421,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Праг на величината на плъзгане Праг преди да се осъществи плъзгането - + Автоматични Субтитри Автоматичните Субтитри са деактивирани Автоматичните Субтитри са активирани - + Бутони за действия Скриване или показване на бутони под видеото Бутони \"Харесвам\" и \"Не харесвам\" @@ -426,23 +462,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Бутонът за Запазване в плейлиста е скрит Бутонът за Запазване в плейлиста се показва - - Бутона за авт. изпълнение - Бутона за авт. изпълнение е скрит - Бутона за авт. изпълнение се показва - - - - Бутона за Субтитри - Бутона за субтити е скрит - Бутона за субтити се показва - - - Бутон за предаване на Тв - Бутонът за предаване е скрит - Бутонът за предаване се показва - - + Бутони за навигация Скриване или промяна на бутоните в лентата за навигация @@ -450,9 +470,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Бутона за начало е скрит Бутона за начало се показва - Бутон за Кратки клипове - Бутона за кратки клипове е скрит - Бутона за кратки клипове се показва + Скриване на Shorts + Бутонът Shorts е скрит + Показан е бутон Shorts Бутон за създаване на клип Бутонът за създаване е скрит @@ -469,7 +489,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Етикетите са скрити Етикетите се показват - + Падащо меню Скриване или показване на елементи от падащото меню на плейъра @@ -480,6 +500,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Меню \"Допълнителни настройки\" Допълнителните настройки са скрити Допълнителните настройки се показват + + Менюто на таймера за заспиване + Менюто на таймера за заспиване е скрито + Менюто на таймера за заспиване се показва \"Повторно възпроизвеждане\" Менюто за повторение е скрито @@ -488,6 +512,8 @@ This is because Crowdin requires temporarily flattening this file and removing t Подсветка около видеото Менюто за подсветка около видеото е скрито Менюто за подсветка около видеото се показва + Постоянно ниво на звука се показва + Постоянно ниво на звука е скрито Помощ & Отзиви Менюто & за помощ е скрито @@ -513,83 +539,44 @@ This is because Crowdin requires temporarily flattening this file and removing t Гледайте във VR Менюто за гледане в VR е скрито Менюто за гледане в VR се показва + Скриване на футъра на менюто за качество на видеото - - Бутони за Предишно & Следващо видео - Бутоните са скрити - Бутоните се показват + + Бутони за Предишно & Следващо видео + Бутоните са скрити + Бутоните се показват + Бутон за предаване на Тв + Бутонът за предаване е скрит + Бутонът за предаване се показва + + Бутона за Субтитри + Бутона за субтити е скрит + Бутона за субтити се показва + Бутона за авт. изпълнение + Бутона за авт. изпълнение е скрит + Бутона за авт. изпълнение се показва - - \"Карти на албумите\" - Албумните карти са скрити - Албумните карти се показват - - - Коментари - Скриване или показване на секцията за коментари - Скриване на „Коментари, направени от членове“ - „Коментари от членове“ са скрити - „Коментари от членове“ се показват - Скриване на секцията с коментари - Секцията с коментари е скрита - Секцията с коментари се показва - Бутон за създаване на Shorts - Бутон за създаване на Shorts е скрит - Бутон за създаване на Shorts се показва - Преглед на коментари - Прегледа на коментари е скрит - Прегледа на коментари се показва - Бутон за благодарност - Бутона за благодарност е скрит - Бутона за благодарност се показва - Бутони в лентата на прогреса и емотикони - Бутоните са скрити - Бутоните се показват - - - Дарителска кутия - Кутията за дарения е скрита - Кутията за дарения се показва - - + Скриване на препоръките в края Препоръките в края са скрити Препоръките в края се показват - - Лента с филтри - Скриване или показване на лентата с категории в емисията, резултатите от търсенето и свързаните видеоклипове - Скриване на горната лента с категории в емисията - Скрита - Показва се - Филтъри на търсене - Панелът с филтъри на търсене е скрит - Панелът с филтъри на търсене се показва - Скриване в сродни видеоклипове - Скриване в сродни видеоклипове - Показано в сродни видеоклипове - - - Плаващ бутон за микрофона - Бутонът на микрофона е скрит - Показан е бутон на микрофона - - + Деактивирайте подсветка около видеото на цял екран Подсветката в режим на цял екран е деактивирана Подсветката в режим на цял екран е активирана - + Скриване на инфо. карти Информационните карти са скрити Информационните карти се показват - + Анимация на числа в реално време Анимацията е деактивирана Анимацията е активирана - + Скриване на лента за време на плейъра Лентата за време на плейъра е скрита Лентата за време на плейъра се показва @@ -597,7 +584,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Лентата за време при миниатюрите е скрита Лентата за време при миниатюрите се показва - + + Играч на Shorts + Скриване или показване на компоненти в Shorts плейъра Скриване на Shorts в началната лента Shorts в началната лента са скрити @@ -692,27 +681,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Навигационната лента е скрита Навигационната лента се показва - + Препоръчани видеоклипове в края Препоръчаните видеоклипове в края са скрити Препоръчаните видеоклипове в края се показват - + Скриване на клеймото за време на видеоклипа Скрито Показва се - + Изскачащи панели на плейъра Изскачащите панели на плейъра са скрити Изскачащите панели на плейъра се показват - + Прозрачност на настройките в Плеара Стойност на прозрачност между 0-100, където 0 е прозрачно Прозрачността на менюто на плейъра трябва да бъде между 0-100 - + Нехаресванията временно не са налични (API timed out) Нехаресванията не са налични (status %d) @@ -756,17 +745,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Открити са ограничения на скоростта на клиента на Api %d пъти %d милисекунди - + Широка лента за търсене Широката лента за търсене е включена Широката лента за търсене е изключена - + Стари миниатюри на времевата линия Над лентата за възпроизвеждане се появяват миниатюри Миниатюрите се показват в режим на цял екран - + Включване на SponsorBlock SponsorBlock е система за прескачане на досадни части и реклами от видеоклиповете в YouTube Облик @@ -947,7 +936,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Относно Данните са предоставени от SponsorBlock API. Докоснете тук за повече информация и изтеглияния - + Подлъгване за версията на приложението Подправена версия Не подправена версия @@ -960,30 +949,39 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Възстановяване на видео скорост & в менюто за качество 18.09.39 - Възстановяване на таб \"Библиотека\" 17.41.37 - Връщане на секцията с плейлиста към стария стил - 17.33.42 - Възстановява стария изглед - + Задай начална страница По подразбиране + Разглеждане на канала Разгледайте + Игри История + Библиотека Харесани видеа + На Живо + Филми + Музика Търсене + Спорт Абонаменти Популярни + Гледай по-късно - + Скриване на Shorts плейъра при стартиране Shorts плейъра при стартиране на приложението е скрит Shorts плейъра при стартиране на приложението се показва - + + + Включи режим за таблет Режим за таблет е вкл. Режим за таблет е изкл. Публикациите в общността не се показват на оформления за таблет - + Минимизиран екран за възпроизвеждане Променете стила на минимизирания екран за възпроизвеждане Минимизиран тип екран за гледане @@ -1001,28 +999,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Бутони за напред и назад Бутони за напред и назад са скрити Бутони за напред и назад са показани + Първоначален размер + Първоначален размер на екрана, в пиксели Прозрачност на менютата Стойност на прозрачност между 0-100, където 0 е прозрачно Прозрачността на менюто трябва да бъде между 0-100 - + Фон на екрана при зареждане на видео Екранът за зареждане ще има градиентен фон Екранът за зареждане ще има плътен фон - + Промяна на цвета на индикатора за време Показва се персонализиран цвят на лентата за напредък Показва се оригиналния цвят на лентата за напредък Персонализиран цвят на лентата за напредък Цветове на лентата за напредък - + Прескочете забраната за зареждане на изображение Домейнът yt4.ggpht.com се използва за зареждане на изображения Оригиналният домейн се използва за зареждане на изображения\n\nАктивирането на тази настройка може да коригира зареждането на изображения, които са блокирани в някои региони - + Раздел Начало @@ -1054,7 +1054,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow временно не е наличен. (код на състоянието: %s) DeArrow временно не е наличен - + Показване на ReVanced съобщения Съобщенията се показват при стартиране Съобщенията не се показват при стартиране @@ -1062,47 +1062,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Неуспешно свързване с доставчик на съобщения Отхвърли - + Предупреждение Историята ви на гледане не се запазва.<br><br>Това най-вероятно е причинено от DNS блокиращ реклами или мрежов прокси.<br><br>За да коригирате това, поставете <b>s.youtube в белия списък.com</b> или изключете всички DNS блокери и проксита. Не показвай отново - + Автоматично повтаряне на текущия видеоклип Включено автоматично повтаряне на текущия видеоклип Изключено автоматично повтаряне на текущия видеоклип - + Лъжливи параметри на устройството Подправената резолюция на устройството\n\nМоже да се отключи по-високо качество на видеото, но може да изпитате засичане при възпроизвеждане на видео, по-лош живот на батерията и неизвестни странични ефекти Резолюцията на устройството не е подправена\n\nАктивирането на това не може да отключи по-високо качество на видеото Разрешаването на това може да причини прекъсване на възпроизвеждането на видео, влошен живот на батерията и неизвестни странични ефекти. - + GmsCore Настройки Настройки на GmsCore - + Заобикаляне на URL пренасочване URL пренасочванията се заобикалят URL пренасочванията не се заобикалят - + Отваряне на връзки в браузъра Отваряне на външни връзки Отваряне на връзки в приложението - + Премахнете параметъра на заявката за проследяване Параметърът на заявката за проследяване е премахнат от връзките Параметърът на заявката за проследяване не е премахнат от връзките - + Деактивиране на вибрация при мащабиране Вибрациите са деактивирани Вибрациите са активирани - + Автоматично качество Запомни промените в качеството на видеото Промените в качеството се отнасят за всички видеоклипове @@ -1113,35 +1113,35 @@ This is because Crowdin requires temporarily flattening this file and removing t wi-fi Променено стандартно %1$s качество на: %2$s - + Показване бутон за скорост Бутонът е показан Бутонът не е показан - + Персонализирани скорости на възпроизвеждане - Добавете или променете наличните скорости на възпроизвеждане + Добавете или променете скоростa на възпроизвеждане Персоналната скорост трябва да е по-малка от %s. Използване на стойности по подразбиране. Невалидни персонализирани скорости на възпроизвеждане. Използване на стойности по подразбиране. - + Запомни промените в скоростта на възпроизвеждане Промените в скоростта на възпроизвеждане се отнасят за всички видеоклипове Промените в скоростта на възпроизвеждане се отнасят само за текущия видеоклип Скорост на възпроизвеждане по подразбиране Скоростта по подразбиране е променена на: %s - + Възстановете старото меню за качество на видеото Показва се старото меню за видео качество Старото меню за видео качество е скрито - + Активиране на слайд за превъртане Слайд за превъртане е активиран Слайд за превъртане е деактивиран - + Подправяне на видео потоци Подправете клиентските видео потоци, за да предотвратите проблеми с възпроизвеждането Подправяне на видео потоци @@ -1159,17 +1159,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Странични ефекти от подправяне на Android VR • Липсва менюто за избор аудио\n• Не е налична стабилна сила на звука - - - - + Аудио реклами Аудио рекламата е блокирана Аудио рекламата е разблокирана - + %s е недостъпен. Може да се показват реклами. Опитайте друга услуга за блокиране на реклами. %s сървърът върна грешка. Може да се показват реклами. Опитайте да превключите друга услуга за блокиране на реклами. Блокиране на вградени видеореклами @@ -1177,30 +1174,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Luminous прокси PurpleAdBlock прокси - + Видео реклами Видео рекламата е блокирана Видео рекламата е разблокирана - + съобщението е изтрито Покажи изтритите съобщения Не показвай изтритите съобщения Скрийте изтритите съобщения зад спойлер Показване на изтритите съобщения като зачеркнат текст - + Автоматично изискване на Channel Points Автоматично изискване на Channel Points Channel Points в канала не се изискват автоматично - + Активирайте режима за отстраняване на грешки в Twitch Режимът за отстраняване на грешки в Twitch е активиран (не се препоръчва) Режимът за отстраняване на грешки в Twitch е деактивиран - + Настройки на ReVanced Реклами Настройки за блокиране на реклами diff --git a/src/main/resources/addresources/values-bn-rBD/strings.xml b/patches/src/main/resources/addresources/values-bn-rBD/strings.xml similarity index 96% rename from src/main/resources/addresources/values-bn-rBD/strings.xml rename to patches/src/main/resources/addresources/values-bn-rBD/strings.xml index 96a0208f1..4d620b162 100644 --- a/src/main/resources/addresources/values-bn-rBD/strings.xml +++ b/patches/src/main/resources/addresources/values-bn-rBD/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + চেক ফেইল করেছে অফিশ্যাল ওয়েবসাইট খুলুন অবজ্ঞা করুন @@ -42,7 +42,7 @@ This is because Crowdin requires temporarily flattening this file and removing t %s দিন আগে প্যাচ করা হয়েছে APK তৈরির তারিখ ত্রুটিপূর্ণ - + আপনি কি এগিয়ে যেতে ইচ্ছুক? আবার সেট করুন রিফ্রেশ করুন এবং আবার চালু করুন @@ -61,7 +61,7 @@ This is because Crowdin requires temporarily flattening this file and removing t অফিশ্যাল লিংকসমূহ দান করুন - + MicroG GmsCore ইনস্টল করা হয়নি। ইনস্টল করুন। পদক্ষেপ প্রয়োজন @@ -72,7 +72,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + সম্পর্কিত বিজ্ঞাপন বিকল্প থাম্বনেইল @@ -84,7 +84,9 @@ This is because Crowdin requires temporarily flattening this file and removing t বিবিধ ভিডিও - + + + ডিবাগিং ডিবাগিং অপশন সক্রিয় বা নিষ্ক্রিয় করুন ডিবাগ লগিং @@ -101,13 +103,19 @@ This is because Crowdin requires temporarily flattening this file and removing t কোন ত্রুটি দেখা গেলে টোস্ট দেখায় না ত্রুটির টোস্ট দেখানো বন্ধ করলে তা ReVanced এর সকল ত্রুটির বিজ্ঞপ্তি লুকিয়ে রাখবে।\n\nআপনি কোন অনাকাঙ্ক্ষিত ঘটনার বিজ্ঞপ্তি পাবেন না। - + পছন্দ / সদস্যতা বোতামের উজ্জ্বলতা নিষ্ক্রিয় করুন পছন্দ এবং সদস্যতা বোতাম যখন উল্লেখ করা হবে উজ্জ্বলতা দিবে না পছন্দ এবং সদস্যতা বোতাম যখন উল্লেখ করা হবে উজ্জ্বলতা দিবে - ধূসর বিভাজক লুকান - ধূসর বিভাজক লুকিয়ে রয়েছে - ধূসর বিভাজক প্রদর্শিত হয়েছে + অ্যালবাম কার্ড লুকান + অ্যালবাম কার্ড লুকিয়ে রয়েছে + অ্যালবাম কার্ড প্রদর্শিত হয়েছে + গণ-অর্থায়ন বাক্স লুকান + গণ-অর্থায়ন বাক্স লুকিয়ে রয়েছে + গণ-অর্থায়ন বাক্স প্রদর্শিত হয়েছে + ভাসমান মাইক্রোফোন বোতাম লুকান + মাইক্রোফোন বোতাম লুকিয়ে রয়েছে + মাইক্রোফোন বোতাম প্রদর্শিত হয়েছে চ্যানেল জলছাপ লুকান জলছাপ লুকানো আছে জলছাপ দেখানো আছে @@ -152,9 +160,6 @@ This is because Crowdin requires temporarily flattening this file and removing t ভিডিওর নিচের সম্প্রসারণযোগ্য চিপস লুকান সম্প্রসারণযোগ্য চিপস লুকিয়ে রয়েছে সম্প্রসারণযোগ্য চিপস প্রদর্শিত হয়েছে - ভিডিও গুণমান মেনুর ফুটার লুকান - ভিডিও গুণমান মেনুর ফুটার লুকিয়ে রয়েছে - ভিডিও গুণমান মেনুর ফুটার প্রদর্শিত হয়েছে সম্প্রদায় পোস্ট লুকান সম্প্রদায় পোস্ট লুকিয়ে রয়েছে সম্প্রদায় পোস্ট প্রদর্শিত হয়েছে @@ -222,6 +227,37 @@ This is because Crowdin requires temporarily flattening this file and removing t ট্রান্সস্ক্রিপ্ট বিভাগ প্রদর্শিত হয়েছে ভিডিওর বিবরণ ভিডিও বিবরণ এর উপাদান লুকান বা প্রদর্শন করুন + ফিল্টার বার + ফিড, অনুসন্ধান এবং সম্পর্কিত ভিডিওতে ফিল্টার বার লুকান বা প্রদর্শন করুন + ফিডে লুকান + ফিডে লুকিয়ে রয়েছে + ফিডে প্রদর্শিত হয়েছে + অনুসন্ধানে লুকান + অনুসন্ধানে লুকিয়ে রয়েছে + অনুসন্ধানে প্রদর্শিত হয়েছে + সম্পর্কিত ভিডিওতে লুকান + সম্পর্কিত ভিডিওতে লুকিয়ে রয়েছে + সম্পর্কিত ভিডিওতে প্রদর্শিত হয়েছে + মন্তব্য + মন্তব্য বিভাগের উপাদানগুলি লুকান বা দেখান৷ + \'মেম্বারদের মন্তব্য\' হেডার লুকান + \'মেম্বারদের মন্তব্য\' হেডার লুকিয়ে রয়েছে + \'মেম্বারদের মন্তব্য\' হেডার প্রদর্শিত হয়েছে + মন্তব্য বিভাগ লুকান + মন্তব্য বিভাগ লুকিয়ে রয়েছে + মন্তব্য বিভাগ প্রদর্শিত হয়েছে + \'Short তৈরি করুন\' বোতাম লুকান + \'Short তৈরি করুন\' বোতাম লুকিয়ে রয়েছে + \'Short তৈরি করুন\' বোতাম প্রদর্শিত হয়েছে + মন্তব্যের পূর্বরূপ লুকান + মন্তব্যের পূর্বরূপ লুকিয়ে রয়েছে + মন্তব্যের পূর্বরূপ প্রদর্শিত হয়েছে + ধন্যবাদ বোতাম লুকান + ধন্যবাদ বোতাম লুকিয়ে রয়েছে + ধন্যবাদ বোতাম প্রদর্শিত হয়েছে + টাইমস্ট্যাম্প ও ইমোজি বোতাম লুকান + টাইমস্ট্যাম্প ও ইমোজি বোতাম লুকিয়ে রয়েছে + টাইমস্ট্যাম্প ও ইমোজি বোতাম প্রদর্শিত হয়েছে কাস্টম ফিল্টার কাস্টম ফিল্টার ব্যবহার করে বিভিন্ন উপাদান লুকান @@ -250,7 +286,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + সাধারণ বিজ্ঞাপন লুকান সাধারণ বিজ্ঞাপন লুকিয়ে রয়েছে সাধারণ বিজ্ঞাপন প্রদর্শিত হয়েছে @@ -285,17 +321,17 @@ This is because Crowdin requires temporarily flattening this file and removing t পূর্ণস্ক্রীন বিজ্ঞাপন লুকানো পুরোনো ডিভাইসে কাজ করে - + YouTube প্রিমিয়াম প্রচারণা লুকান ভিডিওর নিচের YouTube প্রিমিয়াম প্রচারণা লুকিয়ে রয়েছে ভিডিওর নিচের YouTube প্রিমিয়াম প্রচারণা প্রদর্শিত হয়েছে - + ভিডিও বিজ্ঞাপন লুকান ভিডিও বিজ্ঞাপন লুকিয়ে রয়েছে ভিডিও বিজ্ঞাপন প্রদর্শিত হয়েছে - + ক্লিপবোর্ডে URL অনুলিপি করা হয়েছে টাইমস্ট্যাম্প সহ URL অনুলিপি করা হয়েছে ভিডিও URL অনুলিপি বোতাম দেখান @@ -305,13 +341,13 @@ This is because Crowdin requires temporarily flattening this file and removing t বোতাম প্রদর্শিত হয়েছে। টাইমস্ট্যাম্প সহ URL অনুলিপি করতে ট্যাপ করুন। টাইমস্ট্যাম্প ছাড়া URL অনুলিপি করতে ট্যাপ করে ধরে রাখুন। বোতাম প্রদর্শিত হয়নি - + দর্শকের বিচক্ষণতা ডায়ালগ সরান ডায়ালগ সরানো হবে ডায়ালগ প্রদর্শিত হবে এটি বয়সের সীমাবদ্ধতাকে বাইপাস করে না। এটা শুধু স্বয়ংক্রিয়ভাবে গ্রহণ করে। - + বাহিরে ডাউনলোড বাহিরের ডাউনলোডার ব্যবহার করার সেটিং বাহিরের ডাউনলোডার বাটন দেখান @@ -325,17 +361,17 @@ This is because Crowdin requires temporarily flattening this file and removing t আপনার ইনস্টল করা বাইরের ডাউনলোডার অ্যাপের প্যাকেজ নাম, যেমন NewPipe বা Seal %s ইনস্টল করা নেই, ইনস্টল করুন। - + ভিডিওর নির্দিষ্ট অংশে যাওয়ার অঙ্গভঙ্গি নিষ্ক্রিয় করুন অঙ্গভঙ্গি নিষ্ক্রিয় করা হয়েছে অঙ্গভঙ্গি সক্রিয় করা হয়েছে - + সিকবারে চাপ দেওয়া সক্রিয় করুন সিকবারে চাপ দেওয়া সক্রিয় করা হয়েছে সিকবারে চাপ দেওয়া নিষ্ক্রিয় করা হয়েছে - + উজ্জ্বলতার সোয়াইপ অঙ্গভঙ্গি সক্রিয় করুন উজ্জ্বলতা সোয়াইপ সক্রিয় করা হয়েছে উজ্জ্বলতা সোয়াইপ নিষ্ক্রিয় করা হয়েছে @@ -361,10 +397,10 @@ This is because Crowdin requires temporarily flattening this file and removing t সোয়াইপ ওভারলে ব্যাকগ্রাউন্ডের দৃশ্যমানতা সোয়াইপ থ্রেশহোল্ড এর মাত্রা - + স্বয়ংক্রিয় ক্যাপশন বন্ধ করুন - + @@ -382,17 +418,7 @@ This is because Crowdin requires temporarily flattening this file and removing t ক্লিপ বোতাম প্রদর্শিত হয়েছে - - - - - - - কাস্ট বাটন লুকান - কাস্ট বাটন লুকিয়ে রয়েছে - কাস্ট বাটন প্রদর্শিত হয়েছে - - + নেভিগেশন বোতাম নেভিগেশন বারে বোতাম লুকান বা পরিবর্তন করুন @@ -419,7 +445,7 @@ This is because Crowdin requires temporarily flattening this file and removing t লেবেল লুকিয়ে রয়েছে লেবেল প্রদর্শিত হয়েছে - + ফ্লাইআউট মেনু ফ্লাইআউট মেনুর আইটেম দেখান বা লুকান @@ -430,6 +456,7 @@ This is because Crowdin requires temporarily flattening this file and removing t আরও সেটিংস দেখুন লুকান আরও সেটিংস দেখুন মেনু লুকিয়ে রয়েছে আরও সেটিংস দেখুন মেনু প্রদর্শিত হয়েছে + ভিডিও লুপ করুন লুকান ভিডিও লুপ করুন মেনু লুকিয়ে রয়েছে @@ -463,83 +490,38 @@ This is because Crowdin requires temporarily flattening this file and removing t ভিআর-এ ঘড়ি লুকান ভিআর মেনুতে দেখুন লুকানো আছে ভিআর মেনুতে দেখুন দেখানো হয়েছে + ভিডিও গুণমান মেনুর ফুটার লুকান - - পূর্ববর্তী লুকান & পরবর্তী ভিডিও বোতাম - বোতাম লুকানো হয় - বোতাম দেখানো হয় + + পূর্ববর্তী লুকান & পরবর্তী ভিডিও বোতাম + বোতাম লুকানো হয় + বোতাম দেখানো হয় + কাস্ট বাটন লুকান + কাস্ট বাটন লুকিয়ে রয়েছে + কাস্ট বাটন প্রদর্শিত হয়েছে + - - অ্যালবাম কার্ড লুকান - অ্যালবাম কার্ড লুকিয়ে রয়েছে - অ্যালবাম কার্ড প্রদর্শিত হয়েছে - - - মন্তব্য - মন্তব্য বিভাগের উপাদানগুলি লুকান বা দেখান৷ - \'মেম্বারদের মন্তব্য\' হেডার লুকান - \'মেম্বারদের মন্তব্য\' হেডার লুকিয়ে রয়েছে - \'মেম্বারদের মন্তব্য\' হেডার প্রদর্শিত হয়েছে - মন্তব্য বিভাগ লুকান - মন্তব্য বিভাগ লুকিয়ে রয়েছে - মন্তব্য বিভাগ প্রদর্শিত হয়েছে - \'Short তৈরি করুন\' বোতাম লুকান - \'Short তৈরি করুন\' বোতাম লুকিয়ে রয়েছে - \'Short তৈরি করুন\' বোতাম প্রদর্শিত হয়েছে - মন্তব্যের পূর্বরূপ লুকান - মন্তব্যের পূর্বরূপ লুকিয়ে রয়েছে - মন্তব্যের পূর্বরূপ প্রদর্শিত হয়েছে - ধন্যবাদ বোতাম লুকান - ধন্যবাদ বোতাম লুকিয়ে রয়েছে - ধন্যবাদ বোতাম প্রদর্শিত হয়েছে - টাইমস্ট্যাম্প ও ইমোজি বোতাম লুকান - টাইমস্ট্যাম্প ও ইমোজি বোতাম লুকিয়ে রয়েছে - টাইমস্ট্যাম্প ও ইমোজি বোতাম প্রদর্শিত হয়েছে - - - গণ-অর্থায়ন বাক্স লুকান - গণ-অর্থায়ন বাক্স লুকিয়ে রয়েছে - গণ-অর্থায়ন বাক্স প্রদর্শিত হয়েছে - - + শেষ স্ক্রীন কার্ড লুকান শেষ স্ক্রীন কার্ড লুকিয়ে রয়েছে শেষ স্ক্রীন কার্ড প্রদর্শিত হয়েছে - - ফিল্টার বার - ফিড, অনুসন্ধান এবং সম্পর্কিত ভিডিওতে ফিল্টার বার লুকান বা প্রদর্শন করুন - ফিডে লুকান - ফিডে লুকিয়ে রয়েছে - ফিডে প্রদর্শিত হয়েছে - অনুসন্ধানে লুকান - অনুসন্ধানে লুকিয়ে রয়েছে - অনুসন্ধানে প্রদর্শিত হয়েছে - সম্পর্কিত ভিডিওতে লুকান - সম্পর্কিত ভিডিওতে লুকিয়ে রয়েছে - সম্পর্কিত ভিডিওতে প্রদর্শিত হয়েছে - - - ভাসমান মাইক্রোফোন বোতাম লুকান - মাইক্রোফোন বোতাম লুকিয়ে রয়েছে - মাইক্রোফোন বোতাম প্রদর্শিত হয়েছে - - + পূর্ণ স্ক্রীনে অ্যাম্বিয়েন্ট মোড নিষ্ক্রিয় করুন অ্যাম্বিয়েন্ট মোড নিষ্ক্রিয় করা হয়েছে অ্যাম্বিয়েন্ট মোড সক্রিয় করা হয়েছে - + তথ্য কার্ড লুকান তথ্য কার্ড লুকিয়ে রয়েছে তথ্য কার্ড প্রদর্শিত হয়েছে - + রোলিং নাম্বার অ্যানিমেশন নিষ্ক্রিয় করুন রোলিং নাম্বার অ্যানিমেটেড নয় রোলিং নাম্বার অ্যানিমেটেড - + ভিডিও প্লেয়ারে সিকবার লুকান ভিডিও প্লেয়ারে সিকবার লুকিয়ে রয়েছে ভিডিও প্লেয়ারে সিকবার প্রদর্শিত হয়েছে @@ -547,7 +529,7 @@ This is because Crowdin requires temporarily flattening this file and removing t থাম্বনেইলে সিকবার লুকিয়ে রয়েছে থাম্বনেইলে সিকবার প্রদর্শিত হয়েছে - + প্রধান ফিডে Shorts লুকান প্রধান ফিডে Shorts লুকিয়ে রয়েছে @@ -631,27 +613,27 @@ This is because Crowdin requires temporarily flattening this file and removing t নেভিগেশন বার লুকিয়ে রয়েছে পনেভিগেশন বার প্রদর্শিত হয়েছে - + ভিডিওর শেষ স্ক্রিণে সাজেস্ট করা ভিডিও নিষ্ক্রিয় করুন সাজেস্ট করা ভিডিও নিস্ক্রিয় করা হবে সাজেস্ট করা ভিডিও প্রদর্শিত হবে - + ভিডিওর সময়স্ট্যাম্প লুকান সময়স্ট্যাম্প লুকিয়ে রয়েছে সময়স্ট্যাম্প প্রদর্শিত হয়েছে - + প্লেয়ার পপআপ প্যানেলগুলো লুকান প্লেয়ার পপআপ প্যানেলগুলো লুকিয়ে রয়েছে প্লেয়ার পপআপ প্যানেলগুলো প্রদর্শিত হয়েছে - + প্লেয়ার ওভারলে অস্বচ্ছতা অসচ্ছতা মান ০-১০০ এর মধ্যে, যেখানে ০ হল সম্পূর্ণ স্বচ্ছ প্লেয়ার ওভারলে অস্বচ্ছতা অবশ্যই ০-১০০ এর মধ্যে হতে হবে - + অপছন্দ সাময়িকভাবে উপলভ্য নয় (API সময় শেষ হয়েছে) অপছন্দ উপলভ্য নয় (অবস্থা %d) @@ -695,17 +677,17 @@ This is because Crowdin requires temporarily flattening this file and removing t %d বার ক্লায়েন্ট রেট লিমিট এর সম্মুখীন হয়েছে %d মিলিসেকেন্ড - + প্রশস্ত অনুসন্ধান বার সক্রিয় করুন প্রশস্ত অনুসন্ধান বার সক্রিয় হয়েছে প্রশস্ত অনুসন্ধান বার নিষ্ক্রিয় হয়েছে - + পুরোনো সিকবার থাম্বনেইল পুনরুদ্ধার করুন সিকবার এর উপরে সিকবার থাম্বনেইল দেখানো হবে পূর্ণস্ক্রীণে সিকবার থাম্বনেইল দেখানো হবে - + SponsorBlock সক্রিয় করুন SponsorBlock হল ইউটিউব ভিডিওতে বিরক্তিকর অংশ গুলো এড়িয়ে যাওয়ার জন্য একটি ক্রাউড-সোর্স পদ্ধতি রূপ @@ -883,7 +865,7 @@ This is because Crowdin requires temporarily flattening this file and removing t সম্পর্কিত ডেটা SponsorBlock API দ্বারা সরবরাহ করা হয়। আরও জানতে এবং অন্যান্য প্ল্যাটফর্মের ডাউনলোড দেখতে এখানে ট্যাপ করুন - + অ্যাপ সংস্করণ স্পুফ করুন সংস্করণ স্পুফ করা হয়েছে সংস্করণ স্পুফ করা হয়নি @@ -896,9 +878,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - প্রশ্বস্ত ভিডিও স্পিড এবং গুণমান মেনু পুনরুদ্ধার করে 18.09.39 - লাইব্রেরি ট্যাপ পুনরুদ্ধার করে 17.41.37 - পুরোনো প্লেলিস্ট শেলফ পুনরুদ্ধার করে - 17.33.42 - পুরোনো UI লেআউট পুনরুদ্ধার করে - + শুরুর পৃষ্ঠা সেট করুন পূর্ব-নির্ধারিত ঘুরে দেখুন @@ -908,18 +889,20 @@ This is because Crowdin requires temporarily flattening this file and removing t সদস্যতা এখন জনপ্রিয় - + Shorts প্লেয়ার আবার চালানো নিষ্ক্রিয় করুন অ্যাপের শুরুতে Shorts প্লেয়ার আবার চলবে না অ্যাপের শুরুতে Shorts প্লেয়ার আবার চলবে - + + + ট্যাবলেট লেআউট সক্রিয় করুন ট্যাবলেট লেআউট সক্রিয় হয়েছে ট্যাবলেট লেআউট নিষ্ক্রিয় হয়েছে ট্যাবলেট লেআউটে কমিউনিটি পোস্ট দেখাবে না - + মিনিপ্লেয়ার অ্যাপের মধ্যকার মিনিমাইজড প্লেয়ার এর ধরণ পরিবর্তন করুন মিনিপ্লেয়ার ধরণ @@ -940,21 +923,21 @@ This is because Crowdin requires temporarily flattening this file and removing t অসচ্ছতা মান ০-১০০ এর মধ্যে, যেখানে ০ হল সম্পূর্ণ স্বচ্ছ মিনিপ্লেয়ার ওভারলে অস্বচ্ছতা অবশ্যই ০-১০০ এর মধ্যে হতে হবে - + গ্রেডিয়েন্ট লোডিং স্ক্রিণ সক্রিয় করুন লোডিং স্ক্রিণে একটি গ্রেডিয়েন্ড ব্যাকগ্রাউন্ড থাকবে লোডিং স্ক্রিণে একটি সলিড ব্যাকগ্রাউন্ড থাকবে - + সিকবারে নিজস্ব রং সক্রিয় করুন সিকবারে নিজস্ব রং প্রদর্শিত হয়েছে সিকবারে মূল রং প্রদর্শিত হয়েছে নিজস্ব সিকবার রং সিকবারের রং - + - + হোম ট্যাব @@ -986,7 +969,7 @@ This is because Crowdin requires temporarily flattening this file and removing t সাময়িকভাবে DeArrow উপলভ্য নয় (স্টাটাস কোড: %s) DeArrow সাময়িকভাবে উপলভ্য নয় - + ReVanced ঘোষণা দেখান শুরুতে ঘোষণা প্রদর্শিত হয়েছে শুরুতে ঘোষণা প্রদর্শিত হয়নি @@ -994,46 +977,46 @@ This is because Crowdin requires temporarily flattening this file and removing t ঘোষনাদাতার সাথে সম্পর্ক স্থাপন ব্যর্থ হয়েছে বাতিল করুন - + সতর্কীকরণ আবার দেখাবেন না - + স্বয়ংক্রিয়ভাবে-আবার দেখানো সক্রিয় করুন স্বয়ংক্রিয়ভাবে-আবার দেখানো সক্রিয় হয়েছে স্বয়ংক্রিয়ভাবে-আবার দেখানো সক্রিয় নিষ্ক্রিয় হয়েছে - + ডিভাইস ডাইমেনশন স্পুফ করুন ডিভাইস ডাইমেনশন স্পুফ হয়েছে\n\nভিডিওর উন্নত গুণমান আনলক হয়েছে কিন্তু আপনি ভিডিও চলার ক্ষেত্রে আটকে চলা, খারাপ ব্যাটারি লাইফ এবং অজানা পার্শ্ব-প্রতিক্রিয়ার সম্মুখিন হতে পারেন ডিভাইস ডাইমেনশন স্পুফ হয়নি\n\nএটি সক্রিয় করার ফলে উন্নত ভিডিও গুণমান আনলক হবে এটি সক্রিয় করার ফলে আপনি ভিডিও চলার ক্ষেত্রে আটকে চলা, খারাপ ব্যাটারি লাইফ এবং অজানা পার্শ্ব-প্রতিক্রিয়ার সম্মুখিন হতে পারেন। - + GmsCore সেটিং GmsCore এর জন্য সেটিং - + URL পুনঃনির্দেশ বাইপাস করুন URL পুনঃনির্দেশ বাইপাস করছে URL পুনঃনির্দেশ বাইপাস করেনি - + লিংক ব্রাউজারে খুলুন লিংক বাহিরে খুলুন অ্যাপের মধ্যে লিংক খুলছে - + ট্র্যাকিং করার প্যারামিটার মুছুন লিংক থেকে ট্র্যাকিং করার প্যারামিটার মুছে ফেলা হয়েছে লিংক থেকে ট্র্যাকিং করার প্যারামিটার মুছে ফেলা হয়নি - + জুম করার কম্পন নিষ্ক্রিয় করুন কম্পন নিষ্ক্রিয় করা হয়েছে কম্পন সক্রিয় করা হয়েছে - + স্বয়ংক্রিয় গুণমান ভিডিও গুণমান পরিবর্তন মনে রাখুন গুণমান পরিবর্তন সব ভিডিওতে প্রয়োগ করা হয়েছে @@ -1044,35 +1027,34 @@ This is because Crowdin requires temporarily flattening this file and removing t ওয়াই-ফাই ডিফল্ট %1$s গুণমান পরিবর্তন হচ্ছে: %2$s - + স্পিড ডায়ালগ বোতাম দেখান বোতাম প্রদর্শিত হয়েছে বোতাম প্রদর্শিত হয়নি - + নিজস্ব প্লেব্যাক স্পিড - পাওয়া যাচ্ছে এমন প্লেব্যাক স্পিড যুক্ত বা পরিবর্তন করুন নিজস্ব স্পিড অবশ্যই %sগুণ থেকে কম হতে হবে। মূল ভ্যালু ব্যবহৃত হচ্ছে। ভুল নিজস্ব প্লেব্যাক স্পিড। মূল ভ্যালু ব্যবহৃত হচ্ছে। - + প্লেব্যাকের স্পিড পরিবর্তন মনে রাখুন প্লেব্যাকের স্পিড পরিবর্তন সকল ভিডিওতে প্রয়োগ হবে প্লেব্যাকের স্পিড পরিবর্তন এই ভিডিওতে প্রয়োগ হবে প্লেব্যাকের মূল স্পিড মূল স্পিড পরিবর্তন হচ্ছে: %s - + পুরোনো ভিডিও গুণমান উদ্ধার করুন পুরোনো ভিডিও গুণমান মেনু প্রদর্শিত হয়েছে পুরোনো ভিডিও গুণমান মেনু প্রদর্শিত হয়নি - + ভিডিওর নির্দিষ্ট অংশে যেতে টানুন সক্রিয় করুন ভিডিওর নির্দিষ্ট অংশে যেতে টানুন সক্রিয় করা হয়েছে ভিডিওর নির্দিষ্ট অংশে যেতে টানুন সক্রিয় করা হয়নি - + ভিডিও স্ট্রিমিং স্পুফ করুন প্লেব্যাক সমস্যা প্রতিরোধ করতে ক্লায়েন্ট ভিডিও স্ট্রিম স্পুফ করুন ভিডিও স্ট্রিমিং স্পুফ করুন @@ -1084,17 +1066,14 @@ This is because Crowdin requires temporarily flattening this file and removing t ভিডিও কোডেক AVC (H.264) ব্যবহৃত হচ্ছে ভিডিও কোডেক VP9 বা AV1 ব্যবহৃত হচ্ছে - - - - + অডিও বিজ্ঞাপন আটকান অডিও বিজ্ঞাপন আটকানো হয়েছে অডিও বিজ্ঞাপন আনবব্লক করা হয়েছে - + %s উপলভ্য নয়। বিজ্ঞাপন দেখাতে পারে। সেটিং থেকে অন্য কোন বিজ্ঞাপন আটকানো সেবায় সুইচ করুন। %s সার্ভার একটি ত্রুটি দেখাচ্ছে। বিজ্ঞাপন দেখাতে পারে। সেটিং থেকে অন্য কোন বিজ্ঞাপন আটকানো সেবায় সুইচ করুন। এমবেড করা ভিডিও বিজ্ঞাপন আটকান @@ -1102,30 +1081,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Luminous প্রক্সি PurpleAdBlock প্রক্সি - + ভিডিও বিজ্ঞাপন আটকান ভিডিও বিজ্ঞাপন আটকানো হয়েছে ভিডিও বিজ্ঞাপন আটকানো হয়নি - + মুছে ফেলা বার্তা মুছে ফেলা বার্তা দেখান মুছে ফেলা বার্তা দেখাবেন না স্পয়লার এর পেছনে থাকা মুছে ফেলা বার্তা লুকান মুছে ফেলা বার্তাগুলো ক্রসড-আউট পাঠ্য হিসেবে দেখান - + স্বয়ংক্রিয়ভাবে চ্যানেল পয়েন্ট নিয়ে নিন চ্যানেল পয়েন্ট স্বয়ংক্রিয়ভাবে নেওয়া হয়েছে চ্যানেল পয়েন্ট স্বয়ংক্রিয়ভাবে নেওয়া হয়নি - + Twitch ডিবাগ মোড সক্রিয় করুন Twitch ডিবাগ মোড সক্রিয় করুন (প্রস্তাবিত নয়) Twitch ডিবাগ মোড নিষ্ক্রিয় করা হয়েছে - + ReVanced সেটিং বিজ্ঞাপন বিজ্ঞাপন বন্ধ করার সেটিং diff --git a/src/main/resources/addresources/values-bs-rBA/strings.xml b/patches/src/main/resources/addresources/values-bs-rBA/strings.xml similarity index 69% rename from src/main/resources/addresources/values-bs-rBA/strings.xml rename to patches/src/main/resources/addresources/values-bs-rBA/strings.xml index 4281ce7a4..6892649b0 100644 --- a/src/main/resources/addresources/values-bs-rBA/strings.xml +++ b/patches/src/main/resources/addresources/values-bs-rBA/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,105 +139,104 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/src/main/resources/addresources/values-ca-rES/strings.xml b/patches/src/main/resources/addresources/values-ca-rES/strings.xml similarity index 71% rename from src/main/resources/addresources/values-ca-rES/strings.xml rename to patches/src/main/resources/addresources/values-ca-rES/strings.xml index 4620263fe..cf8bc436e 100644 --- a/src/main/resources/addresources/values-ca-rES/strings.xml +++ b/patches/src/main/resources/addresources/values-ca-rES/strings.xml @@ -32,23 +32,25 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + Restablir - + - + Quant a - + - + + + @@ -64,30 +66,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -97,23 +99,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -124,29 +120,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -154,26 +141,26 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + Quant a - + - + - + Aparença @@ -182,84 +169,83 @@ This is because Crowdin requires temporarily flattening this file and removing t Restablir Quant a - + - + - + - + - + - + - + - + - + + + - + - + Advertència - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + Desactivat - + - + - + - + - + diff --git a/src/main/resources/addresources/values-cs-rCZ/strings.xml b/patches/src/main/resources/addresources/values-cs-rCZ/strings.xml similarity index 92% rename from src/main/resources/addresources/values-cs-rCZ/strings.xml rename to patches/src/main/resources/addresources/values-cs-rCZ/strings.xml index d965ed90f..dc0b19e51 100644 --- a/src/main/resources/addresources/values-cs-rCZ/strings.xml +++ b/patches/src/main/resources/addresources/values-cs-rCZ/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Kontroly selhaly Otevřít oficiální webovou stránku Ignorovat @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Patchnuto před %s dny APK datum sestavení je poškozeno - + ReVanced Přejete si pokračovat? Resetovat @@ -63,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Oficiální odkazy Přispět - + MicroG GmsCore není nainstalován. Nainstalujte jej. Potřebná akce @@ -74,7 +74,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + O aplikaci Reklamy Alternativní náhledy @@ -86,7 +86,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Ostatní Video - + + Zakázat automatické přehrávání Shorts v pozadí + Přehrávání Shorts v pozadí je zakázáno + Přehrávání Shorts v pozadí je povoleno + + Debugování Povolit nebo zakázat debugovací možnosti Debugovací záznamy @@ -103,13 +108,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Toast není zobrazena, pokud dojde k chybě Vypnutí toastů při chybě skryje všechna ReVanced chybová oznámení.\n\nNebudete upozorněni na neočekávané události. - + Zakázat záři tlačítka jako / odebírat Tlačítko se mi líbí a odebírá, pokud je zmíněno Tlačítko se mi líbí a odebírá, když je zmíněno - Skrýt šedý oddělovač - Šedé oddělovače jsou skryty - Šedé oddělovače jsou viditelné + Skrýt alba + Album karty jsou skryty + Alba karty jsou zobrazeny + Skrýt box crowdfunding + Crowdfunding box je skrytý + Zobrazí se Crowdfunding box + Skrýt plovoucí tlačítko mikrofonu + Tlačítko mikrofonu skryté + Zobrazené tlačítko mikrofonu Skrýt vodoznak kanálu Vodoznak je skryt Vodoznak je zobrazen @@ -154,9 +165,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Skrýt rozšiřitelný čip pod videem Rozšiřitelné čipy jsou skryty Jsou zobrazeny rozšiřitelné čipy - Skrýt zápatí menu kvality videa - Zápatí nabídky video kvality je skryté - Zápatí nabídky video kvality je zobrazeno Skrýt příspěvky komunity Komunitní příspěvky jsou skryty Komunitní příspěvky jsou zobrazeny @@ -231,6 +239,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Je zobrazena sekce přepisu Popis videa Skrýt nebo zobrazit komponenty popisu videa + Filtrovat lištu + Skrýt nebo zobrazit filtrovací lištu ve kanálu, vyhledávání a souvisejících videích + Skrýt v kanálu + Skryté v krmivu + Zobrazeno v kanálu + Skrýt ve vyhledávání + Skryté ve vyhledávání + Zobrazeno ve vyhledávání + Skrýt v souvisejících videích + Skrytá v souvisejících videích + Zobrazeno v souvisejících videích + Komentáře + Skrýt nebo zobrazit komponenty sekce komentářů + Skrýt \'Komentáře podle záhlaví členů + Komentáře členů jsou skryté + \'Komentáře členů\' jsou zobrazeny + Skrýt sekci komentáře + Sekce komentářů je skrytá + Část Komentáře je zobrazena + Skrýt tlačítko \"Vytvořit Short\" + Tlačítko \"Vytvořit Short\" je skryté + Tlačítko \"Vytvořit Short\" je viditelné + Skrýt náhled komentáře + Náhled komentáře je skrytý + Náhled komentáře je zobrazen + Skrýt poděkování + Tlačítko poděkování je skryté + Tlačítko poděkování je zobrazeno + Skrýt časové razítko a tlačítka emoji + Časové razítko a tlačítka emoji jsou skrytá + Časové razítko a tlačítka emoji jsou zobrazena Skrýt YouTube Dveody Vyhledávací lišty jsou skryté Doodles @@ -261,7 +300,6 @@ This is because Crowdin requires temporarily flattening this file and removing t This is because keywords can be in any language, and showing an example in the localized script helps convey this. --> klíčová slova a fráze ke skrytí, odděleno novými řádky\n\nKlíčová slova mohou být jména kanálů nebo jakýkoli text zobrazený v nadpisech videa\n\nSlova s velkými písmeny uprostřed musí být zadána se skříní (např: iPhone, iPhone, TikTok, LeBlanc) O filtrování klíčových slov - Výsledky domovského/předplatného/vyhledávání jsou filtrovány pro skrytí obsahu, který odpovídá výrazům klíčových slov\n\nOmezení\n• Krátké nelze skrýt podle názvu kanálu\n• Některé komponenty uživatelského rozhraní nemusí být skryté\n• Hledání klíčového slova nemusí zobrazovat žádné výsledky Porovnat celá slova Zaokrouhlení klíčového slova/fráze s dvojitými uvozovkami zabrání částečným shodám s názvy videí a kanálů<br><br>Například<br><b>\"ai\"</b> skryje video: <b>How does AI work?</b><br>, ale nebude skrýt: <b>What does fair use mean?</b> @@ -272,7 +310,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Klíčové slovo je příliš krátké a vyžaduje uvozovky: %s Klíčové slovo skryje všechna videa: %s - + Skrýt obecné reklamy Obecné reklamy jsou skryty Všeobecné reklamy jsou zobrazeny @@ -291,6 +329,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Skrýt banner pro zobrazení produktů Banner je skrytý Banner je zobrazen + Skrýt nákupní šelf hráče + Nákupní polička je skrytá + Nákupní polička je zobrazena Skrýt nákupní odkazy v popisu videa Nákupní odkazy jsou skryté Nákupní odkazy jsou zobrazeny @@ -307,17 +348,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Skrýt reklamy na celou obrazovku fungují pouze se staršími zařízeními - + Skrýt YouTube Premium akce YouTube Premium akce pod přehrávačem videa jsou skryté YouTube Premium akce pod přehrávačem videa jsou zobrazeny - + Skrýt video reklamy Video reklamy jsou skryty Zobrazují se video reklamy - + URL zkopírováno do schránky Adresa URL s časovým razítkem zkopírována Zobrazit tlačítko URL pro kopírování videa @@ -327,13 +368,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Tlačítko je zobrazeno. Klepnutím zkopírujete URL videa s časovým razítkem. Klepnutím a podržením zkopírujete video bez časové značky Tlačítko není zobrazeno - + Odstranit dialog uvážení prohlížeče Dialog bude odstraněn Dialog bude zobrazen Toto neobchází věkové omezení, ale přijímá ho automaticky. - + Externí stahování Nastavení pro použití externího stahování Zobrazit externí tlačítko ke stažení @@ -347,17 +388,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Název balíčku nainstalované externí stahovací aplikace, jako je NewPipe nebo pečeť %s není nainstalován. Prosím, nainstalujte jej. - + Zakázat přesné hledání gesta Gesto je zakázáno Gesto je povoleno - + Povolit klepnutí na vyhledávací lištu Klepnutí na posuvník je povoleno Klepnutí na posuvník je zakázáno - + Povolit gesto jasu Přejeďte jasem je povoleno Posunutí jasu je zakázáno @@ -386,12 +427,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Práh velikosti přetažení Výše prahové hodnoty pro spuštění přejetím - + Zakázat automatické titulky Automatické titulky jsou zakázány Automatické titulky jsou povoleny - + Tlačítka akce Skrýt nebo zobrazit tlačítka pod videem Skrýt oblíbené a neoblíbené @@ -427,23 +468,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Tlačítko Uložit do playlistu je skryté Je zobrazeno tlačítko Uložit do playlistu - - Skrýt tlačítko automatického přehrávání - Tlačítko automatického přehrávání je skryté - Tlačítko automatického přehrávání je zobrazeno - - - - Skrýt tlačítko titulků - Tlačítko titulek je skryté - Tlačítko s titulky je zobrazeno - - - Skrýt tlačítko pro přehrávání - Tlačítko vysílání je skryté - Tlačítko vysílání je viditelné - - + Navigation buttons Skrýt nebo změnit tlačítka v navigačním panelu @@ -470,7 +495,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Štítky jsou skryty Štítky jsou zobrazeny - + Flyout menu Skrýt nebo zobrazit položky nabídky flyoutu @@ -481,6 +506,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Skrýt další nastavení Další menu nastavení je skryto Zobrazí se další menu nastavení + + Skrýt časovač spánku + Menu spánku je skryté + Menu časovače je zobrazeno Skrýt video smyčky Smyčka menu videa je skrytá @@ -489,6 +518,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Skrýt ambientní režim Nabídka ambientního režimu je skrytá Zobrazení nabídky v režimu ambientního nastavení + Skrýt stabilní hlasitost + Zobrazí se stabilní hlasitost menu + Stálá hlasitost menu je skrytá Skrýt nápovědu & zpětnou vazbu Nápověda & Nabídka zpětné vazby je skryta @@ -514,83 +546,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Skrýt sledování ve VR Sledování v nabídce VR je skryté Zobrazeno sledování v nabídce VR + Skrýt zápatí menu kvality videa + Zápatí nabídky video kvality je skryté + Zápatí nabídky video kvality je zobrazeno - - Skrýt předchozí & další video tlačítka - Tlačítka jsou skryta - Tlačítka jsou zobrazena + + Skrýt předchozí & další video tlačítka + Tlačítka jsou skryta + Tlačítka jsou zobrazena + Skrýt tlačítko pro přehrávání + Tlačítko vysílání je skryté + Tlačítko vysílání je viditelné + + Skrýt tlačítko titulků + Tlačítko titulek je skryté + Tlačítko s titulky je zobrazeno + Skrýt tlačítko automatického přehrávání + Tlačítko automatického přehrávání je skryté + Tlačítko automatického přehrávání je zobrazeno - - Skrýt alba - Album karty jsou skryty - Alba karty jsou zobrazeny - - - Komentáře - Skrýt nebo zobrazit komponenty sekce komentářů - Skrýt \'Komentáře podle záhlaví členů - Komentáře členů jsou skryté - \'Komentáře členů\' jsou zobrazeny - Skrýt sekci komentáře - Sekce komentářů je skrytá - Část Komentáře je zobrazena - Skrýt tlačítko \'Vytvořit krátký\' - Tlačítko \"Vytvořit krátký\" je skryté - Zobrazí se tlačítko \'Vytvořit krátké\' - Skrýt náhled komentáře - Náhled komentáře je skrytý - Náhled komentáře je zobrazen - Skrýt poděkování - Tlačítko poděkování je skryté - Tlačítko poděkování je zobrazeno - Skrýt časové razítko a tlačítka emoji - Časové razítko a tlačítka emoji jsou skrytá - Časové razítko a tlačítka emoji jsou zobrazena - - - Skrýt box crowdfunding - Crowdfunding box je skrytý - Zobrazí se Crowdfunding box - - + Skrýt karty na konci obrazovky Karty na konci obrazovky jsou skryty Karty na konci obrazovky jsou zobrazeny - - Filtrovat lištu - Skrýt nebo zobrazit filtrovací lištu ve kanálu, vyhledávání a souvisejících videích - Skrýt v kanálu - Skryté v krmivu - Zobrazeno v kanálu - Skrýt ve vyhledávání - Skryté ve vyhledávání - Zobrazeno ve vyhledávání - Skrýt v souvisejících videích - Skrytá v souvisejících videích - Zobrazeno v souvisejících videích - - - Skrýt plovoucí tlačítko mikrofonu - Tlačítko mikrofonu skryté - Zobrazené tlačítko mikrofonu - - + Zakázat ambientní režim v režimu celé obrazovky Ambientní režim zakázán Ambientní režim povolen - + Skrýt informační karty Informační karty jsou skryty Informační karty jsou zobrazeny - + Zakázat animace čísla Valící se čísla nejsou animována Valící se čísla jsou animována - + Skrýt vyhledávací lištu v přehrávači videa Hledací panel video přehrávače je skrytý Zobrazí se vyhledávací lišta videa @@ -598,20 +593,15 @@ This is because Crowdin requires temporarily flattening this file and removing t Panel hledání náhledů je skrytý Zobrazí se panel hledání náhledů - + + Přehrávač Shorts + Skrýt nebo zobrazit komponenty v přehrávači Shorts - Skrýt zkratky v domovském kanálu - Krátky v domovském kanálu jsou skryté - Zobrazí se zkratky v domovském kanálu - Skrýt zkratky v odběru kanálu - Krátky v předplatném kanálu jsou skryté - Zobrazí se zkratky v předplatném kanálu - Skrýt zkratky ve výsledcích vyhledávání - Krátky ve výsledcích vyhledávání jsou skryty - Zobrazí se zkratky ve výsledcích vyhledávání + Skrýt Shorts ve výsledcích vyhledávání + Shorts jsou ve výsledcích vyhledávání skryté + Shorts jsou ve výsledcích vyhledávání viditelné - Skrýt tlačítko pro připojení Tlačítko spojení je skryté Zobrazí se tlačítko pro připojení @@ -696,27 +686,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Navigační panel je skrytý Navigační panel je zobrazen - + Zakázat koncovku videa Navrhovaná videa budou zakázána Budou zobrazena navrhovaná videa - + Skrýt časové razítko videa Časové razítko je skryté Časové razítko je zobrazeno - + Skrýt vyskakovací panely přehrávače Vyskakovací panely přehrávače jsou skryty Zobrazí se vyskakovací panely přehrávače - + Neprůhlednost překrytí přehrávače Neprůhlednost mezi 0-100, kde 0 je průhledná Průhlednost překrytí přehrávače musí být od 0 do 100 - + Líbí se mi dočasně nedostupné (vypršel časový limit API) Nelíbí se mi nedostupné (status %d) @@ -760,17 +750,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Limit rychlosti klienta byl zjištěn %d krát %d milisekund - + Povolit široký vyhledávací panel Široký vyhledávací panel je povolen Široký vyhledávací panel je zakázán - + + Povolit vysoce kvalitní náhledy + Náhledy jsou vysoce kvalitní + Náhledy jsou středně kvalitní + Náhledy na celou obrazovku jsou vysoce kvalitní + Náhledy na celou obrazovku jsou středně kvalitní + Tímto také obnovíte náhledy na hospodářských zvířatech, která nemají náhledy ve vyhledávacím panelu.\n\nNáhledy budou používat stejnou kvalitu jako aktuální video.\n\nTato funkce funguje nejlépe s kvalitou videa 720p nebo nižší a při použití velmi rychlého internetového připojení. Obnovit staré náhledové lišty Náhledy se zobrazí nad vyhledávací lištou Náhledy se zobrazí v režimu celé obrazovky - + Povolit SponsorBlock SponsorBlock je crowd-sourcový systém pro přeskočení otravných částí YouTube videa Vzhled @@ -951,7 +947,7 @@ This is because Crowdin requires temporarily flattening this file and removing t O aplikaci Data jsou poskytována aplikací SponsorBlock. Klepnutím sem se dozvíte více a podívejte se na stahování pro ostatní platformy - + Verze aplikace Spoof Verze zmařena Verze není falešná @@ -964,9 +960,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Obnovení široké rychlosti videa & kvalitní menu 18.09.39 - Obnovení záložky knihovny 17.41.37 - Obnovit starou skladbu - 17.33.42 - Obnovit staré rozložení UI - + Nastavit úvodní stránku Výchozí Procházet kanály @@ -984,18 +979,26 @@ This is because Crowdin requires temporarily flattening this file and removing t Populární Sledujte později - - Zakázat obnovení krátkého přehrávače - Krátký přehrávač nebude pokračovat při spuštění aplikace + + Zakázat obnovení přehrávače Shorts + Přehrávač Shorts nebude obnoven při spuštění aplikace Krátký přehrávač bude pokračovat při spuštění aplikace - + + Automatické přehrávání Shorts + Shorts se budou automaticky přehrávat + Shorts se budou opakovat + Automatické přehrávání Shorts v pozadí + Přehrávání Shorts v pozadí se bude automaticky přehrávat + Přehrávání Shorts v pozadí se bude opakovat + + Povolit rozložení tabletu Rozložení tabletu je povoleno Rozložení tabletu je zakázáno Komunitní příspěvky se nezobrazují ve vzhledu tabletu - + Minipřehrávač Změnit styl přehrávače minimalizovaného nastavení Typ minipřehrávače @@ -1014,6 +1017,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Povolit přetažení Přetažení je povoleno\n\nMiniplayer může být přetažen do libovolného rohu obrazovky Přetažení je zakázáno + Povolit horizontální přetažení + Povolené gesto vodorovného přetažení\n\nMinipřehrávač může být přetažen na levé nebo pravé straně obrazovky + Gesto vodorovného tažení je zakázáno Skrýt tlačítko zavření Tlačítko zavřít je skryté Tlačítko zavření je zobrazeno @@ -1033,12 +1039,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Neprůhlednost mezi 0-100, kde 0 je průhledná Neprůhlednost překrytí minipřehrávače musí být mezi 0-100 - + Povolit načtení obrazovky gradient Načítání obrazovky bude mít přechod na pozadí Načítání obrazovky bude mít pevné pozadí - + Povolit vlastní barvu vyhledávacího panelu Vlastní barva vyhledávacího panelu je zobrazena Zobrazí se původní barva vyhledávacího panelu @@ -1046,12 +1052,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Barva vyhledávacího panelu Neplatná hodnota barvy vyhledávacího panelu - + Obejít omezení oblasti obrázku Použití hostitele obrázků yt4.gggpht.com Použití originálního hostitele obrázků\n\nPovolení tohoto nastavení může opravit chybějící obrázky, které jsou blokovány v některých regionech - + Záložka Domů @@ -1083,7 +1089,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Doručení dočasně není k dispozici (stavový kód: %s) Odšíp dočasně není k dispozici - + Zobrazit rozšířená oznámení Oznámení jsou zobrazena při spuštění Oznámení nejsou zobrazena při spuštění @@ -1091,47 +1097,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Připojení k poskytovateli oznámení se nezdařilo Zrušit - + Varování Vaše historie sledování není ukládána.<br><br>Příčinou je s největší pravděpodobností DNS blokátor reklam nebo síťový proxy server.<br><br>Chcete-li to opravit, přidejte<b>s.youtube.com</b> na whitelist nebo vypněte všechny DNS blokátory a proxy servery. Znovu nezobrazovat - + Povolit automatické opakování Automaticky opakovat je povoleno Automatické opakování je zakázáno - + Rozměry zařízení pro spouštění Rozměry zařízení jsou zfalšovány\n\nVyšší vlastnosti videa mohou být odemknuty, ale můžete zažít seříznutí videa, horší výdrž baterie a neznámé boční efekty Rozměry zařízení nejsou zfalšovány\n\nPovolením této funkce můžete odemknout vyšší vlastnosti videa Povolením této funkce může být způsobeno přehrávání videa, horší životnost baterie a neznámé vedlejší účinky. - + Nastavení GmsCore Nastavení pro GmsCore - + Obejít přesměrování URL URL přesměrování je obcházeno URL přesměrování není obcházeno - + Otevřít odkazy v prohlížeči Otevírací odkazy vně Otevírání odkazů v aplikaci - + Odstranit parametr dotaz pro sledování Parametr dotaz na sledování je odstraněn z odkazů Parametr dotaz na sledování není odstraněn z odkazů - + Zakázat hmatové přiblížení Hmatové úlohy jsou zakázány Hmatové úlohy jsou povoleny - + Automatická kvalita Zapamatovat změny kvality videa Změny kvality platí pro všechna videa @@ -1142,35 +1148,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Změněna výchozí kvalita %1$s na: %2$s - + Zobrazit tlačítko dialogu rychlosti Tlačítko je zobrazeno Tlačítko není zobrazeno - + + Vlastní nabídka rychlosti přehrávání + Vlastní menu rychlosti je zobrazeno + Vlastní menu rychlosti není zobrazeno Vlastní rychlosti přehrávání - Přidat nebo změnit dostupné rychlosti přehrávání + Přidat nebo změnit vlastní rychlosti přehrávání Vlastní rychlosti musí být menší než %s. Použití výchozích hodnot. Neplatné vlastní rychlosti přehrávání. Použití výchozích hodnot. - + Zapamatovat změny rychlosti přehrávání Změny rychlosti přehrávání platí pro všechna videa Změny rychlosti přehrávání platí pouze pro aktuální video Výchozí rychlost přehrávání Změněna výchozí rychlost na: %s - + Obnovit staré menu kvality videa Zobrazí se staré menu kvality videa Staré menu kvality videa není zobrazeno - + Povolit posunutí pro vyhledání Posunutí pro vyhledání je povoleno Posunutí k vyhledání není povoleno - + Spouštěcí video streamy Spouštět klientské video streamy, aby se zabránilo problémům s přehráváním Spouštěcí video streamy @@ -1188,20 +1197,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Boční efekty Android VR • Menu zvukové stopy chybí\n• Stabilní hlasitost není k dispozici - - - Povolit automatický jas HDR - Automatický HDR jas je povolen - Automatický HDR jas je zakázán - - + Blokovat zvukové reklamy Zvukové reklamy jsou blokovány Zvukové reklamy jsou odblokovány - + %s není dostupný. Reklamy se mohou zobrazit. Zkuste přepnout na jinou službu blokování reklam v nastavení. %s server vrátil chybu. Reklamy se mohou zobrazit. Zkuste přepnout na jinou službu blokování reklam v nastavení. Blokovat vložené video reklamy @@ -1209,30 +1212,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Proxy světelné ukazatele PurpleAdBlock proxy - + Blokovat video reklamy Video reklamy jsou blokovány Video reklamy jsou odblokovány - + zpráva byla smazána Zobrazit smazané zprávy Nezobrazovat smazané zprávy Skrýt smazané zprávy za spoletem Zobrazit smazané zprávy jako skrytý text - + Automaticky nárokovat kanálové body Body kanálu jsou požadovány automaticky Standardní body nejsou požadovány automaticky - + Povolit režim ladění Twitch Režim ladění Twitch je povolen (není doporučeno) Režim ladění Twitch je zakázán - + Rozšířené nastavení Reklamy Nastavení blokování reklamy diff --git a/src/main/resources/addresources/values-da-rDK/strings.xml b/patches/src/main/resources/addresources/values-da-rDK/strings.xml similarity index 93% rename from src/main/resources/addresources/values-da-rDK/strings.xml rename to patches/src/main/resources/addresources/values-da-rDK/strings.xml index 2fd0fbf06..77adc4bb9 100644 --- a/src/main/resources/addresources/values-da-rDK/strings.xml +++ b/patches/src/main/resources/addresources/values-da-rDK/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Kontrol mislykkedes Åbn officielle hjemmeside Ignorer @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Patched %s dage siden APK byggedato er ødelagt - + ReVanced Ønsker du at fortsætte? Nulstil @@ -63,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Officielle links Donér - + MicroG GmsCore er ikke installeret. Installér den. Behov for handling @@ -74,7 +74,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Om Annoncer Alternative miniaturer @@ -86,7 +86,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Diverse Video - + + + Fejlfinding Aktiver eller deaktiver fejlfindingsindstillinger Debug logning @@ -103,13 +105,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Toast ikke vist, hvis der opstår fejl Slår fejl-toasts fra skjuler alle ReVanced fejlmeddelelser.\n\nDu vil ikke blive underrettet om nogen uventede begivenheder. - + Deaktivér som / abonnér knap glow Ligesom og abonnér knap vil ikke gløde når nævnt Ligesom og abonnér knappen vil gløde, når nævnt - Skjul grå separator - Grå separatorer er skjult - Grå separatorer er vist + Skjul albumkort + Albumkort er skjult + Albumkort vises + Skjul crowdfunding boks + Crowdfunding boks er skjult + Crowdfunding boks er vist + Skjul flydende mikrofon knap + Mikrofon knap skjult + Mikrofon knap vist Skjul kanalvandmærke Vandmærke er skjult Vandmærke er vist @@ -154,9 +162,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Skjul udvidelig chip under videoer Kan udvides chips er skjult Udvidede jetoner vises - Skjul sidefod til videokvalitet - Videokvalitetsmenuens sidefod er skjult - Videokvalitet menu footer er vist Skjul fællesskabs indlæg Fællesskabs indlæg er skjult Fællesskabs indlæg er vist @@ -231,6 +236,34 @@ This is because Crowdin requires temporarily flattening this file and removing t Afsnittet er vist Video beskrivelse Skjul eller vis komponenter til videobeskrivelse + Filtrer bjælke + Skjul eller vis filterbjælken i feedet, søg og relaterede videoer + Skjul i feed + Skjult i feed + Vist i feed + Skjul i søgning + Skjult i søgning + Vist i søgning + Skjul i relaterede videoer + Skjult i relaterede videoer + Vist i relaterede videoer + Kommentarer + Skjul eller vis kommentarer sektion komponenter + Skjul \'Kommentarer fra medlemmer\' header + \'Kommentarer fra medlemmer\' overskrift er skjult + \'Kommentarer fra medlemmer\' overskrift vises + Skjul kommentarsektion + Kommentarer sektion er skjult + Kommentarer sektion er vist + Skjul forhåndsvisning kommentar + Forhåndsvisning kommentar er skjult + Forhåndsvis kommentar er vist + Skjul tasteknap + Tak knappen er skjult + Tak knappen er vist + Skjul tidsstempel og emoji knapper + Tidsstempel og emoji knapper er skjult + Tidsstempel og emoji knapper vises Skjul YouTube-Doudler Søgebjælke Doudler er skjult @@ -261,7 +294,6 @@ This is because Crowdin requires temporarily flattening this file and removing t This is because keywords can be in any language, and showing an example in the localized script helps convey this. --> Nøgleord og sætninger at skjule, adskilt af nye linjer\n\nSøgeord kan være kanalnavne eller enhver tekst vist i video titler\n\nOrd med store bogstaver i midten skal indtastes med casing (dvs. iPhone, TikTok, LeBlanc) Om søgeord filtrering - Hjem/Abonnement/Søgeresultater filtreres for at skjule indhold, der matcher søgeordssætninger\n\nBegrænsninger\n• Korte kan ikke skjules ved kanalnavn\n• Nogle UI-komponenter kan ikke skjules\n• Søger efter et søgeord, kan ikke vise nogen resultater Match hele ord Omkring et nøgleord/sætning med dobbelt-citater vil forhindre partielle kampe af videotitler og kanalnavne<br><br>For eksempel<br><b>\"ai\"</b> vil skjule videoen: <b>How does AI work?</b><br>, men skjuler ikke: <b>What does fair use mean?</b> @@ -269,10 +301,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Kan ikke bruge søgeord: %s Tilføj tilbud for at bruge søgeord: %s Nøgleord har modstridende erklæringer: %s - Nøgleord er for kort og kræver tilbud: %s Nøgleord vil skjule alle videoer: %s - + Skjul generelle annoncer Generelle annoncer er skjult Generelle annoncer vises @@ -291,6 +322,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Skjul banner for at se produkter Banner er skjult Banner er vist + Skjul spillerens indkøbshylde + Shopping hylde er skjult + Shopping hylde er vist Skjul shopping links i video beskrivelse Shopping links er skjult Shopping links vises @@ -307,17 +341,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Skjul reklamer på fuld skærm virker kun med ældre enheder - + Skjul YouTube Premium kampagner YouTube Premium kampagner under videoafspiller er skjult YouTube Premium kampagner under videoafspiller vises - + Skjul videoannoncer Videoannoncer er skjult Videoannoncer vises - + URL kopieret til udklipsholder URL med tidsstempel kopieret Vis kopiér video URL knap @@ -327,13 +361,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Knap vises. Tryk for at kopiere video URL med tidsstempel. Tryk og hold for at kopiere video uden tidsstempel Knap vises ikke - + Fjern visningsdialog Dialog vil blive fjernet Dialog vil blive vist Dette går ikke uden om aldersbegrænsningen. Det accepterer bare det automatisk. - + Eksterne downloads Indstillinger for brug af en ekstern downloader Vis ekstern download-knap @@ -347,17 +381,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Pakkenavn på din installerede eksterne downloader-app, såsom NewPipe eller Seal %s er ikke installeret. Installér den. - + Deaktivér præcis søgemåde Bevægelse er deaktiveret Bevægelse er aktiveret - + Aktivér søgelinjeflytning Seekbar aflytning er aktiveret Seekbar aflytning er deaktiveret - + Aktivér lysstyrke-bevægelse Strøg med lysstyrke er aktiveret Stryg for lysstyrke er deaktiveret @@ -386,12 +420,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Stryg størrelse tærskel Beløbet for tærskelværdi for stryg der skal ske - + Deaktivér auto-billedtekster Auto billedtekster er deaktiveret Auto billedtekster er aktiveret - + Handlingsknapper Skjul eller vis knapper under videoer Skjul Like og Dislike @@ -426,23 +460,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Gem i afspilningslisteknappen er skjult Gem i afspilningslisteknappen vises - - Skjul autoplay knap - Automatisk spil-knap er skjult - Automatisk afspilningsknap vises - - - - Skjul billedtekst knap - Undertekster knappen er skjult - Underskriftsknappen vises - - - Skjul cast-knap - Cast-knappen er skjult - Cast knap er vist - - + Navigation buttons Skjul eller skift knapper i navigationsbjælken @@ -469,7 +487,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Etiketter er skjult Etiketter er vist - + Flyout menu Skjul eller vis spiller flyout menupunkter @@ -480,6 +498,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Skjul yderligere indstillinger Yderligere indstillingsmenu er skjult Yderligere indstillingsmenu er vist + + Skjul dvaletimer + Menuen Søvntimer er skjult + Menuen Søvntimer vises Skjul Loop video Loop video menu er skjult @@ -488,6 +510,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Skjul omgivende tilstand Menuen Omgivende tilstand er skjult Menuen Omgivende tilstand vises + Skjul stabil lydstyrke + Stabil lydstyrke-menu vises + Stabil lydstyrke-menu er skjult Skjul Hjælp & feedback Hjælp & feedback-menuen er skjult @@ -513,83 +538,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Skjul vagt i VR Se i VR-menuen er skjult Se i VR-menuen vises + Skjul sidefod til videokvalitet + Videokvalitetsmenuens sidefod er skjult + Videokvalitet menu footer er vist - - Skjul forrige & næste video knapper - Knapper er skjult - Knapper vises + + Skjul forrige & næste video knapper + Knapper er skjult + Knapper vises + Skjul cast-knap + Cast-knappen er skjult + Cast knap er vist + + Skjul billedtekst knap + Undertekster knappen er skjult + Underskriftsknappen vises + Skjul autoplay knap + Automatisk spil-knap er skjult + Automatisk afspilningsknap vises - - Skjul albumkort - Albumkort er skjult - Albumkort vises - - - Kommentarer - Skjul eller vis kommentarer sektion komponenter - Skjul \'Kommentarer fra medlemmer\' header - \'Kommentarer fra medlemmer\' overskrift er skjult - \'Kommentarer fra medlemmer\' overskrift vises - Skjul kommentarsektion - Kommentarer sektion er skjult - Kommentarer sektion er vist - Skjul knappen \'Opret en kort\' - \'Opret en kort\'-knap er skjult - \'Opret en kort\'-knap vises - Skjul forhåndsvisning kommentar - Forhåndsvisning kommentar er skjult - Forhåndsvis kommentar er vist - Skjul tasteknap - Tak knappen er skjult - Tak knappen er vist - Skjul tidsstempel og emoji knapper - Tidsstempel og emoji knapper er skjult - Tidsstempel og emoji knapper vises - - - Skjul crowdfunding boks - Crowdfunding boks er skjult - Crowdfunding boks er vist - - + Skjul slutskærmkort End screen cards are hidden Kort til slutskærm vises - - Filtrer bjælke - Skjul eller vis filterbjælken i feedet, søg og relaterede videoer - Skjul i feed - Skjult i feed - Vist i feed - Skjul i søgning - Skjult i søgning - Vist i søgning - Skjul i relaterede videoer - Skjult i relaterede videoer - Vist i relaterede videoer - - - Skjul flydende mikrofon knap - Mikrofon knap skjult - Mikrofon knap vist - - + Deaktivér omgivende tilstand i fuld skærm Omgivelsestilstand deaktiveret Omgivelsestilstand aktiveret - + Skjul informationskort Info kort er skjult Info kort er vist - + Deaktivér animationer med rullenummer Rullende numre er ikke animeret Rullende numre er animeret - + Skjul søgelinje i videoafspiller Videoafspillerens søgelinje er skjult Videoafspillerens søgelinje vises @@ -597,7 +585,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Miniaturesøgelinjen er skjult Miniaturesøgelinjen vises - + Skjul Shorts i hjemmefeed Shorts i hjemmet feed er skjult @@ -695,27 +683,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Navigationsbjælken er skjult Navigationsbjælken vises - + Deaktivér foreslået videoslutskærm Foreslåede videoer vil blive deaktiveret Foreslåede videoer vil blive vist - + Skjul tidsstempel på video Tidsstempel er skjult Tidsstempel er vist - + Skjul pop op- paneler Spiller popup paneler er skjult Spiller popup paneler vises - + Spiller overlay gennemsigtighed Gennemsigtighedsværdi mellem 0-100, hvor 0 er gennemsigtig Spiller overlay gennemsigtighed skal være mellem 0-100 - + Dislikerer midlertidigt ikke tilgængelig (API-timeout ud) Dislikationer er ikke tilgængelige (status %d) @@ -759,17 +747,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Klient sats grænse stødt %d gange %d millisekunder - + Aktiver bred søgelinje Bred søgelinje er aktiveret Bred søgelinje er deaktiveret - + + Aktiver miniaturer af høj kvalitet + Seekbar miniaturer er af høj kvalitet + Seekbar miniaturer er af middel kvalitet + Fuldskærmssøgerbar miniaturer er af høj kvalitet + Fuldskærmssøgerbar miniaturer er af middel kvalitet + Dette vil også gendanne miniaturer på livestreams, der ikke har søgbar miniaturebilleder.\n\nSeekbar miniaturer vil bruge samme kvalitet som den aktuelle video.\n\nDenne funktion fungerer bedst med en videokvalitet på 720p eller lavere og ved brug af en meget hurtig internetforbindelse. Gendan gamle miniaturer på søgelinjen Seekbar miniaturer vises over søgelinjen Seekbar miniaturer vises i fuld skærm - + Aktiver SponsorBloker SponsorBlock er et crowd-sourcet system til at springe irriterende dele af YouTube-videoer over Udseende @@ -950,7 +944,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Om Data leveres af SponsorBlock API. Tryk her for at få flere oplysninger og se downloads til andre platforme - + Spoof app version Version spoofed Version ikke spoofed @@ -963,9 +957,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Gendan bred video hastighed & kvalitet menu 18.09.39 - Genopret biblioteks fane 17.41.37 - Gendan gammel spilleliste hylde - 17.33.42 - Gendan gammelt UI-layout - + Indstil startside Standard Gennemse kanaler @@ -983,18 +976,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Populære Se senere - + Deaktivér genoptagelse af Shorts spiller - Kortspilleren vil ikke genoptage ved app-opstart Kortspilleren vil genoptage ved app-opstart - + + + Aktivér tabletlayout Tablet layout er aktiveret Tablet layout er deaktiveret Fællesskabsindlæg vises ikke på tabletlayouts - + Miniplayer Ændre stilen for den i app minimeret afspiller Type af miniplayer @@ -1013,6 +1007,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Aktiver træk og slip Træk og slip er aktiveret\n\nMiniplayer kan trækkes til ethvert hjørne af skærmen Træk og slip er deaktiveret + Aktiver vandret træk-bevægelse + Vandret træk gestus aktiveret\n\nMiniplayer kan trækkes fra skærmen til venstre eller højre + Vandret trækbevægelse deaktiveret Skjul lukkeknap Luk knappen er skjult Luk knappen vises @@ -1032,12 +1029,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Gennemsigtighedsværdi mellem 0-100, hvor 0 er gennemsigtig Miniplayer overlay gennemsigtighed skal være mellem 0-100 - + Aktiver gradient indlæsning af skærmen Indlæser skærmen vil have en gradient baggrund Indlæser skærmen vil have en solid baggrund - + Aktivér brugerdefineret søgelinjefarve Brugerdefineret søgelinje farve vises Original søgelinje farve vises @@ -1045,12 +1042,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Farven på den søgende bar Ugyldig søgelinje farveværdi - + Bypass billede region restriktioner Bruger billedvært yt4.ggpht.com Brug af original billedvært\n\nAktivering af dette kan rette manglende billeder, der er blokeret i nogle regioner - + Hjem fane @@ -1082,7 +1079,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow midlertidigt ikke tilgængelig (statuskode: %s) DeArrow midlertidigt ikke tilgængelig - + Vis annonceringer Meddelelser vises ved opstart Meddelelser vises ikke ved opstart @@ -1090,47 +1087,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Kunne ikke forbinde til udbyder af annonceringer Luk - + Advarsel Din urhistorik gemmes ikke.<br><br>Dette skyldes sandsynligvis en DNS-annonceblokker eller netværksproxy.<br><br>For at løse dette, whitelist <b>s.youtube.com</b> eller slå alle DNS-blokkere og fuldmagter fra. Vis ikke igen - + Aktivér auto-gentag Auto-gentag er aktiveret Auto-gentag er deaktiveret - + Spoof enhedens dimensioner Enhedsmål spoofed\n\nHøjere video kvaliteter kan låses op, men du kan opleve videoafspilning stuttering, værre batterilevetid og ukendte bivirkninger Enheds dimensioner ikke forfalsket\n\nAktivering af dette kan låse op for højere videokvaliteter Aktivering af dette kan forårsage videoafspilning stuttering, værre batterilevetid og ukendte bivirkninger. - + GmsCore Indstillinger Indstillinger for GmsCore - + Bypass URL omdirigeringer URL omdirigeringer er omgået URL omdirigeringer er ikke omgået - + Åbn links i browser Åbning af links eksternt Åbner links i appen - + Fjern sporingsforespørgselsparameter Sporingsparameteren er fjernet fra links Sporingsforespørgselsparameteren er ikke fjernet fra links - + Deaktivér zoom haptics Haptics er deaktiveret Haptics er aktiveret - + Automatisk kvalitet Husk ændringer i videokvalitet Kvalitetsændringer gælder for alle videoer @@ -1141,35 +1138,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Ændrede standard %1$s kvalitet til: %2$s - + Vis hastigheds dialogknap Knap vises Knap vises ikke - + + Tilpasset afspilningshastighed menu + Tilpasset hastighed menu er vist + Brugerdefineret hastighedsmenu vises ikke Tilpasset afspilningshastighed - Tilføj eller skift de tilgængelige afspilningshastigheder + Tilføj eller ændr den brugerdefinerede afspilningshastighed Brugerdefinerede hastigheder skal være mindre end %s. Bruger standardværdier. Ugyldig brugerdefineret afspilningshastighed. Brug af standardværdier. - + Husk ændringer i afspilningshastighed Ændring af afspilningshastighed gælder for alle videoer Ændringerne i afspilningshastighed gælder kun for den aktuelle video Standard afspilningshastighed Ændrede standardhastighed til: %s - + Gendan gamle video kvalitet menu Gammel videokvalitetsmenu vises Gammel videokvalitetsmenu vises ikke - + Aktivér dias for at søge Dias for at søge er aktiveret Dias til søgning er ikke aktiveret - + Spoof video streams Spoof klienten video streams for at forhindre afspilning problemer Spoof video streams @@ -1187,20 +1187,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Android VR spoofing bivirkninger • Menuen Lydspor mangler\n• Stabil lydstyrke er ikke tilgængelig - - - Aktivér automatisk HDR- lysstyrke - Auto-HDR-lysstyrke er aktiveret - Auto-HDR-lysstyrke er deaktiveret - - + Blokér lydannoncer Lydannoncer er blokeret Lydannoncer er ublokeret - + %s er ikke tilgængelig. Annoncer kan vises. Prøv at skifte til en anden annonceblok tjeneste i indstillinger. %s server returnerede en fejl. Annoncer kan vises. Prøv at skifte til en anden annonceblok tjeneste i indstillinger. Bloker indlejrede videoannoncer @@ -1208,30 +1202,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Lysende proxy PurpleAdBlock proxy - + Blokér videoannoncer Videoreklamer er blokeret Videoannoncer er ublokerede - + besked slettet Vis slettede beskeder Vis ikke slettede beskeder Skjul slettede beskeder bag en spoiler Vis slettede beskeder som krydset tekst - + Auto-hævde Kanalpunkter Kanalpunkter afhentes automatisk Kanalpunkter afhentes ikke automatisk - + Aktiver Twitch-fejlfindingstilstand Twitch-fejlfindingstilstand er aktiveret (ikke anbefalet) Twitch-fejlfindingstilstand er deaktiveret - + Vigtigste Indstillinger Annoncer Reklame blokeringsindstillinger diff --git a/src/main/resources/addresources/values-de-rDE/strings.xml b/patches/src/main/resources/addresources/values-de-rDE/strings.xml similarity index 92% rename from src/main/resources/addresources/values-de-rDE/strings.xml rename to patches/src/main/resources/addresources/values-de-rDE/strings.xml index 0981d8278..152a6ff44 100644 --- a/src/main/resources/addresources/values-de-rDE/strings.xml +++ b/patches/src/main/resources/addresources/values-de-rDE/strings.xml @@ -32,17 +32,18 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Überprüfungen fehlgeschlagen Offizielle Webseite öffnen Ignorieren + <h5>Diese App wurde offenbar nicht von Ihnen gepatcht.</h5><br>Diese App funktioniert möglicherweise nicht richtig, <b>könnte schädlich oder sogar gefährlich in der Verwendung sein</b>.< br><br>Diese Prüfungen deuten darauf hin, dass diese App vorab gepatcht wurde oder von jemandem bezogen wurde sonst:<br><br><small>%1$s</small><br>Es wird dringend empfohlen, <b>diese App zu deinstallieren und selbst zu patchen</b> um sicherzustellen, dass Sie eine validierte und sichere App verwenden.<p><br>Wenn Sie diese Warnung ignorieren, wird sie nur zweimal angezeigt. Auf einem anderen Gerät gepatcht Nicht von ReVanced Manager installiert Vor mehr als 10 Minuten gepatcht Vor %s Tagen gepatcht APK-Erstellungsdatum ist beschädigt - + ReVanced Möchtest du fortfahren? Zurücksetzen @@ -62,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Offizielle Links Spenden - + MicroG GmsCore ist nicht installiert. Installiers. Aktion notwendig @@ -73,7 +74,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Über Werbung Alternative Thumbnails @@ -85,7 +86,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Sonstiges Video - + + Shorts-Hintergrundwiedergabe deaktivieren + Shorts-Hintergrundwiedergabe ist deaktiviert + Shorts-Hintergrundwiedergabe ist aktiviert + + Fehlerbehebung Debug-Optionen aktivieren oder deaktivieren Debug-Logging @@ -102,13 +108,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Toast wird nicht angezeigt, wenn ein Fehler auftritt Das Deaktivieren von Fehlertoasts verbirgt alle ReVanced Fehlerbenachrichtigungen.\n\nDu wirst nicht über unerwartete Ereignisse benachrichtigt. - + Deaktiviere das Like / Abonnieren Button aufleuchten \"Gefällt mir\" und \"Abonnieren\"-Button wird nicht aufleuchten, wenn er erwähnt wird \"Gefällt mir\" und \"Abonnieren\"-Button wird aufleuchten, wenn er erwähnt wird - Verstecke graue Separatoren - Graue Separatoren sind ausgeblendet - Graue Trennzeichen werden angezeigt + Albumkarten ausblenden + Albumkarten sind ausgeblendet + Albumkarten werden angezeigt + Crowdfunding Box ausblenden + Crowdfunding-Box ist ausgeblendet + Crowdfunding-Box wird angezeigt + Schwebende Mikrofon-Taste ausblenden + Mikrofon-Button ausgeblendet + Mikrofon-Taste angezeigt Wasserzeichen ausblenden Wasserzeichen ist ausgeblendet Wasserzeichen wird angezeigt @@ -153,9 +165,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Erweiterbaren Sektions-Chip unter Videos ausblenden Erweiterbare Chips sind ausgeblendet Erweiterbare Chips werden angezeigt - Videoqualitätsmenüfußzeile ausblenden - Video-Qualität Menü-Fußzeile ist ausgeblendet - Video-Qualität Menü-Fußzeile wird angezeigt Community-Beiträge ausblenden Community-Beiträge sind ausgeblendet Gemeinschaftsbeiträge werden angezeigt @@ -230,6 +239,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Sektion Transkripte wird angezeigt Videobeschreibung Komponenten der Videobeschreibung ausblenden oder anzeigen + Filterleiste + Verstecke oder zeige die Filterleiste im Feed, in der Suche und verwandten Videos + Im Feed ausblenden + Versteckt im Feed + Im Feed angezeigt + In der Suche ausblenden + Versteckt in der Suche + In der Suche angezeigt + In verwandten Videos ausblenden + Versteckt in verwandten Videos + In verwandten Videos angezeigt + Kommentare + Komponenten der Kommentar-Sektion ausblenden oder anzeigen + \'Kommentare von Mitglieder\' im Kopfbereich ausblenden + \'Kommentare von Mitglieder\' Header ist ausgeblendet + \'Kommentare von Mitgliedern\' wird angezeigt + Kommentarbereich ausblenden + Kommentarbereich ist ausgeblendet + Kommentarbereich wird angezeigt + \'Verknüpfung erstellen\'-Button ausblenden + \'Verknüpfung erstellen\' Button ist ausgeblendet + Schaltfläche \"Verknüpfung erstellen\" wird angezeigt + Vorschaukommentar ausblenden + Vorschaukommentar ist ausgeblendet + Vorschau des Kommentars wird angezeigt + Dankeschön ausblenden + Dankeschön-Taste ist ausgeblendet + Dankeschön Button wird angezeigt + Zeitstempel und Emoji-Tasten ausblenden + Zeitstempel und Emoji-Tasten sind ausgeblendet + Zeitstempel und Emoji-Tasten werden angezeigt YouTube Doodles ausblenden Suchleiste Doodles sind versteckt @@ -271,7 +311,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Keyword ist zu kurz und erfordert Anführungszeichen: %s Stichwort wird alle Videos ausblenden: %s - + Allgemeine Werbung ausblenden Allgemeine Anzeigen sind ausgeblendet Allgemeine Anzeigen werden angezeigt @@ -290,6 +330,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Banner ausblenden, um Produkte anzuzeigen Banner ist ausgeblendet Banner wird angezeigt + Spieler-Einkaufsregal ausblenden + Einkaufsregal ist ausgeblendet + Einkaufsregal wird angezeigt Einkaufslinks in der Videobeschreibung ausblenden Shopping-Links sind ausgeblendet Einkaufslinks werden angezeigt @@ -306,17 +349,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Vollbild-Werbung ausblenden funktioniert nur mit älteren Geräten - + YouTube Premium-Aktionen ausblenden YouTube-Premium-Angebote unterm Video-Player sind ausgeblendet YouTube-Premium-Aktionen unter Video-Player werden angezeigt - + Videowerbung ausblenden Video-Anzeigen sind ausgeblendet Video-Anzeigen werden angezeigt - + URL in Zwischenablage kopiert URL mit Zeitstempel kopiert Video-URL-Schaltfläche kopieren anzeigen @@ -326,13 +369,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Schaltfläche wird angezeigt. Tippen, um Video-URL mit Zeitstempel zu kopieren. Tippen und halten um Video ohne Zeitstempel zu kopieren Button wird nicht angezeigt - + Diskretion des Betrachters entfernen Dialog wird entfernt Dialog wird angezeigt Dies umgeht nicht die Altersbeschränkung, sondern akzeptiert sie nur automatisch. - + Externe Downloads Einstellungen für die Verwendung eines externen Downloaders Externen Download-Button anzeigen @@ -346,17 +389,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Paketname deiner installierten externen Downloader-App wie NewPipe oder Siegel %s ist nicht installiert. Installier es. - + Genaue Suchgeste deaktivieren Geste ist deaktiviert Geste ist aktiviert - + Suchleisten-Tippen aktivieren Tippen der Suchleiste ist aktiviert Tippen der Suchleiste ist deaktiviert - + Helligkeitsgeste aktivieren Helligkeitswischen ist aktiviert Helligkeitswischen ist deaktiviert @@ -385,12 +428,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Wischgrößenschwelle Der Schwellenwert für Wischen - + Autounterschriften deaktivieren Autounterschriften sind deaktiviert Automatische Beschriftungen sind aktiviert - + Aktionstasten Verstecke oder zeige Schaltflächen unter Videos Verstecke Likes und Dislikes @@ -426,23 +469,7 @@ This is because Crowdin requires temporarily flattening this file and removing t In Wiedergabeliste speichern Button ist ausgeblendet In Wiedergabeliste speichern wird angezeigt - - Verstecke Autoplay-Schaltfläche - Autoplay Button ist ausgeblendet - Autoplay Button wird angezeigt - - - - Beschriftungstaste ausblenden - Beschriftungs-Button ist ausgeblendet - Schaltfläche für Untertitel wird angezeigt - - - Cast-Button ausblenden - Der Cast button ist ausgeblendet - Der Cast button wird angezeigt - - + Navigation buttons Verstecke oder ändere Schaltflächen in der Navigationsleiste @@ -469,7 +496,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Labels sind ausgeblendet Labels werden angezeigt - + Flyout menu Verstecke oder zeige Player-Flyout-Menüeinträge @@ -480,6 +507,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Zusätzliche Einstellungen ausblenden Zusätzliches Einstellungsmenü ist ausgeblendet Zusätzliches Einstellungsmenü wird angezeigt + + Sleep-Timer ausblenden + Schlaf-Timer-Menü ist ausgeblendet + Schlaf-Timer-Menü wird angezeigt Schleifenvideo ausblenden Loop Video Menü ist ausgeblendet @@ -488,6 +519,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Ambient-Modus ausblenden Ambient Modus Menü ist ausgeblendet Inaktivitätsmodus Menü wird angezeigt + Stabile Lautstärke ausblenden + Stabile Lautstärke Menü wird angezeigt + Stabile Lautstärke Menü ist ausgeblendet Hilfe- & -Feedback ausblenden Hilfe & Feedback-Menü ist ausgeblendet @@ -513,83 +547,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Überwachung in VR ausblenden Im VR-Menü beobachten ist ausgeblendet Im VR-Menü beobachten wird angezeigt + Videoqualitätsmenüfußzeile ausblenden + Video-Qualität Menü-Fußzeile ist ausgeblendet + Video-Qualität Menü-Fußzeile wird angezeigt - - Vorherige & Nächste Video-Tasten ausblenden - Buttons sind ausgeblendet - Tasten werden angezeigt + + Vorherige & Nächste Video-Tasten ausblenden + Buttons sind ausgeblendet + Tasten werden angezeigt + Cast-Button ausblenden + Der Cast button ist ausgeblendet + Der Cast button wird angezeigt + + Beschriftungstaste ausblenden + Beschriftungs-Button ist ausgeblendet + Schaltfläche für Untertitel wird angezeigt + Verstecke Autoplay-Schaltfläche + Autoplay Button ist ausgeblendet + Autoplay Button wird angezeigt - - Albumkarten ausblenden - Albumkarten sind ausgeblendet - Albumkarten werden angezeigt - - - Kommentare - Komponenten der Kommentar-Sektion ausblenden oder anzeigen - \'Kommentare von Mitglieder\' im Kopfbereich ausblenden - \'Kommentare von Mitglieder\' Header ist ausgeblendet - \'Kommentare von Mitgliedern\' wird angezeigt - Kommentarbereich ausblenden - Kommentarbereich ist ausgeblendet - Kommentarbereich wird angezeigt - \'Verknüpfung erstellen\'-Button ausblenden - \'Verknüpfung erstellen\' Button ist ausgeblendet - Schaltfläche \"Verknüpfung erstellen\" wird angezeigt - Vorschaukommentar ausblenden - Vorschaukommentar ist ausgeblendet - Vorschau des Kommentars wird angezeigt - Dankeschön ausblenden - Dankeschön-Taste ist ausgeblendet - Dankeschön Button wird angezeigt - Zeitstempel und Emoji-Tasten ausblenden - Zeitstempel und Emoji-Tasten sind ausgeblendet - Zeitstempel und Emoji-Tasten werden angezeigt - - - Crowdfunding Box ausblenden - Crowdfunding-Box ist ausgeblendet - Crowdfunding-Box wird angezeigt - - + Endkarte ausblenden Endbildschirmkarten sind ausgeblendet Endbildschirmkarten werden angezeigt - - Filterleiste - Verstecke oder zeige die Filterleiste im Feed, in der Suche und verwandten Videos - Im Feed ausblenden - Versteckt im Feed - Im Feed angezeigt - In der Suche ausblenden - Versteckt in der Suche - In der Suche angezeigt - In verwandten Videos ausblenden - Versteckt in verwandten Videos - In verwandten Videos angezeigt - - - Schwebende Mikrofon-Taste ausblenden - Mikrofon-Button ausgeblendet - Mikrofon-Taste angezeigt - - + Ambient-Modus im Vollbildmodus deaktivieren Ambient-Modus deaktiviert Ambient-Modus aktiviert - + Infokarten ausblenden Infokarten sind ausgeblendet Infokarten werden angezeigt - + Rollladen-Animationen deaktivieren Rollende Zahlen sind nicht animiert Rollende Zahlen sind animiert - + Suchleiste im Video-Player ausblenden Video-Player-Suchleiste ist ausgeblendet Suchleiste für Video-Player wird angezeigt @@ -597,7 +594,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Miniaturansicht-Suchleiste ist ausgeblendet Miniaturansicht-Suchleiste wird angezeigt - + + Shorts Spieler + Komponenten im Shorts Spieler ausblenden oder anzeigen Shorts im Home Feed ausblenden Shorts im Home Feed sind ausgeblendet @@ -695,27 +694,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Navigationsleiste ist ausgeblendet Navigationsleiste wird angezeigt - + Empfohlene Video-Endbildschirm deaktivieren Empfohlene Videos werden deaktiviert Empfohlene Videos werden angezeigt - + Verstecke Video-Zeitstempel Zeitstempel ist ausgeblendet Zeitstempel wird angezeigt - + Player-Popup-Panels ausblenden Player-Popup-Fenster sind ausgeblendet Player-Popup-Fenster werden angezeigt - + Spieler-Überlagerung Deckkraft Deckkraft Wert zwischen 0-100, wobei 0 transparent ist Spieler-Overlay-Deckkraft muss zwischen 0-100 liegen - + Dislikes vorläufig nicht verfügbar (API Timeout) Dislikes nicht verfügbar (Status %d) @@ -759,17 +758,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Client-Ratenlimit %d-mal erreicht %d Millisekunden - + breite Suchleiste aktivieren Breite Suchleiste ist aktiviert Breite Suchleiste ist deaktiviert - + + Aktiviere hochwertige Vorschaubilder + Thumbnails der Suchleiste sind hohe Qualität + Thumbnails in der Suchleiste sind mittlere Qualität + Thumbnails in der Suchleiste sind qualitativ hochwertig + Thumbnails in der Suchleiste sind mittlere Qualität + Dadurch werden auch Thumbnails auf Livestreams wiederhergestellt, die keine Suchleisten-Thumbnails haben.\n\nSuchleisten-Thumbnails verwenden die gleiche Qualität wie das aktuelle Video.\n\nDiese Funktion funktioniert am besten mit einer Videoqualität von 720p oder niedriger und bei einer sehr schnellen Internetverbindung. Alte Suchleisten-Thumbnails wiederherstellen Suchleisten-Thumbnails werden über der Suchleiste angezeigt Miniaturansichten werden im Vollbild angezeigt - + SponsorBlock aktivieren SponsorBlock ist ein Crowd-Source-System, um nervige Teile von YouTube-Videos zu überspringen Darstellung @@ -950,7 +955,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Über Daten werden von der SponsorBlock API bereitgestellt. Tippe hier, um mehr zu erfahren und Downloads für andere Plattformen zu sehen - + Spoof-App-Version Version gefälscht Version nicht gefälscht @@ -963,9 +968,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Wiederherstellen der breiten Videogeschwindigkeit & Qualitätsmenü 18.09.39 - Bibliotheks-Tab wiederherstellen 17.41.37 - Alte Wiedergabeliste wiederherstellen - 17.33.42 - Altes UI-Layout wiederherstellen - + Startseite festlegen Standard Kanäle durchsuchen @@ -983,18 +987,26 @@ This is because Crowdin requires temporarily flattening this file and removing t Beliebt Später ansehen - + Fortsetzen des Shorts Players deaktivieren Der Shorts Player wird beim Start der App nicht fortgesetzt Shorts-Player wird beim Start der App fortgesetzt - + + Autoplay Shorts + Shorts werden autoplay + Shorts wiederholen + Autoplay Shorts Hintergrund spielen + Shorts-Hintergrund spielen wird automatisch abgespielt + Shorts-Hintergrundwiedergabe wiederholt sich + + Tablet Layout aktivieren Tablet Layout ist aktiviert Tablet Layout ist deaktiviert Community-Beiträge werden nicht auf Tablet Layouts angezeigt - + Miniplayer Ändere den Stil des in App minimierten Players Minispielertyp @@ -1013,6 +1025,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Drag and Drop aktivieren Drag and Drop ist aktiviert\n\nMiniplayer kann in jede Ecke des Bildschirms gezogen werden Drag and Drop ist deaktiviert + Horizontales Ziehen aktivieren + Horizontale Drag Geste aktiviert\n\nMiniplayer kann vom Bildschirm nach links oder rechts gezogen werden + Horizontale Drag Geste deaktiviert Schließen-Button ausblenden Schließen-Button ist ausgeblendet Schließen-Schaltfläche wird angezeigt @@ -1032,12 +1047,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Deckkraft Wert zwischen 0-100, wobei 0 transparent ist Miniplayer-Overlay-Deckkraft muss zwischen 0-100 liegen - + Gradientenladebildschirm aktivieren Lade Bildschirm hat einen Farbverlauf Hintergrund Das Laden des Bildschirms wird einen soliden Hintergrund haben - + Eigene Suchleistenfarbe aktivieren Angepasste Suchleistenfarbe wird angezeigt Originalfarbe der Suchleiste wird angezeigt @@ -1045,12 +1060,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Die Farbe der Suchleiste Ungültiger Suchleisten-Farbwert - + Bildgebietsbeschränkungen umgehen Bild-Host yt4.ggpht.com verwenden Verwendung des ursprünglichen Bild-Hosts\n\nAktivieren kann fehlende Bilder beheben, die in einigen Regionen blockiert werden - + Home-Tab @@ -1082,7 +1097,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Pfeil vorübergehend nicht verfügbar (Statuscode: %s) Pfeil vorübergehend nicht verfügbar - + Verbesserte Ankündigungen anzeigen Ankündigungen werden beim Start angezeigt Ankündigungen werden beim Start nicht angezeigt @@ -1090,47 +1105,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Verbindung zum Ankündigungsanbieter fehlgeschlagen Verwerfen - + Warnung Ihr Verlauf wird nicht gespeichert.<br><br>Dies wird höchstwahrscheinlich durch einen DNS-Werbeblocker oder einen Netzwerkproxy verursacht.<br><br>Um dies zu beheben, setze <b>s.youtube.com</b> auf die Whitelist oder schalten Sie alle DNS-Blocker und Proxies aus. Nicht wieder anzeigen - + Auto-Wiederholung aktivieren Auto-Wiederholung ist aktiviert Auto-Wiederholung ist deaktiviert - + Spoof-Gerätegröße Die Dimensionen des Geräts wurden mit\n\nverschönert. Höhere Video-Qualitäten können freigeschaltet werden, aber es kann sein, dass Videowiedergabe stuttert, die Batterielebensdauer verschlechtert und unbekannte Nebeneffekte auftreten Gerätedimensionen nicht gefälscht\n\nAktivieren kann höhere Video-Qualitäten freischalten Aktivieren kann dazu führen, dass Videowiedergabe blockiert, die Batterielebensdauer verschlechtert und unbekannte Nebeneffekte entstehen. - + GmsCore Einstellungen Einstellungen für GmsCore - + URL-Weiterleitungen umgehen URL-Umleitungen werden umgangen URL-Umleitungen werden nicht umgangen - + Links im Browser öffnen Links extern öffnen Öffne Links in der App - + Tracking-Abfrageparameter entfernen Tracking-Abfrageparameter wird von Links entfernt Tracking-Abfrageparameter wird nicht von Links entfernt - + Zoomhaptik deaktivieren Haptik ist deaktiviert Haptik ist aktiviert - + Automatische Qualität Änderungen der Videoqualität merken Qualitätsänderungen gelten für alle Videos @@ -1141,35 +1156,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Standard %1$s Qualität geändert zu: %2$s - + Zeige Geschwindigkeitsdialog Taste Button wird angezeigt Button wird nicht angezeigt - + + Benutzerdefiniertes Wiedergabegeschwindigkeitsmenü + Benutzerdefiniertes Geschwindigkeitsmenü wird angezeigt + Benutzerdefiniertes Geschwindigkeitsmenü wird nicht angezeigt Benutzerdefinierte Wiedergabegeschwindigkeiten - Verfügbare Wiedergabegeschwindigkeiten hinzufügen oder ändern + Hinzufügen oder Ändern der benutzerdefinierten Wiedergabegeschwindigkeit Benutzerdefinierte Geschwindigkeiten müssen kleiner als %ssein. Standardwerte werden verwendet. Ungültige benutzerdefinierte Wiedergabegeschwindigkeiten. Standardwerte verwenden. - + Änderungen der Wiedergabegeschwindigkeit merken Änderungen der Wiedergabegeschwindigkeit gelten für alle Videos Änderungen der Wiedergabegeschwindigkeit gelten nur für das aktuelle Video Standard Wiedergabegeschwindigkeit Standardgeschwindigkeit geändert zu: %s - + Altes Videoqualitätsmenü wiederherstellen Altes Video-Qualitätsmenü wird angezeigt Altes Video-Qualitätsmenü wird nicht angezeigt - + Folie zum Suchen aktivieren Slide zum Suchen ist aktiviert Slide zum Suchen ist nicht aktiviert - + Spoof-Video-Streams Spoof der Client-Videostreams um Wiedergabeprobleme zu verhindern Spoof-Video-Streams @@ -1187,20 +1205,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Android VR Spoofing Nebeneffekte • Audio Track Menü fehlt\n• Stabile Lautstärke ist nicht verfügbar - - - Automatische HDR-Helligkeit aktivieren - Automatische HDR-Helligkeit ist aktiviert - Auto-HDR-Helligkeit ist deaktiviert - - + Audio-Werbung blockieren Audiowerbung ist gesperrt Audiowerbung ist entsperrt - + %s ist nicht verfügbar. Ads könnten angezeigt werden. Versuche einem anderen Adblock-Dienst. Der %s Server hat einen Fehler zurückgegeben. Ads könnten angezeigt werden. Versuche einen anderen Adblock-Dienst. Blockiere eingebettete Video-Anzeigen @@ -1208,30 +1220,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Leuchtender Proxy PurpleAdBlock Proxy - + Videowerbung blockieren Videowerbung ist gesperrt Video-Anzeigen sind entsperrt - + Nachricht gelöscht Gelöschte Nachrichten anzeigen Gelöschte Nachrichten nicht anzeigen Gelöschte Nachrichten hinter einem Spoiler ausblenden Gelöschte Nachrichten als überschneidenden Text anzeigen - + Kanalpunkte automatisch beanspruchen Kanalpunkte werden automatisch abgeholt Kanalpunkte werden nicht automatisch abgeholt - + Twitch-Debug-Modus aktivieren Twitch-Debug-Modus ist aktiviert (nicht empfohlen) Twitch-Debug-Modus ist deaktiviert - + Verbesserte Einstellungen Werbung Werbeblocker-Einstellungen diff --git a/src/main/resources/addresources/values-el-rGR/strings.xml b/patches/src/main/resources/addresources/values-el-rGR/strings.xml similarity index 93% rename from src/main/resources/addresources/values-el-rGR/strings.xml rename to patches/src/main/resources/addresources/values-el-rGR/strings.xml index be889d110..9efe9c8f8 100644 --- a/src/main/resources/addresources/values-el-rGR/strings.xml +++ b/patches/src/main/resources/addresources/values-el-rGR/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Αποτυχία ελέγχων Άνοιγμα επίσημης ιστοσελίδας Παράλειψη @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Τροποποιήθηκε πριν %s μέρες Ημερομηνία κατασκευής του APK είναι κατεστραμμένη - + Θέλετε να συνεχίσετε; Επαναφορά Ανανέωση και επανεκκίνηση @@ -62,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Επίσημοι σύνδεσμοι Δωρεά - + Το MicroG GmsCore δεν έχει εγκατασταθεί. Εγκαταστήστε το. Απαιτείται ενέργεια @@ -73,7 +73,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Σχετικά με Διαφημίσεις Εναλλακτικές μικρογραφίες @@ -85,7 +85,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Διάφορα Βίντεο - + + Απενεργοποίηση αναπαραγωγής παρασκηνίου για τα Shorts + Η αναπαραγωγή παρασκηνίου είναι απενεργοποιημένη για τα Shorts + Η αναπαραγωγή παρασκηνίου είναι ενεργοποιημένη για τα Shorts + + Εντοπισμός σφαλμάτων Ενεργοποίηση ή απενεργοποίηση επιλογών εντοπισμού σφαλμάτων Καταγραφή σφαλμάτων @@ -102,13 +107,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Να μην εμφανίζεται μήνυμα στο κάτω μέρος της οθόνης σε περιπτώσεις σφαλμάτων Η απενεργοποίηση των μηνυμάτων σφαλμάτων κρύβει όλες τις ειδοποιήσεις σφαλμάτων που αφορούν το ReVanced.\n\nΔεν θα ειδοποιήστε για τυχόν απρόβλεπτα γεγονότα. - + Απενεργοποίηση λάμψης των κουμπιών «Μου αρέσει» και «Εγγραφή» Τα κουμπιά «Μου αρέσει» και «Εγγραφή» δεν θα λάμπουν όταν αναφέρονται Τα κουμπιά «Μου αρέσει» και «Εγγραφή» θα λάμπουν όταν αναφέρονται - Γκρι διαχωριστικά - Κρυμμένα - Εμφανίζονται + Κάρτες άλμπουμ + Κρυμμένες + Εμφανίζονται + Πλαίσιο δωρεών + Κρυμμένο + Εμφανίζεται + Αιωρούμενο κουμπί μικροφώνου + Κρυμμένο + Εμφανίζεται Υδατογράφημα καναλιού Κρυμμένο Εμφανίζεται @@ -153,9 +164,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Επεκτάσιμα πλαίσια κάτω από τα βίντεο Κρυμμένα Εμφανίζονται - Οδηγίες του μενού ποιότητας βίντεο - Κρυμμένες - Εμφανίζονται Δημοσιεύσεις κοινότητας Κρυμμένες Εμφανίζονται @@ -230,6 +238,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Εμφανίζεται Περιγραφή βίντεο Απόκρυψη ή εμφάνιση στοιχείων περιγραφής βίντεο + Γραμμή φίλτρων + Απόκρυψη η εμφάνιση της γραμμής φίλτρων στη ροή, αναζήτηση και τα σχετικά βίντεο + Απόκρυψη στη ροή + Κρυμμένη + Εμφανίζεται + Απόκρυψη στα αποτελέσματα αναζήτησης + Κρυμμένη + Εμφανίζεται + Απόκρυψη στα σχετικά βίντεο + Κρυμμένη + Εμφανίζεται + Σχόλια + Απόκρυψη ή εμφάνιση στοιχείων στα σχόλια + Ετικέτα «Σχόλια από μέλη» + Κρυμμένη + Εμφανίζεται + Ενότητα σχολίων + Κρυμμένη + Εμφανίζεται + Κουμπί «Δημιουργία Short» + Κρυμμένο + Εμφανίζεται + Προεπισκόπηση σχολίου + Κρυμμένη + Εμφανίζεται + Κουμπί «Σας ευχαριστούμε» + Κρυμμένο + Εμφανίζεται + Κουμπιά χρονοσήμανσης και emoji + Κρυμμένα + Εμφανίζονται YouTube Doodles Κρυμμένα @@ -271,7 +310,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Η λέξη-κλειδί είναι πολύ σύντομη και απαιτεί εισαγωγικά: %s Θα κρυφτούν όλα τα βίντεο με την λέξη-κλειδί: %s - + Γενικές διαφημίσεις Κρυμμένες Εμφανίζονται @@ -290,6 +329,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Διαφημίσεις προβολής προϊόντων Κρυμμένες Εμφανίζονται + Ενότητα αγορών οθόνης αναπαραγωγής + Κρυμμένη + Εμφανίζεται Σύνδεσμοι αγορών στην περιγραφή βίντεο Κρυμμένοι Εμφανίζονται @@ -306,17 +348,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Η απόκρυψη διαφημίσεων πλήρους οθόνης λειτουργεί μόνο με παλαιότερες συσκευές - + Προωθήσεις για απόκτηση YouTube Premium Κρυμμένες Εμφανίζονται - + Διαφημίσεις βίντεο Κρυμμένες Εμφανίζονται - + Η διεύθυνση URL αντιγράφηκε στο πρόχειρο Η διεύθυνση URL αντιγράφηκε με χρονική σήμανση Εμφάνιση κουμπιού αντιγραφής URL βίντεο @@ -326,13 +368,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Το κουμπί εμφανίζεται. Πατήστε για αντιγραφή του συνδέσμου βίντεο με χρονική σήμανση ή πατήστε παρατεταμένα για αντιγραφή του συνδέσμου βίντεο χωρίς χρονική σήμανση Το κουμπί δεν εμφανίζεται - + Παράθυρο ηλικιακού περιορισμού Κρυμμένο Εμφανίζεται Αυτό δεν παρακάμπτει τον ηλικιακό περιορισμό. Απλώς τον αποδέχεται αυτόματα. - + Εξωτερικές λήψεις Ρυθμίσεις για χρήση εξωτερικού προγράμματος λήψης Εμφάνιση κουμπιού εξωτερικής λήψης @@ -346,17 +388,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Το όνομα πακέτου της εγκατεστημένης σας εξωτερικής εφαρμογής λήψης όπως το NewPipe ή το Seal Το %s δεν είναι εγκατεστημένο. Παρακαλούμε εγκαταστήστε το. - + Απενεργοποίηση ακριβής αναζήτησης Η χειρονομία αναζήτησης καρέ-καρέ είναι απενεργοποιημένη Η χειρονομία αναζήτησης καρέ-καρέ είναι ενεργοποιημένη - + Πάτημα γραμμής προόδου Το πάτημα στη γραμμή προόδου είναι ενεργοποιημένο Το πάτημα στη γραμμή προόδου είναι απενεργοποιημένο - + Σάρωση οθόνης για φωτεινότητα Η δυνατότητα αλλαγής φωτεινότητας με χειρονομία κατακόρυφης σάρωσης στην αριστερή πλευρά της οθόνης αναπαραγωγής είναι ενεργοποιημένη Η δυνατότητα αλλαγής φωτεινότητας με χειρονομία σάρωσης στην οθόνη αναπαραγωγής είναι απενεργοποιημένη @@ -385,12 +427,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Κατώτατο όριο μεγέθους σάρωσης Η ελάχιστη απόσταση που θα διανύσετε με το δάκτυλο σας για να είναι αναγνωρίσιμη η χειρονομία σάρωσης - + Απενεργοποίηση αυτόματων υπότιτλων Οι αυτόματοι υπότιτλοι είναι απενεργοποιημένοι Οι αυτόματοι υπότιτλοι είναι ενεργοποιημένοι - + Κουμπιά ενεργειών Απόκρυψη ή εμφάνιση κουμπιών κάτω από βίντεο Κουμπιά «Μου αρέσει» και «Δεν μου αρέσει» @@ -426,23 +468,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Κρυμμένο Εμφανίζεται - - Κουμπί αυτόματης αναπαραγωγής - Κρυμμένο - Εμφανίζεται - - - - Κουμπί υποτίτλων - Κρυμμένο - Εμφανίζεται - - - Κουμπί μετάδοσης - Κρυμμένο - Εμφανίζεται - - + Κουμπιά γραμμής πλοήγησης Απόκρυψη ή αλλαγή κουμπιών στη γραμμή πλοήγησης @@ -469,7 +495,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Κρυμμένες Εμφανίζονται - + Αναδυόμενο μενού ρυθμίσεων Απόκρυψη ή εμφάνιση στοιχείων του αναδυόμενου μενού στην οθόνη αναπαραγωγής @@ -480,6 +506,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Μενού «Πρόσθετες ρυθμίσεις» Κρυμμένο Εμφανίζεται + + Μενού «Χρονόμετρο ύπνου» + Κρυμμένο + Εμφανίζεται Μενού «Επανάληψη βίντεο» Κρυμμένο @@ -488,6 +518,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Μενού «Λειτουργία περιβάλλοντος» Κρυμμένο Εμφανίζεται + Μενού «Σταθερή ένταση» + Εμφανίζεται + Κρυμμένο Μενού «Βοήθεια & σχόλια» Κρυμμένο @@ -513,83 +546,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Μενού «Προβολή σε VR» Κρυμμένο Εμφανίζεται + Οδηγίες του μενού ποιότητας βίντεο + Κρυμμένες + Εμφανίζονται - - Κουμπιά προηγούμενου & επόμενου βίντεο - Κρυμμένα - Εμφανίζονται + + Κουμπιά προηγούμενου & επόμενου βίντεο + Κρυμμένα + Εμφανίζονται + Κουμπί μετάδοσης + Κρυμμένο + Εμφανίζεται + + Κουμπί υποτίτλων + Κρυμμένο + Εμφανίζεται + Κουμπί αυτόματης αναπαραγωγής + Κρυμμένο + Εμφανίζεται - - Κάρτες άλμπουμ - Κρυμμένες - Εμφανίζονται - - - Σχόλια - Απόκρυψη ή εμφάνιση στοιχείων στα σχόλια - Ετικέτα «Σχόλια από μέλη» - Κρυμμένη - Εμφανίζεται - Ενότητα σχολίων - Κρυμμένη - Εμφανίζεται - Κουμπί «Δημιουργία Short» - Κρυμμένο - Εμφανίζεται - Προεπισκόπηση σχολίου - Κρυμμένη - Εμφανίζεται - Κουμπί «Σας ευχαριστούμε» - Κρυμμένο - Εμφανίζεται - Κουμπιά χρονοσήμανσης και emoji - Κρυμμένα - Εμφανίζονται - - - Πλαίσιο δωρεών - Κρυμμένο - Εμφανίζεται - - + Κάρτες τελικής οθόνης Κρυμμένες Εμφανίζονται - - Γραμμή φίλτρων - Απόκρυψη η εμφάνιση της γραμμής φίλτρων στη ροή, αναζήτηση και τα σχετικά βίντεο - Απόκρυψη στη ροή - Κρυμμένη - Εμφανίζεται - Απόκρυψη στα αποτελέσματα αναζήτησης - Κρυμμένη - Εμφανίζεται - Απόκρυψη στα σχετικά βίντεο - Κρυμμένη - Εμφανίζεται - - - Αιωρούμενο κουμπί μικροφώνου - Κρυμμένο - Εμφανίζεται - - + Απενεργοποίηση λειτουργίας περιβάλλοντος σε πλήρη οθόνη Η λειτουργία περιβάλλοντος σε πλήρη οθόνη είναι απενεργοποιημένη Η λειτουργία περιβάλλοντος σε πλήρη οθόνη είναι ενεργοποιημένη - + Κάρτες πληροφοριών Κρυμμένες Εμφανίζονται - + Απενεργοποίηση κινήσεων αριθμών Οι αριθμοί δε θα κινούνται αυξανόμενοι εκθετικά Οι αριθμοί κινούνται αυξανόμενοι εκθετικά - + Γραμμή προόδου στην οθόνη αναπαραγωγής Κρυμμένη Εμφανίζεται @@ -597,7 +593,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Κρυμμένη Εμφανίζεται - + + Οθόνη αναπαραγωγής Shorts + Απόκρυψη ή εμφάνιση στοιχείων στην οθόνη αναπαραγωγής Shorts Shorts στην αρχική σελίδα Κρυμμένα @@ -695,27 +693,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Η γραμμή πλοήγησης θα είναι κρυμμένη κατά την αναπαραγωγή Shorts Η γραμμή πλοήγησης θα εμφανίζεται κατά την αναπαραγωγή Shorts - + Τελική οθόνη προτεινόμενων βίντεο Κρυμμένη Εμφανίζεται - + Χρονική πρόοδος βίντεο Κρυμμένη Εμφανίζεται - + Αναδυόμενα παράθυρα οθόνης αναπαραγωγής Κρυμμένα Εμφανίζονται - + Αδιαφάνεια φόντου οθόνης αναπαραγωγής Τιμή αδιαφάνειας μεταξύ 0-100, όπου το 0 είναι διαφανές Η αδιαφάνεια φόντου οθόνης αναπαραγωγής πρέπει να είναι μεταξύ 0-100 - + Dislike προσωρινά μη διαθέσιμα (καθυστέρηση API) Δεδομένα dislike μη διαθέσιμα (κατάσταση %d) @@ -759,17 +757,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Ανιχνεύθηκαν %d φορές περιορισμοί ορίου πελάτη %d χιλιοστά δευτερολέπτου - + Ευρεία γραμμή αναζήτησης Η ευρεία γραμμή αναζήτησης είναι ενεργοποιημένη Η ευρεία γραμμή αναζήτησης είναι απενεργοποιημένη - + + Μικρογραφίες υψηλής ποιότητας + Οι μικρογραφίες της γραμμής προόδου είναι υψηλής ποιότητας + Οι μικρογραφίες της γραμμής προόδου είναι μέτριας ποιότητας + Οι μικρογραφίες πλήρους οθόνης της γραμμής προόδου είναι υψηλής ποιότητας + Οι μικρογραφίες πλήρους οθόνης της γραμμής προόδου είναι μέτριας ποιότητας + Αυτό θα επαναφέρει επίσης τις μικρογραφίες σε ζωντανές μεταδόσεις που δεν έχουν μικρογραφίες γραμμής προόδου.\n\nΟι μικρογραφίες της γραμμής προόδου θα χρησιμοποιούν την ίδια ποιότητα με το τρέχον βίντεο.\n\nΑυτή η ρύθμιση λειτουργεί καλύτερα με ποιότητα βίντεο 720p ή χαμηλότερη και όταν χρησιμοποιείτε μια πολύ γρήγορη σύνδεση στο διαδίκτυο. Παλιές μικρογραφίες γραμμής προόδου Οι μικρογραφίες προεπισκόπησης θα εμφανίζονται πάνω από τη γραμμή προόδου Οι μικρογραφίες προεπισκόπησης θα εμφανίζονται σε πλήρη οθόνη - + Ενεργοποίηση του SponsorBlock Το SponsorBlock είναι ένα σύστημα που προέρχεται από το κοινό για παράληψη ενοχλητικών τμημάτων βίντεο YouTube Εμφάνιση @@ -950,7 +954,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Σχετικά με Τα δεδομένα παρέχονται από το SponsorBlock API. Πατήστε για να μάθετε περισσότερα και να δείτε λήψεις για άλλες πλατφόρμες - + Τροποποίηση έκδοσης εφαρμογής Η έκδοση τροποποιείται Η έκδοση δεν τροποποιείται @@ -963,10 +967,9 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Επαναφορά ευρέος μενού ταχύτητας & ποιότητας βίντεο 18.09.39 - Επαναφορά της καρτέλας βιβλιοθήκης 17.41.37 - Επαναφορά ενότητας λίστας αναπαραγωγής παλιού στυλ - 17.33.42 - Επαναφορά της παλιάς εμφάνισης UI - - Αλλαγή της αρχικής σελίδας + + Ορισμός αρχικής σελίδας Προεπιλογή Περιήγηση καναλιών Εξερεύνηση @@ -983,18 +986,26 @@ This is because Crowdin requires temporarily flattening this file and removing t Τάσεις Παρακολούθηση αργότερα - + Απενεργοποίηση συνέχισης των Shorts Η αναπαραγωγή Shorts δε θα συνεχίζεται κατά την εκκίνηση της εφαρμογής Η αναπαραγωγή Shorts θα συνεχίζεται κατά την εκκίνηση της εφαρμογής - + + Αυτόματη αναπαραγωγή Shorts + Τα επόμενα Shorts θα αναπαράγονται αυτόματα + Τα Shorts θα επαναλαμβάνονται + Αυτόματη αναπαραγωγή Shorts στο παρασκήνιο + Τα επόμενα Shorts θα αναπαράγονται αυτόματα στο παρασκήνιο + Τα Shorts στο παρασκήνιο θα επαναλαμβάνονται + + Λειτουργία διεπαφής τάμπλετ Η διεπαφή τάμπλετ είναι ενεργοποιημένη Η διεπαφή τάμπλετ είναι απενεργοποιημένη Οι δημοσιεύσεις κοινότητας δεν εμφανίζονται στη διεπαφή τάμπλετ - + Ελαχιστοποιημένη οθόνη αναπαραγωγής Αλλάξτε το στυλ της ελαχιστοποιημένης οθόνης αναπαραγωγής Τύπος ελαχιστοποιημένης οθόνης αναπαραγωγής @@ -1013,6 +1024,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Λειτουργία μεταφοράς και απόθεσης Η λειτουργία μεταφοράς και απόθεσης είναι ενεργοποιημένη\n\nΗ ελαχιστοποιημένη οθόνη αναπαραγωγής μπορεί να μετακινηθεί σε οποιαδήποτε γωνία της οθόνης Η λειτουργία μεταφοράς και απόθεσης είναι απενεργοποιημένη + Ενεργοποίηση οριζόντιας χειρονομίας απόρριψης + Η οριζόντια χειρονομία είναι ενεργή\n\nΗ ελαχιστοποιημένη οθόνη μπορεί να συρθεί εκτός οθόνης προς τα αριστερά ή δεξιά + Η οριζόντια χειρονομία είναι ανενεργή Κουμπί κλεισίματος Κρυμμένο Εμφανίζεται @@ -1032,12 +1046,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Τιμή αδιαφάνειας μεταξύ 0-100, όπου το 0 είναι διαφανές Η αδιαφάνεια φόντου οθόνης αναπαραγωγής πρέπει να είναι μεταξύ 0-100 - + Διαβάθμιση οθόνης φόρτωσης Η οθόνη φόρτωσης θα έχει σταδιακές αποχρώσεις φόντο Η οθόνη φόρτωσης θα έχει στατική απόχρωση φόντο - + Προσαρμοσμένο χρώμα γραμμής προόδου Η γραμμή προόδου εμφανίζεται με προσαρμοσμένο χρώμα Η γραμμή προόδου εμφανίζεται με το αρχικό χρώμα @@ -1045,12 +1059,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Το χρώμα της γραμμής προόδου Μη έγκυρη τιμή χρώματος γραμμής προόδου - + Παράκαμψη μπλοκαρίσματος φόρτωσης εικόνων Χρησιμοποιείται το domain yt4.ggpht.com για την φόρτωση εικόνων Χρησιμοποιείται το αρχικό domain για την φόρτωση εικόνων\n\nΗ ενεργοποίηση αυτής της ρύθμισης μπορεί να διορθώσει την φόρτωση εικόνων που είναι μπλοκαρισμένες σε κάποιες περιοχές - + Αρχική σελίδα @@ -1082,7 +1096,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow προσωρινά μη διαθέσιμο (κωδικός κατάστασης: %s) DeArrow προσωρινά μη διαθέσιμο - + Εμφάνιση ανακοινώσεων ReVanced Οι ανακοινώσεις θα εμφανίζονται κατά την εκκίνηση Οι ανακοινώσεις δε θα εμφανίζονται κατά την εκκίνηση @@ -1090,47 +1104,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Αποτυχία σύνδεσης με τον πάροχο ανακοινώσεων Παράλειψη - + Προειδοποίηση Το ιστορικό παρακολούθησης δεν αποθηκεύεται.<br><br>Πιθανό να συμβαίνει λόγω αποκλεισμού διαφημίσεων μέσω DNS ή μέσω διακομιστή μεσολάβησης δικτύου.<br><br>Μια λύση γι\'αυτό θα ήταν να προσθέσετε σε whitelist το <b>s.youtube.com</b> ή να απενεργοποιήστε τους DNS/proxy blockers. Να μην εμφανιστεί ξανά - + Ενεργοποίηση αυτόματης επανάληψης Η αυτόματη επανάληψη είναι ενεργοποιημένη Η αυτόματη επανάληψη είναι απενεργοποιημένη - + Παραποίηση διαστάσεων συσκευής Οι διαστάσεις συσκευής παραποιούνται\n\nΜπορεί να γίνουν διαθέσιμες υψηλότερες ποιότητες βίντεο, αλλά πιθανότατα να αντιμετωπίσετε μικρο-κολλήματα κατά την αναπαραγωγή, χειρότερη διάρκεια ζωής μπαταρίας και άλλες άγνωστες παρενέργειες Οι διαστάσεις συσκευής δεν παραποιούνται\n\nΕνεργοποιώντας αυτή τη ρύθμιση μπορούν να γίνουν διαθέσιμες υψηλότερες ποιότητες βίντεο Η ενεργοποίηση αυτής της λειτουργίας μπορεί να προκαλέσει μικρο-κολλήματα κατά την αναπαραγωγή, χειρότερη διάρκεια ζωής μπαταρίας, και άλλες άγνωστες παρενέργειες. - + Ρυθμίσεις GmsCore Ρυθμίσεις για το MicroG GmsCore - + Παράκαμψη ανακατευθύνσεων συνδέσμων Οι ανακατευθύνσεις συνδέσμων URL παρακάμπτονται Οι ανακατευθύνσεις συνδέσμων URL δεν παρακάμπτονται - + Άνοιγμα συνδέσμων σε πρόγραμμα περιήγησης Οι σύνδεσμοι ανοίγουν εξωτερικά Οι σύνδεσμοι ανοίγουν εντός της εφαρμογής - + Καθαρισμός συνδέσμων κοινοποίησης Η παράμετρος παρακολούθησης αφαιρείται από τους συνδέσμους στην κοινοποίηση Η παράμετρος παρακολούθησης δεν αφαιρείται από τους συνδέσμους στην κοινοποίηση - + Κατάργηση απόκρισης δόνησης στο ζουμ Η απόκριση δόνησης είναι απενεργοποιημένη Η απόκριση δόνησης είναι ενεργοποιημένη - + Αυτόματη ποιότητα Απομνημόνευση αλλαγών ποιότητας βίντεο Οι αλλαγές ποιότητας θα ισχύουν για όλα τα βίντεο @@ -1141,35 +1155,38 @@ This is because Crowdin requires temporarily flattening this file and removing t Wi-Fi Η προεπιλεγμένη ποιότητα %1$s άλλαξε σε: %2$s - - Κουμπί αλλαγής ταχύτητας βίντεο - Εμφανίζεται - Κρυμμένο + + Εμφάνιση κουμπιού αλλαγής ταχύτητας + Το κουμπί εμφανίζεται + Το κουμπί δεν εμφανίζεται - + + Μενού προσαρμοσμένης ταχύτητας αναπαραγωγής + Το μενού προσαρμοσμένης ταχύτητας αναπαραγωγής εμφανίζεται + Το μενού προσαρμοσμένης ταχύτητας αναπαραγωγής δεν εμφανίζεται Προσαρμοσμένες ταχύτητες αναπαραγωγής - Προσθέστε ή αλλάξτε τις διαθέσιμες ταχύτητες αναπαραγωγής + Προσθέστε ή αλλάξτε τις προσαρμοσμένες ταχύτητες αναπαραγωγής Οι ταχύτητες πρέπει να είναι μικρότερες από %sx. Επαναφορά... Μη έγκυρες ταχύτητες αναπαραγωγής. Επαναφορά... - + Απομνημόνευση αλλαγών ταχύτητας αναπαραγωγής Οι αλλαγές ταχύτητας αναπαραγωγής θα ισχύουν για όλα τα βίντεο Οι αλλαγές ταχύτητας αναπαραγωγής θα ισχύουν μόνο για το τρέχον βίντεο Προεπιλεγμένη ταχύτητα αναπαραγωγής Η προεπιλεγμένη ταχύτητα άλλαξε σε: %s - + Επαναφορά παλιού μενού ποιότητας βίντεο Το μενού ποιότητας βίντεο θα εμφανίζεται με το παλιό στυλ Το μενού ποιότητας βίντεο θα εμφανίζεται με το νέο στυλ - + Χειρονομία οριζόντιας σάρωσης για αναζήτηση Η αναζήτηση στη γραμμή προόδου με χειρονομία οριζόντιας σάρωσης είναι ενεργοποιημένη Η αναζήτηση στη γραμμή προόδου με χειρονομία σάρωσης είναι απενεργοποιημένη - + Παραποίηση ροών βίντεο Παραποίηση ροών βίντεο του προγράμματος πελάτη για την αποφυγή προβλημάτων αναπαραγωγής Παραποίηση ροών βίντεο @@ -1187,17 +1204,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Παρενέργειες παραποίησης σε Android VR • Το μενού «Κομμάτι ήχου» λείπει\n• Η λειτουργία «Σταθερή ένταση» δεν είναι διαθέσιμη - - - - + Αποκλεισμός διαφημίσεων ήχου Οι διαφημίσεις ήχου έχουν αποκλειστεί Οι διαφημίσεις ήχου δεν έχουν αποκλειστεί - + %s μη διαθέσιμο. Οι διαφημίσεις θα εμφανίζονται. Δοκιμάστε άλλη υπηρεσία αποκλεισμού διαφημίσεων. Σφάλμα διακομιστή %s. Οι διαφημίσεις ενδέχεται να εμφανίζονται. Δοκιμάστε κάποια άλλη υπηρεσία αποκλεισμού διαφημίσεων. Αποκλεισμός ενσωματωμένων διαφημίσεων βίντεο @@ -1205,30 +1219,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Διαμεσολαβητής Luminous Διαμεσολαβητής PurpleAdBlock - + Αποκλεισμός διαφημίσεων βίντεο Οι διαφημίσεις βίντεο έχουν αποκλειστεί Οι διαφημίσεις βίντεο δεν έχουν αποκλειστεί - + το μήνυμα διαγράφηκε Εμφάνιση διαγραμμένων μηνυμάτων Να μην εμφανίζονται διαγραμμένα μηνύματα Απόκρυψη διαγραμμένων μηνυμάτων πίσω από ένα spoiler Εμφάνιση διαγραμμένων μηνυμάτων ως σβησμένο κείμενο - + Αυτόματη εξαργύρωση Πόντων Καναλιού Οι Πόντοι Καναλιών εξαργυρώνονται αυτόματα Οι Πόντοι Καναλιών δεν εξαργυρώνονται αυτόματα - + Λειτουργία εντοπισμού σφαλμάτων Twitch Η λειτουργία εντοπισμού σφαλμάτων Twitch είναι ενεργοποιημένη (δε συνιστάται) Η λειτουργία εντοπισμού σφαλμάτων Twitch είναι απενεργοποιημένη - + Ρυθμίσεις ReVanced Διαφημίσεις Ρυθμίσεις αποκλεισμού διαφημίσεων diff --git a/src/main/resources/addresources/values-es-rES/strings.xml b/patches/src/main/resources/addresources/values-es-rES/strings.xml similarity index 93% rename from src/main/resources/addresources/values-es-rES/strings.xml rename to patches/src/main/resources/addresources/values-es-rES/strings.xml index a25a3ae74..6fb1cb8a1 100644 --- a/src/main/resources/addresources/values-es-rES/strings.xml +++ b/patches/src/main/resources/addresources/values-es-rES/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Comprobaciones fallidas Abrir sitio web oficial Ignorar @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Parcheado hace %s días La fecha de compilación de APK está dañada - + ¿Desea continuar? Restablecer Actualizar y reiniciar @@ -62,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Enlaces oficiales Donar - + MicroG GmsCore no está instalado. Instálala. Acción necesaria @@ -73,7 +73,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Acerca de Anuncios Miniaturas alternativas @@ -85,7 +85,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Miscelánea Video - + + Desactivar la reproducción en segundo plano de Shorts + La reproducción de Shorts en segundo plano está desactivada + La reproducción de Shorts en segundo plano está activada + + Depuración Activar o desactivar las opciones de depuración Depurar registro @@ -102,13 +107,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Toast no se muestra si ocurre un error Desactivar los avisos (toasts) de errores oculta todas las notificaciones de error ReVanced\n\nNo se le notificará de ningún evento inesperado. - + Desactivar el brillo del botón de like / suscripción El botón de \"Me gusta\" y \"Suscribir\" no brillará cuando se mencione El botón de \"Me gusta\" y \"Suscribir\" brillará cuando se mencione - Ocultar separador gris - Los separadores de grises están ocultos - Se muestran los separadores grises + Ocultar álbumes + Las tarjetas de álbum están ocultas + Se muestran las tarjetas de álbum + Ocultar caja de recaudación + La caja de Crowdfunding está oculta + Se muestra la caja de Crowdfunding + Ocultar botón de micrófono flotante + Botón de micrófono oculto + Botón del micrófono mostrado Ocultar marca de agua del canal Marca de agua oculta Marca de agua mostrada @@ -153,9 +164,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Ocultar ficha expandible en videos Las fichas expandibles están ocultas Se muestran fichas expandibles - Ocultar pie de página del menú de calidad de vídeo - Pie de menú de calidad de vídeo oculto - El pie del menú de calidad de vídeo se muestra Ocultar mensajes comunitarios Los mensajes de la comunidad están ocultos Se muestran las publicaciones de la comunidad @@ -230,6 +238,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Se muestra la sección transcripción Descripción del vídeo Ocultar o mostrar componentes de descripción de vídeo + Barra de filtros + Ocultar o mostrar la barra de filtros en el feed, la búsqueda y vídeos relacionados + Ocultar en el feed + Escondido en el feed + Mostrar en el feed + Ocultar en búsqueda + Oculto en la búsqueda + Mostrar en búsqueda + Ocultar en vídeos relacionados + Escondido en videos relacionados + Mostrar en vídeos relacionados + Comentarios + Ocultar o mostrar los componentes de sección de comentarios + Ocultar encabezado \'Comentarios por miembros\' + El encabezado \'Comentarios por miembros\' está oculto + La cabecera \'Comentarios por miembros\' se muestra + Ocultar sección de comentarios + La sección de comentarios está oculta + Sección de comentarios mostrada + Ocultar botón \'Crear un Short\' + El botón \'Crear un Short\' está oculto + Se muestra el botón \'Crear un Short\' + Ocultar comentario de vista previa + El comentario de la vista previa está oculto + Vista previa del comentario se muestra + Ocultar botón de gracias + El botón de gracias está oculto + Se muestra el botón de gracias + Ocultar botones de hora y emoji + Botones Timestamp y emoji están ocultos + Se muestran los botones Timestamp y emoji Ocultar YouTube Doodles Barra de búsqueda Doodles están ocultos @@ -271,7 +310,7 @@ This is because Crowdin requires temporarily flattening this file and removing t La palabra clave es demasiado corta y requiere comillas: %s Palabra clave ocultará todos los vídeos: %s - + Ocultar anuncios generales Los anuncios generales están ocultos Se muestran anuncios generales @@ -290,6 +329,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Ocultar banner para ver los productos Banner oculto Banner mostrado + Ocultar estampilla de compra del jugador + El armazón de compras está oculto + Se muestra el shelf de la compra Ocultar enlaces de compras en la descripción de vídeo Enlaces de compras están ocultos Se muestran enlaces de compras @@ -306,17 +348,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Ocultar anuncio solo con dispositivos viejos - + Ocultar promociones de YouTube Premium Las promociones de YouTube Premium en el reproductor de vídeo están ocultas Se muestran las promociones de YouTube Premium en el reproductor de vídeo - + Ocultar anuncios de video Los anuncios de vídeo están ocultos Los anuncios de vídeo se muestran - + URL copiada al portapapeles URL con marca de tiempo copiada Mostrar botón URL de copia de vídeo @@ -326,13 +368,13 @@ This is because Crowdin requires temporarily flattening this file and removing t El botón se muestra. Toque para copiar la URL del vídeo con la marca de tiempo. Toque y mantenga pulsado para copiar el vídeo sin marca de tiempo El botón no se muestra - + Eliminar diálogo de discreción del visor Se eliminará el diálogo Se mostrará el diálogo Esto no pasa por alto la restricción de edad, sino que simplemente la acepta automáticamente. - + Descargas externa Configuración para el uso de un descargador externo Mostrar botón externo de descarga @@ -346,17 +388,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Nombre del paquete de su aplicación de descarga externa instalada, como NewPipe o Seal %s no está instalado. Por favor, instálelo. - + Desactivar gesto de búsqueda preciso El gesto está desactivado Gesto habilitado - + Habilitar toque en la barra de búsqueda Seekbar toping está habilitado Seekbar toping está desactivado - + Activar gesto de brillo Deslizar brillo está habilitado Deslizar brillo está desactivado @@ -385,12 +427,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Umbral de magnitud del deslizamiento La cantidad de umbral para que se desliza - + Desactivar auto subtítulos Los títulos automáticos están desactivados Los títulos automáticos están habilitados - + Botones de acción Ocultar o mostrar botones en videos Ocultar me gusta y no me gusta @@ -426,23 +468,7 @@ This is because Crowdin requires temporarily flattening this file and removing t El botón Guardar a la lista de reproducción está oculto Mostrar el botón Guardar a la lista - - Ocultar botón de reproducción automática - El botón de reproducción automática está oculto - Se muestra el botón de reproducción automática - - - - Ocultar botón de subtítulos - Botón de subtítulos oculto - Botón de subtítulos mostrado - - - Ocultar botón de reparto - El botón de envío a otros dispositivos está oculto - El botón de envío a otros dispositivos es visible - - + Navigation buttons Ocultar o cambiar botones en la barra de navegación @@ -469,7 +495,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Las etiquetas están ocultas Las etiquetas se muestran - + Flyout menu Ocultar o mostrar elementos del menú de vuelo del jugador @@ -480,6 +506,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Ocultar ajustes adicionales Menú de configuración adicional oculto Se muestra el menú de configuración adicional + + Ocultar temporizador de sueño + Menú de temporizador de sueño oculto + El menú de temporizador de sueño se muestra Ocultar video de bucle El menú de video Loop está oculto @@ -488,6 +518,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Ocultar modo ambiente Menú de modo ambiente oculto Se muestra el menú de modo ambiente + Ocultar volumen estable + Se muestra el menú de volumen estable + El menú de volumen estable está oculto Ocultar Ayuda & Comentarios El menú de ayuda & comentarios está oculto @@ -513,83 +546,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Ocultar reloj en VR Ver en el menú VR está oculto Ver en el menú VR se muestra + Ocultar pie de página del menú de calidad de vídeo + Pie de menú de calidad de vídeo oculto + El pie del menú de calidad de vídeo se muestra - - Ocultar botones de vídeo anteriores & siguiente - Los botones están ocultos - Los botones se muestran + + Ocultar botones de vídeo anteriores & siguiente + Los botones están ocultos + Los botones se muestran + Ocultar botón de reparto + El botón de envío a otros dispositivos está oculto + El botón de envío a otros dispositivos es visible + + Ocultar botón de subtítulos + Botón de subtítulos oculto + Botón de subtítulos mostrado + Ocultar botón de reproducción automática + El botón de reproducción automática está oculto + Se muestra el botón de reproducción automática - - Ocultar álbumes - Las tarjetas de álbum están ocultas - Se muestran las tarjetas de álbum - - - Comentarios - Ocultar o mostrar los componentes de sección de comentarios - Ocultar encabezado \'Comentarios por miembros\' - El encabezado \'Comentarios por miembros\' está oculto - La cabecera \'Comentarios por miembros\' se muestra - Ocultar sección de comentarios - La sección de comentarios está oculta - Sección de comentarios mostrada - Ocultar botón \'Crear un Short\' - El botón \'Crear un Short\' está oculto - Se muestra el botón \'Crear un Short\' - Ocultar comentario de vista previa - El comentario de la vista previa está oculto - Vista previa del comentario se muestra - Ocultar botón de gracias - El botón de gracias está oculto - Se muestra el botón de gracias - Ocultar botones de hora y emoji - Botones Timestamp y emoji están ocultos - Se muestran los botones Timestamp y emoji - - - Ocultar caja de recaudación - La caja de Crowdfunding está oculta - Se muestra la caja de Crowdfunding - - + Ocultar tarjetas de pantalla final Las tarjetas de pantalla de fin están ocultas Se muestran las tarjetas de la pantalla final - - Barra de filtros - Ocultar o mostrar la barra de filtros en el feed, la búsqueda y vídeos relacionados - Ocultar en el feed - Escondido en el feed - Mostrar en el feed - Ocultar en búsqueda - Oculto en la búsqueda - Mostrar en búsqueda - Ocultar en vídeos relacionados - Escondido en videos relacionados - Mostrar en vídeos relacionados - - - Ocultar botón de micrófono flotante - Botón de micrófono oculto - Botón del micrófono mostrado - - + Desactivar el modo ambiente en pantalla completa Modo ambiente desactivado Modo ambiente activado - + Ocultar tarjetas de información Las tarjetas de información están ocultas Las tarjetas de información están visibles - + Desactivar animaciones de número de rodamiento Los números de registro no están animados Los números de registro son animados - + Ocultar barra de búsqueda en el reproductor de vídeo La barra de búsqueda del reproductor de vídeo está oculta La barra de búsqueda del reproductor de vídeo se muestra @@ -597,7 +593,9 @@ This is because Crowdin requires temporarily flattening this file and removing t La barra de búsqueda de miniaturas está oculta La barra de búsqueda de miniaturas se muestra - + + Reproductor de Shorts + Oculta o muestra componentes en el reproductor de Shorts Ocultar Shorts en el Inicio Los Shorts en el Inicio están ocultos @@ -695,27 +693,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Barra de navegación oculta Se muestra la barra de navegación - + Desactivar pantalla de final de vídeo sugerida Vídeos sugeridos serán desactivados Se mostrarán vídeos sugeridos - + Ocultar fecha y hora de vídeo Marca de tiempo oculta Marca de tiempo mostrada - + Ocultar paneles emergentes del jugador Los paneles emergentes del jugador están ocultos Se muestran paneles emergentes del jugador - + Opacidad de superposición del jugador Valor de potencia entre 0-100, donde 0 es transparente Opacidad del reproductor debe estar entre 0 y 100 - + No me gusta no disponible temporalmente Dislikes no disponibles (estado %d) @@ -759,17 +757,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Límite de tasa de cliente encontrado %d veces %d milisegundos - + Habilitar barra de búsqueda ancha Barra de búsqueda ancha habilitada Barra de búsqueda ancha desactivada - + + Habilitar miniaturas de alta calidad + Las miniuñas Seekbar son de alta calidad + Las miniuñas Seekbar son de calidad media + Las miniaturas de la barra de búsqueda a pantalla completa son de alta calidad + Las miniaturas de la barra de búsqueda a pantalla completa son de calidad media + Esto también restaurará las miniuñas en vivos que no tienen las miniuñas seekbar.\n\nLas miniaturas de la barra de búsqueda usarán la misma calidad que el vídeo actual.\n\nEsta función funciona mejor con una calidad de vídeo de 720p o inferior y al usar una conexión a internet muy rápida. Restaurar antiguas miniaturas de la barra de búsqueda Las miniaturas de la barra de búsqueda aparecerán por encima de la barra de búsqueda Las miniaturas de Seekbar aparecerán en pantalla completa - + Activar SponsorBlock SponsorBlock es un sistema de fuentes múltiples para omitir partes molestas de vídeos de YouTube Apariencia @@ -950,7 +954,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Acerca de Los datos son proporcionados por la API de SponsorBlock. Pulsa aquí para aprender más y ver las descargas para otras plataformas - + Versión de la aplicación Spoof Versión falseada Versión no falseada @@ -963,9 +967,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Restaurar la velocidad de vídeo ancha & menú de calidad 18.09.39 - Restaurar pestaña de biblioteca 17.41.37 - Restaurar el estante viejo de lista de reproducción - 17.33.42 - Restaurar la disposición antigua de la interfaz de usuario - + Establecer página de inicio Predeterminado Navegar canales @@ -983,18 +986,26 @@ This is because Crowdin requires temporarily flattening this file and removing t Tendencias Ver más tarde - - Desactivar reanudación del reproductor + + Desactivar reanudación del reproductor de Shorts El reproductor de Shorts no se reanudará al iniciar la aplicación El reproductor de Shorts se reanudará al iniciar la aplicación - + + Reproducción automática de Shorts + Los Shorts se reproducirán automáticamente + Los Shorts se repetirán + Reproducción automática de Shorts en segundo plano + Los Shorts se reproducirán automáticamente en segundo plano + Los Shorts se repetirán en segundo plano + + Habilitar diseño de tablet Diseño de tablet habilitado Diseño de tablet deshabilitado Los mensajes de la comunidad no se muestran en los diseños de tablet - + Minireproductor Cambiar el estilo del reproductor minimizado de la aplicación Tipo de minreproductor @@ -1013,6 +1024,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Activar arrastrar y soltar Arrastrar y soltar está habilitado\n\nMinijugador puede ser arrastrado a cualquier esquina de la pantalla Arrastre y suelte está desactivado + Activar gesto de arrastre horizontal + Gesto de arrastre horizontal habilitado\n\nMinijugador puede ser arrastrado de la pantalla a la izquierda o derecha + Gesto de arrastre horizontal desactivado Ocultar botón de cerrar El botón de cierre está oculto Se muestra el botón de cerrar @@ -1032,12 +1046,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Valor de potencia entre 0-100, donde 0 es transparente Opacidad de reproductor debe estar en 0 -100 - + Activar la pantalla de carga del degradado La pantalla de carga tendrá un fondo de degradado La pantalla de carga tendrá un fondo sólido - + Activar el color personalizado de la barra de búsqueda Se muestra el color personalizado de la barra de búsqueda Se muestra el color original de la barra de búsqueda @@ -1045,12 +1059,12 @@ This is because Crowdin requires temporarily flattening this file and removing t El color de la barra de ajustes Valor de color de la barra de búsqueda inválido - + Evitar restricción regional de imágenes Usando host de imagen yt4.ggpht.com Utilizando el host de imágenes original\n\nHabilitar esto puede arreglar las imágenes faltantes que están bloqueadas en algunas regiones - + Pestaña @@ -1082,7 +1096,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow no disponible. (código de estado: %s) DeFlecha temporalmente no disponible - + Mostrar anuncios revalorizados Los anuncios se muestran al iniciar Los anuncios no se muestran al iniciar @@ -1090,47 +1104,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Error al conectar con el proveedor de anuncios Descartar - + Advertencia Tu historial no está siendo guardado.<br><br>Esto puede ser por un bloqueador de anuncios DNS o Proxy.<br><br>Para arreglarlo, permita el dominio <b>s.youtube.com</b> o desactive el bloqueador DNS o Proxy. No mostrar de nuevo - + Activar autorepetición Auto-repetición habilitada Auto-repetición desactivada - + Dimensiones del dispositivo Dimensiones del dispositivo falseadas\n\nCalidad de vídeo más alta puede ser desbloqueada, pero puede experimentar la reproducción de vídeo, peor duración de la batería y efectos secundarios desconocidos Dimensiones del dispositivo no falseadas\n\nHabilitar esto puede desbloquear mayores calidades de vídeo Activar esto puede causar retraso en la reproducción de vídeo, peor duración de la batería y efectos secundarios desconocidos. - + Ajustes de GmsCore Configuración de GmsCore - + Redirecciones URL Bypass Se omiten las redirecciones URL No se omiten las redirecciones URL - + Abrir enlaces en el navegador Abriendo enlaces externamente Abrir enlaces en la aplicación - + Quitar parámetro de consulta de rastreo Parámetro de la consulta de seguimiento se elimina de los enlaces Parámetro de la consulta de seguimiento no se elimina de los enlaces - + Desactivar hábitos de zoom Hápticas desactivadas Haptics están habilitados - + Calidad automática Recordar cambios de calidad de vídeo Los cambios de calidad se aplican a todos los vídeos @@ -1141,35 +1155,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Cambió la calidad predeterminada %1$s a: %2$s - + Mostrar botón de diálogo de velocidad Se muestra el botón El botón no se muestra - + + Menú de velocidad de reproducción personalizada + Menú de velocidad personalizado se muestra + Menú de velocidad personalizado no se muestra Velocidades de reproducción personalizadas - Añadir o cambiar las velocidades de reproducción disponibles + Añadir o cambiar las velocidades de reproducción personalizadas Velocidades personalizadas deben ser inferiores a %s. Utilizando valores predeterminados. Velocidades de reproducción personalizadas no válidas. Utilizando valores predeterminados. - + Recordar cambios de velocidad de reproducción Los cambios de velocidad de reproducción se aplican a todos los vídeos Los cambios de velocidad de reproducción sólo se aplican al vídeo actual Velocidad de reproducción por defecto Cambió la velocidad predeterminada a: %s - + Restaurar menú de calidad de vídeo antiguo Se muestra el antiguo menú de calidad de vídeo El antiguo menú de calidad de vídeo no se muestra - + Habilitar diapositiva para buscar Deslizar para buscar está activado Slide to seek no está habilitado - + Falsificación del stream de vídeo Falsifica el stream de vídeo del cliente para evitar problemas de reproducción Falsificación del stream de vídeo @@ -1187,20 +1204,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Efectos secundarios para la falsificación de Android RV • Falta el menú de pista de audio\n• El volumen estable no está disponible - - - Activar brillo HDR automático - Brillo automático HDR está habilitado - Brillo automático HDR está desactivado - - + Bloquear anuncios de audio Anuncios de audio bloqueados Anuncios de audio desbloqueados - + %s no está disponible. Los anuncios pueden mostrarse. Intenta cambiar a otro servicio de bloqueo de anuncios en la configuración. El servidor %s devolvió un error. Los anuncios pueden mostrar. Intente cambiar a otro servicio de bloqueo de anuncios en la configuración. Bloquear anuncios de vídeo incrustados @@ -1208,30 +1219,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Proxy luminoso Proxy MoradoAdBlock - + Bloquear anuncios de vídeo Los anuncios de vídeo están bloqueados Los anuncios de vídeo están desbloqueados - + mensaje eliminado Mostrar mensajes borrados No mostrar mensajes borrados Ocultar mensajes eliminados detrás de un spoiler Mostrar mensajes borrados como texto cruzado - + Reclamar automáticamente los puntos de canal Los puntos de canal se reclaman automáticamente Los puntos de canal no se reclaman automáticamente - + Activar modo de depuración de Twitch El modo de depuración de Twitch está habilitado (no recomendado) El modo de depuración de Twitch está desactivado - + Ajustes de ReVanced Anuncios Ajustes de bloqueo de anuncios diff --git a/src/main/resources/addresources/values-et-rEE/strings.xml b/patches/src/main/resources/addresources/values-et-rEE/strings.xml similarity index 70% rename from src/main/resources/addresources/values-et-rEE/strings.xml rename to patches/src/main/resources/addresources/values-et-rEE/strings.xml index 27f20ce37..ac44e27b8 100644 --- a/src/main/resources/addresources/values-et-rEE/strings.xml +++ b/patches/src/main/resources/addresources/values-et-rEE/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,107 +139,106 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + Vaikimisi - + - + - + - + - + - + - + + + - + - + Hoiatus - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/patches/src/main/resources/addresources/values-eu-rES/strings.xml b/patches/src/main/resources/addresources/values-eu-rES/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-eu-rES/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/addresources/values-fa-rIR/strings.xml b/patches/src/main/resources/addresources/values-fa-rIR/strings.xml similarity index 70% rename from src/main/resources/addresources/values-fa-rIR/strings.xml rename to patches/src/main/resources/addresources/values-fa-rIR/strings.xml index 1dee473f2..2af7a3447 100644 --- a/src/main/resources/addresources/values-fa-rIR/strings.xml +++ b/patches/src/main/resources/addresources/values-fa-rIR/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,106 +139,105 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + هشدار - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/src/main/resources/addresources/values-fi-rFI/strings.xml b/patches/src/main/resources/addresources/values-fi-rFI/strings.xml similarity index 91% rename from src/main/resources/addresources/values-fi-rFI/strings.xml rename to patches/src/main/resources/addresources/values-fi-rFI/strings.xml index f645af99a..10d68b786 100644 --- a/src/main/resources/addresources/values-fi-rFI/strings.xml +++ b/patches/src/main/resources/addresources/values-fi-rFI/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Tarkastuksia epäonnistui Avaa virallinen sivusto Ohita @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Paikattu %s päivää sitten APK käännöspäivä on vioittunut - + ReVanced Haluatko jatkaa? Nollaa @@ -57,13 +57,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Tuonti/vienti Tuo/vie ReVanced-asetukset - Käytät ReVanced Patches versiota <i>%s</i> + Käytät ReVanced Patchesin versiota <i>%s</i> Huomautus Tämä versio on ennakkojulkaisuversio, joten voit kokea odottamattomia ongelmia Viralliset linkit Lahjoita - + MicroG GmsCorea ei ole asennettu. Asenna se. Vaatii toimenpiteitä @@ -74,19 +74,25 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Tietoja Mainokset Vaihtoehtoiset pikkukuvat Syöte Soitin Yleinen asettelu + Shorts Liukusäädin Pyyhkäisyohjaus Sekalaiset Video - + + Poista Shorts taustatoisto käytöstä + Shorts taustatoisto ei ole käytössä + Shorts taustatoisto on käytössä + + Vianetsintä Ota tai poista vianetsintäasetukset käytöstä Vianetsintätietojen kirjaaminen @@ -103,13 +109,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Ponnahdusilmoitusta ei näytetä, jos tapahtuu virhe Ponnahdusilmoitusten pois käytöstä ottaminen piilottaa kaikki ReVanced-virheilmoitukset.\n\nSinulle ei ilmoiteta odottamattomista tapahtumista. - + Poista tykkää-/tilaa-painikkeiden hehku käytöstä Tykkää- ja tilaa-painikkeet eivät hehku, kun ne mainitaan Tykkää- ja tilaa-painikkeet hehkuvat, kun ne mainitaan - Piilota harmaa erotin - Harmaat erottimet on piilotettu - Harmaat erottimet näytetään + Piilota albumikortit + Albumikortit on piilotettu + Albumikortit näytetään + Piilota joukkorahoituslaatikko + Joukkorahoituslaatikko on piilotettu + Joukkorahoituslaatikko näytetään + Piilota kelluva mikrofonipainike + Mikrofonipainike piilotettu + Mikrofonipainike näytetään Piilota kanavan vesileima Vesileima on piilotettu Vesileima näytetään @@ -154,9 +166,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Piilota laajennettava osio videoiden alle Laajennettavat osiot on piilotettu Laajennettavat osiot näytetään - Piilota videolaatuvalikon alatunniste - Videolaatuvalikon alatunniste on piilotettu - Videolaatuvalikon alatunniste näytetään Piilota yhteisöpostaukset Yhteisöpostaukset on piilotettu Yhteisöpostaukset näytetään @@ -214,7 +223,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Piilota attribuutit osio \'Paikat\', Pelit ja Musiikki -osiot on piilotettu \'Paikat\', Pelit ja Musiikki -osiot näytetään - Piilota Osat-osio + Piilota Videon osat -osio Osat-osio on piilotettu Osat-osio näytetään Piilota \'Tutki podcastia\' -osio @@ -231,11 +240,42 @@ This is because Crowdin requires temporarily flattening this file and removing t Transkriptio-osio näytetään Videon kuvaus Piilota tai näytä videon kuvauskomponentteja + Suodatuspalkki + Piilota tai näytä suodatinpalkki syötteessä, haussa ja liittyvissä videoissa + Piilota syötteessä + Piilotettu syötteessä + Näytetään syötteessä + Piilota haussa + Piilotettu haussa + Näytetään haussa + Piilota liittyvissä videoissa + Piilotettu liittyvissä videoissa + Näytetään liittyvissä videoissa + Kommentit + Piilota tai näytä kommenttiosion komponentteja + Piilota \"Jäsenten kommentit\" -ylätunniste + \"Jäsenten kommentit\" -ylätunniste on piilotettu + \"Jäsenten kommentit\"-ylätunniste näytetään + Piilota kommenttiosio + Kommenttiosio on piilotettu + Kommenttiosio näytetään + Piilota \"Luo Shorts-video\" -painike + \"Luo Shorts-video\" -painike on piilotettu + \"Luo Shorts-video\" -painike näytetään + Piilota kommentin esikatselu + Kommentin esikatselu on piilotettu + Kommentin esikatselu näytetään + Piilota kiitos-painike + Kiitos-painike on piilotettu + Kiitos-painike näytetään + Piilota aikaleima- ja emoji-painikkeet + Aikaleima- ja emoji-painikkeet on piilotettu + Aikaleima- ja emoji-painikkeet näytetään - Piilota YouTube-ovet - Hakupalkki Doodles on piilotettu - Hakupalkki Doodles näytetään - YouTube Doodles esiintyy muutaman päivän joka vuosi.\n\nJos Doodle näyttää tällä hetkellä alueellasi ja tämä piilon asetus on päällä, sitten myös hakupalkin alla oleva suodatinpalkki piilotetaan. + Piilota YouTube Doodlet + Hakupalkin Doodlet on piilotettu + Hakupalkin Doodlet näytetään + YouTube Doodlet näkyvät muutamana päivänä vuodessa.\n\nJos Doodle näkyy tällä hetkellä alueellasi ja tämä piilotusasetus on käytössä, myös hakupalkin alla oleva suodatinpalkki piilotetaan. Mukautettu suodatin Piilota komponentteja mukautetuilla suodattimilla Käytä mukautettua suodatinta @@ -261,7 +301,7 @@ This is because Crowdin requires temporarily flattening this file and removing t This is because keywords can be in any language, and showing an example in the localized script helps convey this. --> Piilotettavia avainsanoja ja lauseita, erotettuna uusilla riveillä\n\nAvainsanat voivat olla kanavien nimiä tai videon otsikoissa\n\nSuurilla kirjaimilla varustetut sanat täytyy syöttää kotelon kanssa (esim. iPhone, TikTok, LeBlanc) Tietoja avainsanoilla suodatuksesta - Etusivu/Tilaus / Hakutulokset suodatetaan piilottamaan sisältö, joka vastaa avainsanalauseita\n\nRajoitukset\n• Shortteja ei voi piilottaa kanavan nimellä\n• Joitakin UI-komponentteja ei välttämättä ole piilotettu\n• Haetaan hakusanalla ei ole tuloksia. + Etusivu/Tilaus / Hakutulokset suodatetaan piilottamaan sisältö, joka vastaa avainsanalauseita\n\nRajoitukset\n• Shorts ei voi piilottaa kanavan nimellä\n• Joitakin UI-komponentteja ei välttämättä ole piilotettu\n• Haetaan hakusanalla ei ole tuloksia Täsmää koko sanaan Hakusanan/lauseen ympäröiminen kaksoislainauksilla estää videon oitsikoiden ja kanavien nimien osittaisia vastaavuuksia<br><br>Esimerkiksi,<br><b>\"ai\"</b> piilottaa videon: <b>How does AI work?</b><br>mutta ei piilota: <b>What does fair use mean?</b> @@ -272,7 +312,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Avainsana on liian lyhyt ja vaatii lainausmerkkejä: %s Avainsana piilottaa kaikki videot: %s - + Piilota yleiset mainokset Yleiset mainokset on piilotettu Yleiset mainokset näytetään @@ -291,6 +331,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Piilota tuotebanneri Banneri on piilotettu Banneri näytetään + Piilota pelaajan ostoshylly + Ostoshylly on piilotettu + Ostoshylly näytetään Piilota ostoslinkit videon kuvauksessa Ostoslinkit on piilotettu Ostoslinkit näytetään @@ -307,17 +350,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Koko ruudun mainosten piilotus toimii vain vanhemmilla laitteilla - + Piilota YouTube Premium -tarjoukset YouTube Premium -tarjoukset videosoittimen alla on piilotettu YouTube Premium -tarjoukset videosoittimen alla näytetään - + Piilota videomainokset Videomainokset on piilotettu Videomainokset näytetään - + URL-osoite kopioitiin leikepöydälle Aikaleimattu URL-osoite kopioitiin Näytä videon URL-osoitteen kopiointipainike @@ -327,13 +370,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Painike näytetään. Napauta kopioidaksesi videon aikaleimatun URL-osoitteen. Napauta ja pidä pohjassa kopioidaksesi URL-osoitteen ilman aikaleimaa Painiketta ei näytetä - + Poista katsojan harkinta -valintaikkuna Valintaikkuna poistetaan Valintaikkuna näytetään Tämä ei ohita ikärajoitusta. Se vain hyväksyy sen automaattisesti. - + Ulkoiset lataukset Asetukset ulkoisen lataajan käyttämiselle Näytä ulkoinen lataus -painike @@ -347,17 +390,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Ulkoisen lataussovelluksesi, kuten NewPipen tai Sealin, paketin nimi %s ei ole asennettu. Asenna se. - + Poista tarkka hakuele käytöstä Ele ei ole käytössä Ele on käytössä - + Ota liukusäätimen napautus käyttöön Liukusäätimen napautus on käytössä Liukusäätimen napautus ei ole käytössä - + Ota kirkkauden ele käyttöön Kirkkauden pyyhkäisy on käytössä Kirkkauden pyyhkäisy ei ole käytössä @@ -386,12 +429,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Pyyhkäisyn kynnysraja Pyyhkäisyä varten tarvittavan kynnyksen määrä - + Poista automaattiset tekstitykset käytöstä Automaattiset tekstitykset eivät ole käytössä Automaattiset tekstitykset ovat käytössä - + Toimintopainikkeet Piilota tai näytä painikkeet videoiden alla Piilota Tykkää ja En tykkää @@ -427,23 +470,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Tallenna soittolistalle -painike on piilotettu Tallenna soittolistalle -painike näytetään - - Piilota automaattisen toiston -painike - Automaattinen toisto -painike on piilotettu - Automaattinen toisto -painike näytetään - - - - Piilota tekstitykset-painike - Tekstitykset-painike on piilotettu - Tekstitykset-painike näytetään - - - Piilota cast-painike - Cast-painike on piilotettu - Cast-painike näytetään - - + Navigointipainikkeet Piilota tai muuta navigointipalkin painikkeita @@ -470,7 +497,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Tunnisteet on piilotettu Tunnisteet näytetään - + Flyout-valikko Piilota tai näytä soittimen flyout-valikon kohteita @@ -481,6 +508,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Piilota Lisäasetukset Lisäasetukset-valikko on piilotettu Lisäasetukset-valikko näytetään + + Piilota lepoajastin + Unen ajastin valikko on piilotettu + Nukkumisajastin valikko näytetään Piilota Toista videota jatkuvasti Toista videota jatkuvasti -valinta on piilotettu @@ -489,6 +520,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Piilota Elokuvatila Elokuvatila-valinta on piilotettu Elokuvatila-valinta näytetään + Piilota vakaa äänenvoimakkuus + Vakaan äänenvoimakkuuden valikko näytetään + Vakaan taltion valikko on piilotettu Piilota Ohjeet ja palaute Ohjeet ja palaute -valinta on piilotettu @@ -514,83 +548,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Piilota Katso VR-tilassa Katso VR-tilassa -valinta on piilotettu Katso VR-tilassa -valinta näytetään + Piilota videolaatuvalikon alatunniste + Videolaatuvalikon alatunniste on piilotettu + Videolaatuvalikon alatunniste näytetään - - Piilota edellinen- ja seuraava video -painikkeet - Painikkeet on piilotettu - Painikkeet näytetään + + Piilota edellinen- ja seuraava video -painikkeet + Painikkeet on piilotettu + Painikkeet näytetään + Piilota cast-painike + Cast-painike on piilotettu + Cast-painike näytetään + + Piilota tekstitykset-painike + Tekstitykset-painike on piilotettu + Tekstitykset-painike näytetään + Piilota automaattisen toiston -painike + Automaattinen toisto -painike on piilotettu + Automaattinen toisto -painike näytetään - - Piilota albumikortit - Albumikortit on piilotettu - Albumikortit näytetään - - - Kommentit - Piilota tai näytä kommenttiosion komponentteja - Piilota \"Jäsenten kommentit\" -ylätunniste - \"Jäsenten kommentit\" -ylätunniste on piilotettu - \"Jäsenten kommentit\"-ylätunniste näytetään - Piilota kommenttiosio - Kommenttiosio on piilotettu - Kommenttiosio näytetään - Piilota \"Luo Shorts-video\" -painike - \"Luo Shorts-video\" -painike on piilotettu - \"Luo Shorts-video\" -painike näytetään - Piilota kommentin esikatselu - Kommentin esikatselu on piilotettu - Kommentin esikatselu näytetään - Piilota kiitos-painike - Kiitos-painike on piilotettu - Kiitos-painike näytetään - Piilota aikaleima- ja emoji-painikkeet - Aikaleima- ja emoji-painikkeet on piilotettu - Aikaleima- ja emoji-painikkeet näytetään - - - Piilota joukkorahoituslaatikko - Joukkorahoituslaatikko on piilotettu - Joukkorahoituslaatikko näytetään - - + Piilota loppunäytön kortit Loppunäytön kortit on piilotettu Loppunäytön kortit näytetään - - Suodatuspalkki - Piilota tai näytä suodatinpalkki syötteessä, haussa ja liittyvissä videoissa - Piilota syötteessä - Piilotettu syötteessä - Näytetään syötteessä - Piilota haussa - Piilotettu haussa - Näytetään haussa - Piilota liittyvissä videoissa - Piilotettu liittyvissä videoissa - Näytetään liittyvissä videoissa - - - Piilota kelluva mikrofonipainike - Mikrofonipainike piilotettu - Mikrofonipainike näytetään - - + Poista elokuvatila käytöstä kokoruututilassa Elokuvatila ei ole käytössä Elokuvatila on käytössä - + Piilota tietokortit Tietokortit on piilotettu Tietokortit näytetään - + Poista vierivät numeroanimaatiot käytöstä Vieriviä numeroita ei animoida Vierivät numerot animoidaan - + Piilota liukusäädin videosoittimessa Videosoittimen liukusäädin on piilotettu Videosoittimen liukusäädin näytetään @@ -598,18 +595,20 @@ This is because Crowdin requires temporarily flattening this file and removing t Pikkukuvan liukusäädin on piilotettu Pikkukuvan liukusäädin näytetään - + + Shorts-soitin + Piilota tai näytä Shorts-soittimen osia - Piilota Shortsit koti-syötteessä - Shortsit on piilotettu koti-syötteessä - Shortsit näytetään koti-syötteessä + Piilota Shorts koti-syötteessä + Shorts on piilotettu koti-syötteessä + Shorts näytetään koti-syötteessä - Piilota Shortsit tilaukset-syötteessä - Shortsit on piilotettu tilaukset-syötteessä - Shortsit näytetään tilaukset-syötteessä + Piilota Shorts tilaukset-syötteessä + Shorts on piilotettu tilaukset-syötteessä + Shorts näytetään tilaukset-syötteessä Piilota Shortsit hakutuloksissa - Shortsit on piilotettu hakutuloksissa - Shortsit näytetään hakutuloksissa + Shorts on piilotettu hakutuloksissa + Shorts näytetään hakutuloksissa Piilota liity-painike Liity-painike on piilotettu @@ -633,7 +632,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Piilota sijaintitieto Sijaintitieto on piilotettu Sijaintitieto näytetään - Piilota musiikin tallennus painike + Piilota musiikin tallennuspainike Tallenna musiikki-painike on piilotettu Tallenna musiikkipainike näytetään Piilota käytä mallinappia @@ -696,27 +695,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Navigointipalkki on piilotettu Navigointipalkki näytetään - + Poista videoehdotukset-loppunäyttö käytöstä Ehdotetut videot on piilotettu Ehdotetut videot näytetään - + Piilota videon aikaleima Aikaleima on piilotettu Aikaleima näytetään - + Piilota soittimen ponnahdusikkunat Soittimen ponnahdusikkunat on piilotettu Soittimen ponnahdusikkunat näytetään - + Soittimen peittoalueen läpinäkyvyys Läpinäkyvyysarvo välillä 0–100, jossa 0 on läpinäkyvä Soittimen peittoalueen läpinäkyvyyden on oltava välillä 0-100 - + Ei-tykkäykset eivät ole tilapäisesti käytettävissä Ei-tykkäykset eivät ole saatavilla (tila %d) @@ -760,17 +759,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Asiakkaan hintaraja kohdistettu %d kertaa %d millisekuntia - + Ota laaja hakupalkki käyttöön Laaja hakupalkki on käytössä Laaja hakupalkki ei ole käytössä - + + Ota käyttöön korkealaatuiset pikkukuvat + Seekbar pikkukuvat ovat laadukkaita + Seekbar pikkukuvat ovat keskitasoisia + Koko näytön seekbar pikkukuvat ovat laadukkaita + Koko näytön seekbar pienoiskuvat ovat keskitasoisia + Tämä myös palauttaa pikkukuvat livestreams että ei ole seekbar pikkukuvat.\n\nSeekbar pikkukuvat käyttävät samaa laatua kuin nykyinen video.\n\nTämä ominaisuus toimii parhaiten, kun videon laatu on 720p tai alempi, ja kun käytetään erittäin nopeaa internet-yhteyttä. Palauta vanhat liukusäätimen pikkukuvat Liukusäätimen pikkukuvat näkyvät liukusäätimen yläpuolella Liukusäätimen pikkukuvat näkyvät kokoruututilassa - + Ota SponsorBlock käyttöön SponsorBlock on käyttäjälähteinen järjestelmä YouTube-videoiden ärsyttävien osien ohittamiseen Ulkoasu @@ -951,7 +956,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Tietoja Tiedot tarjoaa SponsorBlock API. Napauta tätä saadaksesi lisätietoja ja nähdäksesi lataukset muille alustoille - + Naamioi sovellusversio Versio on naamioitu Versiota ei ole naamioitu @@ -964,9 +969,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Palauta laaja videonopeus- ja laatuvalikko 18.09.39 - Palauta kirjasto-välilehti 17.41.37 - Palauta vanha soittolistahylly - 17.33.42 - Palauta vanha käyttöliittymäasettelu - + Aseta aloitussivu Oletus Selaa kanavia @@ -984,18 +988,26 @@ This is because Crowdin requires temporarily flattening this file and removing t Nousussa Katso myöhemmin - + Poista Shorts-soittimen jatkaminen käytöstä Shorts-soitin ei jatku sovelluksen käynnistyessä Shorts-soitin jatkuu sovelluksen käynnistyessä - + + Shortsien automaattinen toisto + Shortsit toistetaan automaattisesti + Shortsit toistuvat uudelleen + Toista Shortsit automaattisesti taustalla + Shortsit toistetaan automaattisesti myös taustalla + Shorsit toistetaan uudelleen myös taustatoiston aikana + + Ota tablettiasettelu käyttöön Tablettiasettelu on käytössä Tablettiasettelu ei ole käytössä Yhteisöpostaukset eivät näy tablettiasettelussa - + Minisoitin Muuta sovelluksen tyyliä pienennetyssä soittimessa Minisoittimen tyyppi @@ -1014,6 +1026,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Ota käyttöön vedä ja pudota Vedä ja pudota on käytössä\n\nMiniplayer voidaan vetää mihin tahansa ruudun kulmaan Vedä ja pudotus pois käytöstä + Ota käyttöön vaakasuora vedä ele + Vaakasuora vedä ele käytössä\n\nMiniplayer voidaan vetää pois näytön vasemmalle tai oikealle + Vaakasuora vedä ele pois käytöstä Piilota sulje painike Sulje painike on piilotettu Sulje painike näytetään @@ -1023,7 +1038,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Piilota alatekstit Alatekstit on piilotettu Alatekstit näytetään - Piilota ohita etu- ja takapainikkeet + Piilota eteenpäin- ja taaksepäin-painikkeet Ohita eteenpäin ja takaisin on piilotettu Ohita eteenpäin ja takaisin näytetään Alkukoko @@ -1033,12 +1048,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Läpinäkyvyysarvo välillä 0–100, jossa 0 on läpinäkyvä Miniplayer peittokuvan läpinäkyvyyden on oltava välillä 0-100 - + Ota latausruudun liukuväri käyttöön Latausruudulla on liukuvärillinen tausta Latausruudulla on tasainen tausta - + Ota mukautettu liukusäätimen väri käyttöön Mukautettu liukusäätimen väri näytetään Alkuperäinen liukusäätimen väri näytetään @@ -1046,11 +1061,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Liukusäätimen väri Virheellinen seekbarin värin arvo - + Ohita kuvan alueen rajoitukset Käyttämällä kuvan isäntä yt4.ggpht.com + Käytetään alkuperistä kuvapalvelua\n\nTämän käyttöönotto voi korjata joillakin alueilla estetyt puuttuvat kuvat - + Koti-välilehti @@ -1072,7 +1088,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Videon kuvakaappaukset Kuvakaappaukset otetaan kunkin videon alusta/keskeltä/lopusta. Nämä kuvat on sisäänrakennettu YouTubeen, eikä ulkoista API:a käytetä Käytä nopeita kuvakaappauksia - Käytetään keskilaatuisia kuvakaappauksia. Pikkukuvat latautuvat nopeammin, mutta suorilla lähetyksillä, julkaisemattomilla tai hyvin vanhoilla videoilla saattaa näkyä tyhjiä pikkukuvia + Käytetään keskilaatuisia kuvakaappauksia. Pikkukuvat latautuvat nopeammin, mutta suoratoistoilla, julkaisemattomilla tai hyvin vanhoilla videoilla saattaa näkyä tyhjiä pikkukuvia Käytetään korkealaatuisia kuvakaappauksia Videon aika, josta kuvakaappaukset otetaan Videon alku @@ -1082,7 +1098,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow ei ole tilapäisesti käytettävissä (tila: %s) DeArrow ei ole tilapäisesti käytettävissä - + Näytä ReVanced-ilmoitukset Ilmoitukset näytetään käynnistettäessä Ilmoituksia ei näytetä käynnistettäessä @@ -1090,47 +1106,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Yhteyden muodostaminen ilmoitusten tarjoajaan epäonnistui Hylkää - + Varoitus Kellon historiaa ei tallenneta.<br><br>Tämä todennäköisesti johtuu DNS mainosten estäjä tai verkkovälityspalvelin.<br><br>Korjataksesi tämän, valkoiselle listalle <b>s.youtube.com</b> tai poistaaksesi kaikki DNS-estäjät ja -profiilit. Älä näytä uudelleen - + Ota automaattinen toisto käyttöön Automaattinen toisto on käytössä Automaattinen toisto ei ole käytössä - + Naamioi laitteen mitat Laitteen mitat on naamioitu\n\nKorkeammat videolaadut saattavat avautua, mutta videon toisto saattaa pätkiä, akun kesto huonontua ja tuntemattomia sivuvaikutuksia saattaa esiintyä Laitteen mittoja ei ole naamioitu\n\nTämän käyttöönotto voi avata korkeampia videon laatuja Tämän käyttöönotto saattaa aiheuttaa videon toiston pätkimistä, akun keston huonontumista ja tuntemattomia sivuvaikutuksia. - + GmsCore-asetukset GmsCoren asetukset - + Ohita URL-osoitteen uudelleenohjaus URL-osoitteen uudelleenohjaukset ohitetaan URL-osoitteen uudelleenohjauksia ei ohiteta - + Avaa linkit selaimessa Linkit avataan ulkoisesti Linkit avataan sovelluksessa - + Poista seurantakyselyparametrit Seurantakyselyparametrit poistetaan linkeistä Seurantakyselyparametrejä ei poisteta linkeistä - + Poista zoomauksen tärinä käytöstä Tärinä ei ole käytössä Tärinä on käytössä - + Automaattinen laatu Muista videolaadun muutokset Laatumuutokset koskevat kaikkia videoita @@ -1141,42 +1157,45 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi %1$s-oletuslaatu muutettiin: %2$s - + Näytä nopeusikkuna painike Painike näytetään Painiketta ei näytetä - + + Mukautettu soiton nopeusvalikko + Mukautettu nopeusvalikko näkyy + Mukautettua nopeusvalikkoa ei näytetä Mukautetut toistonopeudet - Lisää tai muuta käytettävissä olevia toistonopeuksia + Lisää tai muuta mukautettuja soiton nopeuksia Mukautettujen nopeuksien on oltava alle %s. Käytetään oletusarvoja. Virheelliset mukautetut toistonopeudet. Käytetään oletusarvoja. - + Muista toistonopeuden muutokset Toistonopeuden muutokset koskevat kaikkia videoita Toistonopeuden muutokset koskevat vain nykyistä videota Toiston oletusnopeus Toiston oletusnopeus muutettiin: %s - + Palauta vanha videolaatuvalikko Vanha videolaatuvalikko näytetään Vanhaa videolaatuvalikkoa ei näytetä - + Ota kelaus liu\'uttamalla käyttöön Kelaus liu\'uttamalla on käytössä Kelaus liu\'uttamalla ei ole käytössä - - Naamioi videovirrat - Naamioi asiakkaan videovirrat toisto-ongelmien estämiseksi - Naamioi videovirrat - Videovirrat naamioidaan - Videovirtoja ei naamioida\n\nViden toisto ei ehkä toimi + + Naamioi videostriimit + Naamioi asiakasohjelman videostriimit toisto-ongelmien estämiseksi + Naamioi videostriimit + Videostriimit naamioidaan + Videostriimejä ei naamioida\n\nViden toisto ei ehkä toimi Tämän asetuksen poistaminen käytöstä voi aiheuttaa ongelmia videotoistossa. - Oletusasiakas + Oletusasiakasohjelma Pakota AVC (H.264) Videokoodekki on AVC (H.264) Videokoodekki on VP9 tai AV1 @@ -1187,20 +1206,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Android VR -naamioinnin haittavaikutukset • Ääniraitavalikko puuttuu\n• Tasainen äänenvoimakkuus ei ole käytettävissä - - - Ota käyttöön automaattinen HDR-kirkkaus - Automaattinen HDR-kirkkaus on käytössä - Automaattinen HDR-kirkkaus pois päältä - - + Estä äänimainokset Äänimainokset estetään Äänimainoksia ei estetä - + %s ei ole käytettävissä. Mainokset voivat näkyä. Kokeile vaihtaa toiseen mainosestopalveluun asetuksissa. Palvelin %s antoi virheen. Mainokset voivat näkyä. Kokeile vaihtaa toiseen mainosestopalveluun asetuksissa. Estä upotetut videomainokset @@ -1208,30 +1221,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Luminous-välityspalvelin PurpleAdBlock-välityspalvelin - + Estä videomainokset Videomainokset estetään Videomainoksia ei estetä - + viesti poistettu Näytä poistetut viestit Älä näytä poistettuja viestejä Piilota poistetut viestit spoilerin takana Näytä poistetut viestit ristitettyinä teksteinä - + Lunasta kanavapisteet automaattisesti Kanavapisteet lunastetaan automaattisesti Kanavapisteitä ei lunasteta automaattisesti - + Ota Twitch-vianetsintätila käyttöön Twitch-vianetsintätila on käytössä (ei suositeltu) Twitch-vianetsintätila ei ole käytössä - + ReVanced-asetukset Mainokset Mainosestoasetukset diff --git a/src/main/resources/addresources/values-fil-rPH/strings.xml b/patches/src/main/resources/addresources/values-fil-rPH/strings.xml similarity index 94% rename from src/main/resources/addresources/values-fil-rPH/strings.xml rename to patches/src/main/resources/addresources/values-fil-rPH/strings.xml index 74f2ec399..ca69060fa 100644 --- a/src/main/resources/addresources/values-fil-rPH/strings.xml +++ b/patches/src/main/resources/addresources/values-fil-rPH/strings.xml @@ -32,9 +32,9 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + Gusto mo bang magpatuloy? I-reset I-refresh at i-restart @@ -52,7 +52,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Ang bersyon na ito ay isang pre-release at maaari kang makaranas ng mga hindi inaasahang isyu Mga opisyal na link - + Hindi naka-install ang MicroG GmsCore. I-install ito. Kailangan ng aksyon @@ -63,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Tungkol Mga ad Mga alternatibong thumbnail @@ -72,7 +72,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Pangkalahatang layout Mga kontrol sa pag-swipe - + + + Pag-debug Paganahin o huwag paganahin ang mga opsyon sa pag-debug Pag-log sa pag-debug @@ -89,13 +91,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Hindi ipinapakita ang toast kung may naganap na error Itinatago ng pag-off sa mga error toast ang lahat ng ReVanced na notification ng error.\n\nHindi ka aabisuhan ng anumang hindi inaasahang kaganapan. - + I-disable ang glow ng like / subscribe button Hindi magliliwanag ang like and subscribe button kapag nabanggit Ang like and subscribe button ay magliliwanag kapag nabanggit - Itago ang gray na separator - Nakatago ang mga gray na separator - Ipinapakita ang mga gray na separator + Itago ang mga album card + Nakatago ang mga card ng album + Ipinapakita ang mga album card + Itago ang crowdfunding box + Nakatago ang crowdfunding box + Ipinapakita ang kahon ng crowdfunding + Itago ang lumulutang na pindutan ng mikropono + Nakatago ang button ng mikropono + Ipinapakita ang pindutan ng mikropono Itago ang watermark ng channel Nakatago ang watermark Ipinapakita ang watermark @@ -140,9 +148,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Itago ang napapalawak na chip sa ilalim ng mga video Nakatago ang mga napapalawak na chip Ipinapakita ang mga napapalawak na chip - Itago ang footer ng menu ng kalidad ng video - Nakatago ang footer ng menu ng kalidad ng video - Ang footer ng menu ng kalidad ng video ay ipinapakita Itago ang mga post sa komunidad Nakatago ang mga post sa komunidad Ipinapakita ang mga post sa komunidad @@ -214,6 +219,33 @@ This is because Crowdin requires temporarily flattening this file and removing t Ipinapakita ang seksyon ng transcript Paglalarawan ng video Itago o ipakita ang mga bahagi ng paglalarawan ng video + Itago o ipakita ang filter bar sa feed, paghahanap, at mga kaugnay na video + Itago sa feed + Nakatago sa feed + Ipinapakita sa feed + Itago sa paghahanap + Nakatago sa paghahanap + Ipinapakita sa paghahanap + Itago sa mga kaugnay na video + Nakatago sa mga kaugnay na video + Ipinapakita sa mga kaugnay na video + Mga komento + Itago o ipakita ang mga bahagi ng seksyon ng komento + Itago ang header ng \"Mga komento ng mga miyembro\" + Nakatago ang header ng \"Mga komento ng mga miyembro\" + Ipinapakita ang header ng \"Mga komento ng mga miyembro\" + Itago ang seksyon ng mga komento + Nakatago ang seksyon ng mga komento + Ipinapakita ang seksyon ng mga komento + Itago ang preview na komento + Nakatago ang preview ng komento + Ang pag-preview ng komento ay ipinapakita + Itago ang pindutan ng pasasalamat + Nakatago ang buton ng salamat + Ang pindutan ng salamat ay ipinapakita + Itago ang mga pindutan ng timestamp at emoji + Nakatago ang mga pindutan ng timestamp at emoji + Ipinapakita ang mga pindutan ng timestamp at emoji Custom na filter Itago ang mga bahagi gamit ang mga custom na filter @@ -242,7 +274,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Itago ang mga pangkalahatang ad Nakatago ang mga pangkalahatang ad Ipinapakita ang mga pangkalahatang ad @@ -277,17 +309,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Gumagana lang ang Itago ang mga fullscreen na ad sa mga mas lumang device - + Itago ang mga promosyon sa YouTube Premium Nakatago ang mga promosyon ng YouTube Premium sa ilalim ng video player Ipinapakita ang mga promosyon sa YouTube Premium sa ilalim ng video player - + Itago ang mga video ad Nakatago ang mga video ad Ipinapakita ang mga video ad - + Nakopya ang URL sa clipboard URL na may timestamp na kinopya Ipakita ang pindutan ng URL ng kopya ng video @@ -297,13 +329,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Ang pindutan ay ipinapakita. I-tap para kopyahin ang URL ng video gamit ang timestamp. I-tap nang matagal upang kopyahin ang video nang walang timestamp Hindi ipinapakita ang button - + Alisin ang dialog ng paghuhusga ng manonood Aalisin ang dialog Ipapakita ang dialog Hindi nito nilalampasan ang paghihigpit sa edad. Awtomatiko lang itong tinatanggap. - + Mga panlabas na pag-download Mga setting para sa paggamit ng external na downloader Ipakita ang external na button sa pag-download @@ -317,17 +349,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Pangalan ng package ng iyong naka-install na external na downloader app, gaya ng NewPipe o Seal Hindi naka-install ang %s. Mangyaring i-install ito. - + Huwag paganahin ang tumpak na kilos sa paghahanap Naka-disable ang galaw Naka-enable ang galaw - + Paganahin ang pag-tap sa seekbar Ang pag-tap sa Seekbar ay pinagana Naka-disable ang pag-tap sa Seekbar - + I-enable ang brightness gesture Naka-enable ang brightness swipe Naka-disable ang brightness swipe @@ -355,12 +387,12 @@ This is because Crowdin requires temporarily flattening this file and removing t I-swipe ang magnitude threshold Ang halaga ng threshold para sa pag-swipe na magaganap - + Huwag paganahin ang mga auto caption Naka-disable ang mga auto caption Ang mga auto caption ay pinagana - + Mga pindutan ng pagkilos Itago o ipakita ang mga button sa ilalim ng mga video Itago ang Like at Dislike @@ -396,23 +428,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Nakatago ang button na I-save sa playlist I-save sa playlist button ay ipinapakita - - Itago ang autoplay na button - Nakatago ang autoplay button - Ang autoplay na button ay ipinapakita - - - - Button na itago ang mga caption - Nakatago ang button ng mga caption - Ang pindutan ng mga caption ay ipinapakita - - - Itago ang cast button - Nakatago ang pindutan sa cast - Nakikita ang cast button - - + Mga pindutan ng nabigasyon Itago o baguhin ang mga button sa navigation bar @@ -420,7 +436,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Nakatago ang home button Ipinapakita ang home button - Itago ang Shorts Nakatago ang pindutan sa Shorts Nakikita ang Shorts button @@ -439,7 +454,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Nakatago ang mga label Ang mga label ay ipinapakita - + Menu ng flyout Itago o ipakita ang mga item sa menu ng player flyout @@ -450,6 +465,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Itago ang mga Karagdagang setting Nakatago ang menu ng mga karagdagang setting Ang karagdagang menu ng mga setting ay ipinapakita + Itago ang Loop na video Nakatago ang menu ng loop na video @@ -483,82 +499,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Itago ang Panoorin sa VR Nakatago ang panonood sa VR menu Ang panonood sa VR menu ay ipinapakita + Itago ang footer ng menu ng kalidad ng video + Nakatago ang footer ng menu ng kalidad ng video + Ang footer ng menu ng kalidad ng video ay ipinapakita - - Itago ang nakaraang & susunod na mga pindutan ng video - Nakatago ang mga pindutan - Ang mga pindutan ay ipinapakita + + Itago ang nakaraang & susunod na mga pindutan ng video + Nakatago ang mga pindutan + Ang mga pindutan ay ipinapakita + Itago ang cast button + Nakatago ang pindutan sa cast + Nakikita ang cast button + + Button na itago ang mga caption + Nakatago ang button ng mga caption + Ang pindutan ng mga caption ay ipinapakita + Itago ang autoplay na button + Nakatago ang autoplay button + Ang autoplay na button ay ipinapakita - - Itago ang mga album card - Nakatago ang mga card ng album - Ipinapakita ang mga album card - - - Mga komento - Itago o ipakita ang mga bahagi ng seksyon ng komento - Itago ang header ng \"Mga komento ng mga miyembro\" - Nakatago ang header ng \"Mga komento ng mga miyembro\" - Ipinapakita ang header ng \"Mga komento ng mga miyembro\" - Itago ang seksyon ng mga komento - Nakatago ang seksyon ng mga komento - Ipinapakita ang seksyon ng mga komento - Itago ang pindutang \"Gumawa ng Maikling\" - Nakatago ang pindutang \"Gumawa ng Maikling\" - Ipinapakita ang pindutang \"Gumawa ng Maikling\" - Itago ang preview na komento - Nakatago ang preview ng komento - Ang pag-preview ng komento ay ipinapakita - Itago ang pindutan ng pasasalamat - Nakatago ang buton ng salamat - Ang pindutan ng salamat ay ipinapakita - Itago ang mga pindutan ng timestamp at emoji - Nakatago ang mga pindutan ng timestamp at emoji - Ipinapakita ang mga pindutan ng timestamp at emoji - - - Itago ang crowdfunding box - Nakatago ang crowdfunding box - Ipinapakita ang kahon ng crowdfunding - - + Itago ang mga end screen card Nakatago ang mga end screen card Ipinapakita ang mga end screen card - - Itago o ipakita ang filter bar sa feed, paghahanap, at mga kaugnay na video - Itago sa feed - Nakatago sa feed - Ipinapakita sa feed - Itago sa paghahanap - Nakatago sa paghahanap - Ipinapakita sa paghahanap - Itago sa mga kaugnay na video - Nakatago sa mga kaugnay na video - Ipinapakita sa mga kaugnay na video - - - Itago ang lumulutang na pindutan ng mikropono - Nakatago ang button ng mikropono - Ipinapakita ang pindutan ng mikropono - - + I-disable ang ambient mode sa fullscreen Naka-disable ang ambient mode Pinagana ang ambient mode - + Itago ang mga card ng impormasyon Nakatago ang info cards Nakalabas ang info cards - + Huwag paganahin ang rolling number animation Ang mga rolling number ay hindi animated Ang mga rolling number ay animated - + Itago ang seekbar sa video player Nakatago ang seekbar ng video player Ipinapakita ang seekbar ng video player @@ -566,18 +546,18 @@ This is because Crowdin requires temporarily flattening this file and removing t Nakatago ang thumbnail seekbar Ipinapakita ang thumbnail seekbar - + Itago ang Shorts sa home feed - Nakatago ang shorts sa home feed - Ipinapakita ang shorts sa home feed + Nakatago ang Shorts sa home feed + Ipinapakita ang Shorts sa home feed Itago ang Shorts sa feed ng subscription - Nakatago ang mga shorts sa feed ng subscription - Ipinapakita ang mga shorts sa feed ng subscription + Nakatago ang mga Shorts sa feed ng subscription + Ipinapakita ang mga Shorts sa feed ng subscription Itago ang Shorts sa mga resulta ng paghahanap - Nakatago ang mga shorts sa mga resulta ng paghahanap - Ang mga shorts sa mga resulta ng paghahanap ay ipinapakita + Nakatago ang mga Shorts sa mga resulta ng paghahanap + Ang mga Shorts sa mga resulta ng paghahanap ay ipinapakita Itago ang button na sumali Nakatago ang button na sumali @@ -643,27 +623,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Nakatago ang navigation bar Ipinapakita ang navigation bar - + I-disable ang iminungkahing video end screen Idi-disable ang mga iminungkahing video Ipapakita ang mga iminungkahing video - + Itago ang timestamp ng video Nakatago ang timestamp Ipinapakita ang timestamp - + Itago ang mga popup panel ng player Nakatago ang mga popup panel ng player Ipinapakita ang mga popup panel ng player - + Opacity ng overlay ng player Ang halaga ng opacity sa pagitan ng 0-100, kung saan ang 0 ay transparent Ang opacity ng overlay ng player ay dapat nasa pagitan ng 0-100 - + Pansamantalang hindi available ang mga hindi gusto (nag-time out ang API) Hindi available ang mga hindi gusto (status %d) @@ -707,17 +687,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Nakatagpo ng %d beses ang limitasyon sa rate ng kliyente %d millisecond - + Paganahin ang malawak na search bar Ang malawak na search bar ay pinagana Naka-disable ang malawak na search bar - + Ibalik ang mga lumang thumbnail ng seekbar Lalabas ang mga thumbnail ng Seekbar sa itaas ng seekbar Lalabas ang mga thumbnail ng Seekbar sa fullscreen - + I-enable ang SponsorBlock Ang SponsorBlock ay isang crowd-sourced system para sa paglaktaw ng mga nakakainis na bahagi ng mga video sa YouTube Hitsura @@ -888,7 +868,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Tungkol Ang data ay ibinibigay ng SponsorBlock API. Mag-tap dito para matuto pa at makakita ng mga download para sa iba pang platform - + Spoof na bersyon ng app Na-spoof ang bersyon Hindi na-spoof ang bersyon @@ -901,9 +881,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Ibalik ang malawak na bilis ng video & kalidad na menu 18.09.39 - Ibalik ang tab ng library 17.41.37 - Ibalik ang lumang istante ng playlist - 17.33.42 - Ibalik ang lumang layout ng UI - + Itakda ang panimulang pahina Regular Galugarin @@ -912,18 +891,20 @@ This is because Crowdin requires temporarily flattening this file and removing t Maghanap Mga subscription - + Huwag paganahin ang pagpapatuloy na manlalaro ng Shorts - Hindi magpapatuloy ang shorts player sa pagsisimula ng app + Hindi magpapatuloy ang Shorts player sa pagsisimula ng app Magpapatuloy ang manlalaro ng shorts sa pagsisimula ng app - + + + Paganahin ang layout ng tablet Naka-enable ang layout ng tablet Naka-disable ang layout ng tablet Hindi lumalabas ang mga post sa komunidad sa mga layout ng tablet - + Baguhin ang istilo ng in app minimized na player Uri ng miniplayer Orihinal @@ -943,21 +924,21 @@ This is because Crowdin requires temporarily flattening this file and removing t Ang halaga ng opacity sa pagitan ng 0-100, kung saan ang 0 ay transparent Ang opacity ng miniplayer overlay ay dapat nasa pagitan ng 0-100 - + Paganahin ang gradient loading screen Ang paglo-load ng screen ay magkakaroon ng gradient na background Ang paglo-load ng screen ay magkakaroon ng solidong background - + Paganahin ang custom na kulay ng seekbar Ipinapakita ang kulay ng custom na seekbar Ipinapakita ang orihinal na kulay ng seekbar Pasadyang kulay ng seekbar Ang kulay ng seekbar - + - + Tab ng tahanan @@ -988,7 +969,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Pansamantalang hindi available ang DeArrow (status code: %s) Pansamantalang hindi available ang DeArrow - + Ipakita ang mga anunsyo ng ReVanced Ang mga anunsyo ay ipinapakita sa startup Ang mga anunsyo ay hindi ipinapakita sa startup @@ -996,46 +977,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Nabigong kumonekta sa provider ng mga anunsyo Kalimutan - + Babala Huwag ipakitang muli - + Paganahin ang auto-repeat Naka-enable ang auto-repeat Naka-disable ang auto-repeat - + Mga dimensyon ng spoof device Na-spoof ang mga dimensyon ng device\n\nMaaaring ma-unlock ang mas matataas na katangian ng video ngunit maaari kang makaranas ng pag-utal ng pag-playback ng video, mas malala ang buhay ng baterya, at hindi alam na mga side effect Hindi na-spoof ang mga dimensyon ng device\n\nAng pag-enable dito ay makakapag-unlock ng mas matataas na katangian ng video Ang pagpapagana nito ay maaaring magdulot ng pagkautal sa pag-playback ng video, mas malala ang buhay ng baterya, at hindi kilalang mga side effect. - + Mga Setting ng GmsCore Mga setting para sa GmsCore - + I-bypass ang mga pag-redirect ng URL Ang mga pag-redirect ng URL ay na-bypass Hindi na-bypass ang mga pag-redirect ng URL - + Buksan ang mga link sa browser Pagbubukas ng mga link sa labas Pagbubukas ng mga link sa app - + Alisin ang parameter ng query sa pagsubaybay Ang parameter ng query sa pagsubaybay ay tinanggal mula sa mga link Ang parameter ng query sa pagsubaybay ay hindi inaalis sa mga link - + Huwag paganahin ang zoom haptics Naka-disable ang Haptics Pinagana ang Haptics - + Awtomatikong kalidad Tandaan ang mga pagbabago sa kalidad ng video Nalalapat ang mga pagbabago sa kalidad sa lahat ng video @@ -1044,78 +1025,74 @@ This is because Crowdin requires temporarily flattening this file and removing t Default na kalidad ng video sa mobile network Binago ang default na kalidad ng %1$s sa: %2$s - + Ipakita ang pindutan ng dialog ng bilis Ang pindutan ay ipinapakita Hindi ipinapakita ang button - + Mga custom na bilis ng pag-playback - Magdagdag o baguhin ang magagamit na bilis ng pag-playback Ang mga custom na bilis ay dapat mas mababa sa %s. Paggamit ng mga default na halaga. Di-wastong mga custom na bilis ng pag-playback. Paggamit ng mga default na halaga. - + Tandaan ang mga pagbabago sa bilis ng pag-playback Nalalapat ang mga pagbabago sa bilis ng pag-playback sa lahat ng video Nalalapat lamang ang mga pagbabago sa bilis ng pag-playback sa kasalukuyang video Default na bilis ng pag-playback Binago ang default na bilis sa: %s - + Ibalik ang lumang menu ng kalidad ng video Ipinapakita ang lumang menu ng kalidad ng video Hindi ipinapakita ang lumang menu ng kalidad ng video - + Paganahin ang slide para maghanap Naka-enable ang slide to seek Hindi pinagana ang slide to seek - + Ang pag-off sa setting na ito ay maaaring magdulot ng mga isyu sa pag-playback ng video. - - - - + I-block ang mga audio ad Naka-block ang mga audio ad Na-unblock ang mga audio ad - + %s ay hindi magagamit. Maaaring magpakita ang mga ad. Subukang lumipat sa isa pang serbisyo ng ad block sa mga setting. Nagbalik ng error ang server ng %s. Maaaring magpakita ang mga ad. Subukang lumipat sa isa pang serbisyo ng ad block sa mga setting. I-block ang mga naka-embed na video ad Hindi Luminous na proxy - + I-block ang mga video ad Naka-block ang mga video ad Na-unblock ang mga video ad - + mensaheng binura Ipakita ang mga tinanggal na mensahe Huwag ipakita ang mga tinanggal na mensahe Itago ang mga tinanggal na mensahe sa likod ng isang spoiler Ipakita ang mga tinanggal na mensahe bilang naka-cross-out na text - + Awtomatikong i-claim ang Channel Points Awtomatikong kine-claim ang Mga Channel Point Hindi awtomatikong kine-claim ang Mga Channel Point - + Paganahin ang Twitch debug mode Naka-enable ang twitch debug mode (hindi inirerekomenda) Naka-disable ang twitch debug mode - + Mga ad Mga setting ng pag-block ng ad Mga setting ng chat diff --git a/src/main/resources/addresources/values-fr-rFR/strings.xml b/patches/src/main/resources/addresources/values-fr-rFR/strings.xml similarity index 93% rename from src/main/resources/addresources/values-fr-rFR/strings.xml rename to patches/src/main/resources/addresources/values-fr-rFR/strings.xml index 2e0a60836..c09f2bcd0 100644 --- a/src/main/resources/addresources/values-fr-rFR/strings.xml +++ b/patches/src/main/resources/addresources/values-fr-rFR/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Les vérifications ont échoué Ouvrir le site web officiel Ignorer @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Patché il y as %s jours La date de compilation de l\'APK est corrompue - + ReVanced Souhaitez-vous continuer ? Réinitialiser @@ -63,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Liens officiels Faire un don - + MicroG GmsCore n\'est pas installé. Veuillez l’installer. Action requise  @@ -74,7 +74,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + À propos Publicités Miniatures alternatives @@ -86,7 +86,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Divers Vidéo - + + Désactiver la lecture en arrière-plan des Shorts + La lecture en arrière-plan des Shorts est désactivée + La lecture en arrière-plan des Shorts est activée + + Débogage Activer ou désactiver les options de débogage Journal de débogage @@ -103,13 +108,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Toast non affiché si une erreur se produit Désactiver les toasts d\'erreur masque toutes les notifications d\'erreur ReVanced\n\nVous ne serez pas notifié des événements inattendus. - + Désactiver la lueur des boutons \"J\'aime\" / \"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 les séparateurs gris - Les séparateurs gris sont masqués - Les séparateurs gris sont affichés + Cacher les cartes d\'album + Les cartes d\'album sont cachées + Les cartes de l\'album sont affichées + Masquer la boîte de financement participatif + La boîte de Crowdfunding est cachée + La boîte de financement participatif est affichée + Masquer le bouton de microphone flottant + Bouton du microphone masqué + Bouton du microphone affiché Masquer le filigrane de chaine Le filigrane est masqué Le filigrane est affiché @@ -154,9 +165,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Masquer les options extensibles sous les vidéos Les puces extensibles sont masquées Les puces extensibles sont affichées - Masquer le pied de page du menu de la qualité de la vidéo - Le pied de page du menu de la qualité de la vidéo est masqué - Le pied de page du menu de la qualité de la vidéo est affiché Masquer les posts communautaires Les posts communautaires sont masqués Les posts communautaires sont affichés @@ -231,6 +239,37 @@ This is because Crowdin requires temporarily flattening this file and removing t La section \'Transcription\' est affiché Description vidéo Masquer ou afficher des éléments de la description de la vidéo + Barre de filtre + Masquer ou afficher la barre de filtre dans le flux, la recherche et les vidéos connexes + Cacher dans le flux + Caché dans le flux + Affiché dans le flux + Cacher dans la recherche + Caché dans la recherche + Affiché dans la recherche + Cacher dans les vidéos liées + Caché dans les vidéos liées + Affiché dans des vidéos connexes + Commentaires + Masquer ou afficher les composants de la section commentaires + Cacher l\'en-tête \'Commentaires par membres\' + L\'en-tête \'Commentaires par membres\' est masqué + L\'en-tête \'Commentaires par membres\' est affiché + Cacher la section des commentaires + La section Commentaires est cachée + La section Commentaires est affichée + Masquer le bouton « Créer un court » + Le bouton « Créer un court » est masqué + Le bouton « Créer un court » est affiché + Masquer le commentaire de prévisualisation + L\'aperçu du commentaire est masqué + L\'aperçu du commentaire est affiché + Masquer le bouton de remerciement + Le bouton de remerciement est caché + Le bouton de remerciement est affiché + Masquer les boutons d\'horodatage et d\'émoji + Les boutons d\'horodatage et d\'émoji sont cachés + Les boutons d\'horodatage et d\'émoji sont affichés Masquer les Doodles YouTube Les Doodles de la barre de recherche sont masquées @@ -272,7 +311,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Le mot-clé est trop court et nécessite des guillemets : %s Ce mot-clé va masquer toutes les vidéos : %s - + 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 @@ -291,6 +330,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Masquer la bannière \'Afficher les produits\' La bannière est masquée La bannière est affichée + Masquer l\'étagère d\'achat du joueur + L\'étagère d\'achat est cachée + L\'étagère d\'achat est affichée Masquer les liens des produits dans la description de la vidéo Les liens des produits sont masqués Les liens de produits sont affichés @@ -307,17 +349,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Masquer les publicités en plein écran ne fonctionne qu\'avec les appareils plus anciens - + Masquer les publicités pour YouTube Premium Les publicités pour YouTube Premium sous le lecteur vidéo sont masquées Les publicités pour YouTube Premium sous le lecteur vidéo sont affichées - + Masquer les publicités vidéo Les publicités vidéo sont masquées Les publicités vidéo sont affichées - + URL copié dans le presse-papier URL avec horodatage copié Afficher un bouton \"Copier le lien de la vidéo\" @@ -327,13 +369,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Le bouton est affiché. Appuyez pour copier le lien de la vidéo avec horodatage. Appuyez longuement pour copier la vidéo sans horodatage Le bouton n\'est pas affiché - + Supprimer le message \'Confirmer votre âge\' Le message sera supprimé Le message sera affiché Cela ne contourne pas la restriction d\'âge, mais le confirme automatiquement. - + Téléchargeur externe Paramètres d\'utilisation du téléchargeur externe Afficher le bouton de téléchargement externe @@ -347,17 +389,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Nom du paquet de l\'appli de téléchargement externe installée, telle que NewPipe ou Seal %s n\'est pas installé. Veuillez l\'installer. - + Désactiver le geste de recherche précise Les gestes sont désactivés Les gestes sont activés - + Activer l\'appui sur la barre de progression L\'appui sur la barre de progression est activé L\'appui sur la barre de progression est désactivé - + Activer les gestes pour la luminosité Les gestes de contrôle de la luminosité sont activés Les gestes de contrôle de la luminosité sont désactivés @@ -386,12 +428,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Intensité des gestes L\'intensité du mouvement à effectuer pour que les gestes se produise - + Désactiver les sous-titres automatiques Les sous-titres automatiques sont désactivés Les sous-titres automatiques sont activés - + Boutons d\'action Masque ou affiche les boutons sous les vidéos Masquer les \'J\'aime\' et \'Je n\'aime pas\' @@ -427,23 +469,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Le bouton \'Enregistrer dans la playlist\' est masqué Le bouton \'Enregistrer dans la playlist\' est affiché - - Masquer le bouton \'Lecture automatique\' - Le bouton \"Lecture automatique\" est masqué - Le bouton \"Lecture automatique\" est affiché - - - - Masquer le bouton \'Sous-titres\' - Le bouton \'Sous-titres\' est masqué - Le bouton \'Sous-titres\' est affiché - - - Masquer le bouton \'Caster\' - Le bouton \'Caster\' est masqué - Le bouton \'Caster\' est affiché - - + Boutons de navigation Masquer ou modifier les boutons dans la barre de navigation @@ -470,7 +496,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Les noms sont masqués Les noms sont affichés - + Menu déroulant Masquer ou afficher les éléments du menu déroulant du lecteur @@ -481,6 +507,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Masquer les paramètres supplémentaires Le menu des paramètres supplémentaires est masqué Le menu des paramètres supplémentaires est affiché + + Masquer la minuterie de sommeil + Le menu de la minuterie est masquée + Le menu de la minuterie est affiché Masquer la vidéo en boucle Le menu vidéo en boucle est masqué @@ -489,6 +519,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Masquer le mode Veille Le menu du mode Veille est masqué Le menu du mode Veille s\'affiche + Masquer le volume stable + Le menu de volume stable est affiché + Le menu de volume stable est masqué Cacher les commentaires d\'Aide & Le menu d\'aide & de commentaires est masqué @@ -514,83 +547,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Masquer Regarder en VR Le menu Regarder en VR est masqué Le menu Regarder en VR est affiché + Masquer le pied de page du menu de la qualité de la vidéo + Le pied de page du menu qualité vidéo est masqué + Le pied de page du menu de qualité vidéo est affiché - - Masquer les boutons de la vidéo précédente & suivants - Les boutons sont cachés - Les boutons sont affichés + + Masquer les boutons de la vidéo précédente & suivants + Les boutons sont cachés + Les boutons sont affichés + Masquer le bouton \'Caster\' + Le bouton \'Caster\' est masqué + Le bouton \'Caster\' est affiché + + Masquer le bouton \'Sous-titres\' + Le bouton \'Sous-titres\' est masqué + Le bouton \'Sous-titres\' est affiché + Masquer le bouton \'Lecture automatique\' + Le bouton \"Lecture automatique\" est masqué + Le bouton \"Lecture automatique\" est affiché - - Cacher les cartes d\'album - Les cartes d\'album sont cachées - Les cartes de l\'album sont affichées - - - Commentaires - Masquer ou afficher les composants de la section commentaires - Cacher l\'en-tête \'Commentaires par membres\' - L\'en-tête \'Commentaires par membres\' est masqué - L\'en-tête \'Commentaires par membres\' est affiché - Cacher la section des commentaires - La section Commentaires est cachée - La section Commentaires est affichée - Masquer le bouton « Créer un court » - Le bouton « Créer un court » est masqué - Le bouton « Créer un court » est affiché - Masquer le commentaire de prévisualisation - L\'aperçu du commentaire est masqué - L\'aperçu du commentaire est affiché - Masquer le bouton de remerciement - Le bouton de remerciement est caché - Le bouton de remerciement est affiché - Masquer les boutons d\'horodatage et d\'émoji - Les boutons d\'horodatage et d\'émoji sont cachés - Les boutons d\'horodatage et d\'émoji sont affichés - - - Masquer la boîte de financement participatif - La boîte de Crowdfunding est cachée - La boîte de financement participatif est affichée - - + Masquer les cartes de fin d\'écran Les cartes de fin d\'écran sont masquées Les cartes de fin d\'écran sont affichées - - Barre de filtre - Masquer ou afficher la barre de filtre dans le flux, la recherche et les vidéos connexes - Cacher dans le flux - Caché dans le flux - Affiché dans le flux - Cacher dans la recherche - Caché dans la recherche - Affiché dans la recherche - Cacher dans les vidéos liées - Caché dans les vidéos liées - Affiché dans des vidéos connexes - - - Masquer le bouton de microphone flottant - Bouton du microphone masqué - Bouton du microphone affiché - - + Désactiver le mode ambiant en plein écran Mode Veille désactivé Mode Veille activé - + Cacher les infos Les fiches info sont masquées Les fiches info sont affichées - + Désactiver les animations des numéros roulants Les numéros roulants ne sont pas animés Les numéros roulants sont animés - + Cacher la barre de recherche dans le lecteur vidéo La barre de recherche du lecteur vidéo est masquée La barre de recherche du lecteur vidéo est affichée @@ -598,7 +594,9 @@ This is because Crowdin requires temporarily flattening this file and removing t La barre de recherche des miniatures est masquée La barre de recherche des miniatures est affichée - + + Joueur Shorts + Cacher ou afficher les composants dans le joueur Shorts Cacher les Shorts dans la page d\'accueil Les courts dans le flux domestique sont cachés @@ -696,27 +694,27 @@ This is because Crowdin requires temporarily flattening this file and removing t La barre de navigation est cachée La barre de navigation est affichée - + Désactiver l\'écran de fin de la vidéo suggérée Les vidéos suggérées seront désactivées Les vidéos suggérées seront affichées - + Masquer l\'horodatage de la vidéo L\'horodatage est caché L\'horodatage est affiché - + Masquer les fenêtres pop-up du joueur Les fenêtres pop-up sont masquées Les fenêtres pop-up sont affichées - + Opacité de l\'overlay du joueur Valeur d\'opacité entre 0 et 100, où 0 est transparent L\'opacité de l\'overlay du joueur doit être comprise entre 0 et 100 - + API des dislikes temporairement indisponible N\'aime pas disponible (statut %d) @@ -760,17 +758,22 @@ This is because Crowdin requires temporarily flattening this file and removing t Limite de débit du client rencontrée %d fois %d millisecondes - + Activer la barre de recherche large La barre de recherche large est activée La barre de recherche large est désactivée - + + Activer les vignettes de haute qualité + Les vignettes de la barre de recherche sont de haute qualité + Les vignettes de la barre de recherche sont de qualité moyenne + Les vignettes de la barre de recherche plein écran sont de haute qualité + Les vignettes de barre de recherche plein écran ont une qualité moyenne Restaurer les anciennes miniatures de la barre de recherche Les vignettes de la barre de recherche apparaîtront au-dessus de la barre de recherche Les vignettes de la barre de recherche apparaîtront en plein écran - + Activer SponsorBlock SponsorBlock est un système axé sur la foule pour sauter des parties ennuyeuses de vidéos YouTube Apparence @@ -951,7 +954,7 @@ This is because Crowdin requires temporarily flattening this file and removing t À propos Les données sont fournies par l\'API SponsorBlock. Appuyez ici pour en savoir plus et voir les téléchargements pour d\'autres plates-formes - + Spoof version de l\'application Version falsifiée Version non falsifiée @@ -964,9 +967,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Restaurer une grande vitesse vidéo & menu qualité 18.09.39 - Restaurer l\'onglet bibliothèque 17.41.37 - Restaurer l\'ancienne tablette de la liste de lecture - 17.33.42 - Restaurer l\'ancienne mise en page de l\'interface - + Définir la page de démarrage Par défaut Parcourir les chaînes @@ -984,18 +986,25 @@ This is because Crowdin requires temporarily flattening this file and removing t Tendance Regarder plus tard - + Désactiver la reprise du joueur Shorts - Le lecteur court ne reprendra pas au démarrage de l\'application Le lecteur court reprendra au démarrage de l\'application - + + Jouer les Shorts + Les Shorts joueront automatiquement + Les Shorts se répéteront + Lecture automatique des Shorts en arrière-plan + La lecture en arrière-plan des Shorts sera automatique + La lecture en arrière-plan des Shorts se répétera + + Activer la disposition de la tablette Mise en page de la tablette est activée La disposition de la tablette est désactivée Les messages de la communauté n\'apparaissent pas sur la disposition de la tablette - + Lecteur réduit Changer le style du lecteur réduit dans l\'application Type de Miniplayer @@ -1014,6 +1023,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Activer le glisser-déposer Le glisser-déposer est activé\n\nMiniplayer peut être déplacé vers n\'importe quel coin de l\'écran Glisser-déposer est désactivé + Activer le geste de glissement horizontal + Geste de glissement horizontal activé\n\nMiniplayer peut être déplacé hors de l\'écran vers la gauche ou la droite + Geste de glissement horizontal désactivé Masquer le bouton de fermeture Le bouton de fermeture est masqué Le bouton de fermeture est affiché @@ -1033,12 +1045,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Valeur d\'opacité entre 0 et 100, où 0 est transparent L\'opacité de l\'overlay du Miniplayer doit être comprise entre 0 et 100 - + Activer l\'écran de chargement du dégradé L\'écran de chargement aura un fond de dégradé L\'écran de chargement aura un fond solide - + Activer la couleur personnalisée de la barre de recherche La couleur personnalisée de la barre de recherche est affichée La couleur originale de la barre de recherche est affichée @@ -1046,12 +1058,12 @@ This is because Crowdin requires temporarily flattening this file and removing t La couleur de la barre de recherche Valeur de couleur de la barre de recherche invalide - + Ignorer les restrictions de région de l\'image Utiliser l\'hôte d\'image yt4.ggpht.com Utiliser l\'hôte d\'image original\n\nActiver ceci peut corriger les images manquantes qui sont bloquées dans certaines régions - + Onglet d\'accueil @@ -1083,7 +1095,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow temporairement indisponible (code : %s) DeArrow est temporairement indisponible - + Afficher les annonces ReVanced Les annonces sont affichées au démarrage Les annonces ne sont pas affichées au démarrage @@ -1091,47 +1103,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Échec de la connexion au fournisseur d\'annonces Ignorer - + Avertissement Votre historique de surveillance n\'est pas enregistré.<br><br>Ceci est probablement dû à un bloqueur de publicités DNS ou à un proxy réseau.<br><br>Pour résoudre ce problème, mettez sur la liste blanche <b>s.youtube.com</b> ou désactivez tous les bloqueurs DNS et proxies. Ne plus afficher - + Activer la répétition automatique La répétition automatique est activée La répétition automatique est désactivée - + Falsifier les dimensions de l\'appareil Les dimensions de l\'appareil ont falsifié\n\ndes qualités vidéo plus élevées peuvent être débloquées mais vous risquez de rencontrer des difficultés de lecture vidéo, une mauvaise autonomie de la batterie et des effets secondaires inconnus Les dimensions de l\'appareil n\'ont pas étées falsifiées\n\nL\'activation de cette option peut débloquer des qualités vidéo plus élevées L\'activation de cette option peut causer des problèmes de lecture vidéo, une dégradation de la durée de vie de la batterie et des effets secondaires inconnus. - + Paramètres GmsCore Paramètres pour GmsCore - + Outrepasser les redirections d\'URL Les redirections d\'URL sont contournées Les redirections d\'URL ne sont pas contournées - + Ouvrir les liens dans le navigateur Ouverture des liens externes Ouverture des liens dans l\'application - + Supprimer le paramètre de requête de suivi Le paramètre de requête de suivi est supprimé des liens Le paramètre de requête de suivi n\'est pas supprimé des liens - + Désactiver le zoom haptique Haptics sont désactivés Haptics sont activés - + Qualité automatique Mémoriser les changements de qualité vidéo Les changements de qualité s\'appliquent à toutes les vidéos @@ -1142,35 +1154,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wi-fi La qualité %1$s par défaut a été changée en : %2$s - + Afficher le bouton de la boîte de dialogue Vitesse Le bouton est affiché Le bouton n\'est pas affiché - + + Menu de vitesse de lecture personnalisé + Le menu de vitesse personnalisé est affiché + Le menu de vitesse personnalisé n\'est pas affiché Vitesse de lecture personnalisée - Ajouter ou modifier les vitesses de lecture disponibles + Ajouter ou modifier les vitesses de lecture personnalisées Les vitesses personnalisées doivent être inférieures à %s. Utiliser les valeurs par défaut. Vitesses de lecture personnalisées invalides. Utilisation des valeurs par défaut. - + Se souvenir des changements de vitesse de lecture Les changements de vitesse de lecture s\'appliquent à toutes les vidéos Les changements de vitesse de lecture ne s\'appliquent qu\'à la vidéo actuelle Vitesse de lecture par défaut Vitesse par défaut changée en : %s - + Restaurer l\'ancien menu qualité vidéo L\'ancien menu de qualité vidéo est affiché L\'ancien menu de qualité vidéo n\'est pas affiché - + Activer la diapositive pour rechercher Glisser pour chercher est activé Glisser à chercher n\'est pas activé - + Falsifier les flux vidéo Falsifier les flux vidéo du client pour éviter les problèmes de lecture Falsifier les flux vidéo @@ -1188,20 +1203,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Effets secondaires de l\'usurpation VR Android • Le menu de la piste audio manque\n• Le volume stable n\'est pas disponible - - - Activer la luminosité automatique du HDR - Luminosité HDR automatique activée - Luminosité HDR automatique désactivée - - + Bloquer les publicités audio Les publicités audio sont bloquées Les publicités audio sont débloquées - + %s n\'est pas disponible. Les publicités peuvent s\'afficher. Essayez de passer à un autre service de blocage de publicités dans les paramètres. Le serveur %s a renvoyé une erreur. Les publicités peuvent s\'afficher. Essayez de passer à un autre service de blocage de publicités dans les paramètres. Bloquer les publicités vidéo intégrées @@ -1209,30 +1218,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Proxy lumineux Proxy PurpleAdBlock - + Bloquer les publicités vidéo Les publicités vidéo sont bloquées Les publicités vidéo sont débloquées - + message supprimé Afficher les messages supprimés Ne pas afficher les messages supprimés Masquer les messages supprimés derrière un spoiler Afficher les messages supprimés sous forme de texte croisé - + Réclamer automatiquement les points de la chaîne Les points de la chaîne sont automatiquement réclamés Les points de la chaîne ne sont pas réclamés automatiquement - + Activer le mode débogage Twitch Le mode débogage Twitch est activé (non recommandé) Le mode débogage Twitch est désactivé - + Réglages ReVanced Publicités Paramètres de blocage des publicités diff --git a/src/main/resources/addresources/values-ga-rIE/strings.xml b/patches/src/main/resources/addresources/values-ga-rIE/strings.xml similarity index 90% rename from src/main/resources/addresources/values-ga-rIE/strings.xml rename to patches/src/main/resources/addresources/values-ga-rIE/strings.xml index f77e1b097..437b6339c 100644 --- a/src/main/resources/addresources/values-ga-rIE/strings.xml +++ b/patches/src/main/resources/addresources/values-ga-rIE/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Theip ar sheiceálacha Oscailt láithreán gréasáin oifigiúil Déan neamhaird de @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Patáilte %s lá ó shin Tá dáta tógála APK truaillithe - + Ar mhaith leat dul ar aghaidh? Athshocraigh Athnuachan agus atosaigh @@ -62,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Naisc oifigiúla Síntiúis - + Níl microG GMScore suiteáilte. Suiteáil é. Gníomhaíocht a theast @@ -73,7 +73,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Maidir Fógraí Mionsamhlacha malartacha @@ -85,7 +85,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Misc Físeán - + + Díchumasaigh súgradh cúlra Shorts + Tá súgradh cúlra Shorts díchumasaithe + Tá súgradh cúlra Shorts cumasaithe + + Dífhabhtú Cumasaigh nó díchumasaigh roghanna dífhabhtaithe Logáil dífhabhtaithe @@ -102,13 +107,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Ní thaispeántar tósta má tharlaíonn earráid Folaíonn toastanna earráide a mhúchadh gach fógra earráide ReVanced. \n\n Ní chuirfear in iúl duit faoi aon imeachtaí gan choinne. - + Díchumasaigh cosúil/liostáil chnaipe glow Ní ghlacfaidh an cnaipe Like agus Liostáil nuair a luaitear Taispeánfaidh an cnaipe Like agus Liostáil nuair a luait - Folaigh deighilteoir liath - Tá deighilteoirí liath i bhfolach - Taispeántar deighilteoirí liath + Folaigh cártaí albam + Tá cártaí albam i bhfolach + Taispeántar cártaí albam + Folaigh bosca slua-mhaoiniú + Tá bosca crowdfunding i bhfolach + Taispeántar bosca slua-mhaoiniú + Cnaipe micreafón ar snámh + Cnaipe micreafón folach + Taispeántar an cnaipe micreafón Folaigh comhartha uisce cainéal Tá comhartha uisce i bhfolach Taispeántar comhartha uisce @@ -153,9 +164,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Folaigh sliseanna inmhéadaithe faoi fhíseáin Tá sceallóga leathnaithe i bhfolach Taispeántar sceallóga leathnaithe - Folaigh buntásc roghchlár cáilíochta físe - Tá buntásc an roghchláir cáilíochta físeáin folaithe - Taispeántar buntásc roghchlár cáilíochta físeáin Folaigh postálacha pobail Tá postálacha pobail i bhfolach Taispeántar postálacha pobail @@ -230,6 +238,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Taispeántar alt an tras-scríbhinn Cur síos físeán Folaigh nó taispeáint comhpháirteanna tuairisc + Barra scagaire + Folaigh nó taispeáin an barra scagaire sna físeáin beatha, cuardaigh agus gaolmhara + Folaigh i mbeatha + I bhfolach i mbeatha + Taispeántar i mbeatha + Folaigh i gcuardach + I bhfolach i gcuardach + Taispeántar i gcuardach + Folaigh i bhfíseáin gaolmhara + I bhfolach i bhfíseáin ghaolmhara + Taispeántar i bhfíseáin ghaolmhara + Tuairimí + Folaigh nó taispeáin comhpháirteanna na rannóige tuairimí + Folaigh ceanntásc \'Tuairimí ag baill \' + Tá ceanntásc \'Tuairimí ag comhaltaí \'i bhfolach + Taispeántar ceanntásc \'Tuairimí ag comhaltaí\' + Folaigh roinn tuairimí + Tá an chuid tuairimí i bhfolach + Taispeántar an chuid tuairimí + Folaigh an cnaipe \'Cruthaigh Short\' + Tá cnaipe ‘Cruthaigh Short’ i bhfolach + Taispeántar an cnaipe ‘Cruthaigh Short’ + Folaigh trácht réamhamharc + Tá trácht réamhamhar i bhfolach + Taispeántar trácht réamhamharc + Folaigh cnaipe buíochas + Tá cnaipe buíochas i bhfolach + Taispeántar cnaipe buíochas + Folaigh cnaipí ama agus emoji + Tá cnaipí ama agus emoji i bhfolach + Taispeántar cnaipí ama agus emoji Folaigh YouTube Doodles Barra cuardaigh Tá Doodles i bhfolach @@ -260,7 +299,7 @@ This is because Crowdin requires temporarily flattening this file and removing t This is because keywords can be in any language, and showing an example in the localized script helps convey this. --> Eochairfhocail agus frásaí le cur i bhfolach, scartha le línte nua\n\nIs féidir le heochairfhocail a bheith ina n-ainmneacha cainéal nó in aon téacs a thaispeántar i dteideal físeáin\n\nNí mór focail a bhfuil litreacha móra sa lár a chur isteach leis an gcásáil (. i. iPhone, TikTok, LeBlanc) Maidir le scagadh eochairfhocal - Déantar torthaí Baile/Síntiúis/Cuardaigh a scagadh chun inneachar a mheaitseálann frásaí eochairfhocail a chur i bhfolach\n\nTeorainneacha\n• Ní féidir shorts a chur i bhfolach le hainm an chainéil\n• Seans nach mbeidh roinnt comhpháirteanna Chomhéadain i bhfolach\n• Seans nach dtaispeánfar torthaí nuair a chuardaítear eochairfhocal + Déantar torthaí Baile/Síntiúis/Cuardaigh a scagadh chun inneachar a mheaitseálann frásaí eochairfhocail a chur i bhfolach\n\nTeorainneacha\n• Ní féidir Shorts a chur i bhfolach le hainm an chainéil\n• Seans nach mbeidh roinnt comhpháirteanna Chomhéadain i bhfolach\n• Seans nach dtaispeánfar torthaí nuair a chuardaítear eochairfhocal Meaitseáil focail iomlána Má bhaineann tú eochairfhocal/frása le comharthaí athfhriotail dhúbailte, cuirfear cosc ​​ar mheaitseáil pháirteach de theidil físeáin agus ainmneacha cainéal<br><br>Mar shampla,<br><b>\"ai\"</b> ceilteoidh sé an físeán: <b>Conas a oibríonn AI?</b><br>ach ní cheiltfidh sé: <b>Cad is brí le hainm féinig?</b> @@ -271,7 +310,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Tá an eochairfhocal ró-ghearr agus teastaíonn comharthaí athfhriotail: %s Folaigh eochairfhocal gach físeán: %s - + Folaigh fógraí ginearálta Tá fógraí ginearálta i bhfolach Taispeántar fógraí ginearálta @@ -290,6 +329,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Folaigh meirge chun táirgí a fheiceáil Tá bratach i bhfolach Taispeántar an bhratach + Folaigh seilf siopadóireachta an t-imreoir + Tá seilf siopadóireachta i bhfolach + Taispeántar seilf siopadóireachta Folaigh naisc siopadóireachta sa chur síos físeáin Tá naisc siopadóireachta i bhfolach Taispeántar naisc siopadóireachta @@ -306,17 +348,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Ní oibríonn folaigh fógraí lánscáileáin ach le gléasanna níos sine - + Folaigh cur chun cinn Préimhe YouTube Tá cur chun cinn YouTube Premium faoi seinnteoir físe i bhfolach Taispeántar cur chun cinn préimhe YouTube faoi seinnteoir físe - + Folaigh fógraí físe Tá fógraí físe i bhfolach Taispeántar fógraí físe - + URL cóipeáilte chuig gearr URL le stampa ama cóipeáilte Taispeáin cnaipe URL físe cóipeáil @@ -326,13 +368,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Taispeántar an cnaipe. Tapáil chun URL físe a chóipeáil le stampa ama. Tapáil agus coinnigh chun físeán a chóipeáil gan stampa ama Ní thaispeántar an cnaipe - + Bain dialóg rogha féachana Bainfear dialóg Taispeánfar dialóg Ní sheachnaíonn sé seo an srian aoise. Ní ghlacann sé leis go huathoibríoch. - + Íosluchtaigh seachtracha Socruithe chun íoslódálaí seachtrach a úsáid Taispeáin cnaipe íoslódála @@ -346,17 +388,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Ainm pacáiste d\'aip íoslódála seachtrach suiteáilte, mar shampla NewPipe nó Seal Níl %s suiteáilte. Suiteáil é le do thoil. - + Díchumasaigh comhartha cuardaigh beacht Tá comhartha míchumasaithe Tá comhartha cumasaithe - + Cumasaigh tapáil barra iarratais Tá tapáil Seekbar cumasaithe Tá tapáil Seekbar díchumasaithe - + Cumasaigh comhartha gile Tá slip gile cumasaithe Tá slide gile díchumasaithe @@ -385,12 +427,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Tairseach méid swipe Méid an tairseach le haghaidh sruthú tarlú - + Díchumasaigh fotheidil uathoibríoch Tá fotheidil uathoibríoch díchumasaithe Tá fotheidil uathoibríoch cumasaithe - + Cnaipí gníomh Folaigh nó taispeáin cnaipí faoi fhíseáin Folaigh Like agus Dislike @@ -426,23 +468,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Tá cnaipe Sábháil go seinmliosta i bhfolach Taispeántar cnaipe Sábháil go seinmliosta - - Folaigh cnaipe autoplay - Tá cnaipe Autoplay i bhfolach - Taispeántar cnaipe Autoplay - - - - Folaigh cnaipe fotheidil - Tá cnaipe fotheidil i bhfolach - Taispeántar cnaipe fotheidil - - - Folaigh cnaipe teilgthe - Tá cnaipe teilgthe i bhfolach - Taispeántar cnaipe teilgthe - - + Cnaipí nascleanúna Folaigh nó athraigh cnaipí sa bharra nascleanúna @@ -451,8 +477,8 @@ This is because Crowdin requires temporarily flattening this file and removing t Taispeántar cnaipe baile Folaigh Shorts - Tá cnaipe shorts i bhfolach - Taispeántar cnaipe shorts + Tá cnaipe Shorts i bhfolach + Taispeántar cnaipe Shorts Folaigh Cruthaigh Tá cnaipe Cruthaigh i bhfolach @@ -469,7 +495,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Tá lipéid i bhfolach Taispeántar lipéid - + Roghchlár Flyout Folaigh nó taispeáin míreanna roghchlár flyout an imreora @@ -480,6 +506,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Folaigh Socruithe Breise Tá roghchlár socruithe breise i bhfolach Taispeántar roghchlár socruithe breise + + Folaigh lasc ama codlata + Tá an roghchlár lasc ama codlata i bhfolach + Taispeántar roghchlár an lasc ama codlata Folaigh físeán lúb Tá roghchlár físe lúb i bhfolach @@ -488,6 +518,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Folaigh modh comhthimpeallach Tá roghchlár mód chomhthimpeallach Taispeántar roghchlár mód comhthimpeallach + Folaigh toirt cobhsaí + Taispeántar roghchlár toirte cobhsaí + Tá roghchlár toirte cobhsaí i bhfolach Folaigh Cabhair & aiseolas Cabhair & Tá an roghchlár aiseolais i bhfolach @@ -513,83 +546,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Folaigh Watch i VR Tá faire i roghchlár VR i bhfolach Taispeántar an faire sa roghchlár VR + Folaigh buntásc roghchlár cáilíochta físe + Tá buntásc an roghchláir cáilíochta físeáin folaithe + Taispeántar buntásc roghchlár cáilíochta físeáin - - Folaigh & cnaipí físeáin seo chugainn - Tá cnaipí i bhfolach - Taispeántar cnaipí + + Folaigh & cnaipí físeáin seo chugainn + Tá cnaipí i bhfolach + Taispeántar cnaipí + Folaigh cnaipe teilgthe + Tá cnaipe teilgthe i bhfolach + Taispeántar cnaipe teilgthe + + Folaigh cnaipe fotheidil + Tá cnaipe fotheidil i bhfolach + Taispeántar cnaipe fotheidil + Folaigh cnaipe autoplay + Tá cnaipe Autoplay i bhfolach + Taispeántar cnaipe Autoplay - - Folaigh cártaí albam - Tá cártaí albam i bhfolach - Taispeántar cártaí albam - - - Tuairimí - Folaigh nó taispeáin comhpháirteanna na rannóige tuairimí - Folaigh ceanntásc \'Tuairimí ag baill \' - Tá ceanntásc \'Tuairimí ag comhaltaí \'i bhfolach - Taispeántar ceanntásc \'Tuairimí ag comhaltaí\' - Folaigh roinn tuairimí - Tá an chuid tuairimí i bhfolach - Taispeántar an chuid tuairimí - Folaigh cnaipe \'Cruthaigh gearr\' - Tá cnaipe \'Cruthaigh gearr\' i bhfolach - Taispeántar cnaipe \'Cruthaigh gearr\' - Folaigh trácht réamhamharc - Tá trácht réamhamhar i bhfolach - Taispeántar trácht réamhamharc - Folaigh cnaipe buíochas - Tá cnaipe buíochas i bhfolach - Taispeántar cnaipe buíochas - Folaigh cnaipí ama agus emoji - Tá cnaipí ama agus emoji i bhfolach - Taispeántar cnaipí ama agus emoji - - - Folaigh bosca slua-mhaoiniú - Tá bosca crowdfunding i bhfolach - Taispeántar bosca slua-mhaoiniú - - + Folaigh cártaí scáileáin deireadh Tá cártaí scáileáin deiridh i bhfolach Taispeántar cártaí scáileáin deireadh - - Barra scagaire - Folaigh nó taispeáin an barra scagaire sna físeáin beatha, cuardaigh agus gaolmhara - Folaigh i mbeatha - I bhfolach i mbeatha - Taispeántar i mbeatha - Folaigh i gcuardach - I bhfolach i gcuardach - Taispeántar i gcuardach - Folaigh i bhfíseáin gaolmhara - I bhfolach i bhfíseáin ghaolmhara - Taispeántar i bhfíseáin ghaolmhara - - - Cnaipe micreafón ar snámh - Cnaipe micreafón folach - Taispeántar an cnaipe micreafón - - + Díchumasaigh modh comhthimpeallach sa scáile Díchumasaíodh mód comhthimpeallach Mód comhthimpeallach cumasaithe - + Folaigh cártaí faisnéise Tá cártaí faisnéise i bhfolach Taispeántar cártaí faisnéise - + Díchumasaigh beochan uimhreacha Níl uimhreacha rollta beoite Tá uimhreacha rollta beoite - + Folaigh an barra cuardaigh i seinnteoir físeáin Tá barra cuardaigh seinnteoir físe i bhfolach Taispeántar barra cuardaigh an t-imreoir físeán @@ -597,11 +593,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Tá barra cuardaigh mionsamhail i bhfolach Taispeántar barra cuardaigh mionsamhail - + + Shorts seinnteoir + Folaigh nó taispeáin comhpháirteanna san seinnteoir Shorts Folaigh Shorts i mbeatha baile - Tá shorts i mbeatha baile i bhfolach - Taispeántar gearrthóga i mbeatha baile + Tá Shorts i mbeatha baile i bhfolach + Taispeántar Shorts sa bheathú baile Folaigh Shorts i mbeatha síntiúis Tá Shorts i mbeatha síntiúis i bhfolach @@ -644,6 +642,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Folaigh an cnaipe scáileán glas Tá cnaipe an scáileáin glas i bhfolach Taispeántar cnaipe an scáileáin glas + Folaigh an cnaipe hashtag + Tá cnaipe hashtag i bhfolach + Taispeántar an cnaipe hashtag Folaigh moltaí cuardaigh Tá moltaí cuardaigh i bhfolach Taispeántar moltaí cuardaigh @@ -692,27 +693,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Tá barra nascleanúna i bhfolach Taispeántar barra nascleanúna - + Díchumasaigh scáileán deireadh físe molta Déanfar na físeáin mholta a dhíchumasú Taispeánfar físeáin mholta - + Folaigh stampa ama an fhíseáin Tá stampa ama i bhfolach Taispeántar stampa ama - + Folaigh painéil aníos imreoir Tá painéil aníos imreoirí i bhfolach Taispeántar painéil aníos imreoirí - + Trédhearcacht forleagtha an imreoir Luach trédhearcachta idir 0-100, áit a bhfuil 0 trédhearcach Caithfidh trédhearcacht forleagtha imreoirí a bheith idir 0-100 - + Ní dtaitníonn sé ar fáil go sealadach (API amuigh amach) Ní dtaitníonn sé ar fáil (stádas %d) @@ -756,17 +757,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Bhí teorainn ráta cliant ag teacht %d uair %d milleasoicind - + Cumasaigh barra cuardaigh leathan Tá barra cuardaigh leathan cumasaithe Tá barra cuardaigh leathan míchumasaithe - + + Cumasaigh mionsamhlacha ardchaighdeáin + Tá mionsamhlacha Seekbar ardchaighdeáin + Tá mionsamhlacha barra cuardaigh de chaighdeán meánach + Tá mionsamhlacha an bharra cuardaigh lánscáileáin ar ardchaighdeán + Tá mionsamhlacha an bharra cuardaigh lánscáileáin de chaighdeán meánach + Athchóireoidh sé seo mionsamhlacha ar shruthanna beo nach bhfuil mionsamhlacha ar an mbarra cuardaigh acu.\n\nÚsáidfidh mionsamhlacha an bharra cuardaigh atá ar an gcaighdeán céanna leis an bhfíseán reatha.\n\nIs fearr a oibríonn an ghné seo le cáilíocht físeáin 720p nó níos ísle agus nuair a úsáideann sé an-tapa nasc idirlín. Cuir sean-mionsamhlacha barra cuardaigh ar ais Beidh mionsamhlacha Seekbar le feiceáil os cionn an barra cuardaigh Beidh mionsamhlacha Seekbar le feiceáil ar an scáileán - + Cumasaigh SponsorBlock Is córas foinse slua-fhoinse é SponsorBlock chun codanna cráite de fhíseáin YouTube a scipeáil Dealramh @@ -947,7 +954,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Maidir Soláthraíonn an API SponsorBlock sonraí. Tapáil anseo chun níos mó a fhoghlaim agus íoslódálacha a fheiceáil d\'ardáin eile - + Leagan aip spoof Leagan spoofed Leagan gan bhfoláiste @@ -960,30 +967,45 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Athchóirigh luas leathan físe & roghchlár cáilíochta 18.09.39 - Athchóirigh cluaisín leabharlainne 17.41.37 - Athchóirigh sean-seilf seinmliostaí - 17.33.42 - Athchóirigh sean leagan amach Chomhéadain - + Socraigh leathanach tosaigh Réamhshocraithe + Brabhsáil cainéil Déan iniúchadh + Cluichíocht Stair + Leabharlann Físeáin a thaitin + Beo + Scannáin + Ceol Cuardaigh + Spóirt Síntiúis Ag treocht + Féach ar níos déanaí - + Díchumasaigh an t-imreoir Shorts atá ag tosú arís - Ní thosóidh an t-imreoir shorts ar thosú an aip + Ní thosóidh an t-imreoir Shorts ar thosú an aip Athosóidh an t-imreoir Shorts ar thosú an aip - + + Shorts Autoplay + Seinnfidh Shorts go huathoibríoch + Déanfaidh Shorts arís + Cluiche Cúlra Shorts Autoplay + Déanfar súgradh cúlra Shorts go huathoibríoch + Athdhéanfar súgradh cúlra Shorts + + Cumasaigh leagan amach na táibléad Tá leagan amach an táibléad cumasaithe Tá leagan amach an táibléad díchumasaithe Ní thaispeánann poist phobail ar leagan amach táibléad - + Miniplayer Athraigh stíl an imreora íoslaghdaithe san aip Cineál Miniplayer @@ -993,7 +1015,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Nua-aimseartha 1 Nua-Aimseartha 2 Nua-aimseartha 3 + Cumasaigh coirnéil chothromú + Déantar coirnéil a shlánú + Tá coirnéil cearnach + Cumasaigh sconna dúbailte agus pinch chun méid a athrú + Tá gníomh tapáil faoi dhó agus pinch chun méid a athrú cumasaithe\n\n• Tapáil faoi dhó chun méid mion-imreora a mhéadú\n• Tapáil faoi dhó arís chun an bunmhéid a aischur + Díchumasaíodh gníomh tapáil faoi dhó agus pinch chun méid a athrú + Cumasaigh tarraing agus scaoil + Cumasaítear tarraing agus scaoil\n\nIs féidir mion-imreoir a tharraingt go cúinne ar bith den scáileán + Tá tarraing agus scaoil díchumasaithe + Cumasaigh gotha ​​tarraingthe cothrománach + Gothaí tarraingthe cothrománach cumasaithe\n\nIs féidir mion-imreoir a tharraingt den scáileán ar chlé nó ar dheis + Díchumasaíodh an comhartha tarraingthe cothrománach + Folaigh cnaipe dúnta + Tá an cnaipe dúnta i bhfolach + Taispeántar an cnaipe dúnta Folaigh cnaipí leathnú agus dún + Tá cnaipí i bhfolach\n\nSwipe chun leathnú nó dúnadh Taispeántar cnaipí leathnaigh agus dún Folaigh fothéacsanna Tá fothéacsanna i bhfolach @@ -1001,28 +1039,32 @@ This is because Crowdin requires temporarily flattening this file and removing t Folaigh cnaipí scipeáil ar aghaidh agus ar ais Tá scipeanna ar aghaidh agus ar ais i bhfolach Taispeántar scipeáil ar aghaidh agus ar ais + Méid tosaigh + Tosaigh ar mhéid an scáileáin, i bpicteilíní + Caithfidh méid picteilíní a bheith idir %1$s agus %2$s Trédhearcacht forleagan Luach trédhearcachta idir 0-100, áit a bhfuil 0 trédhearcach Caithfidh trédhearcacht forleagtha mionaimreora a bheith idir 0-100 - + Cumasaigh scáileán luchtaithe Beidh cúlra grádáin ag an scáileán lódála Beidh cúlra láidir ag scáileán luchtaithe - + Cumasaigh dath barra cuardaigh saincheaptha Taispeántar dath barra cuardaigh saincheaptha Taispeántar dath barr cuardaigh bunaidh Dath barra cuardaigh saincheaptha Dath an bharra cuardaigh + Luach datha barra cuardaigh neamhbhailí - + Seachbhóthar srianta réigiún íomhá Ag baint úsáide as óstach íomhá yt4.ggpht.com Ag baint úsáide as óstach íomhá bunaidh\n\nAg cumasú seo is féidir íomhánna atá ar iarraidh a shocrú atá bac orthu - + Cluaisín Baile @@ -1054,7 +1096,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Níl DeArrow ar fáil go sealadach (cód stádais: %s) Níl DeArrow ar fáil go sealadach - + Taispeáin fógraí ReVanced Taispeántar fógraí ar thosú Ní thaispeántar fógraí ar thosú @@ -1062,47 +1104,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Theip ar nascadh le soláthraí fógraí Díbhunaigh - + Rabhadh Níl do stair faire á sábháil.<br><br>Is é is dóichí gur seachfhreastalaí fógraí DNS nó seachfhreastalaí líonra is cúis leis seo.<br><br> Chun é seo a réiteach, déan liosta bán <b>s.youtube.com</b> nó gach seachfhreastalaí DNS a mhúchadh. Ná taispeáin arís - + Cumasaigh uath-athdhéanta Tá uath-athdhéanta cumasaithe Tá uath-athdhéanta díchumasaithe - + Toisí feiste spoof D\'fhéadfaí toisí feiste a dhíghlasáil\n\nD\'fhéadfaí cáilíochtaí físe níos airde a dhíghlasáil ach d\'fhéadfadh go mbeadh stuttering athsheinm físe agat, saol ceallraí níos measa, agus fo-iars Toisí feiste nach ndéantar spoofed\n\nA chumasú seo is féidir cáilíochtaí físe níos airde a dhíghlasáil D\'fhéadfadh sé seo a bheith ina chúis le stuttering athsheinm físe, saol ceallraí níos measa, agus fo-iarmhairtí anaithnid. - + Socruithe GMScore Socruithe le haghaidh GMScore - + Atreoracha seachbhóthar URL Seachnaítear atreoruithe URL Ní chuirtear athsheoltaí URL - + Oscail naisc sa bhrabhsála Naisc a oscailt go seachtrach Naisc a oscailt san aip - + Bain paraiméadar ceist rianaithe Baintear paraiméadar ceisteanna rianaithe ó naisc Ní bhaintear paraiméadar fiosrúcháin rianaithe ó naisc - + Díchumasaigh súmáil haptics Tá Haptics díchumasaithe Tá Haptics cumasaithe - + Cáilíocht uathoibríoch Cuimhnigh athruithe ar cháilíocht Baineann athruithe cáilíochta le gach físeán @@ -1113,35 +1155,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Athraigh cáilíocht réamhshocraithe %1$s go dtí: %2$s - + Taispeáin cnaipe dialóg luais Taispeántar an cnaipe Ní thaispeántar an cnaipe - + + Roghchlár luas athsheinm saincheaptha + Taispeántar roghchlár luais saincheaptha + Ní thaispeántar roghchlár luais saincheaptha Luas athsheinm saincheaptha - Cuir nó athraigh na luasanna athsheinm atá ar fáil + Cuir leis nó athraigh na luasanna athsheinm saincheaptha Caithfidh luasanna saincheaptha a bheith níos lú ná %s. Úsáid luachanna réamhshocraithe. Luasanna athsheinm saincheaptha neamhí Luachanna réamhshocraithe a úsáid. - + Cuimhnigh athruithe ar luas athsheinm Baineann athruithe luais athsheinm le gach físeáin Ní bhaineann athruithe luas athsheinm ach leis an bhfíseán reatha Luas athsheinm réamhshocraithe Athraigh luas réamhshocraithe go: %s - + Athchóirigh sean-roghchlár cáilíochta físeáin Taispeántar sean-roghchlár cáilíochta físeáin Ní thaispeántar sean-roghchlár cáilíochta físeáin - + Cumasaigh sleamhnán a lorg Tá sleamhnán le lorg cumasaithe Níl sleamhnán le lorg cumasaithe - + Sruthanna físeán spoof Spoof na sruthanna físeáin cliant chun saincheisteanna athsheinm a chosc Sruthanna físeán spoof @@ -1159,17 +1204,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Fo-iarsmaí spoofing Android VR • Tá roghchlár rian fuaime in easnamh\n• Níl an toirt cobhsaí ar fáil - - - - + Cuir bac ar fógraí fuaime Cuirtear bac ar fhógraí fuaime Déantar fógraí fuaime díbhocáilte - + Níl %s ar fáil. Is féidir fógraí a thaispeáint Bain triail as aistriú chuig seirbhís bloc fógraí eile i socruithe. Chuir freastalaí %s earráid ar ais. Is féidir fógraí a thaispeáint Bain triail as aistriú chuig seirbhís bloc fógraí eile i socruithe. Bloc ar fhógraí físe leabaithe @@ -1177,30 +1219,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Proxy lonrúil Seachfhreastalaí PurpleAdBlock - + Bloc ar fhógraí físe Cuirtear bac ar fhógraí físe Déantar fógraí físe a dhíbhlocáil - + teachtaireacht scriosta Taispeáin teachtaireachtaí scriosta Ná taispeáin teachtaireachtaí scriosta Folaigh teachtaireachtaí scriosta taobh thiar de spoiler Taispeáin teachtaireachtaí scriosta mar théacs trasnaithe - + Tóg Pointí Cainte go huathoibríoch Éilítear Pointí Cainéal go huathoibríoch Ní éilítear Pointí Cainéal go huathoibríoch - + Cumasaigh modh dífhabhtú Twitch Tá modh dífhabhtaithe Twitch cumasaithe (ní mholtar) Tá modh dífhabhtaithe Twitch díchumasaithe - + Socruithe ReVanced Fógraí Socruithe blocála fógraí diff --git a/patches/src/main/resources/addresources/values-gl-rES/strings.xml b/patches/src/main/resources/addresources/values-gl-rES/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-gl-rES/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/addresources/values-gu-rIN/strings.xml b/patches/src/main/resources/addresources/values-gu-rIN/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-gu-rIN/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/addresources/values-hi-rIN/strings.xml b/patches/src/main/resources/addresources/values-hi-rIN/strings.xml similarity index 71% rename from src/main/resources/addresources/values-hi-rIN/strings.xml rename to patches/src/main/resources/addresources/values-hi-rIN/strings.xml index 93a6ba1f3..881565b0e 100644 --- a/src/main/resources/addresources/values-hi-rIN/strings.xml +++ b/patches/src/main/resources/addresources/values-hi-rIN/strings.xml @@ -32,23 +32,25 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + रीसेट करें - + - + विवरण - + - + + + @@ -64,30 +66,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -97,23 +99,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -124,29 +120,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -154,26 +141,26 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + विवरण - + - + - + स्वरूप @@ -182,85 +169,84 @@ This is because Crowdin requires temporarily flattening this file and removing t रीसेट करें विवरण - + - + - + - + - + - + - + - + - + + + - + बंद करें - + चेतावनी - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + निष्क्रिय - + - + - + - + - + diff --git a/src/main/resources/addresources/values-hr-rHR/strings.xml b/patches/src/main/resources/addresources/values-hr-rHR/strings.xml similarity index 70% rename from src/main/resources/addresources/values-hr-rHR/strings.xml rename to patches/src/main/resources/addresources/values-hr-rHR/strings.xml index 30ad42f16..b4db524e7 100644 --- a/src/main/resources/addresources/values-hr-rHR/strings.xml +++ b/patches/src/main/resources/addresources/values-hr-rHR/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,106 +139,105 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + Upozorenje - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/src/main/resources/addresources/values-hu-rHU/strings.xml b/patches/src/main/resources/addresources/values-hu-rHU/strings.xml similarity index 93% rename from src/main/resources/addresources/values-hu-rHU/strings.xml rename to patches/src/main/resources/addresources/values-hu-rHU/strings.xml index cc414016f..578553d1a 100644 --- a/src/main/resources/addresources/values-hu-rHU/strings.xml +++ b/patches/src/main/resources/addresources/values-hu-rHU/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Az ellenőrzések sikertelenek Hivatalos webhelyet megnyitása Mellőzés @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t %s napja patchelve Az APK felépítési dátuma sérült - + Szeretné folytatni? Visszaállítás Frissítés és újraindítás @@ -62,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Hivatalos linkek Támogatás - + MicroG GmsCore nincs telepítve. Telepítse. Művelet szükséges @@ -73,7 +73,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Rólunk Hirdetések Alternatív indexképek @@ -85,7 +85,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Egyéb Videó - + + A Shorts háttérben történő lejátszásának letiltása + A Shorts háttérben történő lejátszása le van tiltva + A Shorts háttérben történő lejátszása engedélyezve van + + Hibakeresés Hibakeresési beállítások engedélyezése vagy letiltása Hibakeresési naplózás @@ -102,13 +107,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Nem jelenik meg üzenet, ha hiba történik A hibaüzenetek kikapcsolása elrejti az összes ReVanced hibaértesítést.\n\nNem kap értesítést semmilyen váratlan eseményről. - + Like / feliratkozás gomb ragyogásának kikapcsolása Like / feliratkozás gomb nem fog ragyogni mikor megemlítik Like / feliratkozás gomb ragyogni fog mikor megemlítik - Szürke elválasztó elrejtése - A szürke elválasztók el vannak rejtve - A szürke elválasztók láthatóak + Album kártyák elrejtése + Az album kártyák el vannak rejtve + Az album kártyák láthatóak + Közösségi finanszírozási doboz elrejtése + A közösségi finanszírozási doboz el van rejtve + A közösségi finanszírozási doboz megjelenik + Lebegő mikrofon gomb elrejtése + A mikrofon gomb elrejtve + A mikrofon gomb látható Csatorna vízjel elrejtése A vízjel el van rejtve Vízjel látható @@ -153,9 +164,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Kiterjeszthető vágások elrejtése a videók alatt A kiterjeszthető vágások el vannak rejtve A kiterjeszthető vágások megjelennek - A videóminőség menü láblécének elrejtése - A videóminőség menü lábléce elrejtve - A videóminőség menü lábléce látható Közösségi posztok elrejtése A közösségi posztok el vannak rejtve A közösségi posztok meg fognak jelenni @@ -230,6 +238,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Az átirat rész megjelenik Videóleírás A videóleírás komponenseinek elrejtése vagy megjelenítése + Szűrősáv + Szűrősáv elrejtése vagy megjelenítése a feedekben, a keresésben és a kapcsolódó videók között + Elrejtés a feedekben + Elrejtve a feedekben + Megjelenítés a feedekben + Elrejtés a keresésben + Elrejtve a keresésben + Megjelenik a keresésben + Elrejtés a kapcsolódó videók között + Elrejtve a kapcsolódó videók között + Megjelenik a kapcsolódó videók között + Megjegyzések + Megjegyzések rész elrejtése vagy megjelenítése + A „Tagok megjegyzései” fejléc elrejtése + A „tagok megjegyzései” fejléc el van rejtve + Megjelenik a „Tagok megjegyzései” fejléc + A megjegyzések szekció elrejtése + A megjegyzések szakasz el van rejtve + Megjelenik a megjegyzések rész + A „Rövid létrehozása” gomb elrejtése + A „Short létrehozása” gomb el van rejtve + Megjelenik a „Short létrehozása” gomb + Megjegyzés előnézet elrejtése + A megjegyzés előnézet el van rejtve + A megjegyzés előnézet megjelenik + Köszönet gomb elrejtése + A köszönet gomb el van rejtve + A köszönet gomb látható + Időbélyeg és az emoji gombok elrejtése + Az időbélyeg és az emoji gombok el vannak rejtve + Megjelennek az időbélyeg és az emoji gombok YouTube Doodles elrejtése A Doodles Keresősáv el van rejtve @@ -271,7 +310,7 @@ This is because Crowdin requires temporarily flattening this file and removing t A kulcsszó túl rövid, és idézőjeleket igényel: %s A kulcsszó elrejti az összes videót: %s - + Általános hirdetések elrejtése Az általános hirdetések el vannak rejtve Az általános hirdetések megjelennek @@ -290,6 +329,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Szalagkép elrejtése a termékek megtekintéséhez A szalagkép rejtett A szalagkép megjelenik + A lejátszó bevásárlópolcának elrejtése + A bevásárlópolc el van rejtve + Megjelenik a bevásárlópolc 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 @@ -306,17 +348,17 @@ This is because Crowdin requires temporarily flattening this file and removing t A teljes képernyős hirdetések elrejtése csak régebbi eszközökön működik - + YouTube Premium promóciók elrejtése A YouTube Premium promóciók a videólejátszó alatt el vannak rejtve A YouTube Premium promóciók a videólejátszó alatt láthatók - + Videó hirdetések elrejtése A videó hirdetések el vannak rejtve A videó hirdetések láthatók - + Az URL a vágólapra másolva Az URL időbélyeggel a vágólapra másolva A videó URL másolása gomb megjelenítése @@ -326,13 +368,13 @@ This is because Crowdin requires temporarily flattening this file and removing t A gomb megjelenik. Koppintson a videó URL másolásához időbélyeggel. Koppintson és tartsa lenyomva az időbélyeg nélküli másoláshoz A gomb nem látható - + Távolítsa el a nézői diszkréciós párbeszédpanelt A párbeszédpanel el lesz rejtve A párbeszédpanel megjelenik Ez nem kerüli meg a korhatárt, csak automatikusan elfogadja. - + Külső letöltések Beállítások külső letöltő használatához Külső letöltés gomb megjelenítése @@ -346,17 +388,17 @@ This is because Crowdin requires temporarily flattening this file and removing t A telepített külső letöltő alkalmazás csomagneve, például NewPipe vagy Seal %s nincs telepítve. Kérjük telepítse. - + Pontos keresési kézmozdulat letiltása A kézmozdulat letiltva A kézmozdulat engedélyezve - + Érintés engedélyezése a keresősávon A keresősáv érintése engedélyezve van A keresősáv érintése le van tiltva - + Fényerő kézmozdulat engedélyezése A fényerő vezérlése kézmozdulattal engedélyezve A fényerő vezérlése kézmozdulattal letiltva @@ -385,12 +427,12 @@ This is because Crowdin requires temporarily flattening this file and removing t A csúsztatás küszöbértéke A csúsztatáshoz szükséges küszöbérték - + Automatikus feliratok letiltása Az automatikus feliratok le vannak tiltva Az automatikus feliratok engedélyezve vannak - + Művelet gombok Gombok megjelenítése vagy elrejtése a videók alatt Tetszik és nem tetszik elrejtése @@ -426,23 +468,7 @@ This is because Crowdin requires temporarily flattening this file and removing t A mentés gomb el van rejtve A mentés gomb látható - - Automatikus lejátszás gomb elrejtése - Az automatikus lejátszás gomb el van rejtve - Az automatikus lejátszás gomb látható - - - - Feliratok gomb elrejtése - A feliratok gomb el van rejtve - A feliratok gomb látható - - - Átküldés gomb elrejtése - Az átküldés gomb rejtve van - Az átküldés gomb látható - - + Navigációs gombok Gombok elrejtése vagy módosítása a navigációs sávon @@ -469,7 +495,7 @@ This is because Crowdin requires temporarily flattening this file and removing t A címkék el vannak rejtve A címkék megjelennek - + Előugró menü A lejátszó előugró menüpontjainak elrejtése vagy megjelenítése @@ -480,6 +506,10 @@ This is because Crowdin requires temporarily flattening this file and removing t További beállítások elrejtése A további beállítások menü el van rejtve A további beállítások menü látható + + Elalvási időzítő elrejtése + Az elalváskapcsoló menüje el van rejtve + Megjelenik az elalvásidőzítő menü Videó folyamatos ismétlése menü elrejtése A videó folyamatos ismétlése menü el van rejtve @@ -488,6 +518,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Mozifilmes világítás elrejtése A mozifilmes világítás menü el van rejtve A mozifilmes világítás menü látható + Stabil hangerő elrejtése + Megjelenik a Stabil hangerő menü + A Stabil hangerő menü el van rejtve Súgó és visszajelzés elrejtése A súgó és visszajelzés menü elrejtve @@ -513,83 +546,46 @@ This is because Crowdin requires temporarily flattening this file and removing t \"Megtekintés VR-módban\" elrejtése A megtekintés VR-módban menü el van rejtve A „Megtekintés VR-módban” menü megjelenik + A videóminőség menü láblécének elrejtése + A videóminőség menü lábléce el van rejtve + Megjelenik a videóminőség menü lábléce - - Az előző és következő videó gombok elrejtése - A gombok elrejtve - A gombok megjelennek + + Az előző és következő videó gombok elrejtése + A gombok elrejtve + A gombok megjelennek + Átküldés gomb elrejtése + Az átküldés gomb rejtve van + Az átküldés gomb látható + + Feliratok gomb elrejtése + A feliratok gomb el van rejtve + A feliratok gomb látható + Automatikus lejátszás gomb elrejtése + Az automatikus lejátszás gomb el van rejtve + Az automatikus lejátszás gomb látható - - Album kártyák elrejtése - Az album kártyák el vannak rejtve - Az album kártyák láthatóak - - - Megjegyzések - Megjegyzések rész elrejtése vagy megjelenítése - A „Tagok megjegyzései” fejléc elrejtése - A „tagok megjegyzései” fejléc el van rejtve - Megjelenik a „Tagok megjegyzései” fejléc - A megjegyzések szekció elrejtése - A megjegyzések szakasz el van rejtve - Megjelenik a megjegyzések rész - A „Rövid létrehozása” gomb elrejtése - A „Short létrehozása” gomb el van rejtve - Megjelenik a „Short létrehozása” gomb - Megjegyzés előnézet elrejtése - A megjegyzés előnézet el van rejtve - A megjegyzés előnézet megjelenik - Köszönet gomb elrejtése - A köszönet gomb el van rejtve - A köszönet gomb látható - Időbélyeg és az emoji gombok elrejtése - Az időbélyeg és az emoji gombok el vannak rejtve - Megjelennek az időbélyeg és az emoji gombok - - - Közösségi finanszírozási doboz elrejtése - A közösségi finanszírozási doboz el van rejtve - A közösségi finanszírozási doboz megjelenik - - + Záróképernyő kártyák elrejtése A záróképernyő kártyák el vannak rejtve A záróképernyő kártyák megjelennek - - Szűrősáv - Szűrősáv elrejtése vagy megjelenítése a feedekben, a keresésben és a kapcsolódó videók között - Elrejtés a feedekben - Elrejtve a feedekben - Megjelenítés a feedekben - Elrejtés a keresésben - Elrejtve a keresésben - Megjelenik a keresésben - Elrejtés a kapcsolódó videók között - Elrejtve a kapcsolódó videók között - Megjelenik a kapcsolódó videók között - - - Lebegő mikrofon gomb elrejtése - A mikrofon gomb elrejtve - A mikrofon gomb látható - - + Mozifilmes világítás letiltása teljes képernyős módban Mozifilmes világítás kikapcsolva Mozifilmes világítás engedélyezve - + Infó kártyák elrejtése Az info kártyák el vannak rejtve Az info kártyák megjelennek - + Gördülőszám-animációk letiltása A gördülő számok nem animáltak A gördülő számok animálva vannak - + Folyamatsáv elrejtése a videólejátszóban A videólejátszó folyamatsávja el van rejtve A videólejátszó folyamatsávja megjelenik @@ -597,7 +593,9 @@ This is because Crowdin requires temporarily flattening this file and removing t A minilejátszó folyamatsávja el van rejtve A minilejátszó folyamatsávja megjelenik - + + Shorts lejátszó + Összetevők elrejtése vagy megjelenítése a Shorts lejátszóban Shorts elrejtése a Kezdőlap feedben A Shortsok elrejtve a Kezdőlap feedben @@ -695,27 +693,27 @@ This is because Crowdin requires temporarily flattening this file and removing t A navigációs sáv el van rejtve A navigációs sáv megjelenik - + Videójavaslatok letiltása a záróképernyőn A videójavaslatok le lesznek tiltva A videójavaslatok megjelennek - + Videó időbélyegzőjének elrejtése Az időbélyegző elrejtve Az időbélyegző megjelenik - + Lejátszó előugró paneleinek elrejtése A lejátszó előugró panelei el vannak rejtve A lejátszó előugró panelei megjelennek - + Lejátszó fedőrétegének átlátszatlansága Az átlátszatlanság értéke 0 és 100 között van, ahol a 0 átlátszó A lejátszó fedvény átlátszatlanságának 0 és 100 között kell lennie - + A nem tetszik funkció átmenetileg nem elérhető A nem tetszik funkció nem elérhető (állapot: %d) @@ -759,17 +757,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Kliens korlátozás %d alkalommal történt %d ezredmásodperc - + Széles keresősáv bekapcsolása Széles keresősáv bekapcsolva Széles keresősáv kikapcsolva - + + Jó minőségű miniatűrök engedélyezése + A keresősáv bélyegképei kiváló minőségűek + A keresősáv bélyegképei közepes minőségűek + A teljes képernyős keresősáv bélyegképei kiváló minőségűek + A teljes képernyős keresősáv bélyegképei közepes minőségűek + Ezzel az élő közvetítések miniatűrjeit is visszaállítja, amelyek nem rendelkeznek keresősáv-bélyegképekkel.\n\nA keresősáv bélyegképei ugyanazt a minőséget fogják használni, mint az aktuális videó.\n\nEz a funkció 720p vagy annál alacsonyabb videóminőség esetén működik a legjobban, és nagyon gyors videót használ. internet kapcsolat. Régi keresősáv bélyegképek visszaállítása A keresősáv bélyegképei megjelennek a keresősáv felett A keresősáv bélyegképei megjelennek a teljes képernyőn - + SponsorBlock bekapcsolása A SponsorBlock egy közösségi rendszer a zavaró részek kihagyására a YouTube videókon Megjelenés @@ -950,7 +954,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Rólunk Az adatokat a SponsorBlock API biztosítja. Koppintson ide, ha többet szeretne megtudni és megtekintené a letöltéseket más platformokra - + Alkalmazásverzió hamisítása A verzió hamisítva A verzió nincs hamisítva @@ -963,9 +967,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Széles videósebesség és minőség menü visszaállítása 18.09.39 - Könyvtár lap visszaállítása 17.41.37 - Régi lejátszási lista polc visszállítása - 17.33.42 - Régi felhasználói felület visszaállítása - + Kezdőlap beállítása Alapértelmezett Csatornák böngészése @@ -983,18 +986,26 @@ This is because Crowdin requires temporarily flattening this file and removing t Felkapott Megnézem később - + A Shorts lejátszás folytatásának kikapcsolása A Shorts lejátszás nem indul el az alkalmazás indításakor A Shorts lejátszás folytatódik az alkalmazás indításakor - + + Shorts automatikus lejátszása + Shorts automatikusan elindul + Shorts ismétlődik + Shorts automatikus lejátszása a háttérben + Shorts automatikusan elindul a háttérben + Shorts háttérben történő lejátszása megismétlődik + + Táblagépes elrendezés engedélyezése Táblagépes elrendezés engedélyezve Táblagépes elrendezés letiltva A közösségi posztok nem jelennek meg táblagépes elrendezésben - + Minilejátszó Módosítsa az alkalmazáson belüli kisméretű lejátszó stílusát Minilejátszó típus @@ -1013,6 +1024,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Fogd és vidd engedélyezése A fogd és vidd be van kapcsolva\n\nA minilejátszó a képernyő bármely sarkába húzható A Fogd és vidd letiltva + Vízszintes húzási kézmozdulat engedélyezése + A vízszintes húzási kézmozdulat engedélyezve\n\nA minilejátszó a képernyőről balra vagy jobbra húzható + A vízszintes húzómozdulat letiltva Bezárás gomb elrejtése A Bezárás gomb el van rejtve A Bezárás gomb látható @@ -1032,12 +1046,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Az átlátszatlanság értéke 0 és 100 között van, ahol a 0 átlátszó A minilejátszó fedvény átlátszatlanságának 0 és 100 között kell lennie - + Színátmenetes betöltési képernyő engedélyezése A betöltési képernyő színátmenetes hatterű lesz A betöltési képernyő egyszínű hátterű lesz - + Egyéni keresősáv szín engedélyezése Az egyéni keresősáv szín megjelenik Az egyéni keresősáv szín nem jelenik meg @@ -1045,12 +1059,12 @@ This is because Crowdin requires temporarily flattening this file and removing t A keresősáv színe Érvénytelen keresősáv színértéke - + Területi kép-korlátozások megkerülése A yt4.ggpht.com képtár használata Az eredeti képgazda használata\n\nEnnek engedélyezése javíthatja a hiányzó képeket, amelyek bizonyos régiókban le vannak tiltva - + Kezdőlap @@ -1082,7 +1096,7 @@ This is because Crowdin requires temporarily flattening this file and removing t A DeArrow átmenetileg nem elérhető (állapot: %s) A DeArrow átmenetileg nem elérhető - + ReVanced közlemények megjelenítése A közlemények megjelenítve indításkor Nem jelennek meg közlemények az indításkor @@ -1090,47 +1104,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Nem sikerült csatlakozni a közlemény szolgáltatóhoz Elvetés - + Figyelmeztetés A megtekintési előzmények mentése nem történik meg.<br><br>Ezt valószínűleg egy DNS-hirdetésblokkoló vagy hálózati proxy okozza.<br><br>A probléma megoldásához vegye fel az engedélyezőlistára az <b>s.youtube.com</b> domaint vagy kapcsolja ki az összes DNS-blokkolót és proxyt. Ne jelenjen meg többet - + Automatikus ismétlés engedélyezése Az automatikus ismétlés be van kapcsolva Az automatikus ismétlés ki van kapcsolva - + Eszközméret hamisítása Az eszköz méretei hamisítottak\n\nMagasabb videóminőség lehet elérhető, de tapasztalhat akadást lejátszás közben, rosszabb akkuidőt és egyéb, ismeretlen hatásokat Az eszköz méretei nincsenek hamisítva\n\nAz engedélyezéssel magasabb videóminőség érhető el Ennek engedélyezése a videólejátszás akadozását, rosszabb akkuidőt és ismeretlen hatásokat okozhat. - + GmsCore beállítások A GmsCore beállításai - + URL átirányítások kikerülése URL átirányítások kikerülve Az URL átirányítások nincsenek kikerülve - + Hivatkozások megnyitása a böngészőben Hivatkozások külső megnyitása Hivatkozások megnyitása az alkalmazásban - + Nyomkövetési lekérdezési paraméter eltávolítása A nyomkövetési lekérdezési paraméter eltávolítva a linkekből A nyomkövetési lekérdezési paraméter nincs eltávolítva a linkekből - + Haptikus zoom letiltása A haptikus zoom letiltva A haptikus zoom engedélyezve - + Automatikus felbontás Felbontás változtatások mentése Felbontás változtatások alkalmazása az összes videóra @@ -1141,35 +1155,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi A(z) %1$s alapértelmezett felbontása erre módosult: %2$s - + Sebesség párbeszédpanel megjelenítése A gomb megjelenik A gomb nem látható - + + Egyedi lejátszási sebesség menü + Megjelenik az egyéni sebesség menü + Az egyéni sebesség menü nem jelenik meg Egyedi lejátszási sebesség - Az elérhető lejátszási sebességek módosítása vagy hozzáadás + Egyéni lejátszási sebesség hozzáadása vagy módosítása Ennek kevesebbnek kell lenniük, mint %s. Alap értékek használata. Érvénytelen sebesség. Az alap értékek használata. - + Lejátszási sebesség módosításainak megjegyzése A lejátszási sebesség módosítása minden videóra érvényes A lejátszási sebesség módosítása csak a jelenlegi videóra érvényes Alapértelmezett lejátszási sebesség Alapértelmezett sebesség módosítva: %s - + Régi videóminőség menü visszaállítása A régi videóminőség menü jelenik meg A régi videóminőség menü nem jelenik meg - + Csúsztatás engedélyezése a kereséshez A csúsztatás a kereséshez engedélyezve van A csúsztatás a kereséshez nincs engedélyezve - + Hamis videó stream Hamisítsa meg az ügyfél videó streamet a lejátszási problémák elkerülése érdekében Hamis videó stream @@ -1187,17 +1204,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Android VR-hamisítási mellékhatások • Hiányzik a hangsáv menü\n• A stabil hangerő nem érhető el - - - - + Audio hirdetések letiltása Az audiohirdetések le vannak tiltva Az audiohirdetések letiltása fel van oldva - + A(z) %s nem érhető el. A reklámok megjelenhetnek. Próbáljon egy másik reklámblokkolóra váltani. A(z) %s szerver hibát jelzett. A reklámok megjelenhetnek. Próbáljon egy másik reklámblokkolóra váltani. Beágyazott videóhirdetések blokkolása @@ -1205,30 +1219,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Luminous proxy PurpleAdBlock proxy - + Videós hirdetések blokkolása Videós hirdetések blokkolva A videós hirdetések nincsenek blokkolva - + üzenet törölve Törölt üzenetek megjelenítése Ne mutassa a törölt üzeneteket A törölt üzenetek elrejtése egy spoiler mögé Törölt üzenetek megjelenítése áthúzott szövegként - + Csatornapontok automatikus gyűjtése A csatornapontok automatikusan begyűjtődnek A csatornapontok nem gyűjtődnek be automatikusan - + Twitch hibakeresési mód bekapcsolása Twitch hibakeresési mód bekapcsolva (nem ajánlott) Twitch hibakeresési mód kikapcsolva - + ReVanced beállítások Hirdetések Hirdetés blokkolás beállításai diff --git a/src/main/resources/addresources/values-hy-rAM/strings.xml b/patches/src/main/resources/addresources/values-hy-rAM/strings.xml similarity index 70% rename from src/main/resources/addresources/values-hy-rAM/strings.xml rename to patches/src/main/resources/addresources/values-hy-rAM/strings.xml index 89d212bf5..c399bf883 100644 --- a/src/main/resources/addresources/values-hy-rAM/strings.xml +++ b/patches/src/main/resources/addresources/values-hy-rAM/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,106 +139,105 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + Զգուշացում - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/src/main/resources/addresources/values-in-rID/strings.xml b/patches/src/main/resources/addresources/values-in-rID/strings.xml similarity index 80% rename from src/main/resources/addresources/values-in-rID/strings.xml rename to patches/src/main/resources/addresources/values-in-rID/strings.xml index df3f8941f..9517467df 100644 --- a/src/main/resources/addresources/values-in-rID/strings.xml +++ b/patches/src/main/resources/addresources/values-in-rID/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Pemeriksaan gagal Buka situs resminya Abaikan @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Dipatch %s hari yang lalu Tanggal pembuatan APK rusak - + Apakah Anda ingin melanjutkan? Setel ulang Segarkan dan mulai ulang @@ -58,11 +58,11 @@ This is because Crowdin requires temporarily flattening this file and removing t Anda menggunakan ReVanced Patches versi <i>%s</i> Catatan - Versi ini prarilis dan kemungkinan akan ada masalah tak terduga + Versi ini adalah pra-rilis dan Anda mungkin mengalami masalah yang tidak terduga Tautan resmi Donasi - + MicroG GmsCore belum dipasang. Pasang dulu. Tindakan diperlukan @@ -73,11 +73,11 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Tentang Iklan Thumbnail alternatif - Feed + Umpan Pemutar Layout umum Seekbar @@ -85,10 +85,15 @@ This is because Crowdin requires temporarily flattening this file and removing t Lainnya Video - + + Nonaktifkan pemutaran Shorts di latar belakang + Pemutaran Shorts di latar belakang dinonaktifkan + Pemutaran Shorts di latar belakang diaktifkan + + Debugging - Menyalakan atau mematikan opsi debugging - Catatan debug + Mengaktifkan atau menonaktifkan pilihan debugging + Pencatatan debug Log debug diaktifkan Log debug dinonaktifkan Buffer protokol log @@ -97,27 +102,33 @@ This is because Crowdin requires temporarily flattening this file and removing t Jejak log stack Log debug menyertakan jejak stack Log debug tidak menyertakan jejak stack - Tampilkan pesan timbul error di ReVanced + Tampilkan pesan timbul pada kesalahan ReVanced Pesan timbul ditampilkan jika terjadi kesalahan Pesan timbul tidak ditampilkan jika terjadi kesalahan - Menonaktifkan pemberitahuan kesalahan akan menyembunyikan semua notifikasi kesalahan ReVanced.\n\nAnda tidak akan diberitahu tentang kejadian yang tidak terduga. + Menonaktifkan pesan timbul kesalahan akan menyembunyikan semua notifikasi kesalahan ReVanced.\n\nAnda tidak akan diberitahu tentang kejadian yang tidak terduga. - + Nonaktifkan kilau tombol suka / langganan Tombol suka dan langganan tidak akan berkilau saat ditekan Tombol suka dan langganan akan berkilau saat ditekan - Sembunyikan pemisah abu-abu - Pemisah abu-abu disembunyikan - Pemisah abu-abu ditampilkan + Sembunyikan kartu album + Kartu album disembunyikan + Kartu album ditampilkan + Sembunyikan kotak penggalangan dana + Kotak penggalangan dana disembunyikan + Kotak penggalangan dana ditampilkan + Sembunyikan tombol mikrofon mengambang + Tombol mikrofon disembunyikan + Tombol mikrofon ditampilkan Sembunyikan tanda air saluran Tanda air disembunyikan Tanda air ditampilkan - Sembunyikan rak mendatar - Rak disembunyikan seperti:\n• Berita terkini\n• Lanjut menonton\n• Jelajahi saluran lainnya\n• Belanja\n• Tonton lagi - Rak ditampilkan + Sembunyikan rak-rak mendatar + Rak-rak yang disembunyikan seperti:\n• Berita terkini\n• Lanjut menonton\n• Jelajahi saluran lainnya\n• Belanja\n• Tonton lagi + Rak-rak ditampilkan - Sembunyikan \'Gabung\' + Sembunyikan tombol \'Gabung\' Tombol disembunyikan Tombol ditampilkan @@ -135,37 +146,34 @@ This is because Crowdin requires temporarily flattening this file and removing t Anjuran ditampilkan - Sembunyikan \'Tampilkan Lebih\' + Sembunyikan tombol \'Tampilkan selengkapnya\' Tombol disembunyikan Tombol ditampilkan - Sembunyikan waktu reaksi - Waktu reaksi disembunyikan - Tampilkan Waktu Reaksi + Sembunyikan reaksi terjadwal + Reaksi terjadwal disembunyikan + Reaksi terjadwal ditampilkan Sembunyikan judul rak hasil pencarian Judul rak disembunyikan Judul rak ditampilkan - Sembunyikan Panduan Saluran + Sembunyikan panduan saluran Panduan saluran disembunyikan Panduan saluran ditampilkan - Sembunyikan rak chip - Rak opsi deret disembunyikan - Rak chip ditampilkan + Sembunyikan kepingan rak + Kepingan rak disembunyikan + Kepingan rak ditampilkan Sembunyikan kepingan deret di bawah video Kepingan deret disembunyikan Kepingan yang dapat diperluas ditampilkan - Sembunyikan footer menu kualitas video - Footer menu kualitas video disembunyikan - Footer menu kualitas video ditampilkan Sembunyikan postingan komunitas Postingan komunitas disembunyikan Postingan komunitas ditampilkan Sembunyikan spanduk ringkas Spanduk ringkas disembunyikan - Banner padat ditampilkan + Spanduk ringkas ditampilkan Sembunyikan bagian film Bagian film disembunyikan Bagian film ditampilkan - Sembunyikan umpan survei + Sembunyikan survei umpan balik Survei umpan balik disembunyikan Survei umpan balik ditampilkan Sembunyikan pedoman komunitas @@ -173,7 +181,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Pedoman komunitas ditampilkan Sembunyikan pedoman komunitas pelanggan Pedoman komunitas pelanggan disembunyikan - Pedoman komunitas untuk para pelanggan ditampilkan + Pedoman komunitas pelanggan ditampilkan Sembunyikan rak anggota saluran Rak anggota saluran disembunyikan Rak anggota saluran ditampilkan @@ -190,8 +198,8 @@ This is because Crowdin requires temporarily flattening this file and removing t Bilah saluran disembunyikan Bilah saluran ditampilkan Sembunyikan konten tonton - Konten tonton disembunyikan - Yang dapat diputar ditunjukkan + Permainan disembunyikan + Permainan ditampilkan Sembunyikan tindakan cepat di layar penuh Tindakan cepat disembunyikan Tindakan cepat ditampilkan @@ -211,39 +219,70 @@ This is because Crowdin requires temporarily flattening this file and removing t Kartu artis disembunyikan Kartu artis ditampilkan Sembunyikan bagian atribut - \'Tempat menonjol\', \'Permainan\', dan \'Musik\' disembunyikan - \'Tempat menonjol\', \'Permainan\', dan \'Musik\' ditampilkan + \'Tempat unggulan\', bagian Permainan dan Musik disembunyikan + \'Tempat unggulan\', bagian Permainan dan Musik ditampilkan Sembunyikan bagian Bab - Bagian Bab sudah disembunyikan - Bagian Bab sudah ditampilkan + Bagian Bab disembunyikan + Bagian Bab ditampilkan Sembunyikan bagian \'Jelajahi podcast\' Bagian \'Jelajahi podcast\' disembunyikan Bagian \'Jelajahi podcast\' ditampilkan Sembunyikan kartu info - Kartu info sudah disembunyikan - Bagian kartu info sudah ditampilkan - Sembunyikan bagian \'Konsep kunci\' - Bagian \'Konsep kunci\' disembunyikan - Bagian \'Konsep kunci\' ditampilkan + Bagian kartu info disembunyikan + Bagian kartu info ditampilkan + Sembunyikan bagian \'Konsep Utama\' + Bagian \'Konsep Utama\' disembunyikan + Bagian \'Konsep Utama\' ditampilkan Sembunyikan bagian transkrip - Bagian transkrip sudah disembunyikan - Bagian transkrip sudah ditampilkan + Bagian transkrip disembunyikan + Bagian transkrip ditampilkan Keterangan video - Sembunyi/tampilkan komponen keterangan video + Sembunyikan atau tampilkan komponen keterangan video + Bilah saring + Sembunyikan atau tampilkan bilah saring di bagian umpan, pencarian, dan video terkait + Sembunyikan di bagian umpan + Disembunyikan di bagian umpan + Tampilkan di bagian umpan + Sembunyikan di pencarian + Disembunyikan di pencarian + Ditampilkan di pencarian + Sembunyikan di video terkait + Disembunyikan di video terkait + Ditampilkan di video terkait + Komentar + Sembunyikan atau tampilkan komponen bagian komentar + Sembunyikan Header \'Komentar oleh anggota\' + Header \'Komentar oleh anggota\' disembunyikan + Header \'Komentar oleh anggota\' disembunyikan + Sembunyikan bagian komentar + Bagian komentar disembunyikan + Bagian komentar ditampilkan + Sembunyikan tombol \'Buat Short\' + Tombol \'Buat Short\' disembunyikan + Tombol \'Buat Short\' ditampilkan + Sembunyikan pratinjau komentar + Pratinjau komentar disembunyikan + Pratinjau komentar ditampilkan + Sembunyikan tombol terima kasih + Tombol terima kasih disembunyikan + Tombol terima kasih ditampilkan + Sembunyikan timestamp dan tombol emoji + Tombol timestamp dan emoji disembunyikan + Tombol timestamp dan emoji ditampilkan Sembunyikan YouTube Doodles Bilah pencarian Doodle disembunyikan Bilah pencarian Doodle ditampilkan YouTube Doodle muncul beberapa hari setiap tahun.\n\nJika Doodle saat ini ditampilkan di wilayah Anda dan pengaturan sembunyikan ini aktif, maka bilah filter di bawah bilah pencarian juga akan disembunyikan. - Penyaring kustom + Penyaring khusus Sembunyikan komponen menggunakan penyaring khusus Aktifkan penyaring khusus Penyaring khusus diaktifkan - Penyaring khusus dimatikan + Penyaring khusus dinonaktifkan Penyaring khusus - Daftar untaian karakter untuk disaring, dipisah dengan baris baru - Penyaring khusus tidak valid: %s + Daftar untaian pembuat jalur komponen untuk disaring dipisahkan oleh baris baru + Penyaring khusus tidak sah: %s Sembunyikan kata kunci konten Sembunyikan pencarian dan umpan video menggunakan penyaring kata kunci Sembunyikan video beranda dengan kata kunci @@ -268,18 +307,18 @@ This is because Crowdin requires temporarily flattening this file and removing t Tidak dapat menggunakan kata kunci: %s Tambahkan tanda kutip untuk menggunakan kata kunci: %s Kata kunci punya keterangan yang bertentangan: %s - Katakunci terlalu pendek dan butuh tanda kutip: %s + Kata kunci terlalu pendek & butuh tanda kutip: %s Kata kunci akan menyembunyikan semua video: %s - + Sembunyikan iklan umum - Iklan umum sudah disembunyikan - Iklan umum sudah ditampilkan + Iklan umum disembunyikan + Iklan umum ditampilkan Sembunyikan iklan layar penuh Iklan layar penuh disembunyikan\n\nFitur ini hanya tersedia untuk perangkat lama - Iklan layar penuh sudah ditampilkan + Iklan layar penuh ditampilkan Sembunyikan iklan bertombol - Iklan berbentuk tombol disembunyikan + Iklan bertombol disembunyikan Iklan bertombol ditampilkan Sembunyikan label promosi berbayar Label promosi berbayar disembunyikan @@ -290,7 +329,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Sembunyikan banner untuk melihat produk Banner disembunyikan Banner ditampilkan - Sembunyikan tautan belanja dalam deskripsi video + Sembunyikan rak belanja pemutar + Rak belanja disembunyikan + Rak belanja ditampilkan + Sembunyikan tautan belanja dalam keterangan video Tautan belanja disembunyikan Tautan belanja ditampilkan @@ -306,17 +348,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Sembunyikan iklan layar penuh hanya berfungsi pada perangkat lama - + Sembunyikan promosi YouTube Premium Promosi YouTube Premium di bawah pemutar video disembunyikan Promosi YouTube Premium di bawah pemutar video ditampilkan - + Sembunyikan iklan video Iklan video disembunyikan Iklan video ditampilkan - + URL disalin ke papan klip URL dengan timestamp telah disalin Tampilkan tombol salin URL video @@ -326,13 +368,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Tombol ditampilkan. Ketuk untuk menyalin URL video dengan timestamp. Ketuk dan tahan untuk menyalin video tanpa timestamp Tombol tidak ditampilkan - + Hapus dialog peringatan untuk penonton Dialog akan dihapus Dialog akan ditampilkan Ini tidak mengabaikan batasan usia. Hanya otomatis menerimanya. - + Unduhan eksternal Pengaturan untuk menggunakan pengunduh eksternal Tampilkan tombol unduhan eksternal @@ -346,33 +388,33 @@ This is because Crowdin requires temporarily flattening this file and removing t Nama paket aplikasi pengunduh eksternal yang Anda pasang, seperti NewPipe atau Seal %s belum terpasang. Silahkan pasang. - + Matikan gerakan pencarian presisi Gerakan dinonaktifkan - Gerakan dinyalakan + Gerakan diaktifkan - + Aktifkan tapping seekbar Tapping seekbar diaktifkan Tapping seekbar dinonaktifkan - - Nyalakan gerakan kecerahan - Sapuan kecerahan dinyalakan - Sapuan kecerahan dimatikan - Nyalakan gerakan volume - Sapuan volume dinyalakan - Sapuan volume dimatikan - Nyalakan gerakan tekan-untuk-menggeser - Tekan-untuk-menggeser dinyalakan - Tekan-untuk-menggeser dimatikan - Nyalakan umpan balik sentuhan - Umpan balik sentuhan dinyalakan - Umpan balik sentuhan dimatikan + + Aktifkan gerakan kecerahan + Sapuan kecerahan diaktifkan + Sapuan kecerahan dinonaktifkan + Aktifkan gerakan volume + Sapuan volume diaktifkan + Sapuan volume dinonaktifkan + Aktifkan gerakan tekan-untuk-menggeser + Tekan-untuk-menggeser diaktifkan + Tekan-untuk-menggeser dinonaktifkan + Aktifkan umpan balik sentuhan + Umpan balik sentuhan diaktifkan + Umpan balik sentuhan dinonaktifkan Simpan dan pulihkan kecerahan Simpan dan pulihkan kecerahan saat keluar atau memasuki layar penuh Jangan simpan dan pulihkan kecerahan saat keluar atau memasuki layar penuh - Nyalakan gerakan kecerahan otomatis + Aktifkan gerakan kecerahan otomatis Mengusap ke bawah ke nilai terendah dari gerakan kecerahan akan menyalakan kecerahan otomatis Mengusap ke bawah ke nilai terendah tidak mengaktifkan kecerahan otomatis Otomatis @@ -385,12 +427,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Ambang batas magnitudo geser Jumlah ambang batas untuk terjadinya geser - + Matikan teks otomatis - Teks otomatis dimatikan - Teks otomatis dinyalakan + Teks otomatis dinonaktifkan + Teks otomatis diaktifkan - + Tombol tindakan Sembunyikan atau tampilkan tombol di bawah video Sembunyikan Suka dan Tidak Suka @@ -411,8 +453,8 @@ This is because Crowdin requires temporarily flattening this file and removing t Tombol remix ditampilkan Sembunyikan Unduhan - Tombol download disembunyikan - Tombol download ditampilkan + Tombol unduh disembunyikan + Tombol unduh ditampilkan Sembunyikan Terima kasih Tombol terima kasih disembunyikan @@ -424,25 +466,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Sembunyikan Simpan ke daftar putar Tombol Simpan ke daftar putar disembunyikan - Tombol Simpan ke daftar putar ditampilkan + Tombol simpan ke daftar putar ditampilkan - - Sembunyikan tombol putar otomatis - Tombol putar otomatis disembunyikan - Tombol putar otomatis ditampilkan - - - - Sembunyikan tombol teks - Tombol teks disembunyikan - Tombol teks ditampilkan - - - Sembunyikan tombol transmisi - Tombol membagikan layar disembunyikan - Tombol membagikan layar ditampilkan - - + Tombol navigasi Sembunyikan atau ganti tombol di bilah navigasi @@ -452,7 +478,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Sembunyikan Shorts Tombol shorts disembunyikan - Tomtol shorts ditampilkan + Tombol shorts ditampilkan Sembunyikan Buat Tombol buat disembunyikan @@ -469,7 +495,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Label disembunyikan Label ditampilkan - + Menu flyout Sembunyikan atau tampilkan item menu pemutar flyout @@ -480,14 +506,21 @@ This is because Crowdin requires temporarily flattening this file and removing t Sembunyikan Pengaturan tambahan Menu pengaturan tambahan disembunyikan Menu pengaturan tambahan ditampilkan + + Sembunyikan pengatur waktu tidur + Menu pengatur waktu tidur disembunyikan + Menu pengatur waktu tidur ditampilkan Sembunyikan Ulangi video Menu ulangi video disembunyikan Menu ulangi video ditampilkan - Sembunyikan Mode pencahayaan sinematik - Menu mode pencahayaan sinematik disembunyikan - Menu mode pencahayaan sinematik ditampilkan + Sembunyikan mode Sinematik + Menu mode sinematik disembunyikan + Menu mode sinematik ditampilkan + Sembunyikan volume Stabil + Menu volume stabil ditampilkan + Menu volume stabil disembunyikan Sembunyikan Bantuan & masukan Bantuan & menu masukan disembunyikan @@ -498,98 +531,61 @@ This is because Crowdin requires temporarily flattening this file and removing t Menu kecepatan pemutar video ditampilkan - Sembunyikan Info lanjut - Menu info lanjut disembunyikan - Menu info lanjut ditampilkan + Sembunyikan Info selengkapnya + Menu info selengkapnya disembunyikan + Menu info selengkapnya ditampilkan Sembunyikan Kunci layar Menu kunci layar disembunyikan Menu kunci layar ditampilkan - Sembunyikan Trek audio + Sembunyikan trek Audio Menu trek audio disembunyikan Menu trek audio ditampilkan Sembunyikan Tonton di VR Menu tonton di VR disembunyikan Menu tonton di VR ditampilkan + Sembunyikan footer menu kualitas video + Footer menu kualitas video disembunyikan + Footer menu kualitas video ditampilkan - - Sembunyikan tombol sebelumnya & berikutnya - Tombol disembunyikan - Tombol ditampilkan + + Sembunyikan tombol video sebelumnya & berikutnya + Tombol disembunyikan + Tombol ditampilkan + Sembunyikan tombol transmisi + Tombol transmisi disembunyikan + Tombol transmisi ditampilkan + + Sembunyikan tombol teks + Tombol teks disembunyikan + Tombol teks ditampilkan + Sembunyikan tombol putar otomatis + Tombol putar otomatis disembunyikan + Tombol putar otomatis ditampilkan - - Sembunyikan kartu album - Kartu album disembunyikan - Kartu album ditampilkan - - - Komentar - Sembunyikan atau tampilkan komponen bagian komentar - Sembunyikan Header \'Komentar oleh anggota\' - Header \'Komentar oleh anggota\' disembunyikan - Header \'Komentar oleh anggota\' disembunyikan - Sembunyikan bagian komentar - Bagian komentar disembunyikan - Bagian komentar ditampilkan - Sembunyikan tombol \"Buat Short\" - Tombol \'Buat Short\' disembunyikan - Tombol \'Buat Short\' ditampilkan - Sembunyikan komentar pratinjau - Komentar pratinjau disembunyikan - Komentar pratinjau ditampilkan - Sembunyikan \'terima kasih\' - Tombol terima kasih disembunyikan - Tombol terima kasih ditampilkan - Sembunyikan timestamp dan tombol emoji - Tombol timestamp dan emoji disembunyikan - Tombol timestamp dan emoji ditampilkan - - - Sembunyikan kotak penggalangan dana - Kotak penggalangan dana disembunyikan - Kotak penggalangan dana ditampilkan - - + Sembunyikan kartu layar akhir Kartu layar akhir disembunyikan Kartu layar akhir ditampilkan - - Bilah saring - Sembunyikan atau tampilkan bilah filter di feed, pencarian, dan video terkait - Sembunyikan di feed - Sembunyikan di feed - Tampilkan di feed - Sembunyikan di pencarian - Disembunyikan di pencarian - Ditampilkan di pencarian - Sembunyikan di video terkait - Disembunyikan di video terkait - Ditampilkan di video terkait + + Nonaktifkan mode sinematik di layar penuh + Mode sinematik dinonaktifkan + Mode sinematik diaktifkan - - Sembunyikan tombol mikrofon mengambang - Tombol mikrofon disembunyikan - Tombol mikrofon ditampilkan - - - Nonaktifkan mode ambien di layar penuh - Mide ambien dinonaktifkan - Mode ambien diaktifkan - - + Sembunyikan kartu info Kartu info disembunyikan Kartu info ditampilkan - + Nonaktifkan animasi angka bergulir Angka bergulir tidak dianimasikan Angka bergulir dianimasikan - + Sembunyikan seekbar di pemutar video Seekbar pemutar video disembunyikan Seekbar pemutar video ditampilkan @@ -597,15 +593,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Seekbar thumbnail disembunyikan Seekbar thumbnail ditampilkan - + + Pemutar Shorts + Sembunyikan atau tampilkan komponen di pemutar Shorts - Sembunyikan Shorts di feed beranda - Shorts di feed beranda disembunyikan - Shorts di feed beranda ditampilkan + Sembunyikan Shorts di umpan beranda + Shorts di umpan beranda disembunyikan + Shorts di umpan beranda ditampilkan - Sembunyikan Shorts di feed subscription - Shorts di feed subscription disembunyikan - Shorts di feed subscription ditampilkan + Sembunyikan Shorts di umpan langganan + Shorts di umpan langganan disembunyikan + Shorts di umpan langganan ditampilkan Sembunyikan Shorts di hasil pencarian Shorts di hasil pencarian disembunyikan Shorts di hasil pencarian ditampilkan @@ -617,9 +615,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Sembunyikan tombol berlangganan Tombol berlangganan disembunyikan Tombol berlangganan ditampilkan - Sembunyikan tombol henti overlay - Tombol henti overlay disembunyikan - Tombol henti overlay ditampilkan + Sembunyikan tombol hamparan terjeda + Tombol hamparan terjeda disembunyikan + Tombol hamparan terjeda ditampilkan Sembunyikan tombol belanja Tombol belanja disembunyikan Tombol belanja ditampilkan @@ -644,6 +642,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Sembunyikan tombol layar hijau Tombol layar hijau disembunyikan Tombol layar hijau ditampilkan + Sembunyikan tombol tagar + Tombol tagar disembunyikan + Tombol tagar ditampilkan Sembunyikan saran penelusuran Saran penelusuran disembunyikan Saran penelusuran ditampilkan @@ -692,54 +693,54 @@ This is because Crowdin requires temporarily flattening this file and removing t Bilah navigasi disembunyikan Bilah navigasi ditampilkan - + Nonaktifkan layar akhir video yang disarankan Video yang disarankan akan dinonaktifkan Video yang disarankan akan ditampilkan - + Sembunyikan timestamp video Timestamp disembunyikan Timestamp ditampilkan - + Sembunyikan panel popup pemutar Panel popup pemutar disembunyikan Panel popup pemutar ditampilkan - - Kapasitas overlay pemutar + + Opasitas hamparan pemutar Nilai opasitas antara 0-100, dimana 0 adalah transparan - Opasitas overlay pemutar harus di antara 0-100 + Opasitas hamparan pemutar harus di antara 0-100 - + Dislike sementara tidak tersedia (waktu API habis) Dislike tidak tersedia (status %d) Dislike tidak tersedia (batas API klien tercapai) Dislike tidak tersedia (%s) - Muat ulang video untuk vote Return YouTube Dislike + Muat ulang video untuk memilih Return YouTube Dislike Dislike ditampilkan Dislike tidak ditampilkan Tampilkan dislike di Shorts Dislike ditampilkan di Shorts - Dislike ditampilan di Shorts\n\nKeterbatasan: Dislike mungkin tidak muncul di mode incognito + Dislike ditampilan di Shorts\n\nKeterbatasan: Dislike mungkin tidak muncul di mode penyamaran Dislike disembunyikan di Shorts Dislike sebagai persentase Dislike ditampilkan sebagai persentase Dislike ditampilkan sebagai angka - Tombol like ringkas - Tombol like ditata untuk lebar minimum - Tombol like ditata untuk penampilan terbaik - Tampilkan dialog jika API tidak tersedia - Dialog ditampilkan jika Return Youtube Dislike tidak tersedia - Dialog tidak ditampilkan jika Return Youtube Dislike tidak tersedia + Tombol suka ringkas + Tombol suka ditata untuk lebar minimum + Tombol suka ditata untuk tampilan terbaik + Tampilkan pesan timbul jika API tidak tersedia + Pesan timbul tidak ditampilkan jika Return YouTube Dislike tidak tersedia + Pesan timbul tidak ditampilkan jika Return YouTube Dislike tidak tersedia Tentang Data disediakan oleh API Return YouTube Dislike. Tekan di sini untuk mempelajari lebih lanjut - Statistik API ReturnYoutubeDislike di perangkat ini + Statistik API ReturnYoutubeDislike dari perangkat ini Waktu respons API, rata-rata Waktu respons API, minimum Waktu respons API, maksimum @@ -749,26 +750,32 @@ This is because Crowdin requires temporarily flattening this file and removing t Tidak ada panggilan jaringan yang terjadi %d panggilan jaringan terjadi API mengambil vote, jumlah dari waktu habis - Tidak ada panggilan jaringan yang habis waktu - %d panggilan jaringan yang habis waktu + Tidak ada panggilan jaringan yang kehabisan waktu + %d panggilan jaringan yang kehabisan waktu Pembatasan tarif API klien Tidak ada pembatasan tarif klien terjadi Pembatasan tarif klien terjadi %d kali - %d milisekon + %d milidetik - + Aktifkan bilah pencarian lebar Bilah pencarian lebar diaktifkan Bilah pencarian lebar dinonaktifkan - + + Aktifkan thumbnail berkualitas tinggi + Thumbnail seekbar berkualitas tinggi + Thumbnail seekbar berkualitas sedang + Layar penuh thumbnail seekbar berkualitas tinggi + Layar penuh thumbnail seekbar berkualitas sedang + Ini juga akan memulihkan thumbnail pada siaran langsung yang tidak memiliki thumbnail seekbar.\n\nThumbnail seekbar akan menggunakan kualitas yang sama dengan video saat ini.\n\nFitur ini berfungsi paling baik dengan kualitas video 720p atau lebih rendah dan saat menggunakan koneksi internet yang sangat cepat. Kembalikan thumbnail seekbar yang lama - Thumbnail seekbar akan muncul diatas seekbar + Thumbnail seekbar akan muncul di atas seekbar Thumbnail seekbar akan muncul di layar penuh - + Aktifkan SponsorBlock - SponsorBlock adalah sistem crowd-source untuk melewatkan bagian yang mengganggu di video YouTube + SponsorBlock adalah sistem yang bersumber dari banyak orang untuk melewatkan bagian video YouTube yang mengganggu Tampilan Tampilkan tombol voting Tombol segmen voting ditampilkan @@ -780,9 +787,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Tombol lewati disembunyikan secara otomatis Tombol lewati disembunyikan setelah beberapa detik Tombol lewati ditampilkan untuk seluruh segmen - Tampilkan dialog ketika melewati segmen otomatis - Dialog ditampilkan saat segmen dilewati secara otomatis. Tekan di sini untuk melihat contohnya - Dialog tidak ditampilkan. Tekan di sini untuk melihat contohnya + Tampilkan pesan timbul ketika melewati segmen otomatis + Pesan timbul ditampilkan saat segmen dilewati secara otomatis. Tekan di sini untuk melihat contohnya + Pesan timbul tidak ditampilkan. Tekan di sini untuk melihat contohnya Tampilkan durasi video tanpa segmen Durasi video dikurangi semua segmen, ditampilkan dalam tanda kurung di samping durasi video penuh Durasi video penuh ditampilkan @@ -800,39 +807,39 @@ This is because Crowdin requires temporarily flattening this file and removing t Sudah dibaca Tunjukkan Umum - Tampilkan pesan toast jika API tidak tersedia - Toast ditampilkan jika SponsorBlock tidak tersedia - Toast tidak ditampilkan jika SponsorBlock tidak tersedia - Nyalakan pelacakan melewati hitungan + Tampilkan pesan timbul jika API tidak tersedia + Pesan timbul ditampilkan jika SponsorBlock tidak tersedia + Pesan timbul tidak ditampilkan jika SponsorBlock tidak tersedia + Aktifkan pelacakan melewati hitungan Mengizinkan leaderboard SponsorBlock mengetahui berapa banyak waktu yang diselamatkan. Sebuah pesan dikirim ke leaderboard setiap kali sebuah segmen dilewati - Pelacakan melewati hitungan tidak dinyalakan + Lewati pelacakan jumlah tidak diaktifkan Durasi minimum segmen Segmen yang lebih pendek pada dari nilai ini (detik) tidak akan ditampilkan atau dilewati - Durasi waktu tidak valid - ID user pribadi Anda + Durasi waktu tidak sah + ID pengguna pribadi Anda Ini harus dijaga kerahasiaannya. Seperti kata sandi dan tidak disarankan untuk dibagikan dengan siapa pun. Jika seseorang mendapatkan ini, mereka dapat menyamar sebagai Anda - ID user harus tidak lebih dari 30 karakter + ID pengguna tidak boleh lebih dari 30 karakter Ubah URL API Alamat yang digunakan SponsorBlock untuk membuat panggilan ke server - Reset URL API - URL API tidak valid - URL API terubah + Atur ulang URL API + URL API tidak sah + URL API diubah Impor/Ekspor pengaturan Salin Konfigurasi JSON SponsorBlock Anda yang dapat diimpor/diekspor ke ReVanced dan platform SponsorBlock lainnya - Konfigurasi JSON SponsorBlock Anda yang dapat diimpor/diekspor ke ReVanced dan platform SponsorBlock lainnya, termasuk ID user Anda. Pastikan untuk membagikannya dengan bijak + Konfigurasi JSON SponsorBlock Anda yang dapat diimpor/diekspor ke ReVanced dan platform SponsorBlock lainnya, termasuk ID pengguna Anda. Pastikan untuk membagikannya dengan bijak Pengaturan berhasil diimpor Gagal mengimpor: %s Gagal mengekspor: %s - Setelan Anda berisi ID user SponsorBlock pribadi.\n\nID user Anda seperti sebuah password yang sebaiknya tidak boleh dibagikan.\n + Setelan Anda berisi ID pengguna SponsorBlock pribadi.\n\nID pengguna Anda seperti sebuah password dan sebaiknya jangan pernah dibagikan.\n Jangan tampilkan lagi Ubah perilaku segmen Sponsor - Promosi dibayar, tautan dibayar dan iklan langsung. Tidak untuk promosi diri sendiri atau dukungan gratis untuk gerakan/kreator/website/produk yang mereka suka + Promosi dibayar, tautan dibayar dan iklan langsung. Tidak untuk promosi diri sendiri atau dukungan gratis untuk gerakan/kreator/website/produk yang mereka sukai Tidak Dibayar/Promosi Diri Sendiri - Serupa dengan \'sponsor\' namun untuk yang tidak bebayar atau promosi diri. Ini termasuk bagian tentang merchandise, donasi, atau informasi mengenai mitra kolaborasi + Serupa dengan \'Sponsor\' namun untuk yang tidak bebayar atau promosi diri. Ini termasuk bagian tentang merchandise, donasi, atau informasi mengenai mitra kolaborasi Pengingat Interaksi (Berlangganan) - Pengingat singkat untuk like, subscribe, atau follow di tengah konten. Jika pengingat berdurasi panjang atau mengenai sesuatu yang spesifik, sebaiknya termasuk kategori promosi diri + Pengingat singkat untuk suka, berlangganan, atau ikuti di tengah konten. Jika berdurasi panjang atau mengenai sesuatu yang spesifik, sebaiknya termasuk kategori promosi diri sendiri Sorotan Bagian video yang paling dilihat oleh orang Jeda/Animasi Intro @@ -840,11 +847,11 @@ This is because Crowdin requires temporarily flattening this file and removing t Kartu Akhir/Kredit Kredit atau ketika layar akhir YouTube muncul. Bukan kesimpulan dengan informasi Pratinjau/Rekap/Pengait - Koleksi klip yang menunjukkan apa yang akan terjadi di video atau di video lain pada series yang dama, di mana informasi diulang di video lain + Kumpulan klip yang menunjukkan apa yang akan datang atau apa yang terjadi di video atau di video lain dari sebuah seri, di mana semua informasi diulang di tempat lain Pengisi Tidak Relevan/Lelucon Adegan berbelit-belit yang ditambahkan hanya sebagai filler atau candaan yang tidak diperlukan untuk memahami isi utama video. Tidak termasuk bagian yang mengandung konteks atau detail latar belakang Musik: Bagian Non-Musik - Hanya untuk digunakan pada video musik. Bagian video musik tanpa musiknya yang belum termasuk pada kategori lain + Hanya untuk digunakan pada video musik. Bagian video musik tanpa musiknya, yang belum tercakup dalam kategori lain Lewati Sorotan Lewati sponsor @@ -855,8 +862,8 @@ This is because Crowdin requires temporarily flattening this file and removing t Lewati jeda Lewati jeda Lewati outro - Lewati preview - Lewati preview + Lewati pratinjau + Lewati pratinjau Lewati rekap Lewati filler Lewati non-musik @@ -873,7 +880,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Pratinjau dilewati Rekap dilewati Pengisi dilewati - Keheningan dilewati + Melewati bagian non-musik Melewati segmen yang belum dikirim Beberapa segmen dilewati Lewati otomatis @@ -882,7 +889,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Tampilkan di seekbar Nonaktifkan Tak dapat mengirim segmen: %s - SponsorBlock sementara anjlok + SponsorBlock sementara tidak tersedia Tak dapat mengirim segmen (status: %1$d %2$s) Tak dapat mengirim segmen.\nJumlah Dibatasi (terlalu banyak dari IP yang sama) Tidak dapat mengirim segmen: %s @@ -892,13 +899,13 @@ This is because Crowdin requires temporarily flattening this file and removing t SponsorBlock tidak tersedia (API kehabisan waktu) SponsorBlock sementara tidak tersedia (status %d) SponsorBlock sementara tidak tersedia - Tidak dapat memilih segmen (API timed out) + Tidak dapat memilih segmen (API kehabisan waktu) Tidak dapat memilih segmen (status: %1$d %2$s) Tidak dapat memilih segmen: %s - Suka - Tidak suka + Sukai + Tidak sukai Ubah kategori - Tidak ada segmen untuk di vote + Tidak ada segmen untuk dipilih Pilih kategori segmen Kategori dinonaktifkan di pengaturan. Aktifkan kategori untuk dikirim. Segmen SponsorBlock Baru @@ -912,117 +919,152 @@ This is because Crowdin requires temporarily flattening this file and removing t Segmen dari\n\n%1$s\nke\n%2$s\n\n(%3$s)\n\nSiap dikirim? Awal harus sebelum akhir Tandai dua lokasi pada bilah waktu terlebih dahulu - Pratinjau segmen, dan memastikan segmen dilewati dengan lancar - Atur pengaturan tempo segmen secara manual - Apakah Anda ingin mengubah tempo untuk awal atau akhir dari segmen? - Waktu yang diberikan tidak valid + Pratinjau segmen, dan pastikan segmen dilewati dengan lancar + Ubah waktu segmen secara manual + Apakah Anda ingin menubah waktu awal atau akhir segmen? + Waktu yang diberikan tidak sah Statistik - Data sementara tidak tersedia (API is down) + Data sementara tidak tersedia (API nonaktif) Memuat... SponsorBlock dinonaktifkan - Username Anda: <b>%s</b> - Tekan di sini untuk mengubah username Anda + Nama pengguna Anda: <b>%s</b> + Tekan di sini untuk mengubah nama pengguna Anda Tidak dapat mengubah nama pengguna: Status: %1$d %2$s Nama pengguna berhasil diubah Reputasi Anda: <b>%.2f</b> - Anda telah membuat segmen <b>%s</b> + Anda telah membuat <b>%s</b> segmen Ketuk di sini untuk melihat segmen Anda Papan peringkat SponsorBlock - Anda menghindarkan orang dari segmen <b>%s</b> + Anda menghindarkan orang dari <b>%s</b> segmen Tekan di sini untuk melihat data global dan kontributor utama Itu <b>%s</b> dari hidup mereka.<br>Tekan di sini untuk melihat papan peringkat Anda melewati <b>%s</b> segmen Itu <b>%s</b> - Reset perhitungan segmen terlewat? + Atur ulang penghitungan segmen terlewati? %1$s jam %2$s menit %1$s menit %2$s detik %s detik Warna: - Warna terubah + Warna berubah Reset warna - Kode warna tidak valid - Reset warna + Kode warna tidak sah + Atur ulang warna Setel ulang Tentang - Data yang disediakan API SponsorBlock. Tekan di sini untuk mempelajari lebih lanjut dan melihat hasil download untuk platform lain + Data disediakan oleh API SponsorBlock. Tekan di sini untuk mempelajari lebih lanjut dan melihat hasil pengunduhan untuk platform lain - + Palsukan versi app Versi yang dipalsukan Versi asli - Versi aplikasi akan dipalsukan ke versi lama YouTube.\n\nIni akan mengubah tampilan dan fitur aplikasi, tapi mungkin terjadi efek samping tidak diketahui.\n\nJika nanti dinonaktifkan, disarankan menghapus data aplikasi agar UI tidak kacau. + Versi aplikasi akan dipalsukan ke versi YouTube yang lebih lama.\n\nIni akan mengubah tampilan dan fitur aplikasi, namun efek samping yang tidak diketahui mungkin terjadi.\n\nJika nanti dinonaktifkan, disarankan untuk menghapus data aplikasi untuk mencegah kesalahan UI. Target versi app yang dipalsukan - 18.33.40 - Kembalikan RYD pada mode incognito Shorts + 18.33.40 - Pulihkan RYD pada mode penyamaran Shorts 18.20.39 - Pulihkan menu kecepatan & kualitas video lebar - 18.09.39 - Pulihkan tab perpustakaan + 18.09.39 - Pulihkan tab pustaka 17.41.37 - Pulihkan rak daftar putar lama - 17.33.42 - Mengembalikan tata letak UI lama - + Tetapkan halaman awal Bawaan + Jelajahi saluran Jelajahi + Permainan Riwayat + Pustaka Video yang disukai + Siaran langsunng + Film + Musik Pencarian + Olahraga Langganan Sedang tren + Tonton nanti - + Matikan melanjutkan pemutar video Shorts Pemutaran Shorts tidak akan dilanjutkan saat aplikasi dimulai Pemutaran Shorts akan dilanjutkan saat aplikasi dimulai - + + Putar otomatis Shorts + Shorts akan diputar otomatis + Shorts akan diulangi + Putar otomatis Shorts di latar belakang + Pemutaran latar belakang Shorts akan diputar otomatis + Pemutaran latar belakang Shorts akan diulangi + + Aktifkan tata letak tablet Tata letak tablet diaktifkan Tata letak tablet dinonaktifkan - Tidak ada postingan komunitas untuk tablet + Postingan komunitas tidak muncul pada tata letak tablet - + Pemutar Mini Mengubah gaya pemutar aplikasi saat diciuitkan - Jenis miniplayer + Jenis pemutar mini Asli Ponsel Tablet Modern 1 Modern 2 Modern 3 + Aktifkan sudut membulat + Sudutnya membulat + Sudutnya persegi + Aktifkan ketuk dua kali dan cubit untuk mengubah ukuran + Tindakan ketuk dua kali dan cubit untuk mengubah ukuran diaktifkan\n\n• Ketuk dua kali untuk menambah ukuran miniplayer\n• Ketuk dua kali lagi untuk mengembalikan ukuran asli + Tindakan ketuk dua kali dan cubit untuk mengubah ukuran dinonaktifkan + Aktifkan seret dan lepas + Seret dan lepas diaktifkan\n\nPemutar mini dapat diseret ke sudut mana pun di layar + Seret dan lepas dinonaktifkan + Aktifkan gerakan seret horizontal + Gerakan seret horizontal diaktifkan\n\nMiniplayer dapat diseret keluar layar ke kiri atau kanan + Gerakan seret horizontal dinonaktifkan + Sembunyikan tombol tutup + Tombol tutup disembunyikan + Tombol tutup ditampilkan Sembunyikan perbesar dan tutup + Tombol disembunyikan\n\nGeser untuk memperluas atau menutup Tombol bentang dan tutup ditampilkan Sembunyikan subteks Subteks disembunyikan Subteks ditampilkan - Sembunyikan percepat dan kembali - Kembali dan percepat disembunyikan - Kembali dan percepat ditampilkan - Kelegapan hamparan + Sembunyikan tombol maju dan mundur + Lewati maju dan mundur disembunyikan + Lewati maju dan mundur ditampilkan + Ukuran awal + Awal pada ukuran layar, dalam piksel + Ukuran piksel harus antara %1$s dan %2$s + Opasitas hamparan Nilai opasitas antara 0-100, di mana 0 adalah transparan - Opasiti overlay miniplayer harus antara 0-100 + Opasitas hamparan pemutar mini antara 0-100 - + Aktifkan layar pemuatan gradien Layar pemuatan akan memiliki latar belakang gradien Layar pemuatan akan memiliki latar belakang yang solid - - Aktifkan warna bilah pencarian khusus - Warna bilah pencarian khusus ditampilkan - Warna bilah pencarian asli ditampilkan - Warna seekbar kustom + + Aktifkan warna seekbar khusus + Warna seekbar khusus ditampilkan + Warna seekbar asli ditampilkan + Warna seekbar khusus Warna dari seekbar + Nilai warna seekbar tidak sah - + Abaikan pembatasan wilayah gambar - Menggunakan sumber yt4.ggpht.com + Menggunakan sumber gambar yt4.ggpht.com Menggunakan sumber gambar asli\n\nMengaktifkan ini akan memperbaiki gambar hilang di daerah tertentu - + Tab beranda @@ -1033,19 +1075,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Hasil pencarian Thumbnail asli DeArrow & Thumbnail asli - DeArrow & Gambar diam + DeArrow & Tangkapan diam Tangkapan diam - DeArrow menyediakan thumbnail yang dibuat oleh banyak orang untuk video YouTube. Thumbnail ini seringkali lebih relevan daripada yang disediakan oleh YouTube\n\nJika dinyalakan, URL video akan dikirim ke server API dan tidak ada data lain yang dikirim. Jika video tidak memiliki thumbnail DeArrow, maka rekaman asli atau gambar diam akan ditampilkan\n\nKetuk di sini untuk mempelajari lebih lanjut tentang DeArrow + DeArrow menyediakan thumbnail yang dibuat oleh banyak orang untuk video YouTube. Thumbnail-thumbnail ini seringkali lebih relevan daripada yang disediakan oleh YouTube\n\nJika dinyalakan, URL video akan dikirim ke server API dan tidak ada data lain yang dikirim. Jika video tidak memiliki thumbnail DeArrow, maka gambar asli atau tangkapan diam akan ditampilkan\n\nKetuk di sini untuk mempelajari lebih lanjut tentang DeArrow Tampilkan pemberitahuan halus jika API tidak tersedia Pemberitahuan halus ditampilkan jika DeArrow tidak tersedia Pemberitahuan halus tidak ditampilkan jika DeArrow tidak tersedia Titik akhir API DeArrow URL titik akhir cache thumbnail DeArrow Tangkapan video diam - Tangkapan gambar diam diambil dari awal/tengah/akhir setiap video. Gambar-gambar ini dibuat di YouTube dan tidak ada API eksternal yang digunakan + Tangkapan diam diambil dari awal/tengah/akhir setiap video. Gambar-gambar ini dibuat di YouTube dan tidak ada API eksternal yang digunakan Gunakan tangkapan diam cepat - Menggunakan tangkapan kualitas sedang. Gambar mini akan dimuat lebih cepat, tetapi siaran langsung, video yang belum dirilis, atau video yang sangat lama mungkin menampilkan gambar mini kosong - Menggunakan gambar diam berkualitas tinggi + Menggunakan tangkapan diam kualitas sedang. Thumbnail akan dimuat lebih cepat, tetapi siaran langsung, video yang belum dirilis, atau video yang sangat lama mungkin menampilkan thumbnail kosong + Menggunakan tangkapan diam berkualitas tinggi Lama waktu menangkap layar video Awal video Pertengahan video @@ -1054,55 +1096,55 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow sementara ini tidak tersedia (kode status: %s) DeArrow sementara ini tidak tersedia - + Tampilkan pengumuman ReVanced Pengumuman ditampilkan saat memulai Pengumuman tidak ditampilkan saat memulai - Tampilkan pengumuman di awal buka + Tampilkan pengumuman saat memulai Gagal menghubungkan ke penyedia pengumuman Abaikan - + Peringatan Riwayat tontonan Anda tidak disimpan.<br><br>Hal ini kemungkinan besar disebabkan oleh pemblokir iklan DNS atau proksi jaringan.<br><br>Untuk memperbaikinya, masukkan daftar putih <b>s.youtube.com</b> atau matikan semua pemblokir DNS dan proksi. Jangan tampilkan lagi - + Aktifkan pengulangan otomatis Pengulangan otomatis diaktifkan Pengulangan otomatis dinonaktifkan - + Palsukan dimensi perangkat Dimensi perangkat dipalsukan\n\nAkan ada resolusi video lebih tinggi tapi video menjadi patah-patah, baterai boros, dan efek lainnya yang tidak jelas - Dimensi perangkat tidak dipalsukan\n\nMengaktifkan ini akan ada resolusi video lebih tinggi - Mengaktifkan ini menyebabkan video jadi patah-patah, baterai boros, dan efek lainnya yang tidak jelas. + Dimensi perangkat tidak dipalsukan\n\nMengaktifkan ini dapat membuka kualitas video yang lebih tinggi + Mengaktifkan ini dapat menyebabkan pemutaran video tersendat-sendat, masa pakai baterai yang lebih buruk, dan efek samping yang tidak diketahui. - + Pengaturan GmsCore Pengaturan untuk GmsCore - + Abaikan pengalihan URL Pengalihan URL diabaikan Pengalihan URL tidak diabaikan - + Buka tautan di peramban Membuka tautan di eksternal Membuka tautan di aplikasi - + Hapus parameter kueri pelacakan Parameter kueri pelacakan dihapus dari tautan Parameter kueri pelacakan tidak dihapus dari tautan - - Matikan getaran zoom + + Matikan sentuh getar zoom Sentuh getar dinonaktifkan Sentuh getar diaktifkan - + Kualitas otomatis Ingat perubahan kualitas video Perubahan kualitas diatur ke semua video @@ -1113,35 +1155,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Kualitas bawaan %1$s diubah ke: %2$s - + Tampilkan tombol dialog kecepatan Tombol ditampilkan Tombol tidak ditampilkan - - Kecepatan putar kustom - Tambah atau ubah kecepatan putar yang tersedia + + Menu kecepatan pemutaran khusus + Menu kecepatan khusus ditampilkan + Menu kecepatan khusus tidak ditampilkan + Kecepatan putar khusus + Tambah atau ubah kecepatan pemutaran khusus Kecepatan khusus harus kurang dari %s. Menggunakan nilai bawaan. Kecepatan pemutaran khusus tidak sah. - + Ingat perubahan kecepatan pemutaran Perubahan kecepatan pemutaran berlaku untuk semua video Perubahan kecepatan pemutaran berlaku untuk video saat ini Kecepatan pemutaran bawaan Mengubah kecepatan bawaan menjadi: %s - + Pulihkan menu kualitas video lawas Menu kualitas video lawas ditampilkan Menu kualitas video lawas tidak ditampilkan - - Nyalakan geser untuk mencari - Geser untuk mencari dinyalakan - Geser untuk mencari tidak dinyalakan + + Aktifkan geser untuk mencari + Geser untuk mencari diaktifkan + Geser untuk mencari tidak diaktifkan - + Palsukan aliran video Palsukan klien aliran video untuk mencegah masalah pemutaran Palsukan aliran video @@ -1159,17 +1204,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Efek samping pemalsuan Android VR • Menu trek audio hilang\n• Volume stabil tidak tersedia - - - - + Blokir iklan audio Iklan audio diblokir Iklan audio tidak diblokir - + %s tidak tersedia. Iklan mungkin muncul. Coba beralih ke layanan pemblokir iklan lain di pengaturan. Server %s mengalami kesalahan. Iklan mungkin muncul. Coba beralih ke layanan pemblokir iklan lain di pengaturan. Blokir iklan video yang disematkan @@ -1177,42 +1219,42 @@ This is because Crowdin requires temporarily flattening this file and removing t Proksi Luminous Proksi PurpleAdBlock - + Blokir iklan video Iklan video diblokir Iklan video tidak diblokir - + pesan terhapus Tampilkan pesan yang terhapus Jangan tampilkan pesan yang terhapus Sembunyikan pesan yang dihapus di balik spoiler Tampilkan pesan yang dihapus sebagai teks yang dicoret - + Klaim Poin Saluran secara otomatis Poin Saluran diklaim secara otomatis Poin Saluran tidak diklaim secara otomatis - + Nyalakan mode debug Twitch - Mode debug Twitch dinyalakan (tidak disarankan) - Mode debug Twitch dimatikan + Mode debug Twitch diaktifkan (tidak disarankan) + Mode debug Twitch dinonaktifkan - + Pengaturan ReVanced Iklan Pengaturan pemblokir iklan Obrolan Pengaturan obrolan - Lainnya + Lain-lain Pengaturan lain-lain Pengaturan umum Pengaturan lainnya Iklan sisi klien Iklan surestream di sisi server - Log awakutu + Pencatatan debug Log debug diaktifkan Log debug dinonaktifkan diff --git a/src/main/resources/addresources/values-is-rIS/strings.xml b/patches/src/main/resources/addresources/values-is-rIS/strings.xml similarity index 70% rename from src/main/resources/addresources/values-is-rIS/strings.xml rename to patches/src/main/resources/addresources/values-is-rIS/strings.xml index 2d7cfe1e7..fafe34bde 100644 --- a/src/main/resources/addresources/values-is-rIS/strings.xml +++ b/patches/src/main/resources/addresources/values-is-rIS/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,106 +139,105 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + Viðvörun - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/src/main/resources/addresources/values-it-rIT/strings.xml b/patches/src/main/resources/addresources/values-it-rIT/strings.xml similarity index 93% rename from src/main/resources/addresources/values-it-rIT/strings.xml rename to patches/src/main/resources/addresources/values-it-rIT/strings.xml index 0a1628615..6b3217fa0 100644 --- a/src/main/resources/addresources/values-it-rIT/strings.xml +++ b/patches/src/main/resources/addresources/values-it-rIT/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Controlli falliti Apri sito ufficiale Ignora @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Patched %s days ago La data di compilazione APK è danneggiata - + Sei sicuro di voler continuare? Reimposta Aggiorna e riavvia @@ -62,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Link ufficiali Dona - + MicroG GmsCore non è installato. Installarlo. Azione necessaria @@ -73,7 +73,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Informazioni Annunci Miniature alternative @@ -85,7 +85,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Varie Video - + + Disabilita riproduzione sfondo Shorts + + Debugging Abilita o disabilita impostazioni di debug Logging di debug @@ -102,13 +105,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Messaggio non mostrato se si verifica un errore Disattivando i messaggi di errore si nascondono tutte le notifiche di errore di ReVanced.\n\nNon sarai avvisato di alcun evento inatteso. - + Disabilita il bagliore del pulsante di / sottoscrizione Come e il pulsante di sottoscrizione non brillerà quando menzionato Come e il pulsante di sottoscrizione brillerà quando menzionato - Nascondi il separatore grigio - I separatori grigi sono nascosti - I separatori grigi sono visibili + Nascondi schede album + Le schede degli album sono nascoste + Le schede degli album sono mostrate + Nascondi box crowdfunding + Crowdfunding box è nascosto + Il Crowdfunding box è mostrato + Nascondi il pulsante del microfono fluttuante + Pulsante microfono nascosto + Pulsante microfono mostrato Nascondi la filigrana del canale Filigrana nascosta Filigrana visibile @@ -153,9 +162,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Nascondi il frammento espandibile sotto i video Frammenti espandibili nascosti Frammenti espandibili visibili - Nascondi piè di pagina del menu qualità video - Piè di pagina del menu di qualità video nascosto - Piè di pagina del menu di qualità video visibile Nascondi i post della community Post della community nascosti Post della community visibili @@ -230,6 +236,37 @@ This is because Crowdin requires temporarily flattening this file and removing t La sezione della trascrizione è mostrata Descrizione video Nascondi o mostra i componenti della descrizione video + Barra dei filtri + Nascondi o mostra la barra dei filtri nel feed, nella ricerca e nei video correlati + Nascondi nel feed + Nascosto nel feed + Mostrato nel feed + Nascondi nella ricerca + Nascosto nella ricerca + Mostrato nella ricerca + Nascondi nei video correlati + Nascosto nei video correlati + Mostrato in video correlati + Commenti + Nascondi o mostra i componenti della sezione commenti + Nascondi l\'intestazione \'Commenti dai membri\' + \'Commenti dei membri\' intestazione è nascosta + L\'intestazione \'Commenti dei membri\' è mostrata + Nascondi sezione commenti + La sezione commenti è nascosta + La sezione Commenti è mostrata + Nascondi il pulsante \'Crea un Short\' + Il pulsante \'Crea uno Short\' è nascosto + Il pulsante \'Crea uno Short\' è visibile + Nascondi commento anteprima + Il commento nell\'anteprima è nascosto + Anteprima commento mostrata + Nascondi pulsante grazie + Grazie pulsante è nascosto + Il pulsante di ringraziamento è mostrato + Nascondi i pulsanti timestamp ed emoji + I pulsanti Timestamp ed emoji sono nascosti + I pulsanti Timestamp ed emoji sono visibili Nascondi Doodles Di YouTube Barra di ricerca Doodles sono nascosti @@ -260,7 +297,6 @@ This is because Crowdin requires temporarily flattening this file and removing t This is because keywords can be in any language, and showing an example in the localized script helps convey this. --> Parole chiave e frasi da nascondere, separate da nuove righe\n\nLe parole chiave possono essere nomi di canali o qualsiasi testo mostrato nei titoli video\n\nLe parole con lettere maiuscole nel centro devono essere inserite con il contenitore (es: iPhone, TikTok, LeBlanc) Informazioni sul filtro delle parole chiave - Home/Abbonamento/I risultati della ricerca sono filtrati per nascondere il contenuto che corrisponde alle frasi di parole chiave\n\nLimitazioni\n• I resort non possono essere nascosti dal nome del canale\n• Alcuni componenti dell\'interfaccia utente potrebbero non essere nascosti\n• La ricerca di una parola chiave potrebbe non mostrare alcun risultato Corrispondenza parole intere Circondare una parola chiave/frase con doppie virgolette impedirà partite parziali di titoli video e nomi di canali<br><br>Per esempio,<br><b>\"ai\"</b> nasconderà il video: <b>How does AI work?</b><br>ma non si nasconde: <b>What does fair use mean?</b> @@ -271,7 +307,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Parola chiave troppo corta e richiede preventivi: %s Parola chiave nasconderà tutti i video: %s - + Nascondi gli annunci generali Gli annunci generali sono nascosti Gli annunci generali sono mostrati @@ -290,6 +326,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Nascondi banner per visualizzare i prodotti Banner nascosto Banner mostrato + Nascondi lo scaffale dello shopping + Lo scaffale è nascosto + Lo scaffale è mostrato Nascondi link agli acquisti nella descrizione del video I link commerciali sono nascosti I collegamenti commerciali sono mostrati @@ -306,17 +345,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Nascondi gli annunci a schermo intero funziona solo con dispositivi più vecchi - + Nascondi le promozioni Premium di YouTube Le promozioni di YouTube Premium sotto il video player sono nascoste Le promozioni di YouTube Premium sotto il lettore video sono mostrate - + Nascondi annunci video Gli annunci video sono nascosti Gli annunci video sono mostrati - + URL copiato negli appunti URL con timestamp copiato Mostra il pulsante URL di copia video @@ -326,13 +365,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Il pulsante è visualizzato. Tocca per copiare l\'URL del video con timestamp. Tocca e tieni premuto per copiare il video senza timestamp Il pulsante non è mostrato - + Rimuovi la finestra di discrezionalità del visualizzatore La finestra di dialogo verrà rimossa Verrà visualizzata la finestra Questo non aggira la restrizione di età. Lo accetta solo automaticamente. - + Download esterni Impostazioni per l\'utilizzo di un downloader esterno Mostra il pulsante di download esterno @@ -346,17 +385,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Nome del pacchetto dell\'applicazione esterna di downloader installata, come NewPipe o Seal %s non è installato. Installalo. - + Disabilita il gesto di ricerca preciso Il gesto è disabilitato Gesture abilitata - + Abilita toccando la seekbar Tocco barra di ricerca è abilitato Tocco barra di ricerca disabilitato - + Abilita gesto luminosità Lo scorrimento della luminosità è abilitato Lo scorrimento della luminosità è disabilitato @@ -385,12 +424,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Soglia magnitudine scorrimento La quantità di soglia per lo scorrimento che si verifica - + Disabilita didascalie automatiche Le didascalie automatiche sono disabilitate Le didascalie automatiche sono abilitate - + Pulsanti azione Nascondi o mostra i pulsanti sotto i video Nascondi Mi piace e Dispiace @@ -426,23 +465,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Il pulsante Salva nella playlist è nascosto Il pulsante Salva nella playlist è mostrato - - Nascondi pulsante autoplay - Il pulsante Autoplay è nascosto - Il pulsante Autoplay è mostrato - - - - Nascondi il pulsante didascalie - Il pulsante sottotitoli è nascosto - Il pulsante sottotitoli è mostrato - - - Nascondi pulsante cast - Il pulsante Trasmetti è nascosto - Il pulsante Trasmetti è visibile - - + Navigation buttons Nascondi o cambia i pulsanti nella barra di navigazione @@ -450,7 +473,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Il pulsante Home è nascosto Il pulsante Home è mostrato - Nascondi Pantaloncini + Nascondi Shorts Il pulsante Shorts è nascosto Il pulsante Shorts è visibile @@ -469,7 +492,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Le etichette sono nascoste Le etichette sono visibili - + Flyout menu Nascondi o mostra le voci del menu di flyout del giocatore @@ -480,6 +503,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Nascondi impostazioni aggiuntive Il menu delle impostazioni aggiuntive è nascosto Viene mostrato il menu impostazioni aggiuntive + + Nascondi timer di sospensione + Il menu del timer di spegnimento è nascosto + Viene mostrato il menù del timer di spegnimento Nascondi video Loop Il menu video Loop è nascosto @@ -488,6 +515,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Nascondi la modalità Ambient Il menu della modalità Ambient è nascosto Viene mostrato il menu della modalità Ambient + Nascondi volume stabile + Viene mostrato il menu volume stabile + Il menu volume stabile è nascosto Nascondi Aiuto & feedback Aiuto & menu di feedback è nascosto @@ -513,83 +543,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Nascondi orologio in VR Guarda nel menu VR è nascosto Guarda nel menu VR + Nascondi piè di pagina del menu qualità video + Il piè di pagina del menu di qualità video è nascosto + Viene mostrato il piè di pagina del menu qualità video - - Nascondi i pulsanti video precedenti & - I pulsanti sono nascosti - I pulsanti sono mostrati + + Nascondi i pulsanti video precedenti & + I pulsanti sono nascosti + I pulsanti sono mostrati + Nascondi pulsante cast + Il pulsante Trasmetti è nascosto + Il pulsante Trasmetti è visibile + + Nascondi il pulsante didascalie + Il pulsante sottotitoli è nascosto + Il pulsante sottotitoli è mostrato + Nascondi pulsante autoplay + Il pulsante Autoplay è nascosto + Il pulsante Autoplay è mostrato - - Nascondi schede album - Le schede degli album sono nascoste - Le schede degli album sono mostrate - - - Commenti - Nascondi o mostra i componenti della sezione commenti - Nascondi l\'intestazione \'Commenti dai membri\' - \'Commenti dei membri\' intestazione è nascosta - L\'intestazione \'Commenti dei membri\' è mostrata - Nascondi sezione commenti - La sezione commenti è nascosta - La sezione Commenti è mostrata - Nascondi il pulsante \'Crea un Short\' - Il pulsante \'Crea uno Short\' è nascosto - Il pulsante \'Crea uno Short\' è visibile - Nascondi commento anteprima - Il commento nell\'anteprima è nascosto - Anteprima commento mostrata - Nascondi pulsante grazie - Grazie pulsante è nascosto - Il pulsante di ringraziamento è mostrato - Nascondi i pulsanti timestamp ed emoji - I pulsanti Timestamp ed emoji sono nascosti - I pulsanti Timestamp ed emoji sono visibili - - - Nascondi box crowdfunding - Crowdfunding box è nascosto - Il Crowdfunding box è mostrato - - + Nascondi schede di fine schermo Le schede di fine schermo sono nascoste Vengono mostrate le schede di fine schermo - - Barra dei filtri - Nascondi o mostra la barra dei filtri nel feed, nella ricerca e nei video correlati - Nascondi nel feed - Nascosto nel feed - Mostrato nel feed - Nascondi nella ricerca - Nascosto nella ricerca - Mostrato nella ricerca - Nascondi nei video correlati - Nascosto nei video correlati - Mostrato in video correlati - - - Nascondi il pulsante del microfono fluttuante - Pulsante microfono nascosto - Pulsante microfono mostrato - - + Disabilita la modalità ambiente a schermo intero Modalità ambiente disabilitata Modalità Ambient abilitata - + Nascondi schede info Le schede informative verranno nascoste Le schede informative verranno mostrate - + Disabilita animazioni numero rolling I numeri di rolling non sono animati I numeri di rotolamento sono animati - + Nascondi la barra di ricerca nel lettore video La barra di ricerca del lettore video è nascosta La barra di ricerca del lettore video è mostrata @@ -597,16 +590,15 @@ This is because Crowdin requires temporarily flattening this file and removing t La barra di ricerca delle miniature è nascosta Barra di ricerca miniature mostrata - + + Riproduttore Shorts + Nascondi o mostra i componenti nel riproduttore Shorts Nascondi Shorts nella scheda Home - Pantaloncini nel feed domestico sono nascosti - Vengono mostrati i pantaloncini in home feed Nascondi Shorts nel feed di abbonamento Shorts in abbonamento feed sono nascosti - Vengono mostrati i resort in abbonamento - Nascondi ricordi nei risultati di ricerca + Nascondi i Video Short nei risultati delle ricerche Shorts nei risultati di ricerca sono nascosti Vengono visualizzati gli Shorts nei risultati di ricerca @@ -695,27 +687,27 @@ This is because Crowdin requires temporarily flattening this file and removing t La barra di navigazione è nascosta Barra di navigazione mostrata - + Disabilita la schermata finale del video suggerita I video suggeriti saranno disabilitati Verranno mostrati i video suggeriti - + Nascondi timestamp video Il timestamp è nascosto Marcatura oraria mostrata - + Nascondi i pannelli popup del giocatore I pannelli popup del giocatore sono nascosti I pannelli popup del giocatore sono mostrati - + Opacità sovrapposizione del giocatore Valore di opacità tra 0-100, dove 0 è trasparente L\'opacità della sovrapposizione del lettore deve essere compresa tra 0-100 - + API dei Dislike temporaneamente non disponibile Non piace (stato %d) @@ -759,17 +751,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Limite di velocità client rilevato %d volte %d millisecondi - + Abilita barra di ricerca larga La barra di ricerca ampia è abilitata L\'ampia barra di ricerca è disabilitata - + + Abilita miniature di alta qualità + Le miniature di Seekbar sono di alta qualità + Le miniature di Seekbar sono di media qualità + Le miniature della barra di ricerca a schermo intero sono di alta qualità + Le miniature della barra di ricerca a schermo intero sono di media qualità + Questo ripristinerà anche le miniature sui livestreams che non hanno miniature nella barra di ricerca. Le miniature\n\nSeekbar useranno la stessa qualità del video corrente.\n\nQuesta funzione funziona al meglio con una qualità video di 720p o inferiore e quando si utilizza una connessione internet molto veloce. Ripristina vecchie miniature della barra di ricerca Le miniature della barra di ricerca appariranno sopra la barra di ricerca Le miniature della barra di ricerca appariranno a schermo intero - + Abilita SponsorBlock SponsorBlock è un sistema crowd-sourced per saltare parti fastidiose dei video di YouTube Aspetto @@ -950,7 +948,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Informazioni I dati sono forniti dall\'API di SponsorBlock. Tocca qui per saperne di più e vedere i download per altre piattaforme - + Versione di Spoof app Versione spoofed Versione non spoofed @@ -963,9 +961,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Ripristina velocità video larga & menu qualità 18.09.39 - Ripristina scheda libreria 17.41.37 - Ripristina vecchi ripiani playlist - 17.33.42 - Ripristina il vecchio layout UI - + Imposta pagina iniziale Predefinito Sfoglia canali @@ -983,18 +980,26 @@ This is because Crowdin requires temporarily flattening this file and removing t Tendenze Guarda più tardi - + Disabilita il ripristino del giocatore Shorts Il giocatore Shorts non riprenderà all\'avvio dell\'app Il giocatore Shorts riprenderà all\'avvio dell\'app - + + Riproduci automaticamente gli Short + Gli Short verranno riprodotti automaticamente + Gli Short si ripeteranno + Riproduzione automatica di Shorts in background + Gli Short in background verranno riprodotti automaticamente + Gli Short in background si ripeteranno + + Abilita disposizione tablet Disposizione tablet abilitata Il layout del tablet è disabilitato I post della comunità non vengono visualizzati sui layout dei tablet - + Miniplayer Cambia lo stile del miniplayer nell\'app Tipo di Miniplayer @@ -1013,6 +1018,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Abilita drag and drop Drag and drop è abilitato\n\nMiniplayer può essere trascinato in qualsiasi angolo dello schermo Trascinare e rilasciare è disabilitato + Abilita il gesto di trascinamento orizzontale + Gesto di trascinamento orizzontale abilitato\n\nMiniplayer può essere trascinato fuori schermo a sinistra o destra + Gesto di trascinamento orizzontale disabilitato Nascondi pulsante di chiusura Il pulsante di chiusura è nascosto Il pulsante Chiudi è mostrato @@ -1032,12 +1040,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Valore di opacità tra 0-100, dove 0 è trasparente L\'opacità della sovrapposizione Miniplayer deve essere compresa tra 0-100 - + Abilita schermata di caricamento gradiente Lo schermo di caricamento avrà uno sfondo gradiente Lo schermo di caricamento avrà uno sfondo solido - + Abilita colore personalizzato della barra di ricerca Il colore personalizzato della barra di ricerca è mostrato Il colore originale della barra di ricerca è mostrato @@ -1045,12 +1053,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Il colore della barra di ricerca Valore colore seekbar non valido - + Bypass restrizioni regione immagine Uso host immagine yt4.ggpht.com Usando l\'host immagine originale\n\nAbilitando questo si possono correggere le immagini mancanti bloccate in alcune regioni - + Scheda home @@ -1082,7 +1090,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow temporaneamente non disponibile (codice %s) DeArrow temporaneamente non disponibile - + Mostra annunci commentati Gli annunci sono mostrati all\'avvio Gli annunci non sono mostrati all\'avvio @@ -1090,47 +1098,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Connessione al provider di annunci non riuscita Chiudi - + Attenzione La cronologia dell\'orologio non è stata salvata.<br><br>Questo molto probabilmente è causato da un blocco annunci DNS o da un proxy di rete.<br><br>Per risolvere questo problema, whitelist <b>s.youtube.com</b> o disattiva tutti i DNS bloccanti e proxy. Non mostrare più - + Abilita ripetizione automatica La ripetizione automatica è abilitata La ripetizione automatica è disattivata - + Dimensioni del dispositivo Dimensioni del dispositivo simulate\n\nLe qualità video più elevate potrebbero essere sbloccate, ma si possono verificare stuttering nella riproduzione video, peggiore durata della batteria ed effetti collaterali sconosciuti Dimensioni dispositivo non simulate\n\nAbilitare questo può sbloccare qualità video superiori Abilitando questo può causare stuttering nella riproduzione video, peggiore durata della batteria ed effetti collaterali sconosciuti. - + Impostazioni GmsCore Impostazioni per GmsCore - + Reindirizza gli URL di bypass I reindirizzamenti URL sono bypassati I reindirizzamenti URL non sono aggirati - + Apri link nel browser Apertura dei collegamenti esternamente Apertura link nell\'app - + Rimuovere il parametro di tracking query Il parametro di tracciamento della query viene rimosso dai link Il parametro di tracciamento della query non viene rimosso dai link - + Disabilita zoom haptics Haptics sono disabilitati Haptics sono abilitati - + Qualità automatica Ricorda i cambiamenti di qualità video I cambiamenti di qualità si applicano a tutti i video @@ -1141,35 +1149,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Modificato la qualità predefinita %1$s in: %2$s - + Mostra il pulsante Velocità Video Il bottone è visibile Il pulsante non è mostrato - + + Menu di velocità di riproduzione personalizzato + Viene mostrato il menu di velocità personalizzato + Il menu di velocità personalizzato non è mostrato Velocità di riproduzione personalizzate - Aggiungi o cambia la velocità di riproduzione disponibile + Aggiungi o modifica la velocità di riproduzione personalizzata Le velocità personalizzate devono essere inferiori a %s. Utilizzando i valori predefiniti. Velocità di riproduzione personalizzata non valide. Utilizzando i valori predefiniti. - + Ricorda le modifiche della velocità di riproduzione Le modifiche alla velocità di riproduzione si applicano a tutti i video Le modifiche della velocità di riproduzione si applicano solo al video corrente Velocità di riproduzione predefinita Cambiato la velocità predefinita a: %s - + Ripristina il vecchio menu di qualità video Viene mostrato il vecchio menu di qualità video Il vecchio menu di qualità video non è mostrato - + Abilita la diapositiva da cercare La diapositiva per cercare è abilitata La diapositiva per cercare non è abilitata - + Spoof flussi video Abbandonare i flussi video client per evitare problemi di riproduzione Spoof flussi video @@ -1187,17 +1198,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Android VR spoofing effetti collaterali • Il menu traccia audio è mancante\n• Volume stabile non disponibile - - - - + Blocca annunci audio Gli annunci audio sono bloccati Gli annunci audio sono sbloccati - + %s non è disponibile. Gli annunci potrebbero mostrare. Prova a passare ad un altro servizio di blocco annunci nelle impostazioni. Il server %s ha restituito un errore. Gli annunci potrebbero mostrare. Prova a passare ad un altro servizio di blocco annunci nelle impostazioni. Blocca annunci video incorporati @@ -1205,30 +1213,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Luminous proxy PurpleAdBlock proxy - + Blocca annunci video Gli annunci video sono bloccati Annunci video sbloccati - + messaggio eliminato Mostra messaggi eliminati Non mostrare i messaggi eliminati Nascondi i messaggi eliminati dietro uno spoiler Mostra i messaggi eliminati come testo eliminato - + Richiama automaticamente i punti del canale I punti del canale vengono rivendicati automaticamente I punti del canale non vengono rivendicati automaticamente - + Abilita la modalità debug Twitch La modalità debug Twitch è abilitata (non consigliato) La modalità debug Twitch è disabilitata - + Impostazioni Avanzate Annunci Impostazioni blocco pubblicità diff --git a/src/main/resources/addresources/values-iw-rIL/strings.xml b/patches/src/main/resources/addresources/values-iw-rIL/strings.xml similarity index 76% rename from src/main/resources/addresources/values-iw-rIL/strings.xml rename to patches/src/main/resources/addresources/values-iw-rIL/strings.xml index 58ee6e5df..940f6f81e 100644 --- a/src/main/resources/addresources/values-iw-rIL/strings.xml +++ b/patches/src/main/resources/addresources/values-iw-rIL/strings.xml @@ -32,25 +32,35 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + איפוס + הפעלה מחדש + ייבוא + העתק + הגדרות ReVanced אופסו לברירת המחדל + יבוא/ ייצוא + יבוא/ ייצוא הגדרות ReVanced - + + המשך - + אודות שונות + וידאו - + + + איתור באגים - + @@ -66,30 +76,31 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + + אוטומטי - + - + @@ -99,25 +110,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - לחצן שידור מסך מוסתר - לחצן שידור מסך מוצג - - + - + + @@ -128,31 +131,24 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + לחצן שידור מסך מוסתר + לחצן שידור מסך מוצג + - + - + - - - - - - - - - - - + כרטיסי המידע מוסתרים כרטיסי המידע מוצגים - + - + - + @@ -160,26 +156,26 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + אודות - + - + - + הפעל את SponsorBlock מראה @@ -235,85 +231,86 @@ This is because Crowdin requires temporarily flattening this file and removing t איפוס אודות - + - + - + - + - + - + - + - + - + + + - + התעלם - + אזהרה + אל תציג שוב - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + הושבת - + - + - + - + - + + הגדרות ReVanced שונות diff --git a/src/main/resources/addresources/values-ja-rJP/strings.xml b/patches/src/main/resources/addresources/values-ja-rJP/strings.xml similarity index 93% rename from src/main/resources/addresources/values-ja-rJP/strings.xml rename to patches/src/main/resources/addresources/values-ja-rJP/strings.xml index ef2f99d84..c5868782b 100644 --- a/src/main/resources/addresources/values-ja-rJP/strings.xml +++ b/patches/src/main/resources/addresources/values-ja-rJP/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + チェックに失敗しました 公式ウェブサイトを開く 無視 @@ -41,8 +41,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 10分以上前にパッチを適用しました APKビルド日付が破損しています - - ReVanced + 続行しますか? リセット 更新して再起動 @@ -61,7 +60,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 公式リンク 寄付 - + MicroG GmsCoreがインストールされていません。インストールしてください。 操作が必要です @@ -72,7 +71,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + このアプリについて 広告 代替サムネイル @@ -84,7 +83,11 @@ This is because Crowdin requires temporarily flattening this file and removing t その他 動画 - + + Shorts のバックグラウンド再生は無効になっています + Shorts のバックグラウンド再生は有効になっています + + デバッグ デバッグオプションを有効または無効にする デバッグログ @@ -101,13 +104,19 @@ This is because Crowdin requires temporarily flattening this file and removing t エラーが発生した場合、トーストは表示されません エラートーストをオフにすると、すべてのReVancedエラー通知が非表示になります。\n\n予期せぬイベントは通知されません。 - + 高評価 / チャンネル登録ボタンのアニメーションを無効にする 「高評価」と「チャンネル登録」ボタンのアニメーションは無効です 「高評価」と「チャンネル登録」ボタンのアニメーションは有効です - 灰色のセパレータを非表示 - 灰色のセパレータは非表示です - 灰色のセパレータは表示されます + アルバムカードを隠す + アルバムカードは非表示です + アルバムカードは表示されます + クラウドファンディングボックスを非表示 + クラウドファンディングボックスは非表示です + クラウドファンディングボックスは表示されます + 音声入力のフローティングボタンを非表示 + マイクボタンは非表示です + マイクボタンは表示されます チャンネルの透かしを非表示 透かしは非表示です 透かしは表示されます @@ -152,9 +161,6 @@ This is because Crowdin requires temporarily flattening this file and removing t 動画の下に表示される展開可能なチップを非表示 展開可能なチップは非表示です 展開可能なチップは表示されます - 画質メニューのフッターを非表示 - 画質メニューのフッターは非表示です - 画質メニューのフッターは表示されます コミュニティの投稿を非表示にする コミュニティの投稿は非表示です コミュニティの投稿は表示されます @@ -229,6 +235,37 @@ This is because Crowdin requires temporarily flattening this file and removing t 文字起こしセクションは表示されます 概要欄 概要欄のコンポーネントを非表示または表示 + フィルタバー + フィード、検索、および関連する動画のフィルターバーを非表示または表示 + フィードで非表示 + フィードで非表示です + フィードでは表示されます + 検索時に隠す + 検索で非表示です + 検索で表示されます + 関連動画で非表示 + 関連動画で非表示です + 関連動画で表示されます + コメント + コメントセクションのコンポーネントを非表示または表示 + 「メンバーによるコメント」ヘッダーを非表示 + 「メンバーによるコメント」ヘッダーは非表示です + 「メンバーによるコメント」ヘッダーは表示されています + コメントセクションを非表示 + コメントセクションは非表示です + コメントセクションは表示されます + 「Shortsを作成」ボタンを非表示 + 「Shortsを作成」ボタンは非表示です + 「Shorsを作成」ボタンは表示されます + コメントのプレビューを非表示 + コメントのプレビューは非表示です + コメントのプレビューは表示されます + Thanks ボタンを非表示 + 「Thanks」ボタンは非表示です + 「Thanks」ボタンは表示されます + タイムスタンプと絵文字ボタンを隠す + タイムスタンプと絵文字ボタンは非表示です + タイムスタンプと絵文字ボタンは表示されます YouTubeのDoodlesを隠す 検索バーの落書きは非表示です @@ -268,7 +305,7 @@ This is because Crowdin requires temporarily flattening this file and removing t キーワードが短すぎるため見積もりが必要です: %s キーワードはすべてのビデオを非表示にします: %s - + 一般的な広告を非表示 一般的な広告は非表示です 一般的な広告は表示されます @@ -287,6 +324,9 @@ This is because Crowdin requires temporarily flattening this file and removing t 商品を見るためのバナーを隠す バナーは非表示です バナーは表示されます + プレイヤーのショッピング棚を非表示 + ショッピング棚は非表示です + ショッピング棚が表示されます ビデオの説明内のショッピングリンクを非表示 ショッピングのリンクは非表示です ショッピングのリンクは表示されます @@ -303,17 +343,17 @@ This is because Crowdin requires temporarily flattening this file and removing t 全画面広告の非表示は、古い端末でのみ動作します - + YouTube Premium の広告を非表示 YouTube Premium の広告は非表示です YouTube Premium の広告は表示されます - + 動画広告を隠す 動画広告は非表示です 動画広告は表示されます - + URL をクリップボードにコピーしました タイムスタンプ付きのURLがコピーされました 動画のURLをコピーボタンを表示 @@ -323,13 +363,13 @@ This is because Crowdin requires temporarily flattening this file and removing t ボタンが表示されます。タップするとタイムスタンプ付きの動画URLをコピーできます。長押しするとタイムスタンプなしで動画をコピーできます。 ボタンは表示されません - + 「ご自身の責任」ダイアログを削除 ダイアログは削除されます ダイアログは表示されます これは年齢制限を回避するものではなく、ダイアログを自動的に承認するだけです。 - + 外部ダウンロード 外部ダウンローダーの設定 外部ダウンロードボタンを表示 @@ -343,17 +383,17 @@ This is because Crowdin requires temporarily flattening this file and removing t NewPipeやSealなど、インストールされている外部ダウンローダーアプリのパッケージ名 %s はインストールされていません。インストールしてください。 - + シークジェスチャーを無効にする ジェスチャーは無効です ジェスチャーは有効です - + シークバータップを有効にする シークバーのタップは有効です シークバーのタップは無効です - + 明るさジェスチャーを有効にする 明るさスワイプは有効です 明るさスワイプは無効です @@ -382,12 +422,12 @@ This is because Crowdin requires temporarily flattening this file and removing t スワイプの大きさのしきい値 スワイプとして検出する量のしきい値 - + 自動字幕を無効にする 自動字幕は無効です 自動字幕は有効です - + アクションボタン ビデオ下のボタンを非表示または表示 高評価と低評価を隠す @@ -423,23 +463,7 @@ This is because Crowdin requires temporarily flattening this file and removing t プレイリストに保存ボタンは非表示です プレイリストに保存ボタンは表示されます - - 自動再生ボタンを隠す - 自動再生ボタンは非表示です - 自動再生ボタンは表示されます - - - - 字幕ボタンを隠す - 字幕ボタンは非表示です - 字幕ボタンが表示されます - - - キャストボタンを隠す - キャスト ボタンは非表示です - キャスト ボタンは表示されます - - + ナビゲーションボタン ナビゲーションバーのボタンを非表示または変更 @@ -466,7 +490,7 @@ This is because Crowdin requires temporarily flattening this file and removing t ラベルは非表示です ラベルは表示されます - + フライアウトメニュー プレイヤーフライアウトメニューアイテムを非表示または表示 @@ -477,6 +501,10 @@ This is because Crowdin requires temporarily flattening this file and removing t 追加設定を非表示にする 追加設定メニューは非表示です 追加設定メニューは表示されます + + スリープタイマーを隠す + スリープタイマーメニューは非表示です + スリープタイマーメニューが表示されます 動画ループを隠す 動画ループメニューは非表示です @@ -485,6 +513,9 @@ This is because Crowdin requires temporarily flattening this file and removing t アンビエントモードを隠す アンビエントモードメニューは非表示です アンビエントモードメニューは表示されます + 安定した音量を隠す + 安定した音量メニューが表示されます + 安定した音量メニューは非表示です ヘルプとフィードバックを非表示にする ヘルプとフィードバックメニューは非表示です @@ -510,83 +541,46 @@ This is because Crowdin requires temporarily flattening this file and removing t VRで見るを隠す VRで見るメニューは非表示です VR で見るメニューは表示されます + 画質メニューのフッターを非表示 + ビデオ品質のメニューフッターは非表示です + ビデオ品質のメニューフッターが表示されます - - 前の動画に戻る/次の動画に進むボタンを非表示 - ボタンは非表示です - ボタンは表示されます + + 前の動画に戻る/次の動画に進むボタンを非表示 + ボタンは非表示です + ボタンは表示されます + キャストボタンを隠す + キャスト ボタンは非表示です + キャスト ボタンは表示されます + + 字幕ボタンを隠す + 字幕ボタンは非表示です + 字幕ボタンが表示されます + 自動再生ボタンを隠す + 自動再生ボタンは非表示です + 自動再生ボタンは表示されます - - アルバムカードを隠す - アルバムカードは非表示です - アルバムカードは表示されます - - - コメント - コメントセクションのコンポーネントを非表示または表示 - 「メンバーによるコメント」ヘッダーを非表示 - 「メンバーによるコメント」ヘッダーは非表示です - 「メンバーによるコメント」ヘッダーは表示されています - コメントセクションを非表示 - コメントセクションは非表示です - コメントセクションは表示されます - 「Shortsを作成」ボタンを非表示 - 「Shortsを作成」ボタンは非表示です - 「Shorsを作成」ボタンは表示されます - コメントのプレビューを非表示 - コメントのプレビューは非表示です - コメントのプレビューは表示されます - Thanks ボタンを非表示 - 「Thanks」ボタンは非表示です - 「Thanks」ボタンは表示されます - タイムスタンプと絵文字ボタンを隠す - タイムスタンプと絵文字ボタンは非表示です - タイムスタンプと絵文字ボタンは表示されます - - - クラウドファンディングボックスを非表示 - クラウドファンディングボックスは非表示です - クラウドファンディングボックスは表示されます - - + 終了画面のカードを非表示にする 終了画面のカードは非表示です 終了画面のカードは表示されます - - フィルタバー - フィード、検索、および関連する動画のフィルターバーを非表示または表示 - フィードで非表示 - フィードで非表示です - フィードでは表示されます - 検索時に隠す - 検索で非表示です - 検索で表示されます - 関連動画で非表示 - 関連動画で非表示です - 関連動画で表示されます - - - 音声入力のフローティングボタンを非表示 - マイクボタンは非表示です - マイクボタンは表示されます - - + 全画面表示でアンビエントモードを無効にする アンビエントモードは無効です アンビエントモードは有効です - + 情報カードを隠す カードは非表示です カードは表示されています - + 数字のアニメーションを無効にする 数字のアニメーションは無効です 数字のアニメーションは有効です - + ビデオプレーヤーでシークバーを隠す ビデオプレーヤーのシークバーは非表示です ビデオプレーヤーのシークバーが表示されます @@ -594,18 +588,11 @@ This is because Crowdin requires temporarily flattening this file and removing t サムネイルシークバーが非表示です サムネイルシークバーが表示されます - + + Shortsプレイヤー + Shorts プレーヤーのコンポーネントを非表示または表示 - ホームフィードにショートパンツを隠す - ホームフィード内のショートパンツが表示されません - ホームフィードに短縮形が表示されます - サブスクリプションフィード内のショーツを非表示 - サブスクリプションフィードのショートパンツは非表示です - サブスクリプションフィードの短縮形が表示されます - 検索結果にショーツを隠す - 検索結果の短縮形は非表示です - 検索結果の短縮形が表示されます 参加ボタンを隠す 結合ボタンは非表示です @@ -692,27 +679,27 @@ This is because Crowdin requires temporarily flattening this file and removing t ナビゲーションバーは非表示です ナビゲーションバーを表示 - + 提案されたビデオ終了画面を無効にする 推奨動画は無効になります おすすめの動画が表示されます - + タイムスタンプを隠す タイムスタンプは非表示です タイムスタンプを表示 - + プレーヤーのポップアップパネルを隠す プレーヤーのポップアップパネルが非表示になります プレーヤーのポップアップパネルが表示されます - + プレイヤーオーバーレイの透明度 透明度の値は 0〜100 の範囲で、0 が透明です プレイヤーオーバーレイの不透明度は0-100の間でなければなりません - + Return YouTube Dislike は一時的に利用できません (API タイムアウト) Return YouTube Dislikeは利用できません (ステータス %d) @@ -756,17 +743,22 @@ This is because Crowdin requires temporarily flattening this file and removing t クライアントレート制限が %d 回発生しました %d ミリ秒前 - + ワイド検索バーを有効にする ワイド検索バーは有効です ワイド検索バーは無効です - + + 高画質サムネイルを有効にする + シークバーのサムネイルは高画質です + シークバーのサムネイルの品質は中程度です + 全画面表示のサムネイルの画質が高い + 全画面表示のサムネイルの品質は中程度です 古いシークバーのサムネイルを復元 シークバーのサムネイルがシークバーの上に表示されます シークバーのサムネイルが全画面表示されます - + SponsorBlock を有効にする SponsorBlockは、YouTube動画の厄介な部分をスキップするためのクラウドソースのシステムです 外観 @@ -947,7 +939,7 @@ This is because Crowdin requires temporarily flattening this file and removing t このアプリについて SponsorBlock APIによって提供されるデータです。詳細はこちらをタップしてください。 - + アプリのバージョンを偽装する バージョン偽装済み バージョンは偽装されていません @@ -960,9 +952,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - ワイドビデオスピード & クオリティメニューを復元 18.09.39 - ライブラリタブを復元 17.41.37 - 古いプレイリストシェルフを復元する - 17.33.42 - 古いUIレイアウトを復元 - + 開始ページを設定 既定 チャンネルを参照 @@ -980,18 +971,22 @@ This is because Crowdin requires temporarily flattening this file and removing t トレンド 後で見る - + Shorts プレイヤーの再開を無効にする Shorts プレイヤーはアプリの起動時に再開しません Shorts プレイヤーはアプリの起動時に再開します - + + Shorts は自動再生されます + Shorts は繰り返し再生されます + + タブレットのレイアウトを有効にする タブレットのレイアウトは有効です タブレットのレイアウトは無効です タブレットのレイアウトではコミュニティ投稿は表示されません - + ミニプレイヤー アプリの最小化プレイヤーのスタイルを変更する ミニプレーヤータイプ @@ -1009,6 +1004,9 @@ This is because Crowdin requires temporarily flattening this file and removing t ドラッグ&ドロップを有効にする ドラッグ&ドロップが有効です\n\nミニプレーヤーは画面の隅にドラッグできます ドラッグ&ドロップは無効です + 水平ドラッグジェスチャーを有効にする + 水平ドラッグジェスチャーを有効にした\n\nミニプレーヤーは画面の左右にドラッグできます + 水平ドラッグジェスチャーが無効になっています 閉じるボタンを隠す 閉じるボタンは非表示です 閉じるボタンが表示されます @@ -1027,12 +1025,12 @@ This is because Crowdin requires temporarily flattening this file and removing t 透明度の値は 0〜100 の範囲で、0 が透明です ミニプレーヤーオーバーレイの不透明度は0-100の間でなければなりません - + グラデーション読み込み画面を有効にする 画面をロードするとグラデーションの背景が表示されます 画面を読み込むと背景が正しく表示されます - + カスタムシークバーの色を有効にする カスタムシークバーの色を表示する 元のシークバーの色が表示されます @@ -1040,12 +1038,12 @@ This is because Crowdin requires temporarily flattening this file and removing t シークバーの色 無効なシークバーの色の値 - + 画像表示の地域制限をバイパスする 画像表示の地域制限を回避するために、 yt4.ggpht.com から画像を取得します。 オリジナルの画像ホストを使用する\n\nこれを有効にすると、一部の地域でブロックされている欠落画像を修正できます - + ホームタブ @@ -1077,7 +1075,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrowは一時的に利用できません(ステータスコード: %s) DeArrowは一時的に利用できません - + ReVancedアナウンスを表示 起動時にお知らせが表示されます 起動時にお知らせは表示されません @@ -1085,47 +1083,47 @@ This is because Crowdin requires temporarily flattening this file and removing t アナウンスプロバイダーへの接続に失敗しました 無視 - + 警告 再生履歴は保存されていません。<br><br>これは、DNS 広告ブロッカーまたはネットワーク プロキシが原因である可能性があります。<br><br>この問題を解決するには、<b>s.youtube.com</b> をホワイトリストに追加するか、すべての DNS ブロッカーとプロキシをオフにしてください。 今後表示しない - + 自動ループ再生を有効化 自動ループ再生は有効です 自動ループ再生は無効です - + 端末の寸法を偽装する 端末の寸法なりすまし\n\nより高いビデオ品質がロック解除される可能性がありますが、ビデオ再生のスタッタリング、バッテリー寿命の悪化、および未知の副作用が発生する可能性があります デバイスの寸法は偽装されていません\n\nこれを有効にすると、より高い画質のビデオが再生可能になります これを有効にすると、ビデオ再生の吃音、バッテリー寿命の悪化、および不明な副作用を引き起こす可能性があります。 - + GmsCore設定 GmsCoreの設定 - + URLリダイレクトをバイパス URL リダイレクトはバイパスされます URL リダイレクトはバイパスされません - + ブラウザでリンクを開く 外部リンクを開く アプリ内でリンクを開く - + トラッキングクエリパラメータを削除 トラッキングクエリパラメータがリンクから削除されました トラッキングクエリパラメータはリンクから削除されません - + ズームした際の触覚機能を無効にする 触覚機能は無効です 触覚機能は有効です - + 自動品質 ビデオ画質の変更を記憶する 品質の変更はすべてのビデオに適用されます @@ -1136,39 +1134,43 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi デフォルトの %1$s 品質を %2$sに変更しました - + スピードダイアログボタンを表示 ボタンは表示されます ボタンは表示されません - + + カスタム再生速度メニュー + カスタムスピードメニューが表示されます + カスタムスピードメニューは表示されません カスタム再生速度 - 使用可能な再生速度を追加または変更します + 再生速度を追加または変更する カスタム速度は %s未満でなければなりません。デフォルト値を使用してください。 無効なカスタム再生速度です。デフォルト値を使用します。 - + 再生速度の変更を記憶する 再生速度の変更はすべてのビデオに適用されます 再生速度の変更は現在のビデオにのみ適用されます デフォルトの再生速度 デフォルトの速度を %sに変更しました - + 古いビデオ品質メニューを復元 古いビデオ品質のメニューが表示されます 古いビデオ品質のメニューは表示されません - + シークするスライドを有効にする Slide to seek is enabled Slide to seek is not enabled - + 動画ストリームを偽装する 再生の問題を防ぐために、クライアントのビデオストリームを偽装します 動画ストリームを偽装する ビデオストリームはなりすましています + クライアントは偽装されていません\n\n動画を再生できない可能性があります この設定をオフにすると、ビデオ再生の問題が発生する可能性があります。 デフォルトのクライアント 強制AVC (H.264) @@ -1177,22 +1179,18 @@ This is because Crowdin requires temporarily flattening this file and removing t お使いのデバイスにはVP9ハードウェアデコードがありません。この設定はクライアントのスプーフィングが有効になっているときに常に有効になります これを有効にするとバッテリー寿命と再生の途切れが改善する可能性があります。\n\nAVCの最大解像度は1080pで、ビデオ再生はVP9やAV1よりも多くの通信量を使用します。 iOSのクライアント偽装での副作用 + • 映画や有料動画は再生できない場合があります\n•ライブは最初から再生されます\n• 動画が 1 秒早く終了する場合があります\n• Opus オーディオは使用できません。 Android-VR クライアント偽装の副作用 - - - - 自動HDR明るさを有効にする - 自動HDRの明るさが有効です - 自動HDRの明るさが無効です + •「音声トラック」メニューは表示されません\n•「一定音量」は使用できません - + オーディオ広告をブロック オーディオ広告はブロックされています オーディオ広告のブロックが解除されました - + %s は利用できません。広告が表示される場合があります。設定から別の広告ブロックサービスに切り替えてみてください。 %s サーバーがエラーを返しました。広告が表示される場合があります。設定で別の広告ブロックサービスに切り替えてみてください。 埋め込みビデオ広告をブロック @@ -1200,30 +1198,30 @@ This is because Crowdin requires temporarily flattening this file and removing t 光沢のあるプロキシ PurpleAdBlock プロキシ - + ビデオ広告をブロック ビデオ広告はブロックされています ビデオ広告のブロックが解除されました - + メッセージが削除されました 削除されたメッセージを表示 削除されたメッセージを表示しない スポイラーの後ろに削除されたメッセージを非表示にする クロスアウトテキストとして削除されたメッセージを表示 - + チャンネルポイントを自動的に獲得する チャンネルポイントは自動的に請求されます チャンネルポイントは自動的に請求されません - + Twitch デバッグモードを有効にする Twitch デバッグモードが有効になっています(非推奨) Twitchデバッグモードは無効です - + Revancedの設定 広告 広告ブロックの設定 diff --git a/patches/src/main/resources/addresources/values-ka-rGE/strings.xml b/patches/src/main/resources/addresources/values-ka-rGE/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-ka-rGE/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/addresources/values-kk-rKZ/strings.xml b/patches/src/main/resources/addresources/values-kk-rKZ/strings.xml similarity index 70% rename from src/main/resources/addresources/values-kk-rKZ/strings.xml rename to patches/src/main/resources/addresources/values-kk-rKZ/strings.xml index 942ca5b50..db8c83a68 100644 --- a/src/main/resources/addresources/values-kk-rKZ/strings.xml +++ b/patches/src/main/resources/addresources/values-kk-rKZ/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,106 +139,105 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + Назар аударыңыз - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/src/main/resources/addresources/values-km-rKH/strings.xml b/patches/src/main/resources/addresources/values-km-rKH/strings.xml similarity index 70% rename from src/main/resources/addresources/values-km-rKH/strings.xml rename to patches/src/main/resources/addresources/values-km-rKH/strings.xml index 4ed6c0438..df1cbc55d 100644 --- a/src/main/resources/addresources/values-km-rKH/strings.xml +++ b/patches/src/main/resources/addresources/values-km-rKH/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,106 +139,105 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + ការព្រមាន - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/patches/src/main/resources/addresources/values-kn-rIN/strings.xml b/patches/src/main/resources/addresources/values-kn-rIN/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-kn-rIN/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/addresources/values-ko-rKR/strings.xml b/patches/src/main/resources/addresources/values-ko-rKR/strings.xml similarity index 91% rename from src/main/resources/addresources/values-ko-rKR/strings.xml rename to patches/src/main/resources/addresources/values-ko-rKR/strings.xml index 3fdc87c40..bd649e40a 100644 --- a/src/main/resources/addresources/values-ko-rKR/strings.xml +++ b/patches/src/main/resources/addresources/values-ko-rKR/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + 환경 검사에 실패함 공식 홈페이지 열기 닫기 @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t %s 일 전에 패치됨 APK 빌드 날짜가 손상됨 - + ReVanced 계속하시겠습니까? 초기화 @@ -63,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 공식 링크 후원 - + MicroG GmsCore가 설치되어 있지 않습니다. 설치하세요 필수 조치 @@ -74,7 +74,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + 정보 광고 대체 썸네일 @@ -86,7 +86,12 @@ This is because Crowdin requires temporarily flattening this file and removing t 기타 동영상 - + + Shorts 백그라운드 재생 비활성화하기 + Shorts 백그라운드 재생을 비활성화합니다 + Shorts 백그라운드 재생을 활성화합니다 + + 디버깅 디버깅 옵션을 활성화하거나 비활성화할 수 있습니다 디버그 로깅 @@ -96,20 +101,26 @@ This is because Crowdin requires temporarily flattening this file and removing t 디버그 로그에 프로토콜 버퍼를 포함합니다 디버그 로그에 프로토콜 버퍼를 포함하지 않습니다 로그 스택 트레이스 - 디버그 로그에 로그 스택 트레이스을 포함합니다 - 디버그 로그에 로그 스택 트레이스을 포함하지 않습니다 + 디버그 로그에 로그 스택 트레이스를 포함합니다 + 디버그 로그에 로그 스택 트레이스를 포함하지 않습니다 ReVanced 오류 팝업 메시지 표시하기 오류가 발생하면 팝업 메시지를 표시합니다 오류가 발생하면 팝업 메시지를 표시하지 않습니다 오류 메시지를 비활성화하면 모든 ReVanced 오류 알림이 숨겨집니다\n\n예상되지 않은 이벤트에 대한 알림을 받지 못할 수 있습니다 - + 빛나는 \'좋아요\' / \'구독\' 버튼 비활성화하기 동영상에서 \'Like (좋아요)\' 또는 \'Subscribe (구독)\' 버튼이 언급되었을 때, 해당 버튼에 빛나는 애니메이션을 적용하지 않습니다\n• 일부 언어는 아직 지원되지 않습니다 동영상에서 \'Like (좋아요)\' 또는 \'Subscribe (구독)\' 버튼이 언급되었을 때, 해당 버튼에 빛나는 애니메이션을 적용합니다\n• 일부 언어는 아직 지원되지 않습니다 - 회색 구분선 숨기기 - 동영상들 사이에서 회색 구분선이 숨겨집니다 - 동영상들 사이에서 회색 구분선이 표시됩니다 + 음악 앨범 카드 숨기기 + 검색 결과에서 음악 앨범 카드가 숨겨집니다 + 검색 결과에서 음악 앨범 카드가 표시됩니다 + 크라우드 펀딩 박스 숨기기 + 플레이어 하단에서 크라우드 펀딩 박스가 숨겨집니다 + 플레이어 하단에서 크라우드 펀딩 박스가 표시됩니다 + 플로팅 마이크 버튼 숨기기 + 플로팅 마이크 버튼이 숨겨집니다 + 플로팅 마이크 버튼이 표시됩니다 동영상 하단에서 채널 워터마크 숨기기 워터마크가 숨겨집니다 워터마크가 표시됩니다 @@ -154,9 +165,6 @@ This is because Crowdin requires temporarily flattening this file and removing t 펼쳐볼 수 있는 정보 숨기기 썸네일 하단에서 다음 정보들이 숨겨집니다:\n동영상 설명, 챕터, 주요 순간, 스크립트,\n재생목록의 동영상, 이 동영상에 나온 제품 썸네일 하단에서 다음 정보들이 표시됩니다:\n동영상 설명, 챕터, 주요 순간, 스크립트,\n재생목록의 동영상, 이 동영상에 나온 제품 - 화질 설정 메뉴에서 하단 설명 숨기기 - 화질 설정 메뉴에서 하단 설명이 숨겨집니다 - 화질 설정 메뉴에서 하단 설명이 표시됩니다 커뮤니티 게시물 숨기기 커뮤니티 게시물이 숨겨집니다 커뮤니티 게시물이 표시됩니다 @@ -231,10 +239,41 @@ This is because Crowdin requires temporarily flattening this file and removing t 스크립트 섹션이 표시됩니다 동영상 설명 동영상 설명에서 구성요소를 숨기거나 표시할 수 있습니다 + 카테고리 바 + 피드, 검색 결과, 관련 동영상에서 카테고리 바를 숨기거나 표시할 수 있습니다 + 피드에서 카테고리 바 숨기기 + 피드에서 카테고리 바가 숨겨집니다 + 피드에서 카테고리 바가 표시됩니다 + 검색 결과에서 카테고리 바 숨기기 + 검색 결과에서 카테고리 바가 숨겨집니다 + 검색 결과에서 카테고리 바가 표시됩니다 + 관련 동영상에서 카테고리 바 숨기기 + 플레이어 하단에 있는 관련 동영상에서 카테고리 바가 숨겨집니다 + 플레이어 하단에 있는 관련 동영상에서 카테고리 바가 표시됩니다 + 댓글 + 댓글 섹션에서 구성요소가 숨기거나 표시할 수 있습니다 + \'회원별 댓글\' 헤더 숨기기 + \'회원별 댓글\' 헤더가 숨겨집니다 + \'회원별 댓글\' 헤더가 표시됩니다 + 댓글 섹션 숨기기 + 댓글 섹션이 숨겨집니다 + 댓글 섹션이 표시됩니다 + \'Shorts 만들기\' 버튼 숨기기 + \'Shorts 만들기\' 버튼이 숨겨집니다 + \'Shorts 만들기\' 버튼이 표시됩니다 + 댓글 미리보기 숨기기 + 댓글 미리보기가 숨겨집니다 + 댓글 미리보기가 표시됩니다 + Thanks 버튼 숨기기 + Thanks 버튼이 숨겨집니다 + Thanks 버튼이 표시됩니다 + 타임스탬프 & 이모지 버튼 숨기기 + 타임스탬프 & 이모지 버튼이 숨겨집니다 + 타임스탬프 & 이모지 버튼이 표시됩니다 YouTube Doodles 숨기기 - YouTube Doodles가 숨겨집니다\n• 이벤트성 YouTube 헤더 - YouTube Doodles가 표시됩니다\n• 이벤트성 YouTube 헤더 + YouTube Doodles가 숨겨집니다\n• Doodles: 기념일 로고 헤더 + YouTube Doodles가 표시됩니다\n• Doodles: 기념일 로고 헤더 YouTube Doodles는 공휴일이나 기념일 등, 그날에 맞춘 디자인으로 변경되는 왼쪽 상단의 YouTube 헤더를 말합니다\n\n현재 거주하는 지역에서 YouTube Doodles가 표시되어 있는데 이 설정이 활성화되어 있는 경우에는 검색창 아래에 표시되는 카테고리 바도 숨겨집니다 사용자 정의 필터 사용자 정의 필터를 사용하여 구성요소를 숨길 수 있습니다 @@ -272,7 +311,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 키워드가 너무 짧아서 따옴표가 필요합니다: %s 키워드가 모든 동영상을 숨깁니다: %s - + 일반 레이아웃 광고 숨기기 일반 레이아웃 광고가 숨겨집니다 일반 레이아웃 광고가 표시됩니다 @@ -291,6 +330,9 @@ This is because Crowdin requires temporarily flattening this file and removing t 제품 보기 배너 숨기기 플레이어에서 제품 보기 배너가 숨겨집니다 플레이어에서 제품 보기 배너가 표시됩니다 + 판매자 쇼핑 선반 숨기기 + 판매자 쇼핑 선반이 숨겨집니다\n• 판매자(크리에이터명) 선반 + 판매자 쇼핑 선반이 표시됩니다\n• 판매자(크리에이터명) 선반 동영상 설명에서 쇼핑 링크 숨기기 쇼핑 링크가 숨겨집니다 쇼핑 링크가 표시됩니다 @@ -301,23 +343,23 @@ This is because Crowdin requires temporarily flattening this file and removing t 웹 검색 결과 숨기기 웹 검색 결과가 숨겨집니다 웹 검색 결과가 표시됩니다 - 태그된 제품 선반 숨기기 - 태그된 제품 선반이 숨겨집니다 - 태그된 제품 선반이 표시됩니다 + 매장 쇼핑 선반 숨기기 + 매장 쇼핑 선반이 숨겨집니다\n• 크리에이터명 매장 쇼핑 선반 + 매장 쇼핑 선반이 표시됩니다\n• 크리에이터명 매장 쇼핑 선반 \'전체 화면 광고 숨기기\'는 구형 기기에서만 사용할 수 있습니다 - + YouTube Premium 프로모션 숨기기 YouTube Premium 프로모션이 숨겨집니다 YouTube Premium 프로모션이 표시됩니다 - + 동영상 광고 숨기기 동영상 광고가 숨겨집니다 동영상 광고가 표시됩니다 - + URL을 클립보드에 복사하였습니다 타임스탬프를 표기한 URL을 클립보드에 복사하였습니다 동영상 URL 복사 버튼 표시하기 @@ -327,13 +369,13 @@ This is because Crowdin requires temporarily flattening this file and removing t 버튼을 표시합니다. 버튼을 눌러서 타임스탬프를 표기한 동영상 URL을 복사할 수 있습니다. 길게 누르면 타임스탬프를 표기하지 않은 동영상 URL이 복사됩니다 버튼을 표시하지 않습니다 - + 시청 경고 다이얼로그 제거하기 다이얼로그가 숨겨집니다 다이얼로그가 표시됩니다 - • 이 설정은 다이얼로그를 자동으로 허용하기만 하며 연령 제한(성인인증 절차)을 우회할 수 없습니다\n• 즉, 성인인증이 필요한 동영상에서 인증을 하려 할 때, 휴대폰 번호가 필요하다고 알려주는 소형 팝업창(다이얼로그) 없이 바로 휴대폰 번호 인증 페이지가 표시됩니다 + • 이 설정은 다이얼로그를 자동으로 허용하기만 하며 연령 제한(성인인증 절차)을 우회할 수 없습니다\n• 즉, 성인인증이 필요한 동영상에서 인증을 하려 할 때, 휴대폰 번호가 필요하다고 알려주는 소형 팝업창(다이얼로그) 없이 바로 휴대폰 번호 인증 페이지가 표시됩니다\n• \'당신은 혼자가 아닙니다\' 페이지에서 \'확인하기\' 버튼이 표시되지 않는다면 이 설정이 아닌 플레이어 설정에서 \'정보 패널 숨기기\'를 비활성화해야 합니다 - + 외부 다운로드 외부 다운로더를 설정할 수 있습니다 외부 다운로드 버튼 표시하기 @@ -347,17 +389,17 @@ This is because Crowdin requires temporarily flattening this file and removing t NewPipe 또는 Seal와 같은 설치된 외부 다운로더 앱 패키지명입니다 %s 는 설치되어 있지 않습니다. 설치하세요 - + 세밀하게 보면서 탐색 제스처 비활성화하기 세밀하게 보면서 탐색 제스처를 비활성화합니다\n• 필름 스트립 오버레이 세밀하게 보면서 탐색 제스처를 활성화합니다\n• 필름 스트립 오버레이 - + 재생바 터치 조작 활성화하기 재생바 터치 조작을 활성화합니다 재생바 터치 조작을 비활성화합니다 - + 스와이프 제스처로 밝기 조절 활성화하기 스와이프 제스처로 밝기 조절을 활성화합니다 스와이프 제스처로 밝기 조절을 비활성화합니다 @@ -386,12 +428,12 @@ This is because Crowdin requires temporarily flattening this file and removing t 스와이프 한계치 제스처 인식을 위해 얼마나 스와이프를 해야 할지를 지정할 수 있으며, 원하지 않은 제스처 인식을 방지할 수 있습니다 - + 자동 자막 비활성화하기 자막 사용이 강제된 동영상에서 자막을 비활성화합니다 자막 사용이 강제된 동영상에서 자막을 활성화합니다 - + 액션 버튼 플레이어 하단에서 액션 버튼을 숨기거나 표시할 수 있습니다 좋아요 & 싫어요 버튼 숨기기 @@ -427,23 +469,7 @@ This is because Crowdin requires temporarily flattening this file and removing t (재생목록에) 저장 버튼이 숨겨집니다 (재생목록에) 저장 버튼이 표시됩니다 - - 자동재생 버튼 숨기기 - 자동재생 버튼이 숨겨집니다 - 자동재생 버튼이 표시됩니다 - - - - 자막 버튼 숨기기 - 자막 버튼이 숨겨집니다 - 자막 버튼이 표시됩니다 - - - 크롬캐스트 버튼 숨기기 - 크롬캐스트 버튼이 숨겨집니다 - 크롬캐스트 버튼이 표시됩니다 - - + 하단바 버튼 하단바에서 버튼을 숨기거나 변경할 수 있습니다 @@ -470,7 +496,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 라벨이 숨겨집니다 라벨이 표시됩니다 - + 메뉴 구성요소 플레이어에서 메뉴 구성요소를 숨기거나 표시할 수 있습니다 @@ -481,6 +507,10 @@ This is because Crowdin requires temporarily flattening this file and removing t 추가 설정 메뉴 숨기기 추가 설정 메뉴가 숨겨집니다 추가 설정 메뉴가 표시됩니다 + + 취침 타이머 메뉴 숨기기 + 취침 타이머 메뉴가 숨겨집니다 + 취침 타이머 메뉴가 표시됩니다 동영상 연속 재생 메뉴 숨기기 동영상 연속 재생 메뉴가 숨겨집니다 @@ -489,6 +519,9 @@ This is because Crowdin requires temporarily flattening this file and removing t 앰비언트 모드 메뉴 숨기기 앰비언트 모드 메뉴가 숨겨집니다 앰비언트 모드 메뉴가 표시됩니다 + 안정적인 볼륨 메뉴 숨기기 + 안정적인 볼륨 메뉴가 표시됩니다 + 안정적인 볼륨 메뉴가 숨겨집니다 고객센터 메뉴 숨기기 고객센터 메뉴가 숨겨집니다 @@ -514,83 +547,46 @@ This is because Crowdin requires temporarily flattening this file and removing t VR로 보기 메뉴 숨기기 VR로 보기 메뉴가 숨겨집니다 VR로 보기 메뉴가 표시됩니다 + 화질 설정 메뉴에서 하단 설명 숨기기 + 화질 설정 메뉴에서 하단 설명이 숨겨집니다 + 화질 설정 메뉴에서 하단 설명이 표시됩니다 - - 이전 & 다음 동영상 버튼 숨기기 - 이전 & 다음 동영상 버튼이 숨겨집니다 - 이전 & 다음 동영상 버튼이 표시됩니다 + + 이전 & 다음 동영상 버튼 숨기기 + 이전 & 다음 동영상 버튼이 숨겨집니다 + 이전 & 다음 동영상 버튼이 표시됩니다 + 크롬캐스트 버튼 숨기기 + 크롬캐스트 버튼이 숨겨집니다 + 크롬캐스트 버튼이 표시됩니다 + + 자막 버튼 숨기기 + 자막 버튼이 숨겨집니다 + 자막 버튼이 표시됩니다 + 자동재생 버튼 숨기기 + 자동재생 버튼이 숨겨집니다 + 자동재생 버튼이 표시됩니다 - - 음악 앨범 카드 숨기기 - 검색 결과에서 음악 앨범 카드가 숨겨집니다 - 검색 결과에서 음악 앨범 카드가 표시됩니다 - - - 댓글 - 댓글 섹션에서 구성요소가 숨기거나 표시할 수 있습니다 - \'회원별 댓글\' 헤더 숨기기 - \'회원별 댓글\' 헤더가 숨겨집니다 - \'회원별 댓글\' 헤더가 표시됩니다 - 댓글 섹션 숨기기 - 댓글 섹션이 숨겨집니다 - 댓글 섹션이 표시됩니다 - \'Shorts 만들기\' 버튼 숨기기 - \'Shorts 만들기\' 버튼이 숨겨집니다 - \'Shorts 만들기\' 버튼이 표시됩니다 - 댓글 미리보기 숨기기 - 댓글 미리보기가 숨겨집니다 - 댓글 미리보기가 표시됩니다 - Thanks 버튼 숨기기 - Thanks 버튼이 숨겨집니다 - Thanks 버튼이 표시됩니다 - 타임스탬프 & 이모지 버튼 숨기기 - 타임스탬프 & 이모지 버튼이 숨겨집니다 - 타임스탬프 & 이모지 버튼이 표시됩니다 - - - 크라우드 펀딩 박스 숨기기 - 플레이어 하단에서 크라우드 펀딩 박스가 숨겨집니다 - 플레이어 하단에서 크라우드 펀딩 박스가 표시됩니다 - - + 최종 화면 카드 숨기기 최종 화면 카드가 숨겨집니다 최종 화면 카드가 표시됩니다 - - 카테고리 바 - 피드, 검색 결과, 관련 동영상에서 카테고리 바를 숨기거나 표시할 수 있습니다 - 피드에서 카테고리 바 숨기기 - 피드에서 카테고리 바가 숨겨집니다 - 피드에서 카테고리 바가 표시됩니다 - 검색 결과에서 카테고리 바 숨기기 - 검색 결과에서 카테고리 바가 숨겨집니다 - 검색 결과에서 카테고리 바가 표시됩니다 - 관련 동영상에서 카테고리 바 숨기기 - 플레이어 하단에 있는 관련 동영상에서 카테고리 바가 숨겨집니다 - 플레이어 하단에 있는 관련 동영상에서 카테고리 바가 표시됩니다 - - - 플로팅 마이크 버튼 숨기기 - 플로팅 마이크 버튼이 숨겨집니다 - 플로팅 마이크 버튼이 표시됩니다 - - + 전체 화면에서 앰비언트 모드 비활성화하기 앰비언트 모드를 비활성화합니다 앰비언트 모드를 활성화합니다 - + 정보 카드 숨기기 정보 카드가 숨겨집니다 정보 카드가 표시됩니다 - + 롤링 넘버 애니메이션 비활성화하기 다음 롤링 넘버 애니메이션을 비활성화합니다\n• 조회수, 시청자 수 롤링 애니메이션 (플레이어 하단)\n• 좋아요 수, 조회수 롤링 애니메이션 (동영상 설명) 다음 롤링 넘버 애니메이션을 활성화합니다\n• 조회수, 시청자 수 롤링 애니메이션 (플레이어 하단)\n• 좋아요 수, 조회수 롤링 애니메이션 (동영상 설명) - + 동영상 플레이어 재생바 숨기기 동영상 플레이어 재생바가 숨겨집니다 동영상 플레이어 재생바가 표시됩니다 @@ -598,7 +594,9 @@ This is because Crowdin requires temporarily flattening this file and removing t 썸네일 재생바가 숨겨집니다 썸네일 재생바가 표시됩니다 - + + Shorts 플레이어 + Shorts 플레이어에서 구성요소를 숨기거나 표시할 수 있습니다 홈 피드에서 Shorts 선반 숨기기 홈 피드에서 Shorts 선반이 숨겨집니다 @@ -696,27 +694,27 @@ This is because Crowdin requires temporarily flattening this file and removing t 하단바가 숨겨집니다 하단바가 표시됩니다 - + 최종 화면에서 \'다음 재생 추천 동영상\' 비활성화하기 다음 재생 추천 동영상을 비활성화합니다 다음 재생 추천 동영상을 활성화합니다 - + 동영상 타임스탬프 숨기기 타임스탬프가 숨겨집니다 타임스탬프가 표시됩니다 - + 플레이어 팝업 패널 숨기기 플레이어 팝업 패널이 숨겨집니다\n• 재생목록, 실시간 채팅, etc. 플레이어 팝업 패널이 표시됩니다\n• 재생목록, 실시간 채팅, etc. - + 플레이어 오버레이 불투명도 불투명도 값은 0-100 사이이며, 0은 투명입니다 플레이어 오버레이 불투명도는 0-100 사이여야 합니다 - + 싫어요 수를 일시적으로 표시할 수 없습니다 (API 시간 초과) 싫어요 수를 표시할 수 없습니다 (상태 코드: %d) @@ -760,17 +758,23 @@ This is because Crowdin requires temporarily flattening this file and removing t %d 건의 클라이언트 비율 제한이 발생하였습니다 %d 밀리초 - + 넓은 검색창 활성화하기 넓은 검색창을 활성화합니다 넓은 검색창을 비활성화합니다 - + + 고화질 썸네일 활성화하기 + 재생바 썸네일이 고화질입니다 + 재생바 썸네일이 일반 화질입니다 + 전체 화면 재생바 썸네일이 고화질입니다 + 전체 화면 재생바 썸네일이 일반화질입니다 + 이 설정을 활성화하면 재생바 썸네일이 없는 실시간 스트림의 썸네일도 복원됩니다\n\n재생바 썸네일에는 현재 동영상과 동일한 화질 값이 사용됩니다\n\n이 설정은 동영상 화질 값이 720p 이하이고 인터넷 연결 상태가 매우 빠를 때 가장 잘 작동합니다 이전 재생바 썸네일 복원하기 재생바 상단에서 최소화된 썸네일을 표시합니다 플레이어에서 전체 화면으로 된 썸네일을 표시합니다 - + SponsorBlock 활성화하기 SponsorBlock은 YouTube 동영상 내 성가신 구간을 건너뛰게 해주는 크라우드소싱 시스템입니다 레이아웃 @@ -951,7 +955,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 정보 건너뛸 구간의 데이터는 SponsorBlock API에 의해 제공됩니다. 자세한 내용을 보려면 여기를 누르세요 - + 앱 버전 변경하기 앱 버전을 변경합니다 앱 버전을 변경하지 않습니다 @@ -964,38 +968,45 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - 넓은 동영상 재생 속도 & 화질 메뉴를 복원합니다 16.09.39 - 이전 보관함 탭을 복원합니다 (내 페이지 탭을 비활성화합니다) 17.41.37 - 이전 재생목록 선반으로 복원합니다 - 17.33.42 - 이전 레이아웃으로 복원합니다 - + 앱 시작 페이지 변경하기 - SponsorBlock 활성화 + 홈 (기본값) 채널 둘러보기 - 플레이어에서 구간 투표 버튼을 표시합니다 + 탐색 게임 - 일반적인 건너뛰기 버튼을 표시합니다 - 나 (보관함) - 최소화된 건너뛰기 버튼을 표시합니다 + 기록 + 내 페이지 + 좋아요 표시한 동영상 실시간 영화 음악 - 외관 + 검색 스포츠 - 투표 버튼 표시하기 - 자동으로 건너뛰기 버튼 숨기기 + 구독 + 인기 급상승 나중에 볼 동영상 - + 앱을 시작할 때, Shorts 플레이어 비활성화하기 앱을 시작할 때, Shorts 플레이어를 다시 실행하지 않습니다 앱을 시작할 때, Shorts 플레이어를 다시 실행합니다 - + + Shorts 자동재생 + Shorts 동영상이 자동넘김됩니다 + Shorts 동영상이 반복재생됩니다 + Shorts 자동 백그라운드 재생 + Shorts 동영상 백그라운드 재생이 자동넘김됩니다 + Shorts 동영상 백그라운드 재생이 반복재생됩니다 + + 태블릿 레이아웃 활성화하기 일부 레이아웃을 태블릿 레이아웃으로 활성화합니다 일부 레이아웃을 태블릿 레이아웃으로 활성화하지 않습니다 태블릿 레이아웃에서는 커뮤니티 게시물을 표시되지 않습니다 - + 미니 플레이어 앱 내에서 최소화된 플레이어의 스타일을 변경할 수 있습니다 미니 플레이어 유형 설정 @@ -1012,17 +1023,20 @@ This is because Crowdin requires temporarily flattening this file and removing t 두 번 누르기 동작 및 핀치하여 크기 조정을 활성화합니다\n\n• 두 번 눌러서 미니 플레이어 크기를 확대합니다\n• 다시 두 번 눌러서 원래 크기로 복원합니다 두 번 누르기 동작 및 핀치하여 크기 조정을 비활성화합니다 드래그 & 드롭 활성화하기 - 드래그 & 드롭이 활성화합니다\n\n미니 플레이어를 화면의 어느 곳이든 드래그할 수 있습니다 + 드래그 & 드롭을 활성화합니다\n\n• 미니 플레이어를 화면의 어느 곳이든 드래그할 수 있습니다 드래그 & 드롭을 비활성화합니다 + 수평 드래그 제스처 활성화하기 + 수평 드래그 제스처를 활성화합니다\n\n미니 플레이어를 화면에서 왼쪽 또는 오른쪽으로 드래그할 수 있습니다 + 수평 드래그 제스처를 비활성화합니다 닫기 버튼 숨기기 닫기 버튼이 숨겨집니다 닫기 버튼이 표시됩니다 \'펼치기\' & \'닫기\' 버튼 숨기기 - 버튼들이 숨겨집니다\n\n스와이프하여 미니 플레이어를 펼치거나 닫을 수 있습니다 + \'펼치기\' & \'닫기\' 버튼이 숨겨집니다\n\n• 스와이프하여 미니 플레이어를 펼치거나 닫을 수 있습니다 \'펼치기\' & \'닫기\' 버튼이 표시됩니다 서브텍스트 숨기기 - 서브텍스트가 숨겨집니다\n• 왼쪽 하단에서 표시되는 \'유료 광고 포함\'과 같은 라벨 - 서브텍스트가 표시됩니다\n• 왼쪽 하단에서 표시되는 \'유료 광고 포함\'과 같은 라벨 + 서브텍스트가 숨겨집니다\n\n• 왼쪽 하단에서 표시되는 \'유료 광고 포함\'과 같은 라벨 + 서브텍스트가 표시됩니다\n\n• 왼쪽 하단에서 표시되는 \'유료 광고 포함\'과 같은 라벨 \'되감기\' & \'빨리 감기\' 버튼 숨기기 \'되감기\' & \'빨리 감기\' 버튼이 숨겨집니다 \'되감기\' & \'빨리 감기\' 버튼이 표시됩니다 @@ -1033,12 +1047,12 @@ This is because Crowdin requires temporarily flattening this file and removing t 불투명도 값은 0-100 사이이며, 0은 투명입니다 미니 플레이어 오버레이 불투명도는 0-100 사이여야 합니다 - + 그라데이션 색상 로딩 화면 활성화하기 그라데이션 색상 로딩 화면을 활성화합니다 기본 로딩 화면을 활성화합니다 - + 사용자 정의 재생바 색상 활성화하기 사용자 정의 재생바 색상을 활성화합니다 기본 재생바 색상을 활성화합니다 @@ -1046,12 +1060,12 @@ This is because Crowdin requires temporarily flattening this file and removing t 재생바 색상 잘못된 재생바 색상값입니다 - + 이미지 표시 제한 국가 우회하기 이미지 호스트로 yt4.ggpht.com를 사용합니다 기본 이미지 호스트를 사용합니다\n\n이 설정을 활성화하면 일부 국가에서 차단된 이미지를 수신할 수 있습니다 (채널 프로필 사진, 커뮤니티 게시물 이미지, etc.) - + 홈 탭 @@ -1083,7 +1097,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow를 일시적으로 사용할 수 없습니다 (상태 코드: %s) DeArrow를 일시적으로 사용할 수 없습니다 - + ReVanced 공지 사항 팝업 표시하기 앱을 시작할 때, 공지 사항 팝업을 표시합니다 앱을 시작할 때, 공지 사항 팝업을 표시하지 않습니다 @@ -1091,47 +1105,47 @@ This is because Crowdin requires temporarily flattening this file and removing t 공지 사항 제공자와 연결할 수 없습니다 닫기 - + Warning 시청 기록이 저장되지 않습니다.<br><br> DNS 광고 차단기 또는 네트워크 프록시로 인해 발생한 문제일 가능성이 높습니다.<br><br> 이 문제를 해결하려면 <b>s.youtube.com</b>을 허용 목록에서 제외하거나 모든 DNS 차단기 및 프록시를 해제하세요. 다시 보지 않기 - + 자동 반복 활성화하기 자동 반복을 활성화합니다 자동 반복을 비활성화합니다 - + 기기 크기 정보 변경하기 기기 크기 정보를 변경합니다\n\n이 설정을 활성화하면 더 높은 화질 동영상 값을 잠금 해제할 수 있지만, 동영상 재생이 끊기거나 배터리 수명이 단축될 수 있으며 알려지지 않은 문제점도 발생할 수 있습니다 기기 크기 정보를 변경하지 않습니다\n\n이 설정을 활성화하면 더 높은 화질 동영상 값을 잠금 해제할 수 있습니다 이 설정을 활성화하면 동영상 재생이 끊기거나 배터리 수명이 단축되고 알려지지 않은 문제점이 발생할 수 있습니다 - + GmsCore 설정 알림 수신을 위한 클라우드 메시징을 설정할 수 있습니다 - + 리다이렉션 없이 링크 바로 열기 앱 내에서 외부 링크를 열 때, URL 리다이렉션(youtube.com/redirect)을 거치지 않고 연결됩니다 앱 내에서 외부 링크를 열 때, URL 리다이렉션(youtube.com/redirect)을 거쳐서 연결됩니다 - + 외부 브라우저 사용하기 앱 내에서 외부 링크를 열 때, 외부 브라우저를 사용합니다 앱 내에서 외부 링크를 열 때, 내부 브라우저를 사용합니다 - + 추적 쿼리를 제거한 링크 공유하기 링크를 공유할 때, URL에서 추적 쿼리 매개변수를 제거합니다 (URL의 뒷부분 \'?si=...\' 이 제거됨) 링크를 공유할 때, URL에서 추적 쿼리 매개변수를 제거하지 않습니다 - + 동영상을 확대할 때, 진동 피드백 비활성화하기 진동 피드백을 비활성화합니다 진동 피드백을 활성화합니다 - + 자동 동영상 화질 저장 활성화하기 동영상 화질 값을 변경할 때마다 저장합니다 @@ -1142,64 +1156,63 @@ This is because Crowdin requires temporarily flattening this file and removing t Wi-Fi 기본 동영상 화질을 %1$s 에서 %2$s 로 변경합니다 - + 동영상 재생 속도 다이얼로그 버튼 표시하기 버튼을 표시합니다 버튼을 표시하지 않습니다 - - 사용자 정의 동영상 재생 속도 + + 사용자 정의 동영상 재생 속도 활성화하기 + 사용자 정의 동영상 재생 속도를 활성화합니다 + 사용자 정의 동영상 재생 속도를 비활성화합니다 + 사용자 정의 동영상 재생 속도 편집하기 사용하고 싶은 동영상 재생 속도 값을 추가 또는 변경할 수 있습니다 재생 속도 값은 %s배속을 초과할 수 없으므로 기본값으로 초기화합니다 잘못된 재생 속도 값이므로 기본값으로 초기화합니다 - + 동영상 재생 속도 저장 활성화하기 동영상 재생 속도 값을 변경할 때마다 저장합니다 동영상 재생 속도 값을 변경할 때마다 저장하지 않습니다 기본 동영상 재생 속도 기본 동영상 재생 속도 값을 %s으로 변경합니다 - + 이전 동영상 화질 설정 메뉴 활성화하기 이전 동영상 화질 설정 메뉴를 활성화합니다 이전 동영상 화질 설정 메뉴를 비활성화합니다 - + 슬라이드하여 탐색 활성화하기 슬라이드하여 탐색을 활성화합니다 슬라이드하여 탐색을 비활성화합니다 - - 동영상 스트림 변경하기 - 동영상 스트림을 변경하여 재생 문제를 방지할 수 있습니다 - 동영상 스트림 변경하기 - 동영상 스트림을 변경합니다 - 동영상 스트림을 변경하지 않습니다\n동영상 재생 문제가 발생할 수 있습니다 + + 스트리밍 데이터 변경하기 + 스트리밍 데이터를 변경하여 재생 문제를 방지할 수 있습니다 + 스트리밍 데이터 변경하기 + 스트리밍 데이터를 변경합니다 + 스트리밍 데이터를 변경하지 않습니다\n동영상 재생 문제가 발생할 수 있습니다 이 설정을 비활성화하면 동영상 재생 문제가 발생할 수 있습니다 기본 클라이언트 AVC (H.264) 강제로 활성화하기 동영상 코덱을 AVC (H.264)로 활성화합니다\n\n• 일부 VP9 코덱 동영상에서 제거되었던 화질 값들이 표시될 수 있습니다.\n• 최대 화질 값이 1080p이므로 초고화질 동영상을 재생할 수 없습니다.\n• HDR 동영상을 재생할 수 없습니다 동영상 코덱을 VP9 또는 AV1으로 활성화합니다\n\n• 예전에 업로드된 동영상을 재생했는데 VP9 코덱 응답을 받았을 경우, 일부 화질 값들이 제거되어 360p와 1080p(Premium 기능)만 선택할 수 있거나 화질 메뉴를 선택할 수 없을 수 있습니다 이 기기는 VP9 하드웨어 디코딩을 지원하지 않습니다. 그러므로 \'클라이언트 변경하기\'가 활성화된 경우에는 이 설정은 항상 켜져 있습니다 - 이 설정을 활성화하면 배터리 수명이 향상되고 재생 끊김 현상이 해결될 수 있습니다\n\nAVC의 최대 화질 값은 1080p이며 동영상을 재생하면 VP9 또는 AV1보다 더 많은 데이터가 사용됩니다 + 이 설정을 활성화하면 배터리 수명이 향상되고 재생 끊김 현상이 해결될 수 있습니다\n\nAVC의 최대 화질 값은 1080p이며 동영상을 재생하면 VP9 또는 AV1보다 더 많은 모바일 데이터가 사용되오니 주의하세요. \'iOS로 변경\'의 알려진 문제점 - • 영화 또는 유료 동영상이 재생되지 않을 수 있습니다\n• 되감기가 가능한 실시간 스트림이 라이브 중인 시점이 아닌 처음부터 재생될 수 있습니다\n• 동영상이 1초 일찍 종료될 수 있습니다\n• OPUS 오디오 코덱이 지원되지 않습니다 + • 영화 또는 회원 전용 동영상과 같은 유료 동영상이 재생되지 않을 수 있습니다\n• 일부 실시간 스트림이 처음부터 재생될 수 있습니다\n• 동영상이 1초 일찍 종료될 수 있습니다\n• OPUS 오디오 코덱이 지원되지 않습니다 \'Android VR로 변경\'의 알려진 문제점 • 오디오 트랙 메뉴가 표시되지 않습니다\n• 안정적인 볼륨 메뉴가 비활성화된 채로 잠겨있습니다 - - - 슬라이드하여 탐색 활성화하기 - - + 음성 광고 차단하기 음성 광고를 차단합니다 음성 광고를 차단하지 않습니다 - + %s 를 차단할 수 없기 때문에 광고가 표시될 것입니다. 설정에서 다른 광고 차단 서비스로 전환해 보세요 %s 서버에서 오류가 발생했기 때문에 광고가 표시될 것입니다. 설정에서 다른 광고 차단 서비스로 전환해 보세요 광고 차단 Proxy 서버 사용하기 @@ -1207,30 +1220,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Luminous Proxy PurpleAdBlock Proxy - + 동영상 광고 차단하기 동영상 광고를 차단합니다 동영상 광고를 차단하지 않습니다 - + 메시지를 제거합니다 제거된 메시지 표시하기 제거된 메시지 표시하지 않기 스포일러 뒤에 제거된 메시지 숨기기 제거된 메시지를 줄이 그어진 텍스트로 표시하기 - + 채널 포인트 자동 적립하기 채널 포인트을 자동으로 적립합니다 채널 포인트를 자동으로 적립하지 않습니다 - + Twitch 디버그 모드 활성화하기 Twitch 디버그 모드를 활성화합니다 (추천하지 않음) Twitch 디버그 모드를 비활성화합니다 - + ReVanced 설정 광고 광고 차단을 설정할 수 있습니다 diff --git a/patches/src/main/resources/addresources/values-ky-rKG/strings.xml b/patches/src/main/resources/addresources/values-ky-rKG/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-ky-rKG/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/addresources/values-lo-rLA/strings.xml b/patches/src/main/resources/addresources/values-lo-rLA/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-lo-rLA/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/addresources/values-lt-rLT/strings.xml b/patches/src/main/resources/addresources/values-lt-rLT/strings.xml similarity index 70% rename from src/main/resources/addresources/values-lt-rLT/strings.xml rename to patches/src/main/resources/addresources/values-lt-rLT/strings.xml index 4f75f9866..c53ee7edd 100644 --- a/src/main/resources/addresources/values-lt-rLT/strings.xml +++ b/patches/src/main/resources/addresources/values-lt-rLT/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -155,107 +142,106 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + Numatyti - + - + - + - + - + - + - + + + - + - + Įspėjimas - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/src/main/resources/addresources/values-lv-rLV/strings.xml b/patches/src/main/resources/addresources/values-lv-rLV/strings.xml similarity index 70% rename from src/main/resources/addresources/values-lv-rLV/strings.xml rename to patches/src/main/resources/addresources/values-lv-rLV/strings.xml index d3f8eb875..5689265fa 100644 --- a/src/main/resources/addresources/values-lv-rLV/strings.xml +++ b/patches/src/main/resources/addresources/values-lv-rLV/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,107 +139,106 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + Parasts - + - + - + - + - + - + - + + + - + - + Brīdinājums - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/patches/src/main/resources/addresources/values-mk-rMK/strings.xml b/patches/src/main/resources/addresources/values-mk-rMK/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-mk-rMK/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/addresources/values-ml-rIN/strings.xml b/patches/src/main/resources/addresources/values-ml-rIN/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-ml-rIN/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/addresources/values-mn-rMN/strings.xml b/patches/src/main/resources/addresources/values-mn-rMN/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-mn-rMN/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/addresources/values-mr-rIN/strings.xml b/patches/src/main/resources/addresources/values-mr-rIN/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-mr-rIN/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/addresources/values-ms-rMY/strings.xml b/patches/src/main/resources/addresources/values-ms-rMY/strings.xml similarity index 70% rename from src/main/resources/addresources/values-ms-rMY/strings.xml rename to patches/src/main/resources/addresources/values-ms-rMY/strings.xml index 66dc6696b..8994253e8 100644 --- a/src/main/resources/addresources/values-ms-rMY/strings.xml +++ b/patches/src/main/resources/addresources/values-ms-rMY/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,106 +139,105 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + Amaran - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/patches/src/main/resources/addresources/values-my-rMM/strings.xml b/patches/src/main/resources/addresources/values-my-rMM/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-my-rMM/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/addresources/values-nb-rNO/strings.xml b/patches/src/main/resources/addresources/values-nb-rNO/strings.xml similarity index 93% rename from src/main/resources/addresources/values-nb-rNO/strings.xml rename to patches/src/main/resources/addresources/values-nb-rNO/strings.xml index a31f92131..dd25c1805 100644 --- a/src/main/resources/addresources/values-nb-rNO/strings.xml +++ b/patches/src/main/resources/addresources/values-nb-rNO/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Sjekker mislyktes Åpne offisiell nettside Ignorer @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Patched %s dager siden APK byggedato er skadet - + ReVanced Ønsker du å fortsette? Oppdater og start på nytt @@ -62,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Offisielle lenker Doner - + MicroG GmsCore er ikke installert. Installer den. Handling nødvendig @@ -73,7 +73,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Om Reklame Alternative miniatyrbilder @@ -85,7 +85,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Diverse Video - + + Deaktiver Shorts bakgrunn spill + + Feilsøking Aktivere eller deaktivere feilsøkingsalternativer Feilsøk logging @@ -102,13 +105,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Toast ikke vist ved feil Slår av feil i altfor å skjule alle ReVanced feilmeldinger.\n\nDu vil ikke bli varslet om noen uventede hendelser. - + Deaktiver \'liker\' / abonnér på glød Som og abonner-knappen vil ikke glød når den nevnes Som og abbonér vil glød når det nevnes - Skjul grå skilletegn - Grå skilletegn er skjult - Grå skilletegn vises + Skjul albumkort + Albumkort er skjult + Albumkort vises + Skjul crowdfunding boks + Crowdfunding box er skjult + Det er vist dekningsboks + Skjul flytende mikrofonknapp + Mikrofonknappen er skjult + Mikrofon knapp vist Skjul kanalvannmerke Vannmerket er skjult Vannmerke er vist @@ -152,9 +161,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Skjul ekspanderbar chip under videoer Utvidbare brikker er skjult Utvidbare brikker vises - Skjul meny for bildekvalitet bunntekst - Menybunntekst for videokvalitet er skjult - Menybunntekst for videokvalitet vises Skjul samfunnsinnlegg Samfunnets innlegg er skjult Samfunnsinnlegg vises @@ -227,6 +233,34 @@ This is because Crowdin requires temporarily flattening this file and removing t Transskripsjonsseksjonen vises Beskrivelse av videoen Skjul eller vis video-beskrivelse komponenter + Filtrer bar + Skjul eller vis filterlinjen i matingen, søk og relaterte videoer + Skjul i feed + Skjult i feed + Vist i feed + Skjul i søk + Skjult i søk + Vist i søk + Skjul i relaterte videoer + Skjult i relaterte videoer + Vises i relaterte videoer + Kommentarer + Skjul eller vis kommentar-deler komponenter + Skjul \'Kommentarer etter medlemmer\' topptekst + \'Kommentarer av medlemmer\' topptekst er skjult + \'Kommentarer etter medlemmer\' topptekst er vist + Skjul kommentarfeltet + Kommentarer seksjonen er skjult + Kommentarer er vist + Skjul forhåndsvisningskommentar + Forhåndsvisningskommentaren er skjult + Forhåndsvisningskommentar vises + Skjul takks-knappen + Tusen takk er skjult + Tusen takk vises + Skjul knapper for tidsstempel og emoji + Tidsstempel og emoji-knapper er skjult + Det vises tidsstempel og emoji-knapper Skjul YouTube dører Søk i barnerøvinger er skjult @@ -257,7 +291,6 @@ This is because Crowdin requires temporarily flattening this file and removing t This is because keywords can be in any language, and showing an example in the localized script helps convey this. --> Nøkkelord og fraser å gjemme seg; atskilt med nye linjer\n\nnøkkelord kan være kanalnavn eller tekst som vises i videotitler\n\nord med store bokstaver i midten må angis med kasus (ie: iPhone, TikTok, bland Om nøkkelordfiltrering - Hjemme/Abonnement/Søkeresultater filtreres for å skjule innhold som matcher nøkkelordfraser\n\nBegrensninger\n• Butikker kan ikke skjules med kanalnavn\n• Noen UI komponenter kan ikke skjules\n• Ved å søke etter et nøkkelord kan det ikke være noen treff Sammenlign hele ord Å sirkulere en søkeord/frase med doble sitater vil hindre delvis treff med videotitler og kanalnavn<br><br>For eksempel,<br><b>\"ai\"</b> vil skjule videoen: <b>How does AI work?</b><br>men vil ikke gjemme: <b>What does fair use mean?</b> @@ -268,7 +301,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Nøkkelordet er for kort og krever atferd: %s Søkeord vil skjule alle videoer: %s - + Skjul generelle annonser Generelle annonser er skjult Generelle annonser er vist @@ -287,6 +320,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Skjul banner for å se produkter Banner er skjult Banner vises + Skjul spiller shopping hylle + Handlehylle er skjult + Handlehold/hylle vises Skjul innkjøpslenker i videobeskrivelse Handlelenker er skjult Lenker for handlekurv vises @@ -303,17 +339,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Skjul fullskjermsannonser bare som fungerer med eldre enheter - + Skjul YouTube Premium kampanjer YouTube Premium kampanjer under videospiller er skjult YouTube Premium kampanjer under videospiller er vist - + Skjul video annonser Videoannonser er skjult Videoannonser vises - + URL kopiert til utklippstavlen URL med tidsstempel kopiert Vis knapp for \"Kopier video-URL\" @@ -323,13 +359,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Knappen er vist. Trykk for å kopiere videoURL med tidsstempel. Trykk og hold for å kopiere video uten tidsstempel Knappen vises ikke - + Fjern dialogboksen for viserdiskresjon Dialogvindu vil bli fjernet Dialogvindu vil bli vist Dette forbigår ikke aldersbegrensningen. Det godtar bare den automatisk. - + Eksterne nedlastinger Innstillinger for å bruke ekstern nedlasting Vis ekstern nedlastingsknapp @@ -343,17 +379,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Pakkenavn på din installerte eksterne nedlastingsapp, slik som NewPipe eller Seal %s er ikke installert. Installer den. - + Deaktivere presis søkefelt Bevegelse er deaktivert Gest er aktivert - + Aktiver søkerklikk Seekbar tapping er aktivert Seekbar kartlegging er deaktivert - + Aktiver lysstyrkebevegelser Lysstyrke swipe er aktivert Lysstyrke swipe er deaktivert @@ -382,12 +418,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Sveip størrelse terskel Mengden terskel du må bruke - + Deaktiver bildetekst Automatisk bildetekst er deaktivert Automatisk bildetekst er aktivert - + Handlingsknapper Skjul eller vis knapper under videoer Skjul \'Like and Disliker\' @@ -422,23 +458,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Lagre i spilleliste-knappen er skjult Lagre i spilleliste-knappen vises - - Skjul automatisk avspillingsknapp - Autospill-knappen er skjult - Autospill-knappen vises - - - - Skjul undertekst-knappen - Tekstknappen er skjult - Tekst-knappen vises - - - Skjul kast knapp - Støp-knappen er skjult - Oversiktsknappen er vist - - + Navigation buttons Skjul eller endre knapper i navigasjonslinjen @@ -465,7 +485,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Etiketter er skjult Etiketter er vist - + Flyout menu Skjul eller vis spillere flukte menyelementer @@ -476,6 +496,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Skjul tilleggsinnstillinger Ytterlige innstillingsmeny er skjult Flere innstillingsmeny er vist + + Skjul sovtidtaker + Dvale-tidtakermenyen er skjult + Sleep Timer meny er vist Skjul video på løkke Video-menyen en loop er skjult @@ -484,6 +508,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Skjul Ambient modus Meny for omgivelsesmodus er skjult Ambient modus meny er vist + Skjul Stabil volum + Stabil volummeny er vist + Stabil volummeny er skjult Skjul hjelp & tilbakemelding Hjelp & Tilbakemeldingsmenyen er skjult @@ -509,83 +536,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Skjul klokke i VR Se i VR-menyen er skjult Se i VR-meny for å vise + Skjul meny for bildekvalitet bunntekst + Menybunntekst for videokvalitet er skjult + Menybunntekst for videokvalitet vises - - Skjul forrige & neste videoknapper - Knappene er skjult - Det vises knapper + + Skjul forrige & neste videoknapper + Knappene er skjult + Det vises knapper + Skjul kast knapp + Støp-knappen er skjult + Oversiktsknappen er vist + + Skjul undertekst-knappen + Tekstknappen er skjult + Tekst-knappen vises + Skjul automatisk avspillingsknapp + Autospill-knappen er skjult + Autospill-knappen vises - - Skjul albumkort - Albumkort er skjult - Albumkort vises - - - Kommentarer - Skjul eller vis kommentar-deler komponenter - Skjul \'Kommentarer etter medlemmer\' topptekst - \'Kommentarer av medlemmer\' topptekst er skjult - \'Kommentarer etter medlemmer\' topptekst er vist - Skjul kommentarfeltet - Kommentarer seksjonen er skjult - Kommentarer er vist - Skjul \"Lag en kort\"-knapp - \'Opprett en kort\' knapp er skjult - \'Opprett en kort\' knapp vises - Skjul forhåndsvisningskommentar - Forhåndsvisningskommentaren er skjult - Forhåndsvisningskommentar vises - Skjul takks-knappen - Tusen takk er skjult - Tusen takk vises - Skjul knapper for tidsstempel og emoji - Tidsstempel og emoji-knapper er skjult - Det vises tidsstempel og emoji-knapper - - - Skjul crowdfunding boks - Crowdfunding box er skjult - Det er vist dekningsboks - - + Skjul avsluttende skjerm-kort Sluttskjerm kort er skjult Sluttskjerm vises - - Filtrer bar - Skjul eller vis filterlinjen i matingen, søk og relaterte videoer - Skjul i feed - Skjult i feed - Vist i feed - Skjul i søk - Skjult i søk - Vist i søk - Skjul i relaterte videoer - Skjult i relaterte videoer - Vises i relaterte videoer - - - Skjul flytende mikrofonknapp - Mikrofonknappen er skjult - Mikrofon knapp vist - - + Deaktiver omgivelsesmodus i fullskjerm Ambient modus deaktivert Ambient modus aktivert - + Skjul informasjonskort Informasjonskort er skjult Informasjonskort vises - + Deaktiver animasjoner i rullende tall Rullende nummer er ikke animert Rullende nummer er animert - + Skjul søkefelt i videospiller Videospillers søkefelt er skjult Vises Videospiller-søkefelt @@ -593,7 +583,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Miniatyrbilde søkefeltet er skjult Miniatyrbilde søkefelt vises - + Skjul Shorts i hjemmefeeden Shorts i hjemmefeed er skjult @@ -691,27 +681,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Navigasjonslinjen er skjult Navigasjonslinjen vises - + Deaktiver foreslått videoslutskjerm Foreslåtte videoer blir deaktivert Foreslåtte videoer vises - + Skjul video tidsstempel Tidsstempel er skjult Tidsstempel vises - + Skjul spiller popup-panel Spillerens popup-paneler er skjult Spillerpopup-paneler vises - + Spiller overlegg ugjennomsiktighet Gjennomsiktighet mellom 0-100, der 0 er gjennomsiktig Spiller overlegg opasitet må være mellom 0-100 - + Misliker ikke tilgjengelig (%s) @@ -752,17 +742,22 @@ This is because Crowdin requires temporarily flattening this file and removing t Klientrate grense oppdaget %d ganger %d millisekunder - + Aktiver bredt søkefelt Bred søkefeltet er aktivert Bred søkefeltet er deaktivert - + + Aktiver miniatyrbilder av høy kvalitet + Seekbar miniatyrbilder er av høy kvalitet + Seekbar miniatyrbilder er middels høy kvalitet + Fullskjerm søkbar miniatyrbilder er høy kvalitet + Fullskjerm søkbar miniatyrbilder er middels kvalitet Gjenopprett gamle miniatyrbilder for søkefelt Seekbar miniatyrbilder vil vises over søkefeltet Seekbar miniatyrbilder vil vises i fullskjerm - + Aktiver SponsorBlock SponsorBlock er et crowsourced system for å hoppe over irriterende deler av YouTube-videoer Utseende @@ -938,7 +933,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Om Data leveres av SponsorBlock API. Trykk her for å lære mer og se nedlastinger for andre plattformer - + Spoof app versjon Versjon skremt Versjon ikke skremt @@ -951,9 +946,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Gjenopprette bred videokastighet & kvalitetsmeny 18.09.39 - Gjenopprett bibliotek-fane 17.41.37 - Gjenopprett gammel spilleliste - 17.33.42 - Gjenopprett gammelt UI oppsett - + Angi startside Standard Bla gjennom kanaler @@ -971,18 +965,20 @@ This is because Crowdin requires temporarily flattening this file and removing t Populært Se senere - + Deaktiver gjenopptakelse av Shorts spiller Shorts spiller vil ikke gjenoppta ved oppstart av app Shorts spiller vil gjenoppta ved oppstart av app - + + + Aktiver oppsett for nettbrett Nettbrettets oppsett er aktivert Nettbrett-oppsett er deaktivert Samfunnsinnlegg dukker ikke opp på nettbrett-oppsett - + Smittespiller Endre stil på appen til den minimerte spilleren Type Minispiller @@ -1000,6 +996,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Aktiver dra og slipp Dra og slipp er aktivert\n\nHjelperavspiller kan flyttes til et hvilken som helst hjørne av skjermen Dra og slipp er deaktivert + Aktiver horisontal dra bevegelse + Horisontal dra bevegelse aktivert\n\nMiniplayer kan flyttes av skjermen til venstre eller høyre + Horisontal dra bevegelse deaktivert Skjul lukkeknapp Lukk-knappen er skjult Lukk-knappen vises @@ -1018,12 +1017,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Gjennomsiktighet mellom 0-100, der 0 er gjennomsiktig Minispiller overlegg opasitet må være mellom 0-100 - + Aktiver gradient lastingsskjerm Lasting av skjerm vil ha en gradert bakgrunn Lasting av skjerm har en solid bakgrunn - + Aktiver egendefinert søkelinje-farge Tilpasset søkelinjefarge vises Opprinnelig søkelinjens farge vises @@ -1031,11 +1030,11 @@ This is because Crowdin requires temporarily flattening this file and removing t Farge på søkefeltet Ugyldig søkelinjens fargeverdi - + Forbigå bilde-restriksjoner Bruk bildesvertyt4.ggpht.com - + Hjem fane @@ -1067,7 +1066,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Pil er midlertidig ikke tilgjengelig (statuskode: %s) Pil midlertidig ikke tilgjengelig - + Vis rehabiliterte kunngjøringer Kunngjøringer er vist ved oppstart Kunngjøringer er ikke vist ved oppstart @@ -1075,47 +1074,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Kan ikke koble til kunngjøringstjenesten Avvis - + Advarsel Overvåkningshistorikken din lagres.<br><br>Dette er mest sannsynlig forårsaket av en DNS-annonseblokkering eller nettverksproxy.<br><br>For å fikse dette, hviteliste <b>s.youtube.com</b> eller slå av alle DNS-blokkere og proxyer. Ikke vis dette igjen - + Aktiver automatisk gjenta Auto-Repetering er aktivert Auto-Repetering er deaktivert - + Utforming av enheten Enhetens dimensjoner skremt\n\nHøyere videokvaliteter kan låses opp, men du kan oppleve videospillingsstilling, forverret batterilevetid og ukjente bivirkninger Enhetdimensjonene ikke spoofed\n\nAktivering av dette kan låse opp høyere videokvaliteter Aktivering av dette kan forårsake utsettelse av videospilling, dårligere batteritid og ukjente bivirkninger. - + GmsCore Innstillinger Innstillinger for GmsCore - + Omgå for omdirigeringer Omadressering av URL-adresser skjer forbipassert URL-omdirigeringer blir ikke omgått - + Åpne linker i nettleser Åpne koblinger eksternt Åpner lenker i appen - + Fjern sporingsspørringens parameter Spores parameteren for spørring fjernes fra linker Spores parameteren for spørring fjernes ikke fra lenker - + Deaktiver zoom-haptics Hapetikk er deaktivert Hapetikk er aktivert - + Automatisk kvalitet Husk endringer i videokvalitet Kvalitetsendringer gjelder for alle videoer @@ -1126,35 +1125,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Endret standard %1$s kvalitet til: %2$s - + Vis hastighetsdialogknapp Knappen vises Knappen vises ikke - + + Egendefinert avspillingshastighetsmeny + Egendefinert hastighetsmeny vises + Egendefinert hastighetsmeny vises ikke Egendefinert avspillingshastighet - Legg til eller endre tilgjengelige avspillingshastigheter + Legg til eller endre egendefinert avspillingshastighet Tilpassede hastigheter må være mindre enn %s. Bruker standardverdier. Ugyldige tilpassede avspillingshastigheter. Bruker standardverdier. - + Husk endringer i avspillingshastighet Avspillingshastighet endringer gjelder for alle videoer Hastighetsendringer for avspilling gjelder kun for gjeldende video Standard avspillingshastighet Endret standard hastighet til: %s - + Gjenopprett gammel video kvalitet meny Gammel video kvalitet meny er vist Gammel video kvalitet meny er ikke vist - + Aktiver lysbilde for å søke Slide for å søke er aktivert Slide for å søke er ikke aktivert - + Spoof video strømmer Forkort klientens videostrømmer for å forhindre avspillingsproblemer Spoof video strømmer @@ -1172,20 +1174,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Android VR opplever bivirkninger • Lydspormeny mangler\n• Stabil volum er ikke tilgjengelig - - - Aktiver automatisk HDR lysstyrke - Automatisk HDR lysstyrke er aktivert - Automatisk HDR lysstyrke er deaktivert - - + Blokker lydannonser Lydannonser er blokkert Lydannonser er ikke blokkert - + %s er utilgjengelig. Annonser kan vise. Prøv å bytte til en annen annonseblokkerings tjeneste i innstillingene. %s -serveren returnerte en feil. Annonser kan vise. Prøv å bytte til en annen annonseblokkerings tjeneste i innstillingene. Blokker innebygde videoannonser @@ -1193,30 +1189,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Lysrør proxy PurpleAdBlock proxy - + Blokker video annonser Videoannonser er blokkert Videoannonser er ublokkert - + melding slettet Vis slettede meldinger Ikke vis slettede meldinger Skjul slettede meldinger bak en spoiler Vis slettede meldinger som utfylt tekst - + Krev automatisk kanalpoeng Kanalpoeng blir hentet automatisk Kanalpoeng blir ikke hentet automatisk - + Aktiver Twitch feilsøkingsmodus Twitch debug modus er aktivert (ikke anbefalt) Twitch debug modus er deaktivert - + Forbedret innstillinger Reklame Innstillinger for annonseblokkering diff --git a/patches/src/main/resources/addresources/values-ne-rIN/strings.xml b/patches/src/main/resources/addresources/values-ne-rIN/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-ne-rIN/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/addresources/values-nl-rNL/strings.xml b/patches/src/main/resources/addresources/values-nl-rNL/strings.xml similarity index 93% rename from src/main/resources/addresources/values-nl-rNL/strings.xml rename to patches/src/main/resources/addresources/values-nl-rNL/strings.xml index a051e0943..dba60d2c6 100644 --- a/src/main/resources/addresources/values-nl-rNL/strings.xml +++ b/patches/src/main/resources/addresources/values-nl-rNL/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Controle mislukt Open officiële website Negeren @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Gekochte %s dagen geleden APK build datum is beschadigd - + ReVanced Wilt u doorgaan? Herstellen naar standaard @@ -63,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Officiële links Doneren - + MicroG GmsCore is niet geïnstalleerd. Installeer het. Actie vereist @@ -74,7 +74,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Over Advertenties Alternatieve miniaturen @@ -86,7 +86,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Diversen Video - + + + Debugging Debugging opties in- of uitschakelen Logboek foutopsporing @@ -103,13 +105,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Toastmelding niet weergegeven als er een fout optreedt Uitschakelen van foutmeldingen verbergt alle ReVanced error meldingen.\n\nJe wordt niet op de hoogte gesteld van onverwachte gebeurtenissen. - + Uitschakelen like- / abonneer-knop gloed De knop \'like\' en \'abonneren\' zal niet gloeien wanneer deze genoemd wordt \'Like en abonneren\' knop zal gloeien wanneer genoemd - Grijze scheidingsbalken verbergen - Grijze scheidingsbalken zijn verborgen - Grijze scheidingsbalken worden weergegeven + Verberg albumkaarten + Albumkaarten zijn verborgen + Albumkaarten worden weergegeven + Crowdfunding box verbergen + Crowdfunding box is verborgen + Crowdfunding box wordt getoond + Zwevende microfoon knop verbergen + Microfoon knop verborgen + Microfoon knop weergegeven Verberg kanaal watermerk Watermerk is verborgen Watermerk wordt getoond @@ -154,9 +162,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Uitbreidbare chip verbergen onder video\'s Uitklapbare chips zijn verborgen Uitklapbare chips worden getoond - Verberg video kwaliteit menu voettekst - Video kwaliteit menu voettekst is verborgen - Videokwaliteit menu voettekst wordt weergegeven Verberg community berichten Gemeenschapsberichten zijn verborgen Community berichten worden getoond @@ -231,6 +236,34 @@ This is because Crowdin requires temporarily flattening this file and removing t Transcriptsectie wordt weergegeven Video beschrijving Verberg of toon video beschrijving componenten + Filter balk + Verberg of toon de filterbalk in de feed, zoeken, en gerelateerde video\'s + Verberg in feed + Verborgen in feed + Zichtbaar in feed + Verbergen in zoekopdracht + Verborgen in zoekopdracht + Weergegeven in zoekopdracht + Verbergen in gerelateerde video\'s + Verborgen in gerelateerde video\'s + Weergegeven in gerelateerde video\'s + Opmerkingen + Opmerkingen sectie onderdelen verbergen of weergeven + Verberg de titel \'Opmerkingen van leden\' + De header \'Reacties door leden\' is verborgen + De header \'Reacties door leden\' wordt getoond + Reacties sectie verbergen + Reacties sectie is verborgen + Reacties sectie wordt weergegeven + Verberg commentaar op voorbeeld + Voorbeeldcommentaar is verborgen + Voorbeeld commentaar wordt weergegeven + Knop voor dank(en) verbergen + Bedankt knop is verborgen + Bedankt knop wordt weergegeven + Verberg tijdstempel en emoji-knoppen + Tijdstempel en emoji knoppen zijn verborgen + Tijdstempel en emoji-knoppen worden getoond Youtube Doodles verbergen Zoekbalk Doodsen zijn verborgen @@ -261,7 +294,6 @@ This is because Crowdin requires temporarily flattening this file and removing t This is because keywords can be in any language, and showing an example in the localized script helps convey this. --> Trefwoorden en zinnen te verbergen, gescheiden door nieuwe regels\n\nTrefwoorden kunnen kanaalnamen zijn of elke tekst getoond in videocategorie\n\nwoorden met hoofdletters in het midden moeten worden ingevoerd met het behuizing (bijvoorbeeld iPhone, TikTok, Lei) Over trefwoord filteren - Thuis/Abonnement/Zoekresultaten worden gefilterd om de inhoud te verbergen die overeenkomt met trefwoordzinnen\n\nBeperkingen\n• Kortingen kunnen niet worden verborgen met kanaalnaam\n• Sommige componenten van de UI kunnen niet verborgen zijn\n• Zoeken naar een sleutelwoord kan geen resultaten laten zien Koppel hele woorden Omkeren van een trefwoord/zin met dubbele aanhalingstekens zal voorkomen dat deel-matches van videotitels en kanaalnamen<br><br>Bijvoorbeeld,<br><b>\"ai\"</b> verbergt de video: <b>How does AI work?</b><br>maar zal deze niet verbergen: <b>What does fair use mean?</b> @@ -272,7 +304,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Trefwoord is te kort en vereist quotes: %s Trefwoord zal alle video\'s verbergen: %s - + Algemene advertenties verbergen Algemene advertenties zijn verborgen Algemene advertenties worden weergegeven @@ -291,6 +323,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Verberg banner om producten te bekijken Banner is verborgen Banner wordt getoond + Verberg spelers boodschappenplank + Winkelwagen is verborgen + Winkelwagen wordt weergegeven Verberg shopping links in video beschrijving Shopping links zijn verborgen Shopping links worden weergegeven @@ -307,17 +342,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Verberg advertenties op volledig scherm werkt alleen met oudere apparaten - + Verberg YouTube Premium promoties YouTube Premium promoties onder videospeler zijn verborgen YouTube Premium promoties onder videospeler worden getoond - + Video-advertenties verbergen Video-advertenties zijn verborgen Video-advertenties worden weergegeven - + URL gekopieerd naar klembord URL met tijdstempel gekopieerd Knop voor kopie video weergeven @@ -327,13 +362,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Knop wordt weergegeven. Tik om de video-URL te kopiëren met tijdstempel. Druk en houd vast om de video zonder tijdstempel te kopiëren Knop wordt niet weergegeven - + Verwijder discretie kijkvenster Dialoogvenster zal worden verwijderd Dialoogvenster wordt getoond Hiermee wordt de leeftijdsbeperking niet omzeild. Het accepteert deze gewoon automatisch. - + Externe downloads Instellingen voor het gebruik van een externe downloader Externe downloadknop weergeven @@ -347,17 +382,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Pakketnaam van uw geïnstalleerde externe downloader-app, zoals NewPipe of Seal %s is niet geïnstalleerd. Installeer het alstublieft. - + Schakel nauwkeurige zoektocht gebaar uit Gebaar is uitgeschakeld Gebaar is ingeschakeld - + Zoekbalk tappen inschakelen Seekbar tikken is ingeschakeld Seekbar tappen is uitgeschakeld - + Helderheid gebaar inschakelen Helderheid vegen is ingeschakeld Helderheid vegen is uitgeschakeld @@ -386,12 +421,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Veeg magnitude drempel Het aantal drempelwaarden om te vegen - + Automatisch bijschrift uitschakelen Automatisch bijschrift uitgeschakeld Automatisch bijschrift is ingeschakeld - + Actie knoppen Verberg of toon knoppen onder video\'s Verberg Like en Niet leuk @@ -427,23 +462,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Opslaan naar afspeellijst knop is verborgen Opslaan naar afspeellijst knop wordt weergegeven - - Knop voor automatisch afspelen verbergen - Automatisch afspelen knop is verborgen - Automatisch afspelen knop wordt weergegeven - - - - Knop voor onderschriften verbergen - Knop voor bijschriften is verborgen - Knop voor bijschriften wordt weergegeven - - - Verberg cast knop - Cast knop is verborgen - Cast knop wordt weergegeven - - + Navigation buttons Verberg of wijzig knoppen in de navigatiebalk @@ -470,7 +489,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Labels zijn verborgen Labels worden weergegeven - + Flyout menu Menu-items van speler verbergen of tonen @@ -481,6 +500,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Aanvullende instellingen verbergen Extra instellingenmenu is verborgen Extra instellingenmenu wordt weergegeven + + Verberg slaaptimer + Slaaptimer menu is verborgen + Slaaptimer menu wordt weergegeven Loop video verbergen Lusvideo menu is verborgen @@ -489,6 +512,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Verberg omgevingsmodus Menu voor omgevingsmodus is verborgen Menu omgevingsmodus wordt weergegeven + Stabiel volume verbergen + Stabiel volume menu wordt weergegeven + Stabiel volume menu is verborgen Help verbergen & feedback Help & feedback menu is verborgen @@ -514,83 +540,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Verberg horloge in VR Bekijk in het VR-menu is verborgen Bekijk in het VR-menu + Verberg video kwaliteit menu voettekst + Video kwaliteit menu voettekst is verborgen + Videokwaliteit menu voettekst wordt weergegeven - - Vorige & volgende video knoppen verbergen - Knoppen zijn verborgen - Knoppen worden weergegeven + + Vorige & volgende video knoppen verbergen + Knoppen zijn verborgen + Knoppen worden weergegeven + Verberg cast knop + Cast knop is verborgen + Cast knop wordt weergegeven + + Knop voor onderschriften verbergen + Knop voor bijschriften is verborgen + Knop voor bijschriften wordt weergegeven + Knop voor automatisch afspelen verbergen + Automatisch afspelen knop is verborgen + Automatisch afspelen knop wordt weergegeven - - Verberg albumkaarten - Albumkaarten zijn verborgen - Albumkaarten worden weergegeven - - - Opmerkingen - Opmerkingen sectie onderdelen verbergen of weergeven - Verberg de titel \'Opmerkingen van leden\' - De header \'Reacties door leden\' is verborgen - De header \'Reacties door leden\' wordt getoond - Reacties sectie verbergen - Reacties sectie is verborgen - Reacties sectie wordt weergegeven - \'Maak een kort\' knop verbergen - \'Maak een kort\' knop is verborgen - De \'Maak een korting\' knop wordt weergegeven - Verberg commentaar op voorbeeld - Voorbeeldcommentaar is verborgen - Voorbeeld commentaar wordt weergegeven - Knop voor dank(en) verbergen - Bedankt knop is verborgen - Bedankt knop wordt weergegeven - Verberg tijdstempel en emoji-knoppen - Tijdstempel en emoji knoppen zijn verborgen - Tijdstempel en emoji-knoppen worden getoond - - - Crowdfunding box verbergen - Crowdfunding box is verborgen - Crowdfunding box wordt getoond - - + Verberg eindschermkaarten Eindscherm kaarten zijn verborgen Eindschermkaarten worden weergegeven - - Filter balk - Verberg of toon de filterbalk in de feed, zoeken, en gerelateerde video\'s - Verberg in feed - Verborgen in feed - Zichtbaar in feed - Verbergen in zoekopdracht - Verborgen in zoekopdracht - Weergegeven in zoekopdracht - Verbergen in gerelateerde video\'s - Verborgen in gerelateerde video\'s - Weergegeven in gerelateerde video\'s - - - Zwevende microfoon knop verbergen - Microfoon knop verborgen - Microfoon knop weergegeven - - + Schakel omgevingsmodus uit in volledig scherm Actieve modus uitgeschakeld Actieve modus ingeschakeld - + Verberg informatiekaarten Info kaarten zijn verborgen Info kaarten worden weergegeven - + Schakel rolnummer animaties uit Rolnummers zijn niet geanimeerd Rolnummers zijn geanimeerd - + Verberg zoekbalk in videospeler Zoekbalk videospeler verborgen Zoekbalk videospeler is weergegeven @@ -598,7 +587,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Thumbnail zoekbalk is verborgen Miniatuur zoekbalk wordt weergegeven - + Shorts verbergen in de homefeed Shorts in de thuisfeed zijn verborgen @@ -696,27 +685,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Navigatiebalk is verborgen Navigatiebalk wordt weergegeven - + Voorgestelde video eindscherm uitschakelen Voorgestelde video\'s worden uitgeschakeld Voorgestelde video\'s worden getoond - + Verberg video tijdstempel Tijdstempel is verborgen Tijdstempel wordt weergegeven - + Verberg speler popup panelen Speler pop-up panelen zijn verborgen Speler pop-up panelen worden getoond - + Doorzichtigheid speler overlay Transparantiewaarde tussen 0-100, waarbij 0 transparant is Speler overlay transparantie moet tussen 0-100 liggen - + Dislikes tijdelijk niet beschikbaar (API time-out) Niet beschikbaar (status %d) @@ -760,17 +749,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Client rate limiet opgelopen %d keer %d milliseconden - + Schakel brede zoekbalk in Brede zoekbalk is ingeschakeld Brede zoekbalk is uitgeschakeld - + + Hoge kwaliteit miniaturen inschakelen + Zoekbalk miniaturen zijn van hoge kwaliteit + Zoekbalk miniaturen zijn gemiddelde kwaliteit + Volledig scherm Zoekbalk miniaturen zijn van hoge kwaliteit + Volledig scherm Zoekbalk miniaturen zijn gemiddelde kwaliteit + Dit zal ook miniaturen op livestreams herstellen die geen zoekbalkminiaturen hebben.\n\nZoekbar miniaturen gebruiken dezelfde kwaliteit als de huidige video.\n\nDeze functie werkt het beste met een videokwaliteit van 720p of lager en bij het gebruik van een zeer snelle internetverbinding. Herstel oude Zoekbalk miniaturen Zoekbalk miniaturen verschijnen boven de zoekbalk Zoekbalk miniaturen worden weergegeven op volledig scherm - + SponsorBlock inschakelen SponsorBlock is een crowd-sourced systeem om vervelende delen van YouTube video\'s over te slaan Uiterlijk @@ -951,7 +946,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Over Gegevens worden verstrekt door de SponsorBlock API. Tik hier om meer te weten te komen en de downloads van andere platforms te bekijken - + Spoof app versie Versie vervalst Versie niet vervalst @@ -964,9 +959,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Herstel brede videosnelheid & kwaliteitsmenu 18.09.39 - Tabblad bibliotheek herstellen 17.41.37 - oude afspeellijst herstellen - 17.33.42 - Herstel oude UI lay-out - + Startpagina instellen Standaard Kanalen doorzoeken @@ -984,18 +978,20 @@ This is because Crowdin requires temporarily flattening this file and removing t Populair Later bekijken - + Schakel het hervatten van Shorts-speler uit Shorts-speler wordt niet hervat bij het opstarten van de app Shorts-speler wordt hervat bij het opstarten van de app - + + + Tablet lay-out inschakelen Tablet lay-out is ingeschakeld Tablet lay-out is uitgeschakeld Gemeenschapsberichten verschijnen niet op de tabletlay-outs - + Minispeler De stijl van de in de app geminimaliseerde speler wijzigen Miniplayer type @@ -1014,6 +1010,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Schakel slepen en neerzetten in Slepen en neerzetten is ingeschakeld\n\nMiniplayer kan naar elke hoek van het scherm worden gesleept Slepen en neerzetten is uitgeschakeld + Horizontaal slepen gebaar inschakelen + Horizontaal sleep gebaar ingeschakeld\n\nMiniplayer kan worden weggesleept van het scherm naar links of rechts + Horizontaal slepen gebaar uitgeschakeld Verberg sluit knop Sluitknop is verborgen Sluitknop wordt weergegeven @@ -1033,12 +1032,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Transparantiewaarde tussen 0-100, waarbij 0 transparant is Minispeler overlay transparantie moet tussen de 0-100 zijn - + Kleurovergang laden scherm inschakelen Het laden van het scherm zal een verloopachtergrond hebben Het laadscherm zal een solide achtergrond hebben - + Aangepaste Zoekbalk kleur inschakelen Aangepaste zoekbalk kleur wordt weergegeven Oorspronkelijke Zoekbalk kleur wordt weergegeven @@ -1046,12 +1045,12 @@ This is because Crowdin requires temporarily flattening this file and removing t De kleur van de zoekbalk Ongeldige zoekbalk kleurwaarde - + Bypass afbeelding regio beperkingen Gebruik yt4.ggpht.com voor afbeeldingen Het gebruik van de originele afbeeldingshost\n\nDit kan ontbrekende afbeeldingen die geblokkeerd zijn in sommige regio\'s herstellen - + Tabblad Home @@ -1083,7 +1082,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Pijl tijdelijk niet beschikbaar (statuscode: %s) Pijl tijdelijk niet beschikbaar - + Toon ReVanced aankondigingen Aankondigingen worden getoond bij het opstarten Aankondigingen worden niet getoond bij het opstarten @@ -1091,47 +1090,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Verbinding met aankondigingsaanbieder mislukt Afwijzen - + Waarschuwing Je kijkgeschiedenis wordt niet opgeslagen.<br><br>Dit wordt waarschijnlijk veroorzaakt door een DNS-adblocker of netwerkproxy.<br><br>Om dit op te lossen, whitelist <b>s.youtube.com</b> of schakel alle DNS blockers en proxy uit. Niet meer weergeven - + Automatisch herhalen inschakelen Auto-herhaal is ingeschakeld Auto-herhaal is uitgeschakeld - + Apparaatafmetingen nabootsen Apparaatdimensies bespoeld\n\nHogere videokwaliteiten kunnen worden ontgrendeld, maar je kunt het afspelen van video vastmaken, een slechter leven van de batterij en onbekende bijwerkingen ervaren Apparaatafmetingen niet vervalst\n\nDit inschakelen kan hogere videokwaliteiten ontgrendelen Als dit wordt ingeschakeld kan het afspelen van video vastlopen, de batterijduur en onbekende neveneffecten doen toenemen. - + GmsCore Instellingen Instellingen voor GmsCore - + URL omleidingen omzeilen URL-omleidingen zijn overgeslagen URL-omleidingen worden niet gepasseerd - + Open links in browser Links extern openen Koppelingen openen in app - + Verwijder tracking query parameter Tracking query parameter is verwijderd uit links Tracking query parameter is niet verwijderd uit links - + Vorm zoom uitschakelen Haptics zijn uitgeschakeld Haptics zijn ingeschakeld - + Automatische kwaliteit Videokwaliteitswijzigingen onthouden Kwaliteitswijzigingen zijn van toepassing op alle video\'s @@ -1142,35 +1141,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Standaard %1$s kwaliteit gewijzigd naar: %2$s - + Snelheids dialoog knop weergeven Knop wordt weergegeven Knop wordt niet weergegeven - + + Eigen snelheid afspeel menu + Aangepast snelheidsmenu wordt weergegeven + Aangepast snelheidsmenu wordt niet weergegeven Aangepaste afspeelsnelheden - Toevoegen of wijzigen van de beschikbare afspeelsnelheden + Voeg toe of verander de aangepaste afspeelsnelheden Aangepaste snelheden moeten kleiner zijn dan %s. Standaard waarden worden gebruikt. Ongeldige aangepaste afspeelsnelheden. Gebruik standaard waarden. - + Onthoud wijzigingen in afspeelsnelheid Wijzigingen in de afspeelsnelheid zijn van toepassing op alle video\'s Wijzigingen in afspeelsnelheid zijn alleen van toepassing op de huidige video Standaard afspeelsnelheid Standaardsnelheid gewijzigd naar: %s - + Herstel oude video kwaliteit menu Oude video kwaliteit menu wordt weergegeven Het oude kwaliteitsmenu wordt niet weergegeven - + Slide om te zoeken inschakelen Slide om te zoeken is ingeschakeld Slide om te zoeken is niet ingeschakeld - + Videostreams omzeilen Videostreams van de client bederven om afspeelproblemen te voorkomen Videostreams omzeilen @@ -1188,20 +1190,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Android VR vervalste bijeffecten • Audio track menu ontbreekt\n• Stabiel volume is niet beschikbaar - - - Inschakelen automatische HDR helderheid - Automatische HDR helderheid is ingeschakeld - Automatische HDR helderheid is uitgeschakeld - - + Audio-advertenties blokkeren Audio-advertenties zijn geblokkeerd Audio-advertenties zijn gedeblokkeerd - + %s is niet beschikbaar. Advertenties kunnen worden weergegeven. Probeer over te schakelen naar een andere block-service in de instellingen. %s server gaf een fout. Advertenties kunnen worden weergegeven. Probeer over te schakelen naar een andere advertentie blok service in instellingen. Ingesloten videoadvertenties blokkeren @@ -1209,30 +1205,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Onduidelijke proxy Paarse AdBlock-proxy - + Video-advertenties blokkeren Video-advertenties zijn geblokkeerd Video-advertenties zijn gedeblokkeerd - + bericht verwijderd Toon verwijderde berichten Verwijderde berichten niet weergeven Verberg verwijderde berichten achter een spoiler Toon verwijderde berichten als gekruiste tekst - + Automatisch ophalen Kanaal Punten Kanaalpunten worden automatisch opgeëist Kanaalpunten worden niet automatisch opgeëist - + Twitch debugmodus inschakelen Twitch debugmodus is ingeschakeld (niet aanbevolen) Twitch debug modus is uitgeschakeld - + Verbeterde instellingen Advertenties Instellingen voor advertentieblokkering diff --git a/patches/src/main/resources/addresources/values-or-rIN/strings.xml b/patches/src/main/resources/addresources/values-or-rIN/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-or-rIN/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/patches/src/main/resources/addresources/values-pa-rIN/strings.xml b/patches/src/main/resources/addresources/values-pa-rIN/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-pa-rIN/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/addresources/values-pl-rPL/strings.xml b/patches/src/main/resources/addresources/values-pl-rPL/strings.xml similarity index 93% rename from src/main/resources/addresources/values-pl-rPL/strings.xml rename to patches/src/main/resources/addresources/values-pl-rPL/strings.xml index bb46bac4d..2e2035197 100644 --- a/src/main/resources/addresources/values-pl-rPL/strings.xml +++ b/patches/src/main/resources/addresources/values-pl-rPL/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Sprawdzanie nie powiodło się Otwórz oficjalną stronę internetową Ignoruj @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Dostosuj %s dni temu Data kompilacji APK jest uszkodzona - + ReVanced Czy chcesz kontynuować? Zresetuj @@ -63,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Oficjalne linki Wesprzyj - + MicroG GmsCore nie jest zainstalowany. Zainstaluj go. Wymagane działanie @@ -74,7 +74,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + O aplikacji Reklamy Alternatywne miniaturki @@ -86,7 +86,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Pozostałe Wideo - + + + Debugowanie Włącz lub wyłącz opcje debugowania Logi do debugowania @@ -103,13 +105,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Niewidoczne Wyłączanie komunikatów o błędach ukrywa wszystkie komunikaty o błędach ReVanced.\n\nNie będziesz powiadamiany o żadnych nieoczekiwanych zdarzeniach. - + Wyłącz polubienie/subskrybuj podświetlenie przycisków Przycisk polubienia i subskrybuj nie pojawi się po wzmiance Przycisk polubienia i subskrybuj się po wzmiance - Szare separatory - Ukryte - Widoczne + Ukryj karty albumu + Karty albumów są ukryte + Karty albumów są wyświetlane + Ukryj program finansowania społecznościowego + Crowdfunding box jest ukryty + Pokazywany jest program Crowdfunding + Ukryj pływający przycisk mikrofonu + Przycisk mikrofonu ukryty + Przycisk mikrofonu pokazany Znaki wodne kanałów Ukryte Widoczne @@ -154,9 +162,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Ukryj rozszerzalny chipy pod filmami Rozwijalne chipy są ukryte Rozwijalne chipy są wyświetlane - Ukryj stopkę menu jakości wideo - Stopka menu jakości wideo jest ukryta - Stopka menu jakości wideo jest pokazana Ukryj posty społeczności Posty społeczności są ukryte Posty społeczności są wyświetlane @@ -231,6 +236,34 @@ This is because Crowdin requires temporarily flattening this file and removing t Sekcja transkryptu jest wyświetlana Opis wideo Ukryj lub pokaż elementy opisu wideo + Pasek filtra + Ukryj lub pokaż pasek filtrów w kanale, wyszukiwaniu i powiązanych filmach + Ukryj w kanale + Ukryte w kanale + Pokazane w kanale + Ukryj w wyszukiwaniu + Ukryte w wyszukiwaniu + Pokaż w wyszukiwarce + Ukryj w powiązanych filmach + Ukryte w powiązanych filmach + Wyświetlane w powiązanych filmach + Komentarze + Ukryj lub pokaż składniki sekcji komentarzy + Ukryj nagłówek \'Komentarze wg użytkowników\' + Nagłówek \'Komentarze użytkowników\' jest ukryty + Wyświetlony jest nagłówek \'Komentarze użytkowników\' + Ukryj sekcję komentarzy + Sekcja komentarzy jest ukryta + Sekcja komentarzy jest wyświetlana + Ukryj podgląd komentarza + Podgląd komentarza jest ukryty + Podgląd komentarza jest wyświetlany + Ukryj przycisk podziękowania + Przycisk podziękowania jest ukryty + Przycisk podziękowania jest pokazany + Ukryj znaczniki czasu i przyciski emoji + Przyciski znacznika czasu i emoji są ukryte + Wyświetlane są przyciski znacznika czasu i emoji Ukryj Doodles YouTube Pasek wyszukiwania jest ukryty @@ -261,7 +294,6 @@ This is because Crowdin requires temporarily flattening this file and removing t This is because keywords can be in any language, and showing an example in the localized script helps convey this. --> Słowa kluczowe i frazy do ukrycia, oddzielone nowymi wierszami\n\nSłowa kluczowe mogą być nazwami kanałów lub dowolnymi tekstami pokazanymi w tytułach wideo\n\nSłowa z wielkimi literami w środku muszą być wpisane z obudową (np. iPhone, TikTok, LeBlanc) O filtrowaniu słów kluczowych - Wyniki strony głównej/Subskrypcji/Wyszukiwarki są filtrowane w celu ukrycia treści pasującej do słów kluczowych\n\nOgraniczenia\n• Skróty nie mogą być ukryte przez nazwę kanału\n• Niektóre komponenty interfejsu użytkownika nie mogą być ukryte\n• Wyszukiwanie słowa kluczowego może nie pokazywać wyników Dopasuj całe słowa Otwarcie słowa kluczowego/frazy podwójnymi cudzysłowami uniemożliwi częściowe dopasowanie tytułów wideo i nazw kanałów<br><br>Na przykład,<br><b>\"ai\"</b> ukryje wideo: <b>How does AI work?</b><br>ale nie ukry: <b>What does fair use mean?</b> @@ -272,7 +304,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Słowo kluczowe jest zbyt krótkie i wymaga cytatów: %s Słowo kluczowe ukryje wszystkie filmy: %s - + Ukryj reklamy ogólne Ogólne reklamy są ukryte Ogólne reklamy są wyświetlane @@ -291,6 +323,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Ukryj baner do wyświetlania produktów Baner jest ukryty Baner jest pokazany + Ukryj półkę na zakupy gracza + Półka Zakupów jest ukryta + Półka Zakupów jest pokazana Ukryj linki do zakupów w opisie wideo Linki do zakupów są ukryte Odnośniki do zakupów są wyświetlane @@ -307,17 +342,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Ukryj reklamy w trybie pełnoekranowym tylko ze starszymi urządzeniami - + Ukryj promocje YouTube Premium Promocje YouTube Premium w odtwarzaczu wideo są ukryte Promocje YouTube Premium w odtwarzaczu wideo są wyświetlane - + Ukryj reklamy wideo Reklamy wideo są ukryte Wyświetlane reklamy wideo - + Adres URL skopiowany do schowka Adres URL z znacznikiem czasu skopiowany Pokaż przycisk kopiowania filmu URL @@ -327,13 +362,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Przycisk jest wyświetlony. Dotknij, aby skopiować URL wideo ze znacznikiem czasu. Dotknij i przytrzymaj aby skopiować film bez znacznika czasu Przycisk nie jest wyświetlany - + Usuń okno dialogowe przeglądarki Okno dialogowe zostanie usunięte Okno dialogowe zostanie wyświetlone To nie pomija ograniczeń wiekowych i akceptuje je automatycznie. - + Pobieranie zewnętrzne Ustawienia dla zewnętrznego pobierania Pokaż zewnętrzny przycisk pobierania @@ -347,17 +382,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Nazwa pakietu zainstalowanej zewnętrznej aplikacji do pobierania pliku, takiej jak NewPipe lub Pieczęć %s nie jest zainstalowany. Zainstaluj go. - + Wyłącz dokładny gest szukania Gest jest wyłączony Gest jest włączony - + Włącz dotknięcie paska nawigacji Naciśnięcie paska wyszukiwania jest włączone Stuknięcie paska wyszukiwania jest wyłączone - + Włącz gest jasności Przesunięcie jasności jest włączone Przesunięcie jasności jest wyłączone @@ -386,12 +421,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Próg wielkości przesunięcia Ilość progu dla przesunięcia palcem - + Wyłącz automatyczne podpisy Automatyczne podpisy są wyłączone Automatyczne podpisy są włączone - + Przyciski akcji Ukryj lub pokaż przyciski pod filmami Ukryj polubienie i nie lubię @@ -427,23 +462,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Zapisz do przycisku playlisty jest ukryty Pokaż przycisk Zapisz do playlisty - - Ukryj przycisk automatycznego odtwarzania - Przycisk automatycznego odtwarzania jest ukryty - Przycisk automatycznego odtwarzania jest wyświetlany - - - - Przycisk ukrycia podpisów - Przycisk podpisów jest ukryty - Przycisk podpisów jest pokazany - - - Ukryj przycisk Cofania - Przycisk Przesyłaj jest ukryty - Przycisk Przesyłaj jest widoczny - - + Navigation buttons Ukryj lub zmień przyciski na pasku nawigacji @@ -470,7 +489,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Etykiety są ukryte Etykiety są wyświetlane - + Flyout menu Ukryj lub pokaż elementy menu flyout gracza @@ -481,6 +500,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Ukryj dodatkowe ustawienia Dodatkowe menu ustawień jest ukryte Dodatkowe menu ustawień jest wyświetlane + + Ukryj zegar uśpienia + Menu timera uśpienia jest ukryte + Wyświetlane jest menu timera Ukryj wideo w pętli Menu wideo w pętli jest ukryte @@ -489,6 +512,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Ukryj tryb Ambient Menu trybu otoczenia jest ukryte Wyświetlane jest menu trybu otoczenia + Ukryj stabilną głośność + Widoczne jest stabilne menu głośności + Stabilne menu głośności jest ukryte Ukryj opinię o pomocy & Menu pomocy & jest ukryte @@ -514,83 +540,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Ukryj wachtę w VR Obejrzyj menu VR jest ukryte Obejrzyj w menu VR + Ukryj stopkę menu jakości wideo + Stopka menu jakości wideo jest ukryta + Stopka menu jakości wideo jest pokazana - - Ukryj poprzednie przyciski & następnego filmu - Przyciski są ukryte - Przyciski są wyświetlane + + Ukryj poprzednie przyciski & następnego filmu + Przyciski są ukryte + Przyciski są wyświetlane + Ukryj przycisk Cofania + Przycisk Przesyłaj jest ukryty + Przycisk Przesyłaj jest widoczny + + Przycisk ukrycia podpisów + Przycisk podpisów jest ukryty + Przycisk podpisów jest pokazany + Ukryj przycisk automatycznego odtwarzania + Przycisk automatycznego odtwarzania jest ukryty + Przycisk automatycznego odtwarzania jest wyświetlany - - Ukryj karty albumu - Karty albumów są ukryte - Karty albumów są wyświetlane - - - Komentarze - Ukryj lub pokaż składniki sekcji komentarzy - Ukryj nagłówek \'Komentarze wg użytkowników\' - Nagłówek \'Komentarze użytkowników\' jest ukryty - Wyświetlony jest nagłówek \'Komentarze użytkowników\' - Ukryj sekcję komentarzy - Sekcja komentarzy jest ukryta - Sekcja komentarzy jest wyświetlana - Ukryj przycisk \'Utwórz krótki\' - Przycisk \'Utwórz krótki\' jest ukryty - Przycisk \'Utwórz krótki\' jest pokazany - Ukryj podgląd komentarza - Podgląd komentarza jest ukryty - Podgląd komentarza jest wyświetlany - Ukryj przycisk podziękowania - Przycisk podziękowania jest ukryty - Przycisk podziękowania jest pokazany - Ukryj znaczniki czasu i przyciski emoji - Przyciski znacznika czasu i emoji są ukryte - Wyświetlane są przyciski znacznika czasu i emoji - - - Ukryj program finansowania społecznościowego - Crowdfunding box jest ukryty - Pokazywany jest program Crowdfunding - - + Ukryj karty ekranu końcowego Karty ekranu końcowego są ukryte Karty ekranu końcowego są wyświetlane - - Pasek filtra - Ukryj lub pokaż pasek filtrów w kanale, wyszukiwaniu i powiązanych filmach - Ukryj w kanale - Ukryte w kanale - Pokazane w kanale - Ukryj w wyszukiwaniu - Ukryte w wyszukiwaniu - Pokaż w wyszukiwarce - Ukryj w powiązanych filmach - Ukryte w powiązanych filmach - Wyświetlane w powiązanych filmach - - - Ukryj pływający przycisk mikrofonu - Przycisk mikrofonu ukryty - Przycisk mikrofonu pokazany - - + Wyłącz tryb otoczenia na pełnym ekranie Tryb otoczenia wyłączony Tryb Ambient włączony - + Ukryj karty informacyjne Karty informacyjne są ukryte Karty informacyjne są wyświetlane - + Wyłącz animacje liczb kroczących Numery rolowania nie są animowane Numery toczenia są animowane - + Ukryj pasek nawigacji w odtwarzaczu wideo Pasek wyszukiwania odtwarzacza wideo jest ukryty Pasek przeszukiwania odtwarzacza wideo jest wyświetlany @@ -598,7 +587,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Pasek miniatur jest ukryty Pasek miniatur jest wyświetlany - + Ukryj Shorts w kanale głównym Shorts w kanale głównym są ukryte @@ -696,27 +685,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Pasek nawigacji jest ukryty Pasek nawigacji jest wyświetlany - + Wyłącz sugerowany ekran końcowy wideo Sugerowane filmy zostaną wyłączone Sugerowane filmy będą wyświetlane - + Ukryj znacznik czasu wideo Znacznik czasu jest ukryty Znacznik czasu jest wyświetlany - + Ukryj wyskakujące panele Panel wyskakujących okienek graczy jest ukryty Wyskakujące panele graczy są wyświetlane - + Przezroczystość nakładki odtwarzacza Wartość przezroczystości między 0-100, gdzie 0 jest przezroczysty Przezroczystość nakładki odtwarzacza musi być pomiędzy 0-100 - + Łapki w dół są tymczasowo niedostępne (API nie reaguje) Brak polubień (status %d) @@ -759,17 +748,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Nie napotkano limitów cenowych klienta %d milisekund - + Włącz szeroki pasek wyszukiwania Szeroki pasek wyszukiwania jest włączony Szeroki pasek wyszukiwania jest wyłączony - + + Włącz miniaturki wysokiej jakości + Miniatury paska wyszukiwania są wysokiej jakości + Miniatury paska wyszukiwania są średniej jakości + Miniatury paska paska paska pełnoekranowego są wysokiej jakości + Miniaturki paska paska paska pełnoekranowego są średniej jakości + Spowoduje to również przywrócenie miniatur na zwierzętach, które nie mają miniatur paska wyszukiwania.\n\nMiniaturki paska wyszukiwania będą miały taką samą jakość jak bieżący film.\n\nTa funkcja działa najlepiej z jakością wideo 720p lub niższą oraz przy użyciu bardzo szybkiego połączenia internetowego. Przywróć stare miniatury paska nawigacji Miniatury paska wyszukiwania pojawią się nad paskiem wyszukiwania Miniaturki paska wyszukiwania pojawią się na pełnym ekranie - + Włącz SponsorBlock\'a SponsorBlock jest systemem crowd-sourcing do pominięcia irytujących części filmów YouTube Wygląd @@ -950,7 +945,7 @@ This is because Crowdin requires temporarily flattening this file and removing t O programie Dane są dostarczane przez API SponsorBlock. Dotknij tutaj, aby dowiedzieć się więcej i zobaczyć pobierania dla innych platform - + Wersja Spoof app Wersja zespoofowana Wersja nie zespoofowana @@ -963,9 +958,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Przywracanie szerokiej prędkości wideo & menu jakości 18.09.39 - Przywróć kartę bibliotek 17.41.37 - Przywróć starą półkę na liście odtwarzania - 17.33.42 - Przywróć stary układ interfejsu użytkownika - + Ustaw stronę startową Domyślnie Przeglądaj kanały @@ -983,18 +977,20 @@ This is because Crowdin requires temporarily flattening this file and removing t Popularne Obserwuj później - + Wyłącz wznawianie odtwarzacza Shorts Odtwarzacz Shorts nie będzie wznawiany przy starcie aplikacji Odtwarzacz Shorts zostanie wznowiony przy starcie aplikacji - + + + Włącz układ tabletu Układ tabletu jest włączony Układ tabletu jest wyłączony Posty społeczności nie pojawiają się w układach tabletów - + Minigracz Zmień styl zminimalizowanego odtwarzacza aplikacji Typ minigracza @@ -1013,6 +1009,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Włącz przeciągnij i upuść Przeciągnij i upuść jest włączone\n\nMiniplayer może być przeciągnięty do dowolnego rogu ekranu Przeciągnij i upuść jest wyłączona + Włącz gest przeciągania poziomego + Gest przeciągania w poziomie włączony\n\nMiniplayer może być przesunięty z ekranu w lewo lub w prawo + Gest przeciągania w poziomie wyłączony Ukryj przycisk zamykania Przycisk zamykający jest ukryty Przycisk zamykający jest pokazany @@ -1032,12 +1031,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Wartość przezroczystości między 0-100, gdzie 0 jest przezroczysty Przezroczystość nakładki musi być pomiędzy 0-100 - + Włącz ekran ładowania gradientu Ekran ładowania będzie miał gradient tła Ekran ładujący będzie miał stałe tło - + Włącz niestandardowy kolor paska paska Niestandardowy kolor paska nawigacji jest wyświetlany Oryginalny kolor paska nawigacji jest wyświetlany @@ -1045,12 +1044,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Kolor paska wyszukiwania Nieprawidłowa wartość koloru paska wyszukiwania - + Pomiń ograniczenia regionu obrazu Używanie hosta obrazu yt4.ggpht.com Używanie oryginalnego hosta obrazu\n\nWłączenie tego może naprawić brakujące obrazy, które są zablokowane w niektórych regionach - + Zakładka domowa @@ -1082,7 +1081,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Strzała tymczasowo niedostępna (kod statusu: %s) Strzałka tymczasowo niedostępna - + Pokaż zaawansowane ogłoszenia Ogłoszenia są wyświetlane przy starcie Ogłoszenia nie są wyświetlane przy starcie @@ -1090,47 +1089,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Nie udało się połączyć z dostawcą ogłoszeń Odrzuć - + Ostrzeżenie Twoja historia zegarka nie jest zapisywana.<br><br>Najprawdopodobniej jest to spowodowane przez blokowanie reklam DNS lub serwer proxy sieciowego.<br><br>Aby to naprawić, biała lista <b>s.youtube.com</b> lub wyłącz wszystkie bloki DNS i proxy. Nie pokazuj ponownie - + Włącz automatyczne powtarzanie Automatyczne powtarzanie jest włączone Automatyczne powtarzanie jest wyłączone - + Zespoofuj wymiar urządzenia Zespoofuj wymiar urządzenia Wymiary urządzenia nie są zespoofowane\n\nWłączenie tej opcji umożliwia ustawienie wyższej jakości wideo niż zwykle Włączenie tego może spowodować zacinanie się odtwarzanego filmu, pogorszenie żywotności baterii i nieznane działania niepożądane. - + Ustawienia GmsCore Ustawienia GmsCore - + Obejście przekierowań URL Przekierowanie URL jest omijane Przekierowanie URL nie jest omijane - + Otwórz linki w przeglądarce Otwieranie linków zewnętrznych Otwieranie linków w aplikacji - + Usuń parametr zapytania Parametr zapytania śledzenia jest usuwany z linków Parametr zapytania nie został usunięty z linków - + Wyłącz szczęście powiększenia Haptyka jest wyłączona Haptyka jest włączona - + Jakość automatyczna Zapamiętaj zmiany jakości wideo Zmiany jakości dotyczą wszystkich filmów @@ -1141,35 +1140,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Zmieniono domyślną jakość %1$s na: %2$s - + Pokaż przycisk szybkiego dialogu. Widoczny Przycisk nie jest wyświetlany - + + Własne menu prędkości odtwarzania + Pokaż niestandardowe menu prędkości + Niestandardowe menu prędkości nie jest wyświetlane Niestandardowe prędkości odtwarzania - Dodaj lub zmień dostępne prędkości odtwarzania + Dodaj lub zmień niestandardowe prędkości odtwarzania Prędkość niestandardowa musi być mniejsza niż %s. Używając wartości domyślnych. Nieprawidłowa niestandardowa prędkość odtwarzania. Używanie wartości domyślnych. - + Zapamiętaj zmiany prędkości odtwarzania Zmiany prędkości odtwarzania dotyczą wszystkich filmów Prędkość odtwarzania zmienia się tylko dla bieżącego filmu Domyślna prędkość odtwarzania Zmieniono domyślną prędkość na: %s - + Przywróć stare menu jakości wideo Wyświetlane jest stare menu jakości wideo Stare menu jakości wideo nie jest wyświetlane - + Włącz slajd, aby wyszukać Przesuń, aby przeszukiwać jest włączony Przesuń, aby przeszukiwać nie jest włączony - + Słuchanie strumieni wideo Słuchaj strumienia wideo klienta, aby zapobiec problemom z odtwarzaniem Słuchanie strumieni wideo @@ -1187,20 +1189,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Działania niepożądane związane z systemem Android VR • Brakuje menu ścieżki dźwiękowej\n• Stabilna głośność nie jest dostępna - - - Włącz automatyczną jasność HDR - Automatyczna jasność HDR jest włączona - Automatyczna jasność HDR jest wyłączona - - + Blokuj reklamy audio Reklamy audio są zablokowane Reklamy audio są odblokowane - + %s jest niedostępny. Reklamy mogą wyskakiwać. Spróbuj przełączyć się na inną usługę w ustawieniach. Serwer %s zwrócił błąd. Reklamy mogą wyświetlić. Spróbuj przełączyć się na inną usługę blokowania reklam w ustawieniach. Blokuj osadzone reklamy wideo @@ -1208,30 +1204,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Proxy świetlne Proxy PurpleAdBlock - + Blokuj reklamy wideo Reklamy wideo są zablokowane Reklamy wideo są odblokowane - + wiadomość usunięta Pokaż usunięte wiadomości Nie pokazuj usuniętych wiadomości Ukryj usunięte wiadomości za spoilerem Pokaż usunięte wiadomości jako przekreślony tekst - + Automatycznie włącz punkty kanału Punkty kanału są automatycznie odejmowane Punkty kanału nie są automatycznie odebrane - + Włącz tryb debugowania Twitch Tryb debugowania Twitch jest włączony (nie zalecane) Tryb debugowania Twitch jest wyłączony - + Ulepszone ustawienia Reklamy Ustawienia blokowania reklam diff --git a/src/main/resources/addresources/values-pt-rBR/strings.xml b/patches/src/main/resources/addresources/values-pt-rBR/strings.xml similarity index 93% rename from src/main/resources/addresources/values-pt-rBR/strings.xml rename to patches/src/main/resources/addresources/values-pt-rBR/strings.xml index 9d2eabc19..aefe04fa6 100644 --- a/src/main/resources/addresources/values-pt-rBR/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rBR/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Verificação falhou Abrir o site oficial Ignorar @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Patcheado há %s dias Data de compilação do APK está corrompida - + ReVanced Você deseja prosseguir? Resetar @@ -63,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Links oficiais Doar - + O MicroG GmsCore não está instalado. Instale-o. Ação necessária @@ -74,7 +74,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Sobre Anúncios Miniaturas alternativas @@ -86,7 +86,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Outras configurações Vídeo - + + Desativar reprodução de fundo dos Shorts + Reprodução de fundo dos Shorts está desativada + Reprodução de fundo dos Shorts está ativada + + Depuração Ativar ou desativar opções de depuração Registro de depuração @@ -103,13 +108,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Não mostrar notificação flutuante se ocorrer erro Desativar ocultar todas as notificações flutuantes de erro do ReVanced.\n\nVocê não será notificado de nenhum evento inesperado. - + Desativar brilho do botão de Inscrever-se / Curtir O botão de curtir e de inscrever-se não vai brilhar quando clicado O botão de curtir e de inscrever-se vai brilhar quando clicado - Ocultar separador cinza - Os separadores cinza está oculto - Separador cinza não está oculto + Ocultar cartões de álbum + Cartões de álbum estão ocultos + Cartões de álbum não estão ocultos + Ocultar caixa de financiamento coletivo + Caixa de financiamento coletivo está oculta + Caixa de financiamento coletivo não está oculta + Ocultar botão de microfone flutuante + Botão microfone está oculto + Botão microfone não está oculto Ocultar marca d\'água do canal Marca d\'água está oculta Marca d\'água não está oculta @@ -154,9 +165,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Ocultar cartão expansível em vídeos Cartão expansível está oculto Cartão expansível não está oculto - Ocultar rodapé do menu de qualidade de vídeo - Rodapé do menu de qualidade de vídeo está oculto - Rodapé do menu de qualidade de vídeo não está ocultos Ocultar publicações da comunidade Publicações da comunidade está oculto Publicações da comunidade não está oculto @@ -231,7 +239,42 @@ This is because Crowdin requires temporarily flattening this file and removing t Seção de transcrição não está oculta Descrição do vídeo Ocultar ou mostrar componentes de descrição do vídeo + Barra de filtro + Ocultar ou mostrar a barra de filtro na tela inicial, pesquisa e vídeos relacionados + Ocultar na tela inicial + Está oculto na tela inicial + Não está oculto na tela inicial + Ocultar na pesquisa + Está oculto na busca + Não está oculto na busca + Ocultar nos vídeos relacionados + Está oculto nos vídeos relacionados + Não está oculto nos vídeos relacionados + Comentários + Ocultar ou mostrar componentes da seção de comentários + Ocultar cabeçalho \'Comentários por membros\' + O cabeçalho \'Comentários dos membros\' está oculto + O cabeçalho \'Comentários dos membros\' é exibido + Ocultar seção de comentários + Seção de comentários está oculta + Seção de comentários exibida + Ocultar botão \'Criar um Short\' + O botão \'Criar um Short\' está oculto + O botão \'Criar um Short\' é exibido + Ocultar prévia de comentário + Prévia de comentário está oculta + Prévia de comentário não está oculta + Ocultar botão valeu + Botão valeu está oculto + Botão valeu não está oculto + Ocultar botões de tempo e emoji + Os botões de cronograma e emoji estão ocultos + Botões de tempo e emoji são mostrados + Ocultar Doodles do YouTube + Doodles na barra de pesquisa estão ocultos + Doodles na barra de pesquisa estão ativos + Doodles do YouTube aparecem alguns dias por ano.\n\nSe um Doodle estiver ativo em sua região e esta configuração de ocultação estiver ativada, a barra de filtros abaixo da barra de pesquisa também será oculta. Filtro personalizado Ocultar componentes usando filtros personalizados Ativar filtro personalizado @@ -268,7 +311,7 @@ This is because Crowdin requires temporarily flattening this file and removing t A palavra-chave é muito curta e requer aspas: %s A palavra-chave irá ocultar todos os vídeos: %s - + Ocultar anúncios gerais Anúncios gerais estão ocultos Anúncios gerais não estão ocultos @@ -287,6 +330,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Ocultar banner ver os produtos Banner está oculto Banner não está oculto + Ocultar painel de compras do reprodutor + O painel de compras está oculto + O painel de compras será exibido Ocultar links de compras na descrição do vídeo Links de compras estão ocultos Links de compras não estão ocultos @@ -303,17 +349,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Ocultar anúncios em tela cheia só funciona com dispositivos antigos - + Ocultar promoções do YouTube Premium Promoções do YouTube Premium sob o reprodutor de vídeo estão ocultas Promoções do YouTube Premium sob o reprodutor de vídeo não estão ocultas - + Ocultar anúncios do vídeo Anúncios do vídeo estão ocultos Anúncios do vídeo não estão ocultos - + URL copiada para a área de transferência URL com tempo copiado Mostrar botão copiar URL no vídeo @@ -323,13 +369,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Botão é exibido. Toque para copiar a URL do vídeo com temo. Toque e segure para copiar vídeos sem tempo Botão não está visível - + Remover diálogo de restrição Diálogo foi removido Diálogo não foi removido Isto não ignora a restrição de idade, apenas a aceita automaticamente. - + App de download externo Configurações para usar um app de download externo Mostrar botão de download externo @@ -343,17 +389,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Nome do pacote do seu app de baixar externo instalado, como NewPipe ou Seal %s não está instalado. Por favor, instale. - + Desativar gesto de busca precisa Gesto desativado O gesto está ativado - + Ativar toque na barra de busca Toque na barra de busca está ativado Toque na barra de busca está desativado - + Ativar gesto de brilho Gesto de brilho está ativado Gesto de brilho está desativado @@ -382,12 +428,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Limiar distância no gesto Quantidade limite que o gesto irá ocorrer - + Desativar legendas automáticas Legendas automáticas estão desativadas Legendas automáticas estão ativadas - + Botões de ação Ocultar ou mostrar botões sob vídeos Ocultar Gostei e Não gostei @@ -423,23 +469,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Botão salvar na playlist está oculto Botão salvar na playlist não está oculto - - Ocultar botão de reprodução automática - Botão de reprodução automática está oculto - Botão de reprodução automática não está oculto - - - - Ocultar botão legendas - Botão legendas está oculto - Botão legendas não está oculto - - - Ocultar botão de transmitir - Botão transmitir está oculto - Botão transmitir não está oculto - - + Botões de navegação Ocultar ou alterar botões na barra de navegação @@ -466,7 +496,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Rótulos são ocultos Rótulos são mostrados - + Menu flutuante Ocultar ou mostrar itens no menu suspenso do reprodutor @@ -477,6 +507,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Ocultar Configurações adicionais Menu de configurações adicionais está oculto Menu de configurações adicionais não está oculto + + Ocultar Timer de suspensão + O menu Timer de suspensão está oculto + O menu Timer de suspensão será exibido Ocultar Vídeo em Loop Menu de vídeo em loop está oculto @@ -485,6 +519,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Ocultar Modo ambiente Menu do modo ambiente está oculto Menu do modo ambiente não está oculto + Ocultar Volume estável + O menu de Volume estável será exibido + O menu de Volume estável está oculto Ocultar Ajuda & Feedback Menu ajuda & opinião está oculto @@ -510,83 +547,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Ocultar Assistir no VR Menu assistir no VR está oculto Menu assistir no VR não está oculto + Ocultar rodapé do menu de qualidade de vídeo + O rodapé do menu de qualidade de vídeo está oculto + Rodapé do menu de qualidade de vídeo não está ocultos - - Ocultar botões anterior & próxima vídeo - Botões estão ocultos - Botões não estão ocultos + + Ocultar botões anterior & próxima vídeo + Os botões estão ocultos + Os botões serão exibidos + Ocultar botão de transmitir + Botão transmitir está oculto + Botão transmitir não está oculto + + Ocultar botão legendas + Botão legendas está oculto + Botão legendas não está oculto + Ocultar botão de reprodução automática + Botão de reprodução automática está oculto + Botão de reprodução automática não está oculto - - Ocultar cartões de álbum - Cartões de álbum estão ocultos - Cartões de álbum não estão ocultos - - - Comentários - Ocultar ou mostrar componentes da seção de comentários - Ocultar cabeçalho \'Comentários por membros\' - O cabeçalho \'Comentários dos membros\' está oculto - O cabeçalho \'Comentários dos membros\' é exibido - Ocultar seção de comentários - Seção de comentários está oculta - Seção de comentários exibida - Ocultar botão \'Criar um Short\' - O botão \'Criar um Short\' está oculto - O botão \'Criar um Short\' é exibido - Ocultar prévia de comentário - Prévia de comentário está oculta - Prévia de comentário não está oculta - Ocultar botão valeu - Botão valeu está oculto - Botão valeu não está oculto - Ocultar botões de tempo e emoji - Os botões de cronograma e emoji estão ocultos - Botões de tempo e emoji são mostrados - - - Ocultar caixa de financiamento coletivo - Caixa de financiamento coletivo está oculta - Caixa de financiamento coletivo não está oculta - - + Ocultar cartões de tela final Cartões de tela final estão ocultos Cartões de tela final não estão ocultos - - Barra de filtro - Ocultar ou mostrar a barra de filtro na tela inicial, pesquisa e vídeos relacionados - Ocultar na tela inicial - Está oculto na tela inicial - Não está oculto na tela inicial - Ocultar na pesquisa - Está oculto na busca - Não está oculto na busca - Ocultar nos vídeos relacionados - Está oculto nos vídeos relacionados - Não está oculto nos vídeos relacionados - - - Ocultar botão de microfone flutuante - Botão microfone está oculto - Botão microfone não está oculto - - + Desativar o modo ambiente em tela cheia Modo ambiente desativado Modo ambiente ativado - + Ocultar cartões de informações Cartões de informações estão ocultos Cartões de informações não estão ocultos - + Desativar animações de números rodando Os números rolantes não são animados Os números rolantes são animados - + Ocultar barra de busca no reprodutor de vídeo Barra de busca no reprodutor de vídeo está oculto Barra de busca no reprodutor de vídeo não está oculto @@ -594,7 +594,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Barra de busca nas miniaturas estão oculta Barra de busca nas miniaturas não estão oculta - + + Reprodutor do Shorts + Ocultar ou mostrar componentes no reprodutor de Shorts Ocultar Shorts na tela inicial Os Shorts no feed inicial estão ocultos @@ -630,6 +632,11 @@ This is because Crowdin requires temporarily flattening this file and removing t Rótulo de localização está oculto Rótulo de localização é mostrado Ocultar o botão de salvar música + O botão Salvar música está oculto + O botão Salvar música será exibido + Ocultar botão Usar template + O botão Usar template está oculto + O botão Usar template será exibido Ocultar sugestões de busca Sugestões de pesquisa estão ocultas Sugestões de pesquisa são mostradas @@ -678,27 +685,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Barra de navegação está oculta Barra de navegação não está oculta - + Desativar tela finais com vídeo sugerido Vídeo sugerido está desativado Vídeo sugerido está ativado - + Ocultar tempo do vídeo Tempo está oculto Tempo não está oculto - + Ocultar painel popup de reprodutor Painel pop-up do reprodutor está oculto Painel pop-up do reprodutor não está oculto - + Opacidade do reprodutor Valor de opacidade entre 0-100, onde 0 é transparente Opacidade do jogador deve estar entre 0-100 - + Não gostei indisponível por um tempo (API expirou) Não gostei indisponível (status %d) @@ -742,17 +749,17 @@ This is because Crowdin requires temporarily flattening this file and removing t %d taxa limite do cliente encontrado %d milisegundos - + Ativar barra de busca ampla Barra de busca ampla está ativada Barra de busca ampla está desativada - + Restaurar as miniaturas antigas da barra de busca As miniaturas aparecerão acima da barra de busca As miniaturas na barra de busca aparecerão em tela cheia - + Ativar SponsorBlock SponsorBlock é um sistema coletivo para pular partes patrocinadas ou irritantes de vídeos do YouTube Aparência @@ -933,7 +940,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Sobre Os dados são fornecidos pela API do SponsorBlock. Toque aqui para aprender mais e ver como baixar para outras plataformas - + Spoofing da versão do aplicativo Versão spoofada Versão não spoofada @@ -946,9 +953,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Restaurar a velocidade de vídeo ampla & menu de qualidade 18.09.39 - Restaurar aba biblioteca 17.41.37 - Restaurar prateleira de lista de reprodução antiga - 17.33.42 - Restaurar layout antigo da interface - + Definir página inicial Padrão Explorar @@ -958,18 +964,20 @@ This is because Crowdin requires temporarily flattening this file and removing t Inscrições Em alta - + Desativar continuar a reproduzir Shorts Shorts não irá continuar reproduzindo ao iniciar o aplicativo Shorts irá continuar reproduzindo ao iniciar o aplicativo - + + + Ativar layout de tablet Layout de tablet está ativado Layout de tablet está desativado Postagens da comunidade não aparecem nos layouts de tablet - + Miniplayer Alterar o estilo do player minimizado no aplicativo Tipo de miniplayer @@ -989,12 +997,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Valor de opacidade entre 0-100, onde 0 é transparente Opacidade da sobreposição de miniplayer deve estar entre 0-100 - + Ativar tela de carregamento em gradiente Tela de carregamento terá um fundo em gradiente Tela de carregamento terá um fundo sólido - + Ativar cor personalizada da barra de busca Mostrar cor personalizada da barra de busca Mostrar cor original da barra de busca @@ -1002,12 +1010,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Cor da barra de busca Valor de cor da barra de busca inválido - + Ignorar restrições de região de imagem Usando imagem host yt4.ggpht.com Usando a imagem original host\n\nHabilitando isso pode corrigir imagens ausentes que estão bloqueadas em algumas regiões - + Início @@ -1039,7 +1047,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow temporariamente indisponível. (status: %s) DeArrow temporariamente indisponível - + Exibir avisos do ReVanced Avisos são mostrados na inicialização Avisos não são mostrados na inicialização @@ -1047,47 +1055,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Falha ao conectar ao provedor de avisos Dispensar - + Atenção Seu histórico de exibição não está sendo salvo.<br><br>Na maioria dos casos isso é causado por um bloqueador de anúncios por DNS ou proxy de rede.<br><br>Para corrigir isso, permita <b>s.youtube.com</b> na sua lista ou desative todos os bloqueadores de DNS e proxies. Não exibir novamente - + Ativar repetição automática Repetição automática está ativada Repetição automática está desativada - + Spoofing de dimensões do dispositivo Dimensões do dispositivo spoofadas\n\nQualidades maiores de vídeo podem ser desbloqueadas, mas você pode experienciar travamentos na repodução de vídeo, maior gasto de bateria e efeitos colaterais desconhecidos Dimensões do dispositivo não spoofadas\n\nAtivando isso pode desbloquear maiores qualidades de vídeo Ativar isto pode causar travamentos na reprodução de vídeo, maior gasto de bateria e efeitos colaterais desconhecidos. - + Configurações do GmsCore Configurações do GmsCore - + Ignorar redirecionamentos de URL Redirecionamentos de URL estão ignorados Redirecionamentos de URL não estão ignorados - + Abrir links no navegador Abrir links externamente Abrir links no aplicativo - + Remover parâmetro de consulta de rastreamento Parâmetro da consulta de rastreamento foi removido dos links Parâmetro da consulta de rastreamento não foi removido dos links - + Desativar zoom tátil Zoom tátil está ativado Zoom tátil está desativado - + Qualidade automática Lembrar mudanças na qualidade do vídeo Mudança na qualidade se aplicam a todos os vídeos @@ -1098,35 +1106,34 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Qualidade padrão %1$s alterada para: %2$s - + Mostrar botão de diálogo de velocidade Botão não esta oculto Botão não está visível - + Velocidade de reprodução personalizada - Adicionar ou alterar as velocidades de reprodução disponíveis Velocidades personalizadas devem ser menores que %s. Usando valores padrão. Velocidade personalizada de reprodução inválida. Usando valores padrão. - + Lembrar mudança na velocidade de reprodução Mudanças de velocidade de reprodução se aplicam a todos os vídeos Mudanças na velocidade de reprodução só se aplicam ao vídeo atual Velocidade padrão de reprodução Velocidade padrão alterada para: %s - + Restaurar menu antigo de qualidade de vídeo Menu antigo de qualidade de vídeo está sendo mostrado Menu antigo de qualidade de vídeo não está sendo mostrado - + Ativar gesto na barra de busca Gesto na barra de busca está ativado Gesto na barra de busca está desativado - + Spoofing do fluxo de vídeo Spoofa o fluxo de vídeo do cliente para evitar problemas de reprodução Spoofing do fluxo de vídeo @@ -1144,20 +1151,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Efeitos colaterais do spoofing de Android VR • Menu de Faixa de Áudio não está disponível\n• Opção Volume Estável não está disponível - - - Ativar o brilho HDR automático - O brilho HDR automático está ativado - O brilho HDR automático está desativado - - + Bloquear anúncios de áudio Anúncios de áudio estão bloqueados Anúncios de áudio não estão bloqueados - + %s não está disponível. Anúncios podem ser exibidos. Tente alternar para outro serviço de bloqueio de anúncios nas configurações. O servidor %s retornou um erro. Os anúncios podem ser exibidos. Tente alternar para outro serviço de bloqueio de anúncios nas configurações. Bloquear anúncios de vídeo incorporados @@ -1165,30 +1166,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Luminous proxy PurpleAdBlock proxy - + Bloquear anúncios em vídeo Anúncios de vídeo estão bloqueados Anúncios de vídeo não estão bloqueados - + Mensagem excluída Mostrar mensagens apagadas Não mostrar mensagens apagadas Ocultar mensagens apagadas atrás de um spoiler Mostrar mensagens apagadas como texto riscado - + Resgatar Pontos do Canal automaticamente Pontos do Canal estão sendo resgatados automaticamente Pontos do Canal não estão sendo resgatados automaticamente - + Ativar modo de depuração da Twitch Modo de depuração da Twitch ativado (não recomendado) Modo de depuração da Twitch está desativado - + Configurações do ReVanced Anúncios Configurações de bloqueio de anúncios diff --git a/src/main/resources/addresources/values-pt-rPT/strings.xml b/patches/src/main/resources/addresources/values-pt-rPT/strings.xml similarity index 91% rename from src/main/resources/addresources/values-pt-rPT/strings.xml rename to patches/src/main/resources/addresources/values-pt-rPT/strings.xml index f19aefbf9..fb873af7e 100644 --- a/src/main/resources/addresources/values-pt-rPT/strings.xml +++ b/patches/src/main/resources/addresources/values-pt-rPT/strings.xml @@ -32,17 +32,18 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Verificação falhou Abrir site oficial Ignorar + <h5>Esta app não parece ter sido modificada por ti.</h5><br>Esta app pode não funcionar corretamente, <b>pode ser maliciosa ou até perigosa de usar</b>.<br><br>Estas verificações implicam que esta app é pré-modificada ou obtida de outros:<br><br><small>%1$s</small><br>É extremamente recomendado <b>desinstalar esta app e modificá-la tu mesmo</b> para garantir que estás a usar uma app segura e validada.<p><br>Se ignorado, este aviso apenas será mostrado duas vezes. Patrulhado em um dispositivo diferente Não instalado pelo ReVanced Manager Corrigida há mais de 10 minutos Corrigida há %s dias Data de compilação do APK está corrompida - + Desejas continuar? Redefinir Atualizar e reiniciar @@ -61,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Links oficiais Doar - + MicroG GmsCore não está instalado. Instale-o. Ação necessária @@ -72,7 +73,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Sobre Anúncios Miniaturas alternativas @@ -84,7 +85,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Outros Vídeo - + + Desativar reprodução de fundo de Shorts + Reprodução de fundo de Shorts está desativada + Reprodução de fundo de Shorts está ativa + + Depuração Ativar ou desativar opções de depuração Registo da depuração @@ -101,13 +107,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Toast não visível se um erro ocorrer Desligar todos os avisos de erros irá esconder todas as notificações de erro do ReVanced.\n\nNão serás notificado de eventos inesperados. - + Desativar brilho do botão de inscrição / Curtir O botão de curtir e assinar não brilhará quando mencionado O botão de curtir e subscrever brilhará quando mencionado - Esconder separador cinza - Os separadores cinzentos estão escondidos - Separadores cinzas são visíveis + Esconder cartões de álbuns + Cartões de álbuns estão escondidos + Cartões de álbum são visíveis + Esconder caixa de crowdfunding + Caixa de Crowdfunding está escondida + Caixa de Crowdfunding é visível + Esconder o botão do microfone flutuante + Botão do microfone escondido + Botão do microfone visível Esconder a Marca D\'Água do canal A marca d\'água está escondida A marca d\'água está visível @@ -152,9 +164,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Esconder chip expansível nos vídeos Chips expansíveis estão escondidos Chips expansíveis são visíveis - Esconder rodapé do menu de qualidade de vídeo - O rodapé do menu de qualidade de vídeo está escondido - Cabeçalho do menu de qualidade de vídeo visível Esconder publicações da comunidade As postagens da comunidade estão escondidas Os posts da comunidade são visíveis @@ -229,6 +238,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Secção de transcrição exibida Descrição do vídeo Esconder ou mostrar componentes de descrição do vídeo + Barra de filtro + Esconder ou mostrar a barra de filtros no feed, pesquisa e vídeos relacionados + Esconder no feed + Escondido no feed + Mostrar no feed + Esconder na busca + Oculto em pesquisa + Mostrado na busca + Esconder em vídeos relacionados + Oculto em vídeos relacionados + Mostrar em vídeos relacionados + Comentários + Esconder ou mostrar componentes da seção de comentários + Ocultar cabeçalho \'Comentários por membros\' + O cabeçalho \'Comentários dos membros\' está oculto + O cabeçalho \'Comentários dos membros\' é exibido + Esconder seção de comentários + Seção de comentários está oculta + Seção de comentários exibida + Ocultar o botão \'Criar um Short\' + O botão \'Criar um Short\' está oculto + O botão \'Criar um Short\' é mostrado + Esconder comentário de pré-visualização + Visualização do comentário está escondida + Pré-visualização de comentário é exibida + Esconder botão de agradecimento + O botão de agradecimento está escondido + O botão Obrigado é visível + Ocultar botões de tempo e emoji + Os botões de cronograma e emoji estão ocultos + Botões de tempo e emoji são mostrados Ocultar Doodles do YouTube Doodles da barra de pesquisa estão escondidos @@ -262,6 +302,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Home/Assinatura/Busca resultados são filtrados para ocultar conteúdo que corresponde às frases chave\n\nLimitações\n• Shorts não podem ser ocultados pelo nome do canal\n• Alguns componentes do UI podem não ser ocultados\n• Procurar por uma palavra-chave pode não mostrar resultados Combinar palavras inteiras + Colocar uma frase/palavra-chave entre aspas irá prevenir correspondências parcias de títuloas de vídeos e nomes de canais<br><br>Por exemplo,<br><b>\"ia\"</b> vai esconder o vídeo: <b>Como IA funciona?</b><br>mas não vai esconder: <b>O que significa Inteligência Artificial?</b> Não é possível usar a palavra-chave: %s Adicionar aspas para usar a palavra-chave: %s @@ -269,7 +310,7 @@ This is because Crowdin requires temporarily flattening this file and removing t A palavra-chave é muito curta e requer citações: %s Palavra-chave irá ocultar todos os vídeos: %s - + Esconder anúncios gerais Anúncios gerais estão escondidos Anúncios gerais são mostrados @@ -288,6 +329,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Esconder banner para ver os produtos Banner está escondido Banner é visível + Ocultar prateleira de compras do jogador + Prateleira de compras está escondida + Prateleira de compras é mostrada Esconder links de compras na descrição do vídeo Links de compras estão escondidos Links de compras são visíveis @@ -304,17 +348,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Ocultar anúncios em tela cheia só funciona com dispositivos antigos - + Esconder promoções Premium do YouTube As promoções do YouTube Premium sob o reprodutor de vídeo estão escondidas Promoções do YouTube Premium sob o reprodutor de vídeo são visíveis - + Esconder anúncios do vídeo Anúncios de vídeo estão escondidos Anúncios de vídeo são visíveis - + URL copiado URL com timestamp copiado Mostrar botão de URL de vídeo copiado @@ -324,13 +368,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Botão é visível. Toque para copiar a URL do vídeo com timestamp. Toque e segure para copiar vídeos sem timestamp O botão não está visível - + Remover diálogo discreto do visualizador A caixa de diálogo será removida A caixa de diálogo será exibida Isto não ignora a restrição de idade, apenas a aceita automaticamente. - + Transferências externas Definições para usar um downloader externo Mostrar botão externo de transferir @@ -344,17 +388,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Nome do pacote da sua aplicação de downloader instalado, como NewPipe ou Seal %s não está instalado. Por favor, instale-o. - + Desativar gesto de pesquisa precisa Gesto está desativado Gesto ativado - + Ativar barra de busca As toques Seekbar estão ativadas As toques Seekbar estão desativadas - + Ativar gesto de brilho O deslize de brilho está ativado O deslize de brilho está desativado @@ -383,12 +427,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Limite de magnitude A quantidade limite para deslizar irá ocorrer - + Desativar legendas automáticas Legendas automáticas desativadas Legendas automáticas estão ativadas - + Botões de ação Esconder ou mostrar botões sob vídeos Esconder Curtir e Descurtir @@ -424,23 +468,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Salvar no botão de playlist está escondido Botão de salvar lista de reprodução é visível - - Esconder botão de reprodução automática - O botão de reprodução automática está escondido - Botão de reprodução automática é visível - - - - Esconder botão de legendas - O botão de legendas está escondido - Botão de legendas é visível - - - Esconder botão transmitir - Botão \"Transmitir\" está escondido - Botão \"Transmitir\" é visível - - + Navigation buttons Esconder ou alterar botões na barra de navegação @@ -467,7 +495,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Marcadores estão ocultos Marcadores são mostrados - + Menu suspenso Esconder ou mostrar itens do menu de saída do player @@ -478,6 +506,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Esconder configurações adicionais Menu de configurações adicionais está escondido Menu de configurações adicionais é visível + + Ocultar Timer de Suspensão + O menu Temporizador está oculto + O menu Timer para dormir é mostrado Esconder vídeo Loop Menu de vídeo Loop escondido @@ -486,6 +518,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Esconder modo ambiente Menu do modo ambiente está escondido Menu do modo ambiente é visível + Ocultar volume estável + Menu de volume estável é mostrado + Menu de volume estável está oculto Esconder Ajuda & Feedback Ajuda & o menu de feedback está escondido @@ -511,83 +546,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Esconder relógio no VR Assista no menu VR está escondido Assistir no menu VR é visível + Esconder rodapé do menu de qualidade de vídeo + O rodapé do menu de qualidade de vídeo está oculto + Cabeçalho do menu de qualidade de vídeo mostrado - - Esconder botões anteriores & próxima vídeo - Botões estão escondidos - Botões são exibidos + + Esconder botões anteriores & próxima vídeo + Botões estão ocultos + Botões são exibidos + Esconder botão transmitir + Botão \"Transmitir\" está escondido + Botão \"Transmitir\" é visível + + Esconder botão de legendas + O botão de legendas está escondido + Botão de legendas é visível + Esconder botão de reprodução automática + O botão de reprodução automática está escondido + Botão de reprodução automática é visível - - Esconder cartões de álbuns - Cartões de álbuns estão escondidos - Cartões de álbum são visíveis - - - Comentários - Esconder ou mostrar componentes da seção de comentários - Ocultar cabeçalho \'Comentários por membros\' - O cabeçalho \'Comentários dos membros\' está oculto - O cabeçalho \'Comentários dos membros\' é exibido - Esconder seção de comentários - Seção de comentários está oculta - Seção de comentários exibida - Ocultar o botão \'Criar um Short\' - O botão \'Criar um Short\' está oculto - O botão \'Criar um Short\' é mostrado - Esconder comentário de pré-visualização - Visualização do comentário está escondida - Pré-visualização de comentário é exibida - Esconder botão de agradecimento - O botão de agradecimento está escondido - O botão Obrigado é visível - Ocultar botões de tempo e emoji - Os botões de cronograma e emoji estão ocultos - Botões de tempo e emoji são mostrados - - - Esconder caixa de crowdfunding - Caixa de Crowdfunding está escondida - Caixa de Crowdfunding é visível - - + Esconder cartões de ecrã final Cartões de fim de ecrã estão escondidos Cartões de fim de ecrã são exibidos - - Barra de filtro - Esconder ou mostrar a barra de filtros no feed, pesquisa e vídeos relacionados - Esconder no feed - Escondido no feed - Mostrar no feed - Esconder na busca - Oculto em pesquisa - Mostrado na busca - Esconder em vídeos relacionados - Oculto em vídeos relacionados - Mostrar em vídeos relacionados - - - Esconder o botão do microfone flutuante - Botão do microfone escondido - Botão do microfone visível - - + Desativar o modo ambiente em ecrã cheia Modo ambiente desativado Modo ambiente ativado - + Esconder cartões de informação Cartões de informação estão escondidos Cartões de informação são exibidos - + Desativar animações de números rolantes Números de rolagem não estão animados Números de rolagem estão animados - + Esconder barra de busca no reprodutor de vídeo Barra de busca do vídeo está escondida Barra de busca do vídeo visível @@ -595,7 +593,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Barra de busca em miniaturas está escondida Barra de busca de miniaturas visível - + + Reprodutor de Shorts + Ocultar ou mostrar componentes no player Shorts Esconder Shorts no feed inicial Os Shorts no feed inicial estão ocultos @@ -693,27 +693,27 @@ This is because Crowdin requires temporarily flattening this file and removing t A barra de navegação está escondida Barra de navegação exibida - + Desativar ecrã de fim de vídeo sugerida Vídeos sugeridos serão desativados Vídeos sugeridos serão exibidos - + Esconder timestamp do vídeo Timestamp está escondido Timestamp é visível - + Esconder painéis popup do player Painéis pop-up do jogador estão escondidos Painéis pop-up do jogador são visíveis - + Opacidade do jogador Valor de opacidade entre 0-100, onde 0 é transparente Opacidade do jogador deve estar entre 0-100 - + \"Não Gosto\" temporariamente indisponível Descurtir não disponível (status %d) @@ -757,17 +757,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Limite da taxa de cliente encontrado %d vezes %d milissegundos - + Ativar barra de pesquisa ampla Barra de pesquisa ampla está ativada Barra de pesquisa ampla está desativada - + + Habilitar miniaturas de alta qualidade + As miniaturas na barra de busca são de alta qualidade + As miniaturas na barra de busca são de qualidade média + As miniaturas da barra de busca completa de tela são de alta qualidade + As miniaturas da barra de busca completa são de qualidade média + Isso também irá restaurar miniaturas em filmagens que não possuem miniaturas na barra de pesquisa.\n\nAs miniaturas da barra de busca usarão a mesma qualidade do vídeo atual.\n\nEste recurso funciona melhor com uma qualidade de vídeo de 720p ou inferior e usando uma conexão de internet muito rápida. Restaurar as miniaturas antigas da barra de pesquisa As miniaturas da barra de busca aparecerão acima da barra de busca As miniaturas da Seekbar aparecerão em ecrã cheia - + Habilitar Patrocínio O Patrocinador é um sistema coletivo para pular partes irritantes dos vídeos do YouTube Aparência @@ -948,7 +954,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Sobre Os dados são fornecidos pela API do SponsorBlock. Toque aqui para aprender mais e ver downloads para outras plataformas - + Versão do Spoof app Versão falsificada Versão não falsificada @@ -961,9 +967,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Restaurar menu de qualidade de vídeo & 18.09.39 - Restaurar aba da biblioteca 17.41.37 - Restaurar a pategoria de playlist antiga - 17.33.42 - Restaurar layout de interface antiga - + Definir página inicial Padrão Procurar canais @@ -981,18 +986,26 @@ This is because Crowdin requires temporarily flattening this file and removing t Tendências Assistir depois - - Desativar modo retomada do player - Curta o reprodutor não continuará na inicialização do aplicativo + + Desativar a retomada do player do Shorts + Desativar a retomada do player do Shorts Shorts que o reprodutor continuará na inicialização do aplicativo - + + Reprodução automática de Shorts + Shorts vão ser reproduzidos automaticamente + Shorts irão repetir + Reprodução automática de Shorts no fundo + Reprodução de fundo de Shorts irá reproduzir automaticamente + Reprodução de fundo de Shorts irá repetir + + Habilitar layout do tablet O layout do tablet está ativado Layout de tablet desativado Postagens da comunidade não aparecem nos layouts do tablet - + Minijogador Alterar o estilo do player minimizado no aplicativo Tipo de minijogador @@ -1011,6 +1024,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Ativar arrastar e soltar Arrastar e soltar é habilitado\n\nMiniplayer pode ser arrastado para qualquer canto da tela Arrastar e soltar está desativado + Ativar o gesto de arrastar horizontal + Gesto de arrastar horizontal ativado\n\nMiniplayer pode ser arrastado para a esquerda ou para a direita + Gestos de arrastar horizontais desativados Ocultar botão fechar O botão fechar está oculto Botão fechar é mostrado @@ -1030,12 +1046,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Valor de opacidade entre 0-100, onde 0 é transparente Opacidade da sobreposição de minijogador deve estar entre 0-100 - + Ativar ecrã de carregamento do gradiente Carregar ecrã terá um fundo em gradiente Carregar ecrã terá um fundo sólido - + Ativar a cor personalizada Cor personalizada da barra de busca é visível Cor original da barra de busca é visível @@ -1043,12 +1059,12 @@ This is because Crowdin requires temporarily flattening this file and removing t A cor da barra de busca Valor de cor de seekbar inválido - + Ignorar restrições de região de imagem Usando imagem host yt4.ggpht.com Usando a imagem original host\n\nHabilitando isso pode corrigir imagens ausentes que estão bloqueadas em algumas regiões - + Aba principal @@ -1080,7 +1096,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow temporariamente indisponível (Status Code: %s) DeArrow temporariamente não disponível - + Exibir avisos de ReVanced Anúncios são visíveis na inicialização Anúncios não são visíveis na inicialização @@ -1088,46 +1104,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Falha ao conectar ao provedor de avisos Dispensar - + Atenção + O teu histórico de visualização não está a ser guardado.<br><br>Isto é mais provávelmente causado por um bloqueador de anúncios DNS ou proxy de rede.<br><br>Para reparar isto, adiciona às exceções <b>s.youtube.com</b> ou desativa todos os bloqueadores DNS e proxies. Não mostrar novamente - + Habilitar auto-repetição Repetição automática está habilitada Repetição automática está desativada - + Dimensões do dispositivo Spoof Dimensões do dispositivo spoofed\n\nQualidades maiores de vídeo podem ser desbloqueadas, mas você pode experimentar uma reprodução de vídeo, pior vida de bateria e efeitos colaterais desconhecidos Dimensões do dispositivo não falsificadas\n\nHabilitando isso pode desbloquear maiores qualidades de vídeo Habilitar isto pode causar travamentos na reprodução de vídeo, pior vida na bateria e efeitos colaterais desconhecidos. - + Configurações do GmsCore Configurações para GmsCore - + Ignorar redirecionamentos de URL Redirecionamentos de URL estão ignorados Redirecionamentos de URL não estão ignorados - + Abrir links no navegador Abrir links externamente Abrir links no aplicativo - + Remover parâmetro de consulta de rastreamento O parâmetro da consulta de rastreamento é removido dos links O parâmetro da consulta de rastreamento não é removido dos links - + Desativar zoom haptics Hápticos estão desativados Hábitos estão ativados - + Qualidade automática Lembrar mudanças na qualidade do vídeo Alterações de qualidade se aplicam a todos os vídeos @@ -1138,35 +1155,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wi-fi Qualidade padrão %1$s alterada para: %2$s - + Mostrar botão de diálogo de velocidade Botão é visível O botão NÃO está visível - + + Menu personalizado de velocidade de reprodução + O menu de velocidade personalizado é exibido + O menu de velocidade personalizado não é mostrado Velocidade de reprodução personalizada - Adicionar ou alterar as velocidades de reprodução disponíveis + Adicionar ou alterar as velocidades de reprodução personalizadas Velocidades personalizadas devem ser menores que %s. Usando valores padrão. Velocidade personalizada de reprodução inválida. Usando valores padrão. - + Lembrar velocidade de reprodução As mudanças de velocidade de reprodução aplicam-se a todos os vídeos As mudanças de velocidade de reprodução só se aplicam ao vídeo atual Velocidade padrão de reprodução Velocidade padrão alterada para: %s - + Restaurar menu antigo de qualidade de vídeo Menu antigo de qualidade de vídeo exibido Menu antigo de qualidade de vídeo não visível - + Habilitar o slide para procurar Deslize para procurar está ativado Deslize para procurar não está habilitado - + Fluxos de vídeo falsos Disfarçar os fluxos de vídeo do cliente para evitar problemas de reprodução Fluxos de vídeo falsos @@ -1184,20 +1204,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Android VR efeito de spoofing side • Falta o menu de faixa de áudio\n• Volume estável não está disponível - - - Ativar o brilho HDR automático - O brilho HDR automático está ativado - O brilho HDR automático está desativado - - + Bloquear anúncios de áudio Anúncios de áudio estão bloqueados Anúncios de áudio são desbloqueados - + %s não está disponível. Anúncios podem ser exibidos. Tente alternar para outro serviço de bloco de anúncios nas configurações. O servidor %s retornou um erro. Os anúncios podem ser exibidos. Tente alternar para outro serviço de bloqueio de anúncios nas configurações. Bloquear anúncios de vídeo incorporados @@ -1205,30 +1219,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Proxy luminoso Proxy do PurpleAdBlock - + Bloquear anúncios em vídeo Anúncios de vídeo estão bloqueados Anúncios de vídeo são desbloqueados - + mensagem apagada Mostrar mensagens excluídas Não exibir mensagens excluídas Esconder mensagens excluídas atrás de um spoiler Mostrar mensagens excluídas como texto ultrapassado - + Automaticamente reivindicar Pontos do Canal Os Pontos de Canal são reivindicados automaticamente Os Pontos do Canal não são reivindicados automaticamente - + Ativar modo de depuração do Twitch Modo de depuração da Twitch ativado (não recomendado) Modo de depuração da Twitch está desativado - + Configurações Avançadas Anúncios Configurações de bloqueio de anúncios diff --git a/src/main/resources/addresources/values-ro-rRO/strings.xml b/patches/src/main/resources/addresources/values-ro-rRO/strings.xml similarity index 92% rename from src/main/resources/addresources/values-ro-rRO/strings.xml rename to patches/src/main/resources/addresources/values-ro-rRO/strings.xml index 8283c58e2..d8986d776 100644 --- a/src/main/resources/addresources/values-ro-rRO/strings.xml +++ b/patches/src/main/resources/addresources/values-ro-rRO/strings.xml @@ -32,17 +32,18 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Verificări eșuate Deschide site-ul oficial Ignoră + <h5>Se pare că aplicația nu este modificată de dv.</h5><br>Este posibil ca aplicația să nu funcționeze corect, <b>să dăuneze dispozitivului sau să prezinte un pericol la utilizare</b>.<br><br>Simpla existență a acestor verificări sugerează faptul că aplicația este modificată în prealabil sau obținută de la altcineva: <br><br><small>%1$s</small><br>Recomandăm cu tărie <b>dezinstalarea și modificarea manuală a acestei aplicații</b> pentru a vă asigura că aplicația pe care o folosiți este validă și sigură.<p><br>După ignorare, mesajul nu va mai fi afișat decât o a doua oară. Patchat pe un alt dispozitiv Nu este instalat de ReVanced Manager Patchat acum mai mult de 10 minute Patchat %s zile în urmă Data construcției APK este coruptă - + ReVanced Doriți să continuați? Resetează @@ -62,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Link-uri oficiale Donează - + MicroG GmsCore nu este instalat. Instalați-o. Acțiuni necesare @@ -73,7 +74,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Despre Anunţuri Miniaturi alternative @@ -85,7 +86,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Diverse Video - + + Dezactivează redarea în fundal a videoclipurilor Shorts + Redarea în fundal este dezactivată + Redarea în fundal este activată + + Depanare Activează sau dezactivează opțiunile de depanare Jurnal depanare @@ -102,13 +108,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Toast nu este afișat dacă apare o eroare Eroare la dezactivare toasts ascunde toate notificările de eroare ReVanced\n\nNu veți fi notificat de niciun eveniment neașteptat. - + Dezactivează ca / abonare strălucire buton Butonul Îmi place și abonare nu va străluci când este menționat Butonul de like-uri și abonare va străluci când este menționat - Ascunde separatorul gri - Separatoarele gri sunt ascunse - Se afișează separatoarele gri + Ascundeți cardurile de album + Cardurile de album sunt ascunse + Cardurile de album sunt afișate + Ascunde caseta de crowdfunding + Caseta multifinanțare este ascunsă + Cutia multifinanțare este afișată + Ascunde butonul de microfon plutitor + Butonul microfon ascuns + Butonul Microfon afișat Ascunde watermark canal Marcajul este ascuns Se afișează filigran @@ -153,9 +165,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Ascunde cipul expandabil sub videoclipuri Cipurile expandabile sunt ascunse Cipurile expandabile sunt afișate - Ascunde subsol meniu calitate video - Subsolul meniului calităţii video este ascuns - Subsolul meniului calității video este afișat Ascunde postările comunității Posturile comunitare sunt ascunse Posturile comunitare sunt afișate @@ -230,6 +239,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Secțiunea de Transcriere este afișată Descriere video Ascunde sau afișează componentele descrierii video + Bară de filtrare + Ascunde sau afișează bara de filtrare în flux, căutare și videoclipuri asociate + Ascunde în feed + Ascuns în feed + Afișat în feed + Ascunde în căutare + Ascuns în căutare + Afișat în căutare + Ascunde în videoclipurile asociate + Ascuns în videoclipuri conexe + Afișat în videoclipuri conexe + Comentarii + Ascunde sau afișează componentele secțiunii comentarii + Ascunde \'Comentarii după antetul membrilor + \'Comentarii de la antetul membrilor este ascuns + \'Comentarii de la antetul membrilor este afișat + Ascunde secțiunea comentarii + Secţiunea de comentarii este ascunsă + Secțiunea comentariilor este afișată + Ascunde butonul \'Creare Short\' + Butonul \'Crează un Short\' este ascuns + Butonul \'Crează un Short\' este afișat + Ascunde previzualizarea comentariului + Previzualizarea comentariului este ascunsă + Previzualizarea comentariului este afișată + Ascunde butonul de mulțumire + Butonul de multumire este ascuns + Butonul de multumire este afisat + Ascunde butoanele timestamp și emoji + Butoanele de timp și emoji sunt ascunse + Butoanele de timp și emoji sunt afișate Ascunde Doodle-urile YouTube Doodles bară de căutare sunt ascunse @@ -260,7 +300,6 @@ This is because Crowdin requires temporarily flattening this file and removing t This is because keywords can be in any language, and showing an example in the localized script helps convey this. --> Cuvinte cheie și fraze de ascuns, separate prin linii noi\n\nCuvintele cheie pot fi nume de canal sau orice text afișat în titlurile video\n\nCuvinte cu litere mari în mijloc trebuie să fie introduse cu caseta (ex: iPhone, TikTok, LeBlanc) Despre filtrarea cuvintelor cheie - Rezultatele Acasă/Abonament/Căutare sunt filtrate pentru a ascunde conținutul care corespunde cuvintelor-cheie\n\nLimitările\n• Scurtăturile nu pot fi ascunse după numele canalului\n• Este posibil ca unele componente UI să nu fie ascunse\n• Căutarea unui cuvânt cheie poate să nu arate rezultate Potrivește cuvinte întregi Supravieţuirea unui cuvânt cheie/frază cu ghilimele duble va preveni meciurile parţiale ale titlurilor video şi numelui canalelor<br><br>De exemplu,<br><b>\"ai\"</b> va ascunde video-ul: <b>How does AI work?</b><br>dar nu se va ascunde: <b>What does fair use mean?</b> @@ -271,7 +310,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Cuvântul cheie este prea scurt și necesită oferte: %s Cuvântul cheie va ascunde toate videoclipurile: %s - + Ascunde reclamele generale Anunțurile generale sunt ascunse Anunțurile generale sunt afișate @@ -290,6 +329,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Ascunde banner-ul pentru a vizualiza produsele Banner-ul este ascuns Banner-ul este afișat + Ascunde raftul jucătorului + Platforma de cumpărături este ascunsă + Platforma de cumpărături este afișată Ascunde link-urile de cumpărături în descrierea video Linkurile de cumpărături sunt ascunse Linkurile de cumpărături sunt afișate @@ -306,17 +348,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Ascunde reclamele pe tot ecranul funcționează doar cu dispozitive mai vechi - + Ascunde promoțiile YouTube Premium Promoțiile YouTube Premium sub video player sunt ascunse Promoțiile YouTube Premium sub video player sunt afișate - + Ascunde reclamele video Anunțurile video sunt ascunse Anunțurile video sunt afișate - + URL copiat în clipboard URL cu marcaj de timp copiat Afișare buton copie URL @@ -326,13 +368,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Butonul este afișat. Atingeți pentru a copia URL-ul video cu marcajul de timp. Atingeți și țineți apăsat pentru a copia videoclipul fără marcaj de timp Butonul nu este afișat - + Eliminați dialogul discreționar al vizualizatorului Dialogul va fi șters Va fi afișat catalogul Aceasta nu ocolește restricția de vârstă. O acceptă automat. - + Descărcări externe Setări pentru utilizarea unui downloader extern Arată butonul extern de descărcare @@ -346,17 +388,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Numele pachetului aplicaţiei externe downloader instalate, cum ar fi NewPipe sau Seal %s nu este instalat. Vă rugăm să-l instalaţi. - + Dezactivează gestul de căutare precis Gestul este dezactivat Gestul este activat - + Activează atingerea barei de căutare Apăsarea pe bara de căutare este activată Apăsarea pe bara de căutare este dezactivată - + Activează gestul de luminozitate Tragerea luminozității este activată Tragerea luminozității este dezactivată @@ -385,12 +427,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Pragul mărimii glisării Cantitatea de prag pentru a glisa - + Dezactivează subtitrările automate Legendele automate sunt dezactivate Legenda automată este activată - + Butoane de acţiune Ascunde sau arată butoanele sub videoclipuri Ascunde Like și Dislike @@ -426,23 +468,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Butonul Salvare în lista de redare este ascuns Butonul Salvare în lista de redare este afișat - - Ascunde butonul de redare automată - Butonul Autoplay este ascuns - Butonul Auto-redare este afișat - - - - Ascunde butonul de legendă - Butonul subtitrari este ascuns - Se afișează butonul de subtitrări - - - Ascunde butonul de redare - Butonul de distribuție este ascuns - Butonul de execuție este afișat - - + Navigation buttons Ascunde sau modifică butoanele din bara de navigare @@ -469,7 +495,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Etichetele sunt ascunse Etichetele sunt afișate - + Flyout menu Ascunde sau arată elementele de meniu Flyout ale jucătorului @@ -480,6 +506,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Ascunde setări adiționale Meniul de setări suplimentare este ascuns Setările adiționale sunt afișate + + Ascunde temporizatorul de somn + Meniul cronometrului de somn este ascuns + Meniul cronometrului de somn este afișat Ascunde repetiție video Meniul Repetă video este ascuns @@ -488,6 +518,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Ascunde modul Ambient Meniul modului Ambient este ascuns Meniul modului ambiental este afișat + Ascunde volum stabil + Meniul de volum stabil este afișat + Meniul volumului stabil este ascuns Ascunde Ajutor & Feedback Meniul Ajutor & Feedback este ascuns @@ -513,83 +546,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Ascunde ceas în VR Vizionarea în meniul VR este ascunsă Vizionați în meniul VR este afișat + Ascunde subsol meniu calitate video + Subsolul meniului calităţii video este ascuns + Subsolul meniului calității video este afișat - - Ascunde butoanele anterioare & următorul video - Butoanele sunt ascunse - Butoanele sunt afișate + + Ascunde butoanele anterioare & următorul video + Butoanele sunt ascunse + Butoanele sunt afișate + Ascunde butonul de redare + Butonul de distribuție este ascuns + Butonul de execuție este afișat + + Ascunde butonul de legendă + Butonul subtitrari este ascuns + Se afișează butonul de subtitrări + Ascunde butonul de redare automată + Butonul Autoplay este ascuns + Butonul Auto-redare este afișat - - Ascundeți cardurile de album - Cardurile de album sunt ascunse - Cardurile de album sunt afișate - - - Comentarii - Ascunde sau afișează componentele secțiunii comentarii - Ascunde \'Comentarii după antetul membrilor - \'Comentarii de la antetul membrilor este ascuns - \'Comentarii de la antetul membrilor este afișat - Ascunde secțiunea comentarii - Secţiunea de comentarii este ascunsă - Secțiunea comentariilor este afișată - Ascunde butonul \'Creare Short\' - Butonul \'Crează un Short\' este ascuns - Butonul \'Crează un Short\' este afișat - Ascunde previzualizarea comentariului - Previzualizarea comentariului este ascunsă - Previzualizarea comentariului este afișată - Ascunde butonul de mulțumire - Butonul de multumire este ascuns - Butonul de multumire este afisat - Ascunde butoanele timestamp și emoji - Butoanele de timp și emoji sunt ascunse - Butoanele de timp și emoji sunt afișate - - - Ascunde caseta de crowdfunding - Caseta multifinanțare este ascunsă - Cutia multifinanțare este afișată - - + Ascunde cardurile ecranului final Cardurile de pe ecranul de închidere sunt ascunse Cardurile de închidere ecran sunt afișate - - Bară de filtrare - Ascunde sau afișează bara de filtrare în flux, căutare și videoclipuri asociate - Ascunde în feed - Ascuns în feed - Afișat în feed - Ascunde în căutare - Ascuns în căutare - Afișat în căutare - Ascunde în videoclipurile asociate - Ascuns în videoclipuri conexe - Afișat în videoclipuri conexe - - - Ascunde butonul de microfon plutitor - Butonul microfon ascuns - Butonul Microfon afișat - - + Dezactivează modul ambiental pe tot ecranul Mod ambiental dezactivat Mod Ambient activat - + Ascunde cardurile cu informații Cardurile de informații sunt ascunse Cardurile de informații sunt afișate - + Dezactivează animațiile cu numere de rulare Numerele de rulare nu sunt animate Numerele de rulare sunt animate - + Ascunde bara de căutare în playerul video Bara de căutare a playerului video este ascunsă Se afișează bara de căutare a playerului video @@ -597,7 +593,8 @@ This is because Crowdin requires temporarily flattening this file and removing t Pictograma bara de căutare este ascunsă Pictograma bara de căutare este afișată - + + Player pentru Shorts Ascunde Short din feed-ul de acasă Shorts în fluxul de acasă sunt ascunse @@ -695,27 +692,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Bara de navigare este ascunsă Bara de navigare este afișată - + Dezactivează ecranul de sfârșit propus pentru video Videoclipurile sugerate vor fi dezactivate Videoclipurile sugerate vor fi afișate - + Ascunde marcajul temporal video Marcajul de timp este ascuns Ora este afișată - + Ascunde panourile pop-up jucător Panourile pop-up ale jucătorilor sunt ascunse Panourile pop-up ale jucătorului sunt afișate - + Opacitate suprapusă jucătorului Valoarea Opacității între 0-100, unde 0 este transparent Opacitatea suportată de jucător trebuie să fie între 0-100 - + Dislike-uri temporar indisponibile (API a expirat) Dislike-uri indisponibile (status %d) @@ -759,17 +756,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Limita ratei clientului a fost întâlnită de %d ori %d milisecunde - + Activează bara de căutare largă Bara de căutare largă este activată Bara de căutare largă este dezactivată - + + Permite miniaturi de înaltă calitate + Miniaturile din bara de afișare sunt de înaltă calitate + Miniaturile din bara de afișare sunt de calitate medie + Miniaturile din bara de căutare a ecranului complet sunt de înaltă calitate + Miniaturile din bara ecran ecran deschis sunt de calitate medie + Acest lucru va restabili miniaturile pentru animale care nu au miniaturi în bara de căutări.\n\nMiniaturile din Bara de căutare vor folosi aceeași calitate ca și videoclipul curent.\n\nAceastă funcție funcționează cel mai bine cu o calitate video mai mică sau egală cu 720p și atunci când se utilizează o conexiune la internet foarte rapidă. Restaurează miniaturile vechi din bara de căutare Miniaturile din bara de căutare vor apărea deasupra barei de căutare Miniaturile din bara de afișare vor apărea pe tot ecranul - + Activează SponsorBlock SponsorBlock este un sistem de crowd-sourted pentru săritul părților enervante ale videoclipurilor de pe YouTube Aspect @@ -950,7 +953,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Despre Datele sunt furnizate de API-ul SponsorBlock. Apasă aici pentru a afla mai multe și a vedea descărcările pentru alte platforme - + Versiune Spoof app Versiune falsificată Versiune neafectată @@ -963,9 +966,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Restaurare viteză video mare & meniu calitate 18.09.39 - Restaurare tab librărie 17.41.37 - Restaurați vechiul raft al listei de redare - 17.33.42 - Restaurați aspectul vechi al interfeței - + Setaţi pagina de start Implicit Navigare canale @@ -983,18 +985,20 @@ This is because Crowdin requires temporarily flattening this file and removing t Populare Urmărește mai târziu - - Dezactivează reluarea Jucător de scurtături - Scurtătura jucătorului nu va fi reluată la pornirea aplicației - Scurtătura va fi reluată la pornirea aplicației + + Nu relua videoclipurile Shorts la deschidere + Videoclipurile Shorts nu vor fi reluate la deschiderea aplicației + Videoclipurile Shorts vor fi reluate la deschiderea aplicației - + + + Activează aspectul tabletei Aspectul tabletei este activat Aspectul tabletei este dezactivat Posturile comunitare nu apar pe tablete - + Minijucător Schimbă stilul aplicaţiei minimizat jucătorul Tip minijucător @@ -1013,6 +1017,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Activează drag and drop Drag and drop este activat\n\nMiniplayer poate fi mutat în orice colț al ecranului Drag and drop este dezactivat + Activează Gestul de tragere orizontal + Gest de tragere orizontal activat\n\nMiniplayer poate fi mutat în stânga sau dreapta + Gest de tragere orizontal dezactivat Ascunde butonul de închidere Butonul de închidere este ascuns Butonul de închidere este afișat @@ -1032,12 +1039,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Valoarea Opacității între 0-100, unde 0 este transparent Opacitatea miniplayer suprapusă trebuie să fie între 0-100 - + Activează ecranul de încărcare gradient Încărcarea ecranului va avea un fundal pentru gradient Ecranul de încărcare va avea un fundal solid - + Activează culoarea barei de căutare personalizate Culoarea personalizată a barei de căutare este afișată Culoarea bara de căutare originală este afișată @@ -1045,12 +1052,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Culoarea barei de căutare Valoare culoare bară căutare nevalidă - + Ignoră restricțiile regiunii imaginii Utilizarea imaginii host yt4.ggpht.com Folosind gazda originală a imaginii\n\nActivarea acesteia poate rezolva imaginile lipsă care sunt blocate în unele regiuni - + Fila principală @@ -1082,7 +1089,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Derow temporar indisponibil (cod de stare: %s) Desăgeată indisponibilă temporar - + Arată anunțuri revizuite Anunțurile sunt afișate la pornire Anunțurile nu sunt afișate la pornire @@ -1090,47 +1097,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Conectarea la furnizorul de anunțuri a eșuat Anulare - + Atenție Istoricul de vizionare nu este salvat.<br><br>Cel mai probabil este cauzată de un DNS de blocare a anunțurilor sau de un proxy de rețea.<br><br>Pentru a remedia acest lucru, lista albă <b>s.youtube.com</b> sau pentru a opri toate blocantele DNS și proxy-urile. Nu mai afișa - + Activare auto-repetare Auto-repetarea este activată Auto-Repetarea este dezactivată - + Spoul dimensiunilor dispozitivului Dimensiunile dispozitivului falsificate\n\nCaracteristici video mai mari ar putea fi deblocate, dar este posibil să vă confruntați cu redare video blocată, o viață mai proastă a bateriei și efecte secundare necunoscute Dimensiunile dispozitivului nu sunt falsificate\n\nActivarea acesteia poate debloca calități video mai înalte Activarea acestei opțiuni poate provoca redare video care rulează, o durată de viață mai slabă a bateriei și efecte secundare necunoscute. - + Setări GmsCore Setări pentru GmsCore - + Ignoră redirecționările adreselor URL Redirecționările URL sunt ocolite Redirecționările URL nu sunt ocolite - + Deschide link-uri în browser Se deschid link-uri extern Se deschid link-uri în aplicație - + Elimină parametrul de urmărire interogare Parametrul de urmărire a interogării este eliminat din link-uri Parametrul de urmărire a interogării nu este eliminat din link-uri - + Dezactivare haptics zoom Hapticele sunt dezactivate Haptic-urile sunt activate - + Calitate automată Memorează modificările calității video Modificările de calitate se aplică tuturor videoclipurilor @@ -1141,35 +1148,38 @@ This is because Crowdin requires temporarily flattening this file and removing t WiFi Calitate %1$s modificată implicit: %2$s - + Arată butonul de dialog de viteză Butonul este afișat Butonul nu este afișat - + + Meniu de redare personalizat + Meniul de viteză personalizat este afișat + Meniul de viteză personalizat nu este afișat Viteze de redare personalizate - Adaugă sau modifică viteza de redare disponibilă + Adaugă sau modifică vitezele de redare personalizate Vitezele personalizate trebuie să fie mai mici decât %s. Utilizarea valorilor implicite. Viteze de redare personalizate invalide. Utilizarea valorilor implicite. - + Memorează schimbările vitezei de redare Schimbarea vitezei de redare se aplică tuturor videoclipurilor Modificările vitezei de redare se aplică numai videoclipului curent Viteza de redare implicită Viteza implicită a fost modificată la: %s - + Restaurează meniul de calitate video vechi Vechea meniu de calitate a videoclipului este afișat Meniul vechi de calitate a videoclipului nu este afișat - + Activează diapozitivul pentru a căuta Slide pentru căutare este activat Slide pentru a căuta nu este activat - + Spoof video stream-uri Sporirea canalelor video client pentru a preveni problemele de redare Spoof video stream-uri @@ -1187,20 +1197,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Reacţii adverse de spoofing Android VR • Meniul piesei audio lipsește\n• Volum stabil nu este disponibil - - - Activează luminozitatea automată HDR - Luminozitatea HDR automată este activată - Luminozitatea HDR automată este dezactivată - - + Blochează reclamele audio Anunţurile audio sunt blocate Anunţurile audio sunt deblocate - + %s este indisponibil. Reclame pot arata. Incercati sa treceti la un alt serviciu de blocare a reclamelor din setari. Serverul %s a returnat o eroare. Anunţurile pot apărea. Încearcă să treci la un alt serviciu de blocare a reclamelor din setări. Blocare reclame video integrate @@ -1208,30 +1212,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Proxy luminos PurpleAdBlock proxy - + Blocare reclame video Anunțurile video sunt blocate Anunţurile video sunt deblocate - + mesaj șters Arată mesajele șterse Nu afișa mesajele șterse Ascunde mesajele șterse în spatele unui spoiler Arată mesajele șterse ca text interceptat - + Solicită automat punctele canalului Punctele canalului sunt revendicate automat Punctele canalului nu sunt revendicate automat - + Activează modul de depanare Twitch Modul de depanare Twitch este activat (nu este recomandat) Modul de depanare Twitch este dezactivat - + Setări ReVanced Anunţuri Setări blocare reclame diff --git a/src/main/resources/addresources/values-ru-rRU/strings.xml b/patches/src/main/resources/addresources/values-ru-rRU/strings.xml similarity index 90% rename from src/main/resources/addresources/values-ru-rRU/strings.xml rename to patches/src/main/resources/addresources/values-ru-rRU/strings.xml index 55cf38401..30e4eafb6 100644 --- a/src/main/resources/addresources/values-ru-rRU/strings.xml +++ b/patches/src/main/resources/addresources/values-ru-rRU/strings.xml @@ -32,18 +32,18 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Проверка не удалась Открыть официальный сайт Пропустить - <h5>Похоже, что это приложение пропатчено не вами.</h5><br>Это приложение может работать неправильно, <b>быть вредным или даже опасным для использования</b>.<br><br>Эти проверки предполагают, что это приложение предварительно пропатчено или получено от кого-то другого:<br><br><small>%1$s</small><br>Настоятельно рекомендуется <b>удалить это приложение и пропатчить его самостоятельно,</b> чтобы быть уверенным, что вы используете проверенное и безопасное приложение.<p><br>Если проигнорировать это предупреждение, то оно будет показано только дважды. + <h5>Похоже, что это приложение пропатчено не вами.</h5><br>Оно может работать неправильно, <b>быть вредным или даже опасным.</b>.<br><br>Эти проверки предполагают, что это приложение пропатчено или получено от кого-то другого:<br><br><small>%1$s</small><br>Настоятельно рекомендуется <b>удалить это приложение и пропатчить его самостоятельно,</b> чтобы быть уверенным, что вы используете проверенное и безопасное приложение.<p><br>Если нажать \"Пропустить\", то это предупреждение будет показано только дважды. Пропатчено на другом устройстве - Не установлено с помощью ReVanced Manager + Установлено не через ReVanced Manager Пропатчено более 10 минут назад Пропатчено %s дней назад Дата сборки APK повреждена - + ReVanced Вы хотите продолжить? Сбросить @@ -63,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Официальные ссылки Пожертвовать - + MicroG GmsCore не установлен. Установите его. Требуется действие @@ -74,7 +74,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Информация Реклама Альтернативные миниатюры @@ -86,7 +86,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Прочие Видео - + + Фоновое воспроизведение Shorts + Фоновое воспроизведение Shorts отключено + Фоновое воспроизведение Shorts включено + + Отладка Включить или отключить параметры отладки Журналы отладки @@ -103,13 +108,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Всплывающее уведомление при ошибке Revanced скрыто Отключение всплывающих уведомлений об ошибках скрывает все сообщения об ошибках ReVanced.\n\nВы не будете получать уведомления о каких-либо непредвиденных событиях. - + Подсветка кнопок Кнопки \"Лайк\" и \"Подписаться\" не будут подсвечиваться при упоминании Кнопки \"Лайк\" и \"Подписаться\" будут подсвечиваться при упоминании - Серые разделители - Серые разделители в ленте между видео и публикациями сообщества скрыты - Серые разделители в ленте между видео и публикациями сообщества отображены + Карточки альбомов + Карточки альбомов под описанием артистов скрыты + Карточки альбомов под описанием артистов отображены + Колонка \"Коллективный сбор\" + Колонка \"Коллективный сбор\" между плеером и описанием видео скрыта + Колонка \"Коллективный сбор\" между плеером и описанием видео отображена + Плавающая кнопка микрофона + Плавающая кнопка микрофона в поиске скрыта + Плавающая кнопка микрофона в поиске отображена Водяной знак канала Водяной знак канала в плеере скрыт Водяной знак канала в плеере отображен @@ -154,9 +165,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Расширяемые фрагменты Расширяемые фрагменты под видео скрыты Расширяемые фрагменты под видео отображены - Меню качества видео - Меню качества видео в выдвижном меню плеера скрыто - Меню качества видео в выдвижном меню плеера отображено Публикации сообщества Публикации сообщества в ленте скрыты Публикации сообщества в ленте отображены @@ -231,6 +239,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Раздел расшифровки в описании видео отображен Описание видео Скрыть или отобразить компоненты описания видео + Панель фильтров + Скрыть или отобразить панель фильтров в ленте, поиске и похожих видео + Панель фильтров в ленте + Панель фильтров в ленте скрыта + Панель фильтров в ленте отображена + Панель фильтров в поиске + Панель фильтров в поиске скрыта + Панель фильтров в поиске отображена + Панель фильтров в похожих видео + Панель фильтров в похожих видео скрыта + Панель фильтров в похожих видео отображена + Комментарии + Скрыть или отобразить компоненты раздела комментариев + Заголовок \"Комментарии спонсоров\" + Заголовок \"Комментарии спонсоров\" скрыт + Заголовок \"Комментарии спонсоров\" отображен + Раздел комментариев + Раздел комментариев под плеером скрыт + Раздел комментариев под плеером отображен + Кнопка \"Создать Short\" + Кнопка \"Создать Short\" скрыта + Кнопка \"Создать Short\" отображена + Предпросмотр комментария + Предпросмотр комментария под плеером скрыт + Предпросмотр комментария под плеером отображен + Кнопка \"Спасибо\" + Кнопка \"Спасибо\" скрыта + Кнопка \"Спасибо\" отображена + Метка времени и кнопки эмодзи + Метка времени и кнопки эмодзи в разделе комментариев скрыты + Метка времени и кнопки эмодзи в разделе комментариев отображены YouTube Doodles Doodles на панели поиска скрыты @@ -272,12 +311,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Ключевое слово слишком короткое и требует кавычки: %s Ключевое слово скроет все видео: %s - + Реклама общего формата Реклама общего формата скрыта Реклама общего формата отображена Полноэкранная реклама - Полноэкранная реклама при запуске приложения скрыта\n\nДанная функция доступна только для старых устройств + Полноэкранная реклама при запуске приложения скрыта\n\nДанная опция доступна только для старых устройств Полноэкранная реклама при запуске приложения отображена Видеообъявления Видеообъявления скрыты @@ -291,6 +330,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Баннеры просмотра товаров Баннеры просмотра товаров в плеере скрыты Баннеры просмотра товаров в плеере отображены + Секция покупок в плеере + Секция покупок в плеере скрыта + Секция покупок в плеере отображена Ссылки на товары Ссылки на товары в описании видео скрыты Ссылки на товары в описании видео отображены @@ -307,17 +349,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Скрытие полноэкранной рекламы работает только для старых устройств - + Реклама YouTube Premium Реклама YouTube Premium под плеером скрыта Реклама YouTube Premium под плеером отображена - + Видеореклама Видеореклама в плеере скрыта Видеореклама в плеере отображена - + URL-адрес скопирован URL-адрес с меткой времени скопирован Кнопка копирования URL @@ -327,13 +369,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Кнопка отображена. Нажмите для копирования URL-адреса видео с меткой времени. Нажмите и удерживайте для копирования URL-адреса видео без метки времени Кнопка копирования URL-адреса видео с меткой времени скрыта - + Окно о нежелательном контенте Диалоговое окно о нежелательном контенте скрыто Диалоговое окно о нежелательном контенте отображено - Данная функция не обходит возрастное ограничение. Она просто принимает возрастное ограничение автоматически. + Данная опция не обходит возрастное ограничение. Она только принимает возрастное ограничение автоматически. - + Внешний загрузчик Настройки использования внешнего загрузчика видео Кнопка внешнего загрузчика @@ -347,17 +389,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Имя пакета установленного Вами приложения внешнего загрузчика, такого как NewPipe или Seal %s не установлен. Пожалуйста, установите его. - + Жест покадровой перемотки Жест покадровой перемотки отключен Жест покадровой перемотки включен - + Перемотка нажатием Перемотка нажатием на прогресс воспроизведения включена Перемотка нажатием на прогресс воспроизведения отключена - + Регулировка яркости жестом Регулировка яркости жестом включена Регулировка яркости жестом отключена @@ -386,12 +428,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Порог величины жеста Минимальная амплитуда движения, распознаваемого как жест - + Автоматические субтитры Автоматические субтитры отключены Автоматические субтитры включены - + Кнопки действий Скрыть или отобразить кнопки действий под видео Кнопки \"Лайк\" и \"Дизлайк\" @@ -427,23 +469,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Кнопка \"Сохранить в плейлист\" под плеером скрыта Кнопка \"Сохранить в плейлист\" под плеером отображена - - Кнопка \"Автовоспроизведение\" - Кнопка \"Автовоспроизведение\" в плеере скрыта - Кнопка \"Автовоспроизведение\" в плеере отображена - - - - Кнопка \"Субтитры\" - Кнопка \"Субтитры\" в плеере скрыта - Кнопка \"Субтитры\" в плеере отображена - - - Кнопка \"Трансляция\" - Кнопка \"Трансляция\" в плеере скрыта - Кнопка \"Трансляция\" в плеере отображена - - + Кнопки навигации Скрыть или изменить кнопки в панели навигации @@ -464,13 +490,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Кнопка \"Подписки\" в панели навигации отображена Инверсия \"Создать\" и \"Уведомления\" - Кнопки \"Создать\" и \"Уведомления\" заменены местами\n\nПримечание: включение этой опции также принудительно скрывает видеорекламу + Кнопки \"Создать\" и \"Уведомления\" заменены местами\n\nПримечание: активация данной опции также принудительно скрывает видеорекламу Кнопки \"Создать\" и \"Уведомления\" не заменены местами Подписи кнопок навигации Подписи кнопок навигации скрыты Подписи кнопок навигации отображены - + Выдвижное меню плеера Скрыть или отобразить пункты выдвижного меню плеера @@ -481,6 +507,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Пункт \"Дополнительные настройки\" Пункт \"Дополнительные настройки\" в выдвижном меню плеера скрыт Пункт \"Дополнительные настройки\" в выдвижном меню плеера отображен + + Пункт \"Таймер сна\" + Пункт \"Таймер сна\" в выдвижном меню плеера скрыт + Пункт \"Таймер сна\" в выдвижном меню плеера отображен Пункт \"Повтор воспроизведения\" Пункт \"Повтор воспроизведения\" в выдвижном меню плеера скрыт @@ -489,6 +519,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Пункт \"Фоновая подсветка\" Пункт \"Фоновая подсветка\" в выдвижном меню плеера скрыт Пункт \"Фоновая подсветка\" в выдвижном меню плеера отображен + Пункт \"Постоянный уровень громкости\" + Пункт \"Постоянный уровень громкости\" в выдвижном меню плеера отображен + Пункт \"Постоянный уровень громкости\" в выдвижном меню плеера скрыт Пункт \"Справка и отзывы\" Пункт \"Справка и отзывы\" в выдвижном меню плеера скрыт @@ -514,83 +547,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Пункт \"Смотреть в VR-режиме\" Пункт \"Смотреть в VR-режиме\" в выдвижном меню плеера скрыт Пункт \"Смотреть в VR-режиме\" в выдвижном меню плеера отображен + Колонтитул меню качества видео + Колонтитул меню качества видео скрыт + Колонтитул меню качества видео отображен - - Кнопки переключения видео - Кнопки предыдущего и следующего видео скрыты - Кнопки предыдущего и следующего видео отображены + + Кнопки переключения видео + Кнопки предыдущего и следующего видео скрыты + Кнопки предыдущего и следующего видео отображены + Кнопка \"Трансляция\" + Кнопка \"Трансляция\" в плеере скрыта + Кнопка \"Трансляция\" в плеере отображена + + Кнопка \"Субтитры\" + Кнопка \"Субтитры\" в плеере скрыта + Кнопка \"Субтитры\" в плеере отображена + Кнопка \"Автовоспроизведение\" + Кнопка \"Автовоспроизведение\" в плеере скрыта + Кнопка \"Автовоспроизведение\" в плеере отображена - - Карточки альбомов - Карточки альбомов под описанием артистов скрыты - Карточки альбомов под описанием артистов отображены - - - Комментарии - Скрыть или отобразить компоненты раздела комментариев - Заголовок \"Комментарии спонсоров\" - Заголовок \"Комментарии спонсоров\" скрыт - Заголовок \"Комментарии спонсоров\" отображен - Раздел комментариев - Раздел комментариев под плеером скрыт - Раздел комментариев под плеером отображен - Кнопка \"Создать Short\" - Кнопка \"Создать Short\" скрыта - Кнопка \"Создать Short\" отображена - Предпросмотр комментария - Предпросмотр комментария под плеером скрыт - Предпросмотр комментария под плеером отображен - Кнопка \"Спасибо\" - Кнопка \"Спасибо\" скрыта - Кнопка \"Спасибо\" отображена - Метка времени и кнопки эмодзи - Метка времени и кнопки эмодзи в разделе комментариев скрыты - Метка времени и кнопки эмодзи в разделе комментариев отображены - - - Колонка \"Коллективный сбор\" - Колонка \"Коллективный сбор\" между плеером и описанием видео скрыта - Колонка \"Коллективный сбор\" между плеером и описанием видео отображена - - + Заставки следущих видео Заставки следующих видео в конце просмотра скрыты Заставки следующих видео в конце просмотра отображены - - Панель фильтров - Скрыть или отобразить панель фильтров в ленте, поиске и похожих видео - Панель фильтров в ленте - Панель фильтров в ленте скрыта - Панель фильтров в ленте отображена - Панель фильтров в поиске - Панель фильтров в поиске скрыта - Панель фильтров в поиске отображена - Панель фильтров в похожих видео - Панель фильтров в похожих видео скрыта - Панель фильтров в похожих видео отображена - - - Плавающая кнопка микрофона - Плавающая кнопка микрофона в поиске скрыта - Плавающая кнопка микрофона в поиске отображена - - + Фоновая подсветка Фоновая подсветка в полноэкранном режиме отключена Фоновая подсветка в полноэкранном режиме включена - + Подсказки Подсказки в видео скрыты Подсказки в видео отображены - + Анимированные счетчики Анимированные счетчики просмотров, лайков и дизлайков отключены Анимированные счетчики просмотров, лайков и дизлайков включены - + Прогресс воспроизведения Прогресс воспроизведения в плеере скрыт Прогресс воспроизведения в плеере отображен @@ -598,7 +594,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Миниатюры прогресса воспроизведения скрыты Миниатюры прогресса воспроизведения отображены - + + Плеер Shorts + Скрыть или отобразить компоненты в плеере Shorts Shorts в ленте \"Главной\" Shorts в ленте \"Главной\" скрыты @@ -645,9 +643,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Кнопка \"Зеленый экран\" Кнопка \"Зеленый экран\" скрыта Кнопка \"Зеленый экран\" отображена - Скрыть хэштег + Кнопка хэштега Кнопка хэштега скрыта - Отображена кнопка хештега + Кнопка хэштега отображена Поисковые подсказки Поисковые подсказки скрыты Поисковые подсказки отображены @@ -696,27 +694,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Панель навигации в Shorts скрыта Панель навигации в Shorts отображена - + Предлагаемые видео Предлагаемые видео в конце просмотра скрыты Предлагаемые видео в конце просмотра отображены - + Метка времени видео Метка времени видео в плеере скрыта Метка времени видео в плеере отображена - + Всплывающие панели плеера Автоматически всплывающие панели (плейлист или живой чат) в плеере скрыты Автоматически всплывающие панели (плейлист или живой чат) в плеере отображены - + Непрозрачность оверлея плеера Значение непрозрачности в пределах 0-100, где 0 - это прозрачно Непрозрачность оверлея плеера должна быть от 0 до 100 - + Дизлайки временно недоступны (таймаут API) Дизлайки недоступны (статус %d) @@ -760,17 +758,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Ограничения скорости клиента API обнаружены %d раз %d миллисекунд - + Широкая панель поиска Широкая панель поиска вместо кнопки поиска включена (логотип YouTube будет скрыт) Широкая панель поиска отключена - + + Миниатюры высокого качества + Миниатюры прогресса воспроизведения высокого качества + Миниатюры прогресса воспроизведения среднего качества + Полноэкранные миниатюры прогресса воспроизведения высокого качества + Полноэкранные миниатюры прогресса воспроизведения среднего качества + Данная опция также восстановит миниатюры в прямых трансляциях, у которых они отсутствуют в прогрессе воспроизведения.\n\nМиниатюры прогресса воспроизведения будут использовать такое же качество, как и текущее видео.\n\nДанная опция работает лучше с качеством видео 720p или ниже, и при использовании очень быстрого подключения к Интернету. Восстановление старых миниатюр Старые миниатюры восстановлены - миниатюры прогресса воспроизведения отображаются над ним Миниатюры прогресса воспроизведения отображаются в полноэкранном режиме - + Включить SponsorBlock SponsorBlock – это краудсорсинговая система для пропуска раздражающих фрагментов видео YouTube Внешний вид @@ -951,7 +955,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Информация Данные предоставлены SponsorBlock API. Нажмите для получения дополнительной информации и просмотра загрузок для других платформ - + Подмена версии приложения Версия приложения подменена Версия приложения не подменена @@ -964,18 +968,17 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Восстановление расширенного меню скорости и качества видео 18.09.39 - Восстановление вкладки \"Библиотека\" 17.41.37 - Восстановление старой секции плейлистов - 17.33.42 - Восстановление старого стиля пользовательского интерфейса - + Начальная страница По умолчанию Просмотр каналов Навигатор - Играть - История + Игры + История просмотров Библиотека Понравившиеся видео - Онлайн + В эфире Фильмы Музыка Поиск @@ -984,18 +987,26 @@ This is because Crowdin requires temporarily flattening this file and removing t Популярные Смотреть позже - + Возобновление плеера Shorts Возобновление плеера Shorts при запуске приложения отключено Возобновление плеера Shorts при запуске приложения включено - + + Автопроигрывание Shorts + Shorts будут автоматически воспроизводиться одно за другим + Shorts будут повторяться + Автопроигрывание Shorts в фоне + Shorts будут автоматически воспроизводиться одно за другим в фоновом режиме + Shorts будут повторяться в фоновом режиме + + Планшетный интерфейс Планшетный интерфейс включен Планшетный интерфейс отключен Публикации сообщества не отображаются в планшетном интерфейсе - + Мини-плеер Стиль свернутого мини-плеера Тип мини-плеера @@ -1005,21 +1016,24 @@ This is because Crowdin requires temporarily flattening this file and removing t Современный 1 Современный 2 Современный 3 - Включить закругленные углы + Закругленные углы Углы закруглены Углы квадратны - Включить двойное нажатие и закрепление для изменения размера - Действие двойного нажатия и устройство для изменения размера включено\n\n• Двойное нажатие для увеличения размера мини-плеера\n• Двойное нажатие еще раз для восстановления исходного размера - Действие двойного нажатия и закрепление для изменения размера отключены - Включить перетаскивание + Двойное нажатие и щипок для изменения размера + Двойное нажатие и щипок для изменения размера включено\n\n• Двойное нажатие для увеличения размера мини-плеера\n• Двойное нажатие еще раз для восстановления исходного размера + Двойное нажатие и щипок для изменения размера отключены + Перетаскивание Перетаскивание включено\n\nМини-плеер можно перетащить в любой угол экрана Перетаскивание отключено - Скрыть кнопку закрытия + Жест горизонтального перетаскивания + Жест горизонтального перетаскивания включен\n\nМини-плеер можно перетаскивать за пределы экрана влево или вправо + Жест горизонтального перетаскивания отключен + Кнопка закрытия Кнопка закрытия скрыта - Кнопка закрытия отображается + Кнопка закрытия отображена Кнопки \"Развернуть\" и \"Закрыть\" - Кнопки скрыты\n\nдля разворачивания или закрытия - Показывать кнопки разворачивания и закрытия + Кнопки скрыты\n\nПроведите по мини-плееру для разворачивания или закрытия + Кнопки разворачивания и закрытия отображены Скрыть подтексты Субтексты скрыты Подтексты показаны @@ -1027,18 +1041,18 @@ This is because Crowdin requires temporarily flattening this file and removing t Кнопки перемотки вперед и назад скрыты Кнопки перемотки вперед и назад отображены Начальный размер - Начальный размер экрана в пикселях - Размер пикселя должен быть между %1$s и %2$s + Начальный размер на экране, в пикселях + Размер в пикселях должен быть между %1$s и %2$s Непрозрачность оверлея мини-плеера Значение непрозрачности в пределах 0-100, где 0 - это прозрачно Непрозрачность оверлея мини-плеера должна быть от 0 до 100 - + Фон экрана загрузки Экран загрузки имеет градиентный фон Экран загрузки имеет сплошной фон - + Цвет прогресса воспроизведения Пользовательский цвет прогресса воспроизведения отображен Оригинальный цвет прогресса воспроизведения отображен @@ -1046,12 +1060,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Редактировать цвет прогресса воспроизведения Неверное значение цвета прогресса воспроизведения - + Обход ограничений региона Использование хоста изображений yt4.ggpht.com - Использование оригинального хоста изображений\n\nВключение этой опции может исправить недостающие изображения, которые заблокированы в некоторых регионах + Использование оригинального хоста изображений\n\nАктивация данной опции может исправить недостающие изображения, которые заблокированы в некоторых регионах - + Вкладка \"Главная\" @@ -1083,7 +1097,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow временно недоступен (код статуса: %s) DeArrow временно недоступен - + Объявления ReVanced Объявления ReVanced при запуске приложения отображены Объявления ReVanced при запуске приложения скрыты @@ -1091,47 +1105,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Не удалось подключиться к поставщику объявлений Закрыть - + Внимание Ваша история просмотра не сохраняется.<br><br>Скорее всего, это из-за блокировщика рекламы DNS или сетевого прокси.<br><br>Чтобы это исправить, добавьте <b>s.youtube.com</b> в белый список блокировщика или отключите все блокировщики DNS и прокси. Не показывать снова - + Автоповтор текущего видео Автоповтор текущего видео включен Автоповтор текущего видео отключен - + Подмена размеров устройства Размеры устройства подменены\n\nБолее высокие качества видео могут быть разблокированы, однако при этом возможны заикания видео при воспроизведении, высокое потребление батареи и неизвестные побочные эффекты Размеры устройства не подменены\n\nАктивация данной опции может разблокировать более высокие качества видео Активация данной опции может привести к заиканиям видео при воспроизведении, высокому потреблению батареи и неизвестным побочным эффектам. - + GmsCore Настройки GmsCore - + Обход перенаправления URL-адресов Обход перенаправления URL-адресов (youtube.com/redirect) при открытии ссылок в описаниях видео включен Обход перенаправления URL-адресов (youtube.com/redirect) при открытии ссылок в описаниях видео отключен - + Открытие ссылок в браузере Ссылки открываются в браузере Ссылки открываются в самом приложении - + Параметр отслеживания запросов Параметр отслеживания запросов из ссылок удален Параметр отслеживания запросов из ссылок не удален - + Виброотклик при масштабировании Виброотклик при масштабировании отключен Виброотклик при масштабировании включен - + Автоматическое качество Запоминание изменений качества Изменения качества воспроизведения применяются ко всем видео @@ -1142,35 +1156,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi сети Качество в %1$s изменено на: %2$s - + Кнопка выбора скорости Кнопка выбора скорости отображена Кнопка выбора скорости скрыта - + + Пользовательское меню скорости воспроизведения + Пользовательское меню скорости воспроизведения отображено + Пользовательское меню скорости воспроизведения скрыто Скорости воспроизведения - Добавить или изменить доступные скорости воспроизведения + Добавить или изменить пользовательские скорости воспроизведения Пользовательские скорости должны быть меньше, чем %s. Использование значений по умолчанию. Недопустимая пользовательская скорость воспроизведения. Использование значений по умолчанию. - + Запоминание изменений скорости Изменения скорости воспроизведения применяются ко всем видео Изменения скорости воспроизведения применяются только к текущему видео Скорость воспроизведения по умолчанию Скорость изменена на: %s - + Старое меню качества видео Старое меню качества видео отображено Старое меню качества видео скрыто - + Перемотка видео слайдом Перемотка видео слайдом включена Перемотка видео слайдом отключена. Ускорение видео \"2x\" при нажатии и удержании на экране включено - + Подмена видеопотоков Подмена видеопотоков клиента для предотвращения проблем с воспроизведением видео Подмена видеопотоков @@ -1181,27 +1198,21 @@ This is because Crowdin requires temporarily flattening this file and removing t Принудительно AVC (H.264) Видеокодек AVC (H.264) Видеокодек VP9 или AV1 - На вашем устройстве нет аппаратного декодирования VP9, и эта настройка всегда включена при активной подмене клиента - Включение данной настройки может улучшить время работы батареи и исправить задержки воспроизведения.\n\nAVC имеет максимальное разрешение 1080p, воспроизведение видео будет использовать больше интернет данных в сравнении с VP9 или AV1. + На вашем устройстве нет аппаратного декодирования VP9, и эта настройка всегда активна при включенной подмене клиента + Активация данной опции может улучшить время работы батареи и исправить задержки воспроизведения.\n\nAVC имеет максимальное разрешение 1080p, воспроизведение видео будет использовать больше интернет данных в сравнении с VP9 или AV1. Побочные эффекты подмены на iOS • Фильмы или платные видео могут не воспроизводиться\n• Прямые трансляции начинаются с самого начала\n• Видео может закончиться на 1 секунду раньше\n• Отсутствует аудиокодек opus Побочные эффекты подмены на Android VR • Пункт меню \"Звуковая дорожка\" отсутствует\n• Пункт меню \"Постоянный уровень громкости\" недоступен - - - Включить авто-яркость HDR - Авто яркость HDR включена - Автоматическая яркость HDR отключена - - + Аудиореклама Аудиореклама заблокирована Аудиореклама разблокирована - + %s недоступен. Реклама может отображаться. Попробуйте переключиться на другую службу блокировки рекламы в настройках. %s сервер вернул ошибку. Реклама может отображаться. Попробуйте переключиться на другую службу блокировки рекламы в настройках. Встроенная реклама в видео @@ -1209,30 +1220,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Luminous прокси PurpleAdBlock прокси - + Реклама в видео Реклама в видео заблокирована Реклама в видео разблокирована - + сообщение удалено Удаленные сообщения Не отображать удаленные сообщения Скрыть удаленные сообщения за спойлером Отображать удаленные сообщения как перекрестный текст - + Автополучение Баллов канала Баллы канала получаются автоматически Баллы канала не получаются автоматически - + Режим отладки Twitch Режим отладки Twitch включен (не рекомендуется) Режим отладки Twitch отключен - + Настройки ReVanced Реклама Настройки блокировки рекламы diff --git a/patches/src/main/resources/addresources/values-si-rLK/strings.xml b/patches/src/main/resources/addresources/values-si-rLK/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-si-rLK/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/addresources/values-sk-rSK/strings.xml b/patches/src/main/resources/addresources/values-sk-rSK/strings.xml similarity index 94% rename from src/main/resources/addresources/values-sk-rSK/strings.xml rename to patches/src/main/resources/addresources/values-sk-rSK/strings.xml index 9f11677c9..b15d02beb 100644 --- a/src/main/resources/addresources/values-sk-rSK/strings.xml +++ b/patches/src/main/resources/addresources/values-sk-rSK/strings.xml @@ -32,9 +32,9 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + Chcete pokračovať? Resetovať Obnovte a reštartujte @@ -52,7 +52,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Táto verzia je predbežná a môžu sa vyskytnúť neočakávané problémy Oficiálne odkazy - + MicroG GmsCore nie je nainštalovaný. Nainštalujte ho. Potrebná akcia @@ -63,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Informácie Reklamy Alternatívne miniatúry @@ -72,7 +72,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Panel vyhľadávania Ovládanie potiahnutím - + + + Ladenie Povoliť alebo zakázať možnosti ladenia Debug logovanie @@ -89,13 +91,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Toast sa nezobrazuje, ak sa vyskytne chyba Vypnutím chybových toastov skryjete všetky chybové upozornenia ReVanced.\n\nNebudete upozornení na žiadne neočakávané udalosti. - + Zakázať podsvietenie tlačidla Páči sa mi/Prihlásiť sa na odber Tlačidlo Páči sa mi a Prihlásiť sa pri zmienke nebude svietiť Pri zmienke sa rozsvieti tlačidlo Páči sa mi a Prihlásiť sa na odber - Skryť sivý oddeľovač - Sivé oddeľovače sú skryté - Sú zobrazené sivé oddeľovače + Skryť karty albumov + Karty albumov sú skryté + Zobrazia sa karty albumov + Skryť crowdfunding box + Crowdfundingový box je skrytý + Je zobrazené pole crowdfundingu + Skryť plávajúce tlačidlo mikrofónu + Tlačidlo mikrofónu je skryté + Zobrazené tlačidlo mikrofónu Skryť vodoznak kanála Vodoznak je skrytý Zobrazí sa vodoznak @@ -140,9 +148,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Skryť rozšíriteľný čip pod videami Rozšíriteľné čipy sú skryté Zobrazujú sa rozšíriteľné čipy - Skryť pätu ponuky kvality videa - Päta ponuky kvality videa je skrytá - Zobrazí sa päta ponuky kvality videa Skryť príspevky komunity Príspevky komunity sú skryté Zobrazujú sa príspevky komunity @@ -214,6 +219,34 @@ This is because Crowdin requires temporarily flattening this file and removing t Zobrazí sa sekcia prepisu Popis videa Skryť alebo zobraziť komponenty popisu videa + Panel filtra + Skryť alebo zobraziť panel filtra v informačnom kanáli, vo vyhľadávaní a v súvisiacich videách + Skryť v feede + Skryté v krmive + Zobrazené v informačnom kanáli + Skryť vo vyhľadávaní + Skryté vo vyhľadávaní + Zobrazuje sa pri vyhľadávaní + Skryť v súvisiacich videách + Skryté v súvisiacich videách + Zobrazuje sa v súvisiacich videách + Komentáre + Skryť alebo zobraziť komponenty sekcie komentárov + Skryť hlavičku \"Komentáre členov\" + Hlavička \"Komentáre členov\" je skrytá + Zobrazí sa hlavička \"Komentáre členov\" + Skryť sekciu komentárov + Sekcia komentárov je skrytá + Zobrazí sa sekcia komentárov + Skryť ukážkový komentár + Komentár ukážky je skrytý + Zobrazí sa ukážka komentára + Skryť tlačidlo poďakovania + Tlačidlo poďakovania je skryté + Zobrazí sa tlačidlo Ďakujem + Skryť tlačidlá časovej pečiatky a emodži + Tlačidlá časovej pečiatky a emotikonov sú skryté + Zobrazia sa tlačidlá časovej pečiatky a emoji Vlastný filter Skryť komponenty pomocou vlastných filtrov @@ -242,7 +275,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Skryť všeobecné reklamy Všeobecné reklamy sú skryté Zobrazujú sa všeobecné reklamy @@ -277,17 +310,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Skryť reklamy na celú obrazovku funguje iba na starších zariadeniach - + Skryť promá YouTube Premium Promá YouTube Premium pod prehrávačom videa sú skryté Promá YouTube Premium sa zobrazujú pod prehrávačom videa - + Skryť videoreklamy Videoreklamy sú skryté Zobrazujú sa videoreklamy - + Adresa URL bola skopírovaná do schránky Adresa URL s časovou pečiatkou bola skopírovaná Zobraziť tlačidlo skopírovať adresu URL videa @@ -297,13 +330,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Tlačidlo je zobrazené. Klepnutím skopírujete webovú adresu videa s časovou pečiatkou. Klepnutím a podržaním skopírujete video bez časovej pečiatky Tlačidlo nie je zobrazené - + Odstrániť dialógové okno uváženia prehliadača Dialóg bude odstránený Zobrazí sa dialógové okno Neobchádza sa tým ani vekové obmedzenie. Len to automaticky akceptuje. - + Externé sťahovanie Nastavenia pre používanie externého sťahovača Zobraziť externé tlačidlo sťahovania @@ -317,17 +350,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Názov balíka nainštalovanej externej aplikácie na sťahovanie, napríklad NewPipe alebo Seal %s nie je nainštalovaný. Nainštalujte si ho. - + Zakázať gesto presného vyhľadávania Gesto je zakázané Gesto je povolené - + Povoliť klepanie na panel vyhľadávania Ťuknutie na panel vyhľadávania je povolené Ťuknutie na panel vyhľadávania je zakázané - + Povoliť gesto jasu Potiahnutie jasu je povolené Potiahnutie prstom po jase je vypnuté @@ -355,12 +388,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Prahová hodnota potiahnutia Hodnota prahu, ktorý sa má vykonať potiahnutím prstom - + Zakázať automatické titulky Automatické titulky sú vypnuté Automatické titulky sú povolené - + Akčné tlačidlá Skryť alebo zobraziť tlačidlá pod videami Skryť Páči sa mi a Nepáči sa mi @@ -396,23 +429,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Tlačidlo Uložiť do zoznamu skladieb je skryté Zobrazí sa tlačidlo Uložiť do zoznamu skladieb - - Skryť tlačidlo automatického prehrávania - Tlačidlo automatického prehrávania je skryté - Zobrazí sa tlačidlo automatického prehrávania - - - - Tlačidlo skryť titulky - Tlačidlo titulkov je skryté - Zobrazí sa tlačidlo titulkov - - - Skryť tlačidlo prenášania - Tlačidlo zdieľania obrazovky je skryté - Tlačidlo zdieľania obrazovky je zobrazené - - + Navigačné tlačidlá Skryť alebo zmeniť tlačidlá na navigačnom paneli @@ -439,7 +456,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Štítky sú skryté Zobrazia sa štítky - + Rozbaľovacie menu Skryť alebo zobraziť položky rozbaľovacej ponuky prehrávača @@ -450,6 +467,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Skryť ďalšie nastavenia Ponuka ďalších nastavení je skrytá Zobrazí sa ponuka ďalších nastavení + Skryť video slučky Ponuka slučkového videa je skrytá @@ -483,83 +501,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Skryť hodinky vo VR Sledovanie v ponuke VR je skryté Zobrazí sa ponuka Sledovať vo VR + Skryť pätu ponuky kvality videa + Päta ponuky kvality videa je skrytá + Zobrazí sa päta ponuky kvality videa - - Skryť predchádzajúce & tlačidlá ďalšieho videa - Tlačidlá sú skryté - Zobrazia sa tlačidlá + + Skryť predchádzajúce & tlačidlá ďalšieho videa + Tlačidlá sú skryté + Zobrazia sa tlačidlá + Skryť tlačidlo prenášania + Tlačidlo zdieľania obrazovky je skryté + Tlačidlo zdieľania obrazovky je zobrazené + + Tlačidlo skryť titulky + Tlačidlo titulkov je skryté + Zobrazí sa tlačidlo titulkov + Skryť tlačidlo automatického prehrávania + Tlačidlo automatického prehrávania je skryté + Zobrazí sa tlačidlo automatického prehrávania - - Skryť karty albumov - Karty albumov sú skryté - Zobrazia sa karty albumov - - - Komentáre - Skryť alebo zobraziť komponenty sekcie komentárov - Skryť hlavičku \"Komentáre členov\" - Hlavička \"Komentáre členov\" je skrytá - Zobrazí sa hlavička \"Komentáre členov\" - Skryť sekciu komentárov - Sekcia komentárov je skrytá - Zobrazí sa sekcia komentárov - Skryť tlačidlo \"Vytvoriť krátke\" - Tlačidlo \"Vytvoriť krátke\" je skryté - Zobrazí sa tlačidlo \"Vytvoriť krátke\" - Skryť ukážkový komentár - Komentár ukážky je skrytý - Zobrazí sa ukážka komentára - Skryť tlačidlo poďakovania - Tlačidlo poďakovania je skryté - Zobrazí sa tlačidlo Ďakujem - Skryť tlačidlá časovej pečiatky a emodži - Tlačidlá časovej pečiatky a emotikonov sú skryté - Zobrazia sa tlačidlá časovej pečiatky a emoji - - - Skryť crowdfunding box - Crowdfundingový box je skrytý - Je zobrazené pole crowdfundingu - - + Skryť karty záverečnej obrazovky Karty záverečnej obrazovky sú skryté Zobrazia sa karty záverečnej obrazovky - - Panel filtra - Skryť alebo zobraziť panel filtra v informačnom kanáli, vo vyhľadávaní a v súvisiacich videách - Skryť v feede - Skryté v krmive - Zobrazené v informačnom kanáli - Skryť vo vyhľadávaní - Skryté vo vyhľadávaní - Zobrazuje sa pri vyhľadávaní - Skryť v súvisiacich videách - Skryté v súvisiacich videách - Zobrazuje sa v súvisiacich videách - - - Skryť plávajúce tlačidlo mikrofónu - Tlačidlo mikrofónu je skryté - Zobrazené tlačidlo mikrofónu - - + Zakázať ambientný režim na celej obrazovke Ambientný režim je vypnutý Ambientný režim je povolený - + Skryť informačné karty Informačné karty sú skryté Informačné karty sú zobrazené - + Zakázať animácie pohyblivých čísel Pohyblivé čísla nie sú animované Pohyblivé čísla sú animované - + Skryť panel vyhľadávania v prehrávači videa Vyhľadávací panel prehrávača videa je skrytý Zobrazí sa vyhľadávací panel prehrávača videa @@ -567,7 +548,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Panel vyhľadávania miniatúr je skrytý Zobrazí sa panel vyhľadávania miniatúr - + Skryť Shorts v domácom informačnom kanáli Shorts v domácom feede sú skryté @@ -644,27 +625,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Navigačný panel je skrytý Zobrazí sa navigačná lišta - + Zakázať záverečnú obrazovku navrhovaného videa Navrhované videá budú zakázané Zobrazia sa navrhované videá - + Skryť časovú pečiatku videa Časová pečiatka je skrytá Zobrazí sa časová pečiatka - + Skryť vyskakovacie panely prehrávača Vyskakovacie panely prehrávača sú skryté Zobrazia sa vyskakovacie panely prehrávača - + Nepriehľadnosť prekrytia prehrávača Hodnota nepriehľadnosti medzi 0-100, kde 0 je transparentné Nepriehľadnosť prekrytia hráča musí byť medzi 0-100 - + Nepáči sa mi dočasne nedostupné (rozhranie API vypršalo) Nepáči sa mi nie sú k dispozícii (stav %d) @@ -708,17 +689,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Limit klientskej sadzby bol zaznamenaný %d-krát %d milisekúnd - + Povoliť široký panel vyhľadávania Široký panel vyhľadávania je povolený Široký panel vyhľadávania je vypnutý - + Obnovte staré miniatúry vyhľadávacieho panela Miniatúry panela vyhľadávania sa zobrazia nad panelom vyhľadávania Miniatúry panela vyhľadávania sa zobrazia na celej obrazovke - + Zapnúť SponsorBlock SponsorBlock je davový systém na preskakovanie nepríjemných častí videí YouTube Vzhľad @@ -896,7 +877,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Informácie Údaje poskytuje SponsorBlock API. Klepnutím sem sa dozviete viac a zobrazíte súbory na stiahnutie pre iné platformy - + Verzia aplikácie Spoof Verzia sfalšovaná Verzia nie je sfalšovaná @@ -909,9 +890,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Obnovenie rýchlosti širokouhlého videa & kvalitné menu 18.09.39 - Záložka Obnoviť knižnicu 17.41.37 - Obnovte starú poličku so zoznamom skladieb - 17.33.42 - Obnovte staré rozloženie používateľského rozhrania - + Nastaviť úvodnú stránku Predvolené Preskúmajte @@ -921,18 +901,20 @@ This is because Crowdin requires temporarily flattening this file and removing t Predplatné Trendy - + Zakázať obnovenie prehrávača Shorts Prehrávač Shorts videí sa pri spustení aplikácie neobnoví Prehrávač Shorts videí sa obnoví pri spustení aplikácie - + + + Povoliť rozloženie tabletu Rozloženie tabletu je povolené Rozloženie tabletu je zakázané Príspevky komunity sa nezobrazujú v rozložení tabletu - + Miniprehrávač Zmeňte štýl minimalizovaného prehrávača v aplikácii Typ miniprehrávača @@ -953,21 +935,21 @@ This is because Crowdin requires temporarily flattening this file and removing t Hodnota nepriehľadnosti medzi 0-100, kde 0 je transparentné Nepriehľadnosť prekrytia miniprehrávača musí byť medzi 0-100 - + Povoliť obrazovku načítania gradientu Načítavacia obrazovka bude mať pozadie s prechodom Načítavacia obrazovka bude mať pevné pozadie - + Povoliť vlastnú farbu vyhľadávacieho panela Zobrazí sa vlastná farba panela vyhľadávania Zobrazí sa pôvodná farba vyhľadávacieho panela Vlastná farba vyhľadávacieho panela Farba vyhľadávacieho panela - + - + Karta Domov @@ -999,7 +981,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow dočasne nedostupný (stavový kód: %s) DeArrow dočasne nedostupný - + Zobraziť oznámenia ReVanced Oznámenia sa zobrazujú pri spustení Oznámenia sa pri spustení nezobrazujú @@ -1007,46 +989,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Nepodarilo sa pripojiť k poskytovateľovi oznámení Odmietnuť - + Upozornenie Neukazuj znovu - + Povoliť automatické opakovanie Automatické opakovanie je povolené Automatické opakovanie je zakázané - + Rozmery spoof zariadenia Sfalšované rozmery zariadenia\n\nMôžu byť odomknuté vyššie kvality videa, ale môžete zaznamenať zasekávanie prehrávania videa, horšiu výdrž batérie a neznáme vedľajšie účinky Rozmery zariadenia nie sú sfalšované\n\nAk to povolíte, môžete odomknúť vyššiu kvalitu videa Povolenie môže spôsobiť zasekávanie prehrávania videa, horšiu výdrž batérie a neznáme vedľajšie účinky. - + Nastavenia GmsCore Nastavenia pre GmsCore - + Obíďte presmerovania adries URL Presmerovania URL sú obchádzané Presmerovania URL nie sú obchádzané - + Otvoriť odkazy v prehliadači Otváranie odkazov externe Otváranie odkazov v aplikácii - + Odstráňte parameter dopytu sledovania Parameter dopytu sledovania je odstránený z odkazov Parameter dopytu sledovania nie je odstránený z odkazov - + Zakázať haptiku priblíženia Haptika je vypnutá Haptika je povolená - + Automatická kvalita Pamätajte na zmeny kvality videa Zmeny kvality sa vzťahujú na všetky videá @@ -1056,48 +1038,44 @@ This is because Crowdin requires temporarily flattening this file and removing t mobilné Predvolená kvalita %1$s bola zmenená na: %2$s - + Zobraziť dialógové tlačidlo rýchlosti Tlačidlo je zobrazené Tlačidlo nie je zobrazené - + Vlastné rýchlosti prehrávania - Pridajte alebo zmeňte dostupné rýchlosti prehrávania Vlastné rýchlosti musia byť nižšie ako %s. Použitie predvolených hodnôt. Neplatné vlastné rýchlosti prehrávania. Použitie predvolených hodnôt. - + Pamätajte na zmeny rýchlosti prehrávania Zmeny rýchlosti prehrávania sa vzťahujú na všetky videá Zmeny rýchlosti prehrávania sa vzťahujú len na aktuálne video Predvolená rýchlosť prehrávania Predvolená rýchlosť bola zmenená na: %s - + Obnovte starú ponuku kvality videa Zobrazí sa stará ponuka kvality videa Stará ponuka kvality videa sa nezobrazuje - + Povoliť vyhľadávanie snímkou Slide to search je zapnuté Nie je povolené posúvanie - + Vypnutie tohto nastavenia môže spôsobiť problémy s prehrávaním videa. - - - - + Blokovať zvukové reklamy Zvukové reklamy sú zablokované Zvukové reklamy sú odblokované - + %s nie je k dispozícii. Môžu sa zobrazovať reklamy. Skúste v nastaveniach prejsť na inú službu blokovania reklám. %s server vrátil chybu. Môžu sa zobrazovať reklamy. Skúste v nastaveniach prejsť na inú službu blokovania reklám. Blokovať vložené videoreklamy @@ -1105,30 +1083,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Svetelný proxy Proxy PurpleAdBlock - + Blokovať videoreklamy Videoreklamy sú zablokované Videoreklamy sú odblokované - + správa vymazaná Zobraziť odstránené správy Nezobrazovať odstránené správy Skryť odstránené správy za spojler Zobraziť odstránené správy ako preškrtnutý text - + Automaticky nárokovať body kanála Kanálové body sa získavajú automaticky Kanálové body sa nenárokujú automaticky - + Povoliť režim ladenia Twitch Režim ladenia Twitch je povolený (neodporúča sa) Režim ladenia Twitch je zakázaný - + Reklamy Nastavenia blokovania reklám Nastavenia chatu diff --git a/src/main/resources/addresources/values-sl-rSI/strings.xml b/patches/src/main/resources/addresources/values-sl-rSI/strings.xml similarity index 71% rename from src/main/resources/addresources/values-sl-rSI/strings.xml rename to patches/src/main/resources/addresources/values-sl-rSI/strings.xml index 091492c51..f93876fc2 100644 --- a/src/main/resources/addresources/values-sl-rSI/strings.xml +++ b/patches/src/main/resources/addresources/values-sl-rSI/strings.xml @@ -32,9 +32,9 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + Ali želite nadaljevati? Ponastavi Osvežitev in ponovni zagon @@ -42,18 +42,20 @@ This is because Crowdin requires temporarily flattening this file and removing t Uvoz - + - + O programu - + + + Razhroščevanje - + @@ -69,30 +71,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -102,23 +104,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -129,29 +125,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -159,26 +146,26 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + O programu - + - + - + Izgled @@ -187,86 +174,85 @@ This is because Crowdin requires temporarily flattening this file and removing t Ponastavi O programu - + - + Privzeto - + - + - + - + - + - + - + + + - + Opusti - + Opozorilo - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + Onemogočeno - + - + - + - + - + diff --git a/src/main/resources/addresources/values-sq-rAL/strings.xml b/patches/src/main/resources/addresources/values-sq-rAL/strings.xml similarity index 70% rename from src/main/resources/addresources/values-sq-rAL/strings.xml rename to patches/src/main/resources/addresources/values-sq-rAL/strings.xml index 48859b456..ab5aab1d0 100644 --- a/src/main/resources/addresources/values-sq-rAL/strings.xml +++ b/patches/src/main/resources/addresources/values-sq-rAL/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,106 +139,105 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + Kujdes - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/src/main/resources/addresources/values-sr-rCS/strings.xml b/patches/src/main/resources/addresources/values-sr-rCS/strings.xml similarity index 93% rename from src/main/resources/addresources/values-sr-rCS/strings.xml rename to patches/src/main/resources/addresources/values-sr-rCS/strings.xml index 6f1832d5d..a0ff99860 100644 --- a/src/main/resources/addresources/values-sr-rCS/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rCS/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Provere nisu uspele Otvori zvanični veb-sajt Zanemari @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Pečovano pre %s dana Datum izrade APK-a je oštećen - + ReVanced Želite li da nastavite? Resetuj @@ -63,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Zvanični linkovi Donacija - + MicroG GmsCore nije instaliran. Instalirajte ga. Neophodna radnja @@ -74,7 +74,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + O programu Oglasi Alternativne sličice @@ -86,7 +86,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Razno Video - + + Onemogući autoplej Shorts videa u pozadini + Puštanje Shorts videa u pozadini je onemogućeno + Puštanje Shorts videa u pozadini je omogućeno + + Otklanjanje grešaka Omogućite ili onemogućite opcije za otklanjanje grešaka Evidentiranje otklanjanja grešaka @@ -103,13 +108,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Iskačuće obavešenje se ne prikazuje ako dođe do greške Isključivanje iskačućih obaveštenja o grešci sakriva sva obaveštenja o greškama u ReVancedu.\n\nNećete biti obavešteni ni o kakvim neočekivanim događajima. - + Onemogući sjaj dugmadi „Sviđanje” / „Zaprati” Dugmad „Sviđanje” i „Zaprati” neće svetleti kada se pritisnu Dugmad „Sviđanje” i „Zaprati” će svetleti kada se pritisnu - Sakrij sive razdelnike - Sivi razdelnici su skriveni - Sivi razdelnici su prikazani + Sakrij kartice albuma + Kartice albuma su skrivene + Kartice albuma su prikazane + Sakrij polje za kolektivno finansiranje + Polje za kolektivno finansiranje je skriveno + Polje za kolektivno finansiranje je prikazano + Sakrij plutajuće dugme mikrofona + Plutajuće dugme mikrofona je skriveno + Plutajuće dugme mikrofona je prikazano Sakrij vodeni žig kanala Vodeni žig kanala je skriven Vodeni žig kanala je prikazan @@ -154,9 +165,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Sakrij proširivi deo ispod videa Proširivi delovi su skriveni Proširivi delovi su prikazani - Sakrij podnožje menija kvaliteta videa - Podnožje menija kvaliteta videa je skriveno - Podnožje menija kvaliteta videa je prikazano Sakrij objave zajednice Objave zajednice su skrivene Objave zajednice su prikazane @@ -231,6 +239,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Odeljak za transkripciju je prikazan Opis videa Sakrijte ili prikažite komponente opisa videa + Traka filtera + Sakrijte ili prikažite traku filtera u fidu, pretrazi ili srodnim videima + Sakrij u fidu + Skriveno u fidu + Prikazano u fidu + Sakrij u pretrazi + Skriveno u pretrazi + Prikazano u pretrazi + Sakrij u srodnim videima + Skriveno u srodnim videima + Prikazano u srodnim videima + Komentari + Sakrijte ili prikažite komponente odeljka za komentare + Sakrij zaglavlje „Komentari od članova” + Zaglavlje „Komentari od članova” je skriveno + Zaglavlje „Komentari od članova” je prikazano + Sakrij odeljak za komentare + Odeljak za komentare je skriven + Odeljak za komentare je prikazan + Sakrij dugme „Napravi Short” + Dugme „Napravi Short” je skriveno + Dugme „Napravi Short” je prikazano + Sakrij komentar za pregled + Komentar za pregled je skriven + Komentar za pregled je prikazan + Sakrij dugme „Hvala” + Dugme „Hvala” je skriveno + Dugme „Hvala” je prikazano + Sakrij dugmad za vremensku oznaku i emodžije + Dugmad za vremensku oznaku i emodžije su skrivena + Dugmad za vremensku oznaku i emodžije su prikazana Sakrij YouTube Doodles YouTube Doodles u traci za pretragu su skriveni @@ -272,7 +311,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Ključna reč je prekratka i zahteva navodnike: %s Ključna reč će sakriti sve videe: %s - + Sakrij opšte oglase Opšti oglasi su skriveni Opšti oglasi su prikazani @@ -291,6 +330,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Sakrij baner za gledanje proizvoda Baner za gledanje proizvoda je skriven Baner za gledanje proizvoda je prikazan + Sakrij policu „Kupovina” u plejeru + Polica „Kupovina” u plejeru je skrivena + Polica „Kupovina” u plejeru je prikazana Sakrij linkove za kupovinu u opisu videa Linkovi za kupovinu u opisu videa su skriveni Linkovi za kupovinu u opisu videa su prikazani @@ -307,17 +349,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Sakrivanje oglasa preko celog ekrana radi samo sa starijim uređajima - + Sakrij promocije za YouTube Premium Promocije za YouTube Premium ispod video plejera su skrivene Promocije za YouTube Premium ispod video plejera su prikazane - + Sakrij oglase u videu Oglasi u videu su skriveni Oglasi u videu su prikazani - + Link je kopiran u privremenu memoriju Link sa vremenskom oznakom je kopiran Prikaži dugme za kopiranje linka videa @@ -327,13 +369,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Dugme je prikazano. Dodirnite da biste kopirali link videa s vremenskom oznakom. Dodirnite i zadržite da biste kopirali link videa bez vremenske oznake Dugme za kopiranje linka videa sa vremenskom oznakom nije prikazano - + Ukloni dijalog o diskreciji gledaoca Dijalog o diskreciji gledaoca će biti uklonjen Dijalog o diskreciji gledaoca će biti prikazan Ovo ne zaobilazi starosno ograničenje. Samo ga automatski prihvata. - + Spoljna preuzimanja Podešavanja za korišćenje spoljnog programa za preuzimanje Prikaži dugme za spoljno preuzimanje @@ -347,17 +389,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Naziv paketa instaliranog spoljnog programa za preuzimanje, kao što je NewPipe ili Seal %s nije instaliran. Instalirajte ga. - + Onemogući pokret preciznog premotavanja Pokret preciznog premotavanja je onemogućen Pokret preciznog premotavanja je omogućen - + Omogući dodirivanje trake za premotavanje Dodirivanje trake za premotavanje je omogućeno Dodirivanje trake za premotavanje je onemogućeno - + Omogući pokret za osvetljenost Prevlačenje za podešavanje osvetljenosti je omogućeno Prevlačenje za podešavanje osvetljenosti je onemogućeno @@ -386,12 +428,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Prag trajanja prevlačenja Iznos praga trajanja prevlačenja - + Onemogući automatske titlove Automatski titlovi su onemogućeni Automatski titlovi su omogućeni - + Dugmad radnji Sakrijte ili prikažite dugmad ispod videa Sakrij dugmad „Sviđanje” i „Nesviđanje” @@ -427,23 +469,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Dugme „Sačuvaj na plejlistu” je skriveno Dugme „Sačuvaj na plejlistu” je prikazano - - Sakrij dugme „Autoplej” - Dugme „Autoplej” je skriveno - Dugme „Autoplej” je prikazano - - - - Sakrij dugme „Titl” - Dugme „Titl” je skriveno - Dugme „Titl” je prikazano - - - Sakrij dugme „Prebacuj” - Dugme „Prebacuj” je skriveno - Dugme „Prebacuj” je prikazano - - + Dugmad navigacije Sakrijte ili promenite dugmad na traci za navigaciju @@ -470,7 +496,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Oznake dugmadi za navigaciju su skrivene Oznake dugmadi za navigaciju su prikazane - + Padajući meni Sakrijte ili prikažite predmete u padajućem meniju plejera @@ -481,6 +507,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Sakrij dugme „Dodatna podešavanja” Meni „Dodatna podešavanja” je skriven Meni „Dodatna podešavanja” je prikazan + + Sakrij tajmer za spavanje + Meni tajmera za spavanje je skriven + Meni tajmera za spavanje je prikazan Sakrij dugme „Ponavljaj video” Dugme „Ponavljaj video” je skriveno @@ -489,6 +519,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Sakrij dugme „Ambijentalni režim” Dugme „Ambijentalni režim” je skriveno Dugme „Ambijentalni režim” je prikazano + Sakrij dugme „Ujednačena jačina zvuka” + Dugme „Ujednačena jačina zvuka” je prikazano + Dugme „Ujednačena jačina zvuka” je skriveno Sakrij dugme „Pomoć i povratne informacije” Dugme „Pomoć i povratne informacije” je skriveno @@ -514,83 +547,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Sakrij dugme „Gledaj u VR” Dugme „Gledaj u VR” je skriveno Dugme „Gledaj u VR” je prikazano + Sakrij podnožje menija kvaliteta videa + Podnožje menija kvaliteta videa je skriveno + Podnožje menija kvaliteta videa je prikazano - - Sakrij dugmad za prethodni i sledeći video - Dugmad za prethodni i sledeći video su skrivena - Dugmad za prethodni i sledeći video su prikazana + + Sakrij dugmad za prethodni i sledeći video + Dugmad za prethodni i sledeći video su skrivena + Dugmad za prethodni i sledeći video su prikazana + Sakrij dugme „Prebacuj” + Dugme „Prebacuj” je skriveno + Dugme „Prebacuj” je prikazano + + Sakrij dugme „Titl” + Dugme „Titl” je skriveno + Dugme „Titl” je prikazano + Sakrij dugme „Autoplej” + Dugme „Autoplej” je skriveno + Dugme „Autoplej” je prikazano - - Sakrij kartice albuma - Kartice albuma su skrivene - Kartice albuma su prikazane - - - Komentari - Sakrijte ili prikažite komponente odeljka za komentare - Sakrij zaglavlje „Komentari od članova” - Zaglavlje „Komentari od članova” je skriveno - Zaglavlje „Komentari od članova” je prikazano - Sakrij odeljak za komentare - Odeljak za komentare je skriven - Odeljak za komentare je prikazan - Sakrij dugme „Napravi Short” - Dugme „Napravi Short” je skriveno - Dugme „Napravi Short” je prikazano - Sakrij komentar za pregled - Komentar za pregled je skriven - Komentar za pregled je prikazan - Sakrij dugme „Hvala” - Dugme „Hvala” je skriveno - Dugme „Hvala” je prikazano - Sakrij dugmad za vremensku oznaku i emodžije - Dugmad za vremensku oznaku i emodžije su skrivena - Dugmad za vremensku oznaku i emodžije su prikazana - - - Sakrij polje za kolektivno finansiranje - Polje za kolektivno finansiranje je skriveno - Polje za kolektivno finansiranje je prikazano - - + Sakrij kartice završnog ekrana Kartice završnog ekrana su skrivene Kartice završnog ekrana su prikazane - - Traka filtera - Sakrijte ili prikažite traku filtera u fidu, pretrazi ili srodnim videima - Sakrij u fidu - Skriveno u fidu - Prikazano u fidu - Sakrij u pretrazi - Skriveno u pretrazi - Prikazano u pretrazi - Sakrij u srodnim videima - Skriveno u srodnim videima - Prikazano u srodnim videima - - - Sakrij plutajuće dugme mikrofona - Plutajuće dugme mikrofona je skriveno - Plutajuće dugme mikrofona je prikazano - - + Onemogući ambijentalni režim u režimu celog ekrana Ambijentalni režim u režimu celog ekrana je onemogućen Ambijentalni režim u režimu celog ekrana je omogućen - + Sakrij kartice sa informacijama Kartice sa informacijama su skrivene Kartice sa informacijama su prikazane - + Onemogući animacije brojeva Brojevi nisu animirani Brojevi su animirani - + Sakrij traku za premotavanje u video plejeru Traka za premotavanje u video plejeru je skrivena Traka za premotavanje u video plejeru je prikazana @@ -598,7 +594,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Traka za premotavanje na sličici videa je skrivena Traka za premotavanje na sličici videa je prikazana - + + Shorts plejer + Sakrijte ili prikažite komponente u Shorts plejeru Sakrij Shorts videe u fidu „Početna” Shorts videi u fidu „Početna” su skriveni @@ -696,27 +694,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Traka za navigaciju je skrivena Traka za navigaciju je prikazana - + Onemogući predloženi video na završnom ekranu Predloženi videi na završnom ekranu će biti skriveni Predloženi videi na završnom ekranu će biti prikazani - + Sakrij vremensku oznaku videa Vremenska oznaka videa je skrivena Vremenska oznaka videa je prikazana - + Sakrij iskačuće table u plejeru Iskačuće table u plejeru su skrivene Iskačuće table u plejeru su prikazane - + Neprozirnost preklopa plejera Vrednost neprozirnosti između 0 i 100, gde je 0 prozirno Neprozirnost preklopa plejera mora biti između 0 i 100 - + Nesviđanja privremeno nisu dostupna (API istekao) Nesviđanja nisu dostupna (status %d) @@ -760,17 +758,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Broj ostvarenih ograničenja stope klijenta: %d %d milisekundi - + Omogući široku traku za pretragu Široka traka za pretragu je omogućena Široka traka za pretragu je onemogućena - + + Omogući visokokvalitetne sličice + Sličice na traci za premotavanje su visokog kvaliteta + Sličice na traci za premotavanje su srednjeg kvaliteta + Sličice na traci za premotavanje u režimu celog ekrana su visokog kvaliteta + Sličice na traci za premotavanje u režimu celog ekrana su srednjeg kvaliteta + Ovo će takođe vratiti sličice na strimovima uživo koji nemaju sličice na traci za premotavanje.\n\nSličice na traci za premotavanje će koristiti isti kvalitet kao i trenutni video.\n\nOva funkcija najbolje funkcioniše sa kvalitetom videa od 720p ili nižim i kada koristite veoma brzu internet vezu. Vrati stare sličice na traci za premotavanje Sličice trake za premotavanje će se pojaviti iznad nje Sličice trake za premotavanje će se pojaviti u režimu celog ekrana - + Omogući SponsorBlock SponsorBlock je sistem napravljen od zajednice korisnika i služi za preskakanje dosadnih delova YouTube videa Izgled @@ -951,7 +955,7 @@ This is because Crowdin requires temporarily flattening this file and removing t O programu Podatke obezbeđuje SponsorBlock API. Dodirnite ovde da biste saznali više i videli preuzimanja za druge platforme - + Lažirana verzija aplikacije Verzija je lažirana Verzija nije lažirana @@ -964,9 +968,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Vraća širok meni za brzinu i kvalitet videa 18.09.39 - Vraća karticu zbirke 17.41.37 - Vraća staru policu plejliste - 17.33.42 - Vraća stari izgled korisničkog interfejsa - + Podešavanje početne stranice Podrazumevana Pretraga kanala @@ -984,18 +987,26 @@ This is because Crowdin requires temporarily flattening this file and removing t U trendu Gledaj kasnije - + Onemogući nastavak reprodukcije Shorts plejera Shorts plejer neće nastaviti reprodukciju pri pokretanju aplikacije Shorts plejer će nastaviti reprodukciju pri pokretanju aplikacije - + + Autoplej Shorts videa + Shorts videi će se automatski puštati + Shorts videi će se ponavljati + Autoplej Shorts videa u pozadini + Shorts videi će se automatski puštati u pozadini + Shorts videi će se ponavljati u pozadini + + Omogući korisnički interfejs tableta Korisnički interfejs tableta je omogućen Korisnički interfejs tableta je onemogućen Objave zajednice se ne prikazuju u korisničkom interfejsu tableta - + Mini-plejer Promenite stil minimiziranog plejera u aplikaciji Tip mini-plejera @@ -1014,6 +1025,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Omogući prevlačenje i otpuštanje Prevlačenje i otpuštanje je omogućeno\n\nMini-plejer može da se prevuče u bilo koji ugao ekrana Prevlačenje i otpuštanje je onemogućeno + Omogući pokret horizontalnog prevlačenja + Pokret horizontalnog prevlačenja je omogućen\n\nMini-plejer može da se prevuče sa ekrana nalevo ili nadesno + Pokret horizontalnog prevlačenja je onemogućen Sakrij dugme za zatvaranje Dugme za zatvaranje je skriveno Dugme za zatvaranje je prikazano @@ -1033,12 +1047,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Vrednost neprozirnosti između 0 i 100, gde je 0 prozirno Neprozirnost preklopa mini-plejera mora biti između 0 i 100 - + Omogući ekran učitavanja s gradijentom Ekran učitavanja će imati pozadinu s gradijentom Ekran učitavanja će imati običnu pozadinu - + Omogući prilagođenu boju trake za premotavanje Prilagođena boja trake za premotavanje je prikazana Originalna boja trake za premotavanje je prikazana @@ -1046,12 +1060,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Boja trake za premotavanje Nevažeća vrednost boje trake za premotavanje - + Zaobiđi ograničenja regiona slike Korišćenje hosta slike yt4.ggpht.com Korišćenje originalnog hosta slike\n\nOmogućavanjem ovoga možete da popravite nedostajuće slike koje su blokirane u nekim regionima - + Kartica „Početna” @@ -1083,7 +1097,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow privremeno nije dostupan (kôd statusa: %s) DeArrow privremeno nije dostupan - + Prikaži saopštenja ReVanceda Saopštenja su prikazana pri pokretanju Saopštenja nisu prikazana pri pokretanju @@ -1091,47 +1105,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Neuspešno povezivanje sa pružaocem saopštenja Odbaci - + Upozorenje Vaša istorija gledanja se ne čuva.<br><br>Ovo je najverovatnije uzrokovano DNS blokatorom oglasa ili mrežnim proksijem.<br><br>Da biste ovo popravili, stavite na belu listu <b>s.youtube.com</b> ili isključite sve DNS blokatore i proksije. Ne prikazuj ponovo - + Omogući automatsko ponavljanje videa Automatsko ponavljanje videa je omogućeno Automatsko ponavljanje videa je onemogućeno - + Lažirane dimenzije uređaja Dimenzije uređaja su lažirane\n\nViši kvaliteti videa će možda biti otključani, ali ćete možda imati zastoj pri reprodukciji videa, kraće trajanje baterije i nepoznate neželjene efekte Dimenzije uređaja nisu lažirane\n\nAko ovo omogućite, viši kvaliteti videa će se možda otključati Ako ovo omogućite, možda će doći do zastoja pri reprodukciji videa, kraćeg trajanja baterije i nepoznatih neželjenih efekata. - + Podešavanja GmsCorea Podešavanja za GmsCore - + Zaobiđi URL preusmeravanja URL preusmeravanja se zaobilaze URL preusmeravanja se ne zaobilaze - + Otvori linkove u pregledaču Otvaranje linkova van aplikacije Otvaranje linkova u aplikaciji - + Ukloni parametar upita za praćenje Parametar upita za praćenje je uklonjen iz linkova Parametar upita za praćenje nije uklonjen iz linkova - + Onemogući vibraciju pri uveličavanju Vibracija pri uveličavanju je onemogućena Vibracija pri uveličavanju je omogućena - + Automatski kvalitet Zapamti promene kvaliteta videa Promene kvaliteta se primenjuju na sve videe @@ -1142,35 +1156,38 @@ This is because Crowdin requires temporarily flattening this file and removing t na Wi-Fi mreži Promenjen podrazumevani kvalitet %1$s na: %2$s - + Prikaži dugme dijaloga za brzinu Dugme dijaloga za brzinu je prikazano Dugme dijaloga za brzinu nije prikazano - + + Meni prilagođene brzine reprodukcije + Meni prilagođene brzine reprodukcije je prikazan + Meni prilagođene brzine reprodukcije nije prikazan Prilagođene brzine reprodukcije - Dodajte ili promenite dostupne brzine reprodukcije + Dodajte ili promenite prilagođene brzine reprodukcije Prilagođene brzine reprodukcije moraju biti manje od %s. Korišćenje podrazumevanih vrednosti. Nevažeće prilagođene brzine reprodukcije. Korišćenje podrazumevanih vrednosti. - + Zapamti promene brzine reprodukcije Promene brzine reprodukcije se primenjuju na sve videe Promene brzine reprodukcije se primenjuju samo na trenutni video Podrazumevana brzina reprodukcije Brzina reprodukcije promenjena na: %s - + Vrati stari meni kvaliteta videa Stari meni kvaliteta videa je prikazan Stari meni kvaliteta videa nije prikazan - + Omogući prevlačenje za premotavanje Prevlačenje za premotavanje je omogućeno Prevlaćenje za premotavanje nije omogućeno - + Lažiran video strim Lažiranje klijenta video strimova da bi se sprečili problemi sa reprodukcijom Lažirani video strimovi @@ -1188,20 +1205,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Neželjeni efekti lažiranja na Android VR • Meni „Audio snimak” nedostaje\n• Opcija „Ujednačena jačina zvuka” nije dostupna - - - Omogući automatsku osvetljenost HDR režima - Automatska osvetljenost HDR režima je omogućena - Automatska osvetljenost HDR režima je onemogućena - - + Blokiraj audio oglase Audio oglasi su blokirani Audio oglasi su odblokirani - + %s nedostupan. Oglasi će se možda prikazivati. Pokušajte da pređete na drugu uslugu za blokiranje oglasa u podešavanjima. %s server je vratio grešku. Oglasi će se možda prikazivati. Pokušajte da pređete na drugu uslugu za blokiranje oglasa u podešavanjima. Blokiranje ugrađenih video oglasa @@ -1209,30 +1220,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Luminous proksi PurpleAdBlock proksi - + Blokiraj video oglase Video oglasi su blokirani Video oglasi su odblokirani - + poruka izbrisana Prikaz izbrisanih poruka Ne prikazuj izbrisane poruke Sakrij izbrisane poruke iza spojlera Prikaži izbrisane poruke kao precrtan tekst - + Automatski preuzmi bodove kanala Bodovi kanala su automatski preuzeti Bodovi kanala nisu automatski preuzeti - + Omogući Twitch režim otklanjanja grešaka Twitch režim otklanjanja grešaka je omogućen (nije preporučeno) Twitch režim otklanjanja grešaka je onemogućen - + Podešavanja ReVanceda Oglasi Podešavanja blokiranja oglasa diff --git a/src/main/resources/addresources/values-sr-rSP/strings.xml b/patches/src/main/resources/addresources/values-sr-rSP/strings.xml similarity index 93% rename from src/main/resources/addresources/values-sr-rSP/strings.xml rename to patches/src/main/resources/addresources/values-sr-rSP/strings.xml index 144861183..d673e1b62 100644 --- a/src/main/resources/addresources/values-sr-rSP/strings.xml +++ b/patches/src/main/resources/addresources/values-sr-rSP/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Провере нису успеле Отвори званични веб-сајт Занемари @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Печовано пре %s дана Датум израде APK-а је оштећен - + ReVanced Желите ли да наставите? Ресетуј @@ -63,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Званични линкови Донација - + MicroG GmsCore није инсталиран. Инсталирајте га. Неопходна радња @@ -74,7 +74,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + О програму Огласи Алтернативне сличице @@ -86,7 +86,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Разно Видео - + + Онемогући аутоплеј Shorts видеа у позадини + Пуштање Shorts видеа у позадини је онемогућено + Пуштање Shorts видеа у позадини је омогућено + + Отклањање грешака Омогућите или онемогућите опције за отклањање грешака Евидентирање отклањања грешака @@ -103,13 +108,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Искачуће обавештење се не приказује ако дође до грешке Искључивање искачућих обавештења о грешци сакрива сва обавештења о грешкама у ReVanced-у.\n\nНећете бити обавештени ни о каквим неочекиваним догађајима. - + Онемогући сјај дугмади „Свиђање” / „Запрати” Дугмад „Свиђање” и „Запрати” неће светлети када се притисну Дугмад „Свиђање” и „Запрати” ће светлети када се притисну - Сакриј сиве разделнике - Сиви разделници су скривени - Сиви разделници су приказани + Сакриј картице албума + Картице албума су скривене + Картице албума су приказане + Сакриј поље за колективно финансирање + Поље за колективно финансирање је скривено + Поље за колективно финансирање је приказано + Сакриј плутајуће дугме микрофона + Плутајуће дугме микрофона је скривено + Плутајуће дугме микрофона је приказано Сакриј водени жиг канала Водени жиг канала је скривен Водени жиг канала је приказан @@ -154,9 +165,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Сакриј прошириви део испод видеа Прошириви делови су скривени Прошириви делови су приказани - Сакриј подножје менија квалитета видеа - Подножје менија квалитета видеа је скривено - Подножје менија квалитета видеа је приказано Сакриј објаве заједнице Објаве заједнице су скривене Објаве заједнице су приказане @@ -231,6 +239,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Одељак за транскрипцију је скривен Опис видеа Сакријте или прикажите компоненте описа видеа + Трака филтера + Сакријте или прикажите траку филтера у фиду, претрази или сродним видеима + Сакриј у фиду + Скривено у фиду + Приказано у фиду + Сакриј у претрази + Скривено у претрази + Приказано у претрази + Сакриј у сродним видеима + Скривено у сродним видеима + Приказано у сродним видеима + Коментари + Сакријте или прикажите компоненте одељка за коментаре + Сакриј заглавље „Коментари од чланова” + Заглавље „Коментари од чланова” је скривено + Заглавље „Коментари од чланова” је приказано + Сакриј одељак за коментаре + Одељак за коментаре је скривен + Одељак за коментаре је приказан + Сакриј дугме „Направи Short” + Дугме „Направи Short” је скривено + Дугме „Направи Short” је приказано + Сакриј коментар за преглед + Коментар за преглед је скривен + Коментар за преглед је приказан + Сакриј дугме „Хвала” + Дугме „Хвала” је скривено + Дугме „Хвала” је приказано + Сакриј дугмад за временску ознаку и емоџије + Дугмад за временску ознаку и емоџије су скривена + Дугмад за временску ознаку и емоџије су приказана Сакриј YouTube Doodles YouTube Doodles у траци за претрагу су скривени @@ -272,7 +311,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Кључна реч је прекратка и захтева наводнике: %s Кључна реч ће сакрити све видее: %s - + Сакриј опште огласе Општи огласи су скривени Општи огласи су приказани @@ -291,6 +330,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Сакриј банер за гледање производа Банер за гледање производа је скривен Банер за гледање производа је приказан + Сакриј полицу „Куповина” у плејеру + Полица „Куповина” у плејеру је скривена + Полица „Куповина” у плејеру је приказана Сакриј линкове за куповину у опису видеа Линкови за куповину у опису видеа су скривени Линкови за куповину у опису видеа су приказани @@ -307,17 +349,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Сакривање огласа преко целог екрана ради само са старијим уређајима - + Сакриј промоције за YouTube Premium Промоције за YouTube Premium испод видео плејера су скривене Промоције за YouTube Premium испод видео плејера су приказане - + Сакриј огласе у видеу Огласи у видеу су скривени Огласи у видеу су приказани - + Линк је копиран у привремену меморију Линк са временском ознаком је копиран Прикажи дугме за копирање линка видеа @@ -327,13 +369,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Дугме је приказано. Додирните да бисте копирали линк видеа с временском ознаком. Додирните и задржите да бисте копирали линк видеа без временске ознаке Дугме за копирање линка видеа са временском ознаком није приказано - + Уклони дијалог о дискрецији гледаоца Дијалог о дискрецији гледаоца ће бити уклоњен Дијалог о дискрецији гледаоца ће бити приказан Ово не заобилази старосно ограничење. Само га аутоматски прихвата. - + Спољна преузимања Подешавања за коришћење спољног програма за преузимање Прикажи дугме за спољно преузимање @@ -347,17 +389,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Назив пакета инсталираног спољног програма за преузимање, као што је NewPipe или Seal %s није инсталиран. Инсталирајте га. - + Онемогући покрет прецизног премотавања Покрет прецизног премотавања је онемогућен Покрет прецизног премотавања је омогућен - + Омогући додиривање траке за премотавање Додиривање траке за премотавање је омогућено Додиривање траке за премотавање је онемогућено - + Омогући покрет за осветљеност Превлачење за подешавање осветљености је омогућено Превлачење за подешавање осветљености је онемогућено @@ -386,12 +428,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Праг трајања превлачења Износ прага трајања превлачења - + Онемогући аутоматске титлове Аутоматски титлови су онемогућени Аутоматски титлови су омогућени - + Дугмад радњи Сакријте или прикажите дугмад испод видеа Сакриј дугмад „Свиђање” и „Несвиђање” @@ -427,23 +469,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Дугме „Сачувај на плејлисту” је скривено Дугме „Сачувај на плејлисту” је приказано - - Сакриј дугме „Аутоплеј” - Дугме „Аутоплеј” је скривено - Дугме „Аутоплеј” је приказано - - - - Сакриј дугме „Титл” - Дугме „Титл” је скривено - Дугме „Титл” је приказано - - - Сакриј дугме „Пребацуј” - Дугме „Пребацуј” је скривено - Дугме „Пребацуј” је приказано - - + Дугмад навигације Сакријте или промените дугмад на траци за навигацију @@ -470,7 +496,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Ознаке дугмади за навигацију су скривене Ознаке дугмади за навигацију су приказане - + Падајући мени Сакријте или прикажите предмете у падајућем менију плејера @@ -481,6 +507,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Сакриј дугме „Додатна подешавања” Мени „Додатна подешавања” је скривен Мени „Додатна подешавања” је приказан + + Сакриј тајмер за спавање + Мени тајмера за спавање је скривен + Мени тајмера за спавање је приказан Сакриј дугме „Понављај видео” Дугме „Понављај видео” је скривено @@ -489,6 +519,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Сакриј дугме „Амбијентални режим” Дугме „Амбијентални режим” је скривено Дугме „Амбијентални режим” је приказано + Сакриј дугме „Уједначена јачина звука” + Дугме „Уједначена јачина звука” је приказано + Дугме „Уједначена јачина звука” је скривено Сакриј дугме „Помоћ и повратне информације” Дугме „Помоћ и повратне информације” је скривено @@ -514,83 +547,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Сакриј дугме „Гледај у ВР” Дугме „Гледај у ВР” је скривено Дугме „Гледај у ВР” је приказано + Сакриј подножје менија квалитета видеа + Подножје менија квалитета видеа је скривено + Подножје менија квалитета видеа је приказано - - Сакриј дугмад за претходни и следећи видео - Дугмад за претходни и следећи видео су скривена - Дугмад за претходни и следећи видео су приказана + + Сакриј дугмад за претходни и следећи видео + Дугмад за претходни и следећи видео су скривена + Дугмад за претходни и следећи видео су приказана + Сакриј дугме „Пребацуј” + Дугме „Пребацуј” је скривено + Дугме „Пребацуј” је приказано + + Сакриј дугме „Титл” + Дугме „Титл” је скривено + Дугме „Титл” је приказано + Сакриј дугме „Аутоплеј” + Дугме „Аутоплеј” је скривено + Дугме „Аутоплеј” је приказано - - Сакриј картице албума - Картице албума су скривене - Картице албума су приказане - - - Коментари - Сакријте или прикажите компоненте одељка за коментаре - Сакриј заглавље „Коментари од чланова” - Заглавље „Коментари од чланова” је скривено - Заглавље „Коментари од чланова” је приказано - Сакриј одељак за коментаре - Одељак за коментаре је скривен - Одељак за коментаре је приказан - Сакриј дугме „Направи Short” - Дугме „Направи Short” је скривено - Дугме „Направи Short” је приказано - Сакриј коментар за преглед - Коментар за преглед је скривен - Коментар за преглед је приказан - Сакриј дугме „Хвала” - Дугме „Хвала” је скривено - Дугме „Хвала” је приказано - Сакриј дугмад за временску ознаку и емоџије - Дугмад за временску ознаку и емоџије су скривена - Дугмад за временску ознаку и емоџије су приказана - - - Сакриј поље за колективно финансирање - Поље за колективно финансирање је скривено - Поље за колективно финансирање је приказано - - + Сакриј картице завршног екрана Картице завршног екрана су скривене Картице завршног екрана су приказане - - Трака филтера - Сакријте или прикажите траку филтера у фиду, претрази или сродним видеима - Сакриј у фиду - Скривено у фиду - Приказано у фиду - Сакриј у претрази - Скривено у претрази - Приказано у претрази - Сакриј у сродним видеима - Скривено у сродним видеима - Приказано у сродним видеима - - - Сакриј плутајуће дугме микрофона - Плутајуће дугме микрофона је скривено - Плутајуће дугме микрофона је приказано - - + Онемогући амбијентални режим у режиму целог екрана Амбијентални режим у режиму целог екрана је онемогућен Амбијентални режим у режиму целог екрана је омогућен - + Сакриј картице са информацијама Картице са информацијама су скривене Картице са информацијама су приказане - + Онемогући анимације бројева Бројеви нису анимирани Бројеви су анимирани - + Сакриј траку за премотавање у видео плејеру Трака за премотавање у видео плејеру је скривена Трака за премотавање у видео плејеру је приказана @@ -598,7 +594,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Трака за премотавање на сличици видеа је скривена Трака за премотавање на сличици видеа је приказана - + + Shorts плејер + Сакријте или прикажите компоненте у Shorts плејеру Сакриј Shorts видее у фиду „Почетна” Shorts видеи у фиду „Почетна” су скривени @@ -696,27 +694,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Трака за навигацију је скривена Трака за навигацију је приказана - + Онемогући предложени видео на завршном екрану Предложени видеи на завршном екрану ће бити скривени Предложени видеи на завршном екрану ће бити приказани - + Сакриј временску ознаку видеа Временска ознака видеа је скривена Временска ознака видеа је приказана - + Сакриј искачуће табле у плејеру Искачуће табле у плејеру су скривене Искачуће табле у плејеру су приказане - + Непрозирност преклопа плејера Вредност непрозирности између 0 и 100, где је 0 прозирно Непрозирност преклопа плејера мора бити између 0 и 100 - + Несвиђања привремено нису доступна (API истекао) Несвиђања нису доступна (статус %d) @@ -760,17 +758,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Број остварених ограничења стопе клијента: %d %d милисекунди - + Омогући широку траку за претрагу Широка трака за претрагу је омогућена Широка трака за претрагу је онемогућена - + + Омогући висококвалитетне сличице + Сличице на траци за премотавање су високог квалитета + Сличице на траци за премотавање су средњег квалитета + Сличице на траци за премотавање у режиму целог екрана су високог квалитета + Сличице на траци за премотавање у режиму целог екрана су средњег квалитета + Ово ће такође вратити сличице на стримовима уживо који немају сличице на траци за премотавање.\n\nСличице на траке за премотавање ће користити исти квалитет као и тренутни видео.\n\nОва функција најбоље функционише са квалитетом видеа од 720p или нижим и када користите веома брзу интернет везу. Врати старе сличице на траци за премотавање Сличице траке за премотавање ће се појавити изнад ње Сличице траке за премотавање ће се појавити у режиму целог екрана - + Омогући SponsorBlock SponsorBlock је систем направљен од заједнице корисника и служи за прескакање досадних делова YouTube видеа Изглед @@ -951,7 +955,7 @@ This is because Crowdin requires temporarily flattening this file and removing t О програму Податке обезбеђује SponsorBlock API. Додирните овде да бисте сазнали више и видели преузимања за друге платформе - + Лажирана верзија апликације Верзија је лажирана Верзија није лажирана @@ -964,9 +968,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Враћа широк мени за брзину и квалитет видеа 18.09.39 - Враћа картицу збирке 17.41.37 - Враћа стару полицу плејлисте - 17.33.42 - Враћа стари изглед корисничког интерфејса - + Подешавање почетне странице Подразумевана Претрага канала @@ -984,18 +987,26 @@ This is because Crowdin requires temporarily flattening this file and removing t У тренду Гледај касније - + Онемогући наставак репродукције Shorts плејера Shorts плејер неће наставити репродукцију при покретању апликације Shorts плејер ће наставити репродукцију при покретању апликације - + + Аутоплеј Shorts видеа + Shorts видеи ће се аутоматски пуштати + Shorts видеи ће се понављати + Аутоплеј Shorts видеа у позадини + Shorts видеи ће се аутоматски пуштати у позадини + Shorts видеи ће се понављати у позадини + + Омогући кориснички интерфејс таблета Кориснички интерфејс таблета је омогућен Кориснички интерфејс таблета је онемогућен Објаве заједнице се не приказују у корисничком интерфејсу таблета - + Мини-плејер Промените стил минимизираног плејера у апликацији Тип мини-плејера @@ -1014,6 +1025,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Омогући превлачење и отпуштање Превлачење и отпуштање је омогућено\n\nМини-плејер може да се превуче у било који угао екрана Превлачење и отпуштање је онемогућено + Омогући покрет хоризонталног превлачења + Покрет хоризонталног превлачења је омогућен\n\nМини-плејер може да се превуче са екрана налево или надесно + Покрет хоризонталног превлачења је онемогућен Сакриј дугме за затварање Дугме за затварање је скривено Дугме за затварање је приказано @@ -1033,12 +1047,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Вредност непрозирности између 0 и 100, где је 0 прозирно Непрозирност преклопа мини-плејера мора бити између 0 и 100 - + Омогући екран учитавања с градијентом Екран учитавања ће имати позадину с градијентом Екран учитавања ће имати обичну позадину - + Омогући прилагођену боју траке за премотавање Прилагођена боја траке за премотавање је приказана Оригинална боја траке за премотавање је приказана @@ -1046,12 +1060,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Боја траке за премотавање Неважећа вредност боје траке за премотавање - + Заобиђи ограничења региона слике Коришћење хоста слике yt4.ggpht.com Коришћење оригиналног хоста слике\n\nОмогућавањем овога можете да поправите недостајуће слике које су блокиране у неким регионима - + Картица „Почетна” @@ -1083,7 +1097,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow привремено није доступан (кôд статуса: %s) DeArrow привремено није доступан - + Прикажи саопштења ReVanced-а Саопштења су приказана при покретању Саопштења нису приказана при покретању @@ -1091,47 +1105,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Неуспешно повезивање са пружаоцем саопштења Одбаци - + Упозорење Ваша историја гледања се не чува.<br><br>Ово је највероватније узроковано DNS блокатором огласа или мрежним проксијем.<br><br>Да бисте ово поправили, ставите на белу листу <b>s.youtube.com</b> или искључите све DNS блокаторе и проксије. Не приказуј поново - + Омогући аутоматско понављање видеа Аутоматско понављање видеа је омогућено Аутоматско понављање видеа је онемогућено - + Лажиране димензије уређаја Димензије уређаја су лажиране\n\nВиши квалитети видеа ће можда бити откључан, али ћете можда имати застој при репродукцији видеа, краће трајање батерије и непознате нежељене ефекте Димензије уређаја нису лажиране\n\nАко ово омогућите, виши квалитети видеа ће се можда откључати Ако ово омогућите, можда ће доћи до застоја при репродукцији видеа, краћег трајања батерије и непознатих нежељених ефеката. - + Подешавања GmsCore-а Подешавања за GmsCore - + Заобиђи URL преусмеравања URL преусмеравања се заобилазе URL преусмеравања се не заобилазе - + Отвори линкове у прегледачу Отварање линкова ван апликације Отварање линкова у апликацији - + Уклони параметар упита за праћење Параметар упита за праћење је уклоњен из линкова Параметар упита за праћење није уклоњен из линкова - + Онемогући вибрацију при увеличавању Вибрација при увеличавању је онемогућена Вибрација при увеличавању је омогућена - + Аутоматски квалитет Запамти промене квалитета видеа Промене квалитета се примењују на све видее @@ -1142,35 +1156,38 @@ This is because Crowdin requires temporarily flattening this file and removing t на Wi-Fi мрежи Промењен подразумевани квалитет %1$s на: %2$s - + Прикажи дугме дијалога за брзину Дугме дијалога за брзину је приказано Дугме дијалога за брзину није приказано - + + Мени прилагођене брзине репродукције + Мени прилагођене брзине репродукције је приказан + Мени прилагођене брзине репродукције није приказан Прилагођене брзине репродукције - Додајте или промените доступне брзине репродукције + Додајте или промените прилагођене брзине репродукције Прилагођене брзине репродукције морају бити мање од %s. Коришћење подразумеваних вредности. Неважеће прилагођене брзине репродукције. Коришћење подразумеваних вредности. - + Запамти промене брзине репродукције Промене брзине репродукције се примењују на све видее Промене брзине репродукције се примењују само на тренутни видео Подразумевана брзина репродукције Брзина репродукције промењена на: %s - + Врати стари мени квалитета видеа Стари мени квалитета видеа је приказан Стари мени квалитета видеа није приказан - + Омогући превлачење за премотавање Превлачење за премотавање је омогућено Превлачење за премотавање није омогућено - + Лажиран видео стрим Лажирање клијента видео стримова да би се спречили проблеми са репродукцијом Лажирани видео стримови @@ -1188,20 +1205,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Нежељени ефекти лажирања на Android VR • Мени „Аудио снимак” недостаје\n• Опција „Уједначена јачина звука” није доступна - - - Омогући аутоматску осветљеност HDR режима - Аутоматска осветљеност HDR режима је омогућена - Аутоматска осветљеност HDR режима је онемогућена - - + Блокирај аудио огласе Аудио огласи су блокирани Аудио огласи су одблокирани - + %s недоступан. Огласи ће се можда приказивати. Покушајте да пређете на другу услугу за блокирање огласа у подешавањима. %s сервер је вратио грешку. Огласи ће се можда приказивати. Покушајте да пређете на другу услугу за блокирање огласа у подешавањима. Блокирање уграђених видео огласа @@ -1209,30 +1220,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Luminous прокси PurpleAdBlock прокси - + Блокирај видео огласе Видео огласи су блокирани Видео огласи су одблокирани - + порука избрисана Приказ избрисаних порука Не приказуј избрисане поруке Сакриј избрисане поруке иза спојлера Прикажи избрисане поруке као прецртан текст - + Аутоматски преузми бодове канала Бодови канала су аутоматски преузети Бодови канала нису аутоматски преузети - + Омогући Twitch режим отклањања грешака Twitch режим отклањања грешака је омогућен (није препоручено) Twitch режим отклањања грешака је онемогућен - + Подешавања ReVanced-а Огласи Подешавања блокирања огласа diff --git a/src/main/resources/addresources/values-sv-rSE/strings.xml b/patches/src/main/resources/addresources/values-sv-rSE/strings.xml similarity index 93% rename from src/main/resources/addresources/values-sv-rSE/strings.xml rename to patches/src/main/resources/addresources/values-sv-rSE/strings.xml index bee30b259..b7e6e0c30 100644 --- a/src/main/resources/addresources/values-sv-rSE/strings.xml +++ b/patches/src/main/resources/addresources/values-sv-rSE/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Kontroller misslyckades Öppna officiell hemsida Ignorera @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Patchade %s dagar sedan APK byggdatum är skadat - + ReVanced Vill du fortsätta? Återställ @@ -63,7 +63,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Officiella länkar Donera - + MicroG GmsCore är inte installerat. Installera. Åtgärd krävs @@ -74,7 +74,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Om Annonser Alternativa miniatyrbilder @@ -86,7 +86,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Övrigt Video - + + Inaktivera Shorts bakgrundsuppspelning + Shorts bakgrundsspel är inaktiverat + Shorts bakgrundsuppspelning är aktiverad + + Felsökning Aktivera eller inaktivera felsökningsalternativ Felsökningsloggning @@ -103,13 +108,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Visa inget meddelande om fel uppstår Att stänga av felmeddelanden döljer alla felaviseringar från ReVanced.\n\nDu kommer inte att meddelas om några oväntade händelser. - + Inaktivera gilla- / prenumerationsknapp glow Gilla och prenumerera knappen kommer inte att lysa när det nämns Gilla och prenumerera knappen lyser när det nämns - Dölj den gråa avgränsaren - De gråa separatorerna är dolda - De gråa separatorerna är synliga + Dölj albumkort + Skivkorten är dolda + Albumkort är synliga + Dölj crowdfunding-rutan + Crowdfunding-rutan är dold + Crowdfunding-rutan är synlig + Dölj flytande mikrofonknapp + Mikrofon knapp dold + Mikrofonknappen är synlig Dölj kanalens vattenstämpel Vattenstämpeln är dold Vattenstämpeln är synlig @@ -154,9 +165,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Dölj utökningsbart chip under videor Utökningsbara marker är dolda Expanderbara marker är synliga - Dölj sidfot för videokvalitet - Videokvalitets sidfot är dold - Videokvalitetsmenyns sidfot är synlig Dölj inlägg i communityn Gemenskapsinlägg är dolda Gemenskapsinlägg är synliga @@ -231,6 +239,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Transkriptsektionen är synlig Videobeskrivning Dölj eller visa videobeskrivningskomponenter + Filterfält + Dölj eller visa filterfältet i flödet, sök och relaterade videor + Dölj i flöde + Dold i flödet + Visas i flöde + Dölj i sökningen + Dold i sökningen + Visas i sök + Dölj i relaterade videor + Dold i relaterade videor + Visas i relaterade videor + Kommentarer + Dölj eller visa kommentarskomponenter + Dölj \'Kommentarer från medlemmar\' header + \'Kommentarer från medlemmar\' huvudet är dolt + \'Kommentarer från medlemmar\' header visas + Dölj kommentarsfältet + Sektionen för kommentarer är dold + Sektionen för kommentarer visas + Dölj knappen \'Skapa en kort\' + \'Skapa en kort\' knappen är dold + Knappen \'Skapa en kort\' visas + Dölj förhandsgranskningskommentar + Förhandsgranska kommentaren är dold + Förhandsgranska kommentar är synlig + Dölj tack-knappen + Tack-knappen är dold + Tackknappen är synlig + Dölj tidsstämplar och emoji-knappar + Tidsstämpel och emoji-knappar är dolda + Knappar för tidsstämpel och emoji visas Dölj YouTube Doodles Sökfältet Doodles är dolda @@ -272,7 +311,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Nyckelordet är för kort och kräver citattecken: %s Nyckelord döljer alla videor: %s - + Dölj allmänna annonser Allmänna annonser är dolda Allmänna annonser är synliga @@ -291,6 +330,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Dölj banner för att visa produkter Banner är dold Banner är synlig + Dölj spelarbutikshyllan + Hyllan för shopping är dold + Hyllan för shopping visas Dölj shoppinglänkar i videobeskrivning Shoppinglänkar är dolda Shoppinglänkar är synliga @@ -307,17 +349,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Dölj fullskärmsannonser fungerar endast med äldre enheter - + Dölj YouTube Premium-kampanjer YouTube Premium-kampanjer under videospelare är dolda YouTube Premium-kampanjer under videospelare är synliga - + Dölj videoannonser Videoannonser är dolda Videoannonser är synliga - + Webbadress kopierad till urklipp Webbadress med tidsstämpel kopierad Visa knappen för kopiering av video-URL @@ -327,13 +369,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Knappen är synlig. Tryck för att kopiera videowebbadressen med tidsstämpel. Tryck och håll ner för att kopiera videon utan tidsstämpel Knappen är dold - + Ta bort dialogrutan för diskretion Dialog kommer att tas bort Dialoger är synliga Detta kringgår inte åldersbegränsningen. Det accepterar bara det automatiskt. - + Externa nerladdningar Inställningar för att använda en extern nerladdare Visa extern nerladdningsknapp @@ -347,17 +389,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Paketnamn för din installerade externa nedladdningsapp, till exempel NewPipe eller Seal %s är inte installerat. Installera det. - + Inaktivera exakt sök-gest Gest är inaktiverad Gest är aktiverad - + Aktivera sökfältsknapp Seekbar tryck är aktiverat Seekbar tryck är inaktiverat - + Aktivera ljusstyrkegest Svep för ljusstyrka är aktiverat Svep för ljusstyrka är inaktiverat @@ -386,12 +428,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Svep magnitud tröskel Mängden tröskel för att svepa ska uppstå - + Inaktivera automatisk bildtext Auto-bildtexter är inaktiverade Automatisk bildtext är aktiverad - + Åtgärd knappar Dölj eller visa knappar under videor Dölj gilla och ogilla @@ -427,23 +469,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Spara till spellistan knappen är dold Knappen spara till spellista är synlig - - Dölj knappen för automatisk uppspelning - Knappen för automatisk uppspelning är dold - Knappen för automatisk uppspelning är synlig - - - - Dölj bildtextknapp - Bildtext-knappen är dold - Bildtextsknappen är synlig - - - Dölj cast-knapp - Cast-knappen är dold - Cast-knappen är synlig - - + Navigeringsknappar Dölj eller ändra knappar i navigeringsfältet @@ -470,7 +496,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Etiketter är dolda Etiketter visas - + Flyout-meny Dölj eller visa spelarflyout menyobjekt @@ -481,6 +507,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Dölj ytterligare inställningar Ytterligare inställningsmeny är dold Menyn ytterligare inställning är synlig + + Dölj vilotimer + Vilotidsmenyn är dold + Menyn för vilotid visas Dölj loop-video Loop videomenyn är dold @@ -489,6 +519,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Dölj omgivningsläge Omgivningsläge menyn är dold Menyn för omgivningsläge är synligt + Dölj stabil volym + Stabil volymmeny visas + Stabil volymmeny är dold Dölj hjälp & feedback Hjälp & återkopplingsmenyn är dold @@ -514,83 +547,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Dölj klocka i VR Titta i VR-menyn är dold Titta i VR-menyn är synlig + Dölj sidfot för videokvalitet + Videokvalitets sidfot är dold + Videokvalitets sidfot visas - - Dölj tidigare & nästa videoknappar - Knappar är dolda - Knappar är synliga + + Dölj tidigare & nästa videoknappar + Knappar är dolda + Knappar är synliga + Dölj cast-knapp + Cast-knappen är dold + Cast-knappen är synlig + + Dölj bildtextknapp + Bildtext-knappen är dold + Bildtextsknappen är synlig + Dölj knappen för automatisk uppspelning + Knappen för automatisk uppspelning är dold + Knappen för automatisk uppspelning är synlig - - Dölj albumkort - Skivkorten är dolda - Albumkort är synliga - - - Kommentarer - Dölj eller visa kommentarskomponenter - Dölj \'Kommentarer från medlemmar\' header - \'Kommentarer från medlemmar\' huvudet är dolt - \'Kommentarer från medlemmar\' header visas - Dölj kommentarsfältet - Sektionen för kommentarer är dold - Sektionen för kommentarer visas - Dölj knappen \'Skapa en kort\' - \'Skapa en kort\' knappen är dold - Knappen \'Skapa en kort\' visas - Dölj förhandsgranskningskommentar - Förhandsgranska kommentaren är dold - Förhandsgranska kommentar är synlig - Dölj tack-knappen - Tack-knappen är dold - Tackknappen är synlig - Dölj tidsstämplar och emoji-knappar - Tidsstämpel och emoji-knappar är dolda - Knappar för tidsstämpel och emoji visas - - - Dölj crowdfunding-rutan - Crowdfunding-rutan är dold - Crowdfunding-rutan är synlig - - + Dölj slutskärmskort Slutskärmskort är dolda Slutskärmskort är synliga - - Filterfält - Dölj eller visa filterfältet i flödet, sök och relaterade videor - Dölj i flöde - Dold i flödet - Visas i flöde - Dölj i sökningen - Dold i sökningen - Visas i sök - Dölj i relaterade videor - Dold i relaterade videor - Visas i relaterade videor - - - Dölj flytande mikrofonknapp - Mikrofon knapp dold - Mikrofonknappen är synlig - - + Inaktivera omgivningsläge i helskärm Omgivningsläge inaktiverat Omgivningsläge aktiverat - + Dölj infokort Informationskort är dolda Informationskort är synliga - + Inaktivera animationer med rullande nummer Rullande nummer är inte animerade Rullande nummer är animerade - + Dölj sökfältet i videospelare Sökfältet för videospelare är dolt Sökfältet för videospelaren är synlig @@ -598,7 +594,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Miniatyrsökningsfältet är dolt Miniatyrens sökfält är synlig - + + Shorts spelare + Dölj eller visa komponenter i Shorts spelaren Dölj Shorts i hemmatningsflödet Shorts i hemmatningsflödet är dolda @@ -696,27 +694,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Navigeringsfältet är dolt Navigeringsfältet visas - + Inaktivera föreslagen video slutskärm Föreslagna videor kommer att inaktiveras Föreslagen video kommer att visas - + Dölj videotidsstämpel Tidsstämpel är dold Tidsstämpel visas - + Dölj popup-paneler för spelare Spelare popup-paneler är dolda Spelarens popup-paneler visas - + Överlagring av spelarens opacitet Opacitetsvärde mellan 0-100, där 0 är transparent Överlagrad spelaropacitet måste vara mellan 0-100 - + Dislikes inte tillgängligt (API timed out) Gillar inte tillgänglig (status %d) @@ -760,17 +758,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Gräns för kundfrekvens påträffad %d gånger %d millisekunder - + Aktivera brett sökfält Brett sökfält är aktiverat Brett sökfält är inaktiverat - + + Aktivera högkvalitativa miniatyrer + Seekbar miniatyrer är av hög kvalitet + Seekbar miniatyrer är av medelhög kvalitet + Fullskärm sökfält miniatyrer är av hög kvalitet + Fullskärm sökfält miniatyrer är medelhög kvalitet + Detta kommer också att återställa miniatyrer på livestreams som inte har sökfält miniatyrer.\n\nSeekbar miniatyrer kommer att använda samma kvalitet som den aktuella videon.\n\nDen här funktionen fungerar bäst med en videokvalitet på 720p eller lägre och när du använder en mycket snabb internetanslutning. Återställ gamla miniatyrer i sökfältet Seekbar miniatyrer visas ovanför sökfältet Seekbar miniatyrer visas i helskärm - + Aktivera SponsorBlock SponsorBlock är en crowd-sourced system för att hoppa över irriterande delar av YouTube-videor Utseende @@ -951,7 +955,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Om Data tillhandahålls av SponsorBlock API. Tryck här för att läsa mer och se nedladdningar för andra plattformar - + Spoof app-version Version förfalskad Version inte förfalskad @@ -964,9 +968,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Återställ videons hastighet & kvalitetsmeny 18.09.39 - Återställ biblioteksfliken 17.41.37 - Återställ gamla spellisthyllor - 17.33.42 - Återställ gamla användargränssnitt - + Ställ in startsida Standard Bläddra bland kanaler @@ -984,18 +987,26 @@ This is because Crowdin requires temporarily flattening this file and removing t Trendande Titta senare - + Inaktivera återupptagande av Shorts spelare Shorts spelare kommer inte att återupptas vid appstart Shorts spelare kommer att återupptas vid appstart - + + Autoplay Shorts + Shorts kommer automatiskt spela upp + Shorts kommer att upprepas + Autoplay Shorts bakgrund spela + Shorts bakgrundsuppspelning kommer automatiskt spela + Shorts bakgrundsuppspelning upprepas + + Aktivera surfplattans layout Surfplattans layout är aktiverad Surfplattans layout är inaktiverad Gemenskapsinlägg visas inte på surfplattans layouter - + Minispelare Ändra stilen på appen minimerad spelare Miniplayer typ @@ -1014,6 +1025,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Aktivera dra och släpp Dra och släpp är aktiverat\n\nMiniplayer kan dras till valfritt hörn av skärmen Dra och släpp är inaktiverat + Aktivera horisontell drag gest + Horisontell drag gesture aktiverad\n\nMiniplayer kan dras från skärmen till vänster eller höger + Horisontell drag gest inaktiverad Dölj stängningsknappen Stäng knappen är dold Stäng knappen visas @@ -1033,12 +1047,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Opacitetsvärde mellan 0-100, där 0 är transparent Miniplayer overlay opacitet måste vara mellan 0-100 - + Aktivera gradient laddar skärmen Laddar skärmen kommer att ha en lutande bakgrund Laddar skärmen kommer att ha en solid bakgrund - + Aktivera anpassad sökfält färg Anpassad sökfält färg visas Original sökfält färg visas @@ -1046,12 +1060,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Färgen på sökfältet Ogiltigt sökfältets färgvärde - + Begränsningar för förbipassering av bildregionen Använder bildvärd yt4.ggpht.com Använda ursprungliga bildvärden\n\nAktivering av detta kan åtgärda saknade bilder som är blockerade i vissa regioner - + Hemflik @@ -1083,7 +1097,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow tillfälligt inte tillgängligt (status %s) DeArrow tillfälligt inte tillgänglig - + Visa vansade notiser Meddelanden visas vid start Meddelanden visas inte vid start @@ -1091,47 +1105,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Det gick inte att ansluta till meddelandeleverantören Avfärda - + Varningar Din klockhistorik sparas inte.<br><br>Detta troligen orsakas av en DNS-annonsblockerare eller nätverksproxy.<br><br>För att åtgärda detta, whitelist <b>s.youtube.com</b> eller stäng av alla DNS-blockerare och proxyer. Visa inte igen - + Aktivera automatisk upprepning Automatisk upprepning är aktiverad Automatisk upprepning är inaktiverad - + Enhetsdimensioner för Spoof Enhetsdimensioner förfalskade\n\nHögre videokvaliteter kan vara upplåsta men du kan uppleva uppspelning av videouppspelning, sämre batteritid och okända biverkningar Enhetsdimensioner som inte är förfalskade\n\nAktivering av detta kan låsa upp högre videokvaliteter Att aktivera detta kan orsaka uppspelning av videouppspelning, sämre batteritid och okända biverkningar. - + GmsCore inställningar Inställningar för GmsCore - + Bypass URL omdirigerar URL-omdirigeringar förbigås URL-omdirigeringar förbigås inte - + Öppna länkar i webbläsaren Öppna länkar externt Öppna länkar i appen - + Ta bort spårningsfrågeparameter Spårnings frågeparameter har tagits bort från länkar Spårnings frågeparameter har inte tagits bort från länkar - + Inaktivera zoomhaptik Haptikerna är inaktiverade Haptikerna är aktiverade - + Automatisk kvalitet Kom ihåg förändringar i videokvaliteten Kvalitetsändringar gäller för alla videor @@ -1142,35 +1156,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Ändrade standardkvalitet %1$s till: %2$s - + Visa hastighetsdialogruta knapp Knappen är synlig Knappen är dold - + + Anpassad meny för uppspelningshastighet + Anpassad hastighetsmeny visas + Anpassad hastighetsmeny visas inte Anpassade uppspelningshastigheter - Lägg till eller ändra tillgängliga uppspelningshastigheter + Lägg till eller ändra de anpassade uppspelningshastigheterna Anpassade hastigheter måste vara mindre än %s. Använda standardvärden. Ogiltig anpassad uppspelningshastighet. Använda standardvärden. - + Kom ihåg ändringar i uppspelningshastighet Ändring av uppspelningshastighet gäller för alla videor Ändring av uppspelningshastighet gäller endast för den aktuella videon Standarduppspelningshastighet Ändrade standardhastigheten till: %s - + Återställ gamla videokvalitetsmenyn Gammal videokvalitetsmeny visas Gamla videokvalitetsmenyn visas inte - + Aktivera glid för att söka Glid för att söka är aktiverat Glid för att söka är inaktiverat - + Spoof videoströmmar Spoof klientens videoströmmar för att förhindra uppspelningsproblem Spoof videoströmmar @@ -1188,20 +1205,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Android VR förfalskning biverkningar • Ljudspårsmenyn saknar\n• Stabil volym är inte tillgänglig - - - Aktivera automatisk HDR-ljusstyrka - Automatisk HDR-ljusstyrka är aktiverad - Automatisk HDR-ljusstyrka är inaktiverad - - + Blockera ljudannonser Ljudannonser är blockerade Ljudannonser är inte blockerade - + %s är inte tillgänglig. Annonser kan visa. Försök att byta till en annan annonsblockstjänst i inställningarna. %s server returnerade ett fel. Annonser kan visa. Försök att byta till en annan annonsblockstjänst i inställningarna. Blockera inbäddade videoannonser @@ -1209,30 +1220,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Lysande proxy PurpleAdBlock proxy - + Blockera videoannonser Videoannonser är blockerade Videoannonser är inte blockerade - + meddelande borttaget Visa borttagna meddelanden Visa inte borttagna meddelanden Dölj borttagna meddelanden bakom en spoiler Visa borttagna meddelanden som överstruken text - + Gör anspråk på kanalpoäng automatiskt Kanalpoäng hämtas automatiskt Kanalpoäng hämtas inte automatiskt - + Aktivera felsökningsläge för Twitch Felsökningsläget på Twitch är aktiverat (rekommenderas inte) Twitch-felsökningsläget är inaktiverat - + ReVanced-inställningar Annonser Inställningar för annonsblockering diff --git a/patches/src/main/resources/addresources/values-sw-rKE/strings.xml b/patches/src/main/resources/addresources/values-sw-rKE/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-sw-rKE/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/addresources/values-ta-rIN/strings.xml b/patches/src/main/resources/addresources/values-ta-rIN/strings.xml similarity index 72% rename from src/main/resources/addresources/values-ta-rIN/strings.xml rename to patches/src/main/resources/addresources/values-ta-rIN/strings.xml index 6305be548..aa5e83d08 100644 --- a/src/main/resources/addresources/values-ta-rIN/strings.xml +++ b/patches/src/main/resources/addresources/values-ta-rIN/strings.xml @@ -32,9 +32,9 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + நீங்கள் தொடர விரும்புகிறீரா? மறுஅமை புதுப்பித்து மீண்டும் துவங்கு @@ -43,17 +43,19 @@ This is because Crowdin requires temporarily flattening this file and removing t நகலெடு - + - + இதைப் பற்றி - + - + + + @@ -69,30 +71,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -102,23 +104,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -129,29 +125,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -159,26 +146,26 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + இதைப் பற்றி - + - + - + தோற்றம் @@ -187,85 +174,84 @@ This is because Crowdin requires temporarily flattening this file and removing t மறுஅமை இதைப் பற்றி - + - + இயல்புநிலை - + - + - + - + - + - + - + + + - + - + எச்சரிக்கை - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + முடக்கப்பட்டது - + - + - + - + - + diff --git a/src/main/resources/addresources/values-te-rIN/strings.xml b/patches/src/main/resources/addresources/values-te-rIN/strings.xml similarity index 70% rename from src/main/resources/addresources/values-te-rIN/strings.xml rename to patches/src/main/resources/addresources/values-te-rIN/strings.xml index be82d6b2f..0099c7335 100644 --- a/src/main/resources/addresources/values-te-rIN/strings.xml +++ b/patches/src/main/resources/addresources/values-te-rIN/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,106 +139,105 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + హెచ్చరిక - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/src/main/resources/addresources/values-th-rTH/strings.xml b/patches/src/main/resources/addresources/values-th-rTH/strings.xml similarity index 81% rename from src/main/resources/addresources/values-th-rTH/strings.xml rename to patches/src/main/resources/addresources/values-th-rTH/strings.xml index 58b62a717..0baa27a65 100644 --- a/src/main/resources/addresources/values-th-rTH/strings.xml +++ b/patches/src/main/resources/addresources/values-th-rTH/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,25 +139,25 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + เปิดใช้งาน SponsorBlock SponsorBlock เป็นระบบที่ใครๆก็เข้าถึงได้ ใช้สำหรับการข้ามส่วนต่างๆของวิดีโอที่คุณไม่ต้องการ หน้าตา @@ -246,83 +233,82 @@ This is because Crowdin requires temporarily flattening this file and removing t สีเปลื่ยนเป็นค่าเริ่มต้นแล้ว รีเซ็ต - + - + ค่าเริ่มต้น - + - + - + - + - + - + - + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/src/main/resources/addresources/values-tr-rTR/strings.xml b/patches/src/main/resources/addresources/values-tr-rTR/strings.xml similarity index 90% rename from src/main/resources/addresources/values-tr-rTR/strings.xml rename to patches/src/main/resources/addresources/values-tr-rTR/strings.xml index 056dfbc3c..d63b9310e 100644 --- a/src/main/resources/addresources/values-tr-rTR/strings.xml +++ b/patches/src/main/resources/addresources/values-tr-rTR/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Kontroller başarısız Resmi websiteyi aç Yok say @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t %s gün önce yamalanmış APK derleme tarihi bozuk - + Devam etmek istiyor musunuz? Sıfırla Yenile ve yeniden başlat @@ -62,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Resmi bağlantılar Bağış yap - + MicroG GmsCore yüklü değil. Yükleyin. Eylem gerekiyor @@ -73,7 +73,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Hakkında Reklamlar Alternatif kapak fotoğrafları @@ -85,7 +85,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Çeşitli Video - + + Shorts\'un arka planda oynatılmasını devre dışı bırak + Shorts\'un arka planda oynatılması devre dışı + Shorts\'un arka planda oynatılması etkin + + Debugging Debugging seçeneklerini etkinleştir veya devre dışı bırak Debug logları @@ -102,13 +107,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Hata oluştuğunda tost bildirimi gösterilmiyor Hata tost bildirimlerini gizlemek bütün ReVanced hata bildirimlerini gizler.\n\nBeklenmeyen olaylar hakkında bilgilendirilmeyeceksiniz. - + Beğen / Abone ol düğmesi parlamasını devre dışı bırak Beğen ve abone ol düğmesi bahsedildiğinde parlamayacak Beğen ve abone ol düğmesi bahsedildiğinde parlayacak - Gri ayırıcıları gizle - Gri ayırıcılar gizleniyor - Gri ayırıcılar gösteriliyor + Albüm kartlarını gizle + Albüm kartları gizleniyor + Albüm kartları gösteriliyor + Bağış etkinliklerini gizle + Bağış etkinliği kutuları gizleniyor + Bağış etkinliği kutuları gösteriliyor + Alttaki mikrofon butonunu gizle + Alttaki mikrofon butonu gizleniyor + Alttaki mikrofon butonu gösteriliyor Kanal filigranını gizle Filigran gizleniyor Filigran gösteriliyor @@ -153,9 +164,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Videoların altındaki genişletilebilir çentiği gizle Genişletilebilir çentikler gizleniyor Genişletilebilir çentikler gösteriliyor - Video kalite menüsü alt bilgisini gizle - Video kalite menüsü alt bilgisi gizleniyor - Video kalite menüsü alt bilgisi gösteriliyor Topluluk gönderilerini gizle Topluluk gönderileri gizleniyor Topluluk gönderileri gösteriliyor @@ -230,6 +238,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Transkript kısmı gösteriliyor Video açıklaması Video açıklamasındaki öğeleri gizle veya göster + Filtreleme çubuğu + Akışta, arama sonuçlarında ve ilgili videolardaki filtreleme çubuğunu gizle veya göster + Akıştakini gizle + Akıştaki gizleniyor + Akıştaki gösteriliyor + Arama sonuçlarındakini gizle + Arama sonuçlarındaki gizleniyor + Arama sonuçlarındaki gösteriliyor + Alakalı videolardakini gizle + Alakalı videolardaki gizleniyor + Alakalı videolardaki gösteriliyor + Yorumlar + Yorumlar kısmındaki öğeleri gizle veya göster + \'Üyeler tarafından yapılan yorumlar\' başlığını gizle + \'Üyeler tarafından yapılan yorumlar\' başlığı gizli + \'Üyeler tarafından yapılan yorumlar\' başlığı görünür + Yorumlar kısmını gizle + Yorumlar kısmı gizli + Yorumlar kısmı görünür + \'Short oluştur\' düğmesini gizle + \'Short oluştur\' düğmesi gizli + \'Short oluştur\' düğmesi görünür + Önizlenen yorumu gizle + Önizlenen yorum gizleniyor + Önizlenen yorum gösteriliyor + Teşekkürler butonunu gizle + \"Teşekkürler\" butonu gizleniyor + \"Teşekkürler\" butonu gösteriliyor + Zaman damgasını ve emoji butonlarını gizle + Zaman damgası ve emoji düğmeleri gizli + Zaman damgası ve emoji düğmeleri görünür YouTube Doodle\'larını gizle Arama çubuğu Doodle\'ları gizli @@ -271,7 +310,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Kelime kısa, tırnak içine alın: %s Anahtar kelime bütün videoları gizler: %s - + Genel reklamları gizle Genel reklamlar gizleniyor Genel reklamlar gösteriliyor @@ -290,6 +329,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Ürünleri görme afişini gizle Afiş gizleniyor Afiş gösteriliyor + Oynatıcı alışveriş rafını gizle + Alışveriş rafı gizli + Alışveriş rafı görünür Video açıklamasındaki alışveriş bağlantılarını gizle Alışveriş bağlantıları gizleniyor Alışveriş bağlantıları gösteriliyor @@ -306,17 +348,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Tam ekran reklamlarını gizleme yalnızca eski cihazlarda çalışır - + YouTube Premium promosyonlarını gizle Videonun altındaki YouTube Premium promosyonları gizleniyor Videonun altındaki YouTube Premium promosyonları gösteriliyor - + Video reklamlarını gizle Video reklamları gizleniyor Video reklamları gösteriliyor - + URL panoya kopyalandı Zaman damgalı URL kopyalandı Video URL\'sini kopyalama butonunu göster @@ -326,13 +368,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Düğme görünür. Zamanlı video URLsini kopyalamak için dokunun. Zaman olmadan kopyalamak için basılı tutun Buton gösterilmiyor - + İzleyici takdiri iletişim kutusunu kaldır İletişim kutusu kaldırılacak İletişim kutusu gösterilecek Bu, yaş kısıtlamasını atlamaz. Sadece otomatik olarak kabul eder. - + Harici indirmeler Harici indirici kullanmak için ayarlar Harici indirme düğmesini göster @@ -346,17 +388,17 @@ This is because Crowdin requires temporarily flattening this file and removing t NewPipe veya Seal gibi yüklü olan harici indirici uygulamanızın paket adı %s yüklü değil. Lütfen yükleyin. - + Hassas sarma hareketini devre dışı bırak Hareket devre dışı Hareket etkin - + Zaman çubuğuna dokunmayı etkinleştir Zaman çubuğuna dokunma etkin Zaman çubuğuna dokunma devre dışı - + Parlaklık hareketini etkinleştir Parlaklık kaydırma hareketi etkin Parlaklık kaydırma hareketi devre dışı @@ -385,12 +427,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Kaydırma büyüklük eşiği Kaydırma işleminin gerçekleşmesi için eşik miktarı - + Altyazıların kendiliğinden açılmasını devre dışı bırak Altyazılar kendiliğinden açılmıyor Altyazılar kendiliğinden açılabilir - + Eylem butonları Videonun altındaki butonları gizle veya göster \"Beğen\" ve \"Beğenme\" butonlarını gizle @@ -426,23 +468,7 @@ This is because Crowdin requires temporarily flattening this file and removing t \"Kaydet\" butonu gizleniyor \"Kaydet\" butonu gösteriliyor - - Otomatik oynatma butonunu gizle - Otomatik oynatma butonu gizleniyor - Otomatik oynatma butonu gösteriliyor - - - - Altyazı butonunu gizle - Altyazı butonu gizleniyor - Altyazı butonu gösteriliyor - - - Yansıtma butonunu gizle - Yansıtma butonu gizleniyor - Yansıtma butonu gösteriliyor - - + Gezinme butonları Gezinme çubuğundaki butonları gizle veya değiştir @@ -469,7 +495,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Etiketler gizli Etiketler görünür - + Açılır menü Oynatıcı açılır menü öğelerini gizle veya göster @@ -480,6 +506,10 @@ This is because Crowdin requires temporarily flattening this file and removing t \"Ek ayarlar\" butonunu gizle \"Ek ayarlar\" butonu gizleniyor \"Ek ayarlar\" butonu gösteriliyor + + Uyku zamanlayıcısını gizle + Uyku zamanlayıcısı menüsü gizli + Uyku zamanlayıcısı menüsü görünür \"Videoyu döngüye al\" butonunu gizle \"Videoyu döngüye al\" butonu gizleniyor @@ -488,6 +518,9 @@ This is because Crowdin requires temporarily flattening this file and removing t \"Ambiyans modu\" butonunu gizle \"Ambiyans modu\" butonu gizleniyor \"Ambiyans modu\" butonu gösteriliyor + Sabit sesi gizle + Sabit ses menüsü görünür + Sabit ses menüsü gizli \"Yardım ve geri bildirim\" butonunu gizle \"Yardım ve geri bildirim\" butonu gizleniyor @@ -513,83 +546,46 @@ This is because Crowdin requires temporarily flattening this file and removing t \"VR modunda izle\" butonunu gizle \"VR modunda izle\" butonu gizleniyor \"VR modunda izle\" butonu gösteriliyor + Video kalite menüsü alt bilgisini gizle + Video kalite menüsü alt bilgisi gizli + Video kalite menüsü alt bilgisi görünür - - Önceki ve Sonraki video butonlarını gizle - Önceki ve Sonraki video butonları gizleniyor - Önceki ve Sonraki video butonları gösteriliyor + + Önceki ve Sonraki video butonlarını gizle + Önceki ve Sonraki video butonları gizleniyor + Önceki ve Sonraki video butonları gösteriliyor + Yansıtma butonunu gizle + Yansıtma butonu gizleniyor + Yansıtma butonu gösteriliyor + + Altyazı butonunu gizle + Altyazı butonu gizleniyor + Altyazı butonu gösteriliyor + Otomatik oynatma butonunu gizle + Otomatik oynatma butonu gizleniyor + Otomatik oynatma butonu gösteriliyor - - Albüm kartlarını gizle - Albüm kartları gizleniyor - Albüm kartları gösteriliyor - - - Yorumlar - Yorumlar kısmındaki öğeleri gizle veya göster - \'Üyeler tarafından yapılan yorumlar\' başlığını gizle - \'Üyeler tarafından yapılan yorumlar\' başlığı gizli - \'Üyeler tarafından yapılan yorumlar\' başlığı görünür - Yorumlar kısmını gizle - Yorumlar kısmı gizli - Yorumlar kısmı görünür - \'Short oluştur\' düğmesini gizle - \'Short oluştur\' düğmesi gizli - \'Short oluştur\' düğmesi görünür - Önizlenen yorumu gizle - Önizlenen yorum gizleniyor - Önizlenen yorum gösteriliyor - Teşekkürler butonunu gizle - \"Teşekkürler\" butonu gizleniyor - \"Teşekkürler\" butonu gösteriliyor - Zaman damgasını ve emoji butonlarını gizle - Zaman damgası ve emoji düğmeleri gizli - Zaman damgası ve emoji düğmeleri görünür - - - Bağış etkinliklerini gizle - Bağış etkinliği kutuları gizleniyor - Bağış etkinliği kutuları gösteriliyor - - + Bitiş ekranı kartlarını gizle Bitiş ekranı kartları gizleniyor Bitiş ekranı kartları gösteriliyor - - Filtreleme çubuğu - Akışta, arama sonuçlarında ve ilgili videolardaki filtreleme çubuğunu gizle veya göster - Akıştakini gizle - Akıştaki gizleniyor - Akıştaki gösteriliyor - Arama sonuçlarındakini gizle - Arama sonuçlarındaki gizleniyor - Arama sonuçlarındaki gösteriliyor - Alakalı videolardakini gizle - Alakalı videolardaki gizleniyor - Alakalı videolardaki gösteriliyor - - - Alttaki mikrofon butonunu gizle - Alttaki mikrofon butonu gizleniyor - Alttaki mikrofon butonu gösteriliyor - - + Tam ekranda ambiyans modunu devre dışı bırak Tam ekranda ambiyans modu devre dışı Tam ekranda ambiyans modu etkin - + Bilgi kartlarını gizle Bilgi kartları gizleniyor Bilgi kartları gösteriliyor - + Kayan sayı animasyonlarını devre dışı bırak Kayan sayı animasyonu kapalı Kayan sayı animasyonu açık - + Video oynatıcıdaki zaman çubuğunu gizle Video oynatıcıdaki zaman çubuğu gizleniyor Video oynatıcıdaki zaman çubuğu gösteriliyor @@ -597,7 +593,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Video kapak fotoğraflarındaki zaman çubuğu gizleniyor Video kapak fotoğraflarındaki zaman çubuğu gösteriliyor - + + Shorts oynatıcı + Shorts oynatıcısındaki bileşenleri gizleyin veya gösterin Ana Sayfa sekmesinde Shorts videolarını gizle Ana Sayfa sekmesinde Shorts videoları gizleniyor @@ -644,6 +642,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Yeşil ekran düğmesini gizle Yeşil ekran düğmesi gizli Yeşil ekran düğmesi görünür + Hashtag düğmesini gizle + Hashtag düğmesi gizli + Hashtag düğmesi görünür Arama önerilerini gizle Arama önerileri gizleniyor Arama önerileri gösteriliyor @@ -692,27 +693,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Gezinme çubuğu gizleniyor Gezinme çubuğu gösteriliyor - + Önerilen video bitiş ekranını gizle Önerilen videolar devre dışı bırakılacak Önerilen videolar gösterilecek - + Video zaman damgasını gizle Video zaman damgası gizleniyor Video zaman damgası gösteriliyor - + Oynatıcı açılır panellerini gizle Oynatıcı açılır panelleri gizleniyor Oynatıcı açılır panelleri gösteriliyor - + Oynatıcı opaklığı 0-100 arasında opaklık değeri, 0 şeffaftır Oynatıcı şeffaflığı 0-100 arasında olmalıdır - + Beğenmeme sayıları zaman aşımına uğradı Beğenmeme sayıları kullanılamıyor (durum %d) @@ -756,17 +757,23 @@ This is because Crowdin requires temporarily flattening this file and removing t %d defa istemci sınırıyla karşılaşıldı %d milisaniye - + Geniş arama çubuğunu etkinleştir Geniş arama çubuğu etkin Geniş arama çubuğu devre dışı - + + Yüksek kalite küçük resimleri etkinleştir + Zaman çubuğu küçük resimleri yüksek kalitede + Zaman çubuğu küçük resimleri orta kalitede + Tam ekran zaman çubuğu küçük resimleri yüksek kalitede + Tam ekran zaman çubuğu küçük resimleri orta kalitede + Bu aynı zamanda zaman çubuğu küçük resimleri olmayan canlı yayınlar için küçük resimleri geri getirecektir.\n\nZaman çubuğu küçük resimleri, şu anki video ile aynı kaliteyi kullanacaktır.\n\nBu özellik 720p video kalitesinde ve çok hızlı bir internet bağlantısında en iyi şekilde çalışır. Eski zaman çubuğu küçük resimlerini geri getir Zaman çubuğu küçük resimleri zaman çubuğunun üzerinde görünecek Zaman çubuğu küçük resimleri tam ekran olarak gösterilecek - + SponsorBlock\'u etkinleştir SponsorBlock, YouTube videolarındaki sıkıcı bölümleri atlamaya yarayan kitle kaynaklı bir sistemdir Görünüm @@ -947,7 +954,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Hakkında Veri, SponsorBlock APIsi tarafından sağlanıyor. Daha fazla öğrenmek için ve diğer platformlar için indirmeleri görmek için dokunun - + Uygulama sürümünü taklit et Sürüm taklit ediliyor Sürüm taklit edilmiyor @@ -960,31 +967,45 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Geniş video hızı & kalite menüsünü geri getir 18.08.39 - Kitaplık sekmesini geri getir 17.41.37 - Eski oynatma listesi rafını geri getir - 17.33.42 - Eski arayüzü geri getir - + Başlangıç sayfasını seç Varsayılan + Kanallara göz at Keşfet + Oyun Geçmiş Kitaplık Beğenilen videolar + Canlı yayın + Filmler + Müzik Arama + Spor Abonelikler Trendler + Daha sonra izle - + Shorts oynatıcıya devam edilmesini devre dışı bırak Shorts oynatıcı açılışta devam etmeyecek Shorts oynatıcı açılışta devam edecek - + + Shorts\'u otomatik oynat + Sıradaki Shorts videosu otomatik olarak oynatılacak + Aynı Shorts videosu sürekli döngü yapacak + Arka planda Shorts\'u otomatik oynat + Shorts arka planda otomatik oynatılacak + Shorts arka planda döngüde olacak + + Tablet düzenini etkinleştir Tablet düzeni etkin Tablet düzeni devre dışı Topluluk gönderileri tablet düzeninde görünmez - + Mini oynatıcı Uygulama içi küçültülmüş oynatıcının tarzını değiştir Mini oynatıcı tipi @@ -994,23 +1015,43 @@ This is because Crowdin requires temporarily flattening this file and removing t Modern 1 Modern 2 Modern 3 + Yuvarlatılmış köşeleri etkinleştir + Köşeler yuvarlatılmış + Köşeler kare şeklinde + Boyutlandırmak için çift dokunmayı ve sıkıştırmayı etkinleştir + Boyutlandırmak için çift dokunma ve sıkıştırma etkin\n\n•Mini oynatıcıyı büyütmek için çift dokunun\n• Orijinal boyuta dönmek için tekrar çift dokunun + Boyutlandırmak için çift dokunma ve sıkıştırma devre dışı + Sürükleyip bırakmayı etkinleştir + Sürükleyip bırakma etkin\n\nMini oynatıcı ekranın herhangi bir köşesine sürüklenebilir + Sürükleyip bırakma devre dışı + Yatay sürükleme hareketini etkinleştir + Yatay sürükleme hareketi etkin\n\nMini oynatıcı ekranın dışına doğru sağa veya sola sürüklenebilir + Yatay sürükleme hareketi devre dışı + Kapatma düğmesini gizle + Kapatma düğmesi gizli + Kapatma düğmesi görünür Büyüt ve kapat düğmelerini gizle + Düğmeler gizli\n\nBüyütmek veya kapatmak için kaydırın + Büyütme ve kapatma düğmeleri görünür Alt metinleri gizle Alt metinler gizli Alt metinler görünür İleri/geri atlama düğmelerini gizle İleri/geri atlama düğmeleri gizli İleri/geri atlama düğmeleri görünür + Başlangıç boyutu + Başlangıçtaki boyut, piksel cinsinden + Piksel boyutu %1$s ve %2$s arasında olmalıdır Katman şeffaflığı 0-100 arasında opaklık değeri, 0 şeffaftır Katman şeffaflığı 0-100 arasında olmalıdır - + Gradyan yükleme ekranını etkinleştir Yükleme ekranı gradyan bir arka plana sahip olacak Yükleme ekranı düz bir arka plana sahip olacak - + Özel zaman çubuğu rengini etkinleştir Özel zaman çubuğu rengi gösteriliyor Orijinal zaman çubuğu rengi gösteriliyor @@ -1018,12 +1059,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Zaman çubuğunun rengi Geçersiz zaman çubuğu renk değeri - + Resimlerin bölge kısıtlamalarını atla yt4.ggpht.com resim sunucusu kullanılıyor Orijinal resim sunucusu kullanılıyor\n\nBunu etkinleştirmek bazı bölgelerde engellenen eksik resimleri düzeltebilir - + \"Ana Sayfa\" sekmesinde @@ -1055,7 +1096,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow geçici olarak kullanılamıyor (durum kodu: %s) DeArrow geçici olarak kullanılamıyor - + ReVanced duyurularını göster Başlangıçta duyurular gösteriliyor Başlangıçta duyurular gösterilmiyor @@ -1063,47 +1104,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Duyuru sağlayıcısına bağlanılamadı Yoksay - + Uyarı İzleme geçmişiniz kaydedilmiyor.<br><br>Bu büyük ihtimalle bir reklam engelleyici DNS\'den veya proxy\'den kaynaklanıyor.<br><br>Bunu düzeltmek için, <b>s.youtube.com</b> adresini beyaz listeye ekleyin veya bütün engelleyici DNSleri ve proxy\'leri kapatın. Bir daha gösterme - + Otomatik tekrarı etkinleştir Otomatik tekrar etkin Otomatik tekrar devre dışı - + Cihaz boyutlarını taklit et Cihaz boyutları taklit ediliyor\n\nDaha yüksek video kaliteleri açılabilir ancak video oynatma takılmaları, daha kötü pil ömrü, ve bilinmeyen yan etkiler yaşayabilirsiniz Cihaz boyutları taklit edilmiyor\n\nBunu etkinleştirmek daha yüksek video kalitelerini açabilir Bunu etkinleştirmek video oynatma takılmaları, daha kötü pil ömrü, ve bilinmeyen yan etkiler yaşamanıza sebep olabilir. - + GmsCore ayarları GmsCore için ayarlar - + URL yönlendirmelerini atla URL yönlendirmeleri atlanıyor URL yönlendirmeleri atlanmıyor - + Bağlantıları tarayıcıda aç Bağlantılar harici olarak açılıyor Bağlantılar uygulamada açılıyor - + İzleyici sorgu parametresini kaldır Bağlantılardan izleyici sorgu parametresi kaldırılıyor Bağlantılardan izleyici sorgu parametresi kaldırılmıyor - + Yakınlaştırırken titreşimi devre dışı bırak Titreşim devre dışı Titreşim etkin - + Otomatik kalite Video kalitesi değişikliklerini hatırla Kalite değişiklikleri tüm videolara uygulanacak @@ -1114,35 +1155,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Varsayılan %1$s kalitesi, %2$s olarak değiştirildi - + Hız diyaloğu düğmesini göster Düğme gösteriliyor Buton gösterilmiyor - + + Özel oynatma hızı menüsü + Özel oynatma hızı menüsü görünür + Özel oynatma hızı menüsü görünmez Özel oynatma hızları - Yeni oynatma hızları ekle veya mevcut olanları değiştir + Özel oynatma hızları ekle veya değiştir Özel hızlar %s\'dan az olmalıdır. Varsayılanlar kullanılıyor. Geçersiz özel oynatma hızları. Varsayılanlar seçildi. - + Oynatma hızı değişikliklerini hatırla Oynatma hızı değişiklikleri tüm videolara uygulanacak Oynatma hızı değişiklikleri yalnızca geçerli videoya uygulanacak Varsayılan oynatma hızı Varsayılan hız %s olarak değiştirildi - + Eski video kalite menüsünü geri getir Eski video kalite menüsü gösteriliyor Eski video kalite menüsü gösterilmiyor - + Kaydırarak sardırmayı etkinleştir Kaydırarak sardırma etkin Kaydırarak saydırma etkin değil - + Video akışlarını taklit et Oynatma sorunlarını önlemek için istemci video akışlarını taklit et Video akışlarını taklit et @@ -1160,17 +1204,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Android VR taklidi yan etkileri • Ses parçası menüsü eksik\n• Sabit ses kullanılamaz - - - - + Ses reklamlarını engelle Ses reklamları engelleniyor Ses reklamları engellenmiyor - + %s kullanılamıyor. Reklamlar görünebilir. Ayarlardan başka bir reklam engelleme hizmetine geçmeyi deneyin. %s sunucusu hata verdi. Reklamlar görünebilir. Ayarlardan başka bir reklam engelleme hizmetine geçmeyi deneyin. Gömülü video reklamlarını engelle @@ -1178,30 +1219,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Luminous proxy PurpleAdBlock proxy - + Video reklamlarını engelle Video reklamları engelleniyor Video reklamları engellenmiyor - + mesaj silindi Silinen mesajları göster Silinen mesajları gösterme Silinen mesajları spoiler ile gizle Silinen mesajları üstü çizilmiş olarak göster - + Kanal Puanlarını otomatik olarak topla Kanal Puanları otomatik olarak toplanıyor Kanal Puanları otomatik olarak toplanmıyor - + Twitch debug modunu etkinleştir Twitch debug modu etkin (önerilmez) Twitch debug modu devre dışı - + ReVanced Ayarları Reklamlar Reklam engelleme ayarları diff --git a/src/main/resources/addresources/values-uk-rUA/strings.xml b/patches/src/main/resources/addresources/values-uk-rUA/strings.xml similarity index 92% rename from src/main/resources/addresources/values-uk-rUA/strings.xml rename to patches/src/main/resources/addresources/values-uk-rUA/strings.xml index b5bba6597..1fe0690e3 100644 --- a/src/main/resources/addresources/values-uk-rUA/strings.xml +++ b/patches/src/main/resources/addresources/values-uk-rUA/strings.xml @@ -32,18 +32,18 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Перевірка не вдалася Відкрити офіційний сайт Ігнорувати - <h5>Схоже, що цей застосунок був пропатчений не Вами.</h5><br>Цей застосунок може працювати не належним чином, <b>бути шкідливим або навіть небезпечним</b>.<br><br>Ці перевірки означають, що цей застосунок був попередньо пропатчений та отриманий від когось іншого:<br><br><small>%1$s</small><br>Наполегливо рекомендується <b>видалити цей застосунок та пропатчити його самостійно,</b> щоб переконатися, що ви використовуєте перевірений та безпечний застосунок.<p><br>Якщо це попередження проігнорувати, воно буде показано лише двічі. + <h5>Схоже, що цей застосунок був пропатчений не Вами.</h5><br>Він може працювати не належним чином, <b>бути шкідливим або навіть небезпечним</b>.<br><br>Ці перевірки означають, що цей застосунок був пропатчений та отриманий від когось іншого:<br><br><small>%1$s</small><br>Наполегливо рекомендується <b>видалити цей застосунок та пропатчити його самостійно,</b> щоб переконатися, що Ви використовуєте перевірений та безпечний застосунок.<p><br>Якщо це попередження проігнорувати, воно буде показано лише двічі. Пропатчено на іншому пристрої - Не встановлено через ReVanced Manager + Встановлено не через ReVanced Manager Пропатчено більше 10 хвилин тому Пропатчено %s дні(в) тому Дата збірки APK пошкоджена - + Бажаєте продовжити? Скинути Оновити та перезавантажити? @@ -62,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Офіційні посилання Підтримати - + MicroG GmsCore не встановлено. Встановіть його. Потрібна дія @@ -73,7 +73,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Інформація Реклама Альтернативні прев\'ю @@ -85,7 +85,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Різне Відео - + + Вимкнути фонове відтворення Shorts + Фонове відтворення Shorts вимкнуто + Фонове відтворення Shorts увімкнено + + Налагодження Увімкнення або вимкнення параметрів налагодження Журнал налагодження @@ -102,13 +107,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Тост не показується, якщо сталася помилка Вимкнення сповіщень про помилки приховує всі сповіщення про помилки ReVanced.\n\nВи не отримуватимете сповіщення про будь-які несподівані події. - + Вимкнути відблиск кнопок \"Подобається\" / \"Підписатися\" Кнопки \"Подобається\" та \"Підписатися\" не відблискуватимуть при згадуванні Кнопки \"Подобається\" та \"Підписатися\" відблискуватимуть при згадуванні - Приховати сірі роздільники - Сірі роздільники між елементами інтерфейсу приховано - Сірі роздільники між елементами інтерфейсу показуються + Приховати картки альбому + Картки альбому приховано + Картки альбому показуються + Приховати скриню фінансування + Скриньку фінансування приховано + Скринька фінансування показується + Приховати кнопку мікрофона + Плаваючу кнопку мікрофона приховано + Плаваюча кнопка мікрофона показується Приховати водяний знак каналу Водяний знак у нижній частині відеоплеєра приховано Водяний знак у нижній частині відеоплеєра показується @@ -153,9 +164,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Приховати розширювану фішку Розширювані фішки під відео приховано Розширювані фішки під відео показуються - Приховати колонтитул меню якості відео - Нижній колонтитул меню якості відео приховано - Нижній колонтитул меню якості відео показується Приховати публікації спільноти Публікації спільноти приховано Публікації спільноти показуються @@ -230,6 +238,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Секція \"Текст відео\" показується Опис відео Приховати або показувати компоненти опису відео + Панель фільтрів + Приховати або показувати панель фільтрів у стрічці, пошуку та пов\'язаних відео + Приховати панель у стрічці + Панель фільтрів у стрічці приховано + Панель фільтрів у стрічці показується + Приховати панель у пошуку + Панель фільтрів у пошуку приховано + Панель фільтрів у пошуку показується + Приховати панель у пов\'язаних відео + Панель фільтрів у пов\'язаних відео приховано + Панель фільтрів у пов\'язаних відео показується + Коментарі + Приховати або показувати компоненти секції коментарів + Приховати \"Коментарі від спонсорів\" + Заголовок \"Коментарі від спонсорів\" приховано + Заголовок \"Коментарі від спонсорів\" показується + Приховати секцію коментарів + Секцію коментарів приховано + Секція коментарів показується + Приховати \"Створити Short\" + Кнопку \"Створити Short\" приховано + Кнопка \"Створити Short\" показується + Приховати прев\'ю коментар + Прев\'ю коментар в секції коментарів приховано + Прев\'ю коментар в секції коментарів показується + Приховати \"Дякую\" + Кнопку \"Дякую\" приховано + Кнопка \"Дякую\" показується + Приховати мітку часу та емодзі + Кнопки мітки часу та емодзі приховано + Кнопки мітки часу та емодзі показуються Приховати YouTube Doodles Doodles у пошуковій панелі приховано @@ -271,7 +310,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Ключове слово занадто коротке і потребує лапок: %s Ключове слово приховає всі відео: %s - + Приховати загальну рекламу Загальну рекламу приховано Загальна реклама показується @@ -290,6 +329,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Приховати банер для перегляду товарів Банер для перегляду товарів приховано Банер для перегляду товарів показується + Приховати полицю покупок в плеєрі + Полицю покупок в плеєрі приховано + Полиця покупок в плеєрі показується Приховати посилання на покупки Посилання на покупки в описі відео приховано Посилання на покупки в описі відео показуються @@ -306,17 +348,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Приховувати повноекранну рекламу працює тільки зі старими пристроями - + Приховати рекламу YouTube Premium Рекламу YouTube Premium під відеоплеєром приховано Реклама YouTube Premium під відеоплеєром показується - + Приховати відеорекламу Відеорекламу приховано Відеореклама показується - + URL-адресу скопійовано до буфера URL-адресу з міткою часу скопійовано до буфера Кнопка копіювання URL відео @@ -326,13 +368,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Кнопка показується. Натисніть, щоб скопіювати URL-адресу відео з міткою часу. Натисніть і утримуйте, щоб скопіювати URL-адресу без мітки часу відео Кнопка копіювання URL-адреси відео із міткою часу не показується - + Вилучити діалог про неприйнятний контент Діалогове вікно про неприйнятний контент буде видалено Діалогове вікно про неприйнятний контент буде показуватися Це не обходить вікові обмеження, а просто приймає їх автоматично. - + Зовнішній завантажувач Налаштування для використання зовнішнього завантажувача відео Кнопка завантажувача в плеєрі @@ -346,17 +388,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Ім\'я пакета встановленого Вами додатку зовнішнього завантажувача, такого як NewPipe чи YTDLnis %s не встановлено. Встановіть його. - + Вимкнути жест покадрового перемотування Жест для покадрового перемотування вимкнуто Жест для покадрового перемотування ввімкнуто - + Перемотування натисканням на панель Перемотування натисканням на панель прогресу ввімкнуто Перемотування натисканням на панель прогресу вимкнуто - + Зміна яскравості жестом Зміну яскравості жестом по лівій частині відеоплеєра ввімкнуто Зміну яскравості жестом по лівій частині відеоплеєра вимкнуто @@ -385,12 +427,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Поріг величини жесту Мінімальна амплітуда руху, що розпізнається як жест - + Вимкнути автоматичні субтитри Автоматичні субтитри вимкнуто Автоматичні субтитри ввімкнуто - + Кнопки дій Приховати або показувати кнопки дій під відео Приховати \"Подобається\" і \"Не подобається\" @@ -426,23 +468,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Кнопку \"Зберегти\" приховано Кнопка \"Зберегти\" показується - - Приховати \"Автовідтворення\" - Кнопку \"Автовідтворення\" у відеоплеєрі приховано - Кнопка \"Автовідтворення\" у відеоплеєрі показується - - - - Приховати \"Субтитри\" - Кнопку \"Субтитри\" у відеоплеєрі приховано - Кнопка \"Субтитри\" у відеоплеєрі показується - - - Приховати \"Трансляція\" - Кнопку \"Трансляція\" у відеоплеєрі приховано - Кнопка \"Трансляція\" у відеоплеєрі показується - - + Кнопки панелі навігації Приховати або змінити кнопки на панелі навігації @@ -469,7 +495,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Підписи кнопок навігації приховано Підписи кнопок навігації показуються - + Висувне меню Приховати або показувати пункти висувного меню плеєра @@ -480,6 +506,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Приховати \"Додаткові налаштування\" Пункт меню \"Додаткові налаштування\" приховано Пункт меню \"Додаткові налаштування\" показується + + Приховати \"Таймер сну\" + Пункт меню \"Таймер сну\" приховано + Пункт меню \"Таймер сну\" показується Приховати \"Повторювати відео\" Пункт меню \"Повторювати відео\" приховано @@ -488,6 +518,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Приховати \"Кінематографічне освітлення\" Пункт меню \"Кінематографічне освітлення\" приховано Пункт меню \"Кінематографічне освітлення\" показується + Приховати \"Стабілізувати гучність\" + Пункт меню \"Стабілізувати гучність\" показується + Пункт меню \"Стабілізувати гучність\" приховано Приховати \"Довідка й відгуки\" Пункт меню \"Довідка й відгуки\" приховано @@ -513,83 +546,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Приховати \"Дивитись у VR\" Пункт меню \"Дивитись у VR\" приховано Пункт меню \"Дивитись у VR\" показується + Приховати колонтитул меню якості відео + Нижній колонтитул меню якості відео приховано + Нижній колонтитул меню якості відео показується - - Приховати кнопки попереднє та наступне - Кнопки попереднього та наступного відео приховано - Кнопки попереднього та наступного відео показуються + + Приховати кнопки попереднє та наступне + Кнопки попереднього та наступного відео приховано + Кнопки попереднього та наступного відео показуються + Приховати \"Трансляція\" + Кнопку \"Трансляція\" у відеоплеєрі приховано + Кнопка \"Трансляція\" у відеоплеєрі показується + + Приховати \"Субтитри\" + Кнопку \"Субтитри\" у відеоплеєрі приховано + Кнопка \"Субтитри\" у відеоплеєрі показується + Приховати \"Автовідтворення\" + Кнопку \"Автовідтворення\" у відеоплеєрі приховано + Кнопка \"Автовідтворення\" у відеоплеєрі показується - - Приховати картки альбому - Картки альбому приховано - Картки альбому показуються - - - Коментарі - Приховати або показувати компоненти секції коментарів - Приховати \"Коментарі від спонсорів\" - Заголовок \"Коментарі від спонсорів\" приховано - Заголовок \"Коментарі від спонсорів\" показується - Приховати секцію коментарів - Секцію коментарів приховано - Секція коментарів показується - Приховати \"Створити Short\" - Кнопку \"Створити Short\" приховано - Кнопка \"Створити Short\" показується - Приховати прев\'ю коментар - Прев\'ю коментар в секції коментарів приховано - Прев\'ю коментар в секції коментарів показується - Приховати \"Дякую\" - Кнопку \"Дякую\" приховано - Кнопка \"Дякую\" показується - Приховати мітку часу та емодзі - Кнопки мітки часу та емодзі приховано - Кнопки мітки часу та емодзі показуються - - - Приховати скриню фінансування - Скриньку фінансування приховано - Скринька фінансування показується - - + Приховати картки кінцевого екрана Картки кінцевого екрана приховано Картки кінцевого екрана показуються - - Панель фільтрів - Приховати або показувати панель фільтрів у стрічці, пошуку та пов\'язаних відео - Приховати панель у стрічці - Панель фільтрів у стрічці приховано - Панель фільтрів у стрічці показується - Приховати панель у пошуку - Панель фільтрів у пошуку приховано - Панель фільтрів у пошуку показується - Приховати панель у пов\'язаних відео - Панель фільтрів у пов\'язаних відео приховано - Панель фільтрів у пов\'язаних відео показується - - - Приховати кнопку мікрофона - Плаваючу кнопку мікрофона приховано - Плаваюча кнопка мікрофона показується - - + Вимкнути кінематографічне освітлення Кінематографічне освітлення в повноекранному режимі вимкнуто Кінематографічне освітлення в повноекранному режимі ввімкнуто - + Приховати підказки Підказки справа вгорі відеоплеєра приховано Підказки справа вгорі відеоплеєра показуються - + Вимкнути анімовані лічильники Лічильники статичні Лічильники анімовані - + Приховати панель прогресу Панель прогресу у відеоплеєрі приховано Панель прогресу у відеоплеєрі показується @@ -597,7 +593,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Панель прогресу у прев\'ю переглянутих відео приховано Панель прогресу у прев\'ю переглянутих відео показується - + + Плеєр Shorts + Приховати або показувати компоненти у плеєрі Shorts Приховати Shorts у стрічці Shorts у домашній стрічці приховано @@ -695,27 +693,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Панель навігації приховано Панель навігації показується - + Вимкнути пропоновані відео кінцевого екрана Пропоновані відео кінцевого екрана буде вимкнуто.\n\nВідома проблема: автовідтворення наступного відео не працює Пропоновані відео кінцевого екрана показуватимуться - + Приховати мітку часу відео Мітку часу відео над панеллю прогресу приховано Мітка часу відео над панеллю прогресу показується - + Приховати спливаючі панелі плеєра Автоматичні спливаючі панелі плеєра приховано, такі як список відтворення чи чат Автоматичні спливаючі панелі плеєра показуються - + Затемнення плеєра при натисканні Значення непрозорості затемнення при натисканні на плеєр в межах 0-100, де 0 це прозоро Значення затемнення плеєра має бути в межах 0-100 - + Дизлайки тимчасово недоступні (тайм-аут API) Дизлайки недоступні (статус %d) @@ -759,17 +757,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Обмеження швидкості клієнта виявлено %d разів %d мілісекунд - + Широка панель пошуку Широку панель пошуку ввімкнуто\n\nЦе також приховує значок YouTube та кнопку пошуку Широку панель пошуку вимкнуто - + + Увімкнути мініатюри високої якості + Мініатюри панелі прогресу під час перемотування мають високу якість + Мініатюри панелі прогресу під час перемотування мають середню якість + Повноекранні мініатюри під час перемотування мають високу якість + Повноекранні мініатюри під час перемотування мають середню якість + Це також відновить мініатюри в прямих трансляціях, які не мають мініатюр при перемотуванні.\n\nМініатюри при перемотуванні матимуть ту саму якість, що й поточне відео.\n\nЦя функція найкраще працює з якістю відео 720p або нижчою та при використанні дуже швидкого підключення до Інтернету. Відновити старі мініатюри Мініатюри під час перемотування показуються у мінівікні над панеллю прогресу Мініатюри під час перемотування показуються в повноекранному режимі - + Увімкнути SponsorBlock SponsorBlock - це краудсорсингова система для пропускання дратівливих частин відео на YouTube Налаштувати зовнішній вигляд @@ -950,7 +954,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Про інтеграцію Дані надаються SponsorBlock API. Натисніть тут, щоб дізнатися більше та побачити завантаження для інших платформ - + Підробити версію програми Версію підроблено Версію не підроблено @@ -963,9 +967,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Відновлення розширеного меню швидкості та якості відео 18.09.39 - Відновлення вкладки Бібліотека 17.41.37 - Відновлення старого інтерфейсу плейлиста - 17.33.42 - Відновлення старого інтерфейсу - + Початкова сторінка За замовчуванням Усі підписки @@ -983,18 +986,26 @@ This is because Crowdin requires temporarily flattening this file and removing t Популярне Переглянути пізніше - + Вимкнути плеєр Shorts при запуску Плеєр Shorts вимкнуто при запуску додатку Плеєр Shorts увімкнуто при запуску додатку - + + Автовідтворення Shorts + Shorts будуть автоматично відтворюватися одне за одним + Shorts будуть повторюватися + Автовідтворення Shorts у фоні + Shorts будуть автоматично відтворюватися одне за одним у фоновому режимі + Shorts будуть повторюватися у фоновому режимі + + Увімкнути планшетний інтерфейс Планшетний інтерфейс увімкнуто Планшетний інтерфейс вимкнуто Публікації спільноти не показуються в планшетному інтерфейсі - + Мініплеєр Змінити стиль згорнутого мініплеєра Тип мініплеєра @@ -1013,6 +1024,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Увімкнути перетягування Перетягування увімкнено\n\nМініплеєр можна перетягнути в будь-який кут екрану Перетягування вимкнуто + Увімкнути жест горизонтального перетягування + Жест горизонтального перетягування увімкнено\n\nМініплеєр можна перетягнути за межі екрана вліво або вправо + Жест горизонтального перетягування вимкнено Приховати кнопку закриття Кнопку закриття мініплеєру приховано Кнопка закриття мініплеєру показується @@ -1032,12 +1046,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Значення затемнення при натисканні на мініплеєр в межах 0-100, де 0 це прозоро Значення затемнення мініплеєра має бути в межах 0-100 - + Увімкнути градієнт завантаження Екран завантаження макета матиме градієнтне тло Екран завантаження макета матиме суцільне тло - + Колір панелі прогресу Показується користувацький колір панелі прогресу відтворення Показується оригінальний колір панелі прогресу відтворення @@ -1045,12 +1059,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Редагувати колір панелі прогресу відтворення Недійсне значення кольору панелі прогресу - + Змінити хост зображень Використовується хост зображень yt4.ggpht.com Використовується оригінальний хост зображень\n\nУвімкнення цього параметра може виправити відсутні зображення, які заблоковано в деяких регіонах - + Вкладка \"Головна\" @@ -1082,7 +1096,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow тимчасово недоступний. (код статусу: %s) DeArrow тимчасово недоступний - + Показувати оголошення від ReVanced Оголошення під час запуску показуються Оголошення під час запуску не показуються @@ -1090,86 +1104,89 @@ This is because Crowdin requires temporarily flattening this file and removing t Не вдалося підключитися до постачальника оголошень Закрити - + Увага Ваша історія переглядів не зберігається.<br><br>Швидше за все, це спричинено блокувальником реклами DNS або мережевим проксі.<br><br>Щоб це виправити, додайте <b>s.youtube.com</b> у білий список блокувальника або вимкніть усі DNS блокувальники та проксі. Більше не показувати - + Увімкнути автоповтор відео Автоповторення поточного відео ввімкнуто Автоповторення поточного відео вимкнуто - + Підробити розміри пристрою Розміри пристрою підроблено\n\nБільш високі якості відео може бути розблоковано, але можуть спостерігатися затримки під час відтворення відео, підвищене споживання батареї та невідомі побічні ефекти Розміри пристрою не підроблено\n\nУвімкнення цієї опції може розблокувати вищі якості відео Увімкнення цієї опції може викликати затримки під час відтворення відео, підвищене споживання акумулятора та невідомі побічні ефекти. - + Налаштування GmsCore Відкрити GmsCore для налаштування та входу в обліковий запис Google - + Обхід URL переадресацій URL переадресації обходяться URL переадресації не обходяться - + Відкривати посилання у браузері Посилання відкриваються зовні Посилання відкриваються у додатку - + Вилучити параметр відстеження Параметри запиту відстеження вилучаються з посилань під час поширення посилань Параметри запиту відстеження не вилучаються з посилань під час поширення посилань - + Вимкнути вібрацію при масштабуванні Вібрації при масштабуванні вимкнуто Вібрації при масштабуванні ввімкнуто - + Автоматична якість Запам\'ятовувати зміни якості відео Зміни якості застосовуються до всіх відео Зміни якості застосовуються лише до поточного відео - Якість відео в Wi-Fi мережі - Якість відео в мобільній мережі + Стандартна якість відео в Wi-Fi мережі + Стандартна якість відео в мобільній мережі в моб. мережі в Wi-Fi мережі Якість %1$s змінено на %2$s - + Кнопка швидкості відтворення Кнопка швидкості відтворення показується в плеєрі Кнопка швидкості відтворення не показується в плеєрі - + + Користувацьке меню швидкості відтворення + Користувацьке меню швидкості відтворення показується + Користувацьке меню швидкості відтворення не показується Користувацькі швидкості відтворення - Додавання або зміна доступних швидкостей відтворення + Додавання або зміна користувацьких швидкостей відтворення Користувацькі швидкості повинні бути менше ніж %s Використовуються значення за замовчуванням. Неправильні користувацькі швидкості відтворення. Використовуються значення за замовчуванням. - + Запам\'ятовувати зміни швидкості Зміни швидкості відтворення застосовуються до всіх відео Зміни швидкості відтворення застосовуються лише до поточного відео Стандартна швидкість відтворення Швидкість змінена на %s - + Відновити старе меню якості відео Показується старе меню якості відео Показується нове меню якості відео - + Увімкнути перемотку пересуванням Перемотку пересуванням увімкнуто\n\nУвімкнуто поведінку старого інтерфейсу \"Проведіть пальцем, щоб перемотати\" Перемотку пересуванням вимкнуто\n\nУвімкнуто поведінку нового інтерфейсу прискорення \"2х >>\" при утриманні на екрані - + Підробка відеопотоків Підробка відеопотоків клієнта для запобігання проблем відтворенням Підробка відеопотоків @@ -1187,17 +1204,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Побічні ефекти підробки Android VR: • Меню звукової доріжки відсутнє\n• Меню стабілізації гучності недоступне - - - - + Блокувати аудіорекламу Аудіорекламу заблоковано Аудіорекламу розблоковано - + %s недоступний. Реклама може відображатися. Спробуйте перейти на іншу службу блокування реклами в налаштуваннях. Сервер %s повернув помилку. Реклама може відображатися. Спробуйте перейти на іншу службу блокування реклами в налаштуваннях. Блокувати вбудовану відеорекламу @@ -1205,30 +1219,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Проксі Luminous Проксі PurpleAdBlock - + Блокувати відеорекламу Відеорекламу заблоковано Відеорекламу розблоковано - + повідомлення видалено Показувати видалені повідомлення Не показувати видалені повідомлення Приховати видалені повідомлення під спойлером Показувати видалені повідомлення як закреслений текст - + Автоматично збирати Бали каналу Бали каналу збираються автоматично Бали каналу не збираються автоматично - + Увімкнути режим налагодження Twitch Режим налагодження Twitch увімкнено (не рекомендовано) Режим налагодження Twitch вимкнуто - + Налаштування ReVanced Реклама Налаштування блокування реклами diff --git a/patches/src/main/resources/addresources/values-ur-rIN/strings.xml b/patches/src/main/resources/addresources/values-ur-rIN/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-ur-rIN/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/addresources/values-uz-rUZ/strings.xml b/patches/src/main/resources/addresources/values-uz-rUZ/strings.xml similarity index 70% rename from src/main/resources/addresources/values-uz-rUZ/strings.xml rename to patches/src/main/resources/addresources/values-uz-rUZ/strings.xml index cf161c4f4..2e43d7197 100644 --- a/src/main/resources/addresources/values-uz-rUZ/strings.xml +++ b/patches/src/main/resources/addresources/values-uz-rUZ/strings.xml @@ -32,21 +32,23 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + - + - + - + - + - + + + @@ -62,30 +64,30 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + @@ -95,23 +97,17 @@ This is because Crowdin requires temporarily flattening this file and removing t - - - - - - - - + - + + @@ -122,29 +118,20 @@ This is because Crowdin requires temporarily flattening this file and removing t - + + - + - + - + - + - + - - - - - - - - - - - + @@ -152,106 +139,105 @@ This is because Crowdin requires temporarily flattening this file and removing t - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + + + - + - + Ogohlantirish - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + diff --git a/src/main/resources/addresources/values-vi-rVN/strings.xml b/patches/src/main/resources/addresources/values-vi-rVN/strings.xml similarity index 95% rename from src/main/resources/addresources/values-vi-rVN/strings.xml rename to patches/src/main/resources/addresources/values-vi-rVN/strings.xml index 86064038c..2743388eb 100644 --- a/src/main/resources/addresources/values-vi-rVN/strings.xml +++ b/patches/src/main/resources/addresources/values-vi-rVN/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Kiểm tra thất bại Mở trang web chính thức Phớt lờ @@ -43,7 +43,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Đã vá %s ngày trước Ngày dựng APK bị hỏng - + Bạn có muốn tiếp tục không? Đặt lại Làm mới và khởi động lại @@ -62,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Các liên kết chính thức Quyên góp - + MicroG GmsCore chưa được cài đặt. Cài nó. Hành động cần thiết @@ -73,7 +73,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + Giới thiệu Quảng cáo Hình thu nhỏ thay thế @@ -85,7 +85,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Cài đặt khác Video - + + + Gỡ lỗi Bật hoặc tắt tùy chọn gỡ lỗi Nhật ký gỡ lỗi @@ -102,13 +104,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Không hiện thông báo nổi khi xảy ra lỗi Việc tắt thông báo lỗi sẽ ẩn tất cả thông báo lỗi ReVanced.\n\nBạn sẽ không được thông báo về bất kỳ sự kiện không mong muốn nào. - + Nút thích / đăng ký tỏa sáng Nút thích và đăng ký sẽ không tỏa sáng khi được nhắc đến Nút thích và đăng ký sẽ tỏa sáng khi được nhắc đến - Ẩn dải phân cách màu xám - Dải phân cách màu xám được ẩn - Dải phân cách màu xám được hiện + Ẩn các thẻ album + Các thẻ album được ẩn + Các thẻ album được hiện + Ẩn hộp chiến dịch gây quỹ + Hộp chiến dịch gây quỹ được ẩn + Hộp chiến dịch gây quỹ được hiện + Ẩn nút micrô nổi + Nút micrô được ẩn + Nút micrô được hiện Ẩn hình mờ của kênh Hình mờ được ẩn Hình mờ được hiện @@ -153,9 +161,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Ẩn bảng mở rộng dưới video Bảng mở rộng được ẩn Bảng mở rộng được hiện - Ẩn ghi chú cuối mục chất lượng video - Ghi chú cuối mục chất lượng video được ẩn - Ghi chú cuối mục Chất lượng video đã được hiển thị Ẩn bài đăng cộng đồng Bài đăng cộng đồng được ẩn Bài đăng cộng đồng được hiện @@ -230,6 +235,37 @@ This is because Crowdin requires temporarily flattening this file and removing t Phần bản chép lời được hiện Mô tả video Ẩn hoặc hiện các thành phần mô tả video + Thanh bộ lọc + Ẩn hoặc hiện thanh bộ lọc trong bảng tin, tìm kiếm và video liên quan + Ẩn trong bảng tin + Đã ẩn trong bảng tin + Đã hiện trong bảng tin + Ẩn tròn tìm kiếm + Đã ẩn trong tìm kiếm + Đã hiện trong tìm kiếm + Ẩn trong video liên quan + Đã ẩn trong video liên quan + Đã hiện trong video liên quan + Bình luận + Ẩn hoặc hiện các thành phần bình luận + Ẩn tiêu đề \'Bình luận bởi hội viên\' + Tiêu đề \'Bình luận bởi hội viên\' được ẩn + Tiêu đề \'Bình luận bởi hội viên\' được hiện + Ẩn phần bình luận + Phần Bình luận được ẩn + Phần Bình luận được hiện + Ẩn nút \'Tạo video ngắn\' + Nút \'Tạo video ngắn\' được ẩn + Nút \'Tạo video ngắn\' được hiện + Ẩn xem trước bình luận + Xem trước bình luận được ẩn + Xem trước bình luận được hiện + Ẩn nút cảm ơn + Nút cảm ơn được ẩn + Nút cảm ơn được hiện + Ẩn mốc thời gian và các nút biểu tượng cảm xúc + Mốc thời gian và các nút biểu tượng cảm xúc được ẩn + Mốc thời gian và các nút biểu tượng cảm xúc được hiện Bộ lọc tùy chỉnh Ẩn các thành phần dùng bộ lọc tùy chỉnh @@ -267,7 +303,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 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 - + Ẩn quảng cáo chung Quảng cáo chung được ẩn Quảng cáo chung được hiện @@ -302,17 +338,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Ẩn QC toàn màn hình chỉ hoạt động với các thiết bị cũ - + Ẩn quảng cáo YouTube Premium Quảng cáo Youtube Premium bên dưới video được ẩn Quảng cáo Youtube Premium bên dưới video được hiện - + Ẩn quảng cáo trên video Quảng cáo trên video được ẩn Quảng cáo trên video được hiện - + Đã chép URL vào bảng nhớ tạm Đã chép URL với dấu thời gian Hiện nút sao chép url video @@ -322,13 +358,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Nút được hiển thị. Chạm để sao chép video URL với dấu thời gian. Chạm và giữ để sao chép video URL không dấu thời gian Nút không được hiện - + Loại bỏ hộp thoại cảnh báo trước khi xem Hộp thoại sẽ bị loại bỏ Hộp thoại sẽ được hiện Điều này sẽ không qua mặt hạn chế độ tuổi. Nó chỉ tự động chấp nhận. - + Tải xuống bên ngoài Các thiết lập trình tải xuống bên ngoài Hiện nút tải xuống bên ngoài @@ -342,17 +378,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Tên gói của ứng dụng tải xuống bên ngoài đã cài đặt của bạn, chẳng hạn như NewPipe hoặc Seal %s chưa được cài đặt. Hãy cài đặt nó. - + Vô hiệu cử chỉ dò tìm chính xác Cử chỉ đã tắt Cử chỉ đã bật - + Bật chạm tua tham tiến trình Chạm tua thanh tiến trình đã bật Chạm tua thanh tiến trình đã tắt - + Bật cử chỉ độ sáng Vuốt độ sáng đã bật Vuốt độ sáng đã tắt @@ -381,12 +417,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Độ rộng ngưỡng vuốt Độ rộng của ngưỡng vuốt để thực hiện cử chỉ vuốt - + Tắt phụ đề tự động Phụ đề tự động đã tắt Phụ đề tự động đã bật - + Các nút hành động Ẩn hoặc hiện nút dưới video Ẩn Thích và Không thích @@ -422,23 +458,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Nút lưu vào danh sách phát được ẩn Nút lưu vào danh sách phát được hiện - - Ẩn nút tự động phát - Nút tự động phát được ẩn - Nút tự động phát được hiện - - - - Ẩn nút phụ đề - Nút phụ đề được ẩn - Nút phụ đề được hiện - - - Ẩn nút truyền - Nút Truyền được ẩn - Nút Truyền được hiển thị - - + Các nút điều hướng Ẩn hoặc hiện các nút ở thanh điều hướng @@ -465,7 +485,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Các nhãn được ẩn Các nhãn được hiện - + Trình đơn nổi Ẩn hoặc hiện các mục trình đơn nổi @@ -476,6 +496,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Ẩn cài đặt bổ sung Trình đơn cài đặt bổ sung được ẩn Trình đơn cài đặt bổ sung được hiện + Ẩn lặp video Trình đơn lặp video được ẩn @@ -509,83 +530,44 @@ This is because Crowdin requires temporarily flattening this file and removing t Ẩn Xem trong thực tế ảo Trình đơn xem trong thực tế ảo được ẩn Trình đơn xem trong thực tế ảo được hiện + Ẩn ghi chú cuối mục chất lượng video - - Ẩn các nút video trước đó & tiếp theo - Các nút được ẩn - Các nút được hiện + + Ẩn các nút video trước đó & tiếp theo + Các nút được ẩn + Các nút được hiện + Ẩn nút truyền + Nút Truyền được ẩn + Nút Truyền được hiển thị + + Ẩn nút phụ đề + Nút phụ đề được ẩn + Nút phụ đề được hiện + Ẩn nút tự động phát + Nút tự động phát được ẩn + Nút tự động phát được hiện - - Ẩn các thẻ album - Các thẻ album được ẩn - Các thẻ album được hiện - - - Bình luận - Ẩn hoặc hiện các thành phần bình luận - Ẩn tiêu đề \'Bình luận bởi hội viên\' - Tiêu đề \'Bình luận bởi hội viên\' được ẩn - Tiêu đề \'Bình luận bởi hội viên\' được hiện - Ẩn phần bình luận - Phần Bình luận được ẩn - Phần Bình luận được hiện - Ẩn nút \'Tạo video ngắn\' - Nút \'Tạo video ngắn\' được ẩn - Nút \'Tạo video ngắn\' được hiện - Ẩn xem trước bình luận - Xem trước bình luận được ẩn - Xem trước bình luận được hiện - Ẩn nút cảm ơn - Nút cảm ơn được ẩn - Nút cảm ơn được hiện - Ẩn mốc thời gian và các nút biểu tượng cảm xúc - Mốc thời gian và các nút biểu tượng cảm xúc được ẩn - Mốc thời gian và các nút biểu tượng cảm xúc được hiện - - - Ẩn hộp chiến dịch gây quỹ - Hộp chiến dịch gây quỹ được ẩn - Hộp chiến dịch gây quỹ được hiện - - + Ẩn thẻ kết thúc màn hình Thẻ kết thúc màn hình được ẩn Thẻ kết thúc màn hình được hiện - - Thanh bộ lọc - Ẩn hoặc hiện thanh bộ lọc trong bảng tin, tìm kiếm và video liên quan - Ẩn trong bảng tin - Đã ẩn trong bảng tin - Đã hiện trong bảng tin - Ẩn tròn tìm kiếm - Đã ẩn trong tìm kiếm - Đã hiện trong tìm kiếm - Ẩn trong video liên quan - Đã ẩn trong video liên quan - Đã hiện trong video liên quan - - - Ẩn nút micrô nổi - Nút micrô được ẩn - Nút micrô được hiện - - + Tắt chế độ môi trường trong toàn màn hình Chế độ môi trường được tắt Chế độ môi trường được bật - + Ẩn thẻ thông tin Thẻ thông tin được ẩn Thẻ thông tin được hiển thị - + Tắt chuyển động cuộn số Số cuộn không chuyển động Số cuộn được chuyển động - + Ẩn thanh tiến trình trong trình phát video Thanh tiến trình trong trình phát video được ẩn Thanh tiến trình trong trình phát video được hiện @@ -593,7 +575,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Thanh tiến trình hình thu nhỏ được ẩn Thanh tiến trình hình thu nhỏ được hiện - + Ẩn Shorts trong bảng tin trang chính Shorts trong bảng tin trang chính được ẩn @@ -677,27 +659,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Thanh điều hướng được ẩn Thanh điều hướng được hiện - + Tắt video đề xuất ở màn hình kết thúc Video đề xuất sẽ được tắt Video đề xuất sẽ được bật - + Ẩn mốc thời gian video Mốc thời gian được ẩn Mốc thời gian được hiện - + Ẩn bảng tự bật lên trên trình phát Bảng bật lên trên trình phát được ẩn Bảng bật lên trên trình phát được hiện - + Độ mờ của lớp phủ trình phát Giá trị độ mờ của lớp phủ trình phát trong khoảng từ 0 đến 100, trong đó 0 là trong suốt Độ phủ mờ trình phát phải nằm giữa 0-100 - + Lượt ko thích tạm thời ko khả dụng (API hết giờ) Lượt không thích không khả dụng ( trạng thái %d) @@ -741,17 +723,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Gặp phải %d lần đạt giới hạn truy cập máy khách %d mili-giây - + Bật thanh tìm kiếm rộng Thanh tìm kiếm rộng được bật Thanh tìm kiếm rộng được tắt - + Khôi phục thanh tiến trình hình thu nhỏ kiểu cũ Thanh tiến trình hình thu nhỏ sẽ xuất hiện phía trên thanh tiến trình Thanh tiến trình hình thu nhỏ sẽ xuất hiện khi toàn màn hình - + Bậ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 trên YouTube Giao diện @@ -932,7 +914,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Giới thiệu Dữ liệu được cung cấp bởi API SponsorBlock. Nhấn vào đây để tìm hiểu thêm và xem các bản tải cho các nền tảng khác - + Giả mạo phiên bản ứng dụng Phiên bản đã được giả mạo Phiên bản không được giả mạo @@ -945,9 +927,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Khôi phục trình đơn tốc độ & chất lượng cho video rộng 18.09.39 - Khôi phục thẻ thư viện 17.41.37 - Khôi phục kệ danh sách phát cũ - 17.33.42 - Khôi phục bố cục giao diện cũ - + Đặt trang bắt đầu Mặc định Khám phá @@ -957,18 +938,20 @@ This is because Crowdin requires temporarily flattening this file and removing t Đăng ký Xu hướng - + Tắt tiếp tục trình phát 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 - + + + Bật bố cục máy tính bảng Bố cục máy tính bảng được bật Bố cục máy tính bảng được tắt Bài đăng của cộng đồng không hiển thị trên bố cục máy tính bảng - + Trình phát thu nhỏ Thay đổi kiểu trình phát thu nhỏ trong ứng dụng Loại trình phát thu nhỏ @@ -989,24 +972,24 @@ This is because Crowdin requires temporarily flattening this file and removing t Giá trị độ mờ của lớp phủ trình phát trong khoảng từ 0 đến 100, trong đó 0 là trong suốt Độ phủ mờ trình phát thu nhỏ phải nằm giữa 0-100 - + Bật màn hình tải màu dốc Màn hình tải sẽ có một nền màu dốc Màn hình tải sẽ có một nền màu đặc - + Bật màu tùy chỉnh thanh tiến trình Màu tùy chỉnh thanh tiến trình được hiện Màu gốc thanh tiến trình được hiện Màu tùy chỉnh thanh tiến trình Màu của thanh tiến trình - + Vượt qua hạn chế khu vực cho hình ảnh 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 - + Thẻ trang chủ @@ -1038,7 +1021,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 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 - + Hiện công bố của ReVanced Công bố được hiện khi khởi chạy Công bố không được hiện khi khởi chạy @@ -1046,47 +1029,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Kết nối đến nguồn cấp công bố thất bại Từ bỏ - + Cảnh báo Lịch sử xem của bạn chưa được lưu.<br><br>Điều này hầu hết gây ra bởi một DNS trình chặn quảng cáo hoặc proxy mạng.<br><br>Để sửa nó, cho phép <b>s.youtube.com</b> hoặc tắt tất cả trình chặn DNS và các proxy. Không hiện lại - + Bật tự phát lại Tự phát lại được bật Tự phát lại được tắt - + Giả mạo kích thước thiết bị Kích thước thiết bị đã được giả mạo\n\nChất lượng video cao hơn có thể được mở khóa nhưng bạn có thể trải nghiệm video phát lắp, hao pin, và các tác dụng phụ chưa biết Kích thước thiết bị không được giả mạo\n\nBật tính năng này có thể mở khóa chất lượng video cao hơn Bật tính năng này có thể làm video phát lắp, hao pin, và các tác dụng phụ chưa biết. - + Cài đặt GmsCore Các cài dặt cho GmsCore - + Vượt chuyển hướng URL Chuyển hướng URL được vượt Chuyển hướng URL không được vượt - + Mở liên kết trong trình duyệt Đang mở liên kết bên ngoài Đang mở liên kết trong ứng dụng - + Loại bỏ tham số truy vấn theo dõi Tham số truy vấn theo dõi được loại bỏ khỏi liên kết Tham số truy vấn theo dõi không được loại bỏ khỏi liên kết - + Tắt phản hồi xúc giác khi thu phóng Phản hồi xúc giác được tắt Phản hồi xúc giác được bật - + Chất lượng tự động Nhớ các thay đổi chất lượng video Thay đổi chất lượng áp dụng cho tất cả video @@ -1097,35 +1080,34 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Đã thay đổi chất lượng %1$s mặc định: %2$s - + Hiện nút hộp thoại tốc độ phát Nút được hiện Nút không được hiện - + Tốc độ phát tùy chỉnh - Thêm hoặc thay đổi tốc độ phát hiện có Tốc độ phát tuỳ chỉnh phải nhỏ hơn %s. Dùng giá trị mặc định. Tốc độ phát không hợp lệ. Dùng giá trị mặc định. - + Nhớ các 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 Tốc độ phát mặc định Đã thay đổi tốc độ phát mặc định thành: %s - + Khôi phục trình đơn chất lượng video kiểu cũ Trình đơn chất lượng video kiểu cũ được hiện Trình đơn chất lượng video kiểu cũ không được hiện - + Bật vuốt để tua Vuốt để tua được bật Vuốt để tua không được bật - + Giả mạo luồng video Giả mạo luồng video máy khách để ngăn ngừa sự cố khi phát nền Giả mạo luồng video @@ -1143,17 +1125,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Tác dụng phụ của giả mạo Android VR • Trình đơn bản âm thanh bị mất\n• Âm lượng thích ứng không hữu dụng - - - - + Chặn quảng cáo âm thanh Quảng cáo âm thanh được chặn Quảng cáo âm thanh không được chặn - + %s không có sẵn. QC có thể sẽ hiện. Hãy thử chuyển sang dịch vụ chặn QC khác trong cài đặt. Máy chủ %s trả về một lỗi. QC có thể sẽ hiện. Hãy thử chuyển sang dịch vụ chặn QC khác trong cài đặt. Chặn quảng cáo video nhúng @@ -1161,30 +1140,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Ủy thác Luminous Ủy thác PurpleAdBlock - + Chặn quảng cáo video Quảng cáo video được chặn Quảng cáo video không được chặn - + đã xóa tin nhắn Hiện tin nhắn đã xóa Không hiện tin nhắn đã xóa Che tin nhắn đã xóa Hiện tin nhắn đã xóa dưới dạnh gạch ngang - + Tự nhận Điểm Kênh Điểm Kênh được nhận tự động Điểm Kênh không được nhận tự động - + Bật chế độ gỡ lỗi Twitch Chế độ gỡ lỗi Twitch được bật (không khuyến nghị) Chế độ gỡ lỗi Twitch được tắt - + Cài đặt ReVanced Quảng cáo Cài đặt chặn quảng cáo diff --git a/src/main/resources/addresources/values-zh-rCN/strings.xml b/patches/src/main/resources/addresources/values-zh-rCN/strings.xml similarity index 93% rename from src/main/resources/addresources/values-zh-rCN/strings.xml rename to patches/src/main/resources/addresources/values-zh-rCN/strings.xml index 72f524411..68fc744cf 100644 --- a/src/main/resources/addresources/values-zh-rCN/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rCN/strings.xml @@ -32,7 +32,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + 检查失败 打开官方网站 忽略 @@ -42,7 +42,7 @@ This is because Crowdin requires temporarily flattening this file and removing t %s 天前补丁 APK 构建日期已损坏 - + 继续? 重置 刷新并重启 @@ -61,7 +61,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 官方链接 捐助 - + MicroG GmsCore 未安装。请安装。 需要采取操作 @@ -72,7 +72,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + 关于 广告 可选缩略图 @@ -84,7 +84,9 @@ This is because Crowdin requires temporarily flattening this file and removing t 杂项 视频 - + + + 调试 启用或禁用调试选项 调试日志 @@ -101,13 +103,19 @@ This is because Crowdin requires temporarily flattening this file and removing t 出错时不显示 Toast 关闭错误提示将隐藏所有 ReVanced 错误通知。\n\n您将不会收到任何意外事件的通知。 - + 禁用点赞 / 订阅按钮动效 点赞和订阅按钮点击时无动效 点赞和订阅按钮点击时有动效 - 隐藏灰色分隔符 - 灰色分隔符已隐藏 - 灰色分隔符已显示 + 隐藏相册卡 + 相册已隐藏 + 显示相册卡 + 隐藏众筹框 + 聚合供资框已隐藏 + 显示集资框 + 隐藏浮动麦克风 + 麦克风按钮隐藏 + 麦克风按钮显示 隐藏频道水印 水印已隐藏 水印将显示 @@ -152,9 +160,6 @@ This is because Crowdin requires temporarily flattening this file and removing t 隐藏视频下方的扩展面板 扩展面板已隐藏 扩展面板已显示 - 隐藏视频质量菜单页脚 - 视频质量菜单页脚已隐藏 - 视频质量菜单页脚已显示 隐藏社区帖子 社区帖子已隐藏 社区帖子已显示 @@ -229,6 +234,37 @@ This is because Crowdin requires temporarily flattening this file and removing t 显示字幕部分 视频描述 隐藏或显示视频描述组件 + 过滤栏 + 在新闻源、搜索和相关视频中隐藏或显示过滤栏 + 在新闻源中隐藏 + 在订阅中隐藏 + 在订阅源中显示 + 在搜索中隐藏 + 在搜索中隐藏 + 在搜索中显示 + 隐藏相关视频 + 在相关视频中隐藏 + 显示在相关视频 + 评论 + 隐藏或显示评论部分组件 + 隐藏会员头部的评论 + 会员头的评论已隐藏 + 成员标题的评论已显示 + 隐藏评论部分 + 评论部分已隐藏 + 评论部分已显示 + 隐藏“创建 Short按钮 + “创建 Short 按钮被隐藏 + 显示“创建 Short”按钮 + 隐藏预览评论 + 预览已隐藏 + 显示预览评论 + 隐藏感谢按钮 + 感谢按钮已隐藏 + 已显示感谢按钮 + 隐藏时间戳和表情按钮 + 隐藏时间戳和表情按钮 + 显示时间戳和表情按钮 隐藏 YouTube Doodles 搜索栏门框已隐藏 @@ -270,7 +306,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 关键词太短,需要引用: %s 关键字将隐藏所有视频: %s - + 隐藏一般广告 一般广告已隐藏 一般广告已显示 @@ -289,6 +325,9 @@ This is because Crowdin requires temporarily flattening this file and removing t 隐藏产品横幅 横幅已隐藏 横幅已显示 + 隐藏玩家购物架 + 购物架已隐藏 + 购物架已显示 在视频描述中隐藏购物链接 购物链接已隐藏 购物链接已显示 @@ -305,17 +344,17 @@ This is because Crowdin requires temporarily flattening this file and removing t 隐藏全屏广告只适用于旧设备 - + 隐藏 YouTube 高级促销活动 视频播放器下的YouTube 高级促销已隐藏 在视频播放器下显示YouTube高级促销活动 - + 隐藏视频广告 视频广告已隐藏 显示视频广告 - + URL 已复制到剪贴板 复制时间戳的 URL 显示复制视频 URL 按钮 @@ -325,13 +364,13 @@ This is because Crowdin requires temporarily flattening this file and removing t 显示按钮。点击复制带有时间戳的视频URL。点击并按住以在没有时间戳的情况下复制视频 未显示按钮 - + 移除查看器的酌处权对话框 对话框将被删除 将显示对话框 这并不是绕过年龄限制。它只是自动接受。 - + 外部下载 使用外部下载器的设置 显示外部下载按钮 @@ -345,17 +384,17 @@ This is because Crowdin requires temporarily flattening this file and removing t 您已安装外部下载程序的包名,如NewPipe 或密封。 %s 未安装。请安装它。 - + 禁用精确滑动定位 手势已禁用 手势已启用 - + 启用搜索栏点击 已启用 Seekbar 点击 已禁用 Seekbar 点击 - + 启用亮度手势 亮度滑动已启用 亮度滑动已禁用 @@ -384,12 +423,12 @@ This is because Crowdin requires temporarily flattening this file and removing t 滑动星等阈值 滑动的阈值数量 - + 禁用自动标题 自动标题已禁用 自动标题已启用 - + 动作按钮 隐藏或显示视频下的按钮 隐藏喜欢和不喜欢的 @@ -425,23 +464,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 保存到播放列表按钮隐藏 显示播放列表按钮 - - 隐藏自动播放按钮 - 自动播放按钮已隐藏 - 显示自动播放按钮 - - - - 隐藏标题按钮 - 字幕按钮已隐藏 - 显示字幕按钮 - - - 隐藏投射按钮 - 已隐藏投屏按钮 - 已显示投屏按钮 - - + Navigation buttons 隐藏或更改导航栏中的按钮 @@ -468,7 +491,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 标签已隐藏 显示标签 - + Flyout menu 隐藏或显示播放器飞行菜单项 @@ -479,6 +502,10 @@ This is because Crowdin requires temporarily flattening this file and removing t 隐藏其他设置 其他设置菜单已隐藏 显示其他设置菜单 + + 隐藏睡眠计时器 + 睡眠计时器菜单已隐藏 + 显示睡眠计时器菜单 隐藏循环视频 循环视频菜单已隐藏 @@ -487,6 +514,9 @@ This is because Crowdin requires temporarily flattening this file and removing t 隐藏环境模式 环境模式菜单已隐藏 显示环境模式菜单 + 隐藏稳定音量 + 已显示稳定音量菜单 + 稳定音量菜单已隐藏 隐藏帮助 & 反馈 帮助 & 反馈菜单已隐藏 @@ -512,83 +542,46 @@ This is because Crowdin requires temporarily flattening this file and removing t 在 VR 中隐藏观察 在 VR 菜单中观看是隐藏的 在VR菜单中查看 + 隐藏视频质量菜单页脚 + 视频质量菜单页脚已隐藏 + 显示视频质量菜单页脚 - - 隐藏之前的 & 下一个视频按钮 - 按钮被隐藏 - 显示按钮 + + 隐藏之前的 & 下一个视频按钮 + 按钮被隐藏 + 显示按钮 + 隐藏投射按钮 + 已隐藏投屏按钮 + 已显示投屏按钮 + + 隐藏标题按钮 + 字幕按钮已隐藏 + 显示字幕按钮 + 隐藏自动播放按钮 + 自动播放按钮已隐藏 + 显示自动播放按钮 - - 隐藏相册卡 - 相册已隐藏 - 显示相册卡 - - - 评论 - 隐藏或显示评论部分组件 - 隐藏会员头部的评论 - 会员头的评论已隐藏 - 成员标题的评论已显示 - 隐藏评论部分 - 评论部分已隐藏 - 评论部分已显示 - 隐藏“创建 Short按钮 - “创建 Short 按钮被隐藏 - 显示“创建 Short”按钮 - 隐藏预览评论 - 预览已隐藏 - 显示预览评论 - 隐藏感谢按钮 - 感谢按钮已隐藏 - 已显示感谢按钮 - 隐藏时间戳和表情按钮 - 隐藏时间戳和表情按钮 - 显示时间戳和表情按钮 - - - 隐藏众筹框 - 聚合供资框已隐藏 - 显示集资框 - - + 隐藏结束屏幕卡片 结束屏幕卡已隐藏 显示结束屏卡 - - 过滤栏 - 在新闻源、搜索和相关视频中隐藏或显示过滤栏 - 在新闻源中隐藏 - 在订阅中隐藏 - 在订阅源中显示 - 在搜索中隐藏 - 在搜索中隐藏 - 在搜索中显示 - 隐藏相关视频 - 在相关视频中隐藏 - 显示在相关视频 - - - 隐藏浮动麦克风 - 麦克风按钮隐藏 - 麦克风按钮显示 - - + 全屏禁用环境模式 环境模式已禁用 环境模式已启用 - + 隐藏信息卡 已隐藏信息卡片 已显示信息卡片 - + 禁用滚动数动画 滚动数字不是动画 滚动数字已动画 - + 在视频播放器中隐藏搜索栏 视频播放器搜索栏已隐藏 视频播放器搜索栏已显示 @@ -596,7 +589,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 缩略图搜索栏已隐藏 显示缩略图搜索栏 - + 在主页动态中隐藏 Shorts 主页动态中的 Shorts已隐藏 @@ -694,27 +687,27 @@ This is because Crowdin requires temporarily flattening this file and removing t 导航栏已隐藏 显示导航栏 - + 禁用建议视频结束屏幕 建议视频将被禁用 建议视频将被显示 - + 隐藏视频时间戳 时间戳已隐藏 显示时间戳 - + 隐藏播放器弹出面板 播放器弹出面板已隐藏 显示播放器弹出面板 - + 玩家覆盖不透明度 0-100之间的不透明度值, 其中0是透明的 玩家叠加层透明度必须介于 0-100 之间 - + 不喜欢暂时不可用 (API 超时) 不喜欢不可用(状态 %d) @@ -758,17 +751,22 @@ This is because Crowdin requires temporarily flattening this file and removing t 客户端费率限制遇到的 %d 次 %d 毫秒 - + 启用宽搜索栏 宽搜索栏已启用 宽搜索栏已禁用 - + + 启用高质量缩略图 + 搜索栏缩略图质量高 + 搜索栏缩略图为中等质量 + 全屏寻找缩略图质量高 + 全屏寻找缩略图为中等质量 恢复旧的搜索栏缩略图 搜索栏缩略图将出现在搜索栏上 搜索栏缩略图将显示在全屏显示 - + 启用 SponsorBlock SponsorBlock是一个由众人组成的系统,用于跳过 YouTube 视频中烦人的部分 外观 @@ -948,7 +946,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 关于 数据由 SpongorBlock API 提供。点击此处了解更多信息并查看其他平台的下载 - + 伪装应用程序版本 客户端版本已伪装 客户端版本未伪装 @@ -961,9 +959,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - 还原宽视频速度 & 画质菜单 18.09.39 - 还原库标签 17.41.37 - 还原旧的播放列表 - 17.33.42 - 还原旧界面布局 - + 设置起始页 默认 浏览频道 @@ -981,18 +978,18 @@ This is because Crowdin requires temporarily flattening this file and removing t 热门主题 稍后查看 - - 禁用恢复快捷播放器 - 应用启动时短暂播放器将不会恢复 + 应用启动时短暂播放器将恢复 - + + + 启用平板电脑布局 平板电脑布局已启用 平板电脑布局已禁用 社区帖子不显示在平板电脑布局 - + 小播放器 更改应用最小化播放器中的样式 最小播放器类型 @@ -1011,6 +1008,9 @@ This is because Crowdin requires temporarily flattening this file and removing t 启用拖放功能 拖放已启用\n\n迷你播放器可以拖动到屏幕的任何角 拖放已禁用 + 启用水平拖动手势 + 水平拖动手势启用\n\n迷你玩家可以从屏幕上拖动到左边或右边。 + 水平拖动手势已禁用 隐藏关闭按钮 关闭按钮已隐藏 显示关闭按钮 @@ -1029,12 +1029,12 @@ This is because Crowdin requires temporarily flattening this file and removing t 0-100之间的不透明度值, 其中0是透明的 最小播放器覆盖不透明度必须介于 0-100 之间 - + 启用渐变加载屏幕 加载屏幕时将有渐变背景 加载屏幕将有一个坚实的背景 - + 启用自定义搜索栏颜色 自定义搜索栏颜色 显示原始搜索栏颜色 @@ -1042,12 +1042,12 @@ This is because Crowdin requires temporarily flattening this file and removing t 搜索栏的颜色 无效的搜索栏颜色值 - + 旁路图像区域限制 使用图像主机 yt4.ggpht.com 使用原始图像主机\n\n启用此功能可以修复在某些区域被阻止的缺失图像 - + 主页标签 @@ -1079,7 +1079,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 暂时不可用 (状态码: %s) 暂时不可用 - + 显示折叠通知 启动时显示通知 启动时不显示通知 @@ -1087,47 +1087,47 @@ This is because Crowdin requires temporarily flattening this file and removing t 无法连接到通知提供商 忽略 - + Warning 您的观察历史未被保存。<br><br>这很可能是由DNS 广告拦截器或网络代理引起的。<br><br>要解决这个问题,请将 <b>s.youtube.com</b> 或关闭所有 DNS 拦截器和代理人。 不再显示 - + 启用自动重复 自动重复已启用 自动重复已禁用 - + 伪装设备尺寸 设备尺寸已伪装\n\n可能解锁更高的视频画质,但可能会遇到视频播放迟缓、电池寿命更短等未知副作用 设备尺寸未伪装\n\n启用此功能可以解锁更高的视频画质 启用此选项可能会导致视频播放卡顿、电池寿命更短等未知副作用。 - + GmsCore 设置 GmsCore 设置 - + 绕过 URL 重定向 URL 重定向将被跳过 URL 重定向将不被跳过 - + 在浏览器中打开链接 对外打开链接 在应用中打开链接 - + 删除跟踪查询参数 跟踪查询参数已从链接中删除 跟踪查询参数未从链接中删除 - + 禁用缩放瞄准镜 禁用休眠 振动功能已启用 - + 自动 记住视频质量变化 质量变化适用于所有视频 @@ -1138,35 +1138,38 @@ This is because Crowdin requires temporarily flattening this file and removing t 无线网络 更改默认的 %1$s 质量到: %2$s - + 显示速度对话框按钮 按钮已显示 未显示按钮 - + + 自定义播放速度菜单 + 显示自定义速度菜单 + 自定义速度菜单未显示 自定义播放速度 - 添加或更改可用的播放速度 + 添加或更改自定义播放速度 自定义速度必须小于 %s。使用默认值。 无效的自定义播放速度。使用默认值。 - + 记住播放速度变化 播放速度变化适用于所有视频 播放速度变化仅适用于当前视频 默认播放速度 更改默认速度到: %s - + 还原旧的视频质量菜单 显示旧视频质量菜单 未显示旧视频质量菜单 - + 启用幻灯片搜索 滑动以定位已启用 未启用滑动搜索 - + Spoof 视频流 用来防止播放问题的客户端视频流 Spoof 视频流 @@ -1183,17 +1186,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Android VR 伪装副作用 • 音轨菜单缺少\n• 稳定音量不可用 - - - - + 屏蔽音频广告 音频广告被阻止 音频广告已解除阻止 - + %s 不可用。广告可能会显示。请尝试在设置中切换到另一个广告块服务。 %s 服务器返回错误。广告可能会显示。请尝试在设置中切换到另一个广告块服务。 屏蔽嵌入式视频广告 @@ -1201,30 +1201,30 @@ This is because Crowdin requires temporarily flattening this file and removing t 亮色代理 PurpleAdBlock 代理 - + 屏蔽视频广告 视频广告被阻止 视频广告已解除阻止 - + 消息已删除 显示已删除的消息 不显示已删除的消息 隐藏破坏者后面已删除的消息 将已删除的消息显示为交叉文本 - + 自动认领频道点 频道点是自动认领的 频道点不是自动认领的 - + 启用 Twitch 调试模式 Twitch 调试模式已启用(不推荐) Twitch调试模式已禁用 - + 光学设置 广告 广告屏蔽设置 diff --git a/src/main/resources/addresources/values-zh-rTW/strings.xml b/patches/src/main/resources/addresources/values-zh-rTW/strings.xml similarity index 92% rename from src/main/resources/addresources/values-zh-rTW/strings.xml rename to patches/src/main/resources/addresources/values-zh-rTW/strings.xml index c74f37b81..4c0dad7c2 100644 --- a/src/main/resources/addresources/values-zh-rTW/strings.xml +++ b/patches/src/main/resources/addresources/values-zh-rTW/strings.xml @@ -32,18 +32,18 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + 檢查失敗 開啟官方網站 忽略 - <h5>此應用程式似乎並非由您修補。</h5><br>該應用程式可能無法正常運作,<b>可能有害甚至存在危險。</b><br><br>這些檢查表明該應用程式是預先修補的或來自其他來源:<br><br><small>%1$s</small><br>強烈建議<b>解除安裝此應用程式並自行修補</b>,以確保您使用的是經過驗證且安全的應用程式。<p><br>如果忽略,此警告僅會顯示兩次。 + <h5>這個應用程式似乎並非由您修補。</h5><br>這個應用程式可能無法正常運作,<b>可能有害甚至存在危險。</b><br><br>這些檢查表明該應用程式是預先修補的或來自其他來源:<br><br><small>%1$s</small><br>強烈建議<b>解除安裝此應用程式並自行修補</b>,以確保您使用的是經過驗證且安全的應用程式。<p><br>如果忽略,此警告僅會顯示兩次。 在其他裝置上修補 未由 ReVanced Manager 安裝 修補時間超過 10 分鐘 修補於 %s 天前 APK 構建日期已損壞 - + 確定要繼續嗎? 重設 套用並重新啟動 @@ -62,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 官方連結 捐贈 - + 未安裝 MicroG GmsCore。請前往安裝。 需要採取行動 @@ -73,19 +73,24 @@ This is because Crowdin requires temporarily flattening this file and removing t - + 關於 廣告 替代縮圖 動態消息 播放器 通用版面 - 進度列 + 拖拽欄 滑動控制 其他 影片 - + + 停用背景播放 Shorts + 已停用背景播放 Shorts + 已啟用背景播放 Shorts + + 偵錯 啟用或停用偵錯選項 偵錯記錄檔 @@ -102,13 +107,19 @@ This is because Crowdin requires temporarily flattening this file and removing t 若發生錯誤,不顯示提示 關閉錯誤提示並隱藏所有 ReVanced 錯誤通知。\n\n您將不會收到任何意外事件的通知。 - + 停用「喜歡」和「訂閱」按鈕的發光效果 提及時,「喜歡」和「訂閱」按鈕不會發光 提及時,「喜歡」和「訂閱」按鈕會發光 - 隱藏灰色分隔線 - 已隱藏灰色分隔線 - 已顯示灰色分隔線 + 隱藏專輯卡 + 已隱藏專輯卡 + 已顯示專輯卡 + 隱藏群眾募資 + 已隱藏群眾募資 + 已顯示群眾募資 + 隱藏語音辨識按鈕 + 已隱藏語音辨識按鈕 + 已顯示語音辨識按鈕 隱藏頻道浮水印 已隱藏影片浮水印 已顯示影片浮水印 @@ -153,9 +164,6 @@ This is because Crowdin requires temporarily flattening this file and removing t 隱藏影片下方的章節選擇欄 已隱藏影片下方的章節選擇欄 已顯示影片下方的章節選擇欄 - 隱藏畫質選單底部 - 已隱藏畫質選單底部 - 已顯示畫質選單底部 隱藏社群貼文 已隱藏社群貼文 已顯示社群貼文 @@ -230,6 +238,37 @@ This is because Crowdin requires temporarily flattening this file and removing t 已顯示字幕記錄區 影片描述欄 隱藏或顯示影片描述欄內容 + 篩選列 + 隱藏或顯示動態消息、搜尋、相關影片篩選列 + 在動態消息中隱藏 + 在動態消息中隱藏 + 在動態消息中顯示 + 在搜尋中隱藏 + 在搜尋中隱藏 + 在搜尋中顯示 + 隱藏相關影片 + 在相關影片中隱藏 + 在相關影片中顯示 + 留言區 + 隱藏或顯示影片留言區內容 + 隱藏「會員留言」標題 + 已隱藏「會員留言」標題 + 已顯示「會員留言」標題 + 隱藏留言區 + 已隱藏留言區塊 + 已顯示留言區塊 + 隱藏「建立 Short」按鈕 + 已隱藏「建立 Short」按鈕 + 已顯示「建立 Short」按鈕 + 隱藏留言預覽 + 已隱藏留言預覽 + 已顯示留言預覽 + 隱藏超級感謝按鈕 + 已隱藏超級感謝按鈕 + 已顯示超級感謝按鈕 + 隱藏時間戳記和表情按鈕 + 已隱藏時間戳記和表情符號按鈕 + 已顯示時間戳記和表情符號按鈕 隱藏 YouTube Doodles 已隱藏位於搜尋欄的 Doodles @@ -271,7 +310,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 關鍵字過短,需要加上引號:%s 關鍵字將隱藏所有影片:%s - + 隱藏一般廣告 已隱藏一般廣告 已顯示一般廣告 @@ -290,6 +329,9 @@ This is because Crowdin requires temporarily flattening this file and removing t 隱藏檢視產品橫幅 已隱藏橫幅 已顯示橫幅 + 隱藏播放器 [購物匣] + 已隱藏 [購物匣] + 已顯示 [購物匣] 隱藏影片描述欄商店連結 已隱藏商店連結 已顯示商店連結 @@ -306,17 +348,17 @@ This is because Crowdin requires temporarily flattening this file and removing t 只能在舊裝置上使用隱藏全螢幕廣告 - + 隱藏 YouTube Premium 推廣 已隱藏影片播放器下方的 YouTube Premium 推廣 已顯示影片播放器下方的 YouTube Premium 推廣 - + 隱藏影片廣告 已隱藏影片廣告 已顯示影片廣告 - + 已複製 URL 到剪貼簿 已複製時間戳記 URL 顯示複製影片 URL 按鈕 @@ -326,13 +368,13 @@ This is because Crowdin requires temporarily flattening this file and removing t 已顯示按鈕。點擊將複製帶有時間戳記的影片 URL。點擊並按住將複製不帶時間戳記的影片 URL 未顯示按鈕 - + 移除謹慎觀看對話框 將移除對話框 將顯示對話框 此功能無法繞過年齡限制。它只是自動按下確認。 - + 外部下載 使用外部下載器的設定 顯示外部下載按鈕 @@ -346,17 +388,17 @@ This is because Crowdin requires temporarily flattening this file and removing t 已安裝的外部下載程式的套件名稱,例如 NewPipe 或 Seal 未安裝 %s。請前往安裝。 - + 停用精確搜尋手勢 已停用手勢 已啟用手勢 - + 啟用進度列點擊 已啟用進度列點擊 已停用進度列點擊 - + 啟用亮度手勢 已啟用亮度滑動 已停用亮度滑動 @@ -385,12 +427,12 @@ This is because Crowdin requires temporarily flattening this file and removing t 滑動幅度臨界點 滑動幅度臨界點 - + 停用自動產生字幕 已停用自動產生字幕 已啟用自動產生字幕 - + 動作按鈕 隱藏或顯示影片下方的按鈕 隱藏「喜歡」和「不喜歡」數 @@ -426,23 +468,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 已隱藏儲存到播放列表 已顯示儲存到播放列表 - - 隱藏自動播放按鈕 - 已隱藏自動播放按鈕 - 已顯示自動播放按鈕 - - - - 隱藏字幕按鈕 - 已隱藏字幕按鈕 - 已顯示字幕按鈕 - - - 隱藏投放按鈕 - 已隱藏投放按鈕 - 已顯示投放按鈕 - - + 導覽列按鈕 隱藏或變更導覽區按鈕 @@ -469,7 +495,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 已隱藏標籤 已顯示標籤 - + 介面選單 隱藏或顯示播放器彈出式選單 @@ -480,6 +506,10 @@ This is because Crowdin requires temporarily flattening this file and removing t 隱藏其他設定 已隱藏其他設定 已顯示其他設定 + + 隱藏 [睡眠計時器] + 已隱藏 [睡眠計時器] + 已顯示 [睡眠計時器] 隱藏循環播放影片 已隱藏循環播放選項 @@ -488,6 +518,9 @@ This is because Crowdin requires temporarily flattening this file and removing t 隱藏微光效果 已隱藏微光模式選項 已顯示微光效果模式 + 隱藏 [平衡音量] + 已顯示 [平衡音量] + 已隱藏 [平衡音量] 隱藏說明和意見回饋 已隱藏說明與回饋選單 @@ -513,83 +546,46 @@ This is because Crowdin requires temporarily flattening this file and removing t 隱藏在 VR 觀看 已隱藏在 VR 觀看 已顯示在 VR 觀看 + 隱藏 [影片畫質] 選單頁尾 + 已隱藏 [影片畫質] 選單頁尾 + 已顯示 [影片畫質] 選單頁尾 - - 隱藏上一部和下一部影片按紐 - 已隱藏按鈕 - 已顯示按鈕 + + 隱藏上一部和下一部影片按紐 + 已隱藏按鈕 + 已顯示按鈕 + 隱藏投放按鈕 + 已隱藏投放按鈕 + 已顯示投放按鈕 + + 隱藏字幕按鈕 + 已隱藏字幕按鈕 + 已顯示字幕按鈕 + 隱藏自動播放按鈕 + 已隱藏自動播放按鈕 + 已顯示自動播放按鈕 - - 隱藏專輯卡 - 已隱藏專輯卡 - 已顯示專輯卡 - - - 留言區 - 隱藏或顯示影片留言區內容 - 隱藏「會員留言」標題 - 已隱藏「會員留言」標題 - 已顯示「會員留言」標題 - 隱藏留言區 - 已隱藏留言區塊 - 已顯示留言區塊 - 隱藏「建立 Short」按鈕 - 已隱藏「建立 Short」按鈕 - 已顯示「建立 Short」按鈕 - 隱藏留言預覽 - 已隱藏留言預覽 - 已顯示留言預覽 - 隱藏超級感謝按鈕 - 已隱藏超級感謝按鈕 - 已顯示超級感謝按鈕 - 隱藏時間戳記和表情按鈕 - 已隱藏時間戳記和表情符號按鈕 - 已顯示時間戳記和表情符號按鈕 - - - 隱藏群眾募資 - 已隱藏群眾募資 - 已顯示群眾募資 - - + 隱藏片尾資訊卡 已隱藏片尾資訊卡 已顯示片尾資訊卡 - - 篩選列 - 隱藏或顯示動態消息、搜尋、相關影片篩選列 - 在動態消息中隱藏 - 在動態消息中隱藏 - 在動態消息中顯示 - 在搜尋中隱藏 - 在搜尋中隱藏 - 在搜尋中顯示 - 隱藏相關影片 - 在相關影片中隱藏 - 在相關影片中顯示 - - - 隱藏語音辨識按鈕 - 已隱藏語音辨識按鈕 - 已顯示語音辨識按鈕 - - + 在全螢幕狀態下停用微光效果 已停用微光效果 已啟用微光效果 - + 隱藏資訊卡 已隱藏資訊卡 已顯示資訊卡 - + 停用數字滾動動畫 非動畫滾動數字 動畫滾動數字 - + 隱藏影片播放器進度列 已隱藏影片播放器進度列 已顯示影片播放器進度列 @@ -597,7 +593,9 @@ This is because Crowdin requires temporarily flattening this file and removing t 已隱藏縮圖進度列 已顯示縮圖進度列 - + + Shorts 播放器 + 隱藏或顯示 Shorts 播放器的元件 隱藏 Shorts 首頁動態消息 已在首頁動態影片中隱藏 Shorts @@ -644,6 +642,9 @@ This is because Crowdin requires temporarily flattening this file and removing t 隱藏 [綠色畫面] 按鈕 已隱藏 [綠色畫面] 按鈕 已顯示 [綠色畫面] 按鈕 + 隱藏主題標籤按鈕 + 主題標籤按鈕已隱藏 + 主題標籤按鈕已顯示 隱藏搜尋建議 已隱藏搜尋建議 已顯示搜尋建議 @@ -692,27 +693,27 @@ This is because Crowdin requires temporarily flattening this file and removing t 已隱藏導覽區 已顯示導覽區 - + 停用片尾建議影片 將停用建議影片 將顯示建議影片 - + 隱藏影片時間戳記 已隱藏時間戳記 已顯示時間戳記 - + 隱藏播放器彈出面板 已隱藏播放器彈出面板 已顯示播放器彈出面板 - + 播放器覆蓋透明度 不透明度值介於 0-100 之間,0 為透明 播放器覆蓋層的不透明度必須在 0-100 之間 - + 暫時無法使用「不喜歡」數(API 超時) 無法使用「不喜歡」數(狀態 %d) @@ -756,17 +757,23 @@ This is because Crowdin requires temporarily flattening this file and removing t 遇到 %d 次用戶端頻率限制 %d 毫秒 - + 啟用寬搜尋列 已啟用寬搜尋列 已停用寬搜尋列 - + + 啟用高畫質縮圖 + 已設定拖拽欄為高畫質縮圖 + 已設定拖拽欄為中畫質縮圖 + 已設定全螢幕拖拽欄為高畫質縮圖 + 已設定全螢幕拖拽欄為中畫質縮圖 + 此操作將復原沒有拖拽欄的即時串流縮圖\n\n拖拽欄縮圖將使用與目前影片的同等畫質。\n\n此功能在您使用高速網際網路,並使用在 720p 或更低的畫質時為最佳狀態。 還原舊版進度列縮圖 進度列縮圖將出現在進度列上 進度列縮圖將出現在全螢幕畫面 - + 啟用 SponsorBlock SponsorBlock 是透過使用者共同編輯,新增片段來跳過 YouTube 的擾人片段 外觀 @@ -947,7 +954,7 @@ This is because Crowdin requires temporarily flattening this file and removing t 關於 資料由 SponsorBlock API 提供。點擊此處來瞭解更多資訊和查看其他平台的下載 - + 欺騙應用程式版本 已欺騙版本 未欺騙版本 @@ -960,9 +967,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - 還原寬影片速度 & 畫質選單 18.09.39 - 還原庫標籤 17.41.37 - 還原舊版播放清單匣 - 17.33.42 - 還原舊版 UI 介面 - + 設定起始頁面 預設 瀏覽頻道 @@ -980,18 +986,26 @@ This is because Crowdin requires temporarily flattening this file and removing t 發燒影片 稍後觀看 - + 停用自動復原 Shorts 播放器 Shorts 播放器將不會在應用程式啟動後復原 Shorts 播放器將會在應用程式啟動後復原 - + + 自動播放 Shorts + Shorts 將自動播放 + Shorts 將重複播放 + 在背景自動播放 Shorts + Shorts 將在背景自動播放 + Shorts 將在背景重複播放 + + 啟用平板介面 已啟用平板介面 已停用平板介面 社群貼文不會顯示在平板介面上 - + 迷你播放器 更改應用程式內縮小播放器的樣式 迷你播放器類型 @@ -1011,6 +1025,9 @@ This is because Crowdin requires temporarily flattening this file and removing t 啟用拖曳 已啟用拖曳\n\n現在迷你播放器可以拖曳至螢幕的任何角落 已停用拖曳 + 啟用水平拖拽手勢 + 已啟用水平拖拽手勢\n\n迷你播放器現可拖拽至螢幕的左側或右側外。 + 已停用水平拖拽手勢 隱藏 [關閉] 按鈕 已隱藏 [關閉] 按鈕 已顯示 [關閉] 按鈕 @@ -1030,12 +1047,12 @@ This is because Crowdin requires temporarily flattening this file and removing t 不透明度值介於 0-100 之間,0 為透明 迷你播放器覆蓋層的不透明度必須在 0-100 之間 - + 啟用漸層載入畫面 載入畫面將有漸層背景 載入畫面將有純色背景 - + 啟用自訂進度列顏色 已顯示自訂進度列顏色 已顯示原版進度列顏色 @@ -1043,12 +1060,12 @@ This is because Crowdin requires temporarily flattening this file and removing t 進度列顏色 滑動桿色彩值無效 - + 繞過圖片區域限制 使用圖片主機 yt4.ggpht.com 使用原始圖片主機\n\n啟用此功能可以修正在某些區域被封鎖的遺失圖片 - + 首頁標籤 @@ -1080,7 +1097,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow 暫時無法使用(狀態代碼:%s) DeArrow 暫時無法使用 - + 顯示 ReVanced 公告 啟動時顯示公告 啟動時不顯示公告 @@ -1088,47 +1105,47 @@ This is because Crowdin requires temporarily flattening this file and removing t 無法連線到公告提供者 忽略 - + 警告 你的觀看記錄未被儲存。<br><br>這很可能是由於 DNS 廣告封鎖器或網路代理所導致。<br><br>若要解決此問題,請將 <b>s.youtube.com</b> 加入白名單,或關閉所有 DNS 封鎖器和代理。 不要再顯示 - + 啟用自動循環播放 已啟用自動循環播放 已停用自動循環播放 - + 欺騙裝置尺寸 已欺騙裝置尺寸\n\n將可能會解鎖更高的影片畫質,但您可能會遇到影片播放不順暢、更差的電池續航和未知的副作用 未欺騙裝置尺寸\n\n啟用此功能可以解鎖更高的影片畫質 啟用此功能可能會遇到影片播放不順暢、更差的電池續航和未知的副作用 - + GmsCore 設定 GmsCore 設定 - + 繞過 URL 重新導向 已繞過 URL 重新導向 未繞過 URL 重新導向 - + 在瀏覽器中開啟連結 在外部開啟連結 在應用程式中開啟連結 - + 移除追蹤查詢參數 已從連結移除追蹤查詢參數 未從連結移除追蹤查詢參數 - + 停用縮放震動 已停用震動 已啟用震動 - + 自動 記住影片畫質 變更畫質套用到所有影片 @@ -1139,35 +1156,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi 變更畫質預設 %1$s 為 %2$s - + 顯示速度對話框按鈕 已顯示按鈕 未顯示按鈕 - + + 自訂 [播放速度] 選單 + 已顯示自訂 [播放速度] 選單 + 已隱藏自訂 [播放速度] 選單 自訂播放速度 - 新增或變更可用的播放速度 + 新增或更改自訂播放速度 自訂速度必須小於 %s。已重設為預設值。 無效的自訂播放速度。已重設為預設值。 - + 記住播放速度 變更播放速度套用到所有影片 變更播放速度僅套用到目前影片 預設播放速度 變更預設速度為:%s - + 還原舊版影片畫質選單 已顯示舊版影片畫質選單 未顯示舊版影片畫質選單 - + 啟用滑動預覽 已啟用滑動預覽 未啟用滑動預覽 - + 欺騙影片串流 欺騙用戶端影片串流以避免播放問題 欺騙影片串流 @@ -1185,17 +1205,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Android VR 欺騙副作用 • 音訊軌道選單缺失\n• 穩定音量不可用 - - - - + 阻擋音訊廣告 已阻擋音訊廣告 未阻擋音訊廣告 - + %s 暫時無法使用。所以可能會顯示廣告。請嘗試在設定中切換為另外一個廣告阻擋服務。 %s 伺服器回傳錯誤。所以可能會顯示廣告。請嘗試在設定中切換為另外一個廣告阻擋服務。 阻擋嵌入式影片廣告 @@ -1203,30 +1220,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Luminous 代理 PurpleAdBlock 代理 - + 阻擋影片廣告 已阻擋影片廣告 未阻擋影片廣告 - + 已刪除訊息 顯示已刪除的訊息 不顯示已刪除的訊息 將已刪除的訊息隱藏在劇透後面 將已刪除的訊息顯示為劃掉的文字 - + 自動領取頻道忠誠點數 已自動地領取頻道忠誠點數 未自動地領取頻道忠誠點數 - + 啟用 Twitch 除錯模式 已啟用 Twitch 除錯模式(不推薦啟用) 已停用 Twitch 除錯模式 - + ReVanced 設定 廣告 廣告阻擋設定 diff --git a/patches/src/main/resources/addresources/values-zu-rZA/strings.xml b/patches/src/main/resources/addresources/values-zu-rZA/strings.xml new file mode 100644 index 000000000..6892649b0 --- /dev/null +++ b/patches/src/main/resources/addresources/values-zu-rZA/strings.xml @@ -0,0 +1,242 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/addresources/values/arrays.xml b/patches/src/main/resources/addresources/values/arrays.xml similarity index 90% rename from src/main/resources/addresources/values/arrays.xml rename to patches/src/main/resources/addresources/values/arrays.xml index 829f9533e..7c3207d72 100644 --- a/src/main/resources/addresources/values/arrays.xml +++ b/patches/src/main/resources/addresources/values/arrays.xml @@ -1,34 +1,32 @@ - + Android VR iOS - + ANDROID_VR IOS - + @string/revanced_spoof_app_version_target_entry_1 @string/revanced_spoof_app_version_target_entry_2 @string/revanced_spoof_app_version_target_entry_3 @string/revanced_spoof_app_version_target_entry_4 - @string/revanced_spoof_app_version_target_entry_5 18.33.40 18.20.39 18.09.39 17.41.37 - 17.33.42 - + @string/revanced_miniplayer_type_entry_1 @string/revanced_miniplayer_type_entry_2 @@ -38,7 +36,7 @@ @string/revanced_miniplayer_type_entry_6 - + ORIGINAL PHONE TABLET @@ -57,7 +55,7 @@ TABLET - + @string/revanced_change_start_page_entry_default @string/revanced_change_start_page_entry_search @@ -77,7 +75,7 @@ @string/revanced_change_start_page_entry_browse - + ORIGINAL SEARCH @@ -98,7 +96,7 @@ BROWSE - + @string/revanced_alt_thumbnail_options_entry_1 @string/revanced_alt_thumbnail_options_entry_2 @@ -106,7 +104,7 @@ @string/revanced_alt_thumbnail_options_entry_4 - + ORIGINAL DEARROW DEARROW_STILL_IMAGES @@ -123,7 +121,7 @@ END - + @string/revanced_video_quality_default_entry_1 @string/revanced_video_quality_default_entry_2 @@ -149,7 +147,7 @@ - + @string/revanced_show_deleted_messages_entry_1 @string/revanced_show_deleted_messages_entry_2 @@ -161,10 +159,9 @@ cross-out - - + - + @string/revanced_block_embedded_ads_entry_1 @string/revanced_block_embedded_ads_entry_2 diff --git a/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml similarity index 93% rename from src/main/resources/addresources/values/strings.xml rename to patches/src/main/resources/addresources/values/strings.xml index 712d8d2fd..d6950433b 100644 --- a/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -31,7 +31,7 @@ This is because Crowdin requires temporarily flattening this file and removing t --> - + Checks failed Open official website Ignore @@ -42,7 +42,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Patched %s days ago APK build date is corrupted - + ReVanced Do you wish to proceed? Reset @@ -62,7 +62,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Official links Donate - + MicroG GmsCore is not installed. Install it. Action needed @@ -73,7 +73,7 @@ This is because Crowdin requires temporarily flattening this file and removing t - + About Ads Alternative thumbnails @@ -86,7 +86,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Misc Video - + + Disable Shorts background play + Shorts background play is disabled + Shorts background play is enabled + + Debugging Enable or disable debugging options Debug logging @@ -103,13 +108,19 @@ This is because Crowdin requires temporarily flattening this file and removing t Toast not shown if error occurs Turning off error toasts hides all ReVanced error notifications.\n\nYou will not be notified of any unexpected events. - + Disable like / subscribe button glow Like and subscribe button will not glow when mentioned Like and subscribe button will glow when mentioned - Hide gray separator - Gray separators are hidden - Gray separators are shown + Hide album cards + Album cards are hidden + Album cards are shown + Hide crowdfunding box + Crowdfunding box is hidden + Crowdfunding box is shown + Hide floating microphone button + Microphone button hidden + Microphone button shown Hide channel watermark Watermark is hidden Watermark is shown @@ -154,9 +165,6 @@ This is because Crowdin requires temporarily flattening this file and removing t Hide expandable chip under videos Expandable chips are hidden Expandable chips are shown - Hide video quality menu footer - Video quality menu footer is hidden - Video quality menu footer is shown Hide community posts Community posts are hidden Community posts are shown @@ -232,6 +240,39 @@ This is because Crowdin requires temporarily flattening this file and removing t Video description Hide or show video description components + Filter bar + Hide or show the filter bar in the feed, search, and related videos + Hide in feed + Hidden in feed + Shown in feed + Hide in search + Hidden in search + Shown in search + Hide in related videos + Hidden in related videos + Shown in related videos + + Comments + Hide or show comments section components + Hide \'Comments by members\' header + \'Comments by members\' header is hidden + \'Comments by members\' header is shown + Hide comments section + Comments section is hidden + Comments section is shown + Hide \'Create a Short\' button + \'Create a Short\' button is hidden + \'Create a Short\' button is shown + Hide preview comment + Preview comment is hidden + Preview comment is shown + Hide thanks button + Thanks button is hidden + Thanks button is shown + Hide timestamp and emoji buttons + Timestamp and emoji buttons are hidden + Timestamp and emoji buttons are shown + Hide YouTube Doodles Search bar Doodles are hidden @@ -275,7 +316,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Keyword is too short and requires quotes: %s Keyword will hide all videos: %s - + Hide general ads General ads are hidden General ads are shown @@ -294,6 +335,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Hide banner to view products Banner is hidden Banner is shown + Hide player shopping shelf + Shopping shelf is hidden + Shopping shelf is shown Hide shopping links in video description Shopping links are hidden Shopping links are shown @@ -310,17 +354,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Hide fullscreen ads only works with older devices - + Hide YouTube Premium promotions YouTube Premium promotions under video player are hidden YouTube Premium promotions under video player are shown - + Hide video ads Video ads are hidden Video ads are shown - + URL copied to clipboard URL with timestamp copied Show copy video URL button @@ -330,13 +374,13 @@ This is because Crowdin requires temporarily flattening this file and removing t Button is shown. Tap to copy video URL with timestamp. Tap and hold to copy video without timestamp Button is not shown - + Remove viewer discretion dialog Dialog will be removed Dialog will be shown This does not bypass the age restriction. It just accepts it automatically. - + External downloads Settings for using an external downloader Show external download button @@ -350,17 +394,17 @@ This is because Crowdin requires temporarily flattening this file and removing t Package name of your installed external downloader app, such as NewPipe or Seal %s is not installed. Please install it. - + Disable precise seeking gesture Gesture is disabled Gesture is enabled - + Enable seekbar tapping Seekbar tapping is enabled Seekbar tapping is disabled - + Enable brightness gesture Brightness swipe is enabled Brightness swipe is disabled @@ -389,12 +433,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Swipe magnitude threshold The amount of threshold for swipe to occur - + Disable auto captions Auto captions are disabled Auto captions are enabled - + Action buttons Hide or show buttons under videos Hide Like and Dislike @@ -430,23 +474,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Save to playlist button is hidden Save to playlist button is shown - - Hide autoplay button - Autoplay button is hidden - Autoplay button is shown - - - - Hide captions button - Captions button is hidden - Captions button is shown - - - Hide cast button - Cast button is hidden - Cast button is shown - - + Navigation buttons Hide or change buttons in the navigation bar @@ -473,7 +501,7 @@ This is because Crowdin requires temporarily flattening this file and removing t Labels are hidden Labels are shown - + Flyout menu Hide or show player flyout menu items @@ -484,6 +512,10 @@ This is because Crowdin requires temporarily flattening this file and removing t Hide Additional settings Additional settings menu is hidden Additional settings menu is shown + + Hide Sleep timer + Sleep timer menu is hidden + Sleep timer menu is shown Hide Loop video Loop video menu is hidden @@ -492,6 +524,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Hide Ambient mode Ambient mode menu is hidden Ambient mode menu is shown + Hide Stable volume + Stable volume menu is shown + Stable volume menu is hidden Hide Help & feedback Help & feedback menu is hidden @@ -517,83 +552,46 @@ This is because Crowdin requires temporarily flattening this file and removing t Hide Watch in VR Watch in VR menu is hidden Watch in VR menu is shown + Hide video quality menu footer + Video quality menu footer is hidden + Video quality menu footer is shown - - Hide previous & next video buttons - Buttons are hidden - Buttons are shown + + Hide previous & next video buttons + Buttons are hidden + Buttons are shown + Hide cast button + Cast button is hidden + Cast button is shown + + Hide captions button + Captions button is hidden + Captions button is shown + Hide autoplay button + Autoplay button is hidden + Autoplay button is shown - - Hide album cards - Album cards are hidden - Album cards are shown - - - Comments - Hide or show comments section components - Hide \'Comments by members\' header - \'Comments by members\' header is hidden - \'Comments by members\' header is shown - Hide comments section - Comments section is hidden - Comments section is shown - Hide \'Create a Short\' button - \'Create a Short\' button is hidden - \'Create a Short\' button is shown - Hide preview comment - Preview comment is hidden - Preview comment is shown - Hide thanks button - Thanks button is hidden - Thanks button is shown - Hide timestamp and emoji buttons - Timestamp and emoji buttons are hidden - Timestamp and emoji buttons are shown - - - Hide crowdfunding box - Crowdfunding box is hidden - Crowdfunding box is shown - - + Hide end screen cards End screen cards are hidden End screen cards are shown - - Filter bar - Hide or show the filter bar in the feed, search, and related videos - Hide in feed - Hidden in feed - Shown in feed - Hide in search - Hidden in search - Shown in search - Hide in related videos - Hidden in related videos - Shown in related videos - - - Hide floating microphone button - Microphone button hidden - Microphone button shown - - + Disable ambient mode in fullscreen Ambient mode disabled Ambient mode enabled - + Hide info cards Info cards are hidden Info cards are shown - + Disable rolling number animations Rolling numbers are not animated Rolling numbers are animated - + Hide seekbar in video player Video player seekbar is hidden Video player seekbar is shown @@ -601,7 +599,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Thumbnail seekbar is hidden Thumbnail seekbar is shown - + + Shorts player + Hide or show components in the Shorts player Hide Shorts in home feed Shorts in home feed are hidden @@ -699,27 +699,27 @@ This is because Crowdin requires temporarily flattening this file and removing t Navigation bar is hidden Navigation bar is shown - + Disable suggested video end screen Suggested videos will be disabled Suggested videos will be shown - + Hide video timestamp Timestamp is hidden Timestamp is shown - + Hide player popup panels Player popup panels are hidden Player popup panels are shown - + Player overlay opacity Opacity value between 0-100, where 0 is transparent Player overlay opacity must be between 0-100 - + Return YouTube Dislike Dislikes temporarily not available (API timed out) @@ -766,17 +766,23 @@ This is because Crowdin requires temporarily flattening this file and removing t Client rate limit encountered %d times %d milliseconds - + Enable wide search bar Wide search bar is enabled Wide search bar is disabled - + + Enable high quality thumbnails + Seekbar thumbnails are high quality + Seekbar thumbnails are medium quality + Fullscreen seekbar thumbnails are high quality + Fullscreen seekbar thumbnails are medium quality + This will also restore thumbnails on livestreams that do not have seekbar thumbnails.\n\nSeekbar thumbnails will use the same quality as the current video.\n\nThis feature works best with a video quality of 720p or lower and when using a very fast internet connection. Restore old seekbar thumbnails Seekbar thumbnails will appear above the seekbar Seekbar thumbnails will appear in fullscreen - + SponsorBlock Enable SponsorBlock SponsorBlock is a crowd-sourced system for skipping annoying parts of YouTube videos @@ -959,7 +965,7 @@ This is because Crowdin requires temporarily flattening this file and removing t sponsor.ajay.app Data is provided by the SponsorBlock API. Tap here to learn more and see downloads for other platforms - + Spoof app version Version spoofed Version not spoofed @@ -972,9 +978,8 @@ This is because Crowdin requires temporarily flattening this file and removing t 18.20.39 - Restore wide video speed & quality menu 18.09.39 - Restore library tab 17.41.37 - Restore old playlist shelf - 17.33.42 - Restore old UI layout - + Set start page Default Browse channels @@ -992,18 +997,26 @@ This is because Crowdin requires temporarily flattening this file and removing t Trending Watch later - + Disable resuming Shorts player Shorts player will not resume on app startup Shorts player will resume on app startup - + + Autoplay Shorts + Shorts will autoplay + Shorts will repeat + Autoplay Shorts background play + Shorts background play will autoplay + Shorts background play will repeat + + Enable tablet layout Tablet layout is enabled Tablet layout is disabled Community posts do not show up on tablet layouts x - + Miniplayer Change the style of the in app minimized player Miniplayer type @@ -1022,6 +1035,9 @@ This is because Crowdin requires temporarily flattening this file and removing t Enable drag and drop Drag and drop is enabled\n\nMiniplayer can be dragged to any corner of the screen Drag and drop is disabled + Enable horizontal drag gesture + Horizontal drag gesture enabled\n\nMiniplayer can be dragged off screen to the left or right + Horizontal drag gesture disabled Hide close button Close button is hidden Close button is shown @@ -1041,12 +1057,12 @@ This is because Crowdin requires temporarily flattening this file and removing t Opacity value between 0-100, where 0 is transparent Miniplayer overlay opacity must be between 0-100 - + Enable gradient loading screen Loading screen will have a gradient background Loading screen will have a solid background - + Enable custom seekbar color Custom seekbar color is shown Original seekbar color is shown @@ -1054,12 +1070,12 @@ This is because Crowdin requires temporarily flattening this file and removing t The color of the seekbar Invalid seekbar color value - + Bypass image region restrictions Using image host yt4.ggpht.com Using original image host\n\nEnabling this can fix missing images that are blocked in some regions - + Home tab @@ -1092,7 +1108,7 @@ This is because Crowdin requires temporarily flattening this file and removing t DeArrow temporarily not available (status code: %s) DeArrow temporarily not available - + Show ReVanced announcements Announcements are shown on startup Announcements are not shown on startup @@ -1100,47 +1116,47 @@ This is because Crowdin requires temporarily flattening this file and removing t Failed connecting to announcements provider Dismiss - + Warning Your watch history is not being saved.<br><br>This most likely is caused by a DNS ad blocker or network proxy.<br><br>To fix this, whitelist <b>s.youtube.com</b> or turn off all DNS blockers and proxies. Do not show again - + Enable auto-repeat Auto-repeat is enabled Auto-repeat is disabled - + Spoof device dimensions Device dimensions spoofed\n\nHigher video qualities might be unlocked but you may experience video playback stuttering, worse battery life, and unknown side effects Device dimensions not spoofed\n\nEnabling this can unlock higher video qualities Enabling this can cause video playback stuttering, worse battery life, and unknown side effects. - + GmsCore Settings Settings for GmsCore - + Bypass URL redirects URL redirects are bypassed URL redirects are not bypassed - + Open links in browser Opening links externally Opening links in app - + Remove tracking query parameter Tracking query parameter is removed from links Tracking query parameter is not removed from links - + Disable zoom haptics Haptics are disabled Haptics are enabled - + Automatic quality 2160p 1440p @@ -1159,35 +1175,38 @@ This is because Crowdin requires temporarily flattening this file and removing t wifi Changed default %1$s quality to: %2$s - + Show speed dialog button Button is shown Button is not shown - + + Custom playback speed menu + Custom speed menu is shown + Custom speed menu is not shown Custom playback speeds - Add or change the available playback speeds + Add or change the custom playback speeds Custom speeds must be less than %s. Using default values. Invalid custom playback speeds. Using default values. - + Remember playback speed changes Playback speed changes apply to all videos Playback speed changes only apply to the current video Default playback speed Changed default speed to: %s - + Restore old video quality menu Old video quality menu is shown Old video quality menu is not shown - + Enable slide to seek Slide to seek is enabled Slide to seek is not enabled - + Spoof video streams Spoof the client video streams to prevent playback issues Spoof video streams @@ -1205,20 +1224,14 @@ This is because Crowdin requires temporarily flattening this file and removing t Android VR spoofing side effects • Audio track menu is missing\n• Stable volume is not available - - - Enable auto HDR brightness - Auto HDR brightness is enabled - Auto HDR brightness is disabled - - + Block audio ads Audio ads are blocked Audio ads are unblocked - + %s is unavailable. Ads may show. Try switching to another ad block service in settings. %s server returned an error. Ads may show. Try switching to another ad block service in settings. Block embedded video ads @@ -1226,30 +1239,30 @@ This is because Crowdin requires temporarily flattening this file and removing t Luminous proxy PurpleAdBlock proxy - + Block video ads Video ads are blocked Video ads are unblocked - + message deleted Show deleted messages Do not show deleted messages Hide deleted messages behind a spoiler Show deleted messages as crossed-out text - + Automatically claim Channel Points Channel Points are claimed automatically Channel Points are not claimed automatically - + Enable Twitch debug mode Twitch debug mode is enabled (not recommended) Twitch debug mode is disabled - + ReVanced Settings Ads Ad blocking settings diff --git a/src/main/resources/change-header/revanced-borderless/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/change-header/revanced-borderless/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/change-header/revanced-borderless/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/change-header/revanced-borderless/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/change-header/revanced-borderless/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/change-header/revanced-borderless/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/change-header/revanced-borderless/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/change-header/revanced-borderless/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/change-header/revanced-borderless/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/change-header/revanced-borderless/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/change-header/revanced-borderless/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/change-header/revanced-borderless/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/change-header/revanced-borderless/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/change-header/revanced-borderless/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/change-header/revanced-borderless/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/change-header/revanced-borderless/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/change-header/revanced-borderless/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/change-header/revanced-borderless/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/change-header/revanced-borderless/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/change-header/revanced-borderless/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/change-header/revanced-borderless/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/change-header/revanced-borderless/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/change-header/revanced-borderless/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/change-header/revanced-borderless/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/change-header/revanced-borderless/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/change-header/revanced-borderless/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/change-header/revanced-borderless/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/change-header/revanced-borderless/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/change-header/revanced-borderless/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/change-header/revanced-borderless/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/change-header/revanced-borderless/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/change-header/revanced-borderless/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/change-header/revanced-borderless/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/change-header/revanced-borderless/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/change-header/revanced-borderless/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/change-header/revanced-borderless/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/change-header/revanced-borderless/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/change-header/revanced-borderless/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/change-header/revanced-borderless/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/change-header/revanced-borderless/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/change-header/revanced/drawable-hdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/change-header/revanced/drawable-hdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/change-header/revanced/drawable-hdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/change-header/revanced/drawable-hdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/change-header/revanced/drawable-hdpi/yt_wordmark_header_light.png b/patches/src/main/resources/change-header/revanced/drawable-hdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/change-header/revanced/drawable-hdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/change-header/revanced/drawable-hdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/change-header/revanced/drawable-mdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/change-header/revanced/drawable-mdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/change-header/revanced/drawable-mdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/change-header/revanced/drawable-mdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/change-header/revanced/drawable-mdpi/yt_wordmark_header_light.png b/patches/src/main/resources/change-header/revanced/drawable-mdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/change-header/revanced/drawable-mdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/change-header/revanced/drawable-mdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/change-header/revanced/drawable-xhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/change-header/revanced/drawable-xhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/change-header/revanced/drawable-xhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/change-header/revanced/drawable-xhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/change-header/revanced/drawable-xhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/change-header/revanced/drawable-xhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/change-header/revanced/drawable-xhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/change-header/revanced/drawable-xhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/change-header/revanced/drawable-xxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/change-header/revanced/drawable-xxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/change-header/revanced/drawable-xxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/change-header/revanced/drawable-xxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/change-header/revanced/drawable-xxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/change-header/revanced/drawable-xxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/change-header/revanced/drawable-xxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/change-header/revanced/drawable-xxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/change-header/revanced/drawable-xxxhdpi/yt_wordmark_header_dark.png b/patches/src/main/resources/change-header/revanced/drawable-xxxhdpi/yt_wordmark_header_dark.png similarity index 100% rename from src/main/resources/change-header/revanced/drawable-xxxhdpi/yt_wordmark_header_dark.png rename to patches/src/main/resources/change-header/revanced/drawable-xxxhdpi/yt_wordmark_header_dark.png diff --git a/src/main/resources/change-header/revanced/drawable-xxxhdpi/yt_wordmark_header_light.png b/patches/src/main/resources/change-header/revanced/drawable-xxxhdpi/yt_wordmark_header_light.png similarity index 100% rename from src/main/resources/change-header/revanced/drawable-xxxhdpi/yt_wordmark_header_light.png rename to patches/src/main/resources/change-header/revanced/drawable-xxxhdpi/yt_wordmark_header_light.png diff --git a/src/main/resources/copyvideourl/drawable/revanced_yt_copy.xml b/patches/src/main/resources/copyvideourl/drawable/revanced_yt_copy.xml similarity index 100% rename from src/main/resources/copyvideourl/drawable/revanced_yt_copy.xml rename to patches/src/main/resources/copyvideourl/drawable/revanced_yt_copy.xml diff --git a/src/main/resources/copyvideourl/drawable/revanced_yt_copy_timestamp.xml b/patches/src/main/resources/copyvideourl/drawable/revanced_yt_copy_timestamp.xml similarity index 100% rename from src/main/resources/copyvideourl/drawable/revanced_yt_copy_timestamp.xml rename to patches/src/main/resources/copyvideourl/drawable/revanced_yt_copy_timestamp.xml diff --git a/src/main/resources/copyvideourl/host/layout/youtube_controls_bottom_ui_container.xml b/patches/src/main/resources/copyvideourl/host/layout/youtube_controls_bottom_ui_container.xml similarity index 100% rename from src/main/resources/copyvideourl/host/layout/youtube_controls_bottom_ui_container.xml rename to patches/src/main/resources/copyvideourl/host/layout/youtube_controls_bottom_ui_container.xml diff --git a/src/main/resources/custom-branding/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/custom-branding/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/custom-branding/mipmap-hdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/custom-branding/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/custom-branding/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/custom-branding/mipmap-hdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/custom-branding/mipmap-hdpi/ic_launcher.png b/patches/src/main/resources/custom-branding/mipmap-hdpi/ic_launcher.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-hdpi/ic_launcher.png rename to patches/src/main/resources/custom-branding/mipmap-hdpi/ic_launcher.png diff --git a/src/main/resources/custom-branding/mipmap-hdpi/ic_launcher_round.png b/patches/src/main/resources/custom-branding/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-hdpi/ic_launcher_round.png rename to patches/src/main/resources/custom-branding/mipmap-hdpi/ic_launcher_round.png diff --git a/src/main/resources/custom-branding/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/custom-branding/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/custom-branding/mipmap-mdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/custom-branding/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/custom-branding/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/custom-branding/mipmap-mdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/custom-branding/mipmap-mdpi/ic_launcher.png b/patches/src/main/resources/custom-branding/mipmap-mdpi/ic_launcher.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-mdpi/ic_launcher.png rename to patches/src/main/resources/custom-branding/mipmap-mdpi/ic_launcher.png diff --git a/src/main/resources/custom-branding/mipmap-mdpi/ic_launcher_round.png b/patches/src/main/resources/custom-branding/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-mdpi/ic_launcher_round.png rename to patches/src/main/resources/custom-branding/mipmap-mdpi/ic_launcher_round.png diff --git a/src/main/resources/custom-branding/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/custom-branding/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/custom-branding/mipmap-xhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/custom-branding/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/custom-branding/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/custom-branding/mipmap-xhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/custom-branding/mipmap-xhdpi/ic_launcher.png b/patches/src/main/resources/custom-branding/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-xhdpi/ic_launcher.png rename to patches/src/main/resources/custom-branding/mipmap-xhdpi/ic_launcher.png diff --git a/src/main/resources/custom-branding/mipmap-xhdpi/ic_launcher_round.png b/patches/src/main/resources/custom-branding/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-xhdpi/ic_launcher_round.png rename to patches/src/main/resources/custom-branding/mipmap-xhdpi/ic_launcher_round.png diff --git a/src/main/resources/custom-branding/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/custom-branding/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/custom-branding/mipmap-xxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/custom-branding/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/custom-branding/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/custom-branding/mipmap-xxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/custom-branding/mipmap-xxhdpi/ic_launcher.png b/patches/src/main/resources/custom-branding/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-xxhdpi/ic_launcher.png rename to patches/src/main/resources/custom-branding/mipmap-xxhdpi/ic_launcher.png diff --git a/src/main/resources/custom-branding/mipmap-xxhdpi/ic_launcher_round.png b/patches/src/main/resources/custom-branding/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-xxhdpi/ic_launcher_round.png rename to patches/src/main/resources/custom-branding/mipmap-xxhdpi/ic_launcher_round.png diff --git a/src/main/resources/custom-branding/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png b/patches/src/main/resources/custom-branding/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png rename to patches/src/main/resources/custom-branding/mipmap-xxxhdpi/adaptiveproduct_youtube_background_color_108.png diff --git a/src/main/resources/custom-branding/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png b/patches/src/main/resources/custom-branding/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png rename to patches/src/main/resources/custom-branding/mipmap-xxxhdpi/adaptiveproduct_youtube_foreground_color_108.png diff --git a/src/main/resources/custom-branding/mipmap-xxxhdpi/ic_launcher.png b/patches/src/main/resources/custom-branding/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-xxxhdpi/ic_launcher.png rename to patches/src/main/resources/custom-branding/mipmap-xxxhdpi/ic_launcher.png diff --git a/src/main/resources/custom-branding/mipmap-xxxhdpi/ic_launcher_round.png b/patches/src/main/resources/custom-branding/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from src/main/resources/custom-branding/mipmap-xxxhdpi/ic_launcher_round.png rename to patches/src/main/resources/custom-branding/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/src/main/resources/downloads/drawable/revanced_yt_download_button.xml b/patches/src/main/resources/downloads/drawable/revanced_yt_download_button.xml similarity index 100% rename from src/main/resources/downloads/drawable/revanced_yt_download_button.xml rename to patches/src/main/resources/downloads/drawable/revanced_yt_download_button.xml diff --git a/src/main/resources/downloads/host/layout/youtube_controls_bottom_ui_container.xml b/patches/src/main/resources/downloads/host/layout/youtube_controls_bottom_ui_container.xml similarity index 100% rename from src/main/resources/downloads/host/layout/youtube_controls_bottom_ui_container.xml rename to patches/src/main/resources/downloads/host/layout/youtube_controls_bottom_ui_container.xml diff --git a/src/main/resources/settings/host/values/styles.xml b/patches/src/main/resources/settings/host/values/styles.xml similarity index 100% rename from src/main/resources/settings/host/values/styles.xml rename to patches/src/main/resources/settings/host/values/styles.xml diff --git a/src/main/resources/settings/layout/revanced_settings_with_toolbar.xml b/patches/src/main/resources/settings/layout/revanced_settings_with_toolbar.xml similarity index 100% rename from src/main/resources/settings/layout/revanced_settings_with_toolbar.xml rename to patches/src/main/resources/settings/layout/revanced_settings_with_toolbar.xml diff --git a/src/main/resources/settings/xml/revanced_prefs.xml b/patches/src/main/resources/settings/xml/revanced_prefs.xml similarity index 100% rename from src/main/resources/settings/xml/revanced_prefs.xml rename to patches/src/main/resources/settings/xml/revanced_prefs.xml diff --git a/src/main/resources/speedbutton/drawable/revanced_playback_speed_dialog_button.xml b/patches/src/main/resources/speedbutton/drawable/revanced_playback_speed_dialog_button.xml similarity index 100% rename from src/main/resources/speedbutton/drawable/revanced_playback_speed_dialog_button.xml rename to patches/src/main/resources/speedbutton/drawable/revanced_playback_speed_dialog_button.xml diff --git a/src/main/resources/speedbutton/host/layout/youtube_controls_bottom_ui_container.xml b/patches/src/main/resources/speedbutton/host/layout/youtube_controls_bottom_ui_container.xml similarity index 100% rename from src/main/resources/speedbutton/host/layout/youtube_controls_bottom_ui_container.xml rename to patches/src/main/resources/speedbutton/host/layout/youtube_controls_bottom_ui_container.xml diff --git a/src/main/resources/sponsorblock/drawable-xxxhdpi/quantum_ic_skip_next_white_24.png b/patches/src/main/resources/sponsorblock/drawable-xxxhdpi/quantum_ic_skip_next_white_24.png similarity index 100% rename from src/main/resources/sponsorblock/drawable-xxxhdpi/quantum_ic_skip_next_white_24.png rename to patches/src/main/resources/sponsorblock/drawable-xxxhdpi/quantum_ic_skip_next_white_24.png diff --git a/src/main/resources/sponsorblock/drawable/revanced_sb_adjust.xml b/patches/src/main/resources/sponsorblock/drawable/revanced_sb_adjust.xml similarity index 100% rename from src/main/resources/sponsorblock/drawable/revanced_sb_adjust.xml rename to patches/src/main/resources/sponsorblock/drawable/revanced_sb_adjust.xml diff --git a/src/main/resources/sponsorblock/drawable/revanced_sb_backward.xml b/patches/src/main/resources/sponsorblock/drawable/revanced_sb_backward.xml similarity index 100% rename from src/main/resources/sponsorblock/drawable/revanced_sb_backward.xml rename to patches/src/main/resources/sponsorblock/drawable/revanced_sb_backward.xml diff --git a/src/main/resources/sponsorblock/drawable/revanced_sb_compare.xml b/patches/src/main/resources/sponsorblock/drawable/revanced_sb_compare.xml similarity index 100% rename from src/main/resources/sponsorblock/drawable/revanced_sb_compare.xml rename to patches/src/main/resources/sponsorblock/drawable/revanced_sb_compare.xml diff --git a/src/main/resources/sponsorblock/drawable/revanced_sb_edit.xml b/patches/src/main/resources/sponsorblock/drawable/revanced_sb_edit.xml similarity index 100% rename from src/main/resources/sponsorblock/drawable/revanced_sb_edit.xml rename to patches/src/main/resources/sponsorblock/drawable/revanced_sb_edit.xml diff --git a/src/main/resources/sponsorblock/drawable/revanced_sb_forward.xml b/patches/src/main/resources/sponsorblock/drawable/revanced_sb_forward.xml similarity index 100% rename from src/main/resources/sponsorblock/drawable/revanced_sb_forward.xml rename to patches/src/main/resources/sponsorblock/drawable/revanced_sb_forward.xml diff --git a/src/main/resources/sponsorblock/drawable/revanced_sb_logo.xml b/patches/src/main/resources/sponsorblock/drawable/revanced_sb_logo.xml similarity index 100% rename from src/main/resources/sponsorblock/drawable/revanced_sb_logo.xml rename to patches/src/main/resources/sponsorblock/drawable/revanced_sb_logo.xml diff --git a/src/main/resources/sponsorblock/drawable/revanced_sb_publish.xml b/patches/src/main/resources/sponsorblock/drawable/revanced_sb_publish.xml similarity index 100% rename from src/main/resources/sponsorblock/drawable/revanced_sb_publish.xml rename to patches/src/main/resources/sponsorblock/drawable/revanced_sb_publish.xml diff --git a/src/main/resources/sponsorblock/drawable/revanced_sb_voting.xml b/patches/src/main/resources/sponsorblock/drawable/revanced_sb_voting.xml similarity index 100% rename from src/main/resources/sponsorblock/drawable/revanced_sb_voting.xml rename to patches/src/main/resources/sponsorblock/drawable/revanced_sb_voting.xml diff --git a/src/main/resources/sponsorblock/host/layout/youtube_controls_layout.xml b/patches/src/main/resources/sponsorblock/host/layout/youtube_controls_layout.xml similarity index 100% rename from src/main/resources/sponsorblock/host/layout/youtube_controls_layout.xml rename to patches/src/main/resources/sponsorblock/host/layout/youtube_controls_layout.xml diff --git a/src/main/resources/sponsorblock/layout/revanced_sb_inline_sponsor_overlay.xml b/patches/src/main/resources/sponsorblock/layout/revanced_sb_inline_sponsor_overlay.xml similarity index 86% rename from src/main/resources/sponsorblock/layout/revanced_sb_inline_sponsor_overlay.xml rename to patches/src/main/resources/sponsorblock/layout/revanced_sb_inline_sponsor_overlay.xml index fb57b7252..8d4f7b41a 100644 --- a/src/main/resources/sponsorblock/layout/revanced_sb_inline_sponsor_overlay.xml +++ b/patches/src/main/resources/sponsorblock/layout/revanced_sb_inline_sponsor_overlay.xml @@ -1,7 +1,7 @@ - - - - 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