Compare commits

..

7 Commits

Author SHA1 Message Date
semantic-release-bot
b2a7a65bc0 chore(release): 4.4.0-dev.4 [skip ci]
# [4.4.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.4.0-dev.3...v4.4.0-dev.4) (2024-03-11)

### Features

* **YouTube :** Remove `HDR auto brightness` patch ([#2863](https://github.com/ReVanced/revanced-patches/issues/2863)) ([32139f1](32139f151b))
2024-03-11 09:35:36 +00:00
LisoUseInAIKyrios
32139f151b feat(YouTube ): Remove HDR auto brightness patch (#2863)
Co-authored-by: oSumAtrIX <johan.melkonyan1@web.de>
2024-03-11 13:33:30 +04:00
semantic-release-bot
e55364ca87 chore(release): 4.4.0-dev.3 [skip ci]
# [4.4.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.4.0-dev.2...v4.4.0-dev.3) (2024-03-08)

### Bug Fixes

* **YouTube - Downloads:** Use new task context ([#2841](https://github.com/ReVanced/revanced-patches/issues/2841)) ([5cca421](5cca42121a))
2024-03-08 05:11:28 +00:00
LisoUseInAIKyrios
5cca42121a fix(YouTube - Downloads): Use new task context (#2841) 2024-03-08 09:09:15 +04:00
semantic-release-bot
7b0365360e chore(release): 4.4.0-dev.2 [skip ci]
# [4.4.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.4.0-dev.1...v4.4.0-dev.2) (2024-03-04)

### Features

* **YouTube - External downloader:** Add ability to use in-app download button ([7f8b7c0](7f8b7c08b2))
2024-03-04 19:38:46 +00:00
oSumAtrIX
7f8b7c08b2 feat(YouTube - External downloader): Add ability to use in-app download button 2024-03-04 20:36:25 +01:00
oSumAtrIX
058acff28c build: Set target bytecode level to JVM 11 2024-03-04 18:10:12 +01:00
19 changed files with 233 additions and 97 deletions

View File

@@ -1,3 +1,24 @@
# [4.4.0-dev.4](https://github.com/ReVanced/revanced-patches/compare/v4.4.0-dev.3...v4.4.0-dev.4) (2024-03-11)
### Features
* **YouTube :** Remove `HDR auto brightness` patch ([#2863](https://github.com/ReVanced/revanced-patches/issues/2863)) ([b4c7bf4](https://github.com/ReVanced/revanced-patches/commit/b4c7bf48084184e5f031f7f5139a9a29341d8ebf))
# [4.4.0-dev.3](https://github.com/ReVanced/revanced-patches/compare/v4.4.0-dev.2...v4.4.0-dev.3) (2024-03-08)
### Bug Fixes
* **YouTube - Downloads:** Use new task context ([#2841](https://github.com/ReVanced/revanced-patches/issues/2841)) ([6d88cb4](https://github.com/ReVanced/revanced-patches/commit/6d88cb49ec739700866290babcba5fb3032dbced))
# [4.4.0-dev.2](https://github.com/ReVanced/revanced-patches/compare/v4.4.0-dev.1...v4.4.0-dev.2) (2024-03-04)
### Features
* **YouTube - External downloader:** Add ability to use in-app download button ([d900011](https://github.com/ReVanced/revanced-patches/commit/d9000113a905c14f8409aa75008f1ef6a1aecd0c))
# [4.4.0-dev.1](https://github.com/ReVanced/revanced-patches/compare/v4.3.0...v4.4.0-dev.1) (2024-03-04)

View File

@@ -1194,8 +1194,8 @@ public final class app/revanced/patches/youtube/interaction/dialog/RemoveViewerD
public synthetic fun execute (Lapp/revanced/patcher/data/Context;)V
}
public final class app/revanced/patches/youtube/interaction/downloads/ExternalDownloadsBytecodePatch : app/revanced/patcher/patch/BytecodePatch {
public static final field INSTANCE Lapp/revanced/patches/youtube/interaction/downloads/ExternalDownloadsBytecodePatch;
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
}
@@ -1748,6 +1748,7 @@ public final class app/revanced/util/BytecodeUtilsKt {
public static final fun indexOfFirstInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;Lkotlin/jvm/functions/Function1;)I
public static final fun indexOfFirstWideLiteralInstructionValue (Lcom/android/tools/smali/dexlib2/iface/Method;J)I
public static final fun injectHideViewCall (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;IILjava/lang/String;Ljava/lang/String;)V
public static final fun resultOrThrow (Lapp/revanced/patcher/fingerprint/MethodFingerprint;)Lapp/revanced/patcher/fingerprint/MethodFingerprintResult;
public static final fun returnEarly (Ljava/util/List;Z)V
public static synthetic fun returnEarly$default (Ljava/util/List;ZILjava/lang/Object;)V
public static final fun transformMethods (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableClass;Lkotlin/jvm/functions/Function1;)V

View File

@@ -1,4 +1,5 @@
import org.gradle.kotlin.dsl.support.listFilesOrdered
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlin)
@@ -33,7 +34,13 @@ dependencies {
}
kotlin {
jvmToolchain(11)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
java {
targetCompatibility = JavaVersion.VERSION_11
}
tasks {

View File

@@ -1,4 +1,4 @@
org.gradle.parallel = true
org.gradle.caching = true
kotlin.code.style = official
version = 4.4.0-dev.1
version = 4.4.0-dev.4

View File

@@ -64,8 +64,8 @@ abstract class BaseIntegrationsPatch(
method.addInstruction(
0,
"sput-object v$contextRegister, " +
"$integrationsDescriptor->context:Landroid/content/Context;",
"invoke-static/range { v$contextRegister .. v$contextRegister }, " +
"$integrationsDescriptor->setContext(Landroid/content/Context;)V",
)
} ?: throw PatchException("Could not find hook target fingerprint.")
}

View File

@@ -39,7 +39,6 @@ object CopyVideoUrlBytecodePatch : BytecodePatch(emptySet()) {
)
override fun execute(context: BytecodeContext) {
// Initialize buttons and inject visibility control
BUTTONS_DESCRIPTORS.forEach { descriptor ->
PlayerControlsBytecodePatch.initializeControl("$descriptor->initializeButton(Landroid/view/View;)V")
PlayerControlsBytecodePatch.injectVisibilityCheckCall("$descriptor->changeVisibility(Z)V")

View File

@@ -0,0 +1,98 @@
package app.revanced.patches.youtube.interaction.downloads
import app.revanced.patcher.data.BytecodeContext
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.annotation.CompatiblePackage
import app.revanced.patcher.patch.annotation.Patch
import app.revanced.patches.youtube.interaction.downloads.fingerprints.DownloadActionCommandResolverFingerprint
import app.revanced.patches.youtube.interaction.downloads.fingerprints.DownloadActionCommandResolverParentFingerprint
import app.revanced.patches.youtube.interaction.downloads.fingerprints.LegacyDownloadCommandResolverFingerprint
import app.revanced.patches.youtube.misc.playercontrols.PlayerControlsBytecodePatch
import app.revanced.patches.youtube.shared.fingerprints.MainActivityFingerprint
import app.revanced.patches.youtube.video.information.VideoInformationPatch
import app.revanced.util.resultOrThrow
@Patch(
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.",
dependencies = [
DownloadsResourcePatch::class,
PlayerControlsBytecodePatch::class,
VideoInformationPatch::class,
],
compatiblePackages = [
CompatiblePackage(
"com.google.android.youtube",
[
"18.48.39",
"18.49.37",
"19.01.34",
"19.02.39",
"19.03.35",
"19.03.36",
"19.04.37",
],
),
],
)
@Suppress("unused")
object DownloadsPatch : BytecodePatch(
setOf(
DownloadActionCommandResolverParentFingerprint,
LegacyDownloadCommandResolverFingerprint,
MainActivityFingerprint
)
) {
private const val INTEGRATIONS_CLASS_DESCRIPTOR = "Lapp/revanced/integrations/youtube/patches/DownloadsPatch;"
private const val BUTTON_DESCRIPTOR = "Lapp/revanced/integrations/youtube/videoplayer/ExternalDownloadButton;"
override fun execute(context: BytecodeContext) {
PlayerControlsBytecodePatch.initializeControl("$BUTTON_DESCRIPTOR->initializeButton(Landroid/view/View;)V")
PlayerControlsBytecodePatch.injectVisibilityCheckCall("$BUTTON_DESCRIPTOR->changeVisibility(Z)V")
// Main activity is used to launch downloader intent.
MainActivityFingerprint.resultOrThrow().mutableMethod.apply {
addInstruction(
implementation!!.instructions.lastIndex,
"invoke-static { p0 }, $INTEGRATIONS_CLASS_DESCRIPTOR->activityCreated(Landroid/app/Activity;)V"
)
}
val commonInstructions = """
move-result v0
if-eqz v0, :show_native_downloader
return-void
:show_native_downloader
nop
"""
DownloadActionCommandResolverFingerprint.resolve(context,
DownloadActionCommandResolverParentFingerprint.resultOrThrow().classDef)
DownloadActionCommandResolverFingerprint.resultOrThrow().mutableMethod.apply {
addInstructionsWithLabels(
0,
"""
invoke-static {}, $INTEGRATIONS_CLASS_DESCRIPTOR->inAppDownloadButtonOnClick()Z
$commonInstructions
"""
)
}
// Legacy fingerprint is used for old spoofed versions,
// or if download playlist is pressed on any version.
// Downloading playlists is not yet supported,
// as the code this hooks does not easily expost the playlist id.
LegacyDownloadCommandResolverFingerprint.resultOrThrow().mutableMethod.apply {
addInstructionsWithLabels(
0,
"""
invoke-static/range {p1 .. p1}, $INTEGRATIONS_CLASS_DESCRIPTOR->inAppDownloadPlaylistLegacyOnClick(Ljava/lang/String;)Z
$commonInstructions
"""
)
}
}
}

View File

@@ -21,7 +21,7 @@ import app.revanced.util.copyResources
AddResourcesPatch::class,
],
)
internal object ExternalDownloadsResourcePatch : ResourcePatch() {
internal object DownloadsResourcePatch : ResourcePatch() {
override fun execute(context: ResourceContext) {
AddResourcesPatch(this::class)
@@ -31,6 +31,7 @@ internal object ExternalDownloadsResourcePatch : ResourcePatch() {
sorting = Sorting.UNSORTED,
preferences = setOf(
SwitchPreference("revanced_external_downloader"),
SwitchPreference("revanced_external_downloader_action_button"),
TextPreference("revanced_external_downloader_name", inputType = InputType.TEXT),
),
),

View File

@@ -1,54 +0,0 @@
package app.revanced.patches.youtube.interaction.downloads
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.youtube.misc.playercontrols.PlayerControlsBytecodePatch
import app.revanced.patches.youtube.video.information.VideoInformationPatch
@Patch(
name = "External downloads",
description = "Adds support to download and save YouTube videos using an external downloader app.",
dependencies = [
ExternalDownloadsResourcePatch::class,
PlayerControlsBytecodePatch::class,
VideoInformationPatch::class,
],
compatiblePackages = [
CompatiblePackage(
"com.google.android.youtube",
[
"18.48.39",
"18.49.37",
"19.01.34",
"19.02.39",
"19.03.35",
"19.03.36",
"19.04.37",
],
),
],
)
@Suppress("unused")
object ExternalDownloadsBytecodePatch : BytecodePatch(emptySet()) {
private const val BUTTON_DESCRIPTOR = "Lapp/revanced/integrations/youtube/videoplayer/ExternalDownloadButton;"
override fun execute(context: BytecodeContext) {
/*
initialize the control
*/
PlayerControlsBytecodePatch.initializeControl(
"$BUTTON_DESCRIPTOR->initializeButton(Landroid/view/View;)V",
)
/*
add code to change the visibility of the control
*/
PlayerControlsBytecodePatch.injectVisibilityCheckCall(
"$BUTTON_DESCRIPTOR->changeVisibility(Z)V",
)
}
}

View File

@@ -0,0 +1,14 @@
package app.revanced.patches.youtube.interaction.downloads.fingerprints
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.AccessFlags
/**
* Resolves to the class found in [DownloadActionCommandResolverParentFingerprint].
*/
internal object DownloadActionCommandResolverFingerprint : MethodFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
returnType = "V",
parameters = listOf("L", "Ljava/util/Map;")
)

View File

@@ -0,0 +1,17 @@
package app.revanced.patches.youtube.interaction.downloads.fingerprints
import app.revanced.patcher.extensions.or
import app.revanced.util.patch.LiteralValueFingerprint
import com.android.tools.smali.dexlib2.AccessFlags
internal object DownloadActionCommandResolverParentFingerprint : LiteralValueFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
returnType = "V",
parameters = listOf("L", "L"),
strings = listOf(
// Strings are not unique and found in other methods.
"com.google.android.libraries.youtube.logging.interaction_logger",
"Unknown command"
),
literalSupplier = { 16 }
)

View File

@@ -0,0 +1,24 @@
package app.revanced.patches.youtube.interaction.downloads.fingerprints
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
/**
* For spoofing to older versions. Also called if download playlist is pressed for any version.
*/
internal object LegacyDownloadCommandResolverFingerprint : MethodFingerprint(
accessFlags = AccessFlags.PRIVATE or AccessFlags.FINAL,
returnType = "V",
parameters = listOf("Ljava/lang/String;", "Ljava/lang/String;", "L", "L"),
strings = listOf(""),
opcodes = listOf(
Opcode.MOVE_OBJECT_FROM16,
Opcode.MOVE_OBJECT_FROM16,
Opcode.NEW_INSTANCE,
Opcode.INVOKE_DIRECT,
Opcode.IGET_OBJECT,
Opcode.IF_NEZ,
)
)

View File

@@ -32,6 +32,7 @@ import app.revanced.patches.youtube.video.videoid.VideoIdPatch
import app.revanced.util.exception
import app.revanced.util.getReference
import app.revanced.util.indexOfFirstInstruction
import app.revanced.util.resultOrThrow
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.FiveRegisterInstruction
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
@@ -88,8 +89,6 @@ object ReturnYouTubeDislikePatch : BytecodePatch(
private const val FILTER_CLASS_DESCRIPTOR =
"Lapp/revanced/integrations/youtube/patches/components/ReturnYouTubeDislikeFilterPatch;"
private fun MethodFingerprint.resultOrThrow() = result ?: throw exception
override fun execute(context: BytecodeContext) {
// region Inject newVideoLoaded event handler to update dislikes when a new video is loaded.

View File

@@ -23,7 +23,7 @@ import app.revanced.patches.youtube.layout.thumbnails.fingerprints.cronet.reques
import app.revanced.patches.youtube.layout.thumbnails.fingerprints.cronet.request.callback.OnSucceededFingerprint
import app.revanced.patches.youtube.misc.integrations.IntegrationsPatch
import app.revanced.patches.youtube.misc.settings.SettingsPatch
import app.revanced.util.exception
import app.revanced.util.resultOrThrow
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.builder.MutableMethodImplementation
@@ -144,11 +144,8 @@ object AlternativeThumbnailsPatch : BytecodePatch(
NonInteractivePreference("revanced_alt_thumbnail_stills_about"),
)
fun MethodFingerprint.getResultOrThrow() =
result ?: throw exception
fun MethodFingerprint.alsoResolve(fingerprint: MethodFingerprint) =
also { resolve(context, fingerprint.getResultOrThrow().classDef) }.getResultOrThrow()
also { resolve(context, fingerprint.resultOrThrow().classDef) }.resultOrThrow()
fun MethodFingerprint.resolveAndLetMutableMethod(
fingerprint: MethodFingerprint,
@@ -172,7 +169,7 @@ object AlternativeThumbnailsPatch : BytecodePatch(
// The URL is required for the failure callback hook, but the URL field is obfuscated.
// Add a helper get method that returns the URL field.
RequestFingerprint.getResultOrThrow().apply {
RequestFingerprint.resultOrThrow().apply {
// The url is the only string field that is set inside the constructor.
val urlFieldInstruction = mutableMethod.getInstructions().first {
if (it.opcode != Opcode.IPUT_OBJECT) return@first false

View File

@@ -3,30 +3,31 @@ package app.revanced.patches.youtube.misc.playeroverlay
import app.revanced.patcher.data.BytecodeContext
import app.revanced.patcher.extensions.InstructionExtensions.addInstruction
import app.revanced.patcher.patch.BytecodePatch
import app.revanced.patcher.patch.annotation.CompatiblePackage
import app.revanced.patcher.patch.annotation.Patch
import app.revanced.patches.youtube.misc.integrations.IntegrationsPatch
import app.revanced.patches.youtube.misc.playeroverlay.fingerprint.PlayerOverlaysOnFinishInflateFingerprint
import app.revanced.util.exception
@Patch(
description = "Hook for adding custom overlays to the video player.",
description = "Hook for the video player overlay",
dependencies = [IntegrationsPatch::class],
compatiblePackages = [
CompatiblePackage("com.google.android.youtube", [
"18.32.39"
])
]
)
/**
* Edit: This patch is not in use and may not work.
*/
@Suppress("unused")
object PlayerOverlaysHookPatch : BytecodePatch( // TODO: delete this unused outdated patch and its integration code.
object PlayerOverlaysHookPatch : BytecodePatch(
setOf(PlayerOverlaysOnFinishInflateFingerprint)
) {
private const val INTEGRATIONS_CLASS_DESCRIPTOR = "Lapp/revanced/integrations/youtube/patches/PlayerOverlaysHookPatch;"
override fun execute(context: BytecodeContext) {
// hook YouTubePlayerOverlaysLayout.onFinishInflate()
val method = PlayerOverlaysOnFinishInflateFingerprint.result!!.mutableMethod
method.addInstruction(
method.implementation!!.instructions.size - 2,
"invoke-static { p0 }, Lapp/revanced/integrations/youtube/patches/PlayerOverlaysHookPatch;->YouTubePlayerOverlaysLayout_onFinishInflateHook(Ljava/lang/Object;)V"
)
PlayerOverlaysOnFinishInflateFingerprint.result?.mutableMethod?.apply {
addInstruction(
implementation!!.instructions.lastIndex,
"invoke-static { p0 }, $INTEGRATIONS_CLASS_DESCRIPTOR->playerOverlayInflated(Landroid/view/ViewGroup;)V"
)
} ?: throw PlayerOverlaysOnFinishInflateFingerprint.exception
}
}

View File

@@ -1,10 +1,14 @@
package app.revanced.patches.youtube.misc.playeroverlay.fingerprint
import app.revanced.patcher.extensions.or
import app.revanced.patcher.fingerprint.MethodFingerprint
import com.android.tools.smali.dexlib2.AccessFlags
internal object PlayerOverlaysOnFinishInflateFingerprint : MethodFingerprint(
accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL,
returnType = "V",
customFingerprint = { methodDef, _ ->
methodDef.definingClass.endsWith("YouTubePlayerOverlaysLayout;") && methodDef.name == "onFinishInflate"
}
methodDef.definingClass.endsWith("/YouTubePlayerOverlaysLayout;")
&& methodDef.name == "onFinishInflate"
},
)

View File

@@ -14,13 +14,14 @@ 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
@Deprecated("Patch is obsolete and the hooked code is no longer present in 19.09+")
@Patch(
name = "HDR auto brightness",
description = "Adds an option to make the brightness of HDR videos follow the system default.",
dependencies = [IntegrationsPatch::class, SettingsPatch::class, AddResourcesPatch::class],
compatiblePackages = [
CompatiblePackage(
"com.google.android.youtube", [
"com.google.android.youtube",
[
"18.32.39",
"18.37.36",
"18.38.44",
@@ -33,20 +34,20 @@ import com.android.tools.smali.dexlib2.iface.reference.FieldReference
"19.02.39",
"19.03.35",
"19.03.36",
"19.04.37"
]
)
]
"19.04.37",
],
),
],
)
@Suppress("unused")
object HDRBrightnessPatch : BytecodePatch(
setOf(HDRBrightnessFingerprint)
setOf(HDRBrightnessFingerprint),
) {
override fun execute(context: BytecodeContext) {
AddResourcesPatch(this::class)
SettingsPatch.PreferenceScreen.VIDEO.addPreferences(
SwitchPreference("revanced_hdr_auto_brightness")
SwitchPreference("revanced_hdr_auto_brightness"),
)
val method = HDRBrightnessFingerprint.result!!.mutableMethod
@@ -64,7 +65,7 @@ object HDRBrightnessPatch : BytecodePatch(
"""
invoke-static {v$register}, Lapp/revanced/integrations/youtube/patches/HDRAutoBrightnessPatch;->getHDRBrightness(F)F
move-result v$register
"""
""",
)
}
}

View File

@@ -15,6 +15,9 @@ 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].
*

View File

@@ -226,12 +226,15 @@
<string name="revanced_remove_viewer_discretion_dialog_summary_off">Dialog will be shown</string>
<string name="revanced_remove_viewer_discretion_dialog_user_dialog_message">This does not bypass the age restriction. It just accepts it automatically.</string>
</patch>
<patch id="interaction.downloads.ExternalDownloadsResourcePatch">
<patch id="interaction.downloads.DownloadsResourcePatch">
<string name="revanced_external_downloader_screen_title">External downloads</string>
<string name="revanced_external_downloader_screen_summary">Settings for using an external downloader</string>
<string name="revanced_external_downloader_title">Show external download button</string>
<string name="revanced_external_downloader_summary_on">Download button shown in player</string>
<string name="revanced_external_downloader_summary_off">Download button not shown in player</string>
<string name="revanced_external_downloader_action_button_title">Override download action button</string>
<string name="revanced_external_downloader_action_button_summary_on">Download button opens your external downloader</string>
<string name="revanced_external_downloader_action_button_summary_off">Download button opens the native in-app downloader</string>
<string name="revanced_external_downloader_name_title">Downloader package name</string>
<string name="revanced_external_downloader_name_summary">Package name of your installed external downloader app, such as NewPipe or Seal</string>
<string name="revanced_external_downloader_not_installed_warning">%s is not installed. Please install it.</string>